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/CHANGELOG.md +336 -0
- package/README.md +68 -65
- package/index.d.ts +8 -0
- package/index.js +1150 -90
- package/package.json +2 -2
- package/AGENTS.md +0 -677
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
|
-
"
|
|
1800
|
-
|
|
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
|
-
"
|
|
1805
|
-
|
|
1806
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
1811
|
-
"
|
|
1812
|
-
|
|
1813
|
-
"
|
|
1814
|
-
|
|
1815
|
-
"
|
|
1816
|
-
|
|
1817
|
-
"
|
|
1818
|
-
"
|
|
1819
|
-
|
|
1820
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
3150
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
12131
|
-
|
|
12132
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
13980
|
-
|
|
13981
|
-
|
|
13982
|
-
|
|
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
|
-
|
|
13985
|
-
|
|
14750
|
+
// Find the variable name
|
|
14751
|
+
const depInfo = stmtInfo.get(depIndex);
|
|
13986
14752
|
|
|
13987
|
-
|
|
13988
|
-
|
|
13989
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14000
|
-
|
|
14001
|
-
|
|
14002
|
-
|
|
14003
|
-
|
|
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
|
-
|
|
14006
|
-
|
|
14007
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14015
|
-
|
|
14016
|
-
|
|
14017
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14028
|
-
|
|
14029
|
-
|
|
14896
|
+
return finalResult;
|
|
14897
|
+
};
|
|
14898
|
+
|
|
14899
|
+
const sortedIndices = sortWithDependenciesHandler();
|
|
14030
14900
|
|
|
14031
14901
|
// Check if sorting actually changes the order
|
|
14032
|
-
const orderChanged =
|
|
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
|
|
14912
|
+
let lastCategory = null;
|
|
14045
14913
|
|
|
14046
|
-
for (let i = 0; i <
|
|
14047
|
-
const
|
|
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
|
|
14050
|
-
if (
|
|
14919
|
+
// Add blank line between different categories
|
|
14920
|
+
if (lastCategory !== null && category !== null && category !== lastCategory) {
|
|
14051
14921
|
newBodyContent += "\n";
|
|
14052
14922
|
}
|
|
14053
14923
|
|
|
14054
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14070
|
-
|
|
14071
|
-
|
|
14072
|
-
|
|
14073
|
-
|
|
14074
|
-
|
|
14075
|
-
|
|
14076
|
-
|
|
14077
|
-
|
|
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,
|