From 2f3bb8e2304f1540d36e8d16bb4d6424a61a01bc Mon Sep 17 00:00:00 2001 From: David Michon Date: Tue, 11 Nov 2025 23:00:13 +0000 Subject: [PATCH 1/5] [eslint-bulk] Report bulk suppressions as suppressions --- .../eslint-patch/fix-sarif-logs_2025-11-11-23-06.json | 10 ++++++++++ .../eslint-bulk-suppressions/generate-patched-file.ts | 7 +++---- 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 common/changes/@rushstack/eslint-patch/fix-sarif-logs_2025-11-11-23-06.json diff --git a/common/changes/@rushstack/eslint-patch/fix-sarif-logs_2025-11-11-23-06.json b/common/changes/@rushstack/eslint-patch/fix-sarif-logs_2025-11-11-23-06.json new file mode 100644 index 00000000000..99c39a864bf --- /dev/null +++ b/common/changes/@rushstack/eslint-patch/fix-sarif-logs_2025-11-11-23-06.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/eslint-patch", + "comment": "In ESLint >= 9.37, report bulk suppressed messages as suppressed messages rather than removing them completely.", + "type": "minor" + } + ], + "packageName": "@rushstack/eslint-patch" +} \ No newline at end of file diff --git a/eslint/eslint-patch/src/eslint-bulk-suppressions/generate-patched-file.ts b/eslint/eslint-patch/src/eslint-bulk-suppressions/generate-patched-file.ts index ac788b840c8..5a0107537a3 100644 --- a/eslint/eslint-patch/src/eslint-bulk-suppressions/generate-patched-file.ts +++ b/eslint/eslint-patch/src/eslint-bulk-suppressions/generate-patched-file.ts @@ -262,7 +262,7 @@ const requireFromPathToLinterJS = bulkSuppressionsPatch.requireFromPathToLinterJ // ); // } // ``` - if (majorVersion >= 9 && minorVersion >= 37) { + if (majorVersion > 9 || (majorVersion === 9 && minorVersion >= 37)) { outputFile += scanUntilMarker('const problem = report.addRuleMessage('); outputFile += scanUntilMarker('ruleId,'); outputFile += scanUntilMarker('severity,'); @@ -274,11 +274,10 @@ const requireFromPathToLinterJS = bulkSuppressionsPatch.requireFromPathToLinterJ outputFile += ` // --- BEGIN MONKEY PATCH ---`; - if (majorVersion >= 9 && minorVersion >= 37) { + if (majorVersion > 9 || (majorVersion === 9 && minorVersion >= 37)) { outputFile += ` if (bulkSuppressionsPatch.shouldBulkSuppress({ filename, currentNode: args[0]?.node ?? currentNode, ruleId, problem })) { - report.messages.splice(report.messages.indexOf(problem), 1); - return; + problem.suppressions ??= []; problem.suppressions.push({kind:"bulk",justification:""}); }`; } else { outputFile += ` From 4af13ae59ca136989b0dd44ff590172183cc246d Mon Sep 17 00:00:00 2001 From: David Michon Date: Tue, 11 Nov 2025 23:00:29 +0000 Subject: [PATCH 2/5] [heft-lint] Include suppressed results --- .../fix-sarif-logs_2025-11-11-23-06.json | 10 ++++++++++ heft-plugins/heft-lint-plugin/src/Eslint.ts | 14 +------------- 2 files changed, 11 insertions(+), 13 deletions(-) create mode 100644 common/changes/@rushstack/heft-lint-plugin/fix-sarif-logs_2025-11-11-23-06.json diff --git a/common/changes/@rushstack/heft-lint-plugin/fix-sarif-logs_2025-11-11-23-06.json b/common/changes/@rushstack/heft-lint-plugin/fix-sarif-logs_2025-11-11-23-06.json new file mode 100644 index 00000000000..a831c368a0c --- /dev/null +++ b/common/changes/@rushstack/heft-lint-plugin/fix-sarif-logs_2025-11-11-23-06.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-lint-plugin", + "comment": "Forward suppressed messages to formatters.", + "type": "patch" + } + ], + "packageName": "@rushstack/heft-lint-plugin" +} \ No newline at end of file diff --git a/heft-plugins/heft-lint-plugin/src/Eslint.ts b/heft-plugins/heft-lint-plugin/src/Eslint.ts index 6cbd4b5727d..b2aab9552bf 100644 --- a/heft-plugins/heft-lint-plugin/src/Eslint.ts +++ b/heft-plugins/heft-lint-plugin/src/Eslint.ts @@ -292,19 +292,7 @@ export class Eslint extends LinterBase 0; }); - const trimmedLintResults: (TEslint.ESLint.LintResult | TEslintLegacy.ESLint.LintResult)[] = []; - for (const lintResult of lintResults) { - if ( - lintResult.messages.length > 0 || - lintResult.warningCount > 0 || - lintResult.errorCount > 0 || - fixMessages.length > 0 - ) { - trimmedLintResults.push(lintResult); - } - } - - return trimmedLintResults; + return lintResults; } protected override async lintingFinishedAsync(lintResults: TEslint.ESLint.LintResult[]): Promise { From 6cc88d6a9684f4c761795ff6987fb15e65d85ee9 Mon Sep 17 00:00:00 2001 From: David Michon Date: Tue, 11 Nov 2025 23:10:38 +0000 Subject: [PATCH 3/5] Enable sarif log for local rigs, add test --- build-tests/eslint-7-11-test/config/heft.json | 16 +++ build-tests/eslint-7-7-test/config/heft.json | 16 +++ .../.eslint-bulk-suppressions.json | 9 ++ build-tests/eslint-9-test/eslint.config.js | 1 + build-tests/eslint-9-test/package.json | 3 +- .../src/__snapshots__/sarif.test.ts.snap | 111 ++++++++++++++++++ build-tests/eslint-9-test/src/index.ts | 5 +- build-tests/eslint-9-test/src/sarif.test.ts | 15 +++ build-tests/eslint-9-test/tsconfig.json | 21 +--- .../profiles/default/config/heft.json | 16 ++- .../profiles/app/config/heft.json | 16 ++- .../profiles/library/config/heft.json | 16 ++- 12 files changed, 220 insertions(+), 25 deletions(-) create mode 100644 build-tests/eslint-7-11-test/config/heft.json create mode 100644 build-tests/eslint-7-7-test/config/heft.json create mode 100644 build-tests/eslint-9-test/.eslint-bulk-suppressions.json create mode 100644 build-tests/eslint-9-test/src/__snapshots__/sarif.test.ts.snap create mode 100644 build-tests/eslint-9-test/src/sarif.test.ts diff --git a/build-tests/eslint-7-11-test/config/heft.json b/build-tests/eslint-7-11-test/config/heft.json new file mode 100644 index 00000000000..e7cc05b1da4 --- /dev/null +++ b/build-tests/eslint-7-11-test/config/heft.json @@ -0,0 +1,16 @@ +{ + "extends": "local-node-rig/profiles/default/config/heft.json", + + "phasesByName": { + "build": { + "tasksByName": { + "lint": { + "taskPlugin": { + // Clear the SARIF emitter + "options": null + } + } + } + } + } +} diff --git a/build-tests/eslint-7-7-test/config/heft.json b/build-tests/eslint-7-7-test/config/heft.json new file mode 100644 index 00000000000..e7cc05b1da4 --- /dev/null +++ b/build-tests/eslint-7-7-test/config/heft.json @@ -0,0 +1,16 @@ +{ + "extends": "local-node-rig/profiles/default/config/heft.json", + + "phasesByName": { + "build": { + "tasksByName": { + "lint": { + "taskPlugin": { + // Clear the SARIF emitter + "options": null + } + } + } + } + } +} diff --git a/build-tests/eslint-9-test/.eslint-bulk-suppressions.json b/build-tests/eslint-9-test/.eslint-bulk-suppressions.json new file mode 100644 index 00000000000..961e6033858 --- /dev/null +++ b/build-tests/eslint-9-test/.eslint-bulk-suppressions.json @@ -0,0 +1,9 @@ +{ + "suppressions": [ + { + "file": "src/index.ts", + "scopeId": ".", + "rule": "@typescript-eslint/naming-convention" + } + ] +} diff --git a/build-tests/eslint-9-test/eslint.config.js b/build-tests/eslint-9-test/eslint.config.js index d86d5e04d6c..75eb0c727fc 100644 --- a/build-tests/eslint-9-test/eslint.config.js +++ b/build-tests/eslint-9-test/eslint.config.js @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +require('local-node-rig/profiles/default/includes/eslint/flat/patch/eslint-bulk-suppressions'); const typescriptEslintParser = require('@typescript-eslint/parser'); const nodeTrustedToolProfile = require('local-node-rig/profiles/default/includes/eslint/flat/profile/node-trusted-tool'); const friendlyLocalsMixin = require('local-node-rig/profiles/default/includes/eslint/flat/mixins/friendly-locals'); diff --git a/build-tests/eslint-9-test/package.json b/build-tests/eslint-9-test/package.json index f9ece522a2f..3a6a4b66879 100644 --- a/build-tests/eslint-9-test/package.json +++ b/build-tests/eslint-9-test/package.json @@ -7,7 +7,8 @@ "license": "MIT", "scripts": { "build": "heft build --clean", - "_phase:build": "heft run --only build -- --clean" + "_phase:build": "heft run --only build -- --clean", + "_phase:test": "heft run --only test -- --clean" }, "devDependencies": { "@rushstack/heft": "workspace:*", diff --git a/build-tests/eslint-9-test/src/__snapshots__/sarif.test.ts.snap b/build-tests/eslint-9-test/src/__snapshots__/sarif.test.ts.snap new file mode 100644 index 00000000000..4dd9678af58 --- /dev/null +++ b/build-tests/eslint-9-test/src/__snapshots__/sarif.test.ts.snap @@ -0,0 +1,111 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Sarif Logs has the expected content 1`] = ` +Object { + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.5", + "runs": Array [ + Object { + "artifacts": Array [ + Object { + "location": Object { + "uri": "src/index.ts", + }, + }, + Object { + "location": Object { + "uri": "src/sarif.test.ts", + }, + }, + ], + "results": Array [ + Object { + "level": "warning", + "locations": Array [ + Object { + "physicalLocation": Object { + "artifactLocation": Object { + "index": 0, + "uri": "src/index.ts", + }, + "region": Object { + "endColumn": 24, + "endLine": 6, + "startColumn": 3, + "startLine": 6, + }, + }, + }, + ], + "message": Object { + "text": "Expected _bar to have a type annotation.", + }, + "ruleId": "@typescript-eslint/typedef", + "ruleIndex": 0, + "suppressions": Array [ + Object { + "justification": "", + "kind": "inSource", + }, + ], + }, + Object { + "level": "warning", + "locations": Array [ + Object { + "physicalLocation": Object { + "artifactLocation": Object { + "index": 0, + "uri": "src/index.ts", + }, + "region": Object { + "endColumn": 30, + "endLine": 10, + "startColumn": 14, + "startLine": 10, + }, + }, + }, + ], + "message": Object { + "text": "Variable name \`Bad_Name\` must match one of the following formats: camelCase, UPPER_CASE, PascalCase", + }, + "ruleId": "@typescript-eslint/naming-convention", + "ruleIndex": 1, + "suppressions": Array [ + Object { + "justification": "", + "kind": "external", + }, + ], + }, + ], + "tool": Object { + "driver": Object { + "informationUri": "https://eslint.org", + "name": "ESLint", + "rules": Array [ + Object { + "helpUri": "https://typescript-eslint.io/rules/typedef", + "id": "@typescript-eslint/typedef", + "properties": Object {}, + "shortDescription": Object { + "text": "Require type annotations in certain places", + }, + }, + Object { + "helpUri": "https://typescript-eslint.io/rules/naming-convention", + "id": "@typescript-eslint/naming-convention", + "properties": Object {}, + "shortDescription": Object { + "text": "Enforce naming conventions for everything across a codebase", + }, + }, + ], + "version": "9.37.0", + }, + }, + }, + ], + "version": "2.1.0", +} +`; diff --git a/build-tests/eslint-9-test/src/index.ts b/build-tests/eslint-9-test/src/index.ts index 428f8caba4f..549373093be 100644 --- a/build-tests/eslint-9-test/src/index.ts +++ b/build-tests/eslint-9-test/src/index.ts @@ -2,6 +2,9 @@ // See LICENSE in the project root for license information. export class Foo { - private _bar: string = 'bar'; + // eslint-disable-next-line @typescript-eslint/typedef + private _bar = 'bar'; public baz: string = this._bar; } + +export const Bad_Name: string = '37'; diff --git a/build-tests/eslint-9-test/src/sarif.test.ts b/build-tests/eslint-9-test/src/sarif.test.ts new file mode 100644 index 00000000000..b24c907090d --- /dev/null +++ b/build-tests/eslint-9-test/src/sarif.test.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +const sarifLogPath: string = path.resolve(__dirname, '../temp/build/lint/lint.sarif'); + +describe('Sarif Logs', () => { + it('has the expected content', () => { + const logContent = fs.readFileSync(sarifLogPath, 'utf-8'); + const parsedLog = JSON.parse(logContent); + expect(parsedLog).toMatchSnapshot(); + }); +}); diff --git a/build-tests/eslint-9-test/tsconfig.json b/build-tests/eslint-9-test/tsconfig.json index 8a46ac2445e..65e0cf100f1 100644 --- a/build-tests/eslint-9-test/tsconfig.json +++ b/build-tests/eslint-9-test/tsconfig.json @@ -1,24 +1,5 @@ { "$schema": "http://json.schemastore.org/tsconfig", - "compilerOptions": { - "outDir": "lib", - "rootDir": "src", - - "forceConsistentCasingInFileNames": true, - "declaration": true, - "sourceMap": true, - "declarationMap": true, - "inlineSources": true, - "experimentalDecorators": true, - "strictNullChecks": true, - "noUnusedLocals": true, - - "module": "esnext", - "moduleResolution": "node", - "target": "es5", - "lib": ["es5"] - }, - "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "lib"] + "extends": "./node_modules/local-node-rig/profiles/default/tsconfig-base.json" } diff --git a/rigs/local-node-rig/profiles/default/config/heft.json b/rigs/local-node-rig/profiles/default/config/heft.json index 437ad9b13ba..b8d42182128 100644 --- a/rigs/local-node-rig/profiles/default/config/heft.json +++ b/rigs/local-node-rig/profiles/default/config/heft.json @@ -1,5 +1,19 @@ { "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", - "extends": "@rushstack/heft-node-rig/profiles/default/config/heft.json" + "extends": "@rushstack/heft-node-rig/profiles/default/config/heft.json", + + "phasesByName": { + "build": { + "tasksByName": { + "lint": { + "taskPlugin": { + "options": { + "sarifLogPath": "temp/build/lint/lint.sarif" + } + } + } + } + } + } } diff --git a/rigs/local-web-rig/profiles/app/config/heft.json b/rigs/local-web-rig/profiles/app/config/heft.json index 199a231f267..e3679801f22 100644 --- a/rigs/local-web-rig/profiles/app/config/heft.json +++ b/rigs/local-web-rig/profiles/app/config/heft.json @@ -1,5 +1,19 @@ { "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", - "extends": "@rushstack/heft-web-rig/profiles/app/config/heft.json" + "extends": "@rushstack/heft-web-rig/profiles/app/config/heft.json", + + "phasesByName": { + "build": { + "tasksByName": { + "lint": { + "taskPlugin": { + "options": { + "sarifLogPath": "temp/build/lint/lint.sarif" + } + } + } + } + } + } } diff --git a/rigs/local-web-rig/profiles/library/config/heft.json b/rigs/local-web-rig/profiles/library/config/heft.json index 1ee90630415..d26de84e78b 100644 --- a/rigs/local-web-rig/profiles/library/config/heft.json +++ b/rigs/local-web-rig/profiles/library/config/heft.json @@ -1,5 +1,19 @@ { "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", - "extends": "@rushstack/heft-web-rig/profiles/library/config/heft.json" + "extends": "@rushstack/heft-web-rig/profiles/library/config/heft.json", + + "phasesByName": { + "build": { + "tasksByName": { + "lint": { + "taskPlugin": { + "options": { + "sarifLogPath": "temp/build/lint/lint.sarif" + } + } + } + } + } + } } From 2bc17ab50355580c9fb97e689d43595f1870ea28 Mon Sep 17 00:00:00 2001 From: David Michon Date: Wed, 12 Nov 2025 00:13:28 +0000 Subject: [PATCH 4/5] Fix lint cache --- heft-plugins/heft-lint-plugin/src/Eslint.ts | 10 ++++++++++ heft-plugins/heft-lint-plugin/src/LinterBase.ts | 15 +++++++++------ heft-plugins/heft-lint-plugin/src/Tslint.ts | 4 ++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/heft-plugins/heft-lint-plugin/src/Eslint.ts b/heft-plugins/heft-lint-plugin/src/Eslint.ts index b2aab9552bf..aea83cbee8d 100644 --- a/heft-plugins/heft-lint-plugin/src/Eslint.ts +++ b/heft-plugins/heft-lint-plugin/src/Eslint.ts @@ -370,6 +370,16 @@ export class Eslint extends LinterBase { + return ( + !lintResult.suppressedMessages?.length && (lintResult.errorCount > 0 || lintResult.warningCount > 0) + ); + }); + } + private _getLintFileError( lintResult: TEslint.ESLint.LintResult | TEslintLegacy.ESLint.LintResult, lintMessage: TEslint.Linter.LintMessage | TEslintLegacy.Linter.LintMessage, diff --git a/heft-plugins/heft-lint-plugin/src/LinterBase.ts b/heft-plugins/heft-lint-plugin/src/LinterBase.ts index 1c981496008..37d3a61a5af 100644 --- a/heft-plugins/heft-lint-plugin/src/LinterBase.ts +++ b/heft-plugins/heft-lint-plugin/src/LinterBase.ts @@ -149,12 +149,13 @@ export abstract class LinterBase { ) { fileCount++; const results: TLintResult[] = await this.lintFileAsync(sourceFile); - if (results.length === 0) { + // Always forward the results, since they might be suppressed. + for (const result of results) { + lintResults.push(result); + } + + if (!this.hasLintFailures(results)) { newNoFailureFileVersions.set(relative, version); - } else { - for (const result of results) { - lintResults.push(result); - } } } else { newNoFailureFileVersions.set(relative, version); @@ -198,7 +199,9 @@ export abstract class LinterBase { protected abstract lintFileAsync(sourceFile: IExtendedSourceFile): Promise; - protected abstract lintingFinishedAsync(lintFailures: TLintResult[]): Promise; + protected abstract lintingFinishedAsync(lintResults: TLintResult[]): Promise; + + protected abstract hasLintFailures(lintResults: TLintResult[]): boolean; protected abstract isFileExcludedAsync(filePath: string): Promise; } diff --git a/heft-plugins/heft-lint-plugin/src/Tslint.ts b/heft-plugins/heft-lint-plugin/src/Tslint.ts index 4f90117c351..3cf540ad902 100644 --- a/heft-plugins/heft-lint-plugin/src/Tslint.ts +++ b/heft-plugins/heft-lint-plugin/src/Tslint.ts @@ -206,6 +206,10 @@ export class Tslint extends LinterBase { return this._tslintPackage.Configuration.isFileExcluded(filePath, this._tslintConfiguration); } + protected hasLintFailures(lintResults: TTslint.RuleFailure[]): boolean { + return lintResults.length > 9; + } + private _getLintFileError(tslintFailure: TTslint.RuleFailure, message?: string): FileError { if (!message) { message = getFormattedErrorMessage(tslintFailure); From 408473ece2407760dcb2302cc5da7b86d82fcf8c Mon Sep 17 00:00:00 2001 From: David Michon Date: Tue, 11 Nov 2025 17:24:52 -0800 Subject: [PATCH 5/5] Fix typo in TSLint --- heft-plugins/heft-lint-plugin/src/Tslint.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/heft-plugins/heft-lint-plugin/src/Tslint.ts b/heft-plugins/heft-lint-plugin/src/Tslint.ts index 3cf540ad902..52a15677ad7 100644 --- a/heft-plugins/heft-lint-plugin/src/Tslint.ts +++ b/heft-plugins/heft-lint-plugin/src/Tslint.ts @@ -207,7 +207,7 @@ export class Tslint extends LinterBase { } protected hasLintFailures(lintResults: TTslint.RuleFailure[]): boolean { - return lintResults.length > 9; + return lintResults.length > 0; } private _getLintFileError(tslintFailure: TTslint.RuleFailure, message?: string): FileError {