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 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
@@ -1,4 +1,4 @@
1
- import { n as plugin, t as PLUGIN_NAME } from "./plugin-BvQt8gSt.mjs";
1
+ import { n as plugin, t as PLUGIN_NAME } from "./plugin-DYVtpuio.mjs";
2
2
  //#region src/configs/index.ts
3
3
  const configs = { recommended: {
4
4
  plugins: { [PLUGIN_NAME]: plugin },
package/dist/oxlint.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as plugin, t as PLUGIN_NAME } from "./plugin-BvQt8gSt.mjs";
1
+ import { n as plugin, t as PLUGIN_NAME } from "./plugin-DYVtpuio.mjs";
2
2
  import { definePlugin } from "@oxlint/plugins";
3
3
  //#region src/oxlint.ts
4
4
  /**
@@ -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.11";
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$10 = "jsx-shorthand-boolean";
83
- const MESSAGE_ID$4 = "setAttributeValue";
84
- const messages$10 = { [MESSAGE_ID$4]: "Set an explicit value for boolean attribute '{{name}}'." };
85
- function createOnce$6(context) {
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$4,
551
+ messageId: MESSAGE_ID$5,
92
552
  node
93
553
  });
94
554
  } };
95
555
  }
96
556
  const jsxShorthandBoolean = createFlawlessRule({
97
- name: RULE_NAME$10,
98
- createOnce: createOnce$6,
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$10,
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$9 = "jsx-shorthand-fragment";
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$9 = {
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$5(context) {
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$9,
204
- createOnce: createOnce$5,
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$9,
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$8 = "naming-convention";
1387
- const messages$8 = {
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$8,
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$8,
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.11",
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",