diff --git a/common/changes/@rushstack/eslint-patch/ating-eslint-sarif-formatter_2024-10-02-21-04.json b/common/changes/@rushstack/eslint-patch/ating-eslint-sarif-formatter_2024-10-02-21-04.json new file mode 100644 index 0000000000..ceefa44d69 --- /dev/null +++ b/common/changes/@rushstack/eslint-patch/ating-eslint-sarif-formatter_2024-10-02-21-04.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/eslint-patch", + "comment": "", + "type": "none" + } + ], + "packageName": "@rushstack/eslint-patch" +} \ No newline at end of file diff --git a/common/changes/@rushstack/eslint-plugin-packlets/ating-eslint-sarif-formatter_2024-10-02-21-04.json b/common/changes/@rushstack/eslint-plugin-packlets/ating-eslint-sarif-formatter_2024-10-02-21-04.json new file mode 100644 index 0000000000..ff918c1ad1 --- /dev/null +++ b/common/changes/@rushstack/eslint-plugin-packlets/ating-eslint-sarif-formatter_2024-10-02-21-04.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/eslint-plugin-packlets", + "comment": "", + "type": "none" + } + ], + "packageName": "@rushstack/eslint-plugin-packlets" +} \ No newline at end of file diff --git a/common/changes/@rushstack/eslint-plugin-security/ating-eslint-sarif-formatter_2024-10-02-21-04.json b/common/changes/@rushstack/eslint-plugin-security/ating-eslint-sarif-formatter_2024-10-02-21-04.json new file mode 100644 index 0000000000..a4477ef1a6 --- /dev/null +++ b/common/changes/@rushstack/eslint-plugin-security/ating-eslint-sarif-formatter_2024-10-02-21-04.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/eslint-plugin-security", + "comment": "", + "type": "none" + } + ], + "packageName": "@rushstack/eslint-plugin-security" +} \ No newline at end of file diff --git a/common/changes/@rushstack/eslint-plugin/ating-eslint-sarif-formatter_2024-10-02-21-04.json b/common/changes/@rushstack/eslint-plugin/ating-eslint-sarif-formatter_2024-10-02-21-04.json new file mode 100644 index 0000000000..dcf9346965 --- /dev/null +++ b/common/changes/@rushstack/eslint-plugin/ating-eslint-sarif-formatter_2024-10-02-21-04.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/eslint-plugin", + "comment": "", + "type": "none" + } + ], + "packageName": "@rushstack/eslint-plugin" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft-lint-plugin/ating-eslint-sarif-formatter_2024-10-01-21-31.json b/common/changes/@rushstack/heft-lint-plugin/ating-eslint-sarif-formatter_2024-10-01-21-31.json new file mode 100644 index 0000000000..5d29c57ed7 --- /dev/null +++ b/common/changes/@rushstack/heft-lint-plugin/ating-eslint-sarif-formatter_2024-10-01-21-31.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-lint-plugin", + "comment": "Add an option `sarifLogPath` that, when specified, will emit logs in the SARIF format: https://sarifweb.azurewebsites.net/. Note that this is only supported by ESLint.", + "type": "minor" + } + ], + "packageName": "@rushstack/heft-lint-plugin" +} \ No newline at end of file diff --git a/common/config/subspaces/build-tests-subspace/repo-state.json b/common/config/subspaces/build-tests-subspace/repo-state.json index ef88d27072..5c13112cf8 100644 --- a/common/config/subspaces/build-tests-subspace/repo-state.json +++ b/common/config/subspaces/build-tests-subspace/repo-state.json @@ -2,5 +2,5 @@ { "pnpmShrinkwrapHash": "5b75a8ef91af53a8caf52319e5eb0042c4d06852", "preferredVersionsHash": "ce857ea0536b894ec8f346aaea08cfd85a5af648", - "packageJsonInjectedDependenciesHash": "8927ca4e0147b9436659f98a2ff8ca347107d52f" + "packageJsonInjectedDependenciesHash": "5ff2fabbffcfb22bb3e29f56c997f7c762e89d20" } diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index 8a615b6e7a..c426ccc554 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -2415,8 +2415,8 @@ importers: specifier: 2.6.31 version: 2.6.31(@rushstack/heft@0.67.2)(@types/node@18.17.15)(supports-color@8.1.1) '@types/eslint': - specifier: 8.2.0 - version: 8.2.0 + specifier: 8.56.10 + version: 8.56.10 '@types/node': specifier: 18.17.15 version: 18.17.15 @@ -2452,8 +2452,8 @@ importers: specifier: 2.6.31 version: 2.6.31(@rushstack/heft@0.67.2)(@types/node@18.17.15)(supports-color@8.1.1) '@types/eslint': - specifier: 8.2.0 - version: 8.2.0 + specifier: 8.56.10 + version: 8.56.10 '@types/estree': specifier: 1.0.5 version: 1.0.5 @@ -2498,8 +2498,8 @@ importers: specifier: 2.6.31 version: 2.6.31(@rushstack/heft@0.67.2)(@types/node@18.17.15)(supports-color@8.1.1) '@types/eslint': - specifier: 8.2.0 - version: 8.2.0 + specifier: 8.56.10 + version: 8.56.10 '@types/estree': specifier: 1.0.5 version: 1.0.5 @@ -2544,8 +2544,8 @@ importers: specifier: 2.6.31 version: 2.6.31(@rushstack/heft@0.67.2)(@types/node@18.17.15)(supports-color@8.1.1) '@types/eslint': - specifier: 8.2.0 - version: 8.2.0 + specifier: 8.56.10 + version: 8.56.10 '@types/estree': specifier: 1.0.5 version: 1.0.5 @@ -2759,8 +2759,8 @@ importers: specifier: workspace:* version: link:../../libraries/terminal '@types/eslint': - specifier: 8.2.0 - version: 8.2.0 + specifier: 8.56.10 + version: 8.56.10 '@types/heft-jest': specifier: 1.0.1 version: 1.0.1 @@ -9023,7 +9023,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.12.12 + '@types/node': 17.0.41 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -12918,8 +12918,8 @@ packages: resolution: {integrity: sha512-XIpxU6Qdvp1ZE6Kr3yrkv1qgUab0fyf4mHYvW8N3Bx3PCsbN6or1q9/q72cv5jIFWolaGH08U9XyYoLLIykyKQ==} dev: true - /@types/eslint@8.2.0: - resolution: {integrity: sha512-74hbvsnc+7TEDa1z5YLSe4/q8hGYB3USNvCuzHUJrjPV6hXaq8IXcngCrHkuvFt0+8rFz7xYXrHgNayIX0UZvQ==} + /@types/eslint@8.56.10: + resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==} dependencies: '@types/estree': 1.0.5 '@types/json-schema': 7.0.15 @@ -21637,7 +21637,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.12.12 + '@types/node': 17.0.41 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -21679,7 +21679,7 @@ packages: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.12.12 + '@types/node': 17.0.41 merge-stream: 2.0.0 supports-color: 7.2.0 dev: true @@ -21696,7 +21696,7 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.12.12 + '@types/node': 17.0.41 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 diff --git a/common/config/subspaces/default/repo-state.json b/common/config/subspaces/default/repo-state.json index 70786e90e0..50d3034203 100644 --- a/common/config/subspaces/default/repo-state.json +++ b/common/config/subspaces/default/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "5afee1d392a0c2404869d7eda687b5571fe88515", + "pnpmShrinkwrapHash": "673f7de41244835915a7947c11b6dbe80f944010", "preferredVersionsHash": "ce857ea0536b894ec8f346aaea08cfd85a5af648" } diff --git a/eslint/eslint-patch/package.json b/eslint/eslint-patch/package.json index b5835cafa2..b02d78c7e1 100644 --- a/eslint/eslint-patch/package.json +++ b/eslint/eslint-patch/package.json @@ -32,7 +32,7 @@ "devDependencies": { "@rushstack/heft": "0.67.2", "@rushstack/heft-node-rig": "2.6.31", - "@types/eslint": "8.2.0", + "@types/eslint": "8.56.10", "@types/node": "18.17.15", "@typescript-eslint/types": "~5.59.2", "eslint": "~8.57.0", diff --git a/eslint/eslint-patch/src/eslint-bulk-suppressions/cli/runEslint.ts b/eslint/eslint-patch/src/eslint-bulk-suppressions/cli/runEslint.ts index ff26a0ad38..be0bdf494e 100644 --- a/eslint/eslint-patch/src/eslint-bulk-suppressions/cli/runEslint.ts +++ b/eslint/eslint-patch/src/eslint-bulk-suppressions/cli/runEslint.ts @@ -35,7 +35,7 @@ export async function runEslintAsync(files: string[], mode: 'suppress' | 'prune' if (results.length > 0) { const stylishFormatter: ESLint.Formatter = await eslint.loadFormatter(); - const formattedResults: string = stylishFormatter.format(results); + const formattedResults: string = await Promise.resolve(stylishFormatter.format(results)); console.log(formattedResults); } diff --git a/eslint/eslint-plugin-packlets/package.json b/eslint/eslint-plugin-packlets/package.json index 644761b112..b42e094143 100644 --- a/eslint/eslint-plugin-packlets/package.json +++ b/eslint/eslint-plugin-packlets/package.json @@ -32,7 +32,7 @@ "devDependencies": { "@rushstack/heft": "0.67.2", "@rushstack/heft-node-rig": "2.6.31", - "@types/eslint": "8.2.0", + "@types/eslint": "8.56.10", "@types/estree": "1.0.5", "@types/heft-jest": "1.0.1", "@types/node": "18.17.15", diff --git a/eslint/eslint-plugin-security/package.json b/eslint/eslint-plugin-security/package.json index e66f0bb89e..1065bde6f6 100644 --- a/eslint/eslint-plugin-security/package.json +++ b/eslint/eslint-plugin-security/package.json @@ -32,7 +32,7 @@ "@eslint/eslintrc": "~3.0.0", "@rushstack/heft": "0.67.2", "@rushstack/heft-node-rig": "2.6.31", - "@types/eslint": "8.2.0", + "@types/eslint": "8.56.10", "@types/estree": "1.0.5", "@types/heft-jest": "1.0.1", "@types/node": "18.17.15", diff --git a/eslint/eslint-plugin/package.json b/eslint/eslint-plugin/package.json index be579e6ee4..66b0b46ae5 100644 --- a/eslint/eslint-plugin/package.json +++ b/eslint/eslint-plugin/package.json @@ -36,7 +36,7 @@ "@eslint/eslintrc": "~3.0.0", "@rushstack/heft": "0.67.2", "@rushstack/heft-node-rig": "2.6.31", - "@types/eslint": "8.2.0", + "@types/eslint": "8.56.10", "@types/estree": "1.0.5", "@types/heft-jest": "1.0.1", "@types/node": "18.17.15", diff --git a/heft-plugins/heft-lint-plugin/package.json b/heft-plugins/heft-lint-plugin/package.json index 00a63df365..9c8448f8e5 100644 --- a/heft-plugins/heft-lint-plugin/package.json +++ b/heft-plugins/heft-lint-plugin/package.json @@ -27,7 +27,7 @@ "@rushstack/heft-typescript-plugin": "workspace:*", "@rushstack/heft-node-rig": "2.6.31", "@rushstack/terminal": "workspace:*", - "@types/eslint": "8.2.0", + "@types/eslint": "8.56.10", "@types/heft-jest": "1.0.1", "@types/node": "18.17.15", "@types/semver": "7.5.0", diff --git a/heft-plugins/heft-lint-plugin/src/Eslint.ts b/heft-plugins/heft-lint-plugin/src/Eslint.ts index edc4603fd2..b5027f2b17 100644 --- a/heft-plugins/heft-lint-plugin/src/Eslint.ts +++ b/heft-plugins/heft-lint-plugin/src/Eslint.ts @@ -6,7 +6,7 @@ import * as semver from 'semver'; import type * as TTypescript from 'typescript'; import type * as TEslint from 'eslint'; import { performance } from 'perf_hooks'; -import { FileError } from '@rushstack/node-core-library'; +import { FileError, FileSystem } from '@rushstack/node-core-library'; import { LinterBase, type ILinterBaseOptions } from './LinterBase'; @@ -59,12 +59,22 @@ export class Eslint extends LinterBase { private readonly _currentFixMessages: TEslint.Linter.LintMessage[] = []; private readonly _fixMessagesByResult: Map = new Map(); + private readonly _sarifLogPath: string | undefined; protected constructor(options: IEslintOptions) { super('eslint', options); - const { buildFolderPath, eslintPackage, linterConfigFilePath, tsProgram, eslintTimings, fix } = options; + const { + buildFolderPath, + eslintPackage, + linterConfigFilePath, + tsProgram, + eslintTimings, + fix, + sarifLogPath + } = options; this._eslintPackage = eslintPackage; + this._sarifLogPath = sarifLogPath; let overrideConfig: TEslint.Linter.Config | undefined; let fixFn: Exclude; @@ -166,17 +176,10 @@ export class Eslint extends LinterBase { return lintResult.fixableErrorCount + lintResult.fixableWarningCount > 0; })); - const failures: TEslint.ESLint.LintResult[] = []; - for (const lintResult of lintResults) { - if (lintResult.messages.length > 0 || lintResult.output) { - failures.push(lintResult); - } - } - - return failures; + return lintResults; } - protected async lintingFinishedAsync(lintFailures: TEslint.ESLint.LintResult[]): Promise { + protected async lintingFinishedAsync(lintResults: TEslint.ESLint.LintResult[]): Promise { let omittedRuleCount: number = 0; const timings: [string, number][] = Array.from(this._eslintTimings).sort( (x: [string, number], y: [string, number]) => { @@ -196,24 +199,23 @@ export class Eslint extends LinterBase { } if (this._fix && this._fixMessagesByResult.size > 0) { - await this._eslintPackage.ESLint.outputFixes(lintFailures); + await this._eslintPackage.ESLint.outputFixes(lintResults); } - for (const lintFailure of lintFailures) { + for (const lintResult of lintResults) { // Report linter fixes to the logger. These will only be returned when the underlying failure was fixed - const fixMessages: TEslint.Linter.LintMessage[] | undefined = - this._fixMessagesByResult.get(lintFailure); + const fixMessages: TEslint.Linter.LintMessage[] | undefined = this._fixMessagesByResult.get(lintResult); if (fixMessages) { for (const fixMessage of fixMessages) { const formattedMessage: string = `[FIXED] ${getFormattedErrorMessage(fixMessage)}`; - const errorObject: FileError = this._getLintFileError(lintFailure, fixMessage, formattedMessage); + const errorObject: FileError = this._getLintFileError(lintResult, fixMessage, formattedMessage); this._scopedLogger.emitWarning(errorObject); } } // Report linter errors and warnings to the logger - for (const lintMessage of lintFailure.messages) { - const errorObject: FileError = this._getLintFileError(lintFailure, lintMessage); + for (const lintMessage of lintResult.messages) { + const errorObject: FileError = this._getLintFileError(lintResult, lintMessage); switch (lintMessage.severity) { case EslintMessageSeverity.error: { this._scopedLogger.emitError(errorObject); @@ -227,6 +229,24 @@ export class Eslint extends LinterBase { } } } + + const sarifLogPath: string | undefined = this._sarifLogPath; + if (sarifLogPath) { + const rulesMeta: TEslint.ESLint.LintResultData['rulesMeta'] = + this._linter.getRulesMetaForResults(lintResults); + const { formatEslintResultsAsSARIF } = await import('./SarifFormatter'); + const sarifString: string = JSON.stringify( + formatEslintResultsAsSARIF(lintResults, rulesMeta, { + ignoreSuppressed: false, + eslintVersion: this._eslintPackage.ESLint.version, + buildFolderPath: this._buildFolderPath + }), + undefined, + 2 + ); + + await FileSystem.writeFileAsync(sarifLogPath, sarifString, { ensureFolderExists: true }); + } } protected async isFileExcludedAsync(filePath: string): Promise { diff --git a/heft-plugins/heft-lint-plugin/src/LintPlugin.ts b/heft-plugins/heft-lint-plugin/src/LintPlugin.ts index faee0f156a..aabfe0dc56 100644 --- a/heft-plugins/heft-lint-plugin/src/LintPlugin.ts +++ b/heft-plugins/heft-lint-plugin/src/LintPlugin.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import path from 'node:path'; + import { FileSystem } from '@rushstack/node-core-library'; import type { HeftConfiguration, @@ -28,6 +30,7 @@ const ESLINTRC_CJS_FILENAME: string = '.eslintrc.cjs'; interface ILintPluginOptions { alwaysFix?: boolean; + sarifLogPath?: string; } interface ILintOptions { @@ -35,6 +38,7 @@ interface ILintOptions { heftConfiguration: HeftConfiguration; tsProgram: IExtendedProgram; fix?: boolean; + sarifLogPath?: string; changedFiles?: ReadonlySet; } @@ -67,6 +71,10 @@ export default class LintPlugin implements IHeftTaskPlugin { fix = false; } + const relativeSarifLogPath: string | undefined = pluginOptions?.sarifLogPath; + const sarifLogPath: string | undefined = + relativeSarifLogPath && path.resolve(heftConfiguration.buildFolderPath, relativeSarifLogPath); + // Use the changed files hook to kick off linting asynchronously taskSession.requestAccessToPluginByName( '@rushstack/heft-typescript-plugin', @@ -80,6 +88,7 @@ export default class LintPlugin implements IHeftTaskPlugin { taskSession, heftConfiguration, fix, + sarifLogPath, tsProgram: changedFilesHookOptions.program as IExtendedProgram, changedFiles: changedFilesHookOptions.changedFiles as ReadonlySet }); @@ -144,7 +153,7 @@ export default class LintPlugin implements IHeftTaskPlugin { } private async _lintAsync(options: ILintOptions): Promise { - const { taskSession, heftConfiguration, tsProgram, changedFiles, fix } = options; + const { taskSession, heftConfiguration, tsProgram, changedFiles, fix, sarifLogPath } = options; // Ensure that we have initialized. This promise is cached, so calling init // multiple times will only init once. @@ -155,6 +164,7 @@ export default class LintPlugin implements IHeftTaskPlugin { const eslintLinter: Eslint = await Eslint.initializeAsync({ tsProgram, fix, + sarifLogPath, scopedLogger: taskSession.logger, linterToolPath: this._eslintToolPath, linterConfigFilePath: this._eslintConfigFilePath, diff --git a/heft-plugins/heft-lint-plugin/src/LinterBase.ts b/heft-plugins/heft-lint-plugin/src/LinterBase.ts index fe996d47e1..a198e4fab2 100644 --- a/heft-plugins/heft-lint-plugin/src/LinterBase.ts +++ b/heft-plugins/heft-lint-plugin/src/LinterBase.ts @@ -21,6 +21,7 @@ export interface ILinterBaseOptions { linterConfigFilePath: string; tsProgram: IExtendedProgram; fix?: boolean; + sarifLogPath?: string; } export interface IRunLinterOptions { @@ -128,7 +129,7 @@ export abstract class LinterBase { // Some of this code comes from here: // https://github.com/palantir/tslint/blob/24d29e421828348f616bf761adb3892bcdf51662/src/linter.ts#L161-L179 // Modified to only lint files that have changed and that we care about - const lintFailures: TLintResult[] = []; + const lintResults: TLintResult[] = []; for (const sourceFile of options.tsProgram.getSourceFiles()) { const filePath: string = sourceFile.fileName; const relative: string | undefined = relativePaths.get(filePath); @@ -147,12 +148,12 @@ export abstract class LinterBase { options.changedFiles.has(sourceFile) ) { fileCount++; - const failures: TLintResult[] = await this.lintFileAsync(sourceFile); - if (failures.length === 0) { + const results: TLintResult[] = await this.lintFileAsync(sourceFile); + if (results.length === 0) { newNoFailureFileVersions.set(relative, version); } else { - for (const failure of failures) { - lintFailures.push(failure); + for (const result of results) { + lintResults.push(result); } } } else { @@ -161,7 +162,7 @@ export abstract class LinterBase { } //#endregion - await this.lintingFinishedAsync(lintFailures); + await this.lintingFinishedAsync(lintResults); if (!this._fix && this._fixesPossible) { this._terminal.writeWarningLine( diff --git a/heft-plugins/heft-lint-plugin/src/SarifFormatter.ts b/heft-plugins/heft-lint-plugin/src/SarifFormatter.ts new file mode 100644 index 0000000000..80ec1d463b --- /dev/null +++ b/heft-plugins/heft-lint-plugin/src/SarifFormatter.ts @@ -0,0 +1,341 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. +import type * as TEslint from 'eslint'; +import path from 'node:path'; +import { Path } from '@rushstack/node-core-library'; + +export interface ISerifFormatterOptions { + ignoreSuppressed: boolean; + eslintVersion?: string; + buildFolderPath: string; +} + +export interface ISarifRun { + tool: { + driver: { + name: string; + informationUri: string; + version?: string; + rules: IStaticAnalysisRules[]; + }; + }; + artifacts?: ISarifFile[]; + results?: ISarifRepresentation[]; + invocations?: { + toolConfigurationNotifications: ISarifRepresentation[]; + executionSuccessful: boolean; + }[]; +} + +export interface ISarifRepresentation { + level: string; + message: { + text: string; + }; + locations: ISarifLocation[]; + ruleId?: string; + ruleIndex?: number; + descriptor?: { + id: string; + }; + suppressions?: ISuppressedAnalysis[]; +} + +// Interface for the SARIF log structure +export interface ISarifLog { + version: string; + $schema: string; + runs: ISarifRun[]; +} + +export interface IRegion { + startLine?: number; + startColumn?: number; + endLine?: number; + endColumn?: number; + snippet?: { + text: string; + }; +} + +export interface IStaticAnalysisRules { + id: string; + name?: string; + shortDescription?: { + text: string; + }; + fullDescription?: { + text: string; + }; + defaultConfiguration?: { + level: 'note' | 'warning' | 'error'; + }; + helpUri?: string; + properties?: { + category?: string; + precision?: 'very-high' | 'high' | 'medium' | 'low'; + tags?: string[]; + problem?: { + severity?: 'recommendation' | 'warning' | 'error'; + securitySeverity?: number; + }; + }; +} + +export interface ISarifFile { + location: { + uri: string; + }; +} + +export interface ISuppressedAnalysis { + kind: string; + justification: string; +} + +export interface ISarifLocation { + physicalLocation: ISarifPhysicalLocation; +} + +export interface ISarifArtifactLocation { + uri: string; + index?: number; +} + +export interface ISarifPhysicalLocation { + artifactLocation: ISarifArtifactLocation; + region?: IRegion; +} + +export interface ISarifRule { + id: string; + helpUri?: string; + shortDescription?: { + text: string; + }; + properties?: { + category?: string; + }; +} + +interface IMessage extends TEslint.Linter.LintMessage { + suppressions?: ISuppressedAnalysis[]; +} + +const INTERNAL_ERROR_ID: 'ESL0999' = 'ESL0999'; +const SARIF_VERSION: '2.1.0' = '2.1.0'; +const SARIF_INFORMATION_URI: 'http://json.schemastore.org/sarif-2.1.0-rtm.5' = + 'http://json.schemastore.org/sarif-2.1.0-rtm.5'; +/** + * Converts ESLint results into a SARIF (Static Analysis Results Interchange Format) log. + * + * This function takes in a list of ESLint lint results, processes them to extract + * relevant information such as errors, warnings, and suppressed messages, and + * outputs a SARIF log which conforms to the SARIF v2.1.0 specification. + * + * @param results - An array of lint results from ESLint that contains linting information, + * such as file paths, messages, and suppression details. + * @param rulesMeta - An object containing metadata about the ESLint rules that were applied during the linting session. + * The keys are the rule names, and the values are rule metadata objects + * that describe each rule. This metadata typically includes: + * - `docs`: Documentation about the rule. + * - `fixable`: Indicates whether the rule is fixable. + * - `messages`: Custom messages that the rule might output when triggered. + * - `schema`: The configuration schema for the rule. + * This metadata helps in providing more context about the rules when generating the SARIF log. + * @param options - An object containing options for formatting: + * - `ignoreSuppressed`: Boolean flag to decide whether to ignore suppressed messages. + * - `eslintVersion`: Optional string to include the version of ESLint in the SARIF log. + * @returns The SARIF log containing information about the linting results in SARIF format. + */ + +export function formatEslintResultsAsSARIF( + results: TEslint.ESLint.LintResult[], + rulesMeta: TEslint.ESLint.LintResultData['rulesMeta'], + options: ISerifFormatterOptions +): ISarifLog { + const { ignoreSuppressed, eslintVersion, buildFolderPath } = options; + const toolConfigurationNotifications: ISarifRepresentation[] = []; + const sarifFiles: ISarifFile[] = []; + const sarifResults: ISarifRepresentation[] = []; + const sarifArtifactIndices: Map = new Map(); + const sarifRules: ISarifRule[] = []; + const sarifRuleIndices: Map = new Map(); + + const sarifRun: ISarifRun = { + tool: { + driver: { + name: 'ESLint', + informationUri: 'https://eslint.org', + version: eslintVersion, + rules: [] + } + } + }; + + const sarifLog: ISarifLog = { + version: SARIF_VERSION, + $schema: SARIF_INFORMATION_URI, + runs: [sarifRun] + }; + + let executionSuccessful: boolean = true; + let currentArtifactIndex: number = 0; + let currentRuleIndex: number = 0; + + for (const result of results) { + const { filePath } = result; + const fileUrl: string = Path.convertToSlashes(path.relative(buildFolderPath, filePath)); + let sarifFileIndex: number | undefined = sarifArtifactIndices.get(fileUrl); + + if (sarifFileIndex === undefined) { + sarifFileIndex = currentArtifactIndex++; + sarifArtifactIndices.set(fileUrl, sarifFileIndex); + sarifFiles.push({ + location: { + uri: fileUrl + } + }); + } + + const artifactLocation: ISarifArtifactLocation = { + uri: fileUrl, + index: sarifFileIndex + }; + + const containsSuppressedMessages: boolean = + result.suppressedMessages && result.suppressedMessages.length > 0; + const messages: IMessage[] = + containsSuppressedMessages && !ignoreSuppressed + ? [...result.messages, ...result.suppressedMessages] + : result.messages; + + for (const message of messages) { + const level: string = message.fatal || message.severity === 2 ? 'error' : 'warning'; + const physicalLocation: ISarifPhysicalLocation = { + artifactLocation + }; + + const sarifRepresentation: ISarifRepresentation = { + level, + message: { + text: message.message + }, + locations: [ + { + physicalLocation + } + ] + }; + + if (message.ruleId) { + sarifRepresentation.ruleId = message.ruleId; + + if (rulesMeta && sarifRuleIndices.get(message.ruleId) === undefined) { + const meta: TEslint.Rule.RuleMetaData = rulesMeta[message.ruleId]; + + // An unknown ruleId will return null. This check prevents unit test failure. + if (meta) { + sarifRuleIndices.set(message.ruleId, currentRuleIndex++); + + if (meta.docs) { + // Create a new entry in the rules dictionary. + const shortDescription: string = meta.docs.description ?? ''; + + const sarifRule: ISarifRule = { + id: message.ruleId, + helpUri: meta.docs.url, + properties: { + category: meta.docs.category + }, + shortDescription: { + text: shortDescription + } + }; + sarifRules.push(sarifRule); + // Some rulesMetas do not have docs property + } else { + sarifRules.push({ + id: message.ruleId, + properties: { + category: 'No category provided' + }, + shortDescription: { + text: 'Please see details in message' + } + }); + } + } + } + + if (sarifRuleIndices.has(message.ruleId)) { + sarifRepresentation.ruleIndex = sarifRuleIndices.get(message.ruleId); + } + + if (containsSuppressedMessages && !ignoreSuppressed) { + sarifRepresentation.suppressions = message.suppressions + ? message.suppressions.map((suppression: ISuppressedAnalysis) => { + return { + kind: suppression.kind === 'directive' ? 'inSource' : 'external', + justification: suppression.justification + }; + }) + : []; + } + } else { + sarifRepresentation.descriptor = { + id: INTERNAL_ERROR_ID + }; + + if (sarifRepresentation.level === 'error') { + executionSuccessful = false; + } + } + + if (message.line !== undefined || message.column !== undefined) { + const { line: startLine, column: startColumn, endLine, endColumn } = message; + const region: IRegion = { + startLine, + startColumn, + endLine, + endColumn + }; + physicalLocation.region = region; + } + + if (message.source) { + physicalLocation.region ??= {}; + physicalLocation.region.snippet = { + text: message.source + }; + } + + if (message.ruleId) { + sarifResults.push(sarifRepresentation); + } else { + toolConfigurationNotifications.push(sarifRepresentation); + } + } + } + + if (sarifRules.length > 0) { + sarifRun.tool.driver.rules = sarifRules; + } + + if (sarifFiles.length > 0) { + sarifRun.artifacts = sarifFiles; + } + + sarifRun.results = sarifResults; + + if (toolConfigurationNotifications.length > 0) { + sarifRun.invocations = [ + { + toolConfigurationNotifications, + executionSuccessful + } + ]; + } + + return sarifLog; +} diff --git a/heft-plugins/heft-lint-plugin/src/schemas/heft-lint-plugin.schema.json b/heft-plugins/heft-lint-plugin/src/schemas/heft-lint-plugin.schema.json index d48c0d3788..072d2c70df 100644 --- a/heft-plugins/heft-lint-plugin/src/schemas/heft-lint-plugin.schema.json +++ b/heft-plugins/heft-lint-plugin/src/schemas/heft-lint-plugin.schema.json @@ -11,6 +11,12 @@ "title": "Always Fix", "description": "If set to true, fix all encountered rule violations where the violated rule provides a fixer, regardless of if the \"--fix\" command-line argument is provided. When running in production mode, fixes will be disabled regardless of this setting.", "type": "boolean" + }, + + "sarifLogPath": { + "title": "SARIF Log Path", + "description": "If specified and using ESLint, a log describing the lint configuration and all messages (suppressed or not) will be emitted in the Static Analysis Results Interchange Format (https://sarifweb.azurewebsites.net/) at the provided path, relative to the project root.", + "type": "string" } } } diff --git a/heft-plugins/heft-lint-plugin/src/test/SarifFormatter.test.ts b/heft-plugins/heft-lint-plugin/src/test/SarifFormatter.test.ts new file mode 100644 index 0000000000..c4f359cf3a --- /dev/null +++ b/heft-plugins/heft-lint-plugin/src/test/SarifFormatter.test.ts @@ -0,0 +1,651 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. +import { formatEslintResultsAsSARIF } from '../SarifFormatter'; +import type { ISerifFormatterOptions } from '../SarifFormatter'; +import type { ESLint } from 'eslint'; + +describe('formatEslintResultsAsSARIF', () => { + test('should correctly format ESLint results into SARIF log', () => { + const mockLintResults: ESLint.LintResult[] = [ + { + filePath: '/src/file1.ts', + messages: [ + { + ruleId: 'no-unused-vars', + severity: 2, + message: "'x' is defined but never used.", + line: 10, + column: 5, + nodeType: 'Identifier', + endLine: 10, + endColumn: 6, + source: 'const x = 1;' + } + ], + suppressedMessages: [], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [], + fatalErrorCount: 0 + } + ]; + + const mockRulesMeta: ESLint.LintResultData['rulesMeta'] = { + 'no-unused-vars': { + type: 'suggestion', + docs: { + description: "'x' is defined but never used.", + recommended: false, + url: 'https://eslint.org/docs/latest/rules/no-unused-vars' + }, + schema: [ + { + type: 'object', + properties: { + allow: { + type: 'array', + items: { + type: 'string' + }, + minItems: 1, + uniqueItems: true + } + }, + additionalProperties: false + } + ], + hasSuggestions: true, + messages: { + unexpected: "'x' is defined but never used." + } + } + }; + + const options: ISerifFormatterOptions = { + ignoreSuppressed: false, + eslintVersion: '7.32.0', + buildFolderPath: '/' + }; + + const sarifLog = formatEslintResultsAsSARIF(mockLintResults, mockRulesMeta, options); + + expect(sarifLog).toMatchSnapshot(); + }); + + test('case with no files', () => { + const mockLintResults: ESLint.LintResult[] = []; + + const mockRulesMeta: ESLint.LintResultData['rulesMeta'] = {}; + + const options: ISerifFormatterOptions = { + ignoreSuppressed: false, + eslintVersion: '7.32.0', + buildFolderPath: '/' + }; + + const sarifLog = formatEslintResultsAsSARIF(mockLintResults, mockRulesMeta, options); + + expect(sarifLog).toMatchSnapshot(); + }); + + test('case with single issues in the same file', () => { + const mockLintResults: ESLint.LintResult[] = [ + { + filePath: '/src/file1.ts', + messages: [ + { + ruleId: 'no-unused-vars', + severity: 2, + message: "'x' is defined but never used.", + line: 10, + column: 5, + nodeType: 'Identifier', + endLine: 10, + endColumn: 6, + source: 'const x = 1;' + } + ], + suppressedMessages: [], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [], + fatalErrorCount: 0 + } + ]; + + const mockRulesMeta: ESLint.LintResultData['rulesMeta'] = { + 'no-unused-vars': { + type: 'suggestion', + docs: { + description: "'x' is defined but never used.", + recommended: false, + url: 'https://eslint.org/docs/latest/rules/no-unused-vars' + }, + schema: [ + { + type: 'object', + properties: { + allow: { + type: 'array', + items: { + type: 'string' + }, + minItems: 1, + uniqueItems: true + } + }, + additionalProperties: false + } + ], + hasSuggestions: true, + messages: { + unexpected: "'x' is defined but never used." + } + } + }; + + const options: ISerifFormatterOptions = { + ignoreSuppressed: false, + eslintVersion: '7.32.0', + buildFolderPath: '/' + }; + + const sarifLog = formatEslintResultsAsSARIF(mockLintResults, mockRulesMeta, options); + + expect(sarifLog).toMatchSnapshot(); + }); + + test('should handle multiple issues in the same file', async () => { + const mockLintResults: ESLint.LintResult[] = [ + { + filePath: '/src/file2.ts', + messages: [ + { + ruleId: 'no-unused-vars', + severity: 2, + message: "'x' is defined but never used.", + line: 5, + column: 10, + nodeType: 'Identifier', + endLine: 5, + endColumn: 11, + source: 'let x;' + }, + { + ruleId: 'no-console', + severity: 1, + message: 'Unexpected console statement.', + line: 10, + column: 5, + nodeType: 'MemberExpression', + endLine: 10, + endColumn: 16, + source: 'console.log("test");' + } + ], + suppressedMessages: [], + errorCount: 1, + warningCount: 1, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [], + fatalErrorCount: 0 + } + ]; + + const mockRulesMeta: ESLint.LintResultData['rulesMeta'] = { + 'no-console': { + type: 'suggestion', + docs: { + description: 'Disallow the use of `console`', + recommended: false, + url: 'https://eslint.org/docs/latest/rules/no-console' + }, + schema: [ + { + type: 'object', + properties: { + allow: { + type: 'array', + items: { + type: 'string' + }, + minItems: 1, + uniqueItems: true + } + }, + additionalProperties: false + } + ], + hasSuggestions: true, + messages: { + unexpected: 'Unexpected console statement.', + removeConsole: 'Remove the console.{{ propertyName }}().' + } + }, + 'no-unused-vars': { + type: 'suggestion', + docs: { + description: "'x' is defined but never used.", + recommended: false, + url: 'https://eslint.org/docs/latest/rules/no-unused-vars' + }, + schema: [ + { + type: 'object', + properties: { + allow: { + type: 'array', + items: { + type: 'string' + }, + minItems: 1, + uniqueItems: true + } + }, + additionalProperties: false + } + ], + hasSuggestions: true, + messages: { + unexpected: "'x' is defined but never used." + } + } + }; + + const options: ISerifFormatterOptions = { + ignoreSuppressed: false, + eslintVersion: '7.32.0', + buildFolderPath: '/' + }; + + const sarifLog = await formatEslintResultsAsSARIF(mockLintResults, mockRulesMeta, options); + + expect(sarifLog).toMatchSnapshot(); + }); + + test('should handle a file with no messages', async () => { + const mockLintResults: ESLint.LintResult[] = [ + { + filePath: '/src/file3.ts', + messages: [], + suppressedMessages: [], + errorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [], + fatalErrorCount: 0 + } + ]; + + const mockRulesMeta: ESLint.LintResultData['rulesMeta'] = {}; + + const options: ISerifFormatterOptions = { + ignoreSuppressed: false, + eslintVersion: '7.32.0', + buildFolderPath: '/' + }; + + const sarifLog = await formatEslintResultsAsSARIF(mockLintResults, mockRulesMeta, options); + + expect(sarifLog).toMatchSnapshot(); + }); + + test('should handle multiple files', async () => { + const mockLintResults: ESLint.LintResult[] = [ + { + filePath: '/src/file1.ts', + messages: [ + { + ruleId: 'no-unused-vars', + severity: 2, + message: "'x' is defined but never used.", + line: 10, + column: 5, + nodeType: 'Identifier', + endLine: 10, + endColumn: 6, + source: 'const x = 1;' + } + ], + suppressedMessages: [], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [], + fatalErrorCount: 0 + }, + { + filePath: '/src/file2.ts', + messages: [ + { + ruleId: 'eqeqeq', + severity: 2, + message: "Expected '===' and instead saw '=='.", + line: 15, + column: 8, + nodeType: 'BinaryExpression', + endLine: 15, + endColumn: 10, + source: 'if (a == b) { }' + } + ], + suppressedMessages: [], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [], + fatalErrorCount: 0 + } + ]; + + const mockRulesMeta: ESLint.LintResultData['rulesMeta'] = { + 'no-console': { + type: 'suggestion', + docs: { + description: 'Disallow the use of `console`', + recommended: false, + url: 'https://eslint.org/docs/latest/rules/no-console' + }, + schema: [ + { + type: 'object', + properties: { + allow: { + type: 'array', + items: { + type: 'string' + }, + minItems: 1, + uniqueItems: true + } + }, + additionalProperties: false + } + ], + hasSuggestions: true, + messages: { + unexpected: 'Unexpected console statement.', + removeConsole: 'Remove the console.{{ propertyName }}().' + } + }, + eqeqeq: { + type: 'problem', + docs: { + description: 'Require the use of === and !==', + recommended: false, + url: 'https://eslint.org/docs/latest/rules/eqeqeq' + }, + schema: [ + { + type: 'object', + properties: { + allow: { + type: 'array', + items: { + type: 'string' + }, + minItems: 1, + uniqueItems: true + } + }, + additionalProperties: false + } + ], + hasSuggestions: true, + messages: { + unexpected: "Expected '===' and instead saw '=='." + } + } + }; + + const options: ISerifFormatterOptions = { + ignoreSuppressed: false, + eslintVersion: '7.32.0', + buildFolderPath: '/' + }; + + const sarifLog = await formatEslintResultsAsSARIF(mockLintResults, mockRulesMeta, options); + + expect(sarifLog).toMatchSnapshot(); + }); + + test('should handle ignoreSuppressed: true with suppressed messages', async () => { + const mockLintResults: ESLint.LintResult[] = [ + { + filePath: '/src/file4.ts', + messages: [ + { + ruleId: 'no-debugger', + severity: 2, + message: "Unexpected 'debugger' statement.", + line: 20, + column: 1, + nodeType: 'DebuggerStatement', + endLine: 20, + endColumn: 9, + source: 'debugger;' + } + ], + suppressedMessages: [ + { + ruleId: 'no-console', + severity: 1, + message: 'Unexpected console statement.', + line: 10, + column: 5, + nodeType: 'MemberExpression', + endLine: 10, + endColumn: 16, + source: 'console.log("test");', + suppressions: [ + { + kind: 'inSource', + justification: 'rejected' + } + ] + } + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [], + fatalErrorCount: 0 + } + ]; + + const mockRulesMeta: ESLint.LintResultData['rulesMeta'] = { + 'no-console': { + type: 'suggestion', + docs: { + description: 'Disallow the use of `console`', + recommended: false, + url: 'https://eslint.org/docs/latest/rules/no-console' + }, + schema: [ + { + type: 'object', + properties: { + allow: { + type: 'array', + items: { + type: 'string' + }, + minItems: 1, + uniqueItems: true + } + }, + additionalProperties: false + } + ], + hasSuggestions: true, + messages: { + unexpected: 'Unexpected console statement.', + removeConsole: 'Remove the console.{{ propertyName }}().' + } + }, + 'no-debugger': { + type: 'suggestion', + docs: { + description: 'Disallow the use of debugger', + recommended: false, + url: 'https://eslint.org/docs/latest/rules/no-debugger' + }, + schema: [ + { + type: 'object', + properties: { + allow: { + type: 'array', + items: { + type: 'string' + }, + minItems: 1, + uniqueItems: true + } + }, + additionalProperties: false + } + ], + hasSuggestions: true, + messages: { + unexpected: "Unexpected 'debugger' statement." + } + } + }; + + const options: ISerifFormatterOptions = { + ignoreSuppressed: true, + eslintVersion: '7.32.0', + buildFolderPath: '/' + }; + + const sarifLog = await formatEslintResultsAsSARIF(mockLintResults, mockRulesMeta, options); + + expect(sarifLog).toMatchSnapshot(); + }); + + test('should handle ignoreSuppressed: false with suppressed messages', async () => { + const mockLintResults: ESLint.LintResult[] = [ + { + filePath: '/src/file4.ts', + messages: [ + { + ruleId: 'no-debugger', + severity: 2, + message: "Unexpected 'debugger' statement.", + line: 20, + column: 1, + nodeType: 'DebuggerStatement', + endLine: 20, + endColumn: 9, + source: 'debugger;' + } + ], + suppressedMessages: [ + { + ruleId: 'no-console', + severity: 1, + message: 'Unexpected console statement.', + line: 10, + column: 5, + nodeType: 'MemberExpression', + endLine: 10, + endColumn: 16, + source: 'console.log("test");', + suppressions: [ + { + kind: 'inSource', + justification: 'rejected' + } + ] + } + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [], + fatalErrorCount: 0 + } + ]; + + const mockRulesMeta: ESLint.LintResultData['rulesMeta'] = { + 'no-console': { + type: 'suggestion', + docs: { + description: 'Disallow the use of `console`', + recommended: false, + url: 'https://eslint.org/docs/latest/rules/no-console' + }, + schema: [ + { + type: 'object', + properties: { + allow: { + type: 'array', + items: { + type: 'string' + }, + minItems: 1, + uniqueItems: true + } + }, + additionalProperties: false + } + ], + hasSuggestions: true, + messages: { + unexpected: 'Unexpected console statement.', + removeConsole: 'Remove the console.{{ propertyName }}().' + } + }, + 'no-debugger': { + type: 'suggestion', + docs: { + description: 'Disallow the use of debugger', + recommended: false, + url: 'https://eslint.org/docs/latest/rules/no-debugger' + }, + schema: [ + { + type: 'object', + properties: { + allow: { + type: 'array', + items: { + type: 'string' + }, + minItems: 1, + uniqueItems: true + } + }, + additionalProperties: false + } + ], + hasSuggestions: true, + messages: { + unexpected: "Unexpected 'debugger' statement." + } + } + }; + + const options: ISerifFormatterOptions = { + ignoreSuppressed: false, + eslintVersion: '7.32.0', + buildFolderPath: '/' + }; + + const sarifLog = await formatEslintResultsAsSARIF(mockLintResults, mockRulesMeta, options); + + expect(sarifLog).toMatchSnapshot(); + }); +}); diff --git a/heft-plugins/heft-lint-plugin/src/test/__snapshots__/SarifFormatter.test.ts.snap b/heft-plugins/heft-lint-plugin/src/test/__snapshots__/SarifFormatter.test.ts.snap new file mode 100644 index 0000000000..5460eb4c2b --- /dev/null +++ b/heft-plugins/heft-lint-plugin/src/test/__snapshots__/SarifFormatter.test.ts.snap @@ -0,0 +1,622 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`formatEslintResultsAsSARIF case with no files 1`] = ` +Object { + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.5", + "runs": Array [ + Object { + "results": Array [], + "tool": Object { + "driver": Object { + "informationUri": "https://eslint.org", + "name": "ESLint", + "rules": Array [], + "version": "7.32.0", + }, + }, + }, + ], + "version": "2.1.0", +} +`; + +exports[`formatEslintResultsAsSARIF case with single issues in the same file 1`] = ` +Object { + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.5", + "runs": Array [ + Object { + "artifacts": Array [ + Object { + "location": Object { + "uri": "src/file1.ts", + }, + }, + ], + "results": Array [ + Object { + "level": "error", + "locations": Array [ + Object { + "physicalLocation": Object { + "artifactLocation": Object { + "index": 0, + "uri": "src/file1.ts", + }, + "region": Object { + "endColumn": 6, + "endLine": 10, + "snippet": Object { + "text": "const x = 1;", + }, + "startColumn": 5, + "startLine": 10, + }, + }, + }, + ], + "message": Object { + "text": "'x' is defined but never used.", + }, + "ruleId": "no-unused-vars", + "ruleIndex": 0, + }, + ], + "tool": Object { + "driver": Object { + "informationUri": "https://eslint.org", + "name": "ESLint", + "rules": Array [ + Object { + "helpUri": "https://eslint.org/docs/latest/rules/no-unused-vars", + "id": "no-unused-vars", + "properties": Object { + "category": undefined, + }, + "shortDescription": Object { + "text": "'x' is defined but never used.", + }, + }, + ], + "version": "7.32.0", + }, + }, + }, + ], + "version": "2.1.0", +} +`; + +exports[`formatEslintResultsAsSARIF should correctly format ESLint results into SARIF log 1`] = ` +Object { + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.5", + "runs": Array [ + Object { + "artifacts": Array [ + Object { + "location": Object { + "uri": "src/file1.ts", + }, + }, + ], + "results": Array [ + Object { + "level": "error", + "locations": Array [ + Object { + "physicalLocation": Object { + "artifactLocation": Object { + "index": 0, + "uri": "src/file1.ts", + }, + "region": Object { + "endColumn": 6, + "endLine": 10, + "snippet": Object { + "text": "const x = 1;", + }, + "startColumn": 5, + "startLine": 10, + }, + }, + }, + ], + "message": Object { + "text": "'x' is defined but never used.", + }, + "ruleId": "no-unused-vars", + "ruleIndex": 0, + }, + ], + "tool": Object { + "driver": Object { + "informationUri": "https://eslint.org", + "name": "ESLint", + "rules": Array [ + Object { + "helpUri": "https://eslint.org/docs/latest/rules/no-unused-vars", + "id": "no-unused-vars", + "properties": Object { + "category": undefined, + }, + "shortDescription": Object { + "text": "'x' is defined but never used.", + }, + }, + ], + "version": "7.32.0", + }, + }, + }, + ], + "version": "2.1.0", +} +`; + +exports[`formatEslintResultsAsSARIF should handle a file with no messages 1`] = ` +Object { + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.5", + "runs": Array [ + Object { + "artifacts": Array [ + Object { + "location": Object { + "uri": "src/file3.ts", + }, + }, + ], + "results": Array [], + "tool": Object { + "driver": Object { + "informationUri": "https://eslint.org", + "name": "ESLint", + "rules": Array [], + "version": "7.32.0", + }, + }, + }, + ], + "version": "2.1.0", +} +`; + +exports[`formatEslintResultsAsSARIF should handle ignoreSuppressed: false with suppressed messages 1`] = ` +Object { + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.5", + "runs": Array [ + Object { + "artifacts": Array [ + Object { + "location": Object { + "uri": "src/file4.ts", + }, + }, + ], + "results": Array [ + Object { + "level": "error", + "locations": Array [ + Object { + "physicalLocation": Object { + "artifactLocation": Object { + "index": 0, + "uri": "src/file4.ts", + }, + "region": Object { + "endColumn": 9, + "endLine": 20, + "snippet": Object { + "text": "debugger;", + }, + "startColumn": 1, + "startLine": 20, + }, + }, + }, + ], + "message": Object { + "text": "Unexpected 'debugger' statement.", + }, + "ruleId": "no-debugger", + "ruleIndex": 0, + "suppressions": Array [], + }, + Object { + "level": "warning", + "locations": Array [ + Object { + "physicalLocation": Object { + "artifactLocation": Object { + "index": 0, + "uri": "src/file4.ts", + }, + "region": Object { + "endColumn": 16, + "endLine": 10, + "snippet": Object { + "text": "console.log(\\"test\\");", + }, + "startColumn": 5, + "startLine": 10, + }, + }, + }, + ], + "message": Object { + "text": "Unexpected console statement.", + }, + "ruleId": "no-console", + "ruleIndex": 1, + "suppressions": Array [ + Object { + "justification": "rejected", + "kind": "external", + }, + ], + }, + ], + "tool": Object { + "driver": Object { + "informationUri": "https://eslint.org", + "name": "ESLint", + "rules": Array [ + Object { + "helpUri": "https://eslint.org/docs/latest/rules/no-debugger", + "id": "no-debugger", + "properties": Object { + "category": undefined, + }, + "shortDescription": Object { + "text": "Disallow the use of debugger", + }, + }, + Object { + "helpUri": "https://eslint.org/docs/latest/rules/no-console", + "id": "no-console", + "properties": Object { + "category": undefined, + }, + "shortDescription": Object { + "text": "Disallow the use of \`console\`", + }, + }, + ], + "version": "7.32.0", + }, + }, + }, + ], + "version": "2.1.0", +} +`; + +exports[`formatEslintResultsAsSARIF should handle ignoreSuppressed: true with suppressed messages 1`] = ` +Object { + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.5", + "runs": Array [ + Object { + "artifacts": Array [ + Object { + "location": Object { + "uri": "src/file4.ts", + }, + }, + ], + "results": Array [ + Object { + "level": "error", + "locations": Array [ + Object { + "physicalLocation": Object { + "artifactLocation": Object { + "index": 0, + "uri": "src/file4.ts", + }, + "region": Object { + "endColumn": 9, + "endLine": 20, + "snippet": Object { + "text": "debugger;", + }, + "startColumn": 1, + "startLine": 20, + }, + }, + }, + ], + "message": Object { + "text": "Unexpected 'debugger' statement.", + }, + "ruleId": "no-debugger", + "ruleIndex": 0, + }, + ], + "tool": Object { + "driver": Object { + "informationUri": "https://eslint.org", + "name": "ESLint", + "rules": Array [ + Object { + "helpUri": "https://eslint.org/docs/latest/rules/no-debugger", + "id": "no-debugger", + "properties": Object { + "category": undefined, + }, + "shortDescription": Object { + "text": "Disallow the use of debugger", + }, + }, + ], + "version": "7.32.0", + }, + }, + }, + ], + "version": "2.1.0", +} +`; + +exports[`formatEslintResultsAsSARIF should handle messages without file locations 1`] = ` +Object { + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.5", + "runs": Array [ + Object { + "artifacts": Array [ + Object { + "location": Object { + "uri": "src/file5.ts", + }, + }, + ], + "results": Array [ + Object { + "level": "warning", + "locations": Array [ + Object { + "physicalLocation": Object { + "artifactLocation": Object { + "index": 0, + "uri": "src/file5.ts", + }, + "region": Object { + "endColumn": undefined, + "endLine": undefined, + "snippet": Object { + "text": "console.log(\\"test\\");", + }, + "startColumn": 5, + "startLine": 10, + }, + }, + }, + ], + "message": Object { + "text": "Unexpected console statement.", + }, + "ruleId": "no-console", + "ruleIndex": 0, + }, + ], + "tool": Object { + "driver": Object { + "informationUri": "https://eslint.org", + "name": "ESLint", + "rules": Array [ + Object { + "helpUri": "https://eslint.org/docs/latest/rules/no-console", + "id": "no-console", + "properties": Object { + "category": undefined, + }, + "shortDescription": Object { + "text": "Disallow the use of \`console\`", + }, + }, + ], + "version": "7.32.0", + }, + }, + }, + ], + "version": "2.1.0", +} +`; + +exports[`formatEslintResultsAsSARIF should handle multiple files 1`] = ` +Object { + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.5", + "runs": Array [ + Object { + "artifacts": Array [ + Object { + "location": Object { + "uri": "src/file1.ts", + }, + }, + Object { + "location": Object { + "uri": "src/file2.ts", + }, + }, + ], + "results": Array [ + Object { + "level": "error", + "locations": Array [ + Object { + "physicalLocation": Object { + "artifactLocation": Object { + "index": 0, + "uri": "src/file1.ts", + }, + "region": Object { + "endColumn": 6, + "endLine": 10, + "snippet": Object { + "text": "const x = 1;", + }, + "startColumn": 5, + "startLine": 10, + }, + }, + }, + ], + "message": Object { + "text": "'x' is defined but never used.", + }, + "ruleId": "no-unused-vars", + }, + Object { + "level": "error", + "locations": Array [ + Object { + "physicalLocation": Object { + "artifactLocation": Object { + "index": 1, + "uri": "src/file2.ts", + }, + "region": Object { + "endColumn": 10, + "endLine": 15, + "snippet": Object { + "text": "if (a == b) { }", + }, + "startColumn": 8, + "startLine": 15, + }, + }, + }, + ], + "message": Object { + "text": "Expected '===' and instead saw '=='.", + }, + "ruleId": "eqeqeq", + "ruleIndex": 0, + }, + ], + "tool": Object { + "driver": Object { + "informationUri": "https://eslint.org", + "name": "ESLint", + "rules": Array [ + Object { + "helpUri": "https://eslint.org/docs/latest/rules/eqeqeq", + "id": "eqeqeq", + "properties": Object { + "category": undefined, + }, + "shortDescription": Object { + "text": "Require the use of === and !==", + }, + }, + ], + "version": "7.32.0", + }, + }, + }, + ], + "version": "2.1.0", +} +`; + +exports[`formatEslintResultsAsSARIF should handle multiple issues in the same file 1`] = ` +Object { + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.5", + "runs": Array [ + Object { + "artifacts": Array [ + Object { + "location": Object { + "uri": "src/file2.ts", + }, + }, + ], + "results": Array [ + Object { + "level": "error", + "locations": Array [ + Object { + "physicalLocation": Object { + "artifactLocation": Object { + "index": 0, + "uri": "src/file2.ts", + }, + "region": Object { + "endColumn": 11, + "endLine": 5, + "snippet": Object { + "text": "let x;", + }, + "startColumn": 10, + "startLine": 5, + }, + }, + }, + ], + "message": Object { + "text": "'x' is defined but never used.", + }, + "ruleId": "no-unused-vars", + "ruleIndex": 0, + }, + Object { + "level": "warning", + "locations": Array [ + Object { + "physicalLocation": Object { + "artifactLocation": Object { + "index": 0, + "uri": "src/file2.ts", + }, + "region": Object { + "endColumn": 16, + "endLine": 10, + "snippet": Object { + "text": "console.log(\\"test\\");", + }, + "startColumn": 5, + "startLine": 10, + }, + }, + }, + ], + "message": Object { + "text": "Unexpected console statement.", + }, + "ruleId": "no-console", + "ruleIndex": 1, + }, + ], + "tool": Object { + "driver": Object { + "informationUri": "https://eslint.org", + "name": "ESLint", + "rules": Array [ + Object { + "helpUri": "https://eslint.org/docs/latest/rules/no-unused-vars", + "id": "no-unused-vars", + "properties": Object { + "category": undefined, + }, + "shortDescription": Object { + "text": "'x' is defined but never used.", + }, + }, + Object { + "helpUri": "https://eslint.org/docs/latest/rules/no-console", + "id": "no-console", + "properties": Object { + "category": undefined, + }, + "shortDescription": Object { + "text": "Disallow the use of \`console\`", + }, + }, + ], + "version": "7.32.0", + }, + }, + }, + ], + "version": "2.1.0", +} +`; diff --git a/heft-plugins/heft-lint-plugin/tsconfig.json b/heft-plugins/heft-lint-plugin/tsconfig.json index a66d877424..e7de6e2eef 100644 --- a/heft-plugins/heft-lint-plugin/tsconfig.json +++ b/heft-plugins/heft-lint-plugin/tsconfig.json @@ -3,6 +3,6 @@ "compilerOptions": { "isolatedModules": true, - "types": ["node"] + "types": ["heft-jest", "node"] } }