Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions build-tests/eslint-9-test/.eslint-bulk-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
"file": "src/index.ts",
"scopeId": ".",
"rule": "@typescript-eslint/naming-convention"
},
{
"file": "src/non-program.custom",
"scopeId": ".",
"rule": "no-undef"
}
]
}
9 changes: 9 additions & 0 deletions build-tests/eslint-9-test/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const nodeTrustedToolProfile = require('local-node-rig/profiles/default/includes
const friendlyLocalsMixin = require('local-node-rig/profiles/default/includes/eslint/flat/mixins/friendly-locals');

module.exports = [
{
ignores: ['coverage/**']
},
...nodeTrustedToolProfile,
...friendlyLocalsMixin,
{
Expand All @@ -25,5 +28,11 @@ module.exports = [
tsconfigRootDir: __dirname
}
}
},
{
files: ['**/*.custom'],
rules: {
'no-undef': 'warn'
}
}
];
43 changes: 43 additions & 0 deletions build-tests/eslint-9-test/src/__snapshots__/sarif.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ Object {
"uri": "src/sarif.test.ts",
},
},
Object {
"location": Object {
"uri": "src/non-program.custom",
},
},
],
"results": Array [
Object {
Expand Down Expand Up @@ -78,6 +83,36 @@ Object {
},
],
},
Object {
"level": "warning",
"locations": Array [
Object {
"physicalLocation": Object {
"artifactLocation": Object {
"index": 2,
"uri": "src/non-program.custom",
},
"region": Object {
"endColumn": 14,
"endLine": 1,
"startColumn": 1,
"startLine": 1,
},
},
},
],
"message": Object {
"text": "'missingGlobal' is not defined.",
},
"ruleId": "no-undef",
"ruleIndex": 2,
"suppressions": Array [
Object {
"justification": "",
"kind": "external",
},
],
},
],
"tool": Object {
"driver": Object {
Expand All @@ -100,6 +135,14 @@ Object {
"text": "Enforce naming conventions for everything across a codebase",
},
},
Object {
"helpUri": "https://eslint.org/docs/latest/rules/no-undef",
"id": "no-undef",
"properties": Object {},
"shortDescription": Object {
"text": "Disallow the use of undeclared variables unless mentioned in \`/*global */\` comments",
},
},
],
"version": "9.37.0",
},
Expand Down
1 change: 1 addition & 0 deletions build-tests/eslint-9-test/src/non-program.custom
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
missingGlobal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"changes": [
{
"packageName": "@rushstack/heft-lint-plugin",
"comment": "Lint files selected by ESLint flat config even when they are not part of the TypeScript program.",
"type": "minor"
}
]
}
122 changes: 61 additions & 61 deletions common/config/subspaces/build-tests-subspace/pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
{
"pnpmShrinkwrapHash": "2f7908424d103b2f677e95bcd5d85a385b75eda2",
"pnpmShrinkwrapHash": "dcbcfb259e8b79444cbdcba26d66a607fff9ce96",
"preferredVersionsHash": "550b4cee0bef4e97db6c6aad726df5149d20e7d9",
"packageJsonInjectedDependenciesHash": "e8fe4109038ad6e9b1e97cbb83e63d9094d37fe4"
}
310 changes: 155 additions & 155 deletions common/config/subspaces/default/pnpm-lock.yaml

Large diffs are not rendered by default.

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": "09068e8d50f8dae26938555320be968d7ed7861d",
"pnpmShrinkwrapHash": "f7863419452aa95244db5c362d58201a9620ee23",
"preferredVersionsHash": "029c99bd6e65c5e1f25e2848340509811ff9753c"
}
121 changes: 111 additions & 10 deletions heft-plugins/heft-lint-plugin/src/Eslint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import type * as TEslintLegacy from 'eslint-8';
import * as semver from 'semver';
import stableStringify from 'json-stable-stringify-without-jsonify';

import { FileError, FileSystem } from '@rushstack/node-core-library';
import { FileError, FileSystem, Path } from '@rushstack/node-core-library';
import type { HeftConfiguration } from '@rushstack/heft';

import { LinterBase, type ILinterBaseOptions } from './LinterBase';
import { LinterBase, type IAdditionalLintFile, type ILinterBaseOptions } from './LinterBase';
import type { IExtendedSourceFile } from './internalTypings/TypeScriptInternals';
import { name as pluginName, version as pluginVersion } from '../package.json';

Expand Down Expand Up @@ -81,20 +81,29 @@ const ESLINT_LEGACY_CONFIG_FILENAMES: Set<string> = new Set([
LEGACY_ESLINTRC_JS_FILENAME,
LEGACY_ESLINTRC_CJS_FILENAME
]);
const ESLINT_DEFAULT_EXTENSIONS: Set<string> = new Set(['.js', '.mjs', '.cjs']);

export class Eslint extends LinterBase<TEslint.ESLint.LintResult | TEslintLegacy.ESLint.LintResult> {
export class Eslint extends LinterBase<
TEslint.ESLint.LintResult | TEslintLegacy.ESLint.LintResult,
IAdditionalLintFile
> {
private readonly _eslintPackage: typeof TEslint | typeof TEslintLegacy;
private readonly _eslintPackageVersion: semver.SemVer;
private readonly _linter: TEslint.ESLint | TEslintLegacy.ESLint;
private readonly _additionalFilesLinter: TEslint.ESLint | undefined;
private readonly _eslintTimings: Map<string, number> = new Map();
private readonly _currentFixMessages: (TEslint.Linter.LintMessage | TEslintLegacy.Linter.LintMessage)[] =
[];
private readonly _fixMessagesByResult: Map<
TEslint.ESLint.LintResult | TEslintLegacy.ESLint.LintResult,
(TEslint.Linter.LintMessage | TEslintLegacy.Linter.LintMessage)[]
> = new Map();
private readonly _additionalLintResults: Set<TEslint.ESLint.LintResult | TEslintLegacy.ESLint.LintResult> =
new Set();
private readonly _sarifLogPath: string | undefined;
private readonly _configHashMap: WeakMap<object, string> = new WeakMap();
private readonly _fileEnumerator: TEslint.ESLint | undefined;
private readonly _typeScriptFilenames: ReadonlySet<string>;

protected constructor(options: IEslintOptions) {
super('eslint', options);
Expand All @@ -106,7 +115,8 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult | TEslintLegacy
tsProgram,
eslintTimings,
fix,
sarifLogPath
sarifLogPath,
additionalFileIgnorePatterns
} = options;
this._eslintPackage = eslintPackage;
this._eslintPackageVersion = new semver.SemVer(eslintPackage.ESLint.version);
Expand Down Expand Up @@ -195,6 +205,32 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult | TEslintLegacy
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fix: fixFn as any
});

this._typeScriptFilenames = new Set(
tsProgram.getRootFileNames().map((filePath: string) => path.resolve(filePath))
);
if (this._eslintPackageVersion.major >= 9) {
const typeScriptIgnorePatterns: string[] = Array.from(this._typeScriptFilenames, (filePath: string) =>
Path.convertToSlashes(path.relative(buildFolderPath, filePath))
).filter((relativePath: string) => relativePath !== '..' && !relativePath.startsWith('../'));
const flatEslintPackage: typeof TEslint = eslintPackage as typeof TEslint;
this._additionalFilesLinter = new flatEslintPackage.ESLint({
cwd: buildFolderPath,
overrideConfigFile: linterConfigFilePath,
fix: fixFn
});
this._fileEnumerator = new flatEslintPackage.ESLint({
cwd: buildFolderPath,
errorOnUnmatchedPattern: false,
overrideConfigFile: linterConfigFilePath,
overrideConfig: {
name: `${pluginName}/ignore-typescript-program-files`,
ignores: [...typeScriptIgnorePatterns, ...(additionalFileIgnorePatterns || [])]
},
ruleFilter: () => false
});
}

this._eslintTimings = eslintTimings;
}

Expand Down Expand Up @@ -250,12 +286,45 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult | TEslintLegacy
}
}

public async getAdditionalLintFilesAsync(): Promise<ReadonlySet<IAdditionalLintFile>> {
if (!this._fileEnumerator) {
return new Set();
}

const lintResults: TEslint.ESLint.LintResult[] = await this._fileEnumerator.lintFiles(['.']);
lintResults.sort((left: TEslint.ESLint.LintResult, right: TEslint.ESLint.LintResult) =>
left.filePath.localeCompare(right.filePath)
);

const additionalLintFiles: IAdditionalLintFile[] = await Promise.all(
lintResults
.filter(
(lintResult: TEslint.ESLint.LintResult) =>
!this._typeScriptFilenames.has(path.resolve(lintResult.filePath)) &&
!ESLINT_DEFAULT_EXTENSIONS.has(path.extname(lintResult.filePath))
)
.map(async (lintResult: TEslint.ESLint.LintResult): Promise<IAdditionalLintFile> => {
return {
kind: 'additional',
fileName: lintResult.filePath,
text: await FileSystem.readFileAsync(lintResult.filePath),
version: ''
};
})
);

return new Set(additionalLintFiles);
}

protected override async getCacheVersionAsync(): Promise<string> {
return `${this._eslintPackageVersion.version}_${process.version}`;
}

protected override async getSourceFileHashAsync(sourceFile: IExtendedSourceFile): Promise<string> {
const sourceFileEslintConfiguration: TEslint.Linter.Config = await this._linter.calculateConfigForFile(
protected override async getSourceFileHashAsync(
sourceFile: IExtendedSourceFile | IAdditionalLintFile
): Promise<string> {
const linter: TEslint.ESLint | TEslintLegacy.ESLint = this._getLinterForSourceFile(sourceFile);
const sourceFileEslintConfiguration: TEslint.Linter.Config = await linter.calculateConfigForFile(
sourceFile.fileName
);

Expand All @@ -272,10 +341,11 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult | TEslintLegacy
}

protected override async lintFileAsync(
sourceFile: TTypescript.SourceFile
sourceFile: TTypescript.SourceFile | IAdditionalLintFile
): Promise<TEslint.ESLint.LintResult[] | TEslintLegacy.ESLint.LintResult[]> {
const linter: TEslint.ESLint | TEslintLegacy.ESLint = this._getLinterForSourceFile(sourceFile);
const lintResults: TEslint.ESLint.LintResult[] | TEslintLegacy.ESLint.LintResult[] =
await this._linter.lintText(sourceFile.text, { filePath: sourceFile.fileName });
await linter.lintText(sourceFile.text, { filePath: sourceFile.fileName });

// Map the fix messages to the results. This API should only return one result per file, so we can be sure
// that the fix messages belong to the returned result. If we somehow receive multiple results, we will
Expand All @@ -286,6 +356,12 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult | TEslintLegacy
this._fixMessagesByResult.set(lintResults[0], fixMessages);
}

if (linter === this._additionalFilesLinter) {
for (const lintResult of lintResults) {
this._additionalLintResults.add(lintResult);
}
}

this._fixesPossible ||=
!this._fix &&
lintResults.some((lintResult: TEslint.ESLint.LintResult | TEslintLegacy.ESLint.LintResult) => {
Expand Down Expand Up @@ -349,8 +425,19 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult | TEslintLegacy

const sarifLogPath: string | undefined = this._sarifLogPath;
if (sarifLogPath) {
const rulesMeta: TEslint.ESLint.LintResultData['rulesMeta'] =
this._linter.getRulesMetaForResults(lintResults);
const primaryLintResults: TEslint.ESLint.LintResult[] = [];
const additionalLintResults: TEslint.ESLint.LintResult[] = [];
for (const lintResult of lintResults) {
const resultSet: TEslint.ESLint.LintResult[] = this._additionalLintResults.has(lintResult)
? additionalLintResults
: primaryLintResults;
resultSet.push(lintResult);
}

const rulesMeta: TEslint.ESLint.LintResultData['rulesMeta'] = {
...this._linter.getRulesMetaForResults(primaryLintResults),
...this._additionalFilesLinter?.getRulesMetaForResults(additionalLintResults)
};
const { formatEslintResultsAsSARIF } = await import('./SarifFormatter');
const sarifString: string = JSON.stringify(
formatEslintResultsAsSARIF(lintResults, rulesMeta, {
Expand Down Expand Up @@ -380,6 +467,20 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult | TEslintLegacy
});
}

private _getLinterForSourceFile(
sourceFile: TTypescript.SourceFile | IAdditionalLintFile
): TEslint.ESLint | TEslintLegacy.ESLint {
if (sourceFile.kind === 'additional') {
if (!this._additionalFilesLinter) {
throw new Error('The ESLint instance for additional files has not been initialized.');
}

return this._additionalFilesLinter;
}

return this._linter;
}

private _getLintFileError(
lintResult: TEslint.ESLint.LintResult | TEslintLegacy.ESLint.LintResult,
lintMessage: TEslint.Linter.LintMessage | TEslintLegacy.Linter.LintMessage,
Expand Down
Loading
Loading