Skip to content

Commit c9e326e

Browse files
authored
Improve compatibility with ESLint v9 (#281)
1 parent 24d3669 commit c9e326e

26 files changed

+5170
-169
lines changed

.changeset/thick-shoes-learn.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+
Improve compatibility with ESLint v9

.eslintrc.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ module.exports = {
2222
"@typescript-eslint/no-non-null-assertion": "off",
2323
"@typescript-eslint/no-explicit-any": "off",
2424
"no-shadow": "off",
25+
complexity: "off",
26+
"one-var": "off",
27+
"no-invalid-this": "off",
2528
// Repo rule
2629
"no-restricted-imports": [
2730
"error",
@@ -44,13 +47,13 @@ module.exports = {
4447
object: "context",
4548
property: "getSourceCode",
4649
message:
47-
"Please use `eslint-compat-utils` module's `getSourceCode(context).getScope()` instead.",
50+
"Please use `eslint-compat-utils` module's `getSourceCode(context)` instead.",
4851
},
4952
{
5053
object: "context",
5154
property: "sourceCode",
5255
message:
53-
"Please use `eslint-compat-utils` module's `getSourceCode(context).getScope()` instead.",
56+
"Please use `eslint-compat-utils` module's `getSourceCode(context)` instead.",
5457
},
5558
{
5659
object: "context",

conf/rules.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ let ruleMap;
33

44
/** Get all rules */
55
module.exports = function getCoreRules() {
6-
if (ruleMap) {
7-
return ruleMap;
6+
const eslint = require("eslint");
7+
try {
8+
return ruleMap || (ruleMap = new eslint.Linter().getRules());
9+
} catch {
10+
// getRules() is no longer available in flat config.
811
}
9-
return (ruleMap = new (require("eslint").Linter)().getRules());
12+
13+
const { builtinRules } = require("eslint/use-at-your-own-risk");
14+
return builtinRules;
1015
};

lib/rules/array-bracket-newline.ts

Lines changed: 231 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1-
import { createRule, defineWrapperListener, getCoreRule } from "../utils";
2-
const coreRule = getCoreRule("array-bracket-newline");
3-
1+
// Most source code was copied from ESLint v8.
2+
// MIT License. Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
3+
import type { AST } from "jsonc-eslint-parser";
4+
import { createRule } from "../utils";
5+
import { getSourceCode } from "eslint-compat-utils";
6+
import { isTokenOnSameLine } from "../utils/eslint-ast-utils";
7+
import type { Token } from "../types";
8+
import { isCommentToken } from "@eslint-community/eslint-utils";
9+
type Schema0 =
10+
| ("always" | "never" | "consistent")
11+
| {
12+
multiline?: boolean;
13+
minItems?: number | null;
14+
};
415
export default createRule("array-bracket-newline", {
516
meta: {
617
docs: {
@@ -10,13 +21,224 @@ export default createRule("array-bracket-newline", {
1021
extensionRule: true,
1122
layout: true,
1223
},
13-
fixable: coreRule.meta?.fixable,
14-
hasSuggestions: (coreRule.meta as any).hasSuggestions,
15-
schema: coreRule.meta!.schema!,
16-
messages: coreRule.meta!.messages!,
17-
type: coreRule.meta!.type!,
24+
type: "layout",
25+
26+
fixable: "whitespace",
27+
28+
schema: [
29+
{
30+
oneOf: [
31+
{
32+
type: "string",
33+
enum: ["always", "never", "consistent"],
34+
},
35+
{
36+
type: "object",
37+
properties: {
38+
multiline: {
39+
type: "boolean",
40+
},
41+
minItems: {
42+
type: ["integer", "null"],
43+
minimum: 0,
44+
},
45+
},
46+
additionalProperties: false,
47+
},
48+
],
49+
},
50+
],
51+
52+
messages: {
53+
unexpectedOpeningLinebreak: "There should be no linebreak after '['.",
54+
unexpectedClosingLinebreak: "There should be no linebreak before ']'.",
55+
missingOpeningLinebreak: "A linebreak is required after '['.",
56+
missingClosingLinebreak: "A linebreak is required before ']'.",
57+
},
1858
},
1959
create(context) {
20-
return defineWrapperListener(coreRule, context, context.options);
60+
const sourceCode = getSourceCode(context);
61+
62+
/**
63+
* Normalizes a given option value.
64+
* @param option An option value to parse.
65+
* @returns Normalized option object.
66+
*/
67+
function normalizeOptionValue(option: Schema0) {
68+
let consistent = false;
69+
let multiline = false;
70+
let minItems = 0;
71+
72+
if (option) {
73+
if (option === "consistent") {
74+
consistent = true;
75+
minItems = Number.POSITIVE_INFINITY;
76+
} else if (
77+
option === "always" ||
78+
(typeof option !== "string" && option.minItems === 0)
79+
) {
80+
minItems = 0;
81+
} else if (option === "never") {
82+
minItems = Number.POSITIVE_INFINITY;
83+
} else {
84+
multiline = Boolean(option.multiline);
85+
minItems = option.minItems || Number.POSITIVE_INFINITY;
86+
}
87+
} else {
88+
consistent = false;
89+
multiline = true;
90+
minItems = Number.POSITIVE_INFINITY;
91+
}
92+
93+
return { consistent, multiline, minItems };
94+
}
95+
96+
/**
97+
* Normalizes a given option value.
98+
* @param options An option value to parse.
99+
* @returns Normalized option object.
100+
*/
101+
function normalizeOptions(options: Schema0) {
102+
const value = normalizeOptionValue(options);
103+
104+
return { JSONArrayExpression: value, JSONArrayPattern: value };
105+
}
106+
107+
/**
108+
* Reports that there shouldn't be a linebreak after the first token
109+
* @param node The node to report in the event of an error.
110+
* @param token The token to use for the report.
111+
*/
112+
function reportNoBeginningLinebreak(node: AST.JSONNode, token: Token) {
113+
context.report({
114+
node: node as any,
115+
loc: token.loc,
116+
messageId: "unexpectedOpeningLinebreak",
117+
fix(fixer) {
118+
const nextToken = sourceCode.getTokenAfter(token, {
119+
includeComments: true,
120+
});
121+
122+
if (!nextToken || isCommentToken(nextToken)) return null;
123+
124+
return fixer.removeRange([token.range[1], nextToken.range[0]]);
125+
},
126+
});
127+
}
128+
129+
/**
130+
* Reports that there shouldn't be a linebreak before the last token
131+
* @param node The node to report in the event of an error.
132+
* @param token The token to use for the report.
133+
*/
134+
function reportNoEndingLinebreak(node: AST.JSONNode, token: Token) {
135+
context.report({
136+
node: node as any,
137+
loc: token.loc,
138+
messageId: "unexpectedClosingLinebreak",
139+
fix(fixer) {
140+
const previousToken = sourceCode.getTokenBefore(token, {
141+
includeComments: true,
142+
});
143+
144+
if (!previousToken || isCommentToken(previousToken)) return null;
145+
146+
return fixer.removeRange([previousToken.range[1], token.range[0]]);
147+
},
148+
});
149+
}
150+
151+
/**
152+
* Reports that there should be a linebreak after the first token
153+
* @param node The node to report in the event of an error.
154+
* @param token The token to use for the report.
155+
*/
156+
function reportRequiredBeginningLinebreak(
157+
node: AST.JSONNode,
158+
token: Token,
159+
) {
160+
context.report({
161+
node: node as any,
162+
loc: token.loc,
163+
messageId: "missingOpeningLinebreak",
164+
fix(fixer) {
165+
return fixer.insertTextAfter(token, "\n");
166+
},
167+
});
168+
}
169+
170+
/**
171+
* Reports that there should be a linebreak before the last token
172+
* @param node The node to report in the event of an error.
173+
* @param token The token to use for the report.
174+
*/
175+
function reportRequiredEndingLinebreak(node: AST.JSONNode, token: Token) {
176+
context.report({
177+
node: node as any,
178+
loc: token.loc,
179+
messageId: "missingClosingLinebreak",
180+
fix(fixer) {
181+
return fixer.insertTextBefore(token, "\n");
182+
},
183+
});
184+
}
185+
186+
/**
187+
* Reports a given node if it violated this rule.
188+
* @param node A node to check. This is an ArrayExpression node or an ArrayPattern node.
189+
*/
190+
function check(node: AST.JSONNode) {
191+
// @ts-expect-error type cast
192+
const elements = node.elements;
193+
const normalizedOptions = normalizeOptions(context.options[0]);
194+
// @ts-expect-error type cast
195+
const options = normalizedOptions[node.type];
196+
const openBracket = sourceCode.getFirstToken(node as any)!;
197+
const closeBracket = sourceCode.getLastToken(node as any)!;
198+
const firstIncComment = sourceCode.getTokenAfter(openBracket, {
199+
includeComments: true,
200+
})!;
201+
const lastIncComment = sourceCode.getTokenBefore(closeBracket, {
202+
includeComments: true,
203+
})!;
204+
const first = sourceCode.getTokenAfter(openBracket)!;
205+
const last = sourceCode.getTokenBefore(closeBracket)!;
206+
const needsLinebreaks =
207+
elements.length >= options.minItems ||
208+
(options.multiline &&
209+
elements.length > 0 &&
210+
firstIncComment.loc!.start.line !== lastIncComment.loc!.end.line) ||
211+
(elements.length === 0 &&
212+
firstIncComment.type === "Block" &&
213+
firstIncComment.loc!.start.line !== lastIncComment.loc!.end.line &&
214+
firstIncComment === lastIncComment) ||
215+
(options.consistent &&
216+
openBracket.loc.end.line !== first.loc.start.line);
217+
218+
/**
219+
* Use tokens or comments to check multiline or not.
220+
* But use only tokens to check whether linebreaks are needed.
221+
* This allows:
222+
* var arr = [ // eslint-disable-line foo
223+
* 'a'
224+
* ]
225+
*/
226+
227+
if (needsLinebreaks) {
228+
if (isTokenOnSameLine(openBracket, first))
229+
reportRequiredBeginningLinebreak(node, openBracket);
230+
if (isTokenOnSameLine(last, closeBracket))
231+
reportRequiredEndingLinebreak(node, closeBracket);
232+
} else {
233+
if (!isTokenOnSameLine(openBracket, first))
234+
reportNoBeginningLinebreak(node, openBracket);
235+
if (!isTokenOnSameLine(last, closeBracket))
236+
reportNoEndingLinebreak(node, closeBracket);
237+
}
238+
}
239+
240+
return {
241+
JSONArrayExpression: check,
242+
};
21243
},
22244
});

0 commit comments

Comments
 (0)