Skip to content

Commit

Permalink
feat: jsonc/auto rule works even in flat config (#298)
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi authored Jan 23, 2024
1 parent d4213c8 commit a40f021
Show file tree
Hide file tree
Showing 15 changed files with 241 additions and 37 deletions.
5 changes: 5 additions & 0 deletions .changeset/flat-bikes-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-jsonc": minor
---

feat: `jsonc/auto` rule works even in flat config
6 changes: 3 additions & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
// Configure tool-specific properties.
"customizations": {
"vscode": {
"extensions": ["dbaeumer.vscode-eslint"]
}
}
"extensions": ["dbaeumer.vscode-eslint"],
},
},

// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
Expand Down
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@
/docs/.vitepress/cache
/docs/.vitepress/build-system/shim
/docs/.vitepress/dist
!/tests/fixtures/integrations/eslint-plugin/test-auto-rule-with-flat-config01/eslint.config.js
1 change: 1 addition & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export default async (): Promise<UserConfig<DefaultTheme.Config>> => {
},
define: {
"process.env.NODE_DEBUG": "false",
"process.env.ESLINT_USE_FLAT_CONFIG": "false",
},
optimizeDeps: {
exclude: ["eslint-compat-utils"],
Expand Down
4 changes: 2 additions & 2 deletions lib/rules/auto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getFilename, getSourceCode } from "eslint-compat-utils";
import { getCwd, getFilename, getSourceCode } from "eslint-compat-utils";
import type { RuleListener, RuleModule } from "../types";
import { createRule } from "../utils";
import { getAutoConfig } from "../utils/get-auto-jsonc-rules-config";
Expand All @@ -22,7 +22,7 @@ export default createRule("auto", {
if (!sourceCode.parserServices.isJSON) {
return {};
}
const autoConfig = getAutoConfig(getFilename(context));
const autoConfig = getAutoConfig(getCwd(context), getFilename(context));

const visitor: RuleListener = {};
for (const ruleId of Object.keys(autoConfig)) {
Expand Down
15 changes: 15 additions & 0 deletions lib/utils/get-auto-jsonc-rules-config/calculate-config-for-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Linter } from "eslint";
// @ts-expect-error -- ignore
import { createSyncFn } from "synckit";

const getSync = createSyncFn(require.resolve("./worker"));

/**
* Synchronously calculateConfigForFile
*/
export function calculateConfigForFile(
cwd: string,
fileName: string,
): Pick<Linter.Config, "rules"> {
return getSync(cwd, fileName);
}
Original file line number Diff line number Diff line change
@@ -1,57 +1,72 @@
import type { Linter } from "eslint";
import { existsSync, statSync } from "fs";
import { dirname, extname, resolve } from "path";
import type { RuleModule } from "../types";
import type { RuleModule } from "../../types";
import { shouldUseFlatConfig } from "./should-use-flat-config";
import { calculateConfigForFile } from "./calculate-config-for-file";

let configResolver: (filePath: string) => Linter.Config, ruleNames: Set<string>;
const configResolvers: Record<
string,
undefined | ((filePath: string) => Pick<Linter.Config, "rules">)
> = {};
let ruleNames: Set<string>;

/**
* Get config resolver
*/
function getConfigResolver(): (filePath: string) => Linter.Config {
function getConfigResolver(
cwd: string,
): (filePath: string) => Pick<Linter.Config, "rules"> {
const configResolver = configResolvers[cwd];
if (configResolver) {
return configResolver;
}

if (shouldUseFlatConfig(cwd)) {
return (configResolvers[cwd] = (filePath: string) =>
calculateConfigForFile(cwd, filePath));
}

// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -- special
const plugin = require("..");
const plugin = require("../..");
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -- special
const eslintrc = require("@eslint/eslintrc");
const configArrayFactory = new eslintrc.Legacy.CascadingConfigArrayFactory({
additionalPluginPool: new Map([["eslint-plugin-jsonc", plugin]]),
getEslintRecommendedConfig() {
// eslint-disable-next-line @typescript-eslint/no-require-imports -- ignore
return require("../../conf/eslint-recommended.js");
return require("../../../conf/eslint-recommended.js");
},
getEslintAllConfig() {
// eslint-disable-next-line @typescript-eslint/no-require-imports -- ignore
return require("../../conf/eslint-all.js");
return require("../../../conf/eslint-all.js");
},
// for v1.1.0
eslintRecommendedPath: require.resolve(
"../../conf/eslint-recommended.js",
"../../../conf/eslint-recommended.js",
),
eslintAllPath: require.resolve("../../conf/eslint-all.js"),
eslintAllPath: require.resolve("../../../conf/eslint-all.js"),
});
return (configResolver = (filePath: string) => {
const absolutePath = resolve(process.cwd(), filePath);
return (configResolvers[cwd] = (filePath: string) => {
const absolutePath = resolve(cwd, filePath);
return configArrayFactory
.getConfigArrayForFile(absolutePath)
.extractConfig(absolutePath)
.toCompatibleObjectAsConfigFileContent();
});
} catch {
} catch (_e) {
// ignore
// console.log(_e);
}
try {
// For ESLint v6

// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -- special
const eslint = require("eslint");
const engine = new eslint.CLIEngine({});
const engine = new eslint.CLIEngine({ cwd });
engine.addPlugin("eslint-plugin-jsonc", plugin);
return (configResolver = (filePath) => {
return (configResolvers[cwd] = (filePath) => {
// Adjust the file name to avoid a crash.
// https://github.com/ota-meshi/eslint-plugin-jsonc/issues/28
let targetFilePath = filePath;
Expand Down Expand Up @@ -95,8 +110,11 @@ function isValidFilename(filename: string) {
* Get config for the given filename
* @param filename
*/
function getConfig(filename: string): Linter.Config {
return getConfigResolver()(filename);
function getConfig(
cwd: string,
filename: string,
): Pick<Linter.Config, "rules"> {
return getConfigResolver(cwd)(filename);
}

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

return ruleNames.has(rule) ? `jsonc/${rule}` : null;
const ruleName = rule.startsWith("@stylistic/")
? rule.split("/").pop() ?? rule
: rule;

return ruleNames.has(ruleName) ? `jsonc/${ruleName}` : null;
}

/**
* Get additional jsonc rules config from fileName
* @param filename
*/
export function getAutoConfig(filename: string): {
export function getAutoConfig(
cwd: string,
filename: string,
): {
[name: string]: Linter.RuleEntry;
} {
const autoConfig: { [name: string]: Linter.RuleEntry } = {};

const config = getConfig(filename);
const config = getConfig(cwd, filename);
if (config.rules) {
for (const ruleName of Object.keys(config.rules)) {
const jsoncName = getJsoncRule(ruleName);
Expand Down
61 changes: 61 additions & 0 deletions lib/utils/get-auto-jsonc-rules-config/should-use-flat-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/** copied from https://github.com/eslint/eslint/blob/v8.56.0/lib/eslint/flat-eslint.js#L1119 */

import path from "path";
import fs from "fs";

const FLAT_CONFIG_FILENAME = "eslint.config.js";
/**
* Returns whether flat config should be used.
* @returns {Promise<boolean>} Whether flat config should be used.
*/
export function shouldUseFlatConfig(cwd: string): boolean {
// eslint-disable-next-line no-process-env -- ignore
switch (process.env.ESLINT_USE_FLAT_CONFIG) {
case "true":
return true;
case "false":
return false;
default:
// If neither explicitly enabled nor disabled, then use the presence
// of a flat config file to determine enablement.
return Boolean(findFlatConfigFile(cwd));
}
}

/**
* Searches from the current working directory up until finding the
* given flat config filename.
* @param {string} cwd The current working directory to search from.
* @returns {string|undefined} The filename if found or `undefined` if not.
*/
function findFlatConfigFile(cwd: string) {
return findUp(FLAT_CONFIG_FILENAME, { cwd });
}

/** We used https://github.com/sindresorhus/find-up/blob/b733bb70d3aa21b22fa011be8089110d467c317f/index.js#L94 as a reference */
function findUp(name: string, options: { cwd: string }) {
let directory = path.resolve(options.cwd);
const { root } = path.parse(directory);
const stopAt = path.resolve(directory, root);

// eslint-disable-next-line no-constant-condition -- ignore
while (true) {
const target = path.resolve(directory, name);
const stat = fs.existsSync(target)
? fs.statSync(target, {
throwIfNoEntry: false,
})
: null;
if (stat?.isFile()) {
return target;
}

if (directory === stopAt) {
break;
}

directory = path.dirname(directory);
}

return null;
}
10 changes: 10 additions & 0 deletions lib/utils/get-auto-jsonc-rules-config/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// @ts-expect-error -- ignore
import { runAsWorker } from "synckit";
import { getESLint } from "eslint-compat-utils/eslint";
const ESLint = getESLint();

runAsWorker(async (cwd: string, fileName: string) => {
const eslint = new ESLint({ cwd });
const config = await eslint.calculateConfigForFile(fileName);
return { rules: config.rules };
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@
"espree": "^9.6.1",
"graphemer": "^1.4.0",
"jsonc-eslint-parser": "^2.0.4",
"natural-compare": "^1.4.0"
"natural-compare": "^1.4.0",
"synckit": "^0.6.0"
},
"peerDependencies": {
"eslint": ">=6.0.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
let plugin;
try {
plugin = require("../../../../../lib/index");
} catch (e) {
// @ts-ignore -- ignore
plugin = require("../../../../../dist/index");
}
const parser = require("jsonc-eslint-parser");

module.exports = [
{
plugins: {
jsonc: plugin,
},
rules: {
indent: "error",
"no-unused-vars": "off",
"no-multi-spaces": "error",
"no-multiple-empty-lines": "error",
"jsonc/auto": "error",
"jsonc/no-comments": "error",
},
},
{
files: ["**/*.json"],
languageOptions: {
parser,
},
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
var a = {
foo: 'bar'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"foo": "bar"
}
Loading

0 comments on commit a40f021

Please sign in to comment.