Skip to content

Commit

Permalink
Implement SARIF formatter for Eslint (#4956)
Browse files Browse the repository at this point in the history
* [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
  • Loading branch information
atingmicrosoft authored Oct 9, 2024
1 parent 982df68 commit 2242751
Show file tree
Hide file tree
Showing 22 changed files with 1,751 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/eslint-patch",
"comment": "",
"type": "none"
}
],
"packageName": "@rushstack/eslint-patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/eslint-plugin-packlets",
"comment": "",
"type": "none"
}
],
"packageName": "@rushstack/eslint-plugin-packlets"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/eslint-plugin-security",
"comment": "",
"type": "none"
}
],
"packageName": "@rushstack/eslint-plugin-security"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/eslint-plugin",
"comment": "",
"type": "none"
}
],
"packageName": "@rushstack/eslint-plugin"
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
{
"pnpmShrinkwrapHash": "5b75a8ef91af53a8caf52319e5eb0042c4d06852",
"preferredVersionsHash": "ce857ea0536b894ec8f346aaea08cfd85a5af648",
"packageJsonInjectedDependenciesHash": "8927ca4e0147b9436659f98a2ff8ca347107d52f"
"packageJsonInjectedDependenciesHash": "5ff2fabbffcfb22bb3e29f56c997f7c762e89d20"
}
32 changes: 16 additions & 16 deletions common/config/subspaces/default/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion common/config/subspaces/default/repo-state.json
Original file line number Diff line number Diff line change
@@ -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"
}
2 changes: 1 addition & 1 deletion eslint/eslint-patch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
2 changes: 1 addition & 1 deletion eslint/eslint-plugin-packlets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion eslint/eslint-plugin-security/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion eslint/eslint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion heft-plugins/heft-lint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
56 changes: 38 additions & 18 deletions heft-plugins/heft-lint-plugin/src/Eslint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -59,12 +59,22 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult> {
private readonly _currentFixMessages: TEslint.Linter.LintMessage[] = [];
private readonly _fixMessagesByResult: Map<TEslint.ESLint.LintResult, TEslint.Linter.LintMessage[]> =
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<TEslint.ESLint.Options['fix'], boolean>;
Expand Down Expand Up @@ -166,17 +176,10 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult> {
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<void> {
protected async lintingFinishedAsync(lintResults: TEslint.ESLint.LintResult[]): Promise<void> {
let omittedRuleCount: number = 0;
const timings: [string, number][] = Array.from(this._eslintTimings).sort(
(x: [string, number], y: [string, number]) => {
Expand All @@ -196,24 +199,23 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult> {
}

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);
Expand All @@ -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> {
Expand Down
12 changes: 11 additions & 1 deletion heft-plugins/heft-lint-plugin/src/LintPlugin.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -28,13 +30,15 @@ const ESLINTRC_CJS_FILENAME: string = '.eslintrc.cjs';

interface ILintPluginOptions {
alwaysFix?: boolean;
sarifLogPath?: string;
}

interface ILintOptions {
taskSession: IHeftTaskSession;
heftConfiguration: HeftConfiguration;
tsProgram: IExtendedProgram;
fix?: boolean;
sarifLogPath?: string;
changedFiles?: ReadonlySet<IExtendedSourceFile>;
}

Expand Down Expand Up @@ -67,6 +71,10 @@ export default class LintPlugin implements IHeftTaskPlugin<ILintPluginOptions> {
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',
Expand All @@ -80,6 +88,7 @@ export default class LintPlugin implements IHeftTaskPlugin<ILintPluginOptions> {
taskSession,
heftConfiguration,
fix,
sarifLogPath,
tsProgram: changedFilesHookOptions.program as IExtendedProgram,
changedFiles: changedFilesHookOptions.changedFiles as ReadonlySet<IExtendedSourceFile>
});
Expand Down Expand Up @@ -144,7 +153,7 @@ export default class LintPlugin implements IHeftTaskPlugin<ILintPluginOptions> {
}

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
// multiple times will only init once.
Expand All @@ -155,6 +164,7 @@ export default class LintPlugin implements IHeftTaskPlugin<ILintPluginOptions> {
const eslintLinter: Eslint = await Eslint.initializeAsync({
tsProgram,
fix,
sarifLogPath,
scopedLogger: taskSession.logger,
linterToolPath: this._eslintToolPath,
linterConfigFilePath: this._eslintConfigFilePath,
Expand Down
Loading

0 comments on commit 2242751

Please sign in to comment.