Skip to content

Commit 0e5ede9

Browse files
committed
feat(bug-detectors): share stack suppressions
Match suppressions against the stack text users see so detectors can share the same low-surprise ignore rules.
1 parent 3e5f2b5 commit 0e5ede9

3 files changed

Lines changed: 211 additions & 0 deletions

File tree

.npmignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ coverage
1313
node_modules
1414
tests
1515
shared
16+
!**/dist/shared/
17+
!**/dist/shared/**
1618

1719
# Exclude docs, those can be accessed online
1820
docs
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright 2026 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {
18+
buildGenericSuppressionSnippet,
19+
getUserFacingStack,
20+
IgnoreList,
21+
matchesIgnoreRules,
22+
} from "./finding-suppression";
23+
24+
describe("finding suppression", () => {
25+
const stack = [
26+
"Error",
27+
" at renderTemplate (C:\\repo\\tests\\bug-detectors\\sample.test.js:10:42)",
28+
" at handleRequest (/repo/src/server.js:20:7)",
29+
" at internal (/repo/jazzer.js/packages/core/core.js:30:1)",
30+
].join("\n");
31+
32+
test("keeps the shown stack text unchanged for matching", () => {
33+
expect(getUserFacingStack(stack)).toBe(
34+
[
35+
" at renderTemplate (C:\\repo\\tests\\bug-detectors\\sample.test.js:10:42)",
36+
" at handleRequest (/repo/src/server.js:20:7)",
37+
].join("\n"),
38+
);
39+
});
40+
41+
test("matches string stack patterns against the shown stack", () => {
42+
expect(
43+
matchesIgnoreRules(
44+
[
45+
{
46+
stackPattern:
47+
"C:\\repo\\tests\\bug-detectors\\sample.test.js:10:42",
48+
},
49+
],
50+
stack,
51+
),
52+
).toBe(true);
53+
expect(
54+
matchesIgnoreRules(
55+
[
56+
{
57+
stackPattern: "C:/repo/tests/bug-detectors/sample.test.js:10:42",
58+
},
59+
],
60+
stack,
61+
),
62+
).toBe(false);
63+
});
64+
65+
test("matches regex stack patterns against the shown stack", () => {
66+
expect(
67+
matchesIgnoreRules(
68+
[{ stackPattern: /handleRequest \(\/repo\/src\/server\.js:20:7\)/ }],
69+
stack,
70+
),
71+
).toBe(true);
72+
});
73+
74+
test("IgnoreList stores and matches suppression rules", () => {
75+
const ignoreList = new IgnoreList();
76+
77+
ignoreList.add({ stackPattern: "sample.test.js:10:42" });
78+
79+
expect(ignoreList.matches(stack)).toBe(true);
80+
});
81+
82+
test("prints generic example snippets with optional chaining", () => {
83+
expect(
84+
buildGenericSuppressionSnippet("code-injection", "ignoreInvocation"),
85+
).toContain('getBugDetectorConfiguration("code-injection")');
86+
expect(
87+
buildGenericSuppressionSnippet("code-injection", "ignoreInvocation"),
88+
).toContain("?.ignoreInvocation({");
89+
expect(
90+
buildGenericSuppressionSnippet("code-injection", "ignoreInvocation"),
91+
).toContain('stackPattern: "test.js:10"');
92+
expect(
93+
buildGenericSuppressionSnippet("code-injection", "ignoreInvocation"),
94+
).toContain("shown stack above");
95+
});
96+
});
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright 2026 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
const JAZZER_INTERNAL_STACK_MARKERS = [
18+
"/@jazzer.js/",
19+
"/jazzer.js/packages/",
20+
"/jazzer.js/core/",
21+
"/jazzer.js-commercial/packages/",
22+
"/jazzer.js-commercial/core/",
23+
"../../packages/",
24+
];
25+
26+
/**
27+
* Defines a suppression rule for bug-detector findings.
28+
*/
29+
export interface IgnoreRule {
30+
/**
31+
* A string or regular expression matching the stack excerpt shown in the
32+
* finding after removing the leading Error line and Jazzer.js frames.
33+
* @example "src/templates.js:41"
34+
* @example /renderTemplate.*handleRequest/s
35+
*/
36+
stackPattern?: string | RegExp;
37+
}
38+
39+
export class IgnoreList {
40+
private readonly _rules: IgnoreRule[] = [];
41+
42+
add(rule: IgnoreRule): void {
43+
this._rules.push(rule);
44+
}
45+
46+
matches(stack: string): boolean {
47+
return matchesIgnoreRules(this._rules, stack);
48+
}
49+
}
50+
51+
export function matchesIgnoreRules(
52+
rules: IgnoreRule[],
53+
stack: string,
54+
): boolean {
55+
return rules.some((rule) => matchesIgnoreRule(rule, stack));
56+
}
57+
58+
export function buildGenericSuppressionSnippet(
59+
detectorName: string,
60+
suppressionMethod: string,
61+
): string {
62+
return [
63+
'const { getBugDetectorConfiguration } = require("@jazzer.js/bug-detectors");',
64+
"",
65+
"// Example only: adapt `stackPattern` to the shown stack above.",
66+
`getBugDetectorConfiguration("${detectorName}")`,
67+
` ?.${suppressionMethod}({`,
68+
' stackPattern: "test.js:10",',
69+
" });",
70+
].join("\n");
71+
}
72+
73+
export function captureStack(): string {
74+
return new Error().stack ?? "";
75+
}
76+
77+
export function getUserFacingStack(stack: string): string {
78+
return getUserFacingStackLines(stack).join("\n");
79+
}
80+
81+
export function getUserFacingStackLines(stack: string): string[] {
82+
return stack
83+
.split("\n")
84+
.slice(1)
85+
.filter((line) => line !== "")
86+
.filter((line) => !isJazzerInternalStackLine(line));
87+
}
88+
89+
function matchesIgnoreRule(rule: IgnoreRule, stack: string): boolean {
90+
return Boolean(
91+
rule.stackPattern &&
92+
matchesStackPattern(rule.stackPattern, getUserFacingStack(stack)),
93+
);
94+
}
95+
96+
function isJazzerInternalStackLine(line: string): boolean {
97+
const normalizedLine = line.replace(/\\/g, "/");
98+
return JAZZER_INTERNAL_STACK_MARKERS.some((marker) =>
99+
normalizedLine.includes(marker),
100+
);
101+
}
102+
103+
function matchesPattern(pattern: RegExp, value: string): boolean {
104+
pattern.lastIndex = 0;
105+
return pattern.test(value);
106+
}
107+
108+
function matchesStackPattern(pattern: string | RegExp, value: string): boolean {
109+
if (typeof pattern === "string") {
110+
return value.includes(pattern);
111+
}
112+
return matchesPattern(pattern, value);
113+
}

0 commit comments

Comments
 (0)