Skip to content

Commit cc1d835

Browse files
authored
[heft-lint] Fix TypeScript program passing (#5428)
Co-authored-by: David Michon <dmichon-msft@users.noreply.github.com>
1 parent 29fe6d1 commit cc1d835

2 files changed

Lines changed: 40 additions & 39 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/heft-lint-plugin",
5+
"comment": "Fix bug where TypeScript program is not reused in ESLint 9.",
6+
"type": "patch"
7+
}
8+
],
9+
"packageName": "@rushstack/heft-lint-plugin"
10+
}

heft-plugins/heft-lint-plugin/src/Eslint.ts

Lines changed: 30 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type { HeftConfiguration } from '@rushstack/heft';
1616

1717
import { LinterBase, type ILinterBaseOptions } from './LinterBase';
1818
import type { IExtendedSourceFile } from './internalTypings/TypeScriptInternals';
19+
import { name as pluginName, version as pluginVersion } from '../package.json';
1920

2021
interface IEslintOptions extends ILinterBaseOptions {
2122
eslintPackage: typeof TEslint | typeof TEslintLegacy;
@@ -61,32 +62,13 @@ function getFormattedErrorMessage(
6162
return lintMessage.ruleId ? `(${lintMessage.ruleId}) ${lintMessage.message}` : lintMessage.message;
6263
}
6364

64-
interface IExtendedEslintConfig extends TEslint.Linter.Config {
65-
// https://github.com/eslint/eslint/blob/d6fa4ac031c2fe24fb778e84940393fbda3ddf77/lib/config/config.js#L264
66-
toJSON: () => object;
67-
__originalToJSON: () => object;
68-
}
69-
70-
function patchedToJSON(this: IExtendedEslintConfig): object {
71-
// If the input config has a parserOptions.programs property, we need to recreate it
72-
// as a non-enumerable property so that it does not get serialized, as it is not
73-
// serializable.
74-
if (
75-
this.languageOptions?.parserOptions?.programs &&
76-
this.languageOptions.parserOptions.propertyIsEnumerable('programs')
77-
) {
78-
let { programs } = this.languageOptions.parserOptions;
79-
Object.defineProperty(this.languageOptions.parserOptions, 'programs', {
80-
get: () => programs,
81-
set: (value: TTypescript.Program[]) => {
82-
programs = value;
83-
},
84-
enumerable: false
85-
});
86-
}
87-
88-
const serializableConfig: object = this.__originalToJSON.call(this);
89-
return serializableConfig;
65+
function parserOptionsToJson(this: TEslint.Linter.LanguageOptions['parserOptions']): object {
66+
const serializableParserOptions: TEslint.Linter.LanguageOptions['parserOptions'] = {
67+
...this,
68+
// Remove the programs to avoid circular references and non-serializable data
69+
programs: undefined
70+
};
71+
return serializableParserOptions;
9072
}
9173

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

11699
protected constructor(options: IEslintOptions) {
117100
super('eslint', options);
@@ -165,21 +148,37 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult | TEslintLegacy
165148
// if we're not fixing.
166149
const legacyEslintOverrideConfig: TEslintLegacy.Linter.Config = {
167150
parserOptions: {
168-
programs: [tsProgram]
151+
programs: [tsProgram],
152+
toJSON: parserOptionsToJson
169153
}
170154
};
171155
overrideConfig = legacyEslintOverrideConfig;
172156
} else {
157+
let overrideParserOptions: TEslint.Linter.ParserOptions = {
158+
programs: [tsProgram],
159+
// Used by stableStringify and ESLint > 9.28.0
160+
toJSON: parserOptionsToJson
161+
};
162+
if (this._eslintPackageVersion.minor < 28) {
163+
overrideParserOptions = Object.defineProperties(overrideParserOptions, {
164+
// Support for `toJSON` within languageOptions was added in ESLint 9.28.0
165+
// This hack tells ESLint's `languageOptionsToJSON` function to replace the entire `parserOptions` object with `@rushstack/heft-lint-plugin@${version}`
166+
meta: {
167+
value: {
168+
name: pluginName,
169+
version: pluginVersion
170+
}
171+
}
172+
});
173+
}
173174
// The @typescript-eslint/parser package allows providing an existing TypeScript program to avoid needing
174175
// to reparse. However, fixers in ESLint run in multiple passes against the underlying code until the
175176
// fix fully succeeds. This conflicts with providing an existing program as the code no longer maps to
176177
// the provided program, producing garbage fix output. To avoid this, only provide the existing program
177178
// if we're not fixing.
178179
const eslintOverrideConfig: TEslint.Linter.Config = {
179180
languageOptions: {
180-
parserOptions: {
181-
programs: [tsProgram]
182-
}
181+
parserOptions: overrideParserOptions
183182
}
184183
};
185184
overrideConfig = eslintOverrideConfig;
@@ -254,18 +253,10 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult | TEslintLegacy
254253
}
255254

256255
protected override async getSourceFileHashAsync(sourceFile: IExtendedSourceFile): Promise<string> {
257-
const sourceFileEslintConfiguration: IExtendedEslintConfig = await this._linter.calculateConfigForFile(
256+
const sourceFileEslintConfiguration: TEslint.Linter.Config = await this._linter.calculateConfigForFile(
258257
sourceFile.fileName
259258
);
260259

261-
// The eslint configuration object contains a toJSON() method that returns a serializable version of the
262-
// configuration. However, we are manually injecting the TypeScript program into the parserOptions, which
263-
// is not serializable. Patch the function to remove the program before returning the serializable version.
264-
if (sourceFileEslintConfiguration.toJSON && !sourceFileEslintConfiguration.__originalToJSON) {
265-
sourceFileEslintConfiguration.__originalToJSON = sourceFileEslintConfiguration.toJSON;
266-
sourceFileEslintConfiguration.toJSON = patchedToJSON.bind(sourceFileEslintConfiguration);
267-
}
268-
269260
const hash: Hash = createHash('sha1');
270261
// Use a stable stringifier to ensure that the hash is always the same, even if the order of the properties
271262
// changes. This is also done in ESLint

0 commit comments

Comments
 (0)