eslint-plugin-code-style 1.14.0 → 1.14.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 +36 -0
- package/index.js +262 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.14.3] - 2026-02-05
|
|
11
|
+
|
|
12
|
+
### Enhanced
|
|
13
|
+
|
|
14
|
+
- **`type-annotation-spacing`** - Add auto-fix to collapse function types with 2 or fewer params to one line
|
|
15
|
+
- **`interface-format`** - Fix circular fix conflict by skipping collapse when property has multi-line function type
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## [1.14.2] - 2026-02-05
|
|
20
|
+
|
|
21
|
+
### Enhanced
|
|
22
|
+
|
|
23
|
+
- **`type-annotation-spacing`** - Add spacing rules for async keyword and function types:
|
|
24
|
+
- Enforce space after `async` keyword: `async()` → `async ()`
|
|
25
|
+
- Enforce space after `=>` in function types: `() =>void` → `() => void`
|
|
26
|
+
- Format function types with 3+ params on multiple lines
|
|
27
|
+
- **`interface-format`** - Skip collapsing single-property interfaces when property has function type with 3+ params
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## [1.14.1] - 2026-02-05
|
|
32
|
+
|
|
33
|
+
### Enhanced
|
|
34
|
+
|
|
35
|
+
- **`function-naming-convention`** - Detect functions destructured from hooks without proper naming
|
|
36
|
+
- Flags: `const { logout } = useAuth()` (should be `logoutHandler`)
|
|
37
|
+
- Auto-fixes to: `const { logout: logoutHandler } = useAuth()`
|
|
38
|
+
- Renames all usages of the local variable
|
|
39
|
+
- Only flags clear action verbs (login, logout, toggle, increment, etc.)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
10
43
|
## [1.14.0] - 2026-02-05
|
|
11
44
|
|
|
12
45
|
**New Rule: useState Naming Convention**
|
|
@@ -1654,6 +1687,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
1654
1687
|
|
|
1655
1688
|
---
|
|
1656
1689
|
|
|
1690
|
+
[1.14.3]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.14.2...v1.14.3
|
|
1691
|
+
[1.14.2]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.14.1...v1.14.2
|
|
1692
|
+
[1.14.1]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.14.0...v1.14.1
|
|
1657
1693
|
[1.14.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.13.0...v1.14.0
|
|
1658
1694
|
[1.13.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.12.1...v1.13.0
|
|
1659
1695
|
[1.12.1]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.12.0...v1.12.1
|
package/index.js
CHANGED
|
@@ -2500,11 +2500,139 @@ const functionNamingConvention = {
|
|
|
2500
2500
|
}
|
|
2501
2501
|
};
|
|
2502
2502
|
|
|
2503
|
+
// Check functions destructured from custom hooks: const { logout } = useAuth()
|
|
2504
|
+
// Valid: onLogout, logoutAction, logoutHandler
|
|
2505
|
+
// Invalid: logout (should be logoutHandler)
|
|
2506
|
+
const checkDestructuredHookFunctionHandler = (node) => {
|
|
2507
|
+
// Check if this is destructuring from a hook call: const { ... } = useXxx()
|
|
2508
|
+
if (!node.init || node.init.type !== "CallExpression") return;
|
|
2509
|
+
if (!node.id || node.id.type !== "ObjectPattern") return;
|
|
2510
|
+
|
|
2511
|
+
const callee = node.init.callee;
|
|
2512
|
+
|
|
2513
|
+
// Check if it's a hook call (starts with "use")
|
|
2514
|
+
if (!callee || callee.type !== "Identifier" || !hookRegex.test(callee.name)) return;
|
|
2515
|
+
|
|
2516
|
+
// Action verbs that are clearly function names (not noun-like)
|
|
2517
|
+
// These are verbs that when used alone as a name, clearly indicate an action/function
|
|
2518
|
+
const actionVerbs = [
|
|
2519
|
+
// Auth actions
|
|
2520
|
+
"login", "logout", "authenticate", "authorize", "signup", "signout", "signin",
|
|
2521
|
+
// CRUD actions (when standalone, not as prefix)
|
|
2522
|
+
"submit", "reset", "clear", "refresh", "reload", "retry",
|
|
2523
|
+
// Toggle/state actions
|
|
2524
|
+
"toggle", "enable", "disable", "activate", "deactivate",
|
|
2525
|
+
"show", "hide", "open", "close", "expand", "collapse",
|
|
2526
|
+
// Navigation
|
|
2527
|
+
"navigate", "redirect", "goBack", "goForward",
|
|
2528
|
+
// Increment/decrement
|
|
2529
|
+
"increment", "decrement", "increase", "decrease",
|
|
2530
|
+
// Other common hook function names
|
|
2531
|
+
"connect", "disconnect", "subscribe", "unsubscribe",
|
|
2532
|
+
"start", "stop", "pause", "resume", "cancel", "abort",
|
|
2533
|
+
"select", "deselect", "check", "uncheck",
|
|
2534
|
+
"add", "remove", "insert", "delete", "update",
|
|
2535
|
+
];
|
|
2536
|
+
|
|
2537
|
+
// Check each destructured property
|
|
2538
|
+
node.id.properties.forEach((prop) => {
|
|
2539
|
+
if (prop.type !== "Property" || prop.key.type !== "Identifier") return;
|
|
2540
|
+
|
|
2541
|
+
const name = prop.key.name;
|
|
2542
|
+
|
|
2543
|
+
// Get the local variable name (value node for non-shorthand, key for shorthand)
|
|
2544
|
+
const valueNode = prop.value || prop.key;
|
|
2545
|
+
const localName = valueNode.type === "Identifier" ? valueNode.name : name;
|
|
2546
|
+
|
|
2547
|
+
// Skip if local name starts with "on" (like onLogout, onClick)
|
|
2548
|
+
if (/^on[A-Z]/.test(localName)) return;
|
|
2549
|
+
|
|
2550
|
+
// Skip if local name ends with "Action" (like logoutAction)
|
|
2551
|
+
if (/Action$/.test(localName)) return;
|
|
2552
|
+
|
|
2553
|
+
// Skip if local name ends with "Handler" (like logoutHandler)
|
|
2554
|
+
if (handlerRegex.test(localName)) return;
|
|
2555
|
+
|
|
2556
|
+
// Skip boolean-like names (is, has, can, should, etc.)
|
|
2557
|
+
const booleanPrefixes = ["is", "has", "can", "should", "will", "did", "was", "were", "does"];
|
|
2558
|
+
|
|
2559
|
+
if (booleanPrefixes.some((prefix) => localName.startsWith(prefix) && localName.length > prefix.length && /[A-Z]/.test(localName[prefix.length]))) return;
|
|
2560
|
+
|
|
2561
|
+
// Only flag if the name is exactly an action verb or starts with one followed by uppercase
|
|
2562
|
+
const isActionVerb = actionVerbs.some((verb) =>
|
|
2563
|
+
localName === verb || (localName.startsWith(verb) && localName.length > verb.length && /[A-Z]/.test(localName[verb.length])));
|
|
2564
|
+
|
|
2565
|
+
if (!isActionVerb) return;
|
|
2566
|
+
|
|
2567
|
+
// This is a function name without proper prefix/suffix
|
|
2568
|
+
// Add Handler suffix: logout -> logoutHandler
|
|
2569
|
+
const suggestedName = localName + "Handler";
|
|
2570
|
+
|
|
2571
|
+
context.report({
|
|
2572
|
+
fix(fixer) {
|
|
2573
|
+
const fixes = [];
|
|
2574
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
2575
|
+
|
|
2576
|
+
if (prop.shorthand) {
|
|
2577
|
+
// Shorthand: { logout } -> { logout: logoutHandler }
|
|
2578
|
+
// Replace the entire property with key: newValue format
|
|
2579
|
+
fixes.push(fixer.replaceText(prop, `${name}: ${suggestedName}`));
|
|
2580
|
+
} else {
|
|
2581
|
+
// Non-shorthand: { logout: myVar } -> { logout: myVarHandler }
|
|
2582
|
+
// Just replace the value identifier
|
|
2583
|
+
fixes.push(fixer.replaceText(valueNode, suggestedName));
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
// Find and rename all usages of the local variable
|
|
2587
|
+
const scope = sourceCode.getScope
|
|
2588
|
+
? sourceCode.getScope(node)
|
|
2589
|
+
: context.getScope();
|
|
2590
|
+
|
|
2591
|
+
// Helper to find variable in scope chain
|
|
2592
|
+
const findVariableHandler = (s, varName) => {
|
|
2593
|
+
const v = s.variables.find((variable) => variable.name === varName);
|
|
2594
|
+
|
|
2595
|
+
if (v) return v;
|
|
2596
|
+
if (s.upper) return findVariableHandler(s.upper, varName);
|
|
2597
|
+
|
|
2598
|
+
return null;
|
|
2599
|
+
};
|
|
2600
|
+
|
|
2601
|
+
const variable = findVariableHandler(scope, localName);
|
|
2602
|
+
|
|
2603
|
+
if (variable) {
|
|
2604
|
+
const fixedRanges = new Set();
|
|
2605
|
+
|
|
2606
|
+
// Add the definition range to avoid double-fixing
|
|
2607
|
+
if (valueNode.range) {
|
|
2608
|
+
fixedRanges.add(`${valueNode.range[0]}-${valueNode.range[1]}`);
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
// Fix all references
|
|
2612
|
+
variable.references.forEach((ref) => {
|
|
2613
|
+
const rangeKey = `${ref.identifier.range[0]}-${ref.identifier.range[1]}`;
|
|
2614
|
+
|
|
2615
|
+
if (!fixedRanges.has(rangeKey)) {
|
|
2616
|
+
fixedRanges.add(rangeKey);
|
|
2617
|
+
fixes.push(fixer.replaceText(ref.identifier, suggestedName));
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
return fixes;
|
|
2623
|
+
},
|
|
2624
|
+
message: `Function "${localName}" destructured from hook should end with "Handler" suffix. Use "${suggestedName}" instead`,
|
|
2625
|
+
node: valueNode,
|
|
2626
|
+
});
|
|
2627
|
+
});
|
|
2628
|
+
};
|
|
2629
|
+
|
|
2503
2630
|
return {
|
|
2504
2631
|
ArrowFunctionExpression: checkFunctionHandler,
|
|
2505
2632
|
FunctionDeclaration: checkFunctionHandler,
|
|
2506
2633
|
FunctionExpression: checkFunctionHandler,
|
|
2507
2634
|
MethodDefinition: checkMethodHandler,
|
|
2635
|
+
VariableDeclarator: checkDestructuredHookFunctionHandler,
|
|
2508
2636
|
};
|
|
2509
2637
|
},
|
|
2510
2638
|
meta: {
|
|
@@ -19852,6 +19980,57 @@ const typeAnnotationSpacing = {
|
|
|
19852
19980
|
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
19853
19981
|
|
|
19854
19982
|
return {
|
|
19983
|
+
ArrowFunctionExpression(node) {
|
|
19984
|
+
// Check for space after async keyword: async() => -> async () =>
|
|
19985
|
+
if (node.async) {
|
|
19986
|
+
const asyncToken = sourceCode.getFirstToken(node, (t) => t.value === "async");
|
|
19987
|
+
const openParen = sourceCode.getTokenAfter(asyncToken, (t) => t.value === "(");
|
|
19988
|
+
|
|
19989
|
+
if (asyncToken && openParen) {
|
|
19990
|
+
const textBetween = sourceCode.text.slice(asyncToken.range[1], openParen.range[0]);
|
|
19991
|
+
|
|
19992
|
+
// Should have exactly one space after async
|
|
19993
|
+
if (textBetween === "") {
|
|
19994
|
+
context.report({
|
|
19995
|
+
fix: (fixer) => fixer.insertTextAfter(asyncToken, " "),
|
|
19996
|
+
message: "Missing space after async keyword",
|
|
19997
|
+
node: asyncToken,
|
|
19998
|
+
});
|
|
19999
|
+
} else if (textBetween !== " " && !textBetween.includes("\n")) {
|
|
20000
|
+
// Has extra spaces but not newline
|
|
20001
|
+
context.report({
|
|
20002
|
+
fix: (fixer) => fixer.replaceTextRange([asyncToken.range[1], openParen.range[0]], " "),
|
|
20003
|
+
message: "Should have exactly one space after async keyword",
|
|
20004
|
+
node: asyncToken,
|
|
20005
|
+
});
|
|
20006
|
+
}
|
|
20007
|
+
}
|
|
20008
|
+
}
|
|
20009
|
+
},
|
|
20010
|
+
FunctionExpression(node) {
|
|
20011
|
+
// Check for space after async keyword in function expressions: async function() -> async function ()
|
|
20012
|
+
if (node.async) {
|
|
20013
|
+
const asyncToken = sourceCode.getFirstToken(node, (t) => t.value === "async");
|
|
20014
|
+
const functionToken = sourceCode.getTokenAfter(asyncToken, (t) => t.value === "function");
|
|
20015
|
+
|
|
20016
|
+
if (functionToken) {
|
|
20017
|
+
const openParen = sourceCode.getTokenAfter(functionToken, (t) => t.value === "(");
|
|
20018
|
+
|
|
20019
|
+
if (openParen) {
|
|
20020
|
+
const textBetween = sourceCode.text.slice(functionToken.range[1], openParen.range[0]);
|
|
20021
|
+
|
|
20022
|
+
// Should have exactly one space after function keyword
|
|
20023
|
+
if (textBetween === "") {
|
|
20024
|
+
context.report({
|
|
20025
|
+
fix: (fixer) => fixer.insertTextAfter(functionToken, " "),
|
|
20026
|
+
message: "Missing space after function keyword",
|
|
20027
|
+
node: functionToken,
|
|
20028
|
+
});
|
|
20029
|
+
}
|
|
20030
|
+
}
|
|
20031
|
+
}
|
|
20032
|
+
}
|
|
20033
|
+
},
|
|
19855
20034
|
TSArrayType(node) {
|
|
19856
20035
|
// Check for space before [] like: Type []
|
|
19857
20036
|
const elementType = node.elementType;
|
|
@@ -20237,6 +20416,83 @@ const typeAnnotationSpacing = {
|
|
|
20237
20416
|
}
|
|
20238
20417
|
}
|
|
20239
20418
|
},
|
|
20419
|
+
TSFunctionType(node) {
|
|
20420
|
+
// Check for space after => in function types: () =>void -> () => void
|
|
20421
|
+
// Find the arrow token by searching all tokens in the node
|
|
20422
|
+
const tokens = sourceCode.getTokens(node);
|
|
20423
|
+
const arrowToken = tokens.find((t) => t.value === "=>");
|
|
20424
|
+
|
|
20425
|
+
if (arrowToken) {
|
|
20426
|
+
const nextToken = sourceCode.getTokenAfter(arrowToken);
|
|
20427
|
+
|
|
20428
|
+
if (nextToken) {
|
|
20429
|
+
const textAfterArrow = sourceCode.text.slice(arrowToken.range[1], nextToken.range[0]);
|
|
20430
|
+
|
|
20431
|
+
// Should have exactly one space after =>
|
|
20432
|
+
if (textAfterArrow === "") {
|
|
20433
|
+
context.report({
|
|
20434
|
+
fix: (fixer) => fixer.insertTextAfter(arrowToken, " "),
|
|
20435
|
+
message: "Missing space after => in function type",
|
|
20436
|
+
node: arrowToken,
|
|
20437
|
+
});
|
|
20438
|
+
} else if (textAfterArrow !== " " && !textAfterArrow.includes("\n")) {
|
|
20439
|
+
// Has extra spaces but not newline
|
|
20440
|
+
context.report({
|
|
20441
|
+
fix: (fixer) => fixer.replaceTextRange([arrowToken.range[1], nextToken.range[0]], " "),
|
|
20442
|
+
message: "Should have exactly one space after => in function type",
|
|
20443
|
+
node: arrowToken,
|
|
20444
|
+
});
|
|
20445
|
+
}
|
|
20446
|
+
}
|
|
20447
|
+
}
|
|
20448
|
+
|
|
20449
|
+
// Check function type params formatting
|
|
20450
|
+
// - 3+ params should be multiline
|
|
20451
|
+
// - 0-2 params should be on one line
|
|
20452
|
+
const params = node.params;
|
|
20453
|
+
const openParen = tokens.find((t) => t.value === "(");
|
|
20454
|
+
|
|
20455
|
+
if (openParen && arrowToken) {
|
|
20456
|
+
const closeParen = sourceCode.getTokenBefore(arrowToken, (t) => t.value === ")");
|
|
20457
|
+
|
|
20458
|
+
if (closeParen) {
|
|
20459
|
+
const isMultiLine = openParen.loc.start.line !== closeParen.loc.end.line;
|
|
20460
|
+
|
|
20461
|
+
if (params && params.length >= 3 && !isMultiLine) {
|
|
20462
|
+
// 3+ params on one line - expand to multiple lines
|
|
20463
|
+
const lineStart = sourceCode.text.lastIndexOf("\n", node.range[0]) + 1;
|
|
20464
|
+
const lineText = sourceCode.text.slice(lineStart, node.range[0]);
|
|
20465
|
+
const match = lineText.match(/^(\s*)/);
|
|
20466
|
+
const baseIndent = match ? match[1] : "";
|
|
20467
|
+
const paramIndent = baseIndent + " ";
|
|
20468
|
+
|
|
20469
|
+
const formattedParams = params.map((p) => {
|
|
20470
|
+
const paramText = sourceCode.getText(p);
|
|
20471
|
+
|
|
20472
|
+
return paramIndent + paramText;
|
|
20473
|
+
}).join(",\n");
|
|
20474
|
+
|
|
20475
|
+
const newParamsText = `(\n${formattedParams},\n${baseIndent})`;
|
|
20476
|
+
|
|
20477
|
+
context.report({
|
|
20478
|
+
fix: (fixer) => fixer.replaceTextRange([openParen.range[0], closeParen.range[1]], newParamsText),
|
|
20479
|
+
message: "Function type with 3+ parameters should have each parameter on its own line",
|
|
20480
|
+
node,
|
|
20481
|
+
});
|
|
20482
|
+
} else if (params && params.length <= 2 && isMultiLine) {
|
|
20483
|
+
// 0-2 params on multiple lines - collapse to one line
|
|
20484
|
+
const formattedParams = params.map((p) => sourceCode.getText(p).trim()).join(", ");
|
|
20485
|
+
const newParamsText = `(${formattedParams})`;
|
|
20486
|
+
|
|
20487
|
+
context.report({
|
|
20488
|
+
fix: (fixer) => fixer.replaceTextRange([openParen.range[0], closeParen.range[1]], newParamsText),
|
|
20489
|
+
message: "Function type with 2 or fewer parameters should be on one line",
|
|
20490
|
+
node,
|
|
20491
|
+
});
|
|
20492
|
+
}
|
|
20493
|
+
}
|
|
20494
|
+
}
|
|
20495
|
+
},
|
|
20240
20496
|
VariableDeclarator(node) {
|
|
20241
20497
|
// Check for space before generic like: ColumnDef <T>
|
|
20242
20498
|
if (node.id && node.id.typeAnnotation) {
|
|
@@ -21705,6 +21961,7 @@ const interfaceFormat = {
|
|
|
21705
21961
|
|
|
21706
21962
|
// For single member, should be on one line without trailing punctuation
|
|
21707
21963
|
// But skip if the property has a nested object type with 2+ members
|
|
21964
|
+
// Or if the property has a multi-line function type (let type-annotation-spacing handle it first)
|
|
21708
21965
|
if (members.length === 1) {
|
|
21709
21966
|
const member = members[0];
|
|
21710
21967
|
const isMultiLine = openBraceToken.loc.end.line !== closeBraceToken.loc.start.line;
|
|
@@ -21715,7 +21972,11 @@ const interfaceFormat = {
|
|
|
21715
21972
|
const hasMultiMemberNestedType = hasNestedType && nestedType.members?.length >= 2;
|
|
21716
21973
|
const hasSingleMemberNestedType = hasNestedType && nestedType.members?.length === 1;
|
|
21717
21974
|
|
|
21718
|
-
if
|
|
21975
|
+
// Check if property has function type that spans multiple lines
|
|
21976
|
+
const hasMultiLineFunctionType = nestedType?.type === "TSFunctionType" &&
|
|
21977
|
+
nestedType.loc.start.line !== nestedType.loc.end.line;
|
|
21978
|
+
|
|
21979
|
+
if (isMultiLine && !hasMultiMemberNestedType && !hasMultiLineFunctionType) {
|
|
21719
21980
|
// Build the collapsed text, handling nested types specially
|
|
21720
21981
|
let cleanText;
|
|
21721
21982
|
|
package/package.json
CHANGED