Skip to content

Commit a40f021

Browse files
authored
feat: jsonc/auto rule works even in flat config (#298)
1 parent d4213c8 commit a40f021

File tree

15 files changed

+241
-37
lines changed

15 files changed

+241
-37
lines changed

.changeset/flat-bikes-pay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-jsonc": minor
3+
---
4+
5+
feat: `jsonc/auto` rule works even in flat config

.devcontainer/devcontainer.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
// Configure tool-specific properties.
1818
"customizations": {
1919
"vscode": {
20-
"extensions": ["dbaeumer.vscode-eslint"]
21-
}
22-
}
20+
"extensions": ["dbaeumer.vscode-eslint"],
21+
},
22+
},
2323

2424
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
2525
// "remoteUser": "root"

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@
2222
/docs/.vitepress/cache
2323
/docs/.vitepress/build-system/shim
2424
/docs/.vitepress/dist
25+
!/tests/fixtures/integrations/eslint-plugin/test-auto-rule-with-flat-config01/eslint.config.js

docs/.vitepress/config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export default async (): Promise<UserConfig<DefaultTheme.Config>> => {
5353
},
5454
define: {
5555
"process.env.NODE_DEBUG": "false",
56+
"process.env.ESLINT_USE_FLAT_CONFIG": "false",
5657
},
5758
optimizeDeps: {
5859
exclude: ["eslint-compat-utils"],

lib/rules/auto.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getFilename, getSourceCode } from "eslint-compat-utils";
1+
import { getCwd, getFilename, getSourceCode } from "eslint-compat-utils";
22
import type { RuleListener, RuleModule } from "../types";
33
import { createRule } from "../utils";
44
import { getAutoConfig } from "../utils/get-auto-jsonc-rules-config";
@@ -22,7 +22,7 @@ export default createRule("auto", {
2222
if (!sourceCode.parserServices.isJSON) {
2323
return {};
2424
}
25-
const autoConfig = getAutoConfig(getFilename(context));
25+
const autoConfig = getAutoConfig(getCwd(context), getFilename(context));
2626

2727
const visitor: RuleListener = {};
2828
for (const ruleId of Object.keys(autoConfig)) {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { Linter } from "eslint";
2+
// @ts-expect-error -- ignore
3+
import { createSyncFn } from "synckit";
4+
5+
const getSync = createSyncFn(require.resolve("./worker"));
6+
7+
/**
8+
* Synchronously calculateConfigForFile
9+
*/
10+
export function calculateConfigForFile(
11+
cwd: string,
12+
fileName: string,
13+
): Pick<Linter.Config, "rules"> {
14+
return getSync(cwd, fileName);
15+
}

lib/utils/get-auto-jsonc-rules-config.ts renamed to lib/utils/get-auto-jsonc-rules-config/index.ts

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,72 @@
11
import type { Linter } from "eslint";
22
import { existsSync, statSync } from "fs";
33
import { dirname, extname, resolve } from "path";
4-
import type { RuleModule } from "../types";
4+
import type { RuleModule } from "../../types";
5+
import { shouldUseFlatConfig } from "./should-use-flat-config";
6+
import { calculateConfigForFile } from "./calculate-config-for-file";
57

6-
let configResolver: (filePath: string) => Linter.Config, ruleNames: Set<string>;
8+
const configResolvers: Record<
9+
string,
10+
undefined | ((filePath: string) => Pick<Linter.Config, "rules">)
11+
> = {};
12+
let ruleNames: Set<string>;
713

814
/**
915
* Get config resolver
1016
*/
11-
function getConfigResolver(): (filePath: string) => Linter.Config {
17+
function getConfigResolver(
18+
cwd: string,
19+
): (filePath: string) => Pick<Linter.Config, "rules"> {
20+
const configResolver = configResolvers[cwd];
1221
if (configResolver) {
1322
return configResolver;
1423
}
1524

25+
if (shouldUseFlatConfig(cwd)) {
26+
return (configResolvers[cwd] = (filePath: string) =>
27+
calculateConfigForFile(cwd, filePath));
28+
}
29+
1630
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -- special
17-
const plugin = require("..");
31+
const plugin = require("../..");
1832
try {
1933
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -- special
2034
const eslintrc = require("@eslint/eslintrc");
2135
const configArrayFactory = new eslintrc.Legacy.CascadingConfigArrayFactory({
2236
additionalPluginPool: new Map([["eslint-plugin-jsonc", plugin]]),
2337
getEslintRecommendedConfig() {
2438
// eslint-disable-next-line @typescript-eslint/no-require-imports -- ignore
25-
return require("../../conf/eslint-recommended.js");
39+
return require("../../../conf/eslint-recommended.js");
2640
},
2741
getEslintAllConfig() {
2842
// eslint-disable-next-line @typescript-eslint/no-require-imports -- ignore
29-
return require("../../conf/eslint-all.js");
43+
return require("../../../conf/eslint-all.js");
3044
},
3145
// for v1.1.0
3246
eslintRecommendedPath: require.resolve(
33-
"../../conf/eslint-recommended.js",
47+
"../../../conf/eslint-recommended.js",
3448
),
35-
eslintAllPath: require.resolve("../../conf/eslint-all.js"),
49+
eslintAllPath: require.resolve("../../../conf/eslint-all.js"),
3650
});
37-
return (configResolver = (filePath: string) => {
38-
const absolutePath = resolve(process.cwd(), filePath);
51+
return (configResolvers[cwd] = (filePath: string) => {
52+
const absolutePath = resolve(cwd, filePath);
3953
return configArrayFactory
4054
.getConfigArrayForFile(absolutePath)
4155
.extractConfig(absolutePath)
4256
.toCompatibleObjectAsConfigFileContent();
4357
});
44-
} catch {
58+
} catch (_e) {
4559
// ignore
60+
// console.log(_e);
4661
}
4762
try {
4863
// For ESLint v6
4964

5065
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -- special
5166
const eslint = require("eslint");
52-
const engine = new eslint.CLIEngine({});
67+
const engine = new eslint.CLIEngine({ cwd });
5368
engine.addPlugin("eslint-plugin-jsonc", plugin);
54-
return (configResolver = (filePath) => {
69+
return (configResolvers[cwd] = (filePath) => {
5570
// Adjust the file name to avoid a crash.
5671
// https://github.com/ota-meshi/eslint-plugin-jsonc/issues/28
5772
let targetFilePath = filePath;
@@ -95,8 +110,11 @@ function isValidFilename(filename: string) {
95110
* Get config for the given filename
96111
* @param filename
97112
*/
98-
function getConfig(filename: string): Linter.Config {
99-
return getConfigResolver()(filename);
113+
function getConfig(
114+
cwd: string,
115+
filename: string,
116+
): Pick<Linter.Config, "rules"> {
117+
return getConfigResolver(cwd)(filename);
100118
}
101119

102120
/**
@@ -108,24 +126,31 @@ function getJsoncRule(rule: string) {
108126
ruleNames ||
109127
new Set(
110128
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -- special
111-
(require("./rules").rules as RuleModule[]).map(
129+
(require("../rules").rules as RuleModule[]).map(
112130
(r) => r.meta.docs.ruleName,
113131
),
114132
);
115133

116-
return ruleNames.has(rule) ? `jsonc/${rule}` : null;
134+
const ruleName = rule.startsWith("@stylistic/")
135+
? rule.split("/").pop() ?? rule
136+
: rule;
137+
138+
return ruleNames.has(ruleName) ? `jsonc/${ruleName}` : null;
117139
}
118140

119141
/**
120142
* Get additional jsonc rules config from fileName
121143
* @param filename
122144
*/
123-
export function getAutoConfig(filename: string): {
145+
export function getAutoConfig(
146+
cwd: string,
147+
filename: string,
148+
): {
124149
[name: string]: Linter.RuleEntry;
125150
} {
126151
const autoConfig: { [name: string]: Linter.RuleEntry } = {};
127152

128-
const config = getConfig(filename);
153+
const config = getConfig(cwd, filename);
129154
if (config.rules) {
130155
for (const ruleName of Object.keys(config.rules)) {
131156
const jsoncName = getJsoncRule(ruleName);
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/** copied from https://github.com/eslint/eslint/blob/v8.56.0/lib/eslint/flat-eslint.js#L1119 */
2+
3+
import path from "path";
4+
import fs from "fs";
5+
6+
const FLAT_CONFIG_FILENAME = "eslint.config.js";
7+
/**
8+
* Returns whether flat config should be used.
9+
* @returns {Promise<boolean>} Whether flat config should be used.
10+
*/
11+
export function shouldUseFlatConfig(cwd: string): boolean {
12+
// eslint-disable-next-line no-process-env -- ignore
13+
switch (process.env.ESLINT_USE_FLAT_CONFIG) {
14+
case "true":
15+
return true;
16+
case "false":
17+
return false;
18+
default:
19+
// If neither explicitly enabled nor disabled, then use the presence
20+
// of a flat config file to determine enablement.
21+
return Boolean(findFlatConfigFile(cwd));
22+
}
23+
}
24+
25+
/**
26+
* Searches from the current working directory up until finding the
27+
* given flat config filename.
28+
* @param {string} cwd The current working directory to search from.
29+
* @returns {string|undefined} The filename if found or `undefined` if not.
30+
*/
31+
function findFlatConfigFile(cwd: string) {
32+
return findUp(FLAT_CONFIG_FILENAME, { cwd });
33+
}
34+
35+
/** We used https://github.com/sindresorhus/find-up/blob/b733bb70d3aa21b22fa011be8089110d467c317f/index.js#L94 as a reference */
36+
function findUp(name: string, options: { cwd: string }) {
37+
let directory = path.resolve(options.cwd);
38+
const { root } = path.parse(directory);
39+
const stopAt = path.resolve(directory, root);
40+
41+
// eslint-disable-next-line no-constant-condition -- ignore
42+
while (true) {
43+
const target = path.resolve(directory, name);
44+
const stat = fs.existsSync(target)
45+
? fs.statSync(target, {
46+
throwIfNoEntry: false,
47+
})
48+
: null;
49+
if (stat?.isFile()) {
50+
return target;
51+
}
52+
53+
if (directory === stopAt) {
54+
break;
55+
}
56+
57+
directory = path.dirname(directory);
58+
}
59+
60+
return null;
61+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// @ts-expect-error -- ignore
2+
import { runAsWorker } from "synckit";
3+
import { getESLint } from "eslint-compat-utils/eslint";
4+
const ESLint = getESLint();
5+
6+
runAsWorker(async (cwd: string, fileName: string) => {
7+
const eslint = new ESLint({ cwd });
8+
const config = await eslint.calculateConfigForFile(fileName);
9+
return { rules: config.rules };
10+
});

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@
7171
"espree": "^9.6.1",
7272
"graphemer": "^1.4.0",
7373
"jsonc-eslint-parser": "^2.0.4",
74-
"natural-compare": "^1.4.0"
74+
"natural-compare": "^1.4.0",
75+
"synckit": "^0.6.0"
7576
},
7677
"peerDependencies": {
7778
"eslint": ">=6.0.0"

0 commit comments

Comments
 (0)