Skip to content

Commit be2df9c

Browse files
jaysooFrozenPandaz
authored andcommitted
fix(linter): handle various flat config override structures (#33548)
Flat config overrides util may fail when it isn't a plain JS object. This PR makes the `hasOverrides` function more robust against these cases. Fixes #31796 (cherry picked from commit 05bd3a4)
1 parent 7fc161e commit be2df9c

2 files changed

Lines changed: 241 additions & 11 deletions

File tree

packages/eslint/src/generators/utils/flat-config/ast-utils.spec.ts

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import ts = require('typescript');
1+
import * as ts from 'typescript';
22
import {
33
addBlockToFlatConfigExport,
44
addFlatCompatToFlatConfig,
55
addImportToFlatConfig,
66
generateAst,
77
generateFlatOverride,
88
generatePluginExtendsElementWithCompatFixup,
9+
hasOverride,
910
removeCompatExtends,
1011
removeImportFromFlatConfig,
1112
removeOverridesFromLintConfig,
@@ -1175,6 +1176,160 @@ export default [
11751176
});
11761177
});
11771178

1179+
describe('hasOverride', () => {
1180+
it('should handle variable references in property values', () => {
1181+
const content = `
1182+
import pluginPackageJson from "eslint-plugin-package-json";
1183+
import jsoncParser from "jsonc-eslint-parser";
1184+
1185+
export default [
1186+
{
1187+
files: ["package.json"],
1188+
plugins: { "package-json": pluginPackageJson },
1189+
languageOptions: {
1190+
parser: jsoncParser,
1191+
},
1192+
}
1193+
];`;
1194+
1195+
const result = hasOverride(
1196+
content,
1197+
(o) => Array.isArray(o.files) && o.files.includes('package.json')
1198+
);
1199+
expect(result).toBe(true);
1200+
});
1201+
1202+
it('should handle spread elements', () => {
1203+
const content = `
1204+
export default [
1205+
{
1206+
files: ['*.json'],
1207+
...(jest.configs['flat/recommended'])
1208+
...getConfig()
1209+
...configs['recommended']
1210+
}
1211+
];`;
1212+
1213+
const result = hasOverride(
1214+
content,
1215+
(o) => Array.isArray(o.files) && o.files.includes('*.json')
1216+
);
1217+
expect(result).toBe(true);
1218+
});
1219+
1220+
it('should extract rules property correctly', () => {
1221+
const content = `
1222+
export default [
1223+
{
1224+
files: ['*.ts'],
1225+
rules: {
1226+
'@nx/enforce-module-boundaries': 'error'
1227+
}
1228+
}
1229+
];`;
1230+
1231+
const result = hasOverride(
1232+
content,
1233+
(o) => !!o.rules?.['@nx/enforce-module-boundaries']
1234+
);
1235+
expect(result).toBe(true);
1236+
});
1237+
1238+
it('should return false when no matching override is found', () => {
1239+
const content = `
1240+
export default [
1241+
{
1242+
files: ['*.ts'],
1243+
rules: {}
1244+
}
1245+
];`;
1246+
1247+
const result = hasOverride(
1248+
content,
1249+
(o) => Array.isArray(o.files) && o.files.includes('*.js')
1250+
);
1251+
expect(result).toBe(false);
1252+
});
1253+
1254+
it('should handle simple spread elements', () => {
1255+
const content = `
1256+
export default [
1257+
{
1258+
files: ['*.ts'],
1259+
...baseConfig
1260+
}
1261+
];`;
1262+
1263+
const result = hasOverride(
1264+
content,
1265+
(o) => Array.isArray(o.files) && o.files.includes('*.ts')
1266+
);
1267+
expect(result).toBe(true);
1268+
});
1269+
1270+
it('should handle nested object literals', () => {
1271+
const content = `
1272+
export default [
1273+
{
1274+
files: ['*.ts'],
1275+
languageOptions: {
1276+
parserOptions: {
1277+
project: './tsconfig.json'
1278+
}
1279+
}
1280+
}
1281+
];`;
1282+
1283+
const result = hasOverride(
1284+
content,
1285+
(o) =>
1286+
(o as any).languageOptions?.parserOptions?.project ===
1287+
'./tsconfig.json'
1288+
);
1289+
expect(result).toBe(true);
1290+
});
1291+
1292+
it('should handle CJS format', () => {
1293+
const content = `
1294+
const jest = require("eslint-plugin-jest");
1295+
1296+
module.exports = [
1297+
{
1298+
files: ['**/*.spec.ts'],
1299+
...(jest.configs['flat/recommended'])
1300+
}
1301+
];`;
1302+
1303+
const result = hasOverride(
1304+
content,
1305+
(o) => Array.isArray(o.files) && o.files.includes('**/*.spec.ts')
1306+
);
1307+
expect(result).toBe(true);
1308+
});
1309+
1310+
it('should handle compat.config(...).map(...) pattern', () => {
1311+
const content = `
1312+
export default [
1313+
...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({
1314+
...config,
1315+
files: [
1316+
"my-lib/**/*.ts",
1317+
"my-lib/**/*.tsx"
1318+
],
1319+
rules: {
1320+
'my-ts-rule': 'error'
1321+
}
1322+
})),
1323+
];`;
1324+
1325+
const result = hasOverride(
1326+
content,
1327+
(o) => Array.isArray(o.files) && o.files.includes('my-lib/**/*.ts')
1328+
);
1329+
expect(result).toBe(true);
1330+
});
1331+
});
1332+
11781333
describe('generatePluginExtendsElementWithCompatFixup', () => {
11791334
it('should return spread element with fixupConfigRules call wrapping the extended plugin', () => {
11801335
const result = generatePluginExtendsElementWithCompatFixup('my-plugin');

packages/eslint/src/generators/utils/flat-config/ast-utils.ts

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -187,19 +187,21 @@ export function hasOverride(
187187
}
188188
for (const node of exportsArray) {
189189
if (isOverride(node)) {
190-
let objSource;
190+
let data: Partial<Linter.ConfigOverride<Linter.RulesRecord>>;
191+
191192
if (ts.isObjectLiteralExpression(node)) {
192-
objSource = node.getFullText();
193-
// strip any spread elements
194-
objSource = objSource.replace(SPREAD_ELEMENTS_REGEXP, '');
193+
data = extractPropertiesFromObjectLiteral(node);
195194
} else {
196-
const fullNodeText =
197-
node['expression'].arguments[0].body.expression.getFullText();
198-
// strip any spread elements
199-
objSource = fullNodeText.replace(SPREAD_ELEMENTS_REGEXP, '');
195+
// Handle compat.config(...).map(...) pattern
196+
const arrowBody = node['expression'].arguments[0].body.expression;
197+
if (ts.isObjectLiteralExpression(arrowBody)) {
198+
data = extractPropertiesFromObjectLiteral(arrowBody);
199+
} else {
200+
continue;
201+
}
200202
}
201-
const data = parseTextToJson(objSource);
202-
if (lookup(data)) {
203+
204+
if (lookup(data as Linter.ConfigOverride<Linter.RulesRecord>)) {
203205
return true;
204206
}
205207
}
@@ -219,6 +221,79 @@ function parseTextToJson(text: string): any {
219221
);
220222
}
221223

224+
/**
225+
* Extracts literal values from AST nodes.
226+
* Returns undefined for complex expressions that can't be statically evaluated.
227+
*/
228+
function extractLiteralValue(node: ts.Node): unknown {
229+
if (ts.isStringLiteral(node)) {
230+
return node.text;
231+
}
232+
if (ts.isNumericLiteral(node)) {
233+
return Number(node.text);
234+
}
235+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
236+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
237+
if (node.kind === ts.SyntaxKind.NullKeyword) return null;
238+
239+
if (ts.isArrayLiteralExpression(node)) {
240+
const arr: unknown[] = [];
241+
for (const element of node.elements) {
242+
const value = extractLiteralValue(element);
243+
if (value === undefined) return undefined;
244+
arr.push(value);
245+
}
246+
return arr;
247+
}
248+
249+
if (ts.isObjectLiteralExpression(node)) {
250+
const obj: Record<string, unknown> = {};
251+
for (const prop of node.properties) {
252+
if (ts.isPropertyAssignment(prop)) {
253+
const name = prop.name.getText().replace(/['"]/g, '');
254+
const value = extractLiteralValue(prop.initializer);
255+
if (value === undefined) {
256+
// Skip properties with non-extractable values (like variable references)
257+
continue;
258+
}
259+
obj[name] = value;
260+
} else if (ts.isSpreadAssignment(prop)) {
261+
// Cannot extract spread assignments statically, skip them
262+
continue;
263+
} else {
264+
// Skip other property types (shorthand, method, etc.)
265+
continue;
266+
}
267+
}
268+
return obj;
269+
}
270+
271+
// For complex expressions (identifiers, function calls, etc.), return undefined
272+
return undefined;
273+
}
274+
275+
/**
276+
* Extracts property values from an ObjectLiteralExpression using AST.
277+
* Only extracts properties that have simple literal values.
278+
* Returns a partial object suitable for the lookup function.
279+
*/
280+
function extractPropertiesFromObjectLiteral(
281+
node: ts.ObjectLiteralExpression
282+
): Partial<Linter.ConfigOverride<Linter.RulesRecord>> {
283+
const result: Record<string, unknown> = {};
284+
for (const prop of node.properties) {
285+
if (ts.isPropertyAssignment(prop)) {
286+
const name = prop.name.getText().replace(/['"]/g, '');
287+
const value = extractLiteralValue(prop.initializer);
288+
if (value !== undefined) {
289+
result[name] = value;
290+
}
291+
}
292+
}
293+
294+
return result as Partial<Linter.ConfigOverride<Linter.RulesRecord>>;
295+
}
296+
222297
/**
223298
* Finds an override matching the lookup function and applies the update function to it
224299
*/

0 commit comments

Comments
 (0)