From b5afc06681e11860b13c29f750f8deffc1ecd571 Mon Sep 17 00:00:00 2001 From: David Michon Date: Tue, 4 Nov 2025 00:30:24 +0000 Subject: [PATCH 1/3] [heft-lint] Fix TypeScript program passing --- .../fix-program-reuse_2025-11-04-00-30.json | 10 ++++ heft-plugins/heft-lint-plugin/src/Eslint.ts | 50 +++++-------------- 2 files changed, 23 insertions(+), 37 deletions(-) create mode 100644 common/changes/@rushstack/heft-lint-plugin/fix-program-reuse_2025-11-04-00-30.json diff --git a/common/changes/@rushstack/heft-lint-plugin/fix-program-reuse_2025-11-04-00-30.json b/common/changes/@rushstack/heft-lint-plugin/fix-program-reuse_2025-11-04-00-30.json new file mode 100644 index 0000000000..dc89182862 --- /dev/null +++ b/common/changes/@rushstack/heft-lint-plugin/fix-program-reuse_2025-11-04-00-30.json @@ -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" +} \ No newline at end of file diff --git a/heft-plugins/heft-lint-plugin/src/Eslint.ts b/heft-plugins/heft-lint-plugin/src/Eslint.ts index 665e7c78e8..406fe2f6e0 100644 --- a/heft-plugins/heft-lint-plugin/src/Eslint.ts +++ b/heft-plugins/heft-lint-plugin/src/Eslint.ts @@ -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'; @@ -112,6 +93,7 @@ export class Eslint extends LinterBase = new Map(); private readonly _sarifLogPath: string | undefined; + private readonly _configHashMap: WeakMap = new WeakMap(); protected constructor(options: IEslintOptions) { super('eslint', options); @@ -165,7 +147,8 @@ export class Eslint extends LinterBase { - 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 From 7ee5b377be1fb2cfe13b7b68a57cffa613ba8b4a Mon Sep 17 00:00:00 2001 From: David Michon Date: Tue, 4 Nov 2025 01:21:55 +0000 Subject: [PATCH 2/3] Patch ESLint 9 support --- heft-plugins/heft-lint-plugin/src/Eslint.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/heft-plugins/heft-lint-plugin/src/Eslint.ts b/heft-plugins/heft-lint-plugin/src/Eslint.ts index 406fe2f6e0..29e414da07 100644 --- a/heft-plugins/heft-lint-plugin/src/Eslint.ts +++ b/heft-plugins/heft-lint-plugin/src/Eslint.ts @@ -160,10 +160,20 @@ export class Eslint extends LinterBase Date: Tue, 4 Nov 2025 01:32:10 +0000 Subject: [PATCH 3/3] Patch for older versions of ESLint@9 --- heft-plugins/heft-lint-plugin/src/Eslint.ts | 33 ++++++++++++--------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/heft-plugins/heft-lint-plugin/src/Eslint.ts b/heft-plugins/heft-lint-plugin/src/Eslint.ts index 29e414da07..4d68afeebb 100644 --- a/heft-plugins/heft-lint-plugin/src/Eslint.ts +++ b/heft-plugins/heft-lint-plugin/src/Eslint.ts @@ -16,6 +16,7 @@ import type { HeftConfiguration } from '@rushstack/heft'; import { LinterBase, type ILinterBaseOptions } from './LinterBase'; import type { IExtendedSourceFile } from './internalTypings/TypeScriptInternals'; +import { name as pluginName, version as pluginVersion } from '../package.json'; interface IEslintOptions extends ILinterBaseOptions { eslintPackage: typeof TEslint | typeof TEslintLegacy; @@ -153,6 +154,23 @@ export class Eslint extends LinterBase 9.28.0 + toJSON: parserOptionsToJson + }; + if (this._eslintPackageVersion.minor < 28) { + overrideParserOptions = Object.defineProperties(overrideParserOptions, { + // Support for `toJSON` within languageOptions was added in ESLint 9.28.0 + // This hack tells ESLint's `languageOptionsToJSON` function to replace the entire `parserOptions` object with `@rushstack/heft-lint-plugin@${version}` + meta: { + value: { + name: pluginName, + version: pluginVersion + } + } + }); + } // The @typescript-eslint/parser package allows providing an existing TypeScript program to avoid needing // to reparse. However, fixers in ESLint run in multiple passes against the underlying code until the // fix fully succeeds. This conflicts with providing an existing program as the code no longer maps to @@ -160,20 +178,7 @@ export class Eslint extends LinterBase