eslint-plugin-flawless 0.1.11 → 0.1.12
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 +2 -0
- package/dist/index.d.mts +31 -0
- package/dist/index.mjs +1 -1
- package/dist/oxlint.mjs +1 -1
- package/dist/{plugin-BvQt8gSt.mjs → plugin-DYVtpuio.mjs} +597 -20
- package/dist/rules/arrow-return-style/worker.d.mts +25 -0
- package/dist/rules/arrow-return-style/worker.mjs +90 -0
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -200,9 +200,11 @@ Requires [type information](https://typescript-eslint.io/linting/typed-linting).
|
|
|
200
200
|
|
|
201
201
|
| Name | Description | 🔧 | 💭 |
|
|
202
202
|
| :-------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------- | :-- | :-- |
|
|
203
|
+
| [arrow-return-style](src/rules/arrow-return-style/documentation.md) | Enforce arrow function return style based on line length | 🔧 | |
|
|
203
204
|
| [jsx-shorthand-boolean](src/rules/jsx-shorthand-boolean/documentation.md) | Disallow shorthand boolean JSX attributes | 🔧 | |
|
|
204
205
|
| [jsx-shorthand-fragment](src/rules/jsx-shorthand-fragment/documentation.md) | Enforce a consistent fragment form: the shorthand `<>...</>` or a named fragment | 🔧 | |
|
|
205
206
|
| [naming-convention](src/rules/naming-convention/documentation.md) | Enforce naming conventions for everything across a codebase | | 💭 |
|
|
207
|
+
| [no-export-default-arrow](src/rules/no-export-default-arrow/documentation.md) | Disallow anonymous arrow functions as export default declarations | 🔧 | |
|
|
206
208
|
| [no-unnecessary-use-callback](src/rules/no-unnecessary-use-callback/documentation.md) | Disallow unnecessary usage of 'useCallback' | | |
|
|
207
209
|
| [no-unnecessary-use-memo](src/rules/no-unnecessary-use-memo/documentation.md) | Disallow unnecessary usage of 'useMemo' | | |
|
|
208
210
|
| [prefer-destructuring-assignment](src/rules/prefer-destructuring-assignment/documentation.md) | Enforce destructuring assignment for component props | 🔧 | |
|
package/dist/index.d.mts
CHANGED
|
@@ -44,6 +44,31 @@ type FlawlessRuleModule<Options extends ReadonlyArray<unknown>, MessageIds exten
|
|
|
44
44
|
createOnce: (context: RuleContextWithSourceCode<MessageIds, Options, SourceCode>) => FlawlessRuleListener;
|
|
45
45
|
};
|
|
46
46
|
//#endregion
|
|
47
|
+
//#region src/rules/arrow-return-style/rule.d.ts
|
|
48
|
+
declare const IMPLICIT = "useImplicitReturn";
|
|
49
|
+
declare const EXPLICIT = "useExplicitReturn";
|
|
50
|
+
declare const COMPLEX_EXPLICIT = "useExplicitReturnComplex";
|
|
51
|
+
type MessageIds$4 = typeof COMPLEX_EXPLICIT | typeof EXPLICIT | typeof IMPLICIT;
|
|
52
|
+
type ObjectReturnStyle = "always-explicit" | "complex-explicit" | "off";
|
|
53
|
+
type Options$5 = [{
|
|
54
|
+
/** Always use explicit returns for JSX bodies. */
|
|
55
|
+
jsxAlwaysUseExplicitReturn?: boolean;
|
|
56
|
+
/** Maximum emitted line length (tab-expanded) before requiring an explicit return. */
|
|
57
|
+
maxLen?: number;
|
|
58
|
+
/** Object property / array element count above which a literal body counts as complex. */
|
|
59
|
+
maxObjectProperties?: number;
|
|
60
|
+
/** Always use explicit returns for arrows assigned to named exports. */
|
|
61
|
+
namedExportsAlwaysUseExplicitReturn?: boolean;
|
|
62
|
+
/** When to force explicit returns for object/array literal bodies. */
|
|
63
|
+
objectReturnStyle?: ObjectReturnStyle;
|
|
64
|
+
/** Columns a tab occupies when measuring against `maxLen`. */
|
|
65
|
+
tabWidth?: number;
|
|
66
|
+
/** Consult oxfmt for boundary decisions; `printWidth` defaults to `maxLen`. */
|
|
67
|
+
useOxfmt?: boolean | {
|
|
68
|
+
printWidth?: number;
|
|
69
|
+
};
|
|
70
|
+
}];
|
|
71
|
+
//#endregion
|
|
47
72
|
//#region src/rules/jsx-shorthand-fragment/rule.d.ts
|
|
48
73
|
declare const MESSAGE_ID_NAMED = "useNamedFragment";
|
|
49
74
|
declare const MESSAGE_ID_SHORTHAND = "useShorthandFragment";
|
|
@@ -225,11 +250,13 @@ declare const plugin: {
|
|
|
225
250
|
version: string;
|
|
226
251
|
};
|
|
227
252
|
rules: {
|
|
253
|
+
"arrow-return-style": FlawlessRuleModule<Options$5, MessageIds$4, Readonly<TSESLint.SourceCode>>;
|
|
228
254
|
"jsx-shorthand-boolean": FlawlessRuleModule<[], "setAttributeValue", Readonly<TSESLint.SourceCode>>;
|
|
229
255
|
"jsx-shorthand-fragment": FlawlessRuleModule<Options$4, MessageIds$3, Readonly<TSESLint.SourceCode>>;
|
|
230
256
|
"naming-convention": TSESLint.RuleModule<MessageIds$2, Options$3, PluginDocumentation, TSESLint.RuleListener> & {
|
|
231
257
|
name: string;
|
|
232
258
|
};
|
|
259
|
+
"no-export-default-arrow": FlawlessRuleModule<[], "disallowExportDefaultArrow", Readonly<TSESLint.SourceCode>>;
|
|
233
260
|
"no-unnecessary-use-callback": FlawlessRuleModule<[], MessageIds$1, Readonly<TSESLint.SourceCode>>;
|
|
234
261
|
"no-unnecessary-use-memo": FlawlessRuleModule<[], MessageIds, Readonly<TSESLint.SourceCode>>;
|
|
235
262
|
"prefer-destructuring-assignment": FlawlessRuleModule<[], "default", Readonly<TSESLint.SourceCode>>;
|
|
@@ -262,11 +289,13 @@ declare const _default: {
|
|
|
262
289
|
version: string;
|
|
263
290
|
};
|
|
264
291
|
rules: {
|
|
292
|
+
"arrow-return-style": FlawlessRuleModule<Options$5, MessageIds$4, Readonly<import("${configDir}").SourceCode>>;
|
|
265
293
|
"jsx-shorthand-boolean": FlawlessRuleModule<[], "setAttributeValue", Readonly<import("${configDir}").SourceCode>>;
|
|
266
294
|
"jsx-shorthand-fragment": FlawlessRuleModule<Options$4, MessageIds$3, Readonly<import("${configDir}").SourceCode>>;
|
|
267
295
|
"naming-convention": import("${configDir}").RuleModule<MessageIds$2, Options$3, PluginDocumentation, import("${configDir}").RuleListener> & {
|
|
268
296
|
name: string;
|
|
269
297
|
};
|
|
298
|
+
"no-export-default-arrow": FlawlessRuleModule<[], "disallowExportDefaultArrow", Readonly<import("${configDir}").SourceCode>>;
|
|
270
299
|
"no-unnecessary-use-callback": FlawlessRuleModule<[], MessageIds$1, Readonly<import("${configDir}").SourceCode>>;
|
|
271
300
|
"no-unnecessary-use-memo": FlawlessRuleModule<[], MessageIds, Readonly<import("${configDir}").SourceCode>>;
|
|
272
301
|
"prefer-destructuring-assignment": FlawlessRuleModule<[], "default", Readonly<import("${configDir}").SourceCode>>;
|
|
@@ -296,11 +325,13 @@ declare const _default: {
|
|
|
296
325
|
version: string;
|
|
297
326
|
};
|
|
298
327
|
rules: {
|
|
328
|
+
"arrow-return-style": FlawlessRuleModule<Options$5, MessageIds$4, Readonly<import("${configDir}").SourceCode>>;
|
|
299
329
|
"jsx-shorthand-boolean": FlawlessRuleModule<[], "setAttributeValue", Readonly<import("${configDir}").SourceCode>>;
|
|
300
330
|
"jsx-shorthand-fragment": FlawlessRuleModule<Options$4, MessageIds$3, Readonly<import("${configDir}").SourceCode>>;
|
|
301
331
|
"naming-convention": import("${configDir}").RuleModule<MessageIds$2, Options$3, PluginDocumentation, import("${configDir}").RuleListener> & {
|
|
302
332
|
name: string;
|
|
303
333
|
};
|
|
334
|
+
"no-export-default-arrow": FlawlessRuleModule<[], "disallowExportDefaultArrow", Readonly<import("${configDir}").SourceCode>>;
|
|
304
335
|
"no-unnecessary-use-callback": FlawlessRuleModule<[], MessageIds$1, Readonly<import("${configDir}").SourceCode>>;
|
|
305
336
|
"no-unnecessary-use-memo": FlawlessRuleModule<[], MessageIds, Readonly<import("${configDir}").SourceCode>>;
|
|
306
337
|
"prefer-destructuring-assignment": FlawlessRuleModule<[], "default", Readonly<import("${configDir}").SourceCode>>;
|
package/dist/index.mjs
CHANGED
package/dist/oxlint.mjs
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { RuleCreator, getParserServices } from "@typescript-eslint/utils/eslint-utils";
|
|
2
1
|
import { ASTUtils, AST_NODE_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { createSyncFn } from "synckit";
|
|
6
|
+
import { RuleCreator, getParserServices } from "@typescript-eslint/utils/eslint-utils";
|
|
3
7
|
import { DefinitionType, ImplicitLibVariable, PatternVisitor, ScopeType, Visitor } from "@typescript-eslint/scope-manager";
|
|
4
8
|
import { requiresQuoting } from "@typescript-eslint/type-utils";
|
|
5
9
|
import assert from "node:assert";
|
|
@@ -9,7 +13,7 @@ import ts9 from "typescript";
|
|
|
9
13
|
import { getStaticTOMLValue } from "toml-eslint-parser";
|
|
10
14
|
//#region package.json
|
|
11
15
|
var name = "eslint-plugin-flawless";
|
|
12
|
-
var version = "0.1.
|
|
16
|
+
var version = "0.1.12";
|
|
13
17
|
var repository = {
|
|
14
18
|
"url": "git+https://github.com/christopher-buss/eslint-plugin-flawless.git",
|
|
15
19
|
"type": "git"
|
|
@@ -78,24 +82,480 @@ function createFlawlessRule({ createOnce, ...meta }) {
|
|
|
78
82
|
return Object.assign(module, { createOnce });
|
|
79
83
|
}
|
|
80
84
|
//#endregion
|
|
85
|
+
//#region src/rules/arrow-return-style/rule.ts
|
|
86
|
+
const RULE_NAME$12 = "arrow-return-style";
|
|
87
|
+
const IMPLICIT = "useImplicitReturn";
|
|
88
|
+
const EXPLICIT = "useExplicitReturn";
|
|
89
|
+
const COMPLEX_EXPLICIT = "useExplicitReturnComplex";
|
|
90
|
+
const DEFAULTS = {
|
|
91
|
+
jsxAlwaysUseExplicitReturn: false,
|
|
92
|
+
maxLen: 80,
|
|
93
|
+
maxObjectProperties: 4,
|
|
94
|
+
namedExportsAlwaysUseExplicitReturn: true,
|
|
95
|
+
objectReturnStyle: "complex-explicit",
|
|
96
|
+
tabWidth: 4,
|
|
97
|
+
useOxfmt: true
|
|
98
|
+
};
|
|
99
|
+
/** `undefined` = not initialized yet; `null` = initialization failed. */
|
|
100
|
+
let formatSync;
|
|
101
|
+
function getFormatSync() {
|
|
102
|
+
if (formatSync !== void 0) return formatSync;
|
|
103
|
+
try {
|
|
104
|
+
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
105
|
+
const workerPath = [
|
|
106
|
+
path.join(directory, "worker.mjs"),
|
|
107
|
+
path.join(directory, "rules", "arrow-return-style", "worker.mjs"),
|
|
108
|
+
path.join(directory, "worker.ts")
|
|
109
|
+
].find((candidate) => existsSync(candidate));
|
|
110
|
+
if (workerPath === void 0) formatSync = null;
|
|
111
|
+
else formatSync = createSyncFn(workerPath, {
|
|
112
|
+
timeout: 15e3,
|
|
113
|
+
...workerPath.endsWith(".ts") ? { tsRunner: "tsx" } : {}
|
|
114
|
+
});
|
|
115
|
+
} catch {
|
|
116
|
+
formatSync = null;
|
|
117
|
+
}
|
|
118
|
+
return formatSync;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Worker verdicts cached across files and fix passes: a snippet formats the
|
|
122
|
+
* same way regardless of which file (or autofix iteration) asked. Bounded LRU
|
|
123
|
+
* so long sessions (editors, watch mode) cannot grow it without limit.
|
|
124
|
+
*/
|
|
125
|
+
const formatCache = /* @__PURE__ */ new Map();
|
|
126
|
+
const FORMAT_CACHE_MAX_ENTRIES = 1024;
|
|
127
|
+
function formatCacheGet(key) {
|
|
128
|
+
const value = formatCache.get(key);
|
|
129
|
+
if (value !== void 0) {
|
|
130
|
+
formatCache.delete(key);
|
|
131
|
+
formatCache.set(key, value);
|
|
132
|
+
}
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
function formatCacheSet(key, value) {
|
|
136
|
+
formatCache.delete(key);
|
|
137
|
+
formatCache.set(key, value);
|
|
138
|
+
if (formatCache.size > FORMAT_CACHE_MAX_ENTRIES) {
|
|
139
|
+
const oldest = formatCache.keys().next().value;
|
|
140
|
+
if (oldest !== void 0) formatCache.delete(oldest);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Visual width of `text` with tabs expanded to `tabWidth`-column tab stops.
|
|
145
|
+
*
|
|
146
|
+
* @param text - The text to measure.
|
|
147
|
+
* @param tabWidth - Columns a tab occupies.
|
|
148
|
+
* @returns The tab-expanded width in columns.
|
|
149
|
+
*/
|
|
150
|
+
function expandedWidth(text, tabWidth) {
|
|
151
|
+
let width = 0;
|
|
152
|
+
for (const character of text) width += character === " " ? tabWidth - width % tabWidth : 1;
|
|
153
|
+
return width;
|
|
154
|
+
}
|
|
155
|
+
function leadingWhitespace(line) {
|
|
156
|
+
return /^[\t ]*/.exec(line)?.[0] ?? "";
|
|
157
|
+
}
|
|
158
|
+
function isJsx(node) {
|
|
159
|
+
return node.type === AST_NODE_TYPES.JSXElement || node.type === AST_NODE_TYPES.JSXFragment;
|
|
160
|
+
}
|
|
161
|
+
function isNamedExportArrow(node) {
|
|
162
|
+
if (node.parent.type !== AST_NODE_TYPES.VariableDeclarator) return false;
|
|
163
|
+
return node.parent.parent.parent.type === AST_NODE_TYPES.ExportNamedDeclaration;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Does the collapsed body need wrapping parens to stay an expression body?
|
|
167
|
+
*
|
|
168
|
+
* @param node - The would-be implicit body expression.
|
|
169
|
+
* @returns Whether the fixer must wrap the body in parentheses.
|
|
170
|
+
*/
|
|
171
|
+
function needsParens(node) {
|
|
172
|
+
return node.type === AST_NODE_TYPES.ObjectExpression || node.type === AST_NODE_TYPES.SequenceExpression;
|
|
173
|
+
}
|
|
174
|
+
function countObjectComplexity(node) {
|
|
175
|
+
let features = 0;
|
|
176
|
+
for (const property of node.properties) {
|
|
177
|
+
if (property.type === AST_NODE_TYPES.SpreadElement) {
|
|
178
|
+
features += 1;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (property.computed) features += 1;
|
|
182
|
+
if (property.value.type === AST_NODE_TYPES.CallExpression) features += 1;
|
|
183
|
+
}
|
|
184
|
+
return features;
|
|
185
|
+
}
|
|
186
|
+
function isComplexLiteral(node, maxObjectProperties) {
|
|
187
|
+
if (node.type === AST_NODE_TYPES.ObjectExpression) {
|
|
188
|
+
if (node.properties.length > maxObjectProperties) return true;
|
|
189
|
+
return countObjectComplexity(node) >= 2;
|
|
190
|
+
}
|
|
191
|
+
if (node.type === AST_NODE_TYPES.ArrayExpression) {
|
|
192
|
+
const spreads = node.elements.filter((element) => element?.type === AST_NODE_TYPES.SpreadElement).length;
|
|
193
|
+
const calls = node.elements.filter((element) => element?.type === AST_NODE_TYPES.CallExpression).length;
|
|
194
|
+
return spreads >= 1 && node.elements.length > 1 || calls >= 2 || spreads + calls >= 2;
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
function isLiteralBody(node) {
|
|
199
|
+
return node.type === AST_NODE_TYPES.ObjectExpression || node.type === AST_NODE_TYPES.ArrayExpression;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Walks up to the outermost node whose parent is the Program.
|
|
203
|
+
*
|
|
204
|
+
* @param node - The node to walk up from.
|
|
205
|
+
* @returns The top-level statement containing `node`.
|
|
206
|
+
*/
|
|
207
|
+
function statementOf(node) {
|
|
208
|
+
let current = node;
|
|
209
|
+
while (current.parent !== void 0 && current.parent.type !== AST_NODE_TYPES.Program) current = current.parent;
|
|
210
|
+
return current;
|
|
211
|
+
}
|
|
212
|
+
function pushChildNodes$1(stack, value) {
|
|
213
|
+
const candidates = Array.isArray(value) ? value : [value];
|
|
214
|
+
for (const item of candidates) if (typeof item === "object" && item !== null && "type" in item) stack.push(item);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Collects arrow functions in `root`'s subtree, ordered by source position.
|
|
218
|
+
*
|
|
219
|
+
* @param root - The subtree to search.
|
|
220
|
+
* @returns All arrow function nodes, sorted by range start.
|
|
221
|
+
*/
|
|
222
|
+
function collectArrows(root) {
|
|
223
|
+
const arrows = [];
|
|
224
|
+
const stack = [root];
|
|
225
|
+
while (stack.length > 0) {
|
|
226
|
+
const current = stack.pop();
|
|
227
|
+
if (current === void 0) continue;
|
|
228
|
+
if (current.type === AST_NODE_TYPES.ArrowFunctionExpression) arrows.push(current);
|
|
229
|
+
for (const [key, value] of Object.entries(current)) if (key !== "parent" && key !== "loc" && key !== "range") pushChildNodes$1(stack, value);
|
|
230
|
+
}
|
|
231
|
+
return arrows.sort((a, b) => a.range[0] - b.range[0]);
|
|
232
|
+
}
|
|
233
|
+
const arrowReturnStyle = createFlawlessRule({
|
|
234
|
+
name: RULE_NAME$12,
|
|
235
|
+
createOnce(context) {
|
|
236
|
+
let config = DEFAULTS;
|
|
237
|
+
let sourceCode;
|
|
238
|
+
let lines;
|
|
239
|
+
let pendingConsults;
|
|
240
|
+
/**
|
|
241
|
+
* Effective line-length limit for emitted lines: the fixer must never
|
|
242
|
+
* produce a line the formatter would immediately rewrap.
|
|
243
|
+
*
|
|
244
|
+
* @returns The maximum permitted tab-expanded line width.
|
|
245
|
+
*/
|
|
246
|
+
function limit() {
|
|
247
|
+
const { maxLen, useOxfmt } = config;
|
|
248
|
+
return useOxfmt === false ? maxLen : Math.min(maxLen, printWidth());
|
|
249
|
+
}
|
|
250
|
+
function printWidth() {
|
|
251
|
+
const { maxLen, useOxfmt } = config;
|
|
252
|
+
if (typeof useOxfmt === "object" && typeof useOxfmt.printWidth === "number") return useOxfmt.printWidth;
|
|
253
|
+
return maxLen;
|
|
254
|
+
}
|
|
255
|
+
function width(text) {
|
|
256
|
+
return expandedWidth(text, config.tabWidth);
|
|
257
|
+
}
|
|
258
|
+
function lineOf(lineNumber) {
|
|
259
|
+
return lines[lineNumber - 1] ?? "";
|
|
260
|
+
}
|
|
261
|
+
function objectStyleWantsExplicit(body) {
|
|
262
|
+
if (!isLiteralBody(body) || config.objectReturnStyle === "off") return false;
|
|
263
|
+
if (config.objectReturnStyle === "always-explicit") return true;
|
|
264
|
+
return isComplexLiteral(body, config.maxObjectProperties);
|
|
265
|
+
}
|
|
266
|
+
function arrowTokenOf(node) {
|
|
267
|
+
let token = sourceCode.getTokenBefore(node.body);
|
|
268
|
+
while (token !== null && token.value === "(") token = sourceCode.getTokenBefore(token);
|
|
269
|
+
if (token?.value !== "=>") throw new Error("arrow-return-style: could not locate the => token");
|
|
270
|
+
return token;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* The `(`/`)` pair directly wrapping the body, if any.
|
|
274
|
+
*
|
|
275
|
+
* @param node - Function whose body may be parenthesized.
|
|
276
|
+
* @param arrowToken - Token of the `=>` operator, used to distinguish
|
|
277
|
+
* body parens from parameter-list parens.
|
|
278
|
+
* @returns The wrapping paren tokens, or `null` when not parenthesized.
|
|
279
|
+
*/
|
|
280
|
+
function bodyParens(node, arrowToken) {
|
|
281
|
+
const before = sourceCode.getTokenBefore(node.body);
|
|
282
|
+
if (before?.value !== "(" || before.range[0] < arrowToken.range[1]) return null;
|
|
283
|
+
const after = sourceCode.getTokenAfter(node.body);
|
|
284
|
+
if (after?.value !== ")") return null;
|
|
285
|
+
return {
|
|
286
|
+
close: after,
|
|
287
|
+
open: before
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Resolves every deferred oxfmt consult with a single worker round-trip:
|
|
292
|
+
* would the formatter render each arrow (params through body) on a single
|
|
293
|
+
* line that fits `maxLen`? Arrows the formatter cannot keep on one fitting
|
|
294
|
+
* line are reported. Fails open (no report) when the worker or oxfmt is
|
|
295
|
+
* unavailable so that an unavailable formatter can never introduce reports
|
|
296
|
+
* it would have to fight over.
|
|
297
|
+
*/
|
|
298
|
+
function resolvePendingConsults() {
|
|
299
|
+
if (pendingConsults.length === 0) return;
|
|
300
|
+
const nodes = pendingConsults;
|
|
301
|
+
pendingConsults = [];
|
|
302
|
+
const worker = getFormatSync();
|
|
303
|
+
if (worker === null) return;
|
|
304
|
+
const consults = nodes.flatMap((node) => {
|
|
305
|
+
const statement = statementOf(node);
|
|
306
|
+
const snippet = sourceCode.getText(statement);
|
|
307
|
+
const baseIndent = leadingWhitespace(lineOf(statement.loc.start.line));
|
|
308
|
+
const arrowIndex = collectArrows(statement).findIndex((arrow) => arrow.range[0] === node.range[0]);
|
|
309
|
+
if (arrowIndex === -1) return [];
|
|
310
|
+
const request = {
|
|
311
|
+
arrowIndex,
|
|
312
|
+
code: `${snippet}\n`,
|
|
313
|
+
printWidth: printWidth(),
|
|
314
|
+
tabWidth: config.tabWidth
|
|
315
|
+
};
|
|
316
|
+
return [{
|
|
317
|
+
baseIndent,
|
|
318
|
+
cacheKey: `${arrowIndex}${request.printWidth}${request.tabWidth}${snippet}`,
|
|
319
|
+
node,
|
|
320
|
+
request
|
|
321
|
+
}];
|
|
322
|
+
});
|
|
323
|
+
const misses = /* @__PURE__ */ new Map();
|
|
324
|
+
for (const consult of consults) if (formatCacheGet(consult.cacheKey) === void 0) misses.set(consult.cacheKey, consult.request);
|
|
325
|
+
if (misses.size > 0) {
|
|
326
|
+
let responses = [];
|
|
327
|
+
try {
|
|
328
|
+
responses = worker([...misses.values()]);
|
|
329
|
+
} catch {
|
|
330
|
+
responses = [];
|
|
331
|
+
}
|
|
332
|
+
for (const [index, cacheKey] of [...misses.keys()].entries()) {
|
|
333
|
+
const response = responses[index];
|
|
334
|
+
if (response !== void 0) formatCacheSet(cacheKey, response);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
for (const consult of consults) {
|
|
338
|
+
const response = formatCacheGet(consult.cacheKey);
|
|
339
|
+
if (response === void 0) continue;
|
|
340
|
+
if (response.lineText === null) continue;
|
|
341
|
+
if (!(response.singleLine && width(consult.baseIndent) + width(response.lineText) <= config.maxLen)) reportExplicit(consult.node, EXPLICIT);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Derives the one-level indent unit for an inserted block: prefer the
|
|
346
|
+
* offset between the arrow's line and the body's (or its continuation's)
|
|
347
|
+
* deeper indentation; otherwise infer from the indent character in use.
|
|
348
|
+
*
|
|
349
|
+
* @param arrowIndent - Leading whitespace of the arrow's line.
|
|
350
|
+
* @param node - The arrow function being fixed.
|
|
351
|
+
* @param bodyStart - The body's first token or node (including parens).
|
|
352
|
+
* @param bodyEnd - The body's last token or node (including parens).
|
|
353
|
+
* @returns The whitespace string for one indentation level.
|
|
354
|
+
*/
|
|
355
|
+
function indentUnit(arrowIndent, node, bodyStart, bodyEnd) {
|
|
356
|
+
const referenceLines = [];
|
|
357
|
+
const arrowLine = bodyStart.loc.start.line;
|
|
358
|
+
if (bodyStart.loc.start.line !== node.loc.start.line) referenceLines.push(bodyStart.loc.start.line);
|
|
359
|
+
else if (bodyEnd.loc.end.line > arrowLine) referenceLines.push(arrowLine + 1);
|
|
360
|
+
for (const lineNumber of referenceLines) {
|
|
361
|
+
const reference = leadingWhitespace(lineOf(lineNumber));
|
|
362
|
+
if (reference.length > arrowIndent.length && reference.startsWith(arrowIndent)) return reference.slice(arrowIndent.length);
|
|
363
|
+
}
|
|
364
|
+
if (arrowIndent.includes(" ")) return " ";
|
|
365
|
+
return arrowIndent.length > 0 ? " " : " ";
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Reports and fixes an implicit arrow into an explicit block body.
|
|
369
|
+
*
|
|
370
|
+
* @param node - The arrow function to convert.
|
|
371
|
+
* @param messageId - The violation to report.
|
|
372
|
+
*/
|
|
373
|
+
function reportExplicit(node, messageId) {
|
|
374
|
+
const arrowToken = arrowTokenOf(node);
|
|
375
|
+
const parens = bodyParens(node, arrowToken);
|
|
376
|
+
const bodyStart = parens?.open ?? node.body;
|
|
377
|
+
const bodyEnd = parens?.close ?? node.body;
|
|
378
|
+
const comments = sourceCode.getCommentsBefore(bodyStart);
|
|
379
|
+
const arrowIndent = leadingWhitespace(lineOf(node.loc.start.line));
|
|
380
|
+
const targetIndent = arrowIndent + indentUnit(arrowIndent, node, bodyStart, bodyEnd);
|
|
381
|
+
const bodyLines = sourceCode.getText(node.body).split("\n");
|
|
382
|
+
const [firstLine, ...restLines] = bodyLines;
|
|
383
|
+
const lastLineIndent = leadingWhitespace(bodyLines.at(-1) ?? "");
|
|
384
|
+
let shifted = restLines;
|
|
385
|
+
if (restLines.length > 0) {
|
|
386
|
+
if (targetIndent.startsWith(lastLineIndent)) {
|
|
387
|
+
const prefix = targetIndent.slice(lastLineIndent.length);
|
|
388
|
+
shifted = restLines.map((line) => prefix + line);
|
|
389
|
+
} else if (lastLineIndent.startsWith(targetIndent)) {
|
|
390
|
+
const strip = lastLineIndent.length - targetIndent.length;
|
|
391
|
+
shifted = restLines.map((line) => {
|
|
392
|
+
return leadingWhitespace(line).length >= strip ? line.slice(strip) : line;
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const commentLines = comments.map((comment) => targetIndent + sourceCode.getText(comment));
|
|
397
|
+
const returnLine = `${targetIndent}return ${[firstLine, ...shifted].join("\n")};`;
|
|
398
|
+
const replacement = ` {\n${[...commentLines, returnLine].join("\n")}\n${arrowIndent}}`;
|
|
399
|
+
context.report({
|
|
400
|
+
fix: (fixer) => fixer.replaceTextRange([arrowToken.range[1], bodyEnd.range[1]], replacement),
|
|
401
|
+
messageId,
|
|
402
|
+
node
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Reports and fixes a single-`return` block into an implicit body.
|
|
407
|
+
*
|
|
408
|
+
* @param node - The arrow function to convert.
|
|
409
|
+
* @param block - The block body being replaced.
|
|
410
|
+
* @param replacement - The implicit body text.
|
|
411
|
+
*/
|
|
412
|
+
function reportImplicit(node, block, replacement) {
|
|
413
|
+
context.report({
|
|
414
|
+
fix: (fixer) => fixer.replaceTextRange(block.range, replacement),
|
|
415
|
+
messageId: IMPLICIT,
|
|
416
|
+
node
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
function checkBlockBody(node) {
|
|
420
|
+
const block = node.body;
|
|
421
|
+
if (block.body.length !== 1) return;
|
|
422
|
+
const [statement] = block.body;
|
|
423
|
+
if (statement?.type !== AST_NODE_TYPES.ReturnStatement || statement.argument === null) return;
|
|
424
|
+
if (sourceCode.getCommentsInside(block).length > 0) return;
|
|
425
|
+
const { argument } = statement;
|
|
426
|
+
if (isJsx(argument) && config.jsxAlwaysUseExplicitReturn) return;
|
|
427
|
+
if (isNamedExportArrow(node) && config.namedExportsAlwaysUseExplicitReturn) return;
|
|
428
|
+
if (objectStyleWantsExplicit(argument)) return;
|
|
429
|
+
if (argument.loc.start.line !== argument.loc.end.line) return;
|
|
430
|
+
const argumentText = sourceCode.getText(argument);
|
|
431
|
+
const collapsed = needsParens(argument) ? `(${argumentText})` : argumentText;
|
|
432
|
+
const prefix = lineOf(block.loc.start.line).slice(0, block.loc.start.column);
|
|
433
|
+
const suffix = lineOf(block.loc.end.line).slice(block.loc.end.column);
|
|
434
|
+
if (width(prefix + collapsed + suffix) > limit()) return;
|
|
435
|
+
reportImplicit(node, block, collapsed);
|
|
436
|
+
}
|
|
437
|
+
function checkExpressionBody(node) {
|
|
438
|
+
const body = node.body;
|
|
439
|
+
const arrowToken = arrowTokenOf(node);
|
|
440
|
+
const parens = bodyParens(node, arrowToken);
|
|
441
|
+
const bodyStart = parens?.open ?? body;
|
|
442
|
+
const bodyEnd = parens?.close ?? body;
|
|
443
|
+
if (sourceCode.getCommentsBefore(bodyStart).length > 0) {
|
|
444
|
+
reportExplicit(node, EXPLICIT);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (isJsx(body) && config.jsxAlwaysUseExplicitReturn) {
|
|
448
|
+
reportExplicit(node, EXPLICIT);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (isNamedExportArrow(node) && config.namedExportsAlwaysUseExplicitReturn) {
|
|
452
|
+
reportExplicit(node, EXPLICIT);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (bodyStart.loc.start.line !== arrowToken.loc.end.line) {
|
|
456
|
+
reportExplicit(node, EXPLICIT);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (bodyEnd.loc.end.line !== bodyStart.loc.start.line) {
|
|
460
|
+
if (body.type === AST_NODE_TYPES.ObjectExpression) reportExplicit(node, EXPLICIT);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (width(lineOf(bodyStart.loc.start.line)) > limit()) {
|
|
464
|
+
if (config.useOxfmt !== false) pendingConsults.push(node);
|
|
465
|
+
else reportExplicit(node, EXPLICIT);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (objectStyleWantsExplicit(body)) reportExplicit(node, COMPLEX_EXPLICIT);
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
"ArrowFunctionExpression": function(node) {
|
|
472
|
+
if (node.body.type === AST_NODE_TYPES.BlockStatement) checkBlockBody(node);
|
|
473
|
+
else checkExpressionBody(node);
|
|
474
|
+
},
|
|
475
|
+
"before": function() {
|
|
476
|
+
config = {
|
|
477
|
+
...DEFAULTS,
|
|
478
|
+
...context.options[0]
|
|
479
|
+
};
|
|
480
|
+
({sourceCode} = context);
|
|
481
|
+
lines = [...sourceCode.lines];
|
|
482
|
+
pendingConsults = [];
|
|
483
|
+
},
|
|
484
|
+
"Program:exit": function() {
|
|
485
|
+
resolvePendingConsults();
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
},
|
|
489
|
+
defaultOptions: [DEFAULTS],
|
|
490
|
+
meta: {
|
|
491
|
+
docs: {
|
|
492
|
+
description: "Enforce arrow function return style based on line length",
|
|
493
|
+
requiresTypeChecking: false
|
|
494
|
+
},
|
|
495
|
+
fixable: "code",
|
|
496
|
+
messages: {
|
|
497
|
+
[COMPLEX_EXPLICIT]: "Use an explicit return block for complex object or array bodies.",
|
|
498
|
+
[EXPLICIT]: "Use an explicit return block for this arrow function body.",
|
|
499
|
+
[IMPLICIT]: "Use an implicit return for this arrow function body."
|
|
500
|
+
},
|
|
501
|
+
schema: [{
|
|
502
|
+
additionalProperties: false,
|
|
503
|
+
properties: {
|
|
504
|
+
jsxAlwaysUseExplicitReturn: { type: "boolean" },
|
|
505
|
+
maxLen: {
|
|
506
|
+
minimum: 0,
|
|
507
|
+
type: "integer"
|
|
508
|
+
},
|
|
509
|
+
maxObjectProperties: {
|
|
510
|
+
minimum: 0,
|
|
511
|
+
type: "integer"
|
|
512
|
+
},
|
|
513
|
+
namedExportsAlwaysUseExplicitReturn: { type: "boolean" },
|
|
514
|
+
objectReturnStyle: {
|
|
515
|
+
enum: [
|
|
516
|
+
"always-explicit",
|
|
517
|
+
"complex-explicit",
|
|
518
|
+
"off"
|
|
519
|
+
],
|
|
520
|
+
type: "string"
|
|
521
|
+
},
|
|
522
|
+
tabWidth: {
|
|
523
|
+
minimum: 1,
|
|
524
|
+
type: "integer"
|
|
525
|
+
},
|
|
526
|
+
useOxfmt: { oneOf: [{ type: "boolean" }, {
|
|
527
|
+
additionalProperties: false,
|
|
528
|
+
properties: { printWidth: {
|
|
529
|
+
minimum: 0,
|
|
530
|
+
type: "integer"
|
|
531
|
+
} },
|
|
532
|
+
type: "object"
|
|
533
|
+
}] }
|
|
534
|
+
},
|
|
535
|
+
type: "object"
|
|
536
|
+
}],
|
|
537
|
+
type: "suggestion"
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
//#endregion
|
|
81
541
|
//#region src/rules/jsx-shorthand-boolean/rule.ts
|
|
82
|
-
const RULE_NAME$
|
|
83
|
-
const MESSAGE_ID$
|
|
84
|
-
const messages$
|
|
85
|
-
function createOnce$
|
|
542
|
+
const RULE_NAME$11 = "jsx-shorthand-boolean";
|
|
543
|
+
const MESSAGE_ID$5 = "setAttributeValue";
|
|
544
|
+
const messages$11 = { [MESSAGE_ID$5]: "Set an explicit value for boolean attribute '{{name}}'." };
|
|
545
|
+
function createOnce$7(context) {
|
|
86
546
|
return { JSXAttribute(node) {
|
|
87
547
|
if (node.value !== null) return;
|
|
88
548
|
context.report({
|
|
89
549
|
data: { name: context.sourceCode.getText(node.name) },
|
|
90
550
|
fix: (fixer) => fixer.insertTextAfter(node.name, "={true}"),
|
|
91
|
-
messageId: MESSAGE_ID$
|
|
551
|
+
messageId: MESSAGE_ID$5,
|
|
92
552
|
node
|
|
93
553
|
});
|
|
94
554
|
} };
|
|
95
555
|
}
|
|
96
556
|
const jsxShorthandBoolean = createFlawlessRule({
|
|
97
|
-
name: RULE_NAME$
|
|
98
|
-
createOnce: createOnce$
|
|
557
|
+
name: RULE_NAME$11,
|
|
558
|
+
createOnce: createOnce$7,
|
|
99
559
|
defaultOptions: [],
|
|
100
560
|
meta: {
|
|
101
561
|
docs: {
|
|
@@ -105,19 +565,19 @@ const jsxShorthandBoolean = createFlawlessRule({
|
|
|
105
565
|
},
|
|
106
566
|
fixable: "code",
|
|
107
567
|
hasSuggestions: false,
|
|
108
|
-
messages: messages$
|
|
568
|
+
messages: messages$11,
|
|
109
569
|
schema: [],
|
|
110
570
|
type: "suggestion"
|
|
111
571
|
}
|
|
112
572
|
});
|
|
113
573
|
//#endregion
|
|
114
574
|
//#region src/rules/jsx-shorthand-fragment/rule.ts
|
|
115
|
-
const RULE_NAME$
|
|
575
|
+
const RULE_NAME$10 = "jsx-shorthand-fragment";
|
|
116
576
|
const MESSAGE_ID_NAMED = "useNamedFragment";
|
|
117
577
|
const MESSAGE_ID_SHORTHAND = "useShorthandFragment";
|
|
118
578
|
const DEFAULT_MODE = "syntax";
|
|
119
579
|
const DEFAULT_FRAGMENT_NAME = "Fragment";
|
|
120
|
-
const messages$
|
|
580
|
+
const messages$10 = {
|
|
121
581
|
[MESSAGE_ID_NAMED]: "Use the '{{name}}' component instead of fragment shorthand syntax.",
|
|
122
582
|
[MESSAGE_ID_SHORTHAND]: "Use the fragment shorthand syntax '<>...</>' instead of the '{{name}}' component."
|
|
123
583
|
};
|
|
@@ -153,7 +613,7 @@ function jsxNameToString(node) {
|
|
|
153
613
|
}
|
|
154
614
|
return null;
|
|
155
615
|
}
|
|
156
|
-
function createOnce$
|
|
616
|
+
function createOnce$6(context) {
|
|
157
617
|
let mode;
|
|
158
618
|
let fragmentName;
|
|
159
619
|
let namedFragments;
|
|
@@ -200,8 +660,8 @@ function createOnce$5(context) {
|
|
|
200
660
|
};
|
|
201
661
|
}
|
|
202
662
|
const jsxShorthandFragment = createFlawlessRule({
|
|
203
|
-
name: RULE_NAME$
|
|
204
|
-
createOnce: createOnce$
|
|
663
|
+
name: RULE_NAME$10,
|
|
664
|
+
createOnce: createOnce$6,
|
|
205
665
|
defaultOptions: [{
|
|
206
666
|
fragmentName: DEFAULT_FRAGMENT_NAME,
|
|
207
667
|
mode: DEFAULT_MODE
|
|
@@ -218,7 +678,7 @@ const jsxShorthandFragment = createFlawlessRule({
|
|
|
218
678
|
},
|
|
219
679
|
fixable: "code",
|
|
220
680
|
hasSuggestions: false,
|
|
221
|
-
messages: messages$
|
|
681
|
+
messages: messages$10,
|
|
222
682
|
schema: schema$2,
|
|
223
683
|
type: "suggestion"
|
|
224
684
|
}
|
|
@@ -1383,8 +1843,8 @@ const SCHEMA = {
|
|
|
1383
1843
|
};
|
|
1384
1844
|
//#endregion
|
|
1385
1845
|
//#region src/rules/naming-convention/rule.ts
|
|
1386
|
-
const RULE_NAME$
|
|
1387
|
-
const messages$
|
|
1846
|
+
const RULE_NAME$9 = "naming-convention";
|
|
1847
|
+
const messages$9 = {
|
|
1388
1848
|
doesNotMatchFormat: "{{type}} name `{{name}}` must match one of the following formats: {{formats}}",
|
|
1389
1849
|
doesNotMatchFormatTrimmed: "{{type}} name `{{name}}` trimmed as `{{processedName}}` must match one of the following formats: {{formats}}",
|
|
1390
1850
|
missingAffix: "{{type}} name `{{name}}` must have one of the following {{position}}es: {{affixes}}",
|
|
@@ -1732,7 +2192,7 @@ function create$3(contextWithoutDefaults) {
|
|
|
1732
2192
|
}));
|
|
1733
2193
|
}
|
|
1734
2194
|
const namingConvention = createEslintRule({
|
|
1735
|
-
name: RULE_NAME$
|
|
2195
|
+
name: RULE_NAME$9,
|
|
1736
2196
|
create: create$3,
|
|
1737
2197
|
defaultOptions: camelCaseNamingConfig,
|
|
1738
2198
|
meta: {
|
|
@@ -1743,7 +2203,7 @@ const namingConvention = createEslintRule({
|
|
|
1743
2203
|
},
|
|
1744
2204
|
fixable: void 0,
|
|
1745
2205
|
hasSuggestions: false,
|
|
1746
|
-
messages: messages$
|
|
2206
|
+
messages: messages$9,
|
|
1747
2207
|
schema: SCHEMA,
|
|
1748
2208
|
type: "suggestion"
|
|
1749
2209
|
}
|
|
@@ -1773,6 +2233,121 @@ function requiresQuoting$1(node, target) {
|
|
|
1773
2233
|
return requiresQuoting(node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.PrivateIdentifier ? node.name : `${node.value}`, target);
|
|
1774
2234
|
}
|
|
1775
2235
|
//#endregion
|
|
2236
|
+
//#region src/rules/no-export-default-arrow/rule.ts
|
|
2237
|
+
const RULE_NAME$8 = "no-export-default-arrow";
|
|
2238
|
+
const MESSAGE_ID$4 = "disallowExportDefaultArrow";
|
|
2239
|
+
const messages$8 = { [MESSAGE_ID$4]: "Assign the arrow function to a named constant instead of exporting it anonymously." };
|
|
2240
|
+
/**
|
|
2241
|
+
* Converts a filename stem to camelCase, treating `-`, `_`, and whitespace as
|
|
2242
|
+
* word separators (`use-mouse` -> `useMouse`).
|
|
2243
|
+
*
|
|
2244
|
+
* @param value - The filename stem.
|
|
2245
|
+
* @returns The camelCase name.
|
|
2246
|
+
*/
|
|
2247
|
+
function toCamelCase(value) {
|
|
2248
|
+
return value.replace(/[-_\s]+(.)?/g, (_, char) => char?.toUpperCase() ?? "").replace(/^[A-Z]/, (char) => char.toLowerCase());
|
|
2249
|
+
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Converts a filename stem to PascalCase, treating `-`, `_`, and whitespace as
|
|
2252
|
+
* word separators (`use-mouse` -> `UseMouse`).
|
|
2253
|
+
*
|
|
2254
|
+
* @param value - The filename stem.
|
|
2255
|
+
* @returns The PascalCase name.
|
|
2256
|
+
*/
|
|
2257
|
+
function toPascalCase(value) {
|
|
2258
|
+
return value.replace(/[-_\s]+(.)?/g, (_, char) => char?.toUpperCase() ?? "").replace(/^[a-z]/, (char) => char.toUpperCase());
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Checks whether a node is a JSX element or fragment.
|
|
2262
|
+
*
|
|
2263
|
+
* @param node - The expression to test.
|
|
2264
|
+
* @returns `true` when the node renders JSX.
|
|
2265
|
+
*/
|
|
2266
|
+
function isJsxElement(node) {
|
|
2267
|
+
return node.type === AST_NODE_TYPES.JSXElement || node.type === AST_NODE_TYPES.JSXFragment;
|
|
2268
|
+
}
|
|
2269
|
+
/**
|
|
2270
|
+
* Collects the expressions an arrow function can return: the body itself for a
|
|
2271
|
+
* concise arrow, or every `return` argument for a block body.
|
|
2272
|
+
*
|
|
2273
|
+
* @param body - The arrow function's body.
|
|
2274
|
+
* @returns The returned expressions, in source order.
|
|
2275
|
+
*/
|
|
2276
|
+
function getArrowReturnValues(body) {
|
|
2277
|
+
if (body.type !== AST_NODE_TYPES.BlockStatement) return [body];
|
|
2278
|
+
return body.body.filter((node) => node.type === AST_NODE_TYPES.ReturnStatement).map((node) => node.argument).filter((argument) => argument !== null);
|
|
2279
|
+
}
|
|
2280
|
+
/**
|
|
2281
|
+
* Determines whether an arrow function is a component — that is, whether any of
|
|
2282
|
+
* its return values is JSX — which selects PascalCase for the generated name.
|
|
2283
|
+
*
|
|
2284
|
+
* @param body - The arrow function's body.
|
|
2285
|
+
* @returns `true` when the arrow can return JSX.
|
|
2286
|
+
*/
|
|
2287
|
+
function arrowReturnIsJsxElement(body) {
|
|
2288
|
+
return getArrowReturnValues(body).some((node) => isJsxElement(node));
|
|
2289
|
+
}
|
|
2290
|
+
/**
|
|
2291
|
+
* Builds the autofix: replaces the `export default` declaration with a named
|
|
2292
|
+
* `const` derived from the filename, and appends `export default <name>` after
|
|
2293
|
+
* the file's last token (comments included, so a trailing comment keeps its
|
|
2294
|
+
* position).
|
|
2295
|
+
*
|
|
2296
|
+
* @param options - The reported arrow, its export declaration, and the context
|
|
2297
|
+
* and source code needed to derive the name and locate the file's end.
|
|
2298
|
+
* @returns A fixer callback producing both edits.
|
|
2299
|
+
*/
|
|
2300
|
+
function createFixFunction({ arrowFunction, context, exportDeclaration, sourceCode }) {
|
|
2301
|
+
return (fixer) => {
|
|
2302
|
+
const program = sourceCode.ast;
|
|
2303
|
+
const lastToken = sourceCode.getLastToken(program, { includeComments: true });
|
|
2304
|
+
const fileName = context.physicalFilename || context.filename || "namedFunction";
|
|
2305
|
+
const { name: stem } = path.parse(fileName);
|
|
2306
|
+
const functionName = arrowReturnIsJsxElement(arrowFunction.body) ? toPascalCase(stem) : toCamelCase(stem);
|
|
2307
|
+
return [fixer.replaceText(exportDeclaration, `const ${functionName} = ${sourceCode.getText(arrowFunction)}`), fixer.insertTextAfter(lastToken ?? exportDeclaration, `\n\nexport default ${functionName}`)];
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Reports anonymous arrow functions used as `export default`, which surface as
|
|
2312
|
+
* unnamed functions in stack traces and devtools.
|
|
2313
|
+
*
|
|
2314
|
+
* @param context - The rule context.
|
|
2315
|
+
* @returns The rule listener.
|
|
2316
|
+
*/
|
|
2317
|
+
function createOnce$5(context) {
|
|
2318
|
+
return { ArrowFunctionExpression(node) {
|
|
2319
|
+
const { parent } = node;
|
|
2320
|
+
if (parent.type !== AST_NODE_TYPES.ExportDefaultDeclaration) return;
|
|
2321
|
+
context.report({
|
|
2322
|
+
fix: createFixFunction({
|
|
2323
|
+
arrowFunction: node,
|
|
2324
|
+
context,
|
|
2325
|
+
exportDeclaration: parent,
|
|
2326
|
+
sourceCode: context.sourceCode
|
|
2327
|
+
}),
|
|
2328
|
+
messageId: MESSAGE_ID$4,
|
|
2329
|
+
node
|
|
2330
|
+
});
|
|
2331
|
+
} };
|
|
2332
|
+
}
|
|
2333
|
+
const noExportDefaultArrow = createFlawlessRule({
|
|
2334
|
+
name: RULE_NAME$8,
|
|
2335
|
+
createOnce: createOnce$5,
|
|
2336
|
+
defaultOptions: [],
|
|
2337
|
+
meta: {
|
|
2338
|
+
docs: {
|
|
2339
|
+
description: "Disallow anonymous arrow functions as export default declarations",
|
|
2340
|
+
recommended: false,
|
|
2341
|
+
requiresTypeChecking: false
|
|
2342
|
+
},
|
|
2343
|
+
fixable: "code",
|
|
2344
|
+
hasSuggestions: false,
|
|
2345
|
+
messages: messages$8,
|
|
2346
|
+
schema: [],
|
|
2347
|
+
type: "suggestion"
|
|
2348
|
+
}
|
|
2349
|
+
});
|
|
2350
|
+
//#endregion
|
|
1776
2351
|
//#region src/utils/nested-expressions.ts
|
|
1777
2352
|
/**
|
|
1778
2353
|
* Determines whether the given AST subtree contains a call or `new`
|
|
@@ -3631,9 +4206,11 @@ const plugin = {
|
|
|
3631
4206
|
version
|
|
3632
4207
|
},
|
|
3633
4208
|
rules: {
|
|
4209
|
+
"arrow-return-style": arrowReturnStyle,
|
|
3634
4210
|
"jsx-shorthand-boolean": jsxShorthandBoolean,
|
|
3635
4211
|
"jsx-shorthand-fragment": jsxShorthandFragment,
|
|
3636
4212
|
"naming-convention": namingConvention,
|
|
4213
|
+
"no-export-default-arrow": noExportDefaultArrow,
|
|
3637
4214
|
"no-unnecessary-use-callback": noUnnecessaryUseCallback,
|
|
3638
4215
|
"no-unnecessary-use-memo": noUnnecessaryUseMemo,
|
|
3639
4216
|
"prefer-destructuring-assignment": preferDestructuringAssignment,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/rules/arrow-return-style/worker.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* One entry of the batched request sent by the rule: format `code` with oxfmt
|
|
4
|
+
* and locate the `arrowIndex`-th arrow function (in source order) in the
|
|
5
|
+
* formatted output. Requests are batched per linted file so a file pays the
|
|
6
|
+
* worker round-trip cost once, not once per arrow.
|
|
7
|
+
*/
|
|
8
|
+
interface FormatRequest {
|
|
9
|
+
arrowIndex: number;
|
|
10
|
+
code: string;
|
|
11
|
+
printWidth: number;
|
|
12
|
+
tabWidth: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Response: whether the arrow (params through body) occupies a single line in
|
|
16
|
+
* the formatted output, and that line's text for width measurement. `lineText`
|
|
17
|
+
* is `null` when formatting failed or the arrow could not be located, which
|
|
18
|
+
* callers must treat as "no verdict".
|
|
19
|
+
*/
|
|
20
|
+
interface FormatResponse {
|
|
21
|
+
lineText: null | string;
|
|
22
|
+
singleLine: boolean;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { FormatRequest, FormatResponse };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { runAsWorker } from "synckit";
|
|
2
|
+
//#region src/rules/arrow-return-style/worker.ts
|
|
3
|
+
function isNodeLike(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && typeof value.type === "string" && typeof value.start === "number" && typeof value.end === "number";
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Collects all arrow function nodes in the tree, ordered by source position.
|
|
8
|
+
*
|
|
9
|
+
* @param root - The parsed AST to search.
|
|
10
|
+
* @returns All arrow function nodes, sorted by start offset.
|
|
11
|
+
*/
|
|
12
|
+
function collectArrows(root) {
|
|
13
|
+
const arrows = [];
|
|
14
|
+
const stack = [root];
|
|
15
|
+
while (stack.length > 0) {
|
|
16
|
+
const current = stack.pop();
|
|
17
|
+
if (typeof current !== "object" || current === null) continue;
|
|
18
|
+
if (isNodeLike(current) && current.type === "ArrowFunctionExpression") arrows.push(current);
|
|
19
|
+
for (const value of Object.values(current)) if (Array.isArray(value)) stack.push(...value);
|
|
20
|
+
else if (typeof value === "object" && value !== null) stack.push(value);
|
|
21
|
+
}
|
|
22
|
+
return arrows.sort((a, b) => a.start - b.start);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Start offsets of every line in `code`, for offset-to-line lookups.
|
|
26
|
+
*
|
|
27
|
+
* @param code - The source text.
|
|
28
|
+
* @returns Ascending offsets at which each line begins.
|
|
29
|
+
*/
|
|
30
|
+
function lineStartsOf(code) {
|
|
31
|
+
const starts = [0];
|
|
32
|
+
for (let index = 0; index < code.length; index += 1) if (code.charAt(index) === "\n") starts.push(index + 1);
|
|
33
|
+
return starts;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Index (0-based) of the line containing `offset`.
|
|
37
|
+
*
|
|
38
|
+
* @param starts - Line start offsets from {@link lineStartsOf}.
|
|
39
|
+
* @param offset - The source offset to locate.
|
|
40
|
+
* @returns The containing line's index.
|
|
41
|
+
*/
|
|
42
|
+
function lineIndexOf(starts, offset) {
|
|
43
|
+
let low = 0;
|
|
44
|
+
let high = starts.length - 1;
|
|
45
|
+
while (low < high) {
|
|
46
|
+
const mid = Math.ceil((low + high) / 2);
|
|
47
|
+
if ((starts[mid] ?? 0) <= offset) low = mid;
|
|
48
|
+
else high = mid - 1;
|
|
49
|
+
}
|
|
50
|
+
return low;
|
|
51
|
+
}
|
|
52
|
+
async function formatOne(request) {
|
|
53
|
+
const { format } = await import("oxfmt");
|
|
54
|
+
const { parseSync } = await import("oxc-parser");
|
|
55
|
+
const result = await format("snippet.tsx", request.code, {
|
|
56
|
+
endOfLine: "lf",
|
|
57
|
+
printWidth: request.printWidth,
|
|
58
|
+
semi: true,
|
|
59
|
+
tabWidth: request.tabWidth,
|
|
60
|
+
useTabs: true
|
|
61
|
+
});
|
|
62
|
+
if (result.errors.length > 0) return {
|
|
63
|
+
lineText: null,
|
|
64
|
+
singleLine: false
|
|
65
|
+
};
|
|
66
|
+
const parsed = parseSync("snippet.tsx", result.code);
|
|
67
|
+
if (parsed.errors.length > 0) return {
|
|
68
|
+
lineText: null,
|
|
69
|
+
singleLine: false
|
|
70
|
+
};
|
|
71
|
+
const arrow = collectArrows(parsed.program)[request.arrowIndex];
|
|
72
|
+
if (arrow === void 0) return {
|
|
73
|
+
lineText: null,
|
|
74
|
+
singleLine: false
|
|
75
|
+
};
|
|
76
|
+
const starts = lineStartsOf(result.code);
|
|
77
|
+
const startLine = lineIndexOf(starts, arrow.start);
|
|
78
|
+
const lineEnd = starts[startLine + 1] ?? result.code.length + 1;
|
|
79
|
+
return {
|
|
80
|
+
lineText: result.code.slice(starts[startLine], lineEnd - 1),
|
|
81
|
+
singleLine: startLine === lineIndexOf(starts, arrow.end)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
runAsWorker(async (requests) => {
|
|
85
|
+
const responses = [];
|
|
86
|
+
for (const request of requests) responses.push(await formatOne(request));
|
|
87
|
+
return responses;
|
|
88
|
+
});
|
|
89
|
+
//#endregion
|
|
90
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-flawless",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Your ESLint plugin description",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
"@typescript-eslint/scope-manager": "8.62.1",
|
|
41
41
|
"@typescript-eslint/type-utils": "8.62.1",
|
|
42
42
|
"@typescript-eslint/utils": "8.62.1",
|
|
43
|
+
"oxc-parser": "0.112.0",
|
|
44
|
+
"oxfmt": "0.59.0",
|
|
45
|
+
"synckit": "0.11.13",
|
|
43
46
|
"toml-eslint-parser": "1.0.3"
|
|
44
47
|
},
|
|
45
48
|
"devDependencies": {
|
|
@@ -58,6 +61,7 @@
|
|
|
58
61
|
"eslint-plugin-pnpm": "1.6.1",
|
|
59
62
|
"eslint-plugin-toml": "1.4.0",
|
|
60
63
|
"eslint-plugin-yml": "3.5.0",
|
|
64
|
+
"eslint-rule-benchmark": "0.8.0",
|
|
61
65
|
"eslint-vitest-rule-tester": "3.1.0",
|
|
62
66
|
"jiti": "2.7.0",
|
|
63
67
|
"lint-staged": "17.0.8",
|
|
@@ -82,6 +86,8 @@
|
|
|
82
86
|
"provenance": true
|
|
83
87
|
},
|
|
84
88
|
"scripts": {
|
|
89
|
+
"prebench": "nr build",
|
|
90
|
+
"bench": "eslint-rule-benchmark run --eslint-config benchmark/eslint.config.mjs",
|
|
85
91
|
"build": "tsdown --clean --dts",
|
|
86
92
|
"create-rule": "tsx scripts/create-rule.ts",
|
|
87
93
|
"dev": "tsdown --stub",
|