Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/heft-lint-plugin",
"comment": "Fix bug where TypeScript program is not reused in ESLint 9.",
"type": "patch"
}
],
"packageName": "@rushstack/heft-lint-plugin"
}
50 changes: 13 additions & 37 deletions heft-plugins/heft-lint-plugin/src/Eslint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,32 +61,13 @@ function getFormattedErrorMessage(
return lintMessage.ruleId ? `(${lintMessage.ruleId}) ${lintMessage.message}` : lintMessage.message;
}

interface IExtendedEslintConfig extends TEslint.Linter.Config {
// https://github.com/eslint/eslint/blob/d6fa4ac031c2fe24fb778e84940393fbda3ddf77/lib/config/config.js#L264
toJSON: () => object;
__originalToJSON: () => object;
}

function patchedToJSON(this: IExtendedEslintConfig): object {
// If the input config has a parserOptions.programs property, we need to recreate it
// as a non-enumerable property so that it does not get serialized, as it is not
// serializable.
if (
this.languageOptions?.parserOptions?.programs &&
this.languageOptions.parserOptions.propertyIsEnumerable('programs')
) {
let { programs } = this.languageOptions.parserOptions;
Object.defineProperty(this.languageOptions.parserOptions, 'programs', {
get: () => programs,
set: (value: TTypescript.Program[]) => {
programs = value;
},
enumerable: false
});
}

const serializableConfig: object = this.__originalToJSON.call(this);
return serializableConfig;
function parserOptionsToJson(this: TEslint.Linter.LanguageOptions['parserOptions']): object {
const serializableParserOptions: TEslint.Linter.LanguageOptions['parserOptions'] = {
...this,
// Remove the programs to avoid circular references and non-serializable data
programs: undefined
};
return serializableParserOptions;
}

const ESLINT_CONFIG_JS_FILENAME: string = 'eslint.config.js';
Expand All @@ -112,6 +93,7 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult | TEslintLegacy
(TEslint.Linter.LintMessage | TEslintLegacy.Linter.LintMessage)[]
> = new Map();
private readonly _sarifLogPath: string | undefined;
private readonly _configHashMap: WeakMap<object, string> = new WeakMap();

protected constructor(options: IEslintOptions) {
super('eslint', options);
Expand Down Expand Up @@ -165,7 +147,8 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult | TEslintLegacy
// if we're not fixing.
const legacyEslintOverrideConfig: TEslintLegacy.Linter.Config = {
parserOptions: {
programs: [tsProgram]
programs: [tsProgram],
toJSON: parserOptionsToJson
}
};
overrideConfig = legacyEslintOverrideConfig;
Expand All @@ -178,7 +161,8 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult | TEslintLegacy
const eslintOverrideConfig: TEslint.Linter.Config = {
languageOptions: {
parserOptions: {
programs: [tsProgram]
programs: [tsProgram],
toJSON: parserOptionsToJson
}
}
};
Expand Down Expand Up @@ -254,18 +238,10 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult | TEslintLegacy
}

protected override async getSourceFileHashAsync(sourceFile: IExtendedSourceFile): Promise<string> {
const sourceFileEslintConfiguration: IExtendedEslintConfig = await this._linter.calculateConfigForFile(
const sourceFileEslintConfiguration: TEslint.Linter.Config = await this._linter.calculateConfigForFile(
sourceFile.fileName
);

// The eslint configuration object contains a toJSON() method that returns a serializable version of the
// configuration. However, we are manually injecting the TypeScript program into the parserOptions, which
// is not serializable. Patch the function to remove the program before returning the serializable version.
if (sourceFileEslintConfiguration.toJSON && !sourceFileEslintConfiguration.__originalToJSON) {
sourceFileEslintConfiguration.__originalToJSON = sourceFileEslintConfiguration.toJSON;
sourceFileEslintConfiguration.toJSON = patchedToJSON.bind(sourceFileEslintConfiguration);
}

const hash: Hash = createHash('sha1');
// Use a stable stringifier to ensure that the hash is always the same, even if the order of the properties
// changes. This is also done in ESLint
Expand Down
Loading