eslint-plugin-code-style 1.3.10 → 1.4.3

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/index.js CHANGED
@@ -1794,32 +1794,78 @@ const functionNamingConvention = {
1794
1794
 
1795
1795
  const hookRegex = /^use[A-Z]/;
1796
1796
 
1797
+ // Comprehensive list of English verbs commonly used in function names
1798
+ // Organized by category for maintainability
1797
1799
  const verbPrefixes = [
1800
+ // CRUD & Data operations
1798
1801
  "get", "set", "fetch", "load", "save", "create", "update", "delete", "remove", "add",
1799
- "is", "has", "can", "should", "will", "did", "was", "were",
1800
- "check", "validate", "verify", "confirm", "ensure",
1802
+ "insert", "append", "prepend", "push", "pop", "shift", "unshift", "put", "patch",
1803
+ // Boolean checks
1804
+ "is", "has", "can", "should", "will", "did", "was", "were", "does", "do",
1805
+ // Validation
1806
+ "check", "validate", "verify", "confirm", "ensure", "assert", "test", "match",
1807
+ // Search & Filter
1801
1808
  "find", "search", "filter", "sort", "map", "reduce", "merge", "split", "join",
1809
+ "query", "lookup", "locate", "detect", "identify", "discover", "scan", "probe",
1810
+ // Transformation
1802
1811
  "parse", "format", "convert", "transform", "normalize", "serialize", "deserialize",
1803
- "encode", "decode", "encrypt", "decrypt", "hash", "sign",
1804
- "render", "display", "show", "hide", "toggle", "enable", "disable",
1805
- "open", "close", "start", "stop", "init", "setup", "reset", "clear",
1806
- "connect", "disconnect", "subscribe", "unsubscribe", "listen", "emit",
1812
+ "encode", "decode", "encrypt", "decrypt", "hash", "sign", "compress", "decompress",
1813
+ "stringify", "objectify", "flatten", "unflatten", "transpose", "invert",
1814
+ // String manipulation
1815
+ "strip", "trim", "pad", "wrap", "unwrap", "sanitize", "escape", "unescape",
1816
+ "capitalize", "lowercase", "uppercase", "camel", "snake", "kebab", "truncate",
1817
+ "replace", "substitute", "interpolate", "template", "render",
1818
+ // Display & UI
1819
+ "display", "show", "hide", "toggle", "enable", "disable", "activate", "deactivate",
1820
+ "highlight", "focus", "blur", "select", "deselect", "expand", "collapse", "resize",
1821
+ "animate", "transition", "fade", "slide", "zoom", "rotate", "scale",
1822
+ // Lifecycle
1823
+ "open", "close", "start", "stop", "init", "setup", "reset", "clear", "destroy",
1824
+ "mount", "unmount", "attach", "detach", "bind", "unbind", "dispose", "cleanup",
1825
+ "register", "unregister", "install", "uninstall", "configure", "reconfigure",
1826
+ // Network & Communication
1827
+ "connect", "disconnect", "subscribe", "unsubscribe", "listen", "emit", "broadcast",
1807
1828
  "send", "receive", "request", "respond", "submit", "cancel", "abort", "poll",
1808
- "read", "write", "copy", "move", "clone", "extract", "insert", "append", "prepend",
1829
+ "download", "upload", "import", "export", "sync", "stream", "pipe",
1830
+ // File & I/O
1831
+ "read", "write", "copy", "move", "clone", "extract", "archive", "restore", "backup",
1832
+ // Computation
1809
1833
  "build", "make", "generate", "compute", "calculate", "process", "execute", "run",
1810
- "apply", "call", "invoke", "trigger", "fire", "dispatch",
1811
- "mount", "unmount", "attach", "detach", "bind", "unbind",
1812
- "register", "unregister", "activate", "deactivate",
1813
- "login", "logout", "authenticate", "authorize",
1814
- "navigate", "redirect", "route", "scroll", "focus", "blur",
1815
- "select", "deselect", "expand", "collapse", "resize", "refresh", "reload",
1816
- "download", "upload", "import", "export", "sync", "async",
1817
- "log", "warn", "error", "debug", "trace", "print",
1818
- "wrap", "unwrap", "sanitize", "escape", "unescape", "trim", "pad",
1819
- "count", "sum", "avg", "min", "max", "clamp", "round", "floor", "ceil",
1820
- "throw", "catch", "resolve", "reject", "retry", "await",
1834
+ "evaluate", "analyze", "measure", "benchmark", "profile", "optimize",
1835
+ "count", "sum", "avg", "min", "max", "clamp", "round", "floor", "ceil", "abs",
1836
+ // Invocation
1837
+ "apply", "call", "invoke", "trigger", "fire", "dispatch", "emit", "raise", "signal",
1838
+ // Auth
1839
+ "login", "logout", "authenticate", "authorize", "grant", "revoke", "permit", "deny",
1840
+ // Navigation
1841
+ "navigate", "redirect", "route", "scroll", "jump", "go", "back", "forward",
1842
+ "refresh", "reload", "restore",
1843
+ // Logging
1844
+ "log", "warn", "error", "debug", "trace", "print", "dump", "inspect",
1845
+ // Error handling
1846
+ "throw", "catch", "resolve", "reject", "retry", "await", "recover", "fallback",
1847
+ // Performance
1821
1848
  "debounce", "throttle", "memoize", "cache", "batch", "queue", "defer", "delay",
1822
- "handle", "on", "click", "change", "input", "submit", "press", "drag", "drop",
1849
+ "schedule", "preload", "prefetch", "lazy",
1850
+ // Events
1851
+ "handle", "on", "click", "change", "input", "press", "drag", "drop",
1852
+ "hover", "enter", "leave", "touch", "swipe", "pinch", "tap",
1853
+ // Comparison
1854
+ "compare", "diff", "equal", "differ", "overlap", "intersect", "union", "exclude",
1855
+ // Grouping
1856
+ "group", "ungroup", "partition", "chunk", "segment", "categorize", "classify",
1857
+ // Ordering
1858
+ "order", "reorder", "arrange", "reverse", "shuffle", "randomize", "pick", "sample",
1859
+ // Validation state
1860
+ "lock", "unlock", "freeze", "unfreeze", "seal", "mark", "unmark", "flag", "unflag",
1861
+ // Misc common verbs
1862
+ "use", "require", "need", "want", "try", "attempt", "ensure", "guarantee",
1863
+ "prepare", "finalize", "complete", "finish", "end", "begin", "continue", "resume",
1864
+ "pause", "suspend", "interrupt", "break", "skip", "ignore", "include", "exclude",
1865
+ "accept", "decline", "approve", "reject", "confirm", "dismiss", "acknowledge",
1866
+ "assign", "allocate", "distribute", "collect", "gather", "aggregate", "accumulate",
1867
+ "populate", "fill", "empty", "drain", "flush", "purge", "prune", "clean", "sanitize",
1868
+ "compose", "decompose", "assemble", "disassemble", "construct", "deconstruct",
1823
1869
  ];
1824
1870
 
1825
1871
  const startsWithVerbHandler = (name) => verbPrefixes.some((verb) => name.startsWith(verb));
@@ -3146,8 +3192,10 @@ const multilineIfConditions = {
3146
3192
  if (isCorrectionNeeded) {
3147
3193
  context.report({
3148
3194
  fix: (fixer) => {
3149
- const indent = " ".repeat(node.loc.start.column + 4);
3150
- const parenIndent = " ".repeat(node.loc.start.column);
3195
+ // Get the indentation of the if statement line
3196
+ const lineText = sourceCode.lines[node.loc.start.line - 1];
3197
+ const parenIndent = lineText.match(/^\s*/)[0];
3198
+ const indent = parenIndent + " ";
3151
3199
 
3152
3200
  const buildMultilineHandler = (n) => {
3153
3201
  if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
@@ -3321,6 +3369,251 @@ const multilineIfConditions = {
3321
3369
  },
3322
3370
  };
3323
3371
 
3372
+ /**
3373
+ * ───────────────────────────────────────────────────────────────
3374
+ * Rule: Ternary Condition Multiline
3375
+ * ───────────────────────────────────────────────────────────────
3376
+ *
3377
+ * Description:
3378
+ * When a ternary condition has multiple operands (>3), format
3379
+ * each operand on its own line for readability.
3380
+ *
3381
+ * ✓ Good:
3382
+ * const x = a && b && c ? "yes" : "no";
3383
+ *
3384
+ * const x =
3385
+ * variant === "ghost"
3386
+ * || variant === "ghost-danger"
3387
+ * || variant === "muted"
3388
+ * || variant === "primary"
3389
+ * ? "value1"
3390
+ * : "value2";
3391
+ *
3392
+ * ✗ Bad:
3393
+ * const x = variant === "ghost" || variant === "ghost-danger" || variant === "muted" || variant === "primary" ? "value1" : "value2";
3394
+ */
3395
+ const ternaryConditionMultiline = {
3396
+ create(context) {
3397
+ const sourceCode = context.sourceCode || context.getSourceCode();
3398
+ const options = context.options[0] || {};
3399
+ const maxOperands = options.maxOperands ?? 3;
3400
+
3401
+ // Check if node is wrapped in parentheses
3402
+ const isParenthesizedHandler = (node) => {
3403
+ const tokenBefore = sourceCode.getTokenBefore(node);
3404
+ const tokenAfter = sourceCode.getTokenAfter(node);
3405
+
3406
+ if (!tokenBefore || !tokenAfter) return false;
3407
+
3408
+ return tokenBefore.value === "(" && tokenAfter.value === ")";
3409
+ };
3410
+
3411
+ // Get source text including any surrounding parentheses
3412
+ const getSourceTextWithGroupsHandler = (node) => {
3413
+ let start = node.range[0];
3414
+ let end = node.range[1];
3415
+ let left = sourceCode.getTokenBefore(node);
3416
+ let right = sourceCode.getTokenAfter(node);
3417
+
3418
+ while (left && left.value === "(" && right && right.value === ")") {
3419
+ start = left.range[0];
3420
+ end = right.range[1];
3421
+ left = sourceCode.getTokenBefore(left);
3422
+ right = sourceCode.getTokenAfter(right);
3423
+ }
3424
+
3425
+ return sourceCode.text.slice(start, end);
3426
+ };
3427
+
3428
+ // Collect all operands from a logical expression
3429
+ const collectOperandsHandler = (node) => {
3430
+ const operands = [];
3431
+
3432
+ const collectHelperHandler = (n) => {
3433
+ if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
3434
+ collectHelperHandler(n.left);
3435
+ collectHelperHandler(n.right);
3436
+ } else {
3437
+ operands.push(n);
3438
+ }
3439
+ };
3440
+
3441
+ collectHelperHandler(node);
3442
+
3443
+ return operands;
3444
+ };
3445
+
3446
+ // Check if a BinaryExpression is split across lines
3447
+ const isBinaryExpressionSplitHandler = (n) => {
3448
+ if (n.type !== "BinaryExpression") return false;
3449
+
3450
+ const { left, right } = n;
3451
+
3452
+ if (left.loc.end.line !== right.loc.start.line) return true;
3453
+ if (isBinaryExpressionSplitHandler(left)) return true;
3454
+ if (isBinaryExpressionSplitHandler(right)) return true;
3455
+
3456
+ return false;
3457
+ };
3458
+
3459
+ // Collapse a BinaryExpression to single line
3460
+ const buildBinaryExpressionSingleLineHandler = (n) => {
3461
+ if (n.type === "BinaryExpression") {
3462
+ const leftText = buildBinaryExpressionSingleLineHandler(n.left);
3463
+ const rightText = buildBinaryExpressionSingleLineHandler(n.right);
3464
+
3465
+ return `${leftText} ${n.operator} ${rightText}`;
3466
+ }
3467
+
3468
+ return sourceCode.getText(n);
3469
+ };
3470
+
3471
+ // Helper to check if any operator is at end of line (wrong position)
3472
+ const hasOperatorAtEndOfLineHandler = (n) => {
3473
+ if (n.type !== "LogicalExpression") return false;
3474
+
3475
+ const operatorToken = sourceCode.getTokenAfter(
3476
+ n.left,
3477
+ (t) => t.value === "||" || t.value === "&&",
3478
+ );
3479
+
3480
+ if (operatorToken) {
3481
+ const tokenAfterOperator = sourceCode.getTokenAfter(operatorToken);
3482
+
3483
+ if (tokenAfterOperator && operatorToken.loc.end.line < tokenAfterOperator.loc.start.line) {
3484
+ return true;
3485
+ }
3486
+ }
3487
+
3488
+ if (hasOperatorAtEndOfLineHandler(n.left)) return true;
3489
+ if (hasOperatorAtEndOfLineHandler(n.right)) return true;
3490
+
3491
+ return false;
3492
+ };
3493
+
3494
+ return {
3495
+ ConditionalExpression(node) {
3496
+ const { test } = node;
3497
+
3498
+ // Only handle ternaries with logical expression conditions
3499
+ if (test.type !== "LogicalExpression") return;
3500
+
3501
+ const operands = collectOperandsHandler(test);
3502
+ const testStartLine = test.loc.start.line;
3503
+ const testEndLine = test.loc.end.line;
3504
+ const isMultiLine = testStartLine !== testEndLine;
3505
+
3506
+ // ≤maxOperands operands: keep on single line
3507
+ if (operands.length <= maxOperands) {
3508
+ const firstOperandStartLine = operands[0].loc.start.line;
3509
+ const allOperandsStartOnSameLine = operands.every(
3510
+ (op) => op.loc.start.line === firstOperandStartLine,
3511
+ );
3512
+
3513
+ const hasSplitBinaryExpression = operands.some(
3514
+ (op) => isBinaryExpressionSplitHandler(op),
3515
+ );
3516
+
3517
+ if (!allOperandsStartOnSameLine || hasSplitBinaryExpression) {
3518
+ context.report({
3519
+ fix: (fixer) => {
3520
+ const buildSameLineHandler = (n) => {
3521
+ if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
3522
+ const leftText = buildSameLineHandler(n.left);
3523
+ const rightText = buildSameLineHandler(n.right);
3524
+
3525
+ return `${leftText} ${n.operator} ${rightText}`;
3526
+ }
3527
+
3528
+ if (n.type === "BinaryExpression" && isBinaryExpressionSplitHandler(n)) {
3529
+ return buildBinaryExpressionSingleLineHandler(n);
3530
+ }
3531
+
3532
+ return getSourceTextWithGroupsHandler(n);
3533
+ };
3534
+
3535
+ return fixer.replaceText(test, buildSameLineHandler(test));
3536
+ },
3537
+ message: `Ternary conditions with ≤${maxOperands} operands should be single line`,
3538
+ node: test,
3539
+ });
3540
+ }
3541
+
3542
+ return;
3543
+ }
3544
+
3545
+ // More than maxOperands: each on its own line
3546
+ let isCorrectionNeeded = !isMultiLine;
3547
+
3548
+ if (isMultiLine) {
3549
+ for (let i = 0; i < operands.length - 1; i += 1) {
3550
+ if (operands[i].loc.end.line === operands[i + 1].loc.start.line) {
3551
+ isCorrectionNeeded = true;
3552
+ break;
3553
+ }
3554
+ }
3555
+
3556
+ // Check if any operator is at end of line (should be at beginning)
3557
+ if (!isCorrectionNeeded && hasOperatorAtEndOfLineHandler(test)) {
3558
+ isCorrectionNeeded = true;
3559
+ }
3560
+ }
3561
+
3562
+ if (isCorrectionNeeded) {
3563
+ context.report({
3564
+ fix: (fixer) => {
3565
+ // Get the indentation based on where the ternary starts
3566
+ const lineText = sourceCode.lines[node.loc.start.line - 1];
3567
+ const baseIndent = lineText.match(/^\s*/)[0];
3568
+ const conditionIndent = baseIndent + " ";
3569
+ const branchIndent = baseIndent + " ";
3570
+
3571
+ const buildMultilineHandler = (n) => {
3572
+ if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
3573
+ const leftText = buildMultilineHandler(n.left);
3574
+ const rightText = buildMultilineHandler(n.right);
3575
+
3576
+ return `${leftText}\n${conditionIndent}${n.operator} ${rightText}`;
3577
+ }
3578
+
3579
+ return getSourceTextWithGroupsHandler(n);
3580
+ };
3581
+
3582
+ const consequentText = sourceCode.getText(node.consequent);
3583
+ const alternateText = sourceCode.getText(node.alternate);
3584
+
3585
+ const newText = `\n${conditionIndent}${buildMultilineHandler(test)}\n${branchIndent}? ${consequentText}\n${branchIndent}: ${alternateText}`;
3586
+
3587
+ return fixer.replaceText(node, newText);
3588
+ },
3589
+ message: `Ternary conditions with more than ${maxOperands} operands should be multiline, with each operand on its own line`,
3590
+ node: test,
3591
+ });
3592
+ }
3593
+ },
3594
+ };
3595
+ },
3596
+ meta: {
3597
+ docs: { description: "Enforce multiline formatting for ternary expressions with complex conditions" },
3598
+ fixable: "code",
3599
+ schema: [
3600
+ {
3601
+ additionalProperties: false,
3602
+ properties: {
3603
+ maxOperands: {
3604
+ default: 3,
3605
+ description: "Maximum operands to keep on single line (default: 3)",
3606
+ minimum: 1,
3607
+ type: "integer",
3608
+ },
3609
+ },
3610
+ type: "object",
3611
+ },
3612
+ ],
3613
+ type: "layout",
3614
+ },
3615
+ };
3616
+
3324
3617
  /**
3325
3618
  * ───────────────────────────────────────────────────────────────
3326
3619
  * Rule: Absolute Imports Only
@@ -4758,6 +5051,57 @@ const indexExportStyle = {
4758
5051
  },
4759
5052
  };
4760
5053
 
5054
+ /**
5055
+ * ───────────────────────────────────────────────────────────────
5056
+ * Rule: Index Exports Only
5057
+ * ───────────────────────────────────────────────────────────────
5058
+ *
5059
+ * Description:
5060
+ * Index files (index.ts, index.tsx, index.js, index.jsx) should
5061
+ * only contain imports and exports, not type or interface definitions.
5062
+ * Types should be moved to a separate file (e.g., types.ts).
5063
+ *
5064
+ * ✓ Good:
5065
+ * // index.ts
5066
+ * export { Button } from "./Button";
5067
+ * export type { ButtonProps } from "./types";
5068
+ *
5069
+ * ✗ Bad:
5070
+ * // index.ts
5071
+ * export type ButtonVariant = "primary" | "secondary";
5072
+ * export interface ButtonProps { ... }
5073
+ */
5074
+ const indexExportsOnly = {
5075
+ create(context) {
5076
+ const filename = context.filename || context.getFilename();
5077
+ const normalizedFilename = filename.replace(/\\/g, "/");
5078
+ const isIndexFile = /\/index\.(js|jsx|ts|tsx)$/.test(normalizedFilename)
5079
+ || /^index\.(js|jsx|ts|tsx)$/.test(normalizedFilename);
5080
+
5081
+ if (!isIndexFile) return {};
5082
+
5083
+ return {
5084
+ TSInterfaceDeclaration(node) {
5085
+ context.report({
5086
+ message: "Interface definitions should not be in index files. Move to a types file.",
5087
+ node,
5088
+ });
5089
+ },
5090
+ TSTypeAliasDeclaration(node) {
5091
+ context.report({
5092
+ message: "Type definitions should not be in index files. Move to a types file.",
5093
+ node,
5094
+ });
5095
+ },
5096
+ };
5097
+ },
5098
+ meta: {
5099
+ docs: { description: "Index files should only contain imports and exports, not type definitions" },
5100
+ schema: [],
5101
+ type: "suggestion",
5102
+ },
5103
+ };
5104
+
4761
5105
  /**
4762
5106
  * ───────────────────────────────────────────────────────────────
4763
5107
  * Rule: JSX Children On New Line
@@ -6319,7 +6663,19 @@ const classNameMultiline = {
6319
6663
  if (current.type === "JSXAttribute"
6320
6664
  || current.type === "VariableDeclarator"
6321
6665
  || current.type === "Property") {
6322
- return getLineIndent(current);
6666
+ const lineIndent = getLineIndent(current);
6667
+ const lineText = sourceCode.lines[current.loc.start.line - 1];
6668
+
6669
+ // Check if there's content before the node on this line
6670
+ const contentBefore = lineText.slice(0, current.loc.start.column).trim();
6671
+
6672
+ if (contentBefore) {
6673
+ // Attribute is inline (e.g., <Component className=...>)
6674
+ // Use column position as indent for proper alignment
6675
+ return " ".repeat(current.loc.start.column);
6676
+ }
6677
+
6678
+ return lineIndent;
6323
6679
  }
6324
6680
 
6325
6681
  current = current.parent;
@@ -11992,6 +12348,31 @@ const functionObjectDestructure = {
11992
12348
  const accessedProps = [...new Set(accesses.map((a) => a.property))];
11993
12349
 
11994
12350
  context.report({
12351
+ fix: (fixer) => {
12352
+ const fixes = [];
12353
+
12354
+ // Get the first statement in the block body to insert before it
12355
+ const firstStatement = body.body[0];
12356
+
12357
+ if (firstStatement) {
12358
+ // Get indentation from the first statement
12359
+ const firstStatementLine = sourceCode.lines[firstStatement.loc.start.line - 1];
12360
+ const indent = firstStatementLine.match(/^\s*/)[0];
12361
+
12362
+ // Create the destructuring statement
12363
+ const destructureStatement = `const { ${accessedProps.join(", ")} } = ${paramName};\n${indent}`;
12364
+
12365
+ // Insert at the beginning of the first statement
12366
+ fixes.push(fixer.insertTextBefore(firstStatement, destructureStatement));
12367
+ }
12368
+
12369
+ // Replace all param.prop accesses with just prop
12370
+ accesses.forEach((access) => {
12371
+ fixes.push(fixer.replaceText(access.node, access.property));
12372
+ });
12373
+
12374
+ return fixes;
12375
+ },
11995
12376
  message: `Parameter "${paramName}" is accessed via dot notation. Destructure it at the top of the function body: "const { ${accessedProps.join(", ")} } = ${paramName};"`,
11996
12377
  node: accesses[0].node,
11997
12378
  });
@@ -12017,6 +12398,31 @@ const functionObjectDestructure = {
12017
12398
  const accessedProps = [...new Set(accesses.map((a) => a.property))];
12018
12399
 
12019
12400
  context.report({
12401
+ fix: (fixer) => {
12402
+ const fixes = [];
12403
+
12404
+ // Get the first statement in the block body to insert before it
12405
+ const firstStatement = body.body[0];
12406
+
12407
+ if (firstStatement) {
12408
+ // Get indentation from the first statement
12409
+ const firstStatementLine = sourceCode.lines[firstStatement.loc.start.line - 1];
12410
+ const indent = firstStatementLine.match(/^\s*/)[0];
12411
+
12412
+ // Create the destructuring statement
12413
+ const destructureStatement = `const { ${accessedProps.join(", ")} } = ${propName};\n\n${indent}`;
12414
+
12415
+ // Insert at the beginning of the first statement
12416
+ fixes.push(fixer.insertTextBefore(firstStatement, destructureStatement));
12417
+ }
12418
+
12419
+ // Replace all prop.subprop accesses with just subprop
12420
+ accesses.forEach((access) => {
12421
+ fixes.push(fixer.replaceText(access.node, access.property));
12422
+ });
12423
+
12424
+ return fixes;
12425
+ },
12020
12426
  message: `Prop "${propName}" is accessed via dot notation. Destructure it at the top of the component: "const { ${accessedProps.join(", ")} } = ${propName};"`,
12021
12427
  node: accesses[0].node,
12022
12428
  });
@@ -12115,6 +12521,44 @@ const componentPropsDestructure = {
12115
12521
  return false;
12116
12522
  };
12117
12523
 
12524
+ // Find all property accesses on a parameter in the function body
12525
+ const findPropAccessesHandler = (body, paramName) => {
12526
+ const accesses = [];
12527
+
12528
+ const visitNode = (n) => {
12529
+ if (!n || typeof n !== "object") return;
12530
+
12531
+ // Check for member expression like props.name
12532
+ if (n.type === "MemberExpression" && !n.computed) {
12533
+ if (n.object.type === "Identifier" && n.object.name === paramName) {
12534
+ const propName = n.property.name;
12535
+
12536
+ accesses.push({
12537
+ node: n,
12538
+ property: propName,
12539
+ });
12540
+ }
12541
+ }
12542
+
12543
+ // Recurse into child nodes
12544
+ for (const key of Object.keys(n)) {
12545
+ if (key === "parent") continue;
12546
+
12547
+ const child = n[key];
12548
+
12549
+ if (Array.isArray(child)) {
12550
+ child.forEach(visitNode);
12551
+ } else if (child && typeof child === "object" && child.type) {
12552
+ visitNode(child);
12553
+ }
12554
+ }
12555
+ };
12556
+
12557
+ visitNode(body);
12558
+
12559
+ return accesses;
12560
+ };
12561
+
12118
12562
  const checkComponentPropsHandler = (node) => {
12119
12563
  if (!isReactComponentHandler(node)) return;
12120
12564
 
@@ -12127,12 +12571,53 @@ const componentPropsDestructure = {
12127
12571
  const firstParam = params[0];
12128
12572
 
12129
12573
  if (firstParam.type === "Identifier") {
12130
- context.report({
12131
- message: `Component props should be destructured. Use "({ ...props })" instead of "${firstParam.name}"`,
12132
- node: firstParam,
12133
- });
12134
- }
12135
- };
12574
+ const paramName = firstParam.name;
12575
+ const accesses = findPropAccessesHandler(node.body, paramName);
12576
+ const accessedProps = [...new Set(accesses.map((a) => a.property))];
12577
+
12578
+ // Check if param is used directly (not just via dot notation)
12579
+ const allRefs = [];
12580
+ const countRefs = (n) => {
12581
+ if (!n || typeof n !== "object") return;
12582
+
12583
+ if (n.type === "Identifier" && n.name === paramName) allRefs.push(n);
12584
+
12585
+ for (const key of Object.keys(n)) {
12586
+ if (key === "parent") continue;
12587
+
12588
+ const child = n[key];
12589
+
12590
+ if (Array.isArray(child)) child.forEach(countRefs);
12591
+ else if (child && typeof child === "object" && child.type) countRefs(child);
12592
+ }
12593
+ };
12594
+
12595
+ countRefs(node.body);
12596
+
12597
+ // Can only auto-fix if all references are covered by dot notation accesses
12598
+ const canAutoFix = accessedProps.length > 0 && allRefs.length === accesses.length;
12599
+
12600
+ context.report({
12601
+ fix: canAutoFix
12602
+ ? (fixer) => {
12603
+ const fixes = [];
12604
+
12605
+ // Replace param with destructured pattern
12606
+ fixes.push(fixer.replaceText(firstParam, `{ ${accessedProps.join(", ")} }`));
12607
+
12608
+ // Replace all props.x with just x
12609
+ accesses.forEach((access) => {
12610
+ fixes.push(fixer.replaceText(access.node, access.property));
12611
+ });
12612
+
12613
+ return fixes;
12614
+ }
12615
+ : undefined,
12616
+ message: `Component props should be destructured. Use "({ ...props })" instead of "${firstParam.name}"`,
12617
+ node: firstParam,
12618
+ });
12619
+ }
12620
+ };
12136
12621
 
12137
12622
  return {
12138
12623
  ArrowFunctionExpression: checkComponentPropsHandler,
@@ -12142,6 +12627,7 @@ const componentPropsDestructure = {
12142
12627
  },
12143
12628
  meta: {
12144
12629
  docs: { description: "Enforce that React component props must be destructured in the function parameter" },
12630
+ fixable: "code",
12145
12631
  schema: [],
12146
12632
  type: "suggestion",
12147
12633
  },
@@ -12592,6 +13078,146 @@ const componentPropsInlineType = {
12592
13078
  },
12593
13079
  };
12594
13080
 
13081
+ /**
13082
+ * ───────────────────────────────────────────────────────────────
13083
+ * Rule: No Inline Type Definitions
13084
+ * ───────────────────────────────────────────────────────────────
13085
+ *
13086
+ * Description:
13087
+ * Function parameters with inline union types should be extracted
13088
+ * to a separate type file. This rule reports union types with
13089
+ * more than a threshold number of members or exceeding a length limit.
13090
+ *
13091
+ * ✓ Good:
13092
+ * // In types.ts:
13093
+ * export type ButtonVariant = "primary" | "muted" | "danger";
13094
+ *
13095
+ * // In Button.tsx:
13096
+ * import { ButtonVariant } from "./types";
13097
+ * export const Button = ({ variant }: { variant?: ButtonVariant }) => ...
13098
+ *
13099
+ * ✗ Bad:
13100
+ * export const Button = ({ variant }: { variant?: "primary" | "muted" | "danger" }) => ...
13101
+ */
13102
+ const noInlineTypeDefinitions = {
13103
+ create(context) {
13104
+ const sourceCode = context.sourceCode || context.getSourceCode();
13105
+ const options = context.options[0] || {};
13106
+ const maxUnionMembers = options.maxUnionMembers ?? 2;
13107
+ const maxLength = options.maxLength ?? 50;
13108
+
13109
+ // Count union type members
13110
+ const countUnionMembersHandler = (node) => {
13111
+ if (node.type !== "TSUnionType") return 1;
13112
+
13113
+ let count = 0;
13114
+
13115
+ for (const type of node.types) {
13116
+ count += countUnionMembersHandler(type);
13117
+ }
13118
+
13119
+ return count;
13120
+ };
13121
+
13122
+ // Check if a type annotation contains a complex inline union
13123
+ const checkTypeAnnotationHandler = (typeNode, paramName) => {
13124
+ if (!typeNode) return;
13125
+
13126
+ // Handle union types directly
13127
+ if (typeNode.type === "TSUnionType") {
13128
+ const memberCount = countUnionMembersHandler(typeNode);
13129
+ const typeText = sourceCode.getText(typeNode);
13130
+
13131
+ if (memberCount > maxUnionMembers || typeText.length > maxLength) {
13132
+ context.report({
13133
+ message: `Inline union type with ${memberCount} members is too complex. Extract to a named type in a types file.`,
13134
+ node: typeNode,
13135
+ });
13136
+ }
13137
+
13138
+ return;
13139
+ }
13140
+
13141
+ // Handle object types with union properties
13142
+ if (typeNode.type === "TSTypeLiteral") {
13143
+ for (const member of typeNode.members) {
13144
+ if (member.type === "TSPropertySignature" && member.typeAnnotation) {
13145
+ const propType = member.typeAnnotation.typeAnnotation;
13146
+ const propName = member.key && member.key.name ? member.key.name : "unknown";
13147
+
13148
+ if (propType && propType.type === "TSUnionType") {
13149
+ const memberCount = countUnionMembersHandler(propType);
13150
+ const typeText = sourceCode.getText(propType);
13151
+
13152
+ if (memberCount > maxUnionMembers || typeText.length > maxLength) {
13153
+ context.report({
13154
+ message: `Property "${propName}" has inline union type with ${memberCount} members. Extract to a named type in a types file.`,
13155
+ node: propType,
13156
+ });
13157
+ }
13158
+ }
13159
+ }
13160
+ }
13161
+ }
13162
+ };
13163
+
13164
+ return {
13165
+ // Check function parameters
13166
+ ArrowFunctionExpression(node) {
13167
+ for (const param of node.params) {
13168
+ if (param.typeAnnotation && param.typeAnnotation.typeAnnotation) {
13169
+ const paramName = param.type === "Identifier" ? param.name : "param";
13170
+
13171
+ checkTypeAnnotationHandler(param.typeAnnotation.typeAnnotation, paramName);
13172
+ }
13173
+ }
13174
+ },
13175
+ FunctionDeclaration(node) {
13176
+ for (const param of node.params) {
13177
+ if (param.typeAnnotation && param.typeAnnotation.typeAnnotation) {
13178
+ const paramName = param.type === "Identifier" ? param.name : "param";
13179
+
13180
+ checkTypeAnnotationHandler(param.typeAnnotation.typeAnnotation, paramName);
13181
+ }
13182
+ }
13183
+ },
13184
+ FunctionExpression(node) {
13185
+ for (const param of node.params) {
13186
+ if (param.typeAnnotation && param.typeAnnotation.typeAnnotation) {
13187
+ const paramName = param.type === "Identifier" ? param.name : "param";
13188
+
13189
+ checkTypeAnnotationHandler(param.typeAnnotation.typeAnnotation, paramName);
13190
+ }
13191
+ }
13192
+ },
13193
+ };
13194
+ },
13195
+ meta: {
13196
+ docs: { description: "Enforce extracting inline union types to named types in type files" },
13197
+ schema: [
13198
+ {
13199
+ additionalProperties: false,
13200
+ properties: {
13201
+ maxLength: {
13202
+ default: 50,
13203
+ description: "Maximum character length for inline union types (default: 50)",
13204
+ minimum: 1,
13205
+ type: "integer",
13206
+ },
13207
+ maxUnionMembers: {
13208
+ default: 2,
13209
+ description: "Maximum union members to keep inline (default: 2)",
13210
+ minimum: 1,
13211
+ type: "integer",
13212
+ },
13213
+ },
13214
+ type: "object",
13215
+ },
13216
+ ],
13217
+ type: "suggestion",
13218
+ },
13219
+ };
13220
+
12595
13221
  /*
12596
13222
  * type-format
12597
13223
  *
@@ -13945,25 +14571,162 @@ const reactCodeOrder = {
13945
14571
  || s.type === "ExpressionStatement"
13946
14572
  || s.type === "ReturnStatement";
13947
14573
 
14574
+ // Get declared variable names from a statement
14575
+ const getDeclaredNamesHandler = (stmt) => {
14576
+ const names = new Set();
14577
+
14578
+ if (stmt.type === "VariableDeclaration") {
14579
+ for (const decl of stmt.declarations) {
14580
+ if (decl.id.type === "Identifier") {
14581
+ names.add(decl.id.name);
14582
+ } else if (decl.id.type === "ObjectPattern") {
14583
+ for (const prop of decl.id.properties) {
14584
+ if (prop.type === "Property" && prop.value.type === "Identifier") {
14585
+ names.add(prop.value.name);
14586
+ } else if (prop.type === "RestElement" && prop.argument.type === "Identifier") {
14587
+ names.add(prop.argument.name);
14588
+ }
14589
+ }
14590
+ } else if (decl.id.type === "ArrayPattern") {
14591
+ for (const element of decl.id.elements) {
14592
+ if (element && element.type === "Identifier") {
14593
+ names.add(element.name);
14594
+ }
14595
+ }
14596
+ }
14597
+ }
14598
+ } else if (stmt.type === "FunctionDeclaration" && stmt.id) {
14599
+ names.add(stmt.id.name);
14600
+ }
14601
+
14602
+ return names;
14603
+ };
14604
+
14605
+ // Get referenced variable names from a node (recursively)
14606
+ const getReferencedNamesHandler = (node, refs = new Set()) => {
14607
+ if (!node) return refs;
14608
+
14609
+ if (node.type === "Identifier") {
14610
+ refs.add(node.name);
14611
+ } else if (node.type === "MemberExpression") {
14612
+ getReferencedNamesHandler(node.object, refs);
14613
+ } else if (node.type === "CallExpression") {
14614
+ getReferencedNamesHandler(node.callee, refs);
14615
+ node.arguments.forEach((arg) => getReferencedNamesHandler(arg, refs));
14616
+ } else if (node.type === "BinaryExpression" || node.type === "LogicalExpression") {
14617
+ getReferencedNamesHandler(node.left, refs);
14618
+ getReferencedNamesHandler(node.right, refs);
14619
+ } else if (node.type === "ConditionalExpression") {
14620
+ getReferencedNamesHandler(node.test, refs);
14621
+ getReferencedNamesHandler(node.consequent, refs);
14622
+ getReferencedNamesHandler(node.alternate, refs);
14623
+ } else if (node.type === "UnaryExpression") {
14624
+ getReferencedNamesHandler(node.argument, refs);
14625
+ } else if (node.type === "ArrayExpression") {
14626
+ node.elements.forEach((el) => getReferencedNamesHandler(el, refs));
14627
+ } else if (node.type === "ObjectExpression") {
14628
+ node.properties.forEach((prop) => {
14629
+ if (prop.type === "Property") {
14630
+ getReferencedNamesHandler(prop.value, refs);
14631
+ }
14632
+ });
14633
+ } else if (node.type === "TemplateLiteral") {
14634
+ node.expressions.forEach((expr) => getReferencedNamesHandler(expr, refs));
14635
+ } else if (node.type === "ChainExpression") {
14636
+ getReferencedNamesHandler(node.expression, refs);
14637
+ } else if (node.type === "TSAsExpression" || node.type === "TSNonNullExpression") {
14638
+ getReferencedNamesHandler(node.expression, refs);
14639
+ }
14640
+
14641
+ return refs;
14642
+ };
14643
+
14644
+ // Get dependencies for a statement (variables it uses in initialization)
14645
+ const getStatementDependenciesHandler = (stmt) => {
14646
+ const deps = new Set();
14647
+
14648
+ if (stmt.type === "VariableDeclaration") {
14649
+ for (const decl of stmt.declarations) {
14650
+ if (decl.init) {
14651
+ getReferencedNamesHandler(decl.init, deps);
14652
+ }
14653
+ }
14654
+ }
14655
+
14656
+ return deps;
14657
+ };
14658
+
13948
14659
  // Filter to only categorizable statements for order checking
13949
14660
  const categorizableStatements = statements.filter(isCategorizableStatement);
13950
14661
 
13951
14662
  if (categorizableStatements.length < 2) return;
13952
14663
 
13953
- // Check order violations using only categorizable statements
14664
+ // Build dependency information for all statements
14665
+ const stmtInfo = new Map();
14666
+ const declaredNames = new Map(); // Map from variable name to statement index
14667
+
14668
+ // First pass: build declaredNames map (so we can look up where each variable is declared)
14669
+ for (let i = 0; i < statements.length; i++) {
14670
+ const stmt = statements[i];
14671
+ const declared = getDeclaredNamesHandler(stmt);
14672
+
14673
+ for (const name of declared) {
14674
+ declaredNames.set(name, i);
14675
+ }
14676
+ }
14677
+
14678
+ // Second pass: build full statement info including dependencies
14679
+ for (let i = 0; i < statements.length; i++) {
14680
+ const stmt = statements[i];
14681
+ const declared = getDeclaredNamesHandler(stmt);
14682
+ const dependencies = getStatementDependenciesHandler(stmt);
14683
+ const category = isCategorizableStatement(stmt)
14684
+ ? getStatementCategoryHandler(stmt, propNames)
14685
+ : ORDER.UNKNOWN;
14686
+
14687
+ stmtInfo.set(i, {
14688
+ category,
14689
+ declared,
14690
+ dependencies,
14691
+ index: i,
14692
+ statement: stmt,
14693
+ });
14694
+ }
14695
+
14696
+ // Build dependency graph first
14697
+ const dependsOn = new Map(); // stmtIndex -> Set of stmtIndices it depends on
14698
+
14699
+ for (let i = 0; i < statements.length; i++) {
14700
+ const info = stmtInfo.get(i);
14701
+ const deps = new Set();
14702
+
14703
+ for (const depName of info.dependencies) {
14704
+ const depIndex = declaredNames.get(depName);
14705
+
14706
+ if (depIndex !== undefined && depIndex !== i) {
14707
+ deps.add(depIndex);
14708
+ }
14709
+ }
14710
+
14711
+ dependsOn.set(i, deps);
14712
+ }
14713
+
14714
+ // Check for violations: category order OR dependency order
13954
14715
  let hasOrderViolation = false;
14716
+ let hasDependencyViolation = false;
13955
14717
  let lastCategory = 0;
13956
14718
  let violatingStatement = null;
13957
14719
  let violatingCategory = null;
13958
14720
  let previousCategory = null;
14721
+ let dependencyViolationStmt = null;
14722
+ let dependencyViolationVar = null;
13959
14723
 
14724
+ // Check category violations
13960
14725
  for (const statement of categorizableStatements) {
13961
14726
  const category = getStatementCategoryHandler(statement, propNames);
13962
14727
 
13963
- // Skip unknown statements for order checking
13964
14728
  if (category === ORDER.UNKNOWN) continue;
13965
14729
 
13966
- // Check if current category comes before the last one
13967
14730
  if (category < lastCategory && !hasOrderViolation) {
13968
14731
  hasOrderViolation = true;
13969
14732
  violatingStatement = statement;
@@ -13974,114 +14737,407 @@ const reactCodeOrder = {
13974
14737
  lastCategory = category;
13975
14738
  }
13976
14739
 
13977
- if (!hasOrderViolation) return;
14740
+ // Check dependency violations (using variable before declaration)
14741
+ for (let i = 0; i < statements.length; i++) {
14742
+ const deps = dependsOn.get(i) || new Set();
13978
14743
 
13979
- // For auto-fix, process ALL statements and assign sort keys
13980
- // Non-categorizable statements (if, for, while, etc.) get the category of the NEXT categorizable statement
13981
- // This keeps them positioned just before whatever comes after them
13982
- const allStatementsWithSortKeys = [];
14744
+ for (const depIndex of deps) {
14745
+ if (depIndex > i) {
14746
+ // This statement uses a variable declared later
14747
+ hasDependencyViolation = true;
14748
+ dependencyViolationStmt = statements[i];
13983
14749
 
13984
- // First pass: assign categories to categorizable statements
13985
- const categoryMap = new Map();
14750
+ // Find the variable name
14751
+ const depInfo = stmtInfo.get(depIndex);
13986
14752
 
13987
- for (const stmt of statements) {
13988
- if (isCategorizableStatement(stmt)) {
13989
- categoryMap.set(stmt, getStatementCategoryHandler(stmt, propNames));
14753
+ for (const name of depInfo.declared) {
14754
+ const currentDeps = stmtInfo.get(i).dependencies;
14755
+
14756
+ if (currentDeps.has(name)) {
14757
+ dependencyViolationVar = name;
14758
+
14759
+ break;
14760
+ }
14761
+ }
14762
+
14763
+ break;
14764
+ }
13990
14765
  }
14766
+
14767
+ if (hasDependencyViolation) break;
13991
14768
  }
13992
14769
 
13993
- // Second pass: assign sort keys to all statements
13994
- // Non-categorizable statements get the category of the next categorizable statement
13995
- for (let i = 0; i < statements.length; i++) {
13996
- const stmt = statements[i];
13997
- let sortKey;
14770
+ if (!hasOrderViolation && !hasDependencyViolation) return;
13998
14771
 
13999
- if (isCategorizableStatement(stmt)) {
14000
- sortKey = categoryMap.get(stmt);
14001
- } else {
14002
- // Find the next categorizable statement's category
14003
- sortKey = ORDER.RETURN; // Default to RETURN if none found
14772
+ // Dependency-aware topological sort
14773
+ // Sort by category, but ensure dependencies come before dependents
14774
+ const sortWithDependenciesHandler = () => {
14775
+ const result = [];
14776
+ const visited = new Set();
14777
+ const visiting = new Set(); // For cycle detection
14004
14778
 
14005
- for (let j = i + 1; j < statements.length; j++) {
14006
- if (isCategorizableStatement(statements[j])) {
14007
- sortKey = categoryMap.get(statements[j]);
14779
+ // DFS-based topological sort
14780
+ const visitHandler = (index) => {
14781
+ if (visited.has(index)) return;
14782
+ if (visiting.has(index)) return; // Cycle detected, skip
14008
14783
 
14009
- break;
14784
+ visiting.add(index);
14785
+
14786
+ // Visit dependencies first
14787
+ const deps = dependsOn.get(index) || new Set();
14788
+
14789
+ for (const depIndex of deps) {
14790
+ visitHandler(depIndex);
14791
+ }
14792
+
14793
+ visiting.delete(index);
14794
+ visited.add(index);
14795
+ result.push(index);
14796
+ };
14797
+
14798
+ // Group statements by category first, then sort within each group by dependencies
14799
+ const byCategory = new Map();
14800
+
14801
+ for (let i = 0; i < statements.length; i++) {
14802
+ const info = stmtInfo.get(i);
14803
+ let effectiveCategory = info.category;
14804
+
14805
+ // For non-categorizable statements, use the next categorizable statement's category
14806
+ if (effectiveCategory === ORDER.UNKNOWN) {
14807
+ for (let j = i + 1; j < statements.length; j++) {
14808
+ const nextInfo = stmtInfo.get(j);
14809
+
14810
+ if (nextInfo.category !== ORDER.UNKNOWN) {
14811
+ effectiveCategory = nextInfo.category;
14812
+
14813
+ break;
14814
+ }
14815
+ }
14816
+
14817
+ if (effectiveCategory === ORDER.UNKNOWN) {
14818
+ effectiveCategory = ORDER.RETURN;
14819
+ }
14820
+ }
14821
+
14822
+ // Check if this statement has dependencies from a later category
14823
+ // If so, it needs to come after those dependencies
14824
+ const deps = dependsOn.get(i) || new Set();
14825
+ let maxDepCategory = effectiveCategory;
14826
+
14827
+ for (const depIndex of deps) {
14828
+ const depInfo = stmtInfo.get(depIndex);
14829
+ let depCategory = depInfo.category;
14830
+
14831
+ if (depCategory === ORDER.UNKNOWN) {
14832
+ depCategory = ORDER.DERIVED_STATE; // Assume derived for unknown
14833
+ }
14834
+
14835
+ if (depCategory > maxDepCategory) {
14836
+ maxDepCategory = depCategory;
14010
14837
  }
14011
14838
  }
14839
+
14840
+ // If dependencies are from a later category, this statement must come after them
14841
+ // So we use the max dependency category as the effective category
14842
+ const sortCategory = Math.max(effectiveCategory, maxDepCategory);
14843
+
14844
+ if (!byCategory.has(sortCategory)) {
14845
+ byCategory.set(sortCategory, []);
14846
+ }
14847
+
14848
+ byCategory.get(sortCategory).push({
14849
+ deps,
14850
+ effectiveCategory,
14851
+ index: i,
14852
+ originalCategory: info.category,
14853
+ });
14012
14854
  }
14013
14855
 
14014
- allStatementsWithSortKeys.push({
14015
- index: i,
14016
- sortKey,
14017
- statement: stmt,
14018
- });
14019
- }
14856
+ // Sort categories
14857
+ const sortedCategories = [...byCategory.keys()].sort((a, b) => a - b);
14858
+
14859
+ // Build final sorted list
14860
+ const finalResult = [];
14861
+
14862
+ for (const category of sortedCategories) {
14863
+ const stmtsInCategory = byCategory.get(category);
14864
+
14865
+ // Sort statements within category by dependencies using topological sort
14866
+ const categoryVisited = new Set();
14867
+ const categoryResult = [];
14868
+
14869
+ const visitCategoryHandler = (item) => {
14870
+ if (categoryVisited.has(item.index)) return;
14871
+
14872
+ categoryVisited.add(item.index);
14873
+
14874
+ // Visit dependencies in same category first
14875
+ for (const depIndex of item.deps) {
14876
+ const depItem = stmtsInCategory.find((s) => s.index === depIndex);
14877
+
14878
+ if (depItem && !categoryVisited.has(depIndex)) {
14879
+ visitCategoryHandler(depItem);
14880
+ }
14881
+ }
14882
+
14883
+ categoryResult.push(item.index);
14884
+ };
14885
+
14886
+ // Sort by original index first to maintain stability
14887
+ stmtsInCategory.sort((a, b) => a.index - b.index);
14888
+
14889
+ for (const item of stmtsInCategory) {
14890
+ visitCategoryHandler(item);
14891
+ }
14020
14892
 
14021
- // Sort all statements by sort key (stable sort - maintains relative order within same key)
14022
- const sortedStatements = [...allStatementsWithSortKeys].sort((a, b) => {
14023
- if (a.sortKey !== b.sortKey) {
14024
- return a.sortKey - b.sortKey;
14893
+ finalResult.push(...categoryResult);
14025
14894
  }
14026
14895
 
14027
- // Maintain original order within the same sort key
14028
- return a.index - b.index;
14029
- });
14896
+ return finalResult;
14897
+ };
14898
+
14899
+ const sortedIndices = sortWithDependenciesHandler();
14030
14900
 
14031
14901
  // Check if sorting actually changes the order
14032
- const orderChanged = sortedStatements.some((s, i) => s.index !== i);
14902
+ const orderChanged = sortedIndices.some((idx, i) => idx !== i);
14033
14903
 
14034
14904
  if (!orderChanged) return;
14035
14905
 
14036
14906
  // Build the fix
14037
14907
  const fixHandler = (fixer) => {
14038
- // Get the base indentation from the first statement
14039
14908
  const firstStatementLine = sourceCode.lines[statements[0].loc.start.line - 1];
14040
14909
  const baseIndent = firstStatementLine.match(/^\s*/)[0];
14041
14910
 
14042
- // Build new body content
14043
14911
  let newBodyContent = "";
14044
- let lastSortKey = null;
14912
+ let lastCategory = null;
14045
14913
 
14046
- for (let i = 0; i < sortedStatements.length; i++) {
14047
- const { sortKey, statement } = sortedStatements[i];
14914
+ for (let i = 0; i < sortedIndices.length; i++) {
14915
+ const stmtIndex = sortedIndices[i];
14916
+ const info = stmtInfo.get(stmtIndex);
14917
+ const category = info.category !== ORDER.UNKNOWN ? info.category : null;
14048
14918
 
14049
- // Add blank line between different categories (except UNKNOWN)
14050
- if (lastSortKey !== null && sortKey !== ORDER.UNKNOWN && lastSortKey !== ORDER.UNKNOWN && sortKey !== lastSortKey) {
14919
+ // Add blank line between different categories
14920
+ if (lastCategory !== null && category !== null && category !== lastCategory) {
14051
14921
  newBodyContent += "\n";
14052
14922
  }
14053
14923
 
14054
- // Get the statement text with proper indentation
14055
- const stmtText = sourceCode.getText(statement);
14924
+ const stmtText = sourceCode.getText(info.statement);
14056
14925
 
14057
14926
  newBodyContent += baseIndent + stmtText.trim() + "\n";
14058
14927
 
14059
- lastSortKey = sortKey;
14928
+ if (category !== null) {
14929
+ lastCategory = category;
14930
+ }
14060
14931
  }
14061
14932
 
14062
- // Find the range to replace (all statements)
14063
14933
  const firstStmt = statements[0];
14064
14934
  const lastStmt = statements[statements.length - 1];
14065
14935
 
14066
14936
  return fixer.replaceTextRange([firstStmt.range[0], lastStmt.range[1]], newBodyContent.trimEnd());
14067
14937
  };
14068
14938
 
14069
- context.report({
14070
- data: {
14071
- current: ORDER_NAMES[violatingCategory],
14072
- previous: ORDER_NAMES[previousCategory],
14073
- type: isHook ? "hook" : "component",
14074
- },
14075
- fix: fixHandler,
14076
- message: "\"{{current}}\" should come before \"{{previous}}\" in {{type}}. Order: refs → state → redux → router → context → custom hooks → derived → useMemo → useCallback → handlers → useEffect → return",
14077
- node: violatingStatement,
14078
- });
14939
+ // Report the appropriate violation
14940
+ if (hasDependencyViolation && dependencyViolationStmt) {
14941
+ context.report({
14942
+ data: {
14943
+ type: isHook ? "hook" : "component",
14944
+ varName: dependencyViolationVar || "variable",
14945
+ },
14946
+ fix: fixHandler,
14947
+ message: "\"{{varName}}\" is used before it is declared. Reorder statements so dependencies are declared first in {{type}}",
14948
+ node: dependencyViolationStmt,
14949
+ });
14950
+ } else if (hasOrderViolation && violatingStatement) {
14951
+ context.report({
14952
+ data: {
14953
+ current: ORDER_NAMES[violatingCategory],
14954
+ previous: ORDER_NAMES[previousCategory],
14955
+ type: isHook ? "hook" : "component",
14956
+ },
14957
+ fix: fixHandler,
14958
+ message: "\"{{current}}\" should come before \"{{previous}}\" in {{type}}. Order: refs → state → redux → router → context → custom hooks → derived → useMemo → useCallback → handlers → useEffect → return",
14959
+ node: violatingStatement,
14960
+ });
14961
+ }
14962
+ };
14963
+
14964
+ // Check for module-level constants that should be inside the component
14965
+ const checkModuleLevelConstantsHandler = (node, isHook) => {
14966
+ const sourceCode = context.sourceCode || context.getSourceCode();
14967
+
14968
+ // Get the program (root) node to find module-level declarations
14969
+ let programNode = node;
14970
+
14971
+ while (programNode.parent) {
14972
+ programNode = programNode.parent;
14973
+ }
14974
+
14975
+ if (programNode.type !== "Program") return;
14976
+
14977
+ // Get the component body for insertion
14978
+ const componentBody = node.body;
14979
+
14980
+ if (componentBody.type !== "BlockStatement") return;
14981
+
14982
+ // Get component/hook name
14983
+ const componentName = getFunctionNameHandler(node);
14984
+
14985
+ // Find module-level variable declarations with simple literal values
14986
+ for (const statement of programNode.body) {
14987
+ if (statement.type !== "VariableDeclaration") continue;
14988
+
14989
+ for (const decl of statement.declarations) {
14990
+ if (!decl.init || !decl.id || decl.id.type !== "Identifier") continue;
14991
+
14992
+ const varName = decl.id.name;
14993
+
14994
+ // Only check simple literals (numbers, strings, booleans)
14995
+ const isSimpleLiteral = decl.init.type === "Literal"
14996
+ && (typeof decl.init.value === "number"
14997
+ || typeof decl.init.value === "string"
14998
+ || typeof decl.init.value === "boolean");
14999
+
15000
+ if (!isSimpleLiteral) continue;
15001
+
15002
+ // Skip if it looks like a config constant (SCREAMING_CASE is typically intentional module-level)
15003
+ if (/^[A-Z][A-Z0-9_]*$/.test(varName)) continue;
15004
+
15005
+ // Check if this variable is used inside the current component/hook
15006
+ const componentText = sourceCode.getText(node);
15007
+
15008
+ // Simple check: see if the variable name appears in the component
15009
+ // Use word boundary to avoid false positives
15010
+ const regex = new RegExp(`\\b${varName}\\b`);
15011
+
15012
+ if (regex.test(componentText)) {
15013
+ // Build the fix
15014
+ const fixHandler = (fixer) => {
15015
+ const fixes = [];
15016
+
15017
+ // Get the declaration text
15018
+ const declText = sourceCode.getText(decl);
15019
+ const kind = statement.kind; // const, let, var
15020
+
15021
+ // Remove from module level
15022
+ if (statement.declarations.length === 1) {
15023
+ // Remove the entire statement including newline
15024
+ const nextToken = sourceCode.getTokenAfter(statement);
15025
+ const endPos = nextToken ? statement.range[1] : statement.range[1];
15026
+
15027
+ // Include trailing newline if present
15028
+ const textAfter = sourceCode.text.slice(statement.range[1], statement.range[1] + 2);
15029
+ const removeEnd = textAfter.startsWith("\n") ? statement.range[1] + 1
15030
+ : textAfter.startsWith("\r\n") ? statement.range[1] + 2
15031
+ : statement.range[1];
15032
+
15033
+ fixes.push(fixer.removeRange([statement.range[0], removeEnd]));
15034
+ } else {
15035
+ // Remove just this declarator (and comma)
15036
+ const declIndex = statement.declarations.indexOf(decl);
15037
+ const isLast = declIndex === statement.declarations.length - 1;
15038
+
15039
+ if (isLast) {
15040
+ // Remove comma before and the declarator
15041
+ const prevDecl = statement.declarations[declIndex - 1];
15042
+
15043
+ fixes.push(fixer.removeRange([prevDecl.range[1], decl.range[1]]));
15044
+ } else {
15045
+ // Remove declarator and comma after
15046
+ const nextDecl = statement.declarations[declIndex + 1];
15047
+
15048
+ fixes.push(fixer.removeRange([decl.range[0], nextDecl.range[0]]));
15049
+ }
15050
+ }
15051
+
15052
+ // Find the right position to insert inside the component
15053
+ // Insert after all hooks but before handlers
15054
+ const bodyStatements = componentBody.body;
15055
+
15056
+ // Get the base indentation inside the component
15057
+ let insertIndent = " "; // Default
15058
+
15059
+ if (bodyStatements.length > 0) {
15060
+ const firstStmtLine = sourceCode.lines[bodyStatements[0].loc.start.line - 1];
15061
+
15062
+ insertIndent = firstStmtLine.match(/^\s*/)[0];
15063
+ }
15064
+
15065
+ // Find insertion point: after last hook/derived state, before handlers
15066
+ let insertIndex = 0;
15067
+
15068
+ for (let i = 0; i < bodyStatements.length; i++) {
15069
+ const stmt = bodyStatements[i];
15070
+
15071
+ if (stmt.type === "VariableDeclaration") {
15072
+ const stmtDecl = stmt.declarations[0];
15073
+
15074
+ if (stmtDecl && stmtDecl.init) {
15075
+ // Check if it's a hook call
15076
+ if (stmtDecl.init.type === "CallExpression") {
15077
+ const callee = stmtDecl.init.callee;
15078
+ const hookName = callee.type === "Identifier" ? callee.name
15079
+ : callee.type === "MemberExpression" && callee.property.type === "Identifier"
15080
+ ? callee.property.name
15081
+ : "";
15082
+
15083
+ if (/^use[A-Z]/.test(hookName)) {
15084
+ insertIndex = i + 1;
15085
+
15086
+ continue;
15087
+ }
15088
+ }
15089
+
15090
+ // Check if it's a function (handler)
15091
+ if (stmtDecl.init.type === "ArrowFunctionExpression"
15092
+ || stmtDecl.init.type === "FunctionExpression") {
15093
+ break;
15094
+ }
15095
+
15096
+ // It's derived state, insert after it
15097
+ insertIndex = i + 1;
15098
+ }
15099
+ } else if (stmt.type === "FunctionDeclaration") {
15100
+ // Handler function, stop here
15101
+ break;
15102
+ }
15103
+ }
15104
+
15105
+ // Insert the constant
15106
+ const newLine = `${insertIndent}${kind} ${declText};\n\n`;
15107
+
15108
+ if (insertIndex === 0 && bodyStatements.length > 0) {
15109
+ // Insert at the beginning
15110
+ fixes.push(fixer.insertTextBefore(bodyStatements[0], newLine));
15111
+ } else if (insertIndex < bodyStatements.length) {
15112
+ // Insert before the target statement
15113
+ fixes.push(fixer.insertTextBefore(bodyStatements[insertIndex], newLine));
15114
+ } else if (bodyStatements.length > 0) {
15115
+ // Insert at the end
15116
+ fixes.push(fixer.insertTextAfter(bodyStatements[bodyStatements.length - 1], "\n\n" + insertIndent + kind + " " + declText + ";"));
15117
+ }
15118
+
15119
+ return fixes;
15120
+ };
15121
+
15122
+ context.report({
15123
+ data: {
15124
+ name: varName,
15125
+ type: isHook ? "hook" : "component",
15126
+ },
15127
+ fix: fixHandler,
15128
+ message: "Constant \"{{name}}\" should be declared inside the {{type}} as derived state, not at module level",
15129
+ node: decl.id,
15130
+ });
15131
+ }
15132
+ }
15133
+ }
14079
15134
  };
14080
15135
 
14081
15136
  const checkFunctionHandler = (node) => {
14082
15137
  // Check if it's a React component
14083
15138
  if (isReactComponentHandler(node)) {
14084
15139
  checkCodeOrderHandler(node, false);
15140
+ checkModuleLevelConstantsHandler(node, false);
14085
15141
 
14086
15142
  return;
14087
15143
  }
@@ -14089,6 +15145,7 @@ const reactCodeOrder = {
14089
15145
  // Check if it's a custom hook
14090
15146
  if (isCustomHookHandler(node)) {
14091
15147
  checkCodeOrderHandler(node, true);
15148
+ checkModuleLevelConstantsHandler(node, true);
14092
15149
  }
14093
15150
  };
14094
15151
 
@@ -14665,6 +15722,7 @@ export default {
14665
15722
  "if-statement-format": ifStatementFormat,
14666
15723
  "multiline-if-conditions": multilineIfConditions,
14667
15724
  "no-empty-lines-in-switch-cases": noEmptyLinesInSwitchCases,
15725
+ "ternary-condition-multiline": ternaryConditionMultiline,
14668
15726
 
14669
15727
  // Function rules
14670
15728
  "function-call-spacing": functionCallSpacing,
@@ -14684,6 +15742,7 @@ export default {
14684
15742
  "import-format": importFormat,
14685
15743
  "import-source-spacing": importSourceSpacing,
14686
15744
  "index-export-style": indexExportStyle,
15745
+ "index-exports-only": indexExportsOnly,
14687
15746
  "module-index-exports": moduleIndexExports,
14688
15747
 
14689
15748
  // JSX rules
@@ -14716,6 +15775,7 @@ export default {
14716
15775
  // TypeScript rules
14717
15776
  "enum-format": enumFormat,
14718
15777
  "interface-format": interfaceFormat,
15778
+ "no-inline-type-definitions": noInlineTypeDefinitions,
14719
15779
  "type-annotation-spacing": typeAnnotationSpacing,
14720
15780
  "type-format": typeFormat,
14721
15781
  "typescript-definition-location": typescriptDefinitionLocation,