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.
Files changed (3) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/index.js +262 -1
  3. 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 (isMultiLine && !hasMultiMemberNestedType) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-code-style",
3
- "version": "1.14.0",
3
+ "version": "1.14.3",
4
4
  "description": "A custom ESLint plugin for enforcing consistent code formatting and style rules in React/JSX projects",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",