eslint-plugin-flawless 0.1.11 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -13
- package/dist/index.d.mts +82 -4
- package/dist/index.mjs +1 -1
- package/dist/oxlint.mjs +1 -1
- package/dist/{plugin-BvQt8gSt.mjs → plugin-DFLOyhkl.mjs} +1497 -32
- package/dist/rules/arrow-return-style/worker.d.mts +25 -0
- package/dist/rules/arrow-return-style/worker.mjs +90 -0
- package/package.json +8 -1
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
2
|
import { ASTUtils, AST_NODE_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils";
|
|
3
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createSyncFn } from "synckit";
|
|
7
|
+
import { RuleCreator, getParserServices } from "@typescript-eslint/utils/eslint-utils";
|
|
8
|
+
import { findVariable, isArrowToken, isOpeningParenToken } from "@typescript-eslint/utils/ast-utils";
|
|
3
9
|
import { DefinitionType, ImplicitLibVariable, PatternVisitor, ScopeType, Visitor } from "@typescript-eslint/scope-manager";
|
|
4
10
|
import { requiresQuoting } from "@typescript-eslint/type-utils";
|
|
5
11
|
import assert from "node:assert";
|
|
12
|
+
import { getStaticJSONValue, parseForESLint } from "jsonc-eslint-parser";
|
|
6
13
|
import * as core from "@eslint-react/core";
|
|
7
|
-
import { findVariable } from "@typescript-eslint/utils/ast-utils";
|
|
8
14
|
import ts9 from "typescript";
|
|
9
15
|
import { getStaticTOMLValue } from "toml-eslint-parser";
|
|
10
16
|
//#region package.json
|
|
11
17
|
var name = "eslint-plugin-flawless";
|
|
12
|
-
var version = "0.
|
|
18
|
+
var version = "1.0.0";
|
|
13
19
|
var repository = {
|
|
14
20
|
"url": "git+https://github.com/christopher-buss/eslint-plugin-flawless.git",
|
|
15
21
|
"type": "git"
|
|
@@ -78,24 +84,538 @@ function createFlawlessRule({ createOnce, ...meta }) {
|
|
|
78
84
|
return Object.assign(module, { createOnce });
|
|
79
85
|
}
|
|
80
86
|
//#endregion
|
|
87
|
+
//#region src/rules/arrow-return-style/rule.ts
|
|
88
|
+
const RULE_NAME$15 = "arrow-return-style";
|
|
89
|
+
const IMPLICIT = "useImplicitReturn";
|
|
90
|
+
const EXPLICIT = "useExplicitReturn";
|
|
91
|
+
const COMPLEX_EXPLICIT = "useExplicitReturnComplex";
|
|
92
|
+
const DEFAULTS$1 = {
|
|
93
|
+
jsxAlwaysUseExplicitReturn: false,
|
|
94
|
+
maxLen: 80,
|
|
95
|
+
maxObjectProperties: 4,
|
|
96
|
+
namedExportsAlwaysUseExplicitReturn: true,
|
|
97
|
+
objectReturnStyle: "complex-explicit",
|
|
98
|
+
tabWidth: 4,
|
|
99
|
+
useOxfmt: true
|
|
100
|
+
};
|
|
101
|
+
/** `undefined` = not initialized yet; `null` = initialization failed. */
|
|
102
|
+
let formatSync;
|
|
103
|
+
function getFormatSync() {
|
|
104
|
+
if (formatSync !== void 0) return formatSync;
|
|
105
|
+
try {
|
|
106
|
+
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
107
|
+
const workerPath = [
|
|
108
|
+
path.join(directory, "worker.mjs"),
|
|
109
|
+
path.join(directory, "rules", "arrow-return-style", "worker.mjs"),
|
|
110
|
+
path.join(directory, "worker.ts")
|
|
111
|
+
].find((candidate) => existsSync(candidate));
|
|
112
|
+
if (workerPath === void 0) formatSync = null;
|
|
113
|
+
else formatSync = createSyncFn(workerPath, {
|
|
114
|
+
timeout: 15e3,
|
|
115
|
+
...workerPath.endsWith(".ts") ? { tsRunner: "tsx" } : {}
|
|
116
|
+
});
|
|
117
|
+
} catch {
|
|
118
|
+
formatSync = null;
|
|
119
|
+
}
|
|
120
|
+
return formatSync;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Worker verdicts cached across files and fix passes: a snippet formats the
|
|
124
|
+
* same way regardless of which file (or autofix iteration) asked. Bounded LRU
|
|
125
|
+
* so long sessions (editors, watch mode) cannot grow it without limit.
|
|
126
|
+
*/
|
|
127
|
+
const formatCache = /* @__PURE__ */ new Map();
|
|
128
|
+
const FORMAT_CACHE_MAX_ENTRIES = 1024;
|
|
129
|
+
function formatCacheGet(key) {
|
|
130
|
+
const value = formatCache.get(key);
|
|
131
|
+
if (value !== void 0) {
|
|
132
|
+
formatCache.delete(key);
|
|
133
|
+
formatCache.set(key, value);
|
|
134
|
+
}
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
function formatCacheSet(key, value) {
|
|
138
|
+
formatCache.delete(key);
|
|
139
|
+
formatCache.set(key, value);
|
|
140
|
+
if (formatCache.size > FORMAT_CACHE_MAX_ENTRIES) {
|
|
141
|
+
const oldest = formatCache.keys().next().value;
|
|
142
|
+
if (oldest !== void 0) formatCache.delete(oldest);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Visual width of `text` with tabs expanded to `tabWidth`-column tab stops.
|
|
147
|
+
*
|
|
148
|
+
* @param text - The text to measure.
|
|
149
|
+
* @param tabWidth - Columns a tab occupies.
|
|
150
|
+
* @returns The tab-expanded width in columns.
|
|
151
|
+
*/
|
|
152
|
+
function expandedWidth(text, tabWidth) {
|
|
153
|
+
let width = 0;
|
|
154
|
+
for (const character of text) width += character === " " ? tabWidth - width % tabWidth : 1;
|
|
155
|
+
return width;
|
|
156
|
+
}
|
|
157
|
+
function leadingWhitespace(line) {
|
|
158
|
+
return /^[\t ]*/.exec(line)?.[0] ?? "";
|
|
159
|
+
}
|
|
160
|
+
function isJsx(node) {
|
|
161
|
+
return node.type === AST_NODE_TYPES.JSXElement || node.type === AST_NODE_TYPES.JSXFragment;
|
|
162
|
+
}
|
|
163
|
+
function isNamedExportArrow(node) {
|
|
164
|
+
if (node.parent.type !== AST_NODE_TYPES.VariableDeclarator) return false;
|
|
165
|
+
return node.parent.parent.parent.type === AST_NODE_TYPES.ExportNamedDeclaration;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Does the collapsed body need wrapping parens to stay an expression body?
|
|
169
|
+
*
|
|
170
|
+
* @param node - The would-be implicit body expression.
|
|
171
|
+
* @returns Whether the fixer must wrap the body in parentheses.
|
|
172
|
+
*/
|
|
173
|
+
function needsParens(node) {
|
|
174
|
+
return node.type === AST_NODE_TYPES.ObjectExpression || node.type === AST_NODE_TYPES.SequenceExpression;
|
|
175
|
+
}
|
|
176
|
+
function countObjectComplexity(node) {
|
|
177
|
+
let features = 0;
|
|
178
|
+
for (const property of node.properties) {
|
|
179
|
+
if (property.type === AST_NODE_TYPES.SpreadElement) {
|
|
180
|
+
features += 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (property.computed) features += 1;
|
|
184
|
+
if (property.value.type === AST_NODE_TYPES.CallExpression) features += 1;
|
|
185
|
+
}
|
|
186
|
+
return features;
|
|
187
|
+
}
|
|
188
|
+
function isComplexLiteral(node, maxObjectProperties) {
|
|
189
|
+
if (node.type === AST_NODE_TYPES.ObjectExpression) {
|
|
190
|
+
if (node.properties.length > maxObjectProperties) return true;
|
|
191
|
+
return countObjectComplexity(node) >= 2;
|
|
192
|
+
}
|
|
193
|
+
if (node.type === AST_NODE_TYPES.ArrayExpression) {
|
|
194
|
+
const spreads = node.elements.filter((element) => element?.type === AST_NODE_TYPES.SpreadElement).length;
|
|
195
|
+
const calls = node.elements.filter((element) => element?.type === AST_NODE_TYPES.CallExpression).length;
|
|
196
|
+
return spreads >= 1 && node.elements.length > 1 || calls >= 2 || spreads + calls >= 2;
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
function isLiteralBody(node) {
|
|
201
|
+
return node.type === AST_NODE_TYPES.ObjectExpression || node.type === AST_NODE_TYPES.ArrayExpression;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Walks up to the outermost node whose parent is the Program.
|
|
205
|
+
*
|
|
206
|
+
* @param node - The node to walk up from.
|
|
207
|
+
* @returns The top-level statement containing `node`.
|
|
208
|
+
*/
|
|
209
|
+
function statementOf(node) {
|
|
210
|
+
let current = node;
|
|
211
|
+
while (current.parent !== void 0 && current.parent.type !== AST_NODE_TYPES.Program) current = current.parent;
|
|
212
|
+
return current;
|
|
213
|
+
}
|
|
214
|
+
function pushChildNodes$1(stack, value) {
|
|
215
|
+
const candidates = Array.isArray(value) ? value : [value];
|
|
216
|
+
for (const item of candidates) if (typeof item === "object" && item !== null && "type" in item) stack.push(item);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Collects arrow functions in `root`'s subtree, ordered by source position.
|
|
220
|
+
*
|
|
221
|
+
* @param root - The subtree to search.
|
|
222
|
+
* @returns All arrow function nodes, sorted by range start.
|
|
223
|
+
*/
|
|
224
|
+
function collectArrows(root) {
|
|
225
|
+
const arrows = [];
|
|
226
|
+
const stack = [root];
|
|
227
|
+
while (stack.length > 0) {
|
|
228
|
+
const current = stack.pop();
|
|
229
|
+
if (current === void 0) continue;
|
|
230
|
+
if (current.type === AST_NODE_TYPES.ArrowFunctionExpression) arrows.push(current);
|
|
231
|
+
for (const [key, value] of Object.entries(current)) if (key !== "parent" && key !== "loc" && key !== "range") pushChildNodes$1(stack, value);
|
|
232
|
+
}
|
|
233
|
+
return arrows.sort((a, b) => a.range[0] - b.range[0]);
|
|
234
|
+
}
|
|
235
|
+
const arrowReturnStyle = createFlawlessRule({
|
|
236
|
+
name: RULE_NAME$15,
|
|
237
|
+
createOnce(context) {
|
|
238
|
+
let config = DEFAULTS$1;
|
|
239
|
+
let sourceCode;
|
|
240
|
+
let lines;
|
|
241
|
+
let pendingConsults;
|
|
242
|
+
/**
|
|
243
|
+
* Effective line-length limit for emitted lines: the fixer must never
|
|
244
|
+
* produce a line the formatter would immediately rewrap.
|
|
245
|
+
*
|
|
246
|
+
* @returns The maximum permitted tab-expanded line width.
|
|
247
|
+
*/
|
|
248
|
+
function limit() {
|
|
249
|
+
const { maxLen, useOxfmt } = config;
|
|
250
|
+
return useOxfmt === false ? maxLen : Math.min(maxLen, printWidth());
|
|
251
|
+
}
|
|
252
|
+
function printWidth() {
|
|
253
|
+
const { maxLen, useOxfmt } = config;
|
|
254
|
+
if (typeof useOxfmt === "object" && typeof useOxfmt.printWidth === "number") return useOxfmt.printWidth;
|
|
255
|
+
return maxLen;
|
|
256
|
+
}
|
|
257
|
+
function width(text) {
|
|
258
|
+
return expandedWidth(text, config.tabWidth);
|
|
259
|
+
}
|
|
260
|
+
function lineOf(lineNumber) {
|
|
261
|
+
return lines[lineNumber - 1] ?? "";
|
|
262
|
+
}
|
|
263
|
+
function objectStyleWantsExplicit(body) {
|
|
264
|
+
if (!isLiteralBody(body) || config.objectReturnStyle === "off") return false;
|
|
265
|
+
if (config.objectReturnStyle === "always-explicit") return true;
|
|
266
|
+
return isComplexLiteral(body, config.maxObjectProperties);
|
|
267
|
+
}
|
|
268
|
+
function arrowTokenOf(node) {
|
|
269
|
+
let token = sourceCode.getTokenBefore(node.body);
|
|
270
|
+
while (token !== null && token.value === "(") token = sourceCode.getTokenBefore(token);
|
|
271
|
+
if (token?.value !== "=>") throw new Error("arrow-return-style: could not locate the => token");
|
|
272
|
+
return token;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* The `(`/`)` pair directly wrapping the body, if any.
|
|
276
|
+
*
|
|
277
|
+
* @param node - Function whose body may be parenthesized.
|
|
278
|
+
* @param arrowToken - Token of the `=>` operator, used to distinguish
|
|
279
|
+
* body parens from parameter-list parens.
|
|
280
|
+
* @returns The wrapping paren tokens, or `null` when not parenthesized.
|
|
281
|
+
*/
|
|
282
|
+
function bodyParens(node, arrowToken) {
|
|
283
|
+
const before = sourceCode.getTokenBefore(node.body);
|
|
284
|
+
if (before?.value !== "(" || before.range[0] < arrowToken.range[1]) return null;
|
|
285
|
+
const after = sourceCode.getTokenAfter(node.body);
|
|
286
|
+
if (after?.value !== ")") return null;
|
|
287
|
+
return {
|
|
288
|
+
close: after,
|
|
289
|
+
open: before
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Prepares the oxfmt question for `node`: how would the formatter render
|
|
294
|
+
* the enclosing statement once the candidate fix is applied? `collapse`
|
|
295
|
+
* supplies the implicit-direction candidate (the block body textually
|
|
296
|
+
* replaced by its collapsed expression); omitting it asks about the
|
|
297
|
+
* statement as written.
|
|
298
|
+
*
|
|
299
|
+
* @param node - The arrow function in question.
|
|
300
|
+
* @param collapse - The implicit-direction candidate, if any.
|
|
301
|
+
* @returns The consult, or `null` when the arrow cannot be located.
|
|
302
|
+
*/
|
|
303
|
+
function buildConsult(node, collapse) {
|
|
304
|
+
const statement = statementOf(node);
|
|
305
|
+
const arrowIndex = collectArrows(statement).findIndex((arrow) => arrow.range[0] === node.range[0]);
|
|
306
|
+
if (arrowIndex === -1) return null;
|
|
307
|
+
const offset = statement.range[0];
|
|
308
|
+
const source = sourceCode.getText(statement);
|
|
309
|
+
const snippet = collapse === void 0 ? source : source.slice(0, collapse.block.range[0] - offset) + collapse.replacement + source.slice(collapse.block.range[1] - offset);
|
|
310
|
+
const request = {
|
|
311
|
+
arrowIndex,
|
|
312
|
+
code: `${snippet}\n`,
|
|
313
|
+
printWidth: printWidth(),
|
|
314
|
+
tabWidth: config.tabWidth
|
|
315
|
+
};
|
|
316
|
+
return {
|
|
317
|
+
baseIndent: leadingWhitespace(lineOf(statement.loc.start.line)),
|
|
318
|
+
cacheKey: `${arrowIndex}${request.printWidth}${request.tabWidth}${snippet}`,
|
|
319
|
+
node,
|
|
320
|
+
request
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Resolves every deferred oxfmt consult with a single worker round-trip:
|
|
325
|
+
* would the formatter render each candidate arrow (params through body) on
|
|
326
|
+
* a single line that fits `maxLen`? Explicit consults report when it
|
|
327
|
+
* cannot, implicit consults report when it can. Fails open (no report)
|
|
328
|
+
* when the worker or oxfmt is unavailable so that an unavailable formatter
|
|
329
|
+
* can never introduce reports it would have to fight over.
|
|
330
|
+
*/
|
|
331
|
+
function resolvePendingConsults() {
|
|
332
|
+
if (pendingConsults.length === 0) return;
|
|
333
|
+
const consults = pendingConsults;
|
|
334
|
+
pendingConsults = [];
|
|
335
|
+
const worker = getFormatSync();
|
|
336
|
+
if (worker === null) return;
|
|
337
|
+
const misses = /* @__PURE__ */ new Map();
|
|
338
|
+
for (const consult of consults) if (formatCacheGet(consult.cacheKey) === void 0) misses.set(consult.cacheKey, consult.request);
|
|
339
|
+
if (misses.size > 0) {
|
|
340
|
+
let responses = [];
|
|
341
|
+
try {
|
|
342
|
+
responses = worker([...misses.values()]);
|
|
343
|
+
} catch {
|
|
344
|
+
responses = [];
|
|
345
|
+
}
|
|
346
|
+
for (const [index, cacheKey] of [...misses.keys()].entries()) {
|
|
347
|
+
const response = responses[index];
|
|
348
|
+
if (response !== void 0) formatCacheSet(cacheKey, response);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
for (const consult of consults) {
|
|
352
|
+
const response = formatCacheGet(consult.cacheKey);
|
|
353
|
+
if (response === void 0) continue;
|
|
354
|
+
if (response.lineText === null) continue;
|
|
355
|
+
const fits = response.singleLine && width(consult.baseIndent) + width(response.lineText) <= config.maxLen;
|
|
356
|
+
if (consult.kind === "implicit") {
|
|
357
|
+
if (fits) reportImplicit(consult.node, consult.block, consult.replacement);
|
|
358
|
+
} else if (!fits) reportExplicit(consult.node, EXPLICIT);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Derives the one-level indent unit for an inserted block: prefer the
|
|
363
|
+
* offset between the arrow's line and the body's (or its continuation's)
|
|
364
|
+
* deeper indentation; otherwise infer from the indent character in use.
|
|
365
|
+
*
|
|
366
|
+
* @param arrowIndent - Leading whitespace of the arrow's line.
|
|
367
|
+
* @param node - The arrow function being fixed.
|
|
368
|
+
* @param bodyStart - The body's first token or node (including parens).
|
|
369
|
+
* @param bodyEnd - The body's last token or node (including parens).
|
|
370
|
+
* @returns The whitespace string for one indentation level.
|
|
371
|
+
*/
|
|
372
|
+
function indentUnit(arrowIndent, node, bodyStart, bodyEnd) {
|
|
373
|
+
const referenceLines = [];
|
|
374
|
+
const arrowLine = bodyStart.loc.start.line;
|
|
375
|
+
if (bodyStart.loc.start.line !== node.loc.start.line) referenceLines.push(bodyStart.loc.start.line);
|
|
376
|
+
else if (bodyEnd.loc.end.line > arrowLine) referenceLines.push(arrowLine + 1);
|
|
377
|
+
for (const lineNumber of referenceLines) {
|
|
378
|
+
const reference = leadingWhitespace(lineOf(lineNumber));
|
|
379
|
+
if (reference.length > arrowIndent.length && reference.startsWith(arrowIndent)) return reference.slice(arrowIndent.length);
|
|
380
|
+
}
|
|
381
|
+
if (arrowIndent.includes(" ")) return " ";
|
|
382
|
+
return arrowIndent.length > 0 ? " " : " ";
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* The `)` of a wrapped sole-argument call whose trailing comma the fix
|
|
386
|
+
* should absorb. `foo(() =>\n\tbody,\n)` only carries that comma because
|
|
387
|
+
* the argument was wrapped; once the block body hugs the call again the
|
|
388
|
+
* comma is debris (`},\n)` where the formatter writes `})`).
|
|
389
|
+
*
|
|
390
|
+
* @param node - The arrow function being converted.
|
|
391
|
+
* @param bodyEnd - The body's last token or node (including parens).
|
|
392
|
+
* @returns The closing paren to absorb up to, or `null`.
|
|
393
|
+
*/
|
|
394
|
+
function danglingCloser(node, bodyEnd) {
|
|
395
|
+
const { loc, parent } = node;
|
|
396
|
+
if (!(parent.type === AST_NODE_TYPES.CallExpression || parent.type === AST_NODE_TYPES.NewExpression) || parent.arguments.length !== 1 || parent.arguments[0] !== node) return null;
|
|
397
|
+
const opener = sourceCode.getTokenBefore(node);
|
|
398
|
+
if (opener?.value !== "(" || opener.loc.end.line !== loc.start.line) return null;
|
|
399
|
+
const comma = sourceCode.getTokenAfter(bodyEnd);
|
|
400
|
+
if (comma?.value !== ",") return null;
|
|
401
|
+
const closer = sourceCode.getTokenAfter(comma);
|
|
402
|
+
if (closer?.value !== ")" || closer.range[1] !== parent.range[1]) return null;
|
|
403
|
+
if (sourceCode.getCommentsBefore(closer).length > 0) return null;
|
|
404
|
+
return closer;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Reports and fixes an implicit arrow into an explicit block body.
|
|
408
|
+
*
|
|
409
|
+
* @param node - The arrow function to convert.
|
|
410
|
+
* @param messageId - The violation to report.
|
|
411
|
+
*/
|
|
412
|
+
function reportExplicit(node, messageId) {
|
|
413
|
+
const arrowToken = arrowTokenOf(node);
|
|
414
|
+
const parens = bodyParens(node, arrowToken);
|
|
415
|
+
const bodyStart = parens?.open ?? node.body;
|
|
416
|
+
const bodyEnd = parens?.close ?? node.body;
|
|
417
|
+
const comments = sourceCode.getCommentsBefore(bodyStart);
|
|
418
|
+
const arrowIndent = leadingWhitespace(lineOf(node.loc.start.line));
|
|
419
|
+
const targetIndent = arrowIndent + indentUnit(arrowIndent, node, bodyStart, bodyEnd);
|
|
420
|
+
const bodyLines = sourceCode.getText(node.body).split("\n");
|
|
421
|
+
const [firstLine, ...restLines] = bodyLines;
|
|
422
|
+
const lastLineIndent = leadingWhitespace(bodyLines.at(-1) ?? "");
|
|
423
|
+
let shifted = restLines;
|
|
424
|
+
if (restLines.length > 0) {
|
|
425
|
+
if (targetIndent.startsWith(lastLineIndent)) {
|
|
426
|
+
const prefix = targetIndent.slice(lastLineIndent.length);
|
|
427
|
+
shifted = restLines.map((line) => prefix + line);
|
|
428
|
+
} else if (lastLineIndent.startsWith(targetIndent)) {
|
|
429
|
+
const strip = lastLineIndent.length - targetIndent.length;
|
|
430
|
+
shifted = restLines.map((line) => {
|
|
431
|
+
return leadingWhitespace(line).length >= strip ? line.slice(strip) : line;
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const commentLines = comments.map((comment) => targetIndent + sourceCode.getText(comment));
|
|
436
|
+
const returnLine = `${targetIndent}return ${[firstLine, ...shifted].join("\n")};`;
|
|
437
|
+
const replacement = ` {\n${[...commentLines, returnLine].join("\n")}\n${arrowIndent}}`;
|
|
438
|
+
const end = danglingCloser(node, bodyEnd)?.range[0] ?? bodyEnd.range[1];
|
|
439
|
+
context.report({
|
|
440
|
+
fix: (fixer) => fixer.replaceTextRange([arrowToken.range[1], end], replacement),
|
|
441
|
+
messageId,
|
|
442
|
+
node
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Reports and fixes a single-`return` block into an implicit body.
|
|
447
|
+
*
|
|
448
|
+
* @param node - The arrow function to convert.
|
|
449
|
+
* @param block - The block body being replaced.
|
|
450
|
+
* @param replacement - The implicit body text.
|
|
451
|
+
*/
|
|
452
|
+
function reportImplicit(node, block, replacement) {
|
|
453
|
+
context.report({
|
|
454
|
+
fix: (fixer) => fixer.replaceTextRange(block.range, replacement),
|
|
455
|
+
messageId: IMPLICIT,
|
|
456
|
+
node
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
function checkBlockBody(node) {
|
|
460
|
+
const block = node.body;
|
|
461
|
+
if (block.body.length !== 1) return;
|
|
462
|
+
const [statement] = block.body;
|
|
463
|
+
if (statement?.type !== AST_NODE_TYPES.ReturnStatement || statement.argument === null) return;
|
|
464
|
+
if (sourceCode.getCommentsInside(block).length > 0) return;
|
|
465
|
+
const { argument } = statement;
|
|
466
|
+
if (isJsx(argument) && config.jsxAlwaysUseExplicitReturn) return;
|
|
467
|
+
if (isNamedExportArrow(node) && config.namedExportsAlwaysUseExplicitReturn) return;
|
|
468
|
+
if (objectStyleWantsExplicit(argument)) return;
|
|
469
|
+
if (argument.loc.start.line !== argument.loc.end.line) return;
|
|
470
|
+
const argumentText = sourceCode.getText(argument);
|
|
471
|
+
const collapsed = needsParens(argument) ? `(${argumentText})` : argumentText;
|
|
472
|
+
const prefix = lineOf(block.loc.start.line).slice(0, block.loc.start.column);
|
|
473
|
+
const suffix = lineOf(block.loc.end.line).slice(block.loc.end.column);
|
|
474
|
+
if (width(prefix + collapsed + suffix) > limit()) return;
|
|
475
|
+
if (statementOf(node).loc.end.line === block.loc.end.line || config.useOxfmt === false) {
|
|
476
|
+
reportImplicit(node, block, collapsed);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const consult = buildConsult(node, {
|
|
480
|
+
block,
|
|
481
|
+
replacement: collapsed
|
|
482
|
+
});
|
|
483
|
+
if (consult !== null) pendingConsults.push({
|
|
484
|
+
...consult,
|
|
485
|
+
block,
|
|
486
|
+
kind: "implicit",
|
|
487
|
+
replacement: collapsed
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
function checkExpressionBody(node) {
|
|
491
|
+
const body = node.body;
|
|
492
|
+
const arrowToken = arrowTokenOf(node);
|
|
493
|
+
const parens = bodyParens(node, arrowToken);
|
|
494
|
+
const bodyStart = parens?.open ?? body;
|
|
495
|
+
const bodyEnd = parens?.close ?? body;
|
|
496
|
+
if (sourceCode.getCommentsBefore(bodyStart).length > 0) {
|
|
497
|
+
reportExplicit(node, EXPLICIT);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (isJsx(body) && config.jsxAlwaysUseExplicitReturn) {
|
|
501
|
+
reportExplicit(node, EXPLICIT);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (isNamedExportArrow(node) && config.namedExportsAlwaysUseExplicitReturn) {
|
|
505
|
+
reportExplicit(node, EXPLICIT);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (bodyStart.loc.start.line !== arrowToken.loc.end.line) {
|
|
509
|
+
reportExplicit(node, EXPLICIT);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (bodyEnd.loc.end.line !== bodyStart.loc.start.line) {
|
|
513
|
+
if (body.type === AST_NODE_TYPES.ObjectExpression) reportExplicit(node, EXPLICIT);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (width(lineOf(bodyStart.loc.start.line)) > limit()) {
|
|
517
|
+
if (config.useOxfmt === false) {
|
|
518
|
+
reportExplicit(node, EXPLICIT);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const consult = buildConsult(node);
|
|
522
|
+
if (consult !== null) pendingConsults.push({
|
|
523
|
+
...consult,
|
|
524
|
+
kind: "explicit"
|
|
525
|
+
});
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (objectStyleWantsExplicit(body)) reportExplicit(node, COMPLEX_EXPLICIT);
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
"ArrowFunctionExpression": function(node) {
|
|
532
|
+
if (node.body.type === AST_NODE_TYPES.BlockStatement) checkBlockBody(node);
|
|
533
|
+
else checkExpressionBody(node);
|
|
534
|
+
},
|
|
535
|
+
"before": function() {
|
|
536
|
+
config = {
|
|
537
|
+
...DEFAULTS$1,
|
|
538
|
+
...context.options[0]
|
|
539
|
+
};
|
|
540
|
+
({sourceCode} = context);
|
|
541
|
+
lines = [...sourceCode.lines];
|
|
542
|
+
pendingConsults = [];
|
|
543
|
+
},
|
|
544
|
+
"Program:exit": function() {
|
|
545
|
+
resolvePendingConsults();
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
},
|
|
549
|
+
defaultOptions: [DEFAULTS$1],
|
|
550
|
+
meta: {
|
|
551
|
+
docs: {
|
|
552
|
+
description: "Enforce arrow function return style based on line length",
|
|
553
|
+
requiresTypeChecking: false
|
|
554
|
+
},
|
|
555
|
+
fixable: "code",
|
|
556
|
+
messages: {
|
|
557
|
+
[COMPLEX_EXPLICIT]: "Use an explicit return block for complex object or array bodies.",
|
|
558
|
+
[EXPLICIT]: "Use an explicit return block for this arrow function body.",
|
|
559
|
+
[IMPLICIT]: "Use an implicit return for this arrow function body."
|
|
560
|
+
},
|
|
561
|
+
schema: [{
|
|
562
|
+
additionalProperties: false,
|
|
563
|
+
properties: {
|
|
564
|
+
jsxAlwaysUseExplicitReturn: { type: "boolean" },
|
|
565
|
+
maxLen: {
|
|
566
|
+
minimum: 0,
|
|
567
|
+
type: "integer"
|
|
568
|
+
},
|
|
569
|
+
maxObjectProperties: {
|
|
570
|
+
minimum: 0,
|
|
571
|
+
type: "integer"
|
|
572
|
+
},
|
|
573
|
+
namedExportsAlwaysUseExplicitReturn: { type: "boolean" },
|
|
574
|
+
objectReturnStyle: {
|
|
575
|
+
enum: [
|
|
576
|
+
"always-explicit",
|
|
577
|
+
"complex-explicit",
|
|
578
|
+
"off"
|
|
579
|
+
],
|
|
580
|
+
type: "string"
|
|
581
|
+
},
|
|
582
|
+
tabWidth: {
|
|
583
|
+
minimum: 1,
|
|
584
|
+
type: "integer"
|
|
585
|
+
},
|
|
586
|
+
useOxfmt: { oneOf: [{ type: "boolean" }, {
|
|
587
|
+
additionalProperties: false,
|
|
588
|
+
properties: { printWidth: {
|
|
589
|
+
minimum: 0,
|
|
590
|
+
type: "integer"
|
|
591
|
+
} },
|
|
592
|
+
type: "object"
|
|
593
|
+
}] }
|
|
594
|
+
},
|
|
595
|
+
type: "object"
|
|
596
|
+
}],
|
|
597
|
+
type: "suggestion"
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
//#endregion
|
|
81
601
|
//#region src/rules/jsx-shorthand-boolean/rule.ts
|
|
82
|
-
const RULE_NAME$
|
|
83
|
-
const MESSAGE_ID$
|
|
84
|
-
const messages$
|
|
85
|
-
function createOnce$
|
|
602
|
+
const RULE_NAME$14 = "jsx-shorthand-boolean";
|
|
603
|
+
const MESSAGE_ID$7 = "setAttributeValue";
|
|
604
|
+
const messages$14 = { [MESSAGE_ID$7]: "Set an explicit value for boolean attribute '{{name}}'." };
|
|
605
|
+
function createOnce$9(context) {
|
|
86
606
|
return { JSXAttribute(node) {
|
|
87
607
|
if (node.value !== null) return;
|
|
88
608
|
context.report({
|
|
89
609
|
data: { name: context.sourceCode.getText(node.name) },
|
|
90
610
|
fix: (fixer) => fixer.insertTextAfter(node.name, "={true}"),
|
|
91
|
-
messageId: MESSAGE_ID$
|
|
611
|
+
messageId: MESSAGE_ID$7,
|
|
92
612
|
node
|
|
93
613
|
});
|
|
94
614
|
} };
|
|
95
615
|
}
|
|
96
616
|
const jsxShorthandBoolean = createFlawlessRule({
|
|
97
|
-
name: RULE_NAME$
|
|
98
|
-
createOnce: createOnce$
|
|
617
|
+
name: RULE_NAME$14,
|
|
618
|
+
createOnce: createOnce$9,
|
|
99
619
|
defaultOptions: [],
|
|
100
620
|
meta: {
|
|
101
621
|
docs: {
|
|
@@ -105,23 +625,23 @@ const jsxShorthandBoolean = createFlawlessRule({
|
|
|
105
625
|
},
|
|
106
626
|
fixable: "code",
|
|
107
627
|
hasSuggestions: false,
|
|
108
|
-
messages: messages$
|
|
628
|
+
messages: messages$14,
|
|
109
629
|
schema: [],
|
|
110
630
|
type: "suggestion"
|
|
111
631
|
}
|
|
112
632
|
});
|
|
113
633
|
//#endregion
|
|
114
634
|
//#region src/rules/jsx-shorthand-fragment/rule.ts
|
|
115
|
-
const RULE_NAME$
|
|
635
|
+
const RULE_NAME$13 = "jsx-shorthand-fragment";
|
|
116
636
|
const MESSAGE_ID_NAMED = "useNamedFragment";
|
|
117
637
|
const MESSAGE_ID_SHORTHAND = "useShorthandFragment";
|
|
118
638
|
const DEFAULT_MODE = "syntax";
|
|
119
639
|
const DEFAULT_FRAGMENT_NAME = "Fragment";
|
|
120
|
-
const messages$
|
|
640
|
+
const messages$13 = {
|
|
121
641
|
[MESSAGE_ID_NAMED]: "Use the '{{name}}' component instead of fragment shorthand syntax.",
|
|
122
642
|
[MESSAGE_ID_SHORTHAND]: "Use the fragment shorthand syntax '<>...</>' instead of the '{{name}}' component."
|
|
123
643
|
};
|
|
124
|
-
const schema$
|
|
644
|
+
const schema$3 = [{
|
|
125
645
|
additionalProperties: false,
|
|
126
646
|
properties: {
|
|
127
647
|
fragmentName: {
|
|
@@ -153,7 +673,7 @@ function jsxNameToString(node) {
|
|
|
153
673
|
}
|
|
154
674
|
return null;
|
|
155
675
|
}
|
|
156
|
-
function createOnce$
|
|
676
|
+
function createOnce$8(context) {
|
|
157
677
|
let mode;
|
|
158
678
|
let fragmentName;
|
|
159
679
|
let namedFragments;
|
|
@@ -200,8 +720,8 @@ function createOnce$5(context) {
|
|
|
200
720
|
};
|
|
201
721
|
}
|
|
202
722
|
const jsxShorthandFragment = createFlawlessRule({
|
|
203
|
-
name: RULE_NAME$
|
|
204
|
-
createOnce: createOnce$
|
|
723
|
+
name: RULE_NAME$13,
|
|
724
|
+
createOnce: createOnce$8,
|
|
205
725
|
defaultOptions: [{
|
|
206
726
|
fragmentName: DEFAULT_FRAGMENT_NAME,
|
|
207
727
|
mode: DEFAULT_MODE
|
|
@@ -218,7 +738,328 @@ const jsxShorthandFragment = createFlawlessRule({
|
|
|
218
738
|
},
|
|
219
739
|
fixable: "code",
|
|
220
740
|
hasSuggestions: false,
|
|
221
|
-
messages: messages$
|
|
741
|
+
messages: messages$13,
|
|
742
|
+
schema: schema$3,
|
|
743
|
+
type: "suggestion"
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
//#endregion
|
|
747
|
+
//#region src/rules/max-lines-per-function/core-ast-utils.ts
|
|
748
|
+
/**
|
|
749
|
+
* @file Helpers ported from ESLint core's `lib/rules/utils/ast-utils.js` and
|
|
750
|
+
* `lib/shared/string-utils.js`, so this rule's diagnostics read and point
|
|
751
|
+
* identically to core's `max-lines-per-function`. `@typescript-eslint/utils`
|
|
752
|
+
* re-exports `@eslint-community/eslint-utils` equivalents
|
|
753
|
+
* (`getFunctionNameWithKind`, `getFunctionHeadLocation`), but those diverge from
|
|
754
|
+
* core — they name variable-bound functions (`const f = () => {}` →
|
|
755
|
+
* `arrow function 'f'`) and bracket computed keys — so the port is what keeps
|
|
756
|
+
* parity with core, not availability. ESLint is MIT licensed — Copyright OpenJS
|
|
757
|
+
* Foundation and other contributors.
|
|
758
|
+
*/
|
|
759
|
+
/**
|
|
760
|
+
* Describes a function by name and kind, as core does, so diagnostics read the
|
|
761
|
+
* same. Examples: `function 'foo'`, `arrow function`, `constructor`,
|
|
762
|
+
* `static async generator method 'foo'`, `private method #foo`.
|
|
763
|
+
*
|
|
764
|
+
* Core's `TSPropertySignature` / `TSMethodSignature` branches are omitted: this
|
|
765
|
+
* rule only visits the three function node types, so a signature node can never
|
|
766
|
+
* reach here.
|
|
767
|
+
*
|
|
768
|
+
* @param node - The function to describe.
|
|
769
|
+
* @returns The space-joined description.
|
|
770
|
+
*/
|
|
771
|
+
function getFunctionNameWithKind({ id, async, generator, parent, type }) {
|
|
772
|
+
const tokens = [];
|
|
773
|
+
if (parent.type === AST_NODE_TYPES.MethodDefinition || parent.type === AST_NODE_TYPES.PropertyDefinition) {
|
|
774
|
+
if (parent.static) tokens.push("static");
|
|
775
|
+
if (!parent.computed && parent.key.type === AST_NODE_TYPES.PrivateIdentifier) tokens.push("private");
|
|
776
|
+
}
|
|
777
|
+
if (async) tokens.push("async");
|
|
778
|
+
if (generator) tokens.push("generator");
|
|
779
|
+
if (parent.type === AST_NODE_TYPES.Property || parent.type === AST_NODE_TYPES.MethodDefinition) {
|
|
780
|
+
if (parent.kind === "constructor") return "constructor";
|
|
781
|
+
if (parent.kind === "get") tokens.push("getter");
|
|
782
|
+
else if (parent.kind === "set") tokens.push("setter");
|
|
783
|
+
else tokens.push("method");
|
|
784
|
+
} else if (parent.type === AST_NODE_TYPES.PropertyDefinition) tokens.push("method");
|
|
785
|
+
else {
|
|
786
|
+
if (type === AST_NODE_TYPES.ArrowFunctionExpression) tokens.push("arrow");
|
|
787
|
+
tokens.push("function");
|
|
788
|
+
}
|
|
789
|
+
if (parent.type === AST_NODE_TYPES.Property || parent.type === AST_NODE_TYPES.MethodDefinition || parent.type === AST_NODE_TYPES.PropertyDefinition) if (!parent.computed && parent.key.type === AST_NODE_TYPES.PrivateIdentifier) tokens.push(`#${parent.key.name}`);
|
|
790
|
+
else {
|
|
791
|
+
const name = getStaticPropertyName(parent);
|
|
792
|
+
if (name !== null) tokens.push(`'${name}'`);
|
|
793
|
+
else if (id !== null) tokens.push(`'${id.name}'`);
|
|
794
|
+
}
|
|
795
|
+
else if (id !== null) tokens.push(`'${id.name}'`);
|
|
796
|
+
return tokens.join(" ");
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Locates the head of a function — the part worth underlining in a diagnostic,
|
|
800
|
+
* rather than the function's whole multi-line body. For example the
|
|
801
|
+
* `function foo` of a declaration, or the `=>` of an arrow function.
|
|
802
|
+
*
|
|
803
|
+
* Core's `TSPropertySignature` / `TSMethodSignature` branches are omitted for
|
|
804
|
+
* the same reason as in {@link getFunctionNameWithKind}: those nodes never
|
|
805
|
+
* reach here.
|
|
806
|
+
*
|
|
807
|
+
* @param node - The function to locate.
|
|
808
|
+
* @param sourceCode - The source code being linted.
|
|
809
|
+
* @returns The location of the function's head.
|
|
810
|
+
*/
|
|
811
|
+
function getFunctionHeadLoc(node, sourceCode) {
|
|
812
|
+
const { body, loc, parent, type } = node;
|
|
813
|
+
if (parent.type === AST_NODE_TYPES.Property || parent.type === AST_NODE_TYPES.MethodDefinition || parent.type === AST_NODE_TYPES.PropertyDefinition) return {
|
|
814
|
+
end: getOpeningParenOfParameters(node, sourceCode)?.loc.start ?? loc.end,
|
|
815
|
+
start: parent.loc.start
|
|
816
|
+
};
|
|
817
|
+
if (type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
818
|
+
const arrowToken = sourceCode.getTokenBefore(body, isArrowToken);
|
|
819
|
+
if (arrowToken !== null) return {
|
|
820
|
+
end: arrowToken.loc.end,
|
|
821
|
+
start: arrowToken.loc.start
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
end: getOpeningParenOfParameters(node, sourceCode)?.loc.start ?? loc.end,
|
|
826
|
+
start: loc.start
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Upper-cases the first character of a string.
|
|
831
|
+
*
|
|
832
|
+
* @param value - The string to convert.
|
|
833
|
+
* @returns The converted string.
|
|
834
|
+
*/
|
|
835
|
+
function upperCaseFirst(value) {
|
|
836
|
+
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Reads the statically known string value of a property key.
|
|
840
|
+
*
|
|
841
|
+
* @param node - The key node.
|
|
842
|
+
* @returns The value as a string, or `null` when it is not statically known.
|
|
843
|
+
*/
|
|
844
|
+
function getStaticStringValue(node) {
|
|
845
|
+
if (node.type === AST_NODE_TYPES.Literal) return node.value === null ? node.raw : String(node.value);
|
|
846
|
+
if (node.type === AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0 && node.quasis.length === 1) return node.quasis[0]?.value.cooked ?? null;
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Reads the property name of a member definition.
|
|
851
|
+
*
|
|
852
|
+
* @param node - The member whose key to read.
|
|
853
|
+
* @returns The property name, or `null` when the key is computed from a
|
|
854
|
+
* dynamic expression.
|
|
855
|
+
*/
|
|
856
|
+
function getStaticPropertyName(node) {
|
|
857
|
+
if (!node.computed && node.key.type === AST_NODE_TYPES.Identifier) return node.key.name;
|
|
858
|
+
return getStaticStringValue(node.key);
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Finds the token opening a function's parameter list.
|
|
862
|
+
*
|
|
863
|
+
* @param node - The function whose parameters to locate.
|
|
864
|
+
* @param sourceCode - The source code being linted.
|
|
865
|
+
* @returns The opening paren, or — for an arrow function with a single
|
|
866
|
+
* parameter written without parentheses — that parameter's first token.
|
|
867
|
+
* `null` when neither can be found.
|
|
868
|
+
*/
|
|
869
|
+
function getOpeningParenOfParameters(node, sourceCode) {
|
|
870
|
+
const [firstParameter] = node.params;
|
|
871
|
+
if (node.type === AST_NODE_TYPES.ArrowFunctionExpression && node.params.length === 1 && firstParameter !== void 0) {
|
|
872
|
+
const argumentToken = sourceCode.getFirstToken(firstParameter);
|
|
873
|
+
if (argumentToken === null) return null;
|
|
874
|
+
const maybeParenToken = sourceCode.getTokenBefore(argumentToken);
|
|
875
|
+
return maybeParenToken !== null && isOpeningParenToken(maybeParenToken) ? maybeParenToken : argumentToken;
|
|
876
|
+
}
|
|
877
|
+
return node.id === null ? sourceCode.getFirstToken(node, isOpeningParenToken) : sourceCode.getTokenAfter(node.id, isOpeningParenToken);
|
|
878
|
+
}
|
|
879
|
+
//#endregion
|
|
880
|
+
//#region src/rules/max-lines-per-function/rule.ts
|
|
881
|
+
const RULE_NAME$12 = "max-lines-per-function";
|
|
882
|
+
const MESSAGE_ID_EXCEED = "exceed";
|
|
883
|
+
/** A line that is empty or holds only whitespace. */
|
|
884
|
+
const BLANK_LINE = /^\s*$/u;
|
|
885
|
+
const DEFAULTS = {
|
|
886
|
+
countFrom: "body",
|
|
887
|
+
IIFEs: false,
|
|
888
|
+
max: 50,
|
|
889
|
+
skipBlankLines: false,
|
|
890
|
+
skipComments: false
|
|
891
|
+
};
|
|
892
|
+
const messages$12 = { [MESSAGE_ID_EXCEED]: "{{name}} has too many lines ({{lineCount}}). Maximum allowed is {{maxLines}}." };
|
|
893
|
+
const schema$2 = [{
|
|
894
|
+
additionalProperties: false,
|
|
895
|
+
properties: {
|
|
896
|
+
countFrom: {
|
|
897
|
+
description: "Where the counted range begins: \"body\" (default) excludes the signature, \"function\" counts the whole node like ESLint core.",
|
|
898
|
+
enum: ["body", "function"],
|
|
899
|
+
type: "string"
|
|
900
|
+
},
|
|
901
|
+
IIFEs: {
|
|
902
|
+
description: "Whether immediately-invoked function expressions are measured.",
|
|
903
|
+
type: "boolean"
|
|
904
|
+
},
|
|
905
|
+
max: {
|
|
906
|
+
description: "The maximum number of lines a function may span.",
|
|
907
|
+
minimum: 0,
|
|
908
|
+
type: "integer"
|
|
909
|
+
},
|
|
910
|
+
skipBlankLines: {
|
|
911
|
+
description: "Whether lines containing only whitespace are excluded from the count.",
|
|
912
|
+
type: "boolean"
|
|
913
|
+
},
|
|
914
|
+
skipComments: {
|
|
915
|
+
description: "Whether lines consisting solely of a comment are excluded from the count.",
|
|
916
|
+
type: "boolean"
|
|
917
|
+
}
|
|
918
|
+
},
|
|
919
|
+
type: "object"
|
|
920
|
+
}];
|
|
921
|
+
/**
|
|
922
|
+
* Indexes comments by every source line they occupy, so a line can be tested
|
|
923
|
+
* for comment-only status in constant time.
|
|
924
|
+
*
|
|
925
|
+
* @param comments - Every comment in the file.
|
|
926
|
+
* @returns A map from one-indexed line number to the comment on that line.
|
|
927
|
+
*/
|
|
928
|
+
function getCommentLineNumbers(comments) {
|
|
929
|
+
const map = /* @__PURE__ */ new Map();
|
|
930
|
+
for (const comment of comments) for (let { line } = comment.loc.start; line <= comment.loc.end.line; line += 1) map.set(line, comment);
|
|
931
|
+
return map;
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Determines whether a comment occupies a whole line, leaving no code beside
|
|
935
|
+
* it.
|
|
936
|
+
*
|
|
937
|
+
* @param line - The source text of the line.
|
|
938
|
+
* @param lineNumber - The one-indexed number of that line.
|
|
939
|
+
* @param comment - The comment occupying part of the line.
|
|
940
|
+
* @returns `true` when nothing but the comment appears on the line.
|
|
941
|
+
*/
|
|
942
|
+
function isFullLineComment(line, lineNumber, comment) {
|
|
943
|
+
const { end, start } = comment.loc;
|
|
944
|
+
const isFirstTokenOnLine = start.line === lineNumber && line.slice(0, start.column).trim() === "";
|
|
945
|
+
const isLastTokenOnLine = end.line === lineNumber && line.slice(end.column).trim() === "";
|
|
946
|
+
return (start.line < lineNumber || isFirstTokenOnLine) && (end.line > lineNumber || isLastTokenOnLine);
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Determines whether a function is the callee of its own call expression.
|
|
950
|
+
*
|
|
951
|
+
* @param node - The node to test.
|
|
952
|
+
* @returns `true` when the node is immediately invoked.
|
|
953
|
+
*/
|
|
954
|
+
function isIIFE(node) {
|
|
955
|
+
if (node.type !== AST_NODE_TYPES.ArrowFunctionExpression && node.type !== AST_NODE_TYPES.FunctionExpression) return false;
|
|
956
|
+
const { parent } = node;
|
|
957
|
+
return parent.type === AST_NODE_TYPES.CallExpression && parent.callee === node;
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Determines whether a function is the value of a class method or of an object
|
|
961
|
+
* shorthand method or accessor. In that case the enclosing member — not the
|
|
962
|
+
* bare function expression — is the unit measured and reported.
|
|
963
|
+
*
|
|
964
|
+
* @param node - The function to test.
|
|
965
|
+
* @returns `true` when the function is embedded in a member definition.
|
|
966
|
+
*/
|
|
967
|
+
function isEmbedded(node) {
|
|
968
|
+
const { parent } = node;
|
|
969
|
+
if (parent.type === AST_NODE_TYPES.MethodDefinition) return parent.value === node;
|
|
970
|
+
if (parent.type === AST_NODE_TYPES.Property) return parent.value === node && (parent.method || parent.kind === "get" || parent.kind === "set");
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Selects the source range whose lines are counted for a function.
|
|
975
|
+
*
|
|
976
|
+
* Under `"body"` this is the function's body — the braces themselves for a
|
|
977
|
+
* block body, or the expression for a concise arrow body — so the signature is
|
|
978
|
+
* excluded. Under `"function"` it is the reported node, matching ESLint core.
|
|
979
|
+
*
|
|
980
|
+
* @param countFrom - Which range to measure.
|
|
981
|
+
* @param funcNode - The function being measured.
|
|
982
|
+
* @param reportNode - The node the diagnostic attaches to: the enclosing member
|
|
983
|
+
* for an embedded method, otherwise `funcNode` itself.
|
|
984
|
+
* @returns The location whose line span is counted.
|
|
985
|
+
*/
|
|
986
|
+
function getCountedLoc(countFrom, funcNode, reportNode) {
|
|
987
|
+
return countFrom === "function" ? reportNode.loc : funcNode.body.loc;
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Builds the rule's per-file listener.
|
|
991
|
+
*
|
|
992
|
+
* @param context - The rule context.
|
|
993
|
+
* @returns The visitors measuring each function.
|
|
994
|
+
*/
|
|
995
|
+
function createOnce$7(context) {
|
|
996
|
+
let commentLineNumbers;
|
|
997
|
+
let config;
|
|
998
|
+
let lines;
|
|
999
|
+
let sourceCode;
|
|
1000
|
+
/**
|
|
1001
|
+
* Counts a function's lines and reports it when it exceeds the maximum.
|
|
1002
|
+
*
|
|
1003
|
+
* @param funcNode - The function to measure.
|
|
1004
|
+
*/
|
|
1005
|
+
function processFunction(funcNode) {
|
|
1006
|
+
const node = isEmbedded(funcNode) ? funcNode.parent : funcNode;
|
|
1007
|
+
if (!config.IIFEs && isIIFE(node)) return;
|
|
1008
|
+
const { end, start } = getCountedLoc(config.countFrom, funcNode, node);
|
|
1009
|
+
let lineCount = 0;
|
|
1010
|
+
for (let index = start.line - 1; index < end.line; index += 1) {
|
|
1011
|
+
const line = lines[index] ?? "";
|
|
1012
|
+
const lineNumber = index + 1;
|
|
1013
|
+
if (config.skipComments) {
|
|
1014
|
+
const comment = commentLineNumbers.get(lineNumber);
|
|
1015
|
+
if (comment !== void 0 && isFullLineComment(line, lineNumber, comment)) continue;
|
|
1016
|
+
}
|
|
1017
|
+
if (config.skipBlankLines && BLANK_LINE.test(line)) continue;
|
|
1018
|
+
lineCount += 1;
|
|
1019
|
+
}
|
|
1020
|
+
if (lineCount > config.max) context.report({
|
|
1021
|
+
data: {
|
|
1022
|
+
name: upperCaseFirst(getFunctionNameWithKind(funcNode)),
|
|
1023
|
+
lineCount,
|
|
1024
|
+
maxLines: config.max
|
|
1025
|
+
},
|
|
1026
|
+
loc: getFunctionHeadLoc(funcNode, sourceCode),
|
|
1027
|
+
messageId: MESSAGE_ID_EXCEED,
|
|
1028
|
+
node
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
return {
|
|
1032
|
+
ArrowFunctionExpression: processFunction,
|
|
1033
|
+
before() {
|
|
1034
|
+
const options = context.options[0];
|
|
1035
|
+
config = {
|
|
1036
|
+
countFrom: options?.countFrom ?? DEFAULTS.countFrom,
|
|
1037
|
+
IIFEs: options?.IIFEs ?? DEFAULTS.IIFEs,
|
|
1038
|
+
max: options?.max ?? DEFAULTS.max,
|
|
1039
|
+
skipBlankLines: options?.skipBlankLines ?? DEFAULTS.skipBlankLines,
|
|
1040
|
+
skipComments: options?.skipComments ?? DEFAULTS.skipComments
|
|
1041
|
+
};
|
|
1042
|
+
({sourceCode} = context);
|
|
1043
|
+
lines = [...sourceCode.lines];
|
|
1044
|
+
commentLineNumbers = config.skipComments ? getCommentLineNumbers(sourceCode.getAllComments()) : /* @__PURE__ */ new Map();
|
|
1045
|
+
},
|
|
1046
|
+
FunctionDeclaration: processFunction,
|
|
1047
|
+
FunctionExpression: processFunction
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
const maxLinesPerFunction = createFlawlessRule({
|
|
1051
|
+
name: RULE_NAME$12,
|
|
1052
|
+
createOnce: createOnce$7,
|
|
1053
|
+
defaultOptions: [DEFAULTS],
|
|
1054
|
+
meta: {
|
|
1055
|
+
defaultOptions: [DEFAULTS],
|
|
1056
|
+
docs: {
|
|
1057
|
+
description: "Enforce a maximum number of lines of code in a function",
|
|
1058
|
+
recommended: false,
|
|
1059
|
+
requiresTypeChecking: false
|
|
1060
|
+
},
|
|
1061
|
+
hasSuggestions: false,
|
|
1062
|
+
messages: messages$12,
|
|
222
1063
|
schema: schema$2,
|
|
223
1064
|
type: "suggestion"
|
|
224
1065
|
}
|
|
@@ -1383,8 +2224,8 @@ const SCHEMA = {
|
|
|
1383
2224
|
};
|
|
1384
2225
|
//#endregion
|
|
1385
2226
|
//#region src/rules/naming-convention/rule.ts
|
|
1386
|
-
const RULE_NAME$
|
|
1387
|
-
const messages$
|
|
2227
|
+
const RULE_NAME$11 = "naming-convention";
|
|
2228
|
+
const messages$11 = {
|
|
1388
2229
|
doesNotMatchFormat: "{{type}} name `{{name}}` must match one of the following formats: {{formats}}",
|
|
1389
2230
|
doesNotMatchFormatTrimmed: "{{type}} name `{{name}}` trimmed as `{{processedName}}` must match one of the following formats: {{formats}}",
|
|
1390
2231
|
missingAffix: "{{type}} name `{{name}}` must have one of the following {{position}}es: {{affixes}}",
|
|
@@ -1414,7 +2255,7 @@ const camelCaseNamingConfig = [
|
|
|
1414
2255
|
selector: "typeLike"
|
|
1415
2256
|
}
|
|
1416
2257
|
];
|
|
1417
|
-
function create$
|
|
2258
|
+
function create$4(contextWithoutDefaults) {
|
|
1418
2259
|
const context = contextWithoutDefaults.options.length > 0 ? contextWithoutDefaults : Object.setPrototypeOf({ options: camelCaseNamingConfig }, contextWithoutDefaults);
|
|
1419
2260
|
const validators = parseOptions(context);
|
|
1420
2261
|
const compilerOptions = getParserServices(context, true).program?.getCompilerOptions() ?? {};
|
|
@@ -1732,8 +2573,8 @@ function create$3(contextWithoutDefaults) {
|
|
|
1732
2573
|
}));
|
|
1733
2574
|
}
|
|
1734
2575
|
const namingConvention = createEslintRule({
|
|
1735
|
-
name: RULE_NAME$
|
|
1736
|
-
create: create$
|
|
2576
|
+
name: RULE_NAME$11,
|
|
2577
|
+
create: create$4,
|
|
1737
2578
|
defaultOptions: camelCaseNamingConfig,
|
|
1738
2579
|
meta: {
|
|
1739
2580
|
docs: {
|
|
@@ -1743,7 +2584,7 @@ const namingConvention = createEslintRule({
|
|
|
1743
2584
|
},
|
|
1744
2585
|
fixable: void 0,
|
|
1745
2586
|
hasSuggestions: false,
|
|
1746
|
-
messages: messages$
|
|
2587
|
+
messages: messages$11,
|
|
1747
2588
|
schema: SCHEMA,
|
|
1748
2589
|
type: "suggestion"
|
|
1749
2590
|
}
|
|
@@ -1773,6 +2614,505 @@ function requiresQuoting$1(node, target) {
|
|
|
1773
2614
|
return requiresQuoting(node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.PrivateIdentifier ? node.name : `${node.value}`, target);
|
|
1774
2615
|
}
|
|
1775
2616
|
//#endregion
|
|
2617
|
+
//#region src/rules/no-export-default-arrow/rule.ts
|
|
2618
|
+
const RULE_NAME$10 = "no-export-default-arrow";
|
|
2619
|
+
const MESSAGE_ID$6 = "disallowExportDefaultArrow";
|
|
2620
|
+
const messages$10 = { [MESSAGE_ID$6]: "Assign the arrow function to a named constant instead of exporting it anonymously." };
|
|
2621
|
+
/**
|
|
2622
|
+
* Converts a filename stem to camelCase, treating `-`, `_`, and whitespace as
|
|
2623
|
+
* word separators (`use-mouse` -> `useMouse`).
|
|
2624
|
+
*
|
|
2625
|
+
* @param value - The filename stem.
|
|
2626
|
+
* @returns The camelCase name.
|
|
2627
|
+
*/
|
|
2628
|
+
function toCamelCase(value) {
|
|
2629
|
+
return value.replace(/[-_\s]+(.)?/g, (_, char) => char?.toUpperCase() ?? "").replace(/^[A-Z]/, (char) => char.toLowerCase());
|
|
2630
|
+
}
|
|
2631
|
+
/**
|
|
2632
|
+
* Converts a filename stem to PascalCase, treating `-`, `_`, and whitespace as
|
|
2633
|
+
* word separators (`use-mouse` -> `UseMouse`).
|
|
2634
|
+
*
|
|
2635
|
+
* @param value - The filename stem.
|
|
2636
|
+
* @returns The PascalCase name.
|
|
2637
|
+
*/
|
|
2638
|
+
function toPascalCase(value) {
|
|
2639
|
+
return value.replace(/[-_\s]+(.)?/g, (_, char) => char?.toUpperCase() ?? "").replace(/^[a-z]/, (char) => char.toUpperCase());
|
|
2640
|
+
}
|
|
2641
|
+
/**
|
|
2642
|
+
* Checks whether a node is a JSX element or fragment.
|
|
2643
|
+
*
|
|
2644
|
+
* @param node - The expression to test.
|
|
2645
|
+
* @returns `true` when the node renders JSX.
|
|
2646
|
+
*/
|
|
2647
|
+
function isJsxElement(node) {
|
|
2648
|
+
return node.type === AST_NODE_TYPES.JSXElement || node.type === AST_NODE_TYPES.JSXFragment;
|
|
2649
|
+
}
|
|
2650
|
+
/**
|
|
2651
|
+
* Collects the expressions an arrow function can return: the body itself for a
|
|
2652
|
+
* concise arrow, or every `return` argument for a block body.
|
|
2653
|
+
*
|
|
2654
|
+
* @param body - The arrow function's body.
|
|
2655
|
+
* @returns The returned expressions, in source order.
|
|
2656
|
+
*/
|
|
2657
|
+
function getArrowReturnValues(body) {
|
|
2658
|
+
if (body.type !== AST_NODE_TYPES.BlockStatement) return [body];
|
|
2659
|
+
return body.body.filter((node) => node.type === AST_NODE_TYPES.ReturnStatement).map((node) => node.argument).filter((argument) => argument !== null);
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Determines whether an arrow function is a component — that is, whether any of
|
|
2663
|
+
* its return values is JSX — which selects PascalCase for the generated name.
|
|
2664
|
+
*
|
|
2665
|
+
* @param body - The arrow function's body.
|
|
2666
|
+
* @returns `true` when the arrow can return JSX.
|
|
2667
|
+
*/
|
|
2668
|
+
function arrowReturnIsJsxElement(body) {
|
|
2669
|
+
return getArrowReturnValues(body).some((node) => isJsxElement(node));
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* Builds the autofix: replaces the `export default` declaration with a named
|
|
2673
|
+
* `const` derived from the filename, and appends `export default <name>` after
|
|
2674
|
+
* the file's last token (comments included, so a trailing comment keeps its
|
|
2675
|
+
* position).
|
|
2676
|
+
*
|
|
2677
|
+
* @param options - The reported arrow, its export declaration, and the context
|
|
2678
|
+
* and source code needed to derive the name and locate the file's end.
|
|
2679
|
+
* @returns A fixer callback producing both edits.
|
|
2680
|
+
*/
|
|
2681
|
+
function createFixFunction({ arrowFunction, context, exportDeclaration, sourceCode }) {
|
|
2682
|
+
return (fixer) => {
|
|
2683
|
+
const program = sourceCode.ast;
|
|
2684
|
+
const lastToken = sourceCode.getLastToken(program, { includeComments: true });
|
|
2685
|
+
const fileName = context.physicalFilename || context.filename || "namedFunction";
|
|
2686
|
+
const { name: stem } = path.parse(fileName);
|
|
2687
|
+
const functionName = arrowReturnIsJsxElement(arrowFunction.body) ? toPascalCase(stem) : toCamelCase(stem);
|
|
2688
|
+
return [fixer.replaceText(exportDeclaration, `const ${functionName} = ${sourceCode.getText(arrowFunction)}`), fixer.insertTextAfter(lastToken ?? exportDeclaration, `\n\nexport default ${functionName}`)];
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
/**
|
|
2692
|
+
* Reports anonymous arrow functions used as `export default`, which surface as
|
|
2693
|
+
* unnamed functions in stack traces and devtools.
|
|
2694
|
+
*
|
|
2695
|
+
* @param context - The rule context.
|
|
2696
|
+
* @returns The rule listener.
|
|
2697
|
+
*/
|
|
2698
|
+
function createOnce$6(context) {
|
|
2699
|
+
return { ArrowFunctionExpression(node) {
|
|
2700
|
+
const { parent } = node;
|
|
2701
|
+
if (parent.type !== AST_NODE_TYPES.ExportDefaultDeclaration) return;
|
|
2702
|
+
context.report({
|
|
2703
|
+
fix: createFixFunction({
|
|
2704
|
+
arrowFunction: node,
|
|
2705
|
+
context,
|
|
2706
|
+
exportDeclaration: parent,
|
|
2707
|
+
sourceCode: context.sourceCode
|
|
2708
|
+
}),
|
|
2709
|
+
messageId: MESSAGE_ID$6,
|
|
2710
|
+
node
|
|
2711
|
+
});
|
|
2712
|
+
} };
|
|
2713
|
+
}
|
|
2714
|
+
const noExportDefaultArrow = createFlawlessRule({
|
|
2715
|
+
name: RULE_NAME$10,
|
|
2716
|
+
createOnce: createOnce$6,
|
|
2717
|
+
defaultOptions: [],
|
|
2718
|
+
meta: {
|
|
2719
|
+
docs: {
|
|
2720
|
+
description: "Disallow anonymous arrow functions as export default declarations",
|
|
2721
|
+
recommended: false,
|
|
2722
|
+
requiresTypeChecking: false
|
|
2723
|
+
},
|
|
2724
|
+
fixable: "code",
|
|
2725
|
+
hasSuggestions: false,
|
|
2726
|
+
messages: messages$10,
|
|
2727
|
+
schema: [],
|
|
2728
|
+
type: "suggestion"
|
|
2729
|
+
}
|
|
2730
|
+
});
|
|
2731
|
+
//#endregion
|
|
2732
|
+
//#region src/rules/no-redundant-tsconfig-options/resolve-extends.ts
|
|
2733
|
+
const TOP_LEVEL_KEYS = [
|
|
2734
|
+
"include",
|
|
2735
|
+
"exclude",
|
|
2736
|
+
"files"
|
|
2737
|
+
];
|
|
2738
|
+
function isRecord(value) {
|
|
2739
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2740
|
+
}
|
|
2741
|
+
/**
|
|
2742
|
+
* Resolves one `extends` specifier to an absolute config path, or `undefined`
|
|
2743
|
+
* when it cannot be found. Path specifiers resolve against the extending file's
|
|
2744
|
+
* directory (appending `.json` or `/tsconfig.json` as TypeScript does); package
|
|
2745
|
+
* specifiers go through `require.resolve`, which honours the package `exports`
|
|
2746
|
+
* map (so `@scope/pkg/subpath` maps correctly).
|
|
2747
|
+
*
|
|
2748
|
+
* @param spec - The raw `extends` specifier.
|
|
2749
|
+
* @param fromFile - The absolute path of the config doing the extending.
|
|
2750
|
+
* @returns The resolved absolute path, or `undefined`.
|
|
2751
|
+
*/
|
|
2752
|
+
function resolveExtendsTarget(spec, fromFile) {
|
|
2753
|
+
if (isPathSpecifier(spec)) {
|
|
2754
|
+
const base = path.resolve(path.dirname(fromFile), spec);
|
|
2755
|
+
return (base.endsWith(".json") ? [base] : [`${base}.json`, path.join(base, "tsconfig.json")]).find((candidate) => fileExists(candidate));
|
|
2756
|
+
}
|
|
2757
|
+
const require = createRequire(fromFile);
|
|
2758
|
+
for (const candidate of [
|
|
2759
|
+
spec,
|
|
2760
|
+
`${spec}.json`,
|
|
2761
|
+
`${spec}/tsconfig.json`
|
|
2762
|
+
]) try {
|
|
2763
|
+
return require.resolve(candidate);
|
|
2764
|
+
} catch {}
|
|
2765
|
+
}
|
|
2766
|
+
/**
|
|
2767
|
+
* Builds the config a child inherits from its `extends` targets alone (the child
|
|
2768
|
+
* itself excluded). Later targets in an `extends` array override earlier ones,
|
|
2769
|
+
* matching TypeScript's precedence.
|
|
2770
|
+
*
|
|
2771
|
+
* @param childFile - The absolute path of the config being linted.
|
|
2772
|
+
* @param extendsField - The child's raw `extends` value (string or array).
|
|
2773
|
+
* @returns The inherited config, or `undefined` when nothing resolves.
|
|
2774
|
+
*/
|
|
2775
|
+
function buildInheritedConfig(childFile, extendsField) {
|
|
2776
|
+
const context = {
|
|
2777
|
+
cache: /* @__PURE__ */ new Map(),
|
|
2778
|
+
stack: /* @__PURE__ */ new Set([childFile])
|
|
2779
|
+
};
|
|
2780
|
+
const result = emptyConfig();
|
|
2781
|
+
let resolvedAny = false;
|
|
2782
|
+
for (const spec of normalizeExtends(extendsField)) {
|
|
2783
|
+
const target = resolveExtendsTarget(spec, childFile);
|
|
2784
|
+
if (target === void 0) continue;
|
|
2785
|
+
resolvedAny = true;
|
|
2786
|
+
mergeParent(result, flatten(target, context));
|
|
2787
|
+
}
|
|
2788
|
+
return resolvedAny ? result : void 0;
|
|
2789
|
+
}
|
|
2790
|
+
/**
|
|
2791
|
+
* Whether an `extends` specifier is a filesystem path (relative or absolute)
|
|
2792
|
+
* rather than a package specifier. Matches TypeScript: a leading `.` or a rooted
|
|
2793
|
+
* path is a path; anything else resolves through node module resolution.
|
|
2794
|
+
*
|
|
2795
|
+
* @param spec - The raw `extends` specifier.
|
|
2796
|
+
* @returns True when the specifier should resolve against the filesystem.
|
|
2797
|
+
*/
|
|
2798
|
+
function isPathSpecifier(spec) {
|
|
2799
|
+
return spec.startsWith(".") || path.isAbsolute(spec);
|
|
2800
|
+
}
|
|
2801
|
+
function fileExists(candidate) {
|
|
2802
|
+
try {
|
|
2803
|
+
return statSync(candidate).isFile();
|
|
2804
|
+
} catch {
|
|
2805
|
+
return false;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
function normalizeExtends(value) {
|
|
2809
|
+
if (typeof value === "string") return [value];
|
|
2810
|
+
if (Array.isArray(value)) return value.filter((entry) => typeof entry === "string");
|
|
2811
|
+
return [];
|
|
2812
|
+
}
|
|
2813
|
+
function emptyConfig() {
|
|
2814
|
+
return {
|
|
2815
|
+
compilerOptions: /* @__PURE__ */ new Map(),
|
|
2816
|
+
topLevel: /* @__PURE__ */ new Map()
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
function mergeInto(target, source) {
|
|
2820
|
+
for (const [key, entry] of source) target.set(key, entry);
|
|
2821
|
+
}
|
|
2822
|
+
function mergeParent(into, parent) {
|
|
2823
|
+
mergeInto(into.compilerOptions, parent.compilerOptions);
|
|
2824
|
+
mergeInto(into.topLevel, parent.topLevel);
|
|
2825
|
+
}
|
|
2826
|
+
function parseTsconfig(file) {
|
|
2827
|
+
try {
|
|
2828
|
+
const { ast } = parseForESLint(readFileSync(file, "utf8"), {});
|
|
2829
|
+
const statement = ast.body.at(0);
|
|
2830
|
+
if (statement?.expression.type !== "JSONObjectExpression") return;
|
|
2831
|
+
const value = getStaticJSONValue(statement.expression);
|
|
2832
|
+
return isRecord(value) ? value : void 0;
|
|
2833
|
+
} catch {
|
|
2834
|
+
return;
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
/**
|
|
2838
|
+
* Reads and parses a parent tsconfig from disk. Returns `undefined` for any
|
|
2839
|
+
* failure — missing file, unreadable, invalid JSONC, or a top level that is not
|
|
2840
|
+
* an object — so a broken ancestor never crashes the lint.
|
|
2841
|
+
*
|
|
2842
|
+
* @param file - The absolute path of the config to read.
|
|
2843
|
+
* @param cache - Per-resolution parse cache, keyed by absolute path.
|
|
2844
|
+
* @returns The parsed config subset, or `undefined`.
|
|
2845
|
+
*/
|
|
2846
|
+
function readTsconfig(file, cache) {
|
|
2847
|
+
if (cache.has(file)) return cache.get(file);
|
|
2848
|
+
const parsed = parseTsconfig(file);
|
|
2849
|
+
cache.set(file, parsed);
|
|
2850
|
+
return parsed;
|
|
2851
|
+
}
|
|
2852
|
+
function overlay(target, options, source) {
|
|
2853
|
+
if (!isRecord(options)) return;
|
|
2854
|
+
for (const [key, value] of Object.entries(options)) target.set(key, {
|
|
2855
|
+
source,
|
|
2856
|
+
value
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
/**
|
|
2860
|
+
* Flattens the effective config a file resolves to — its own extends chain,
|
|
2861
|
+
* overlaid by its own values (a file's own options win over what it extends).
|
|
2862
|
+
* The `visited` set guards against circular `extends`.
|
|
2863
|
+
*
|
|
2864
|
+
* @param file - The absolute path of the config to flatten.
|
|
2865
|
+
* @param context - The shared cycle stack and parse cache.
|
|
2866
|
+
* @returns The flattened config, with each entry's `source` naming its definer.
|
|
2867
|
+
*/
|
|
2868
|
+
function flatten(file, context) {
|
|
2869
|
+
const result = emptyConfig();
|
|
2870
|
+
if (context.stack.has(file)) return result;
|
|
2871
|
+
context.stack.add(file);
|
|
2872
|
+
try {
|
|
2873
|
+
const config = readTsconfig(file, context.cache);
|
|
2874
|
+
if (config === void 0) return result;
|
|
2875
|
+
for (const spec of normalizeExtends(config.extends)) {
|
|
2876
|
+
const target = resolveExtendsTarget(spec, file);
|
|
2877
|
+
if (target !== void 0) mergeParent(result, flatten(target, context));
|
|
2878
|
+
}
|
|
2879
|
+
overlay(result.compilerOptions, config.compilerOptions, file);
|
|
2880
|
+
for (const key of TOP_LEVEL_KEYS) if (config[key] !== void 0) result.topLevel.set(key, {
|
|
2881
|
+
source: file,
|
|
2882
|
+
value: config[key]
|
|
2883
|
+
});
|
|
2884
|
+
return result;
|
|
2885
|
+
} finally {
|
|
2886
|
+
context.stack.delete(file);
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
//#endregion
|
|
2890
|
+
//#region src/rules/no-redundant-tsconfig-options/rule.ts
|
|
2891
|
+
const RULE_NAME$9 = "no-redundant-tsconfig-options";
|
|
2892
|
+
const MESSAGE_ID$5 = "redundant";
|
|
2893
|
+
/**
|
|
2894
|
+
* Compiler options TypeScript resolves case-insensitively; a child that re-sets
|
|
2895
|
+
* one differing only in case is still redundant.
|
|
2896
|
+
*/
|
|
2897
|
+
const ENUM_SCALAR_KEYS = /* @__PURE__ */ new Set([
|
|
2898
|
+
"jsx",
|
|
2899
|
+
"module",
|
|
2900
|
+
"moduleDetection",
|
|
2901
|
+
"moduleResolution",
|
|
2902
|
+
"newLine",
|
|
2903
|
+
"target"
|
|
2904
|
+
]);
|
|
2905
|
+
/**
|
|
2906
|
+
* Array-valued options TypeScript treats as unordered sets, so a reordered but
|
|
2907
|
+
* otherwise identical value is still redundant.
|
|
2908
|
+
*/
|
|
2909
|
+
const SET_KEYS = /* @__PURE__ */ new Set(["lib", "types"]);
|
|
2910
|
+
/**
|
|
2911
|
+
* `compilerOptions` keys whose values are resolved as paths relative to the
|
|
2912
|
+
* config that declares them. An identical value repeated in a child in a
|
|
2913
|
+
* different directory means a different path, so these are only redundant when
|
|
2914
|
+
* the value is location-independent (see {@link isLocationIndependent}).
|
|
2915
|
+
*/
|
|
2916
|
+
const PATH_KEYS = /* @__PURE__ */ new Set([
|
|
2917
|
+
"baseUrl",
|
|
2918
|
+
"declarationDir",
|
|
2919
|
+
"outDir",
|
|
2920
|
+
"outFile",
|
|
2921
|
+
"paths",
|
|
2922
|
+
"rootDir",
|
|
2923
|
+
"rootDirs",
|
|
2924
|
+
"tsBuildInfoFile",
|
|
2925
|
+
"typeRoots"
|
|
2926
|
+
]);
|
|
2927
|
+
const messages$9 = { [MESSAGE_ID$5]: "'{{option}}' is redundant: it is already set to this value by {{source}}. Remove it." };
|
|
2928
|
+
/**
|
|
2929
|
+
* Structural equality between two static JSON values. Strings compare
|
|
2930
|
+
* case-insensitively when `caseInsensitive` is set (used for enum-valued
|
|
2931
|
+
* options and `lib` entries, which TypeScript itself folds).
|
|
2932
|
+
*
|
|
2933
|
+
* @param a - The child's value.
|
|
2934
|
+
* @param b - The inherited value.
|
|
2935
|
+
* @param caseInsensitive - Whether string scalars compare case-insensitively.
|
|
2936
|
+
* @returns Whether the two values are equal.
|
|
2937
|
+
*/
|
|
2938
|
+
function equalValues(a, b, caseInsensitive) {
|
|
2939
|
+
if (typeof a === "string" && typeof b === "string") return caseInsensitive ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
2940
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, index) => equalValues(item, b[index], caseInsensitive));
|
|
2941
|
+
if (isRecord(a) && isRecord(b)) {
|
|
2942
|
+
const aKeys = Object.keys(a);
|
|
2943
|
+
const bKeys = Object.keys(b);
|
|
2944
|
+
return aKeys.length === bKeys.length && aKeys.every((key) => key in b && equalValues(a[key], b[key], caseInsensitive));
|
|
2945
|
+
}
|
|
2946
|
+
return a === b;
|
|
2947
|
+
}
|
|
2948
|
+
/**
|
|
2949
|
+
* Set equality for two arrays: same length and every element of one has a
|
|
2950
|
+
* distinct equal counterpart in the other, ignoring order.
|
|
2951
|
+
*
|
|
2952
|
+
* @param a - The child's array.
|
|
2953
|
+
* @param b - The inherited array.
|
|
2954
|
+
* @param caseInsensitive - Whether string elements compare case-insensitively.
|
|
2955
|
+
* @returns Whether the arrays hold the same elements regardless of order.
|
|
2956
|
+
*/
|
|
2957
|
+
function equalUnordered(a, b, caseInsensitive) {
|
|
2958
|
+
if (a.length !== b.length) return false;
|
|
2959
|
+
const remaining = [...b];
|
|
2960
|
+
for (const item of a) {
|
|
2961
|
+
const index = remaining.findIndex((other) => equalValues(item, other, caseInsensitive));
|
|
2962
|
+
if (index === -1) return false;
|
|
2963
|
+
remaining.splice(index, 1);
|
|
2964
|
+
}
|
|
2965
|
+
return true;
|
|
2966
|
+
}
|
|
2967
|
+
/**
|
|
2968
|
+
* Whether a path-valued option is location-independent — every path string it
|
|
2969
|
+
* contains is either `${configDir}`-anchored (re-anchored to the extending
|
|
2970
|
+
* config, so an identical value resolves to the same files) or absolute. Only
|
|
2971
|
+
* then is repeating a path option genuinely redundant.
|
|
2972
|
+
*
|
|
2973
|
+
* @param value - The option value.
|
|
2974
|
+
* @returns Whether the value resolves the same regardless of config location.
|
|
2975
|
+
*/
|
|
2976
|
+
function isLocationIndependent(value) {
|
|
2977
|
+
if (typeof value === "string") return value.includes("${configDir}") || path.isAbsolute(value);
|
|
2978
|
+
if (Array.isArray(value)) return value.every((item) => isLocationIndependent(item));
|
|
2979
|
+
if (isRecord(value)) return Object.values(value).every((item) => isLocationIndependent(item));
|
|
2980
|
+
return true;
|
|
2981
|
+
}
|
|
2982
|
+
/**
|
|
2983
|
+
* Whether a child option re-setting `inherited` to `childValue` is redundant:
|
|
2984
|
+
* the values are equal and, for path-valued options, location-independent.
|
|
2985
|
+
*
|
|
2986
|
+
* @param key - The option name.
|
|
2987
|
+
* @param childValue - The value the child sets.
|
|
2988
|
+
* @param inherited - The inherited value.
|
|
2989
|
+
* @param isPathKey - Whether the key is path-valued.
|
|
2990
|
+
* @returns Whether the child option is redundant.
|
|
2991
|
+
*/
|
|
2992
|
+
function isRedundant(key, childValue, inherited, isPathKey) {
|
|
2993
|
+
const caseInsensitive = key === "lib" || ENUM_SCALAR_KEYS.has(key);
|
|
2994
|
+
if (!(SET_KEYS.has(key) && Array.isArray(childValue) && Array.isArray(inherited) ? equalUnordered(childValue, inherited, caseInsensitive) : equalValues(childValue, inherited, caseInsensitive))) return false;
|
|
2995
|
+
return !isPathKey || isLocationIndependent(childValue);
|
|
2996
|
+
}
|
|
2997
|
+
function keyName({ key }) {
|
|
2998
|
+
return key.type === "JSONIdentifier" ? key.name : String(getStaticJSONValue(key));
|
|
2999
|
+
}
|
|
3000
|
+
function findProperty(object, name) {
|
|
3001
|
+
return object.properties.find((property) => keyName(property) === name);
|
|
3002
|
+
}
|
|
3003
|
+
/**
|
|
3004
|
+
* Reads a node's static value, returning `undefined` when it cannot be evaluated
|
|
3005
|
+
* (such as an exotic non-JSON node), so an odd value never crashes the rule. The
|
|
3006
|
+
* result is boxed so a genuine `undefined` value stays distinguishable.
|
|
3007
|
+
*
|
|
3008
|
+
* @param node - The value node to evaluate.
|
|
3009
|
+
* @returns A box holding the value, or `undefined` on failure.
|
|
3010
|
+
*/
|
|
3011
|
+
function safeStaticValue(node) {
|
|
3012
|
+
try {
|
|
3013
|
+
return { value: getStaticJSONValue(node) };
|
|
3014
|
+
} catch {
|
|
3015
|
+
return;
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
function create$3(context) {
|
|
3019
|
+
const { sourceCode } = context;
|
|
3020
|
+
if (sourceCode.parserServices.isJSON !== true) return {};
|
|
3021
|
+
const { filename } = context;
|
|
3022
|
+
if (!path.isAbsolute(filename)) return {};
|
|
3023
|
+
/**
|
|
3024
|
+
* Reports a redundant property, removing it (with its delimiter comma) unless
|
|
3025
|
+
* a comment is attached, in which case it reports without a fix to avoid
|
|
3026
|
+
* stranding the comment.
|
|
3027
|
+
*
|
|
3028
|
+
* @param property - The redundant property node.
|
|
3029
|
+
* @param name - The property's key name (already computed by the caller).
|
|
3030
|
+
* @param entry - The inherited value and the config that defined it.
|
|
3031
|
+
*/
|
|
3032
|
+
function report(property, name, entry) {
|
|
3033
|
+
const relative = path.relative(path.dirname(filename), entry.source) || entry.source;
|
|
3034
|
+
context.report({
|
|
3035
|
+
data: {
|
|
3036
|
+
option: name,
|
|
3037
|
+
source: relative.split(path.sep).join("/")
|
|
3038
|
+
},
|
|
3039
|
+
fix: buildFix(property),
|
|
3040
|
+
loc: property.key.loc,
|
|
3041
|
+
messageId: MESSAGE_ID$5
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
3044
|
+
function buildFix(property) {
|
|
3045
|
+
const node = property;
|
|
3046
|
+
const before = sourceCode.getTokenBefore(node);
|
|
3047
|
+
const after = sourceCode.getTokenAfter(node);
|
|
3048
|
+
const leading = sourceCode.getCommentsBefore(node);
|
|
3049
|
+
const between = sourceCode.getCommentsAfter(node);
|
|
3050
|
+
const trailing = after?.value === "," ? sourceCode.getCommentsAfter(after).filter((comment) => comment.loc.start.line === after.loc.end.line) : [];
|
|
3051
|
+
if (leading.length > 0 || between.length > 0 || trailing.length > 0) return;
|
|
3052
|
+
let start;
|
|
3053
|
+
let end;
|
|
3054
|
+
if (after?.value === ",") {
|
|
3055
|
+
start = before?.range[1] ?? property.range[0];
|
|
3056
|
+
end = after.range[1];
|
|
3057
|
+
} else if (before?.value === ",") {
|
|
3058
|
+
start = before.range[0];
|
|
3059
|
+
end = property.range[1];
|
|
3060
|
+
} else {
|
|
3061
|
+
start = before?.range[1] ?? property.range[0];
|
|
3062
|
+
end = property.range[1];
|
|
3063
|
+
}
|
|
3064
|
+
return (fixer) => fixer.removeRange([start, end]);
|
|
3065
|
+
}
|
|
3066
|
+
function checkObject(object, lookup, isPathKey) {
|
|
3067
|
+
for (const property of object.properties) {
|
|
3068
|
+
const name = keyName(property);
|
|
3069
|
+
const entry = lookup(name);
|
|
3070
|
+
if (entry === void 0) continue;
|
|
3071
|
+
const childValue = safeStaticValue(property.value);
|
|
3072
|
+
if (childValue !== void 0 && isRedundant(name, childValue.value, entry.value, isPathKey(name))) report(property, name, entry);
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
return { Program() {
|
|
3076
|
+
const statement = sourceCode.ast.body.at(0);
|
|
3077
|
+
if (statement?.expression.type !== "JSONObjectExpression") return;
|
|
3078
|
+
const root = statement.expression;
|
|
3079
|
+
const extendsProperty = findProperty(root, "extends");
|
|
3080
|
+
if (extendsProperty === void 0) return;
|
|
3081
|
+
const extendsValue = safeStaticValue(extendsProperty.value);
|
|
3082
|
+
if (extendsValue === void 0) return;
|
|
3083
|
+
const inherited = buildInheritedConfig(filename, extendsValue.value);
|
|
3084
|
+
if (inherited === void 0) return;
|
|
3085
|
+
const compilerOptions = findProperty(root, "compilerOptions");
|
|
3086
|
+
if (compilerOptions?.value.type === "JSONObjectExpression") checkObject(compilerOptions.value, (name) => {
|
|
3087
|
+
return inherited.compilerOptions.get(name);
|
|
3088
|
+
}, (name) => {
|
|
3089
|
+
return PATH_KEYS.has(name);
|
|
3090
|
+
});
|
|
3091
|
+
checkObject(root, (name) => {
|
|
3092
|
+
return inherited.topLevel.get(name);
|
|
3093
|
+
}, () => {
|
|
3094
|
+
return true;
|
|
3095
|
+
});
|
|
3096
|
+
} };
|
|
3097
|
+
}
|
|
3098
|
+
const noRedundantTsconfigOptions = createEslintRule({
|
|
3099
|
+
name: RULE_NAME$9,
|
|
3100
|
+
create: create$3,
|
|
3101
|
+
defaultOptions: [],
|
|
3102
|
+
meta: {
|
|
3103
|
+
docs: {
|
|
3104
|
+
description: "Disallow tsconfig options that redundantly re-set a value already provided by an extended config",
|
|
3105
|
+
recommended: false,
|
|
3106
|
+
requiresTypeChecking: false
|
|
3107
|
+
},
|
|
3108
|
+
fixable: "code",
|
|
3109
|
+
hasSuggestions: false,
|
|
3110
|
+
messages: messages$9,
|
|
3111
|
+
schema: [],
|
|
3112
|
+
type: "suggestion"
|
|
3113
|
+
}
|
|
3114
|
+
});
|
|
3115
|
+
//#endregion
|
|
1776
3116
|
//#region src/utils/nested-expressions.ts
|
|
1777
3117
|
/**
|
|
1778
3118
|
* Determines whether the given AST subtree contains a call or `new`
|
|
@@ -2010,15 +3350,15 @@ function resolveFactory(sourceCode, node) {
|
|
|
2010
3350
|
}
|
|
2011
3351
|
//#endregion
|
|
2012
3352
|
//#region src/rules/no-unnecessary-use-callback/rule.ts
|
|
2013
|
-
const RULE_NAME$
|
|
3353
|
+
const RULE_NAME$8 = "no-unnecessary-use-callback";
|
|
2014
3354
|
const MESSAGE_ID_DEFAULT$1 = "default";
|
|
2015
3355
|
const MESSAGE_ID_INSIDE_USE_EFFECT$1 = "noUnnecessaryUseCallbackInsideUseEffect";
|
|
2016
|
-
const messages$
|
|
3356
|
+
const messages$8 = {
|
|
2017
3357
|
[MESSAGE_ID_DEFAULT$1]: "An 'useCallback' with empty deps and no references to the component scope may be unnecessary.",
|
|
2018
3358
|
[MESSAGE_ID_INSIDE_USE_EFFECT$1]: "'{{name}}' is only used inside 1 useEffect, which may be unnecessary. You can move the computation into useEffect directly and merge the dependency arrays."
|
|
2019
3359
|
};
|
|
2020
3360
|
const noUnnecessaryUseCallback = createFlawlessRule({
|
|
2021
|
-
name: RULE_NAME$
|
|
3361
|
+
name: RULE_NAME$8,
|
|
2022
3362
|
createOnce: createUnnecessaryHookRule({
|
|
2023
3363
|
hook: "useCallback",
|
|
2024
3364
|
messageIds: {
|
|
@@ -2034,22 +3374,22 @@ const noUnnecessaryUseCallback = createFlawlessRule({
|
|
|
2034
3374
|
requiresTypeChecking: false
|
|
2035
3375
|
},
|
|
2036
3376
|
hasSuggestions: false,
|
|
2037
|
-
messages: messages$
|
|
3377
|
+
messages: messages$8,
|
|
2038
3378
|
schema: [],
|
|
2039
3379
|
type: "suggestion"
|
|
2040
3380
|
}
|
|
2041
3381
|
});
|
|
2042
3382
|
//#endregion
|
|
2043
3383
|
//#region src/rules/no-unnecessary-use-memo/rule.ts
|
|
2044
|
-
const RULE_NAME$
|
|
3384
|
+
const RULE_NAME$7 = "no-unnecessary-use-memo";
|
|
2045
3385
|
const MESSAGE_ID_DEFAULT = "default";
|
|
2046
3386
|
const MESSAGE_ID_INSIDE_USE_EFFECT = "noUnnecessaryUseMemoInsideUseEffect";
|
|
2047
|
-
const messages$
|
|
3387
|
+
const messages$7 = {
|
|
2048
3388
|
[MESSAGE_ID_DEFAULT]: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary.",
|
|
2049
3389
|
[MESSAGE_ID_INSIDE_USE_EFFECT]: "'{{name}}' is only used inside 1 useEffect, which may be unnecessary. You can move the computation into useEffect directly and merge the dependency arrays."
|
|
2050
3390
|
};
|
|
2051
3391
|
const noUnnecessaryUseMemo = createFlawlessRule({
|
|
2052
|
-
name: RULE_NAME$
|
|
3392
|
+
name: RULE_NAME$7,
|
|
2053
3393
|
createOnce: createUnnecessaryHookRule({
|
|
2054
3394
|
hook: "useMemo",
|
|
2055
3395
|
messageIds: {
|
|
@@ -2065,12 +3405,132 @@ const noUnnecessaryUseMemo = createFlawlessRule({
|
|
|
2065
3405
|
requiresTypeChecking: false
|
|
2066
3406
|
},
|
|
2067
3407
|
hasSuggestions: false,
|
|
2068
|
-
messages: messages$
|
|
3408
|
+
messages: messages$7,
|
|
2069
3409
|
schema: [],
|
|
2070
3410
|
type: "suggestion"
|
|
2071
3411
|
}
|
|
2072
3412
|
});
|
|
2073
3413
|
//#endregion
|
|
3414
|
+
//#region src/rules/padding-after-expect-assertions/rule.ts
|
|
3415
|
+
const RULE_NAME$6 = "padding-after-expect-assertions";
|
|
3416
|
+
const MESSAGE_ID$4 = "missingPadding";
|
|
3417
|
+
const messages$6 = { [MESSAGE_ID$4]: "Expected a blank line after '{{name}}'." };
|
|
3418
|
+
/**
|
|
3419
|
+
* Matches a statement that declares the expected assertion count at the top of
|
|
3420
|
+
* a test: `expect.assertions(n)` or `expect.hasAssertions()`.
|
|
3421
|
+
*
|
|
3422
|
+
* Detection is purely syntactic (a non-computed `expect.assertions` /
|
|
3423
|
+
* `expect.hasAssertions` call) with no scope analysis, mirroring the deliberate
|
|
3424
|
+
* looseness of `@vitest/eslint-plugin`'s own padding rules. A shadowed local
|
|
3425
|
+
* `expect` is vanishingly rare, and the only consequence is a harmless blank
|
|
3426
|
+
* line.
|
|
3427
|
+
*
|
|
3428
|
+
* @param node - The expression statement to inspect.
|
|
3429
|
+
* @returns The matched member name (`expect.assertions` / `expect.hasAssertions`)
|
|
3430
|
+
* for the message, or `undefined` when the statement is not an assertion count.
|
|
3431
|
+
*/
|
|
3432
|
+
function getAssertionName({ expression }) {
|
|
3433
|
+
if (expression.type !== AST_NODE_TYPES.CallExpression) return;
|
|
3434
|
+
const { callee } = expression;
|
|
3435
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed || callee.object.type !== AST_NODE_TYPES.Identifier || callee.object.name !== "expect" || callee.property.type !== AST_NODE_TYPES.Identifier) return;
|
|
3436
|
+
const { name } = callee.property;
|
|
3437
|
+
if (name !== "assertions" && name !== "hasAssertions") return;
|
|
3438
|
+
return `expect.${name}`;
|
|
3439
|
+
}
|
|
3440
|
+
/**
|
|
3441
|
+
* Returns the statement list that directly contains a node, so its following
|
|
3442
|
+
* sibling can be found. The assertion count opens an `it`/`test` callback body
|
|
3443
|
+
* (a `BlockStatement`) or, at worst, sits at the top level (`Program`).
|
|
3444
|
+
*
|
|
3445
|
+
* @param node - The node whose containing list is wanted.
|
|
3446
|
+
* @returns The sibling statements, or `undefined` when the parent holds no list.
|
|
3447
|
+
*/
|
|
3448
|
+
function getContainingList({ parent }) {
|
|
3449
|
+
if (parent.type === AST_NODE_TYPES.BlockStatement || parent.type === AST_NODE_TYPES.Program) return parent.body;
|
|
3450
|
+
}
|
|
3451
|
+
/**
|
|
3452
|
+
* Locates the two tokens the padding is measured and inserted between: the last
|
|
3453
|
+
* token of the assertion statement (advanced past any trailing comment on the
|
|
3454
|
+
* same line) and the first token of the following statement (a leading comment
|
|
3455
|
+
* included). This mirrors ESLint core's `padding-line-between-statements`, so a
|
|
3456
|
+
* trailing `// note` does not defeat the rule and the fix lands in the right
|
|
3457
|
+
* place. Note that `yaml-block-key-blank-lines` deliberately takes the opposite
|
|
3458
|
+
* stance and bails on comments, since rewriting there would re-attach them.
|
|
3459
|
+
*
|
|
3460
|
+
* @param sourceCode - The source code, for token lookups.
|
|
3461
|
+
* @param node - The assertion statement.
|
|
3462
|
+
* @param nextNode - The statement that follows it.
|
|
3463
|
+
* @returns The anchor tokens, or `undefined` when either cannot be resolved.
|
|
3464
|
+
*/
|
|
3465
|
+
function findPaddingAnchor(sourceCode, node, nextNode) {
|
|
3466
|
+
const lastToken = sourceCode.getLastToken(node);
|
|
3467
|
+
if (lastToken === null) return;
|
|
3468
|
+
let previousToken = lastToken;
|
|
3469
|
+
const nextToken = sourceCode.getFirstTokenBetween(previousToken, nextNode, {
|
|
3470
|
+
filter(token) {
|
|
3471
|
+
if (ASTUtils.isTokenOnSameLine(previousToken, token)) {
|
|
3472
|
+
previousToken = token;
|
|
3473
|
+
return false;
|
|
3474
|
+
}
|
|
3475
|
+
return true;
|
|
3476
|
+
},
|
|
3477
|
+
includeComments: true
|
|
3478
|
+
}) ?? sourceCode.getFirstToken(nextNode);
|
|
3479
|
+
if (nextToken === null) return;
|
|
3480
|
+
return {
|
|
3481
|
+
nextToken,
|
|
3482
|
+
previousToken
|
|
3483
|
+
};
|
|
3484
|
+
}
|
|
3485
|
+
/**
|
|
3486
|
+
* Requires a blank line after the assertion count that opens a test, keeping the
|
|
3487
|
+
* bookkeeping visually separate from the expectations that follow.
|
|
3488
|
+
*
|
|
3489
|
+
* @param context - The rule context.
|
|
3490
|
+
* @returns The rule listener.
|
|
3491
|
+
*/
|
|
3492
|
+
function createOnce$3(context) {
|
|
3493
|
+
return { ExpressionStatement(node) {
|
|
3494
|
+
const name = getAssertionName(node);
|
|
3495
|
+
if (name === void 0) return;
|
|
3496
|
+
const list = getContainingList(node);
|
|
3497
|
+
if (list === void 0) return;
|
|
3498
|
+
const nextNode = list[list.indexOf(node) + 1];
|
|
3499
|
+
if (nextNode === void 0) return;
|
|
3500
|
+
const { sourceCode } = context;
|
|
3501
|
+
const anchor = findPaddingAnchor(sourceCode, node, nextNode);
|
|
3502
|
+
if (anchor === void 0) return;
|
|
3503
|
+
const { nextToken, previousToken } = anchor;
|
|
3504
|
+
const gap = nextToken.loc.start.line - previousToken.loc.end.line;
|
|
3505
|
+
if (gap > 1) return;
|
|
3506
|
+
context.report({
|
|
3507
|
+
data: { name },
|
|
3508
|
+
fix(fixer) {
|
|
3509
|
+
return fixer.insertTextAfter(previousToken, gap === 0 ? "\n\n" : "\n");
|
|
3510
|
+
},
|
|
3511
|
+
loc: node.loc,
|
|
3512
|
+
messageId: MESSAGE_ID$4
|
|
3513
|
+
});
|
|
3514
|
+
} };
|
|
3515
|
+
}
|
|
3516
|
+
const paddingAfterExpectAssertions = createFlawlessRule({
|
|
3517
|
+
name: RULE_NAME$6,
|
|
3518
|
+
createOnce: createOnce$3,
|
|
3519
|
+
defaultOptions: [],
|
|
3520
|
+
meta: {
|
|
3521
|
+
docs: {
|
|
3522
|
+
description: "Enforce a blank line after `expect.assertions` and `expect.hasAssertions`",
|
|
3523
|
+
recommended: false,
|
|
3524
|
+
requiresTypeChecking: false
|
|
3525
|
+
},
|
|
3526
|
+
fixable: "whitespace",
|
|
3527
|
+
hasSuggestions: false,
|
|
3528
|
+
messages: messages$6,
|
|
3529
|
+
schema: [],
|
|
3530
|
+
type: "layout"
|
|
3531
|
+
}
|
|
3532
|
+
});
|
|
3533
|
+
//#endregion
|
|
2074
3534
|
//#region src/rules/prefer-destructuring-assignment/rule.ts
|
|
2075
3535
|
const RULE_NAME$5 = "prefer-destructuring-assignment";
|
|
2076
3536
|
const MESSAGE_ID$3 = "default";
|
|
@@ -3631,11 +5091,16 @@ const plugin = {
|
|
|
3631
5091
|
version
|
|
3632
5092
|
},
|
|
3633
5093
|
rules: {
|
|
5094
|
+
"arrow-return-style": arrowReturnStyle,
|
|
3634
5095
|
"jsx-shorthand-boolean": jsxShorthandBoolean,
|
|
3635
5096
|
"jsx-shorthand-fragment": jsxShorthandFragment,
|
|
5097
|
+
"max-lines-per-function": maxLinesPerFunction,
|
|
3636
5098
|
"naming-convention": namingConvention,
|
|
5099
|
+
"no-export-default-arrow": noExportDefaultArrow,
|
|
5100
|
+
"no-redundant-tsconfig-options": noRedundantTsconfigOptions,
|
|
3637
5101
|
"no-unnecessary-use-callback": noUnnecessaryUseCallback,
|
|
3638
5102
|
"no-unnecessary-use-memo": noUnnecessaryUseMemo,
|
|
5103
|
+
"padding-after-expect-assertions": paddingAfterExpectAssertions,
|
|
3639
5104
|
"prefer-destructuring-assignment": preferDestructuringAssignment,
|
|
3640
5105
|
"prefer-parameter-destructuring": preferParameterDestructuring,
|
|
3641
5106
|
"prefer-read-only-props": preferReadOnlyProps,
|