Implement SARIF formatter for Eslint (#4956)

* [heft-lint-plugin] Support outputting SARIF logs

* fixed type issues in typescript for Sarifformmater. Upgrade @types/eslint to version 18.56.10

* PR fixes for creating SARIF Formatter

* rush update

* rush change

* rush update

* rush update

* updated test snapshot

* rush change

* pnpm file fix

* updated snapshot

* 2nd round PR fixes for sarifFormatter

* resolve repo-state.json merge conflict

* deleted change file for webpack5-localization-plugin. Changes were reverted.

* PR changes 3 for implementing SARIF formatter

* 3rd round PR changes for SARIF formatter

* made IRegion interface exportable

* created snapshot tests and updated schema descriptions

* minor fixes and improvements to formatEslintResultsAsSARIF function

* edited rush changes

* fixed functionality of sarifFormmater to properly track rule and artifact indicies. Added more snapshot tests. URL tracked in sariff formatter are relative vs absolute. Rules Meta now included in sarif file

* fixed bug with importing linter

* imporved efficiency of formatAsASARIF function
This commit is contained in:
atingmicrosoft 2024-10-09 13:06:46 -07:00 коммит произвёл GitHub
Родитель 982df68fdb
Коммит 2242751217
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
22 изменённых файлов: 1751 добавлений и 50 удалений

Просмотреть файл

@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/eslint-patch",
"comment": "",
"type": "none"
}
],
"packageName": "@rushstack/eslint-patch"
}

Просмотреть файл

@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/eslint-plugin-packlets",
"comment": "",
"type": "none"
}
],
"packageName": "@rushstack/eslint-plugin-packlets"
}

Просмотреть файл

@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/eslint-plugin-security",
"comment": "",
"type": "none"
}
],
"packageName": "@rushstack/eslint-plugin-security"
}

Просмотреть файл

@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/eslint-plugin",
"comment": "",
"type": "none"
}
],
"packageName": "@rushstack/eslint-plugin"
}

Просмотреть файл

@ -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"
}

Просмотреть файл

@ -2,5 +2,5 @@
{ {
"pnpmShrinkwrapHash": "5b75a8ef91af53a8caf52319e5eb0042c4d06852", "pnpmShrinkwrapHash": "5b75a8ef91af53a8caf52319e5eb0042c4d06852",
"preferredVersionsHash": "ce857ea0536b894ec8f346aaea08cfd85a5af648", "preferredVersionsHash": "ce857ea0536b894ec8f346aaea08cfd85a5af648",
"packageJsonInjectedDependenciesHash": "8927ca4e0147b9436659f98a2ff8ca347107d52f" "packageJsonInjectedDependenciesHash": "5ff2fabbffcfb22bb3e29f56c997f7c762e89d20"
} }

Просмотреть файл

@ -2415,8 +2415,8 @@ importers:
specifier: 2.6.31 specifier: 2.6.31
version: 2.6.31(@rushstack/heft@0.67.2)(@types/node@18.17.15)(supports-color@8.1.1) version: 2.6.31(@rushstack/heft@0.67.2)(@types/node@18.17.15)(supports-color@8.1.1)
'@types/eslint': '@types/eslint':
specifier: 8.2.0 specifier: 8.56.10
version: 8.2.0 version: 8.56.10
'@types/node': '@types/node':
specifier: 18.17.15 specifier: 18.17.15
version: 18.17.15 version: 18.17.15
@ -2452,8 +2452,8 @@ importers:
specifier: 2.6.31 specifier: 2.6.31
version: 2.6.31(@rushstack/heft@0.67.2)(@types/node@18.17.15)(supports-color@8.1.1) version: 2.6.31(@rushstack/heft@0.67.2)(@types/node@18.17.15)(supports-color@8.1.1)
'@types/eslint': '@types/eslint':
specifier: 8.2.0 specifier: 8.56.10
version: 8.2.0 version: 8.56.10
'@types/estree': '@types/estree':
specifier: 1.0.5 specifier: 1.0.5
version: 1.0.5 version: 1.0.5
@ -2498,8 +2498,8 @@ importers:
specifier: 2.6.31 specifier: 2.6.31
version: 2.6.31(@rushstack/heft@0.67.2)(@types/node@18.17.15)(supports-color@8.1.1) version: 2.6.31(@rushstack/heft@0.67.2)(@types/node@18.17.15)(supports-color@8.1.1)
'@types/eslint': '@types/eslint':
specifier: 8.2.0 specifier: 8.56.10
version: 8.2.0 version: 8.56.10
'@types/estree': '@types/estree':
specifier: 1.0.5 specifier: 1.0.5
version: 1.0.5 version: 1.0.5
@ -2544,8 +2544,8 @@ importers:
specifier: 2.6.31 specifier: 2.6.31
version: 2.6.31(@rushstack/heft@0.67.2)(@types/node@18.17.15)(supports-color@8.1.1) version: 2.6.31(@rushstack/heft@0.67.2)(@types/node@18.17.15)(supports-color@8.1.1)
'@types/eslint': '@types/eslint':
specifier: 8.2.0 specifier: 8.56.10
version: 8.2.0 version: 8.56.10
'@types/estree': '@types/estree':
specifier: 1.0.5 specifier: 1.0.5
version: 1.0.5 version: 1.0.5
@ -2759,8 +2759,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../../libraries/terminal version: link:../../libraries/terminal
'@types/eslint': '@types/eslint':
specifier: 8.2.0 specifier: 8.56.10
version: 8.2.0 version: 8.56.10
'@types/heft-jest': '@types/heft-jest':
specifier: 1.0.1 specifier: 1.0.1
version: 1.0.1 version: 1.0.1
@ -9023,7 +9023,7 @@ packages:
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies: dependencies:
'@jest/types': 29.6.3 '@jest/types': 29.6.3
'@types/node': 20.12.12 '@types/node': 17.0.41
chalk: 4.1.2 chalk: 4.1.2
jest-message-util: 29.7.0 jest-message-util: 29.7.0
jest-util: 29.7.0 jest-util: 29.7.0
@ -12918,8 +12918,8 @@ packages:
resolution: {integrity: sha512-XIpxU6Qdvp1ZE6Kr3yrkv1qgUab0fyf4mHYvW8N3Bx3PCsbN6or1q9/q72cv5jIFWolaGH08U9XyYoLLIykyKQ==} resolution: {integrity: sha512-XIpxU6Qdvp1ZE6Kr3yrkv1qgUab0fyf4mHYvW8N3Bx3PCsbN6or1q9/q72cv5jIFWolaGH08U9XyYoLLIykyKQ==}
dev: true dev: true
/@types/eslint@8.2.0: /@types/eslint@8.56.10:
resolution: {integrity: sha512-74hbvsnc+7TEDa1z5YLSe4/q8hGYB3USNvCuzHUJrjPV6hXaq8IXcngCrHkuvFt0+8rFz7xYXrHgNayIX0UZvQ==} resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==}
dependencies: dependencies:
'@types/estree': 1.0.5 '@types/estree': 1.0.5
'@types/json-schema': 7.0.15 '@types/json-schema': 7.0.15
@ -21637,7 +21637,7 @@ packages:
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies: dependencies:
'@jest/types': 29.6.3 '@jest/types': 29.6.3
'@types/node': 20.12.12 '@types/node': 17.0.41
chalk: 4.1.2 chalk: 4.1.2
ci-info: 3.9.0 ci-info: 3.9.0
graceful-fs: 4.2.11 graceful-fs: 4.2.11
@ -21679,7 +21679,7 @@ packages:
resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
engines: {node: '>= 10.13.0'} engines: {node: '>= 10.13.0'}
dependencies: dependencies:
'@types/node': 20.12.12 '@types/node': 17.0.41
merge-stream: 2.0.0 merge-stream: 2.0.0
supports-color: 7.2.0 supports-color: 7.2.0
dev: true dev: true
@ -21696,7 +21696,7 @@ packages:
resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies: dependencies:
'@types/node': 20.12.12 '@types/node': 17.0.41
jest-util: 29.7.0 jest-util: 29.7.0
merge-stream: 2.0.0 merge-stream: 2.0.0
supports-color: 8.1.1 supports-color: 8.1.1

Просмотреть файл

@ -1,5 +1,5 @@
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
{ {
"pnpmShrinkwrapHash": "5afee1d392a0c2404869d7eda687b5571fe88515", "pnpmShrinkwrapHash": "673f7de41244835915a7947c11b6dbe80f944010",
"preferredVersionsHash": "ce857ea0536b894ec8f346aaea08cfd85a5af648" "preferredVersionsHash": "ce857ea0536b894ec8f346aaea08cfd85a5af648"
} }

Просмотреть файл

@ -32,7 +32,7 @@
"devDependencies": { "devDependencies": {
"@rushstack/heft": "0.67.2", "@rushstack/heft": "0.67.2",
"@rushstack/heft-node-rig": "2.6.31", "@rushstack/heft-node-rig": "2.6.31",
"@types/eslint": "8.2.0", "@types/eslint": "8.56.10",
"@types/node": "18.17.15", "@types/node": "18.17.15",
"@typescript-eslint/types": "~5.59.2", "@typescript-eslint/types": "~5.59.2",
"eslint": "~8.57.0", "eslint": "~8.57.0",

Просмотреть файл

@ -35,7 +35,7 @@ export async function runEslintAsync(files: string[], mode: 'suppress' | 'prune'
if (results.length > 0) { if (results.length > 0) {
const stylishFormatter: ESLint.Formatter = await eslint.loadFormatter(); 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); console.log(formattedResults);
} }

Просмотреть файл

@ -32,7 +32,7 @@
"devDependencies": { "devDependencies": {
"@rushstack/heft": "0.67.2", "@rushstack/heft": "0.67.2",
"@rushstack/heft-node-rig": "2.6.31", "@rushstack/heft-node-rig": "2.6.31",
"@types/eslint": "8.2.0", "@types/eslint": "8.56.10",
"@types/estree": "1.0.5", "@types/estree": "1.0.5",
"@types/heft-jest": "1.0.1", "@types/heft-jest": "1.0.1",
"@types/node": "18.17.15", "@types/node": "18.17.15",

Просмотреть файл

@ -32,7 +32,7 @@
"@eslint/eslintrc": "~3.0.0", "@eslint/eslintrc": "~3.0.0",
"@rushstack/heft": "0.67.2", "@rushstack/heft": "0.67.2",
"@rushstack/heft-node-rig": "2.6.31", "@rushstack/heft-node-rig": "2.6.31",
"@types/eslint": "8.2.0", "@types/eslint": "8.56.10",
"@types/estree": "1.0.5", "@types/estree": "1.0.5",
"@types/heft-jest": "1.0.1", "@types/heft-jest": "1.0.1",
"@types/node": "18.17.15", "@types/node": "18.17.15",

Просмотреть файл

@ -36,7 +36,7 @@
"@eslint/eslintrc": "~3.0.0", "@eslint/eslintrc": "~3.0.0",
"@rushstack/heft": "0.67.2", "@rushstack/heft": "0.67.2",
"@rushstack/heft-node-rig": "2.6.31", "@rushstack/heft-node-rig": "2.6.31",
"@types/eslint": "8.2.0", "@types/eslint": "8.56.10",
"@types/estree": "1.0.5", "@types/estree": "1.0.5",
"@types/heft-jest": "1.0.1", "@types/heft-jest": "1.0.1",
"@types/node": "18.17.15", "@types/node": "18.17.15",

Просмотреть файл

@ -27,7 +27,7 @@
"@rushstack/heft-typescript-plugin": "workspace:*", "@rushstack/heft-typescript-plugin": "workspace:*",
"@rushstack/heft-node-rig": "2.6.31", "@rushstack/heft-node-rig": "2.6.31",
"@rushstack/terminal": "workspace:*", "@rushstack/terminal": "workspace:*",
"@types/eslint": "8.2.0", "@types/eslint": "8.56.10",
"@types/heft-jest": "1.0.1", "@types/heft-jest": "1.0.1",
"@types/node": "18.17.15", "@types/node": "18.17.15",
"@types/semver": "7.5.0", "@types/semver": "7.5.0",

Просмотреть файл

@ -6,7 +6,7 @@ import * as semver from 'semver';
import type * as TTypescript from 'typescript'; import type * as TTypescript from 'typescript';
import type * as TEslint from 'eslint'; import type * as TEslint from 'eslint';
import { performance } from 'perf_hooks'; 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'; import { LinterBase, type ILinterBaseOptions } from './LinterBase';
@ -59,12 +59,22 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult> {
private readonly _currentFixMessages: TEslint.Linter.LintMessage[] = []; private readonly _currentFixMessages: TEslint.Linter.LintMessage[] = [];
private readonly _fixMessagesByResult: Map<TEslint.ESLint.LintResult, TEslint.Linter.LintMessage[]> = private readonly _fixMessagesByResult: Map<TEslint.ESLint.LintResult, TEslint.Linter.LintMessage[]> =
new Map(); new Map();
private readonly _sarifLogPath: string | undefined;
protected constructor(options: IEslintOptions) { protected constructor(options: IEslintOptions) {
super('eslint', options); super('eslint', options);
const { buildFolderPath, eslintPackage, linterConfigFilePath, tsProgram, eslintTimings, fix } = options; const {
buildFolderPath,
eslintPackage,
linterConfigFilePath,
tsProgram,
eslintTimings,
fix,
sarifLogPath
} = options;
this._eslintPackage = eslintPackage; this._eslintPackage = eslintPackage;
this._sarifLogPath = sarifLogPath;
let overrideConfig: TEslint.Linter.Config | undefined; let overrideConfig: TEslint.Linter.Config | undefined;
let fixFn: Exclude<TEslint.ESLint.Options['fix'], boolean>; let fixFn: Exclude<TEslint.ESLint.Options['fix'], boolean>;
@ -166,17 +176,10 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult> {
return lintResult.fixableErrorCount + lintResult.fixableWarningCount > 0; return lintResult.fixableErrorCount + lintResult.fixableWarningCount > 0;
})); }));
const failures: TEslint.ESLint.LintResult[] = []; return lintResults;
for (const lintResult of lintResults) {
if (lintResult.messages.length > 0 || lintResult.output) {
failures.push(lintResult);
}
}
return failures;
} }
protected async lintingFinishedAsync(lintFailures: TEslint.ESLint.LintResult[]): Promise<void> { protected async lintingFinishedAsync(lintResults: TEslint.ESLint.LintResult[]): Promise<void> {
let omittedRuleCount: number = 0; let omittedRuleCount: number = 0;
const timings: [string, number][] = Array.from(this._eslintTimings).sort( const timings: [string, number][] = Array.from(this._eslintTimings).sort(
(x: [string, number], y: [string, number]) => { (x: [string, number], y: [string, number]) => {
@ -196,24 +199,23 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult> {
} }
if (this._fix && this._fixMessagesByResult.size > 0) { 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 // Report linter fixes to the logger. These will only be returned when the underlying failure was fixed
const fixMessages: TEslint.Linter.LintMessage[] | undefined = const fixMessages: TEslint.Linter.LintMessage[] | undefined = this._fixMessagesByResult.get(lintResult);
this._fixMessagesByResult.get(lintFailure);
if (fixMessages) { if (fixMessages) {
for (const fixMessage of fixMessages) { for (const fixMessage of fixMessages) {
const formattedMessage: string = `[FIXED] ${getFormattedErrorMessage(fixMessage)}`; 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); this._scopedLogger.emitWarning(errorObject);
} }
} }
// Report linter errors and warnings to the logger // Report linter errors and warnings to the logger
for (const lintMessage of lintFailure.messages) { for (const lintMessage of lintResult.messages) {
const errorObject: FileError = this._getLintFileError(lintFailure, lintMessage); const errorObject: FileError = this._getLintFileError(lintResult, lintMessage);
switch (lintMessage.severity) { switch (lintMessage.severity) {
case EslintMessageSeverity.error: { case EslintMessageSeverity.error: {
this._scopedLogger.emitError(errorObject); this._scopedLogger.emitError(errorObject);
@ -227,6 +229,24 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult> {
} }
} }
} }
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<boolean> { protected async isFileExcludedAsync(filePath: string): Promise<boolean> {

Просмотреть файл

@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information. // See LICENSE in the project root for license information.
import path from 'node:path';
import { FileSystem } from '@rushstack/node-core-library'; import { FileSystem } from '@rushstack/node-core-library';
import type { import type {
HeftConfiguration, HeftConfiguration,
@ -28,6 +30,7 @@ const ESLINTRC_CJS_FILENAME: string = '.eslintrc.cjs';
interface ILintPluginOptions { interface ILintPluginOptions {
alwaysFix?: boolean; alwaysFix?: boolean;
sarifLogPath?: string;
} }
interface ILintOptions { interface ILintOptions {
@ -35,6 +38,7 @@ interface ILintOptions {
heftConfiguration: HeftConfiguration; heftConfiguration: HeftConfiguration;
tsProgram: IExtendedProgram; tsProgram: IExtendedProgram;
fix?: boolean; fix?: boolean;
sarifLogPath?: string;
changedFiles?: ReadonlySet<IExtendedSourceFile>; changedFiles?: ReadonlySet<IExtendedSourceFile>;
} }
@ -67,6 +71,10 @@ export default class LintPlugin implements IHeftTaskPlugin<ILintPluginOptions> {
fix = false; 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 // Use the changed files hook to kick off linting asynchronously
taskSession.requestAccessToPluginByName( taskSession.requestAccessToPluginByName(
'@rushstack/heft-typescript-plugin', '@rushstack/heft-typescript-plugin',
@ -80,6 +88,7 @@ export default class LintPlugin implements IHeftTaskPlugin<ILintPluginOptions> {
taskSession, taskSession,
heftConfiguration, heftConfiguration,
fix, fix,
sarifLogPath,
tsProgram: changedFilesHookOptions.program as IExtendedProgram, tsProgram: changedFilesHookOptions.program as IExtendedProgram,
changedFiles: changedFilesHookOptions.changedFiles as ReadonlySet<IExtendedSourceFile> changedFiles: changedFilesHookOptions.changedFiles as ReadonlySet<IExtendedSourceFile>
}); });
@ -144,7 +153,7 @@ export default class LintPlugin implements IHeftTaskPlugin<ILintPluginOptions> {
} }
private async _lintAsync(options: ILintOptions): Promise<void> { private async _lintAsync(options: ILintOptions): Promise<void> {
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 // Ensure that we have initialized. This promise is cached, so calling init
// multiple times will only init once. // multiple times will only init once.
@ -155,6 +164,7 @@ export default class LintPlugin implements IHeftTaskPlugin<ILintPluginOptions> {
const eslintLinter: Eslint = await Eslint.initializeAsync({ const eslintLinter: Eslint = await Eslint.initializeAsync({
tsProgram, tsProgram,
fix, fix,
sarifLogPath,
scopedLogger: taskSession.logger, scopedLogger: taskSession.logger,
linterToolPath: this._eslintToolPath, linterToolPath: this._eslintToolPath,
linterConfigFilePath: this._eslintConfigFilePath, linterConfigFilePath: this._eslintConfigFilePath,

Просмотреть файл

@ -21,6 +21,7 @@ export interface ILinterBaseOptions {
linterConfigFilePath: string; linterConfigFilePath: string;
tsProgram: IExtendedProgram; tsProgram: IExtendedProgram;
fix?: boolean; fix?: boolean;
sarifLogPath?: string;
} }
export interface IRunLinterOptions { export interface IRunLinterOptions {
@ -128,7 +129,7 @@ export abstract class LinterBase<TLintResult> {
// Some of this code comes from here: // Some of this code comes from here:
// https://github.com/palantir/tslint/blob/24d29e421828348f616bf761adb3892bcdf51662/src/linter.ts#L161-L179 // 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 // 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()) { for (const sourceFile of options.tsProgram.getSourceFiles()) {
const filePath: string = sourceFile.fileName; const filePath: string = sourceFile.fileName;
const relative: string | undefined = relativePaths.get(filePath); const relative: string | undefined = relativePaths.get(filePath);
@ -147,12 +148,12 @@ export abstract class LinterBase<TLintResult> {
options.changedFiles.has(sourceFile) options.changedFiles.has(sourceFile)
) { ) {
fileCount++; fileCount++;
const failures: TLintResult[] = await this.lintFileAsync(sourceFile); const results: TLintResult[] = await this.lintFileAsync(sourceFile);
if (failures.length === 0) { if (results.length === 0) {
newNoFailureFileVersions.set(relative, version); newNoFailureFileVersions.set(relative, version);
} else { } else {
for (const failure of failures) { for (const result of results) {
lintFailures.push(failure); lintResults.push(result);
} }
} }
} else { } else {
@ -161,7 +162,7 @@ export abstract class LinterBase<TLintResult> {
} }
//#endregion //#endregion
await this.lintingFinishedAsync(lintFailures); await this.lintingFinishedAsync(lintResults);
if (!this._fix && this._fixesPossible) { if (!this._fix && this._fixesPossible) {
this._terminal.writeWarningLine( this._terminal.writeWarningLine(

Просмотреть файл

@ -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<string, number> = new Map();
const sarifRules: ISarifRule[] = [];
const sarifRuleIndices: Map<string, number> = 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;
}

Просмотреть файл

@ -11,6 +11,12 @@
"title": "Always Fix", "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.", "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" "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"
} }
} }
} }

Просмотреть файл

@ -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();
});
});

Просмотреть файл

@ -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",
}
`;

Просмотреть файл

@ -3,6 +3,6 @@
"compilerOptions": { "compilerOptions": {
"isolatedModules": true, "isolatedModules": true,
"types": ["node"] "types": ["heft-jest", "node"]
} }
} }