eslint-plugin-code-style 1.11.7 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1709,9 +1709,16 @@ const commentFormat = {
1709
1709
  const isSingleLine = !value.includes("\n");
1710
1710
 
1711
1711
  if (isSingleLine) {
1712
- // Single-line block comment should use // syntax
1712
+ // Skip ESLint directive comments (eslint-disable, eslint-enable, etc.)
1713
1713
  const trimmedValue = value.trim();
1714
+ const isEslintDirective = /^eslint-disable|^eslint-enable|^eslint-disable-next-line|^eslint-disable-line/.test(trimmedValue);
1715
+
1716
+ if (isEslintDirective) {
1717
+ // Allow /* */ syntax for ESLint directives
1718
+ return;
1719
+ }
1714
1720
 
1721
+ // Single-line block comment should use // syntax
1715
1722
  context.report({
1716
1723
  fix: (fixer) => fixer.replaceText(comment, `// ${trimmedValue}`),
1717
1724
  loc: comment.loc,
@@ -2108,6 +2115,7 @@ const functionNamingConvention = {
2108
2115
  "build", "make", "generate", "compute", "calculate", "process", "execute", "run",
2109
2116
  "evaluate", "analyze", "measure", "benchmark", "profile", "optimize",
2110
2117
  "count", "sum", "avg", "min", "max", "clamp", "round", "floor", "ceil", "abs",
2118
+ "increment", "decrement", "multiply", "divide", "mod", "negate",
2111
2119
  // Invocation
2112
2120
  "apply", "call", "invoke", "trigger", "fire", "dispatch", "emit", "raise", "signal",
2113
2121
  // Auth
@@ -2209,6 +2217,23 @@ const functionNamingConvention = {
2209
2217
  if (node.parent && node.parent.type === "VariableDeclarator" && node.parent.id) {
2210
2218
  name = node.parent.id.name;
2211
2219
  identifierNode = node.parent.id;
2220
+ } else if (node.parent && node.parent.type === "CallExpression") {
2221
+ // Check for useCallback wrapped functions: const login = useCallback(() => {...}, [])
2222
+ // Note: Skip useMemo - it returns computed values, not action functions
2223
+ const callExpr = node.parent;
2224
+ const callee = callExpr.callee;
2225
+
2226
+ // Only check useCallback (not useMemo, useEffect, etc.)
2227
+ if (callee && callee.type === "Identifier" && callee.name === "useCallback") {
2228
+ // Check if the function is the first argument to useCallback
2229
+ if (callExpr.arguments && callExpr.arguments[0] === node) {
2230
+ // Check if the CallExpression is the init of a VariableDeclarator
2231
+ if (callExpr.parent && callExpr.parent.type === "VariableDeclarator" && callExpr.parent.id) {
2232
+ name = callExpr.parent.id.name;
2233
+ identifierNode = callExpr.parent.id;
2234
+ }
2235
+ }
2236
+ }
2212
2237
  }
2213
2238
  }
2214
2239
 
@@ -2314,15 +2339,14 @@ const functionNamingConvention = {
2314
2339
  if (!hasVerbPrefix && !hasHandlerSuffix) {
2315
2340
  context.report({
2316
2341
  message: `Function "${name}" should start with a verb (get, set, fetch, etc.) AND end with "Handler" (e.g., getDataHandler, clickHandler)`,
2317
- node: node.id || node.parent.id,
2342
+ node: identifierNode,
2318
2343
  });
2319
2344
  } else if (!hasVerbPrefix) {
2320
2345
  context.report({
2321
2346
  message: `Function "${name}" should start with a verb (get, set, fetch, handle, click, submit, etc.)`,
2322
- node: node.id || node.parent.id,
2347
+ node: identifierNode,
2323
2348
  });
2324
2349
  } else if (!hasHandlerSuffix) {
2325
- const identifierNode = node.id || node.parent.id;
2326
2350
  const newName = `${name}Handler`;
2327
2351
 
2328
2352
  context.report({
@@ -3305,6 +3329,249 @@ const hookDepsPerLine = {
3305
3329
  },
3306
3330
  };
3307
3331
 
3332
+ /**
3333
+ * ───────────────────────────────────────────────────────────────
3334
+ * Rule: useState Naming Convention
3335
+ * ───────────────────────────────────────────────────────────────
3336
+ *
3337
+ * Description:
3338
+ * When useState holds a boolean value, the state variable name
3339
+ * should start with a valid boolean prefix (is, has, with, without).
3340
+ *
3341
+ * ✓ Good:
3342
+ * const [isLoading, setIsLoading] = useState(false);
3343
+ * const [hasError, setHasError] = useState(false);
3344
+ * const [isAuthenticated, setIsAuthenticated] = useState<boolean>(() => checkAuth());
3345
+ *
3346
+ * ✗ Bad:
3347
+ * const [loading, setLoading] = useState(false);
3348
+ * const [authenticated, setAuthenticated] = useState<boolean>(true);
3349
+ * const [error, setError] = useState<boolean>(false);
3350
+ */
3351
+ const useStateNamingConvention = {
3352
+ create(context) {
3353
+ const options = context.options[0] || {};
3354
+
3355
+ // Boolean prefixes handling (same pattern as prop-naming-convention)
3356
+ const defaultBooleanPrefixes = ["is", "has", "with", "without"];
3357
+ const booleanPrefixes = options.booleanPrefixes || [
3358
+ ...defaultBooleanPrefixes,
3359
+ ...(options.extendBooleanPrefixes || []),
3360
+ ];
3361
+
3362
+ const allowPastVerbBoolean = options.allowPastVerbBoolean || false;
3363
+ const allowContinuousVerbBoolean = options.allowContinuousVerbBoolean || false;
3364
+
3365
+ // Pattern to check if name starts with valid boolean prefix followed by capital letter
3366
+ const booleanPrefixPattern = new RegExp(`^(${booleanPrefixes.join("|")})[A-Z]`);
3367
+
3368
+ // Pattern for past verb booleans (ends with -ed: disabled, selected, checked, etc.)
3369
+ const pastVerbPattern = /^[a-z]+ed$/;
3370
+
3371
+ // Pattern for continuous verb booleans (ends with -ing: loading, saving, etc.)
3372
+ const continuousVerbPattern = /^[a-z]+ing$/;
3373
+
3374
+ // Words that suggest "has" prefix instead of "is"
3375
+ const hasKeywords = [
3376
+ "children", "content", "data", "error", "errors", "items",
3377
+ "permission", "permissions", "value", "values",
3378
+ ];
3379
+
3380
+ // Convert name to appropriate boolean prefix
3381
+ const toBooleanNameHandler = (name) => {
3382
+ const lowerName = name.toLowerCase();
3383
+ const prefix = hasKeywords.some((k) => lowerName.includes(k)) ? "has" : "is";
3384
+
3385
+ return prefix + name[0].toUpperCase() + name.slice(1);
3386
+ };
3387
+
3388
+ // Convert setter name based on new state name
3389
+ const toSetterNameHandler = (stateName) => "set" + stateName[0].toUpperCase() + stateName.slice(1);
3390
+
3391
+ // Check if name is a valid boolean state name
3392
+ const isValidBooleanNameHandler = (name) => {
3393
+ // Starts with valid prefix
3394
+ if (booleanPrefixPattern.test(name)) return true;
3395
+
3396
+ // Allow past verb booleans if option is enabled (disabled, selected, checked, etc.)
3397
+ if (allowPastVerbBoolean && pastVerbPattern.test(name)) return true;
3398
+
3399
+ // Allow continuous verb booleans if option is enabled (loading, saving, etc.)
3400
+ if (allowContinuousVerbBoolean && continuousVerbPattern.test(name)) return true;
3401
+
3402
+ return false;
3403
+ };
3404
+
3405
+ // Check if the useState initial value indicates boolean
3406
+ const isBooleanValueHandler = (arg) => {
3407
+ if (!arg) return false;
3408
+
3409
+ // Direct boolean literal: useState(false) or useState(true)
3410
+ if (arg.type === "Literal" && typeof arg.value === "boolean") return true;
3411
+
3412
+ // Arrow function returning boolean: useState(() => checkAuth())
3413
+ // We can't reliably determine return type, so skip these unless typed
3414
+ return false;
3415
+ };
3416
+
3417
+ // Check if the useState has boolean type annotation
3418
+ const hasBooleanTypeAnnotationHandler = (node) => {
3419
+ // useState<boolean>(...) or useState<boolean | null>(...)
3420
+ if (node.typeParameters && node.typeParameters.params && node.typeParameters.params.length > 0) {
3421
+ const typeParam = node.typeParameters.params[0];
3422
+
3423
+ if (typeParam.type === "TSBooleanKeyword") return true;
3424
+
3425
+ // Check union types: boolean | null, boolean | undefined
3426
+ if (typeParam.type === "TSUnionType") {
3427
+ return typeParam.types.some((t) => t.type === "TSBooleanKeyword");
3428
+ }
3429
+ }
3430
+
3431
+ return false;
3432
+ };
3433
+
3434
+ return {
3435
+ CallExpression(node) {
3436
+ // Check if it's a useState call
3437
+ if (node.callee.type !== "Identifier" || node.callee.name !== "useState") return;
3438
+
3439
+ // Check if it's in a variable declaration with array destructuring
3440
+ if (!node.parent || node.parent.type !== "VariableDeclarator") return;
3441
+ if (!node.parent.id || node.parent.id.type !== "ArrayPattern") return;
3442
+
3443
+ const arrayPattern = node.parent.id;
3444
+
3445
+ // Must have at least the state variable (first element)
3446
+ if (!arrayPattern.elements || arrayPattern.elements.length < 1) return;
3447
+
3448
+ const stateElement = arrayPattern.elements[0];
3449
+ const setterElement = arrayPattern.elements[1];
3450
+
3451
+ if (!stateElement || stateElement.type !== "Identifier") return;
3452
+
3453
+ const stateName = stateElement.name;
3454
+
3455
+ // Check if this is a boolean useState
3456
+ const isBooleanState = (node.arguments && node.arguments.length > 0 && isBooleanValueHandler(node.arguments[0]))
3457
+ || hasBooleanTypeAnnotationHandler(node);
3458
+
3459
+ if (!isBooleanState) return;
3460
+
3461
+ // Check if state name follows boolean naming convention
3462
+ if (isValidBooleanNameHandler(stateName)) return;
3463
+
3464
+ const suggestedStateName = toBooleanNameHandler(stateName);
3465
+ const suggestedSetterName = toSetterNameHandler(suggestedStateName);
3466
+
3467
+ context.report({
3468
+ fix(fixer) {
3469
+ const fixes = [];
3470
+ const scope = context.sourceCode
3471
+ ? context.sourceCode.getScope(node)
3472
+ : context.getScope();
3473
+
3474
+ // Helper to find variable in scope
3475
+ const findVariableHandler = (s, name) => {
3476
+ const v = s.variables.find((variable) => variable.name === name);
3477
+
3478
+ if (v) return v;
3479
+ if (s.upper) return findVariableHandler(s.upper, name);
3480
+
3481
+ return null;
3482
+ };
3483
+
3484
+ // Fix state variable
3485
+ const stateVar = findVariableHandler(scope, stateName);
3486
+ const stateFixedRanges = new Set();
3487
+
3488
+ // Fix definition first
3489
+ const stateDefRangeKey = `${stateElement.range[0]}-${stateElement.range[1]}`;
3490
+
3491
+ stateFixedRanges.add(stateDefRangeKey);
3492
+ fixes.push(fixer.replaceText(stateElement, suggestedStateName));
3493
+
3494
+ // Fix all usages
3495
+ if (stateVar) {
3496
+ stateVar.references.forEach((ref) => {
3497
+ const rangeKey = `${ref.identifier.range[0]}-${ref.identifier.range[1]}`;
3498
+
3499
+ if (!stateFixedRanges.has(rangeKey)) {
3500
+ stateFixedRanges.add(rangeKey);
3501
+ fixes.push(fixer.replaceText(ref.identifier, suggestedStateName));
3502
+ }
3503
+ });
3504
+ }
3505
+
3506
+ // Fix setter if exists
3507
+ if (setterElement && setterElement.type === "Identifier") {
3508
+ const setterName = setterElement.name;
3509
+ const setterVar = findVariableHandler(scope, setterName);
3510
+ const setterFixedRanges = new Set();
3511
+
3512
+ // Fix definition first
3513
+ const setterDefRangeKey = `${setterElement.range[0]}-${setterElement.range[1]}`;
3514
+
3515
+ setterFixedRanges.add(setterDefRangeKey);
3516
+ fixes.push(fixer.replaceText(setterElement, suggestedSetterName));
3517
+
3518
+ // Fix all usages
3519
+ if (setterVar) {
3520
+ setterVar.references.forEach((ref) => {
3521
+ const rangeKey = `${ref.identifier.range[0]}-${ref.identifier.range[1]}`;
3522
+
3523
+ if (!setterFixedRanges.has(rangeKey)) {
3524
+ setterFixedRanges.add(rangeKey);
3525
+ fixes.push(fixer.replaceText(ref.identifier, suggestedSetterName));
3526
+ }
3527
+ });
3528
+ }
3529
+ }
3530
+
3531
+ return fixes;
3532
+ },
3533
+ message: `Boolean state "${stateName}" should start with a valid prefix (${booleanPrefixes.join(", ")}). Use "${suggestedStateName}" instead.`,
3534
+ node: stateElement,
3535
+ });
3536
+ },
3537
+ };
3538
+ },
3539
+ meta: {
3540
+ docs: { description: "Enforce boolean useState variables to start with is/has/with/without prefix" },
3541
+ fixable: "code",
3542
+ schema: [
3543
+ {
3544
+ additionalProperties: false,
3545
+ properties: {
3546
+ allowContinuousVerbBoolean: {
3547
+ default: false,
3548
+ description: "Allow continuous verb boolean state without prefix (e.g., loading, saving)",
3549
+ type: "boolean",
3550
+ },
3551
+ allowPastVerbBoolean: {
3552
+ default: false,
3553
+ description: "Allow past verb boolean state without prefix (e.g., disabled, selected)",
3554
+ type: "boolean",
3555
+ },
3556
+ booleanPrefixes: {
3557
+ description: "Replace default boolean prefixes entirely",
3558
+ items: { type: "string" },
3559
+ type: "array",
3560
+ },
3561
+ extendBooleanPrefixes: {
3562
+ default: [],
3563
+ description: "Add additional prefixes to the defaults (is, has, with, without)",
3564
+ items: { type: "string" },
3565
+ type: "array",
3566
+ },
3567
+ },
3568
+ type: "object",
3569
+ },
3570
+ ],
3571
+ type: "suggestion",
3572
+ },
3573
+ };
3574
+
3308
3575
  /**
3309
3576
  * ───────────────────────────────────────────────────────────────
3310
3577
  * Rule: If Statement Format
@@ -7818,16 +8085,6 @@ const jsxChildrenOnNewLine = {
7818
8085
  message: "JSX child should be on its own line",
7819
8086
  node: firstChild,
7820
8087
  });
7821
- } else if (openingTag.loc.end.line < firstChild.loc.start.line - 1) {
7822
- // Check for extra blank lines after opening tag (more than 1 newline)
7823
- context.report({
7824
- fix: (fixer) => fixer.replaceTextRange(
7825
- [openingTag.range[1], firstChild.range[0]],
7826
- "\n" + childIndent,
7827
- ),
7828
- message: "Remove blank lines after opening tag",
7829
- node: firstChild,
7830
- });
7831
8088
  }
7832
8089
 
7833
8090
  // Check if closing tag is on same line as last child
@@ -8560,134 +8817,583 @@ const jsxPropNamingConvention = {
8560
8817
 
8561
8818
  /**
8562
8819
  * ───────────────────────────────────────────────────────────────
8563
- * Rule: JSX Simple Element On One Line
8820
+ * Rule: Prop Naming Convention
8564
8821
  * ───────────────────────────────────────────────────────────────
8565
8822
  *
8566
8823
  * Description:
8567
- * Simple JSX elements with only a single text or expression
8568
- * child should be collapsed onto a single line.
8824
+ * Enforces naming conventions for boolean and callback/method props:
8825
+ * - Boolean props must start with: is, has, with, without (followed by capital letter)
8826
+ * - Callback props must start with: on (followed by capital letter)
8827
+ *
8828
+ * Applies to: interfaces, type aliases, inline types, and nested object types
8829
+ * at any nesting level. Does NOT apply to JSX element attributes.
8830
+ *
8831
+ * Options:
8832
+ * { booleanPrefixes: ["is", "has"] } - Replace default prefixes entirely
8833
+ * { extendBooleanPrefixes: ["should", "can"] } - Add to default prefixes
8834
+ * { allowPastVerbBoolean: false } - Allow past verb booleans (disabled, selected, checked, opened, etc.)
8835
+ * { allowContinuousVerbBoolean: false } - Allow continuous verb booleans (loading, saving, closing, etc.)
8836
+ * { callbackPrefix: "on" } - Required prefix for callbacks
8837
+ * { allowActionSuffix: false } - Allow "xxxAction" pattern for callbacks
8569
8838
  *
8570
8839
  * ✓ Good:
8571
- * <Button>{buttonLinkText}</Button>
8572
- * <Title>Hello</Title>
8840
+ * interface PropsInterface {
8841
+ * isLoading: boolean,
8842
+ * hasError: boolean,
8843
+ * onClick: () => void,
8844
+ * onSubmit: (data: Data) => void,
8845
+ * config: {
8846
+ * isEnabled: boolean,
8847
+ * onToggle: () => void,
8848
+ * },
8849
+ * }
8573
8850
  *
8574
8851
  * ✗ Bad:
8575
- * <Button>
8576
- * {buttonLinkText}
8577
- * </Button>
8852
+ * interface PropsInterface {
8853
+ * loading: boolean, // Should be isLoading
8854
+ * error: boolean, // Should be hasError
8855
+ * click: () => void, // Should be onClick
8856
+ * handleSubmit: () => void, // Should be onSubmit
8857
+ * config: {
8858
+ * enabled: boolean, // Should be isEnabled (nested)
8859
+ * toggle: () => void, // Should be onToggle (nested)
8860
+ * },
8861
+ * }
8862
+ *
8863
+ * ✓ Good (with allowPastVerbBoolean: true):
8864
+ * interface PropsInterface {
8865
+ * disabled: boolean, // Past verb - allowed
8866
+ * selected: boolean, // Past verb - allowed
8867
+ * checked: boolean, // Past verb - allowed
8868
+ * }
8869
+ *
8870
+ * ✓ Good (with allowContinuousVerbBoolean: true):
8871
+ * interface PropsInterface {
8872
+ * loading: boolean, // Continuous verb - allowed
8873
+ * saving: boolean, // Continuous verb - allowed
8874
+ * fetching: boolean, // Continuous verb - allowed
8875
+ * }
8578
8876
  */
8579
- const jsxSimpleElementOneLine = {
8877
+ const propNamingConvention = {
8580
8878
  create(context) {
8581
- const sourceCode = context.sourceCode || context.getSourceCode();
8879
+ const options = context.options[0] || {};
8582
8880
 
8583
- // Check if an argument is simple (identifier, literal, or simple member expression)
8584
- const isSimpleArgHandler = (node) => {
8585
- if (!node) return false;
8586
- if (node.type === "Identifier") return true;
8587
- if (node.type === "Literal") return true;
8588
- if (node.type === "MemberExpression") {
8589
- return node.object.type === "Identifier" && node.property.type === "Identifier";
8590
- }
8881
+ // Boolean prefixes handling (like module-index-exports pattern)
8882
+ const defaultBooleanPrefixes = ["is", "has", "with", "without"];
8883
+ const booleanPrefixes = options.booleanPrefixes || [
8884
+ ...defaultBooleanPrefixes,
8885
+ ...(options.extendBooleanPrefixes || []),
8886
+ ];
8591
8887
 
8592
- return false;
8888
+ const allowPastVerbBoolean = options.allowPastVerbBoolean || false;
8889
+ const allowContinuousVerbBoolean = options.allowContinuousVerbBoolean || false;
8890
+ const callbackPrefix = options.callbackPrefix || "on";
8891
+ const allowActionSuffix = options.allowActionSuffix || false;
8892
+
8893
+ // Pattern to check if name starts with valid boolean prefix followed by capital letter
8894
+ const booleanPrefixPattern = new RegExp(`^(${booleanPrefixes.join("|")})[A-Z]`);
8895
+
8896
+ // Pattern for callback prefix
8897
+ const callbackPrefixPattern = new RegExp(`^${callbackPrefix}[A-Z]`);
8898
+
8899
+ // Pattern for past verb booleans (ends with -ed: disabled, selected, checked, opened, closed, etc.)
8900
+ const pastVerbPattern = /^[a-z]+ed$/;
8901
+
8902
+ // Pattern for continuous verb booleans (ends with -ing: loading, saving, closing, etc.)
8903
+ const continuousVerbPattern = /^[a-z]+ing$/;
8904
+
8905
+ // Words that suggest "has" prefix instead of "is"
8906
+ const hasKeywords = [
8907
+ "children", "content", "data", "error", "errors", "items",
8908
+ "permission", "permissions", "value", "values",
8909
+ ];
8910
+
8911
+ // Convert name to appropriate boolean prefix
8912
+ const toBooleanNameHandler = (name) => {
8913
+ const lowerName = name.toLowerCase();
8914
+ const prefix = hasKeywords.some((k) => lowerName.includes(k)) ? "has" : "is";
8915
+
8916
+ return prefix + name[0].toUpperCase() + name.slice(1);
8593
8917
  };
8594
8918
 
8595
- // Check if an expression is simple (identifier, literal, member expression, or simple function call)
8596
- const isSimpleExpressionHandler = (node) => {
8597
- if (!node) return false;
8919
+ // Convert name to callback format (add "on" prefix)
8920
+ const toCallbackNameHandler = (name) => {
8921
+ // Handle "handleXxx" pattern -> "onXxx"
8922
+ if (name.startsWith("handle") && name.length > 6) {
8923
+ const rest = name.slice(6);
8598
8924
 
8599
- if (node.type === "Identifier") return true;
8600
- if (node.type === "Literal") return true;
8601
- if (node.type === "MemberExpression") {
8602
- // Allow simple member expressions like obj.prop
8603
- return node.object.type === "Identifier" && node.property.type === "Identifier";
8925
+ return callbackPrefix + rest[0].toUpperCase() + rest.slice(1);
8604
8926
  }
8605
8927
 
8606
- // Allow simple function calls with 0-1 simple arguments
8607
- if (node.type === "CallExpression") {
8608
- // Check callee is simple (identifier or member expression)
8609
- const { callee } = node;
8610
- const isSimpleCallee = callee.type === "Identifier" ||
8611
- (callee.type === "MemberExpression" &&
8612
- callee.object.type === "Identifier" &&
8613
- callee.property.type === "Identifier");
8928
+ // Handle "xxxHandler" pattern -> "onXxx"
8929
+ if (name.endsWith("Handler") && name.length > 7) {
8930
+ const rest = name.slice(0, -7);
8614
8931
 
8615
- if (!isSimpleCallee) return false;
8932
+ return callbackPrefix + rest[0].toUpperCase() + rest.slice(1);
8933
+ }
8616
8934
 
8617
- // Allow 0-1 arguments, and the argument must be simple
8618
- if (node.arguments.length === 0) return true;
8619
- if (node.arguments.length === 1 && isSimpleArgHandler(node.arguments[0])) return true;
8935
+ // Simple case: just add "on" prefix
8936
+ return callbackPrefix + name[0].toUpperCase() + name.slice(1);
8937
+ };
8620
8938
 
8621
- return false;
8939
+ // Check if type annotation indicates boolean
8940
+ const isBooleanTypeHandler = (typeAnnotation) => {
8941
+ if (!typeAnnotation) return false;
8942
+ const type = typeAnnotation.typeAnnotation;
8943
+
8944
+ if (!type) return false;
8945
+ if (type.type === "TSBooleanKeyword") return true;
8946
+ // Check for union with boolean (e.g., boolean | undefined)
8947
+ if (type.type === "TSUnionType") {
8948
+ return type.types.some((t) => t.type === "TSBooleanKeyword");
8622
8949
  }
8623
8950
 
8624
8951
  return false;
8625
8952
  };
8626
8953
 
8627
- // Check if child is simple (text or simple expression)
8628
- const isSimpleChildHandler = (child) => {
8629
- if (child.type === "JSXText") {
8630
- return child.value.trim().length > 0;
8954
+ // React event handler type names
8955
+ const reactEventHandlerTypes = [
8956
+ "MouseEventHandler",
8957
+ "ChangeEventHandler",
8958
+ "FormEventHandler",
8959
+ "KeyboardEventHandler",
8960
+ "FocusEventHandler",
8961
+ "TouchEventHandler",
8962
+ "PointerEventHandler",
8963
+ "DragEventHandler",
8964
+ "WheelEventHandler",
8965
+ "AnimationEventHandler",
8966
+ "TransitionEventHandler",
8967
+ "ClipboardEventHandler",
8968
+ "CompositionEventHandler",
8969
+ "UIEventHandler",
8970
+ "ScrollEventHandler",
8971
+ "EventHandler",
8972
+ ];
8973
+
8974
+ // Check if type annotation indicates function/callback
8975
+ const isCallbackTypeHandler = (typeAnnotation) => {
8976
+ if (!typeAnnotation) return false;
8977
+ const type = typeAnnotation.typeAnnotation;
8978
+
8979
+ if (!type) return false;
8980
+ if (type.type === "TSFunctionType") return true;
8981
+ if (type.type === "TSTypeReference") {
8982
+ const typeName = type.typeName?.name;
8983
+
8984
+ // Check for Function, VoidFunction, or React event handler types
8985
+ if (typeName === "Function" || typeName === "VoidFunction") return true;
8986
+ if (reactEventHandlerTypes.includes(typeName)) return true;
8631
8987
  }
8632
8988
 
8633
- if (child.type === "JSXExpressionContainer") {
8634
- return isSimpleExpressionHandler(child.expression);
8989
+ // Check for union with function (e.g., (() => void) | undefined)
8990
+ if (type.type === "TSUnionType") {
8991
+ return type.types.some((t) =>
8992
+ t.type === "TSFunctionType" ||
8993
+ (t.type === "TSTypeReference" && (
8994
+ t.typeName?.name === "Function" ||
8995
+ t.typeName?.name === "VoidFunction" ||
8996
+ reactEventHandlerTypes.includes(t.typeName?.name)
8997
+ )));
8635
8998
  }
8636
8999
 
8637
9000
  return false;
8638
9001
  };
8639
9002
 
8640
- return {
8641
- JSXElement(node) {
8642
- const openingTag = node.openingElement;
8643
- const closingTag = node.closingElement;
9003
+ // Check if type annotation is a nested object type (TSTypeLiteral)
9004
+ const isNestedObjectTypeHandler = (typeAnnotation) => {
9005
+ if (!typeAnnotation) return false;
9006
+ const type = typeAnnotation.typeAnnotation;
8644
9007
 
8645
- // Skip self-closing elements
8646
- if (!closingTag) return;
9008
+ if (!type) return false;
8647
9009
 
8648
- // Check if element is multiline
8649
- if (openingTag.loc.end.line === closingTag.loc.start.line) return;
9010
+ return type.type === "TSTypeLiteral";
9011
+ };
8650
9012
 
8651
- const { children } = node;
9013
+ // Check if name is a valid boolean prop name
9014
+ const isValidBooleanNameHandler = (name) => {
9015
+ // Starts with valid prefix
9016
+ if (booleanPrefixPattern.test(name)) return true;
8652
9017
 
8653
- // Filter out whitespace-only text children
8654
- const significantChildren = children.filter((child) => {
8655
- if (child.type === "JSXText") {
8656
- return child.value.trim() !== "";
8657
- }
9018
+ // Allow past verb booleans if option is enabled (disabled, selected, checked, etc.)
9019
+ if (allowPastVerbBoolean && pastVerbPattern.test(name)) return true;
8658
9020
 
8659
- return true;
8660
- });
9021
+ // Allow continuous verb booleans if option is enabled (loading, saving, etc.)
9022
+ if (allowContinuousVerbBoolean && continuousVerbPattern.test(name)) return true;
8661
9023
 
8662
- // Must have exactly one simple child
8663
- if (significantChildren.length !== 1) return;
9024
+ return false;
9025
+ };
8664
9026
 
8665
- const child = significantChildren[0];
9027
+ // Check if name is a valid callback prop name
9028
+ const isValidCallbackNameHandler = (name) => {
9029
+ // Starts with "on" prefix
9030
+ if (callbackPrefixPattern.test(name)) return true;
8666
9031
 
8667
- if (!isSimpleChildHandler(child)) return;
9032
+ // Allow "xxxAction" suffix if option is enabled
9033
+ if (allowActionSuffix && name.endsWith("Action") && name.length > 6) return true;
8668
9034
 
8669
- // Check if opening tag itself is simple (single line, not too many attributes)
8670
- if (openingTag.loc.start.line !== openingTag.loc.end.line) return;
9035
+ return false;
9036
+ };
8671
9037
 
8672
- // Get the text content
8673
- const openingText = sourceCode.getText(openingTag);
8674
- const childText = child.type === "JSXText" ? child.value.trim() : sourceCode.getText(child);
8675
- const closingText = sourceCode.getText(closingTag);
9038
+ // Find the containing function for inline type annotations
9039
+ const findContainingFunctionHandler = (node) => {
9040
+ let current = node;
8676
9041
 
8677
- context.report({
8678
- fix: (fixer) => fixer.replaceText(
8679
- node,
8680
- `${openingText}${childText}${closingText}`,
8681
- ),
8682
- message: "Simple JSX element with single text/expression child should be on one line",
8683
- node,
8684
- });
8685
- },
8686
- };
8687
- },
8688
- meta: {
8689
- docs: { description: "Simple JSX elements with only text/expression children should be on one line" },
8690
- fixable: "whitespace",
9042
+ while (current) {
9043
+ if (current.type === "ArrowFunctionExpression" ||
9044
+ current.type === "FunctionExpression" ||
9045
+ current.type === "FunctionDeclaration") {
9046
+ return current;
9047
+ }
9048
+
9049
+ current = current.parent;
9050
+ }
9051
+
9052
+ return null;
9053
+ };
9054
+
9055
+ // Find matching property in ObjectPattern (destructured params)
9056
+ const findDestructuredPropertyHandler = (funcNode, propName) => {
9057
+ if (!funcNode || !funcNode.params || funcNode.params.length === 0) return null;
9058
+
9059
+ const firstParam = funcNode.params[0];
9060
+
9061
+ if (firstParam.type !== "ObjectPattern") return null;
9062
+
9063
+ // Find the property in the ObjectPattern
9064
+ for (const prop of firstParam.properties) {
9065
+ if (prop.type === "Property" && prop.key && prop.key.type === "Identifier") {
9066
+ if (prop.key.name === propName) {
9067
+ return prop;
9068
+ }
9069
+ }
9070
+ }
9071
+
9072
+ return null;
9073
+ };
9074
+
9075
+ // Create fix that renames type annotation, destructured prop, and all references
9076
+ const createRenamingFixHandler = (fixer, member, propName, suggestedName) => {
9077
+ const fixes = [fixer.replaceText(member.key, suggestedName)];
9078
+
9079
+ // Find the containing function
9080
+ const funcNode = findContainingFunctionHandler(member);
9081
+
9082
+ if (!funcNode) return fixes;
9083
+
9084
+ // Find the matching destructured property
9085
+ const destructuredProp = findDestructuredPropertyHandler(funcNode, propName);
9086
+
9087
+ if (!destructuredProp) return fixes;
9088
+
9089
+ // Get the value node (the actual variable being declared)
9090
+ const valueNode = destructuredProp.value || destructuredProp.key;
9091
+
9092
+ if (!valueNode || valueNode.type !== "Identifier") return fixes;
9093
+
9094
+ // If key and value are the same (shorthand: { copied }), we need to expand and rename
9095
+ // If different (renamed: { copied: isCopied }), just rename the value
9096
+ const isShorthand = destructuredProp.shorthand !== false &&
9097
+ destructuredProp.key === destructuredProp.value;
9098
+
9099
+ if (isShorthand) {
9100
+ // Shorthand syntax: { copied } -> { copied: isCopied }
9101
+ // We need to keep the key (for the type match) and add the renamed value
9102
+ fixes.push(fixer.replaceText(destructuredProp, `${propName}: ${suggestedName}`));
9103
+ } else {
9104
+ // Already renamed syntax or explicit: just update the value
9105
+ fixes.push(fixer.replaceText(valueNode, suggestedName));
9106
+ }
9107
+
9108
+ // Find all references to the variable and rename them
9109
+ const scope = context.sourceCode
9110
+ ? context.sourceCode.getScope(funcNode)
9111
+ : context.getScope();
9112
+
9113
+ // Find the variable in the scope
9114
+ const findVariableHandler = (s, name) => {
9115
+ const v = s.variables.find((variable) => variable.name === name);
9116
+
9117
+ if (v) return v;
9118
+ if (s.upper) return findVariableHandler(s.upper, name);
9119
+
9120
+ return null;
9121
+ };
9122
+
9123
+ const variable = findVariableHandler(scope, propName);
9124
+
9125
+ if (variable) {
9126
+ const fixedRanges = new Set();
9127
+
9128
+ variable.references.forEach((ref) => {
9129
+ // Skip the definition itself
9130
+ if (ref.identifier === valueNode) return;
9131
+ // Skip if already fixed
9132
+ const rangeKey = `${ref.identifier.range[0]}-${ref.identifier.range[1]}`;
9133
+
9134
+ if (fixedRanges.has(rangeKey)) return;
9135
+ fixedRanges.add(rangeKey);
9136
+ fixes.push(fixer.replaceText(ref.identifier, suggestedName));
9137
+ });
9138
+ }
9139
+
9140
+ return fixes;
9141
+ };
9142
+
9143
+ // Check a property signature (interface/type member) - recursive for nested types
9144
+ const checkPropertySignatureHandler = (member) => {
9145
+ if (member.type !== "TSPropertySignature") return;
9146
+ if (!member.key || member.key.type !== "Identifier") return;
9147
+
9148
+ const propName = member.key.name;
9149
+
9150
+ // Skip private properties (starting with _)
9151
+ if (propName.startsWith("_")) return;
9152
+
9153
+ // Check for nested object types and recursively check their members
9154
+ if (isNestedObjectTypeHandler(member.typeAnnotation)) {
9155
+ const nestedType = member.typeAnnotation.typeAnnotation;
9156
+
9157
+ if (nestedType && nestedType.members) {
9158
+ nestedType.members.forEach(checkPropertySignatureHandler);
9159
+ }
9160
+
9161
+ return;
9162
+ }
9163
+
9164
+ // Check boolean props
9165
+ if (isBooleanTypeHandler(member.typeAnnotation)) {
9166
+ if (!isValidBooleanNameHandler(propName)) {
9167
+ const suggestedName = toBooleanNameHandler(propName);
9168
+
9169
+ context.report({
9170
+ fix: (fixer) => createRenamingFixHandler(fixer, member, propName, suggestedName),
9171
+ message: `Boolean prop "${propName}" should start with a valid prefix (${booleanPrefixes.join(", ")}). Use "${suggestedName}" instead.`,
9172
+ node: member.key,
9173
+ });
9174
+ }
9175
+
9176
+ return;
9177
+ }
9178
+
9179
+ // Check callback props
9180
+ if (isCallbackTypeHandler(member.typeAnnotation)) {
9181
+ if (!isValidCallbackNameHandler(propName)) {
9182
+ const suggestedName = toCallbackNameHandler(propName);
9183
+
9184
+ context.report({
9185
+ fix: (fixer) => createRenamingFixHandler(fixer, member, propName, suggestedName),
9186
+ message: `Callback prop "${propName}" should start with "${callbackPrefix}" prefix. Use "${suggestedName}" instead.`,
9187
+ node: member.key,
9188
+ });
9189
+ }
9190
+ }
9191
+ };
9192
+
9193
+ // Check members of a type literal (inline types, type aliases)
9194
+ const checkTypeLiteralHandler = (node) => {
9195
+ if (!node.members) return;
9196
+ node.members.forEach(checkPropertySignatureHandler);
9197
+ };
9198
+
9199
+ return {
9200
+ // Interface declarations
9201
+ TSInterfaceDeclaration(node) {
9202
+ if (!node.body || !node.body.body) return;
9203
+ node.body.body.forEach(checkPropertySignatureHandler);
9204
+ },
9205
+
9206
+ // Type alias declarations with object type
9207
+ TSTypeAliasDeclaration(node) {
9208
+ if (node.typeAnnotation?.type === "TSTypeLiteral") {
9209
+ checkTypeLiteralHandler(node.typeAnnotation);
9210
+ }
9211
+ },
9212
+
9213
+ // Inline type literals (e.g., in function parameters)
9214
+ TSTypeLiteral(node) {
9215
+ // Skip if already handled by TSTypeAliasDeclaration
9216
+ if (node.parent?.type === "TSTypeAliasDeclaration") return;
9217
+ checkTypeLiteralHandler(node);
9218
+ },
9219
+ };
9220
+ },
9221
+ meta: {
9222
+ docs: { description: "Enforce naming conventions: boolean props must start with is/has/with/without, callback props must start with on" },
9223
+ fixable: "code",
9224
+ schema: [
9225
+ {
9226
+ additionalProperties: false,
9227
+ properties: {
9228
+ allowActionSuffix: {
9229
+ default: false,
9230
+ description: "Allow 'xxxAction' pattern for callback props (e.g., submitAction, copyAction)",
9231
+ type: "boolean",
9232
+ },
9233
+ allowContinuousVerbBoolean: {
9234
+ default: false,
9235
+ description: "Allow continuous verb boolean props without prefix (e.g., loading, saving, fetching, closing)",
9236
+ type: "boolean",
9237
+ },
9238
+ allowPastVerbBoolean: {
9239
+ default: false,
9240
+ description: "Allow past verb boolean props without prefix (e.g., disabled, selected, checked, opened)",
9241
+ type: "boolean",
9242
+ },
9243
+ booleanPrefixes: {
9244
+ description: "Replace default boolean prefixes entirely. If not provided, defaults are used with extendBooleanPrefixes",
9245
+ items: { type: "string" },
9246
+ type: "array",
9247
+ },
9248
+ callbackPrefix: {
9249
+ default: "on",
9250
+ description: "Required prefix for callback props",
9251
+ type: "string",
9252
+ },
9253
+ extendBooleanPrefixes: {
9254
+ default: [],
9255
+ description: "Add additional prefixes to the defaults (is, has, with, without)",
9256
+ items: { type: "string" },
9257
+ type: "array",
9258
+ },
9259
+ },
9260
+ type: "object",
9261
+ },
9262
+ ],
9263
+ type: "suggestion",
9264
+ },
9265
+ };
9266
+
9267
+ /**
9268
+ * ───────────────────────────────────────────────────────────────
9269
+ * Rule: JSX Simple Element On One Line
9270
+ * ───────────────────────────────────────────────────────────────
9271
+ *
9272
+ * Description:
9273
+ * Simple JSX elements with only a single text or expression
9274
+ * child should be collapsed onto a single line.
9275
+ *
9276
+ * ✓ Good:
9277
+ * <Button>{buttonLinkText}</Button>
9278
+ * <Title>Hello</Title>
9279
+ *
9280
+ * ✗ Bad:
9281
+ * <Button>
9282
+ * {buttonLinkText}
9283
+ * </Button>
9284
+ */
9285
+ const jsxSimpleElementOneLine = {
9286
+ create(context) {
9287
+ const sourceCode = context.sourceCode || context.getSourceCode();
9288
+
9289
+ // Check if an argument is simple (identifier, literal, or simple member expression)
9290
+ const isSimpleArgHandler = (node) => {
9291
+ if (!node) return false;
9292
+ if (node.type === "Identifier") return true;
9293
+ if (node.type === "Literal") return true;
9294
+ if (node.type === "MemberExpression") {
9295
+ return node.object.type === "Identifier" && node.property.type === "Identifier";
9296
+ }
9297
+
9298
+ return false;
9299
+ };
9300
+
9301
+ // Check if an expression is simple (identifier, literal, member expression, or simple function call)
9302
+ const isSimpleExpressionHandler = (node) => {
9303
+ if (!node) return false;
9304
+
9305
+ if (node.type === "Identifier") return true;
9306
+ if (node.type === "Literal") return true;
9307
+ if (node.type === "MemberExpression") {
9308
+ // Allow simple member expressions like obj.prop
9309
+ return node.object.type === "Identifier" && node.property.type === "Identifier";
9310
+ }
9311
+
9312
+ // Allow simple function calls with 0-1 simple arguments
9313
+ if (node.type === "CallExpression") {
9314
+ // Check callee is simple (identifier or member expression)
9315
+ const { callee } = node;
9316
+ const isSimpleCallee = callee.type === "Identifier" ||
9317
+ (callee.type === "MemberExpression" &&
9318
+ callee.object.type === "Identifier" &&
9319
+ callee.property.type === "Identifier");
9320
+
9321
+ if (!isSimpleCallee) return false;
9322
+
9323
+ // Allow 0-1 arguments, and the argument must be simple
9324
+ if (node.arguments.length === 0) return true;
9325
+ if (node.arguments.length === 1 && isSimpleArgHandler(node.arguments[0])) return true;
9326
+
9327
+ return false;
9328
+ }
9329
+
9330
+ return false;
9331
+ };
9332
+
9333
+ // Check if child is simple (text or simple expression)
9334
+ const isSimpleChildHandler = (child) => {
9335
+ if (child.type === "JSXText") {
9336
+ return child.value.trim().length > 0;
9337
+ }
9338
+
9339
+ if (child.type === "JSXExpressionContainer") {
9340
+ return isSimpleExpressionHandler(child.expression);
9341
+ }
9342
+
9343
+ return false;
9344
+ };
9345
+
9346
+ return {
9347
+ JSXElement(node) {
9348
+ const openingTag = node.openingElement;
9349
+ const closingTag = node.closingElement;
9350
+
9351
+ // Skip self-closing elements
9352
+ if (!closingTag) return;
9353
+
9354
+ // Check if element is multiline
9355
+ if (openingTag.loc.end.line === closingTag.loc.start.line) return;
9356
+
9357
+ const { children } = node;
9358
+
9359
+ // Filter out whitespace-only text children
9360
+ const significantChildren = children.filter((child) => {
9361
+ if (child.type === "JSXText") {
9362
+ return child.value.trim() !== "";
9363
+ }
9364
+
9365
+ return true;
9366
+ });
9367
+
9368
+ // Must have exactly one simple child
9369
+ if (significantChildren.length !== 1) return;
9370
+
9371
+ const child = significantChildren[0];
9372
+
9373
+ if (!isSimpleChildHandler(child)) return;
9374
+
9375
+ // Check if opening tag itself is simple (single line, not too many attributes)
9376
+ if (openingTag.loc.start.line !== openingTag.loc.end.line) return;
9377
+
9378
+ // Get the text content
9379
+ const openingText = sourceCode.getText(openingTag);
9380
+ const childText = child.type === "JSXText" ? child.value.trim() : sourceCode.getText(child);
9381
+ const closingText = sourceCode.getText(closingTag);
9382
+
9383
+ context.report({
9384
+ fix: (fixer) => fixer.replaceText(
9385
+ node,
9386
+ `${openingText}${childText}${closingText}`,
9387
+ ),
9388
+ message: "Simple JSX element with single text/expression child should be on one line",
9389
+ node,
9390
+ });
9391
+ },
9392
+ };
9393
+ },
9394
+ meta: {
9395
+ docs: { description: "Simple JSX elements with only text/expression children should be on one line" },
9396
+ fixable: "whitespace",
8691
9397
  schema: [],
8692
9398
  type: "layout",
8693
9399
  },
@@ -11141,39 +11847,42 @@ const noEmptyLinesInFunctionParams = {
11141
11847
  const firstParam = params[0];
11142
11848
  const lastParam = params[params.length - 1];
11143
11849
 
11144
- // Find opening paren (could be after async keyword for async functions)
11145
- let openParen = sourceCode.getFirstToken(node);
11146
-
11147
- while (openParen && openParen.value !== "(") {
11148
- openParen = sourceCode.getTokenAfter(openParen);
11149
- }
11150
-
11151
- if (!openParen) return;
11152
-
11153
- const closeParen = sourceCode.getTokenAfter(lastParam, (t) => t.value === ")");
11154
-
11155
- if (!closeParen) return;
11156
-
11157
- if (firstParam.loc.start.line - openParen.loc.end.line > 1) {
11158
- context.report({
11159
- fix: (fixer) => fixer.replaceTextRange(
11160
- [openParen.range[1], firstParam.range[0]],
11161
- "\n" + " ".repeat(firstParam.loc.start.column),
11162
- ),
11163
- message: "No empty line after opening parenthesis in function parameters",
11164
- node: firstParam,
11165
- });
11166
- }
11850
+ // Find opening paren - must be WITHIN this function's range (not from an outer call expression)
11851
+ const tokenBeforeFirstParam = sourceCode.getTokenBefore(firstParam);
11852
+ // Check that the ( is within this function's range (not from .map( or similar)
11853
+ const hasParenAroundParams = tokenBeforeFirstParam
11854
+ && tokenBeforeFirstParam.value === "("
11855
+ && tokenBeforeFirstParam.range[0] >= node.range[0];
11856
+
11857
+ // Only check open/close paren spacing if params are wrapped in parentheses
11858
+ if (hasParenAroundParams) {
11859
+ const openParen = tokenBeforeFirstParam;
11860
+ const closeParen = sourceCode.getTokenAfter(lastParam);
11861
+
11862
+ // Verify closeParen is actually a ) right after lastParam AND within this function's range
11863
+ if (closeParen && closeParen.value === ")" && closeParen.range[1] <= (node.body ? node.body.range[0] : node.range[1])) {
11864
+ if (firstParam.loc.start.line - openParen.loc.end.line > 1) {
11865
+ context.report({
11866
+ fix: (fixer) => fixer.replaceTextRange(
11867
+ [openParen.range[1], firstParam.range[0]],
11868
+ "\n" + " ".repeat(firstParam.loc.start.column),
11869
+ ),
11870
+ message: "No empty line after opening parenthesis in function parameters",
11871
+ node: firstParam,
11872
+ });
11873
+ }
11167
11874
 
11168
- if (closeParen.loc.start.line - lastParam.loc.end.line > 1) {
11169
- context.report({
11170
- fix: (fixer) => fixer.replaceTextRange(
11171
- [lastParam.range[1], closeParen.range[0]],
11172
- "\n" + " ".repeat(closeParen.loc.start.column),
11173
- ),
11174
- message: "No empty line before closing parenthesis in function parameters",
11175
- node: lastParam,
11176
- });
11875
+ if (closeParen.loc.start.line - lastParam.loc.end.line > 1) {
11876
+ context.report({
11877
+ fix: (fixer) => fixer.replaceTextRange(
11878
+ [lastParam.range[1], closeParen.range[0]],
11879
+ "\n" + " ".repeat(closeParen.loc.start.column),
11880
+ ),
11881
+ message: "No empty line before closing parenthesis in function parameters",
11882
+ node: lastParam,
11883
+ });
11884
+ }
11885
+ }
11177
11886
  }
11178
11887
 
11179
11888
  for (let i = 0; i < params.length - 1; i += 1) {
@@ -15726,7 +16435,7 @@ const variableNamingConvention = {
15726
16435
 
15727
16436
  const name = node.key.name;
15728
16437
 
15729
- if (name.startsWith("_") || constantRegex.test(name) || allowedIdentifiers.includes(name)) return;
16438
+ if (name.startsWith("_") || allowedIdentifiers.includes(name)) return;
15730
16439
 
15731
16440
  // Allow PascalCase for properties that hold component references
15732
16441
  // e.g., Icon: AdminPanelSettingsIcon, FormComponent: UpdateEventForm
@@ -15746,8 +16455,11 @@ const variableNamingConvention = {
15746
16455
  if (name.startsWith("Mui")) return;
15747
16456
 
15748
16457
  if (!camelCaseRegex.test(name)) {
16458
+ const camelCaseName = toCamelCaseHandler(name);
16459
+
15749
16460
  context.report({
15750
- message: `Property "${name}" should be camelCase`,
16461
+ fix: (fixer) => fixer.replaceText(node.key, camelCaseName),
16462
+ message: `Property "${name}" should be camelCase (e.g., ${camelCaseName} instead of ${name})`,
15751
16463
  node: node.key,
15752
16464
  });
15753
16465
  }
@@ -16540,24 +17252,52 @@ const functionObjectDestructure = {
16540
17252
  if (param.type !== "Identifier") return;
16541
17253
 
16542
17254
  const paramName = param.name;
17255
+
17256
+ // Check if param is used in a spread operation - skip because destructuring would break it
17257
+ let usedInSpread = false;
17258
+ const checkSpread = (n, parent) => {
17259
+ if (!n || typeof n !== "object") return;
17260
+ if (n.type === "SpreadElement" && n.argument && n.argument.type === "Identifier" && n.argument.name === paramName) {
17261
+ usedInSpread = true;
17262
+
17263
+ return;
17264
+ }
17265
+ for (const key of Object.keys(n)) {
17266
+ if (key === "parent") continue;
17267
+ const child = n[key];
17268
+ if (Array.isArray(child)) child.forEach((c) => checkSpread(c, n));
17269
+ else if (child && typeof child === "object" && child.type) checkSpread(child, n);
17270
+ }
17271
+ };
17272
+ checkSpread(body, null);
17273
+
17274
+ if (usedInSpread) return;
17275
+
16543
17276
  const accesses = findObjectAccessesHandler(body, paramName);
16544
17277
 
16545
17278
  if (accesses.length > 0) {
16546
17279
  const accessedProps = [...new Set(accesses.map((a) => a.property))];
16547
17280
 
16548
- // Count all references to paramName in the body to check if it's used beyond dot notation
17281
+ // Count all actual references to paramName (excluding object property keys)
16549
17282
  const allRefs = [];
16550
- const countRefs = (n) => {
17283
+ const countRefs = (n, parent) => {
16551
17284
  if (!n || typeof n !== "object") return;
16552
- if (n.type === "Identifier" && n.name === paramName) allRefs.push(n);
17285
+ if (n.type === "Identifier" && n.name === paramName) {
17286
+ // Exclude object property keys (non-computed)
17287
+ const isPropertyKey = parent && parent.type === "Property" && parent.key === n && !parent.computed;
17288
+
17289
+ if (!isPropertyKey) {
17290
+ allRefs.push(n);
17291
+ }
17292
+ }
16553
17293
  for (const key of Object.keys(n)) {
16554
17294
  if (key === "parent") continue;
16555
17295
  const child = n[key];
16556
- if (Array.isArray(child)) child.forEach(countRefs);
16557
- else if (child && typeof child === "object" && child.type) countRefs(child);
17296
+ if (Array.isArray(child)) child.forEach((c) => countRefs(c, n));
17297
+ else if (child && typeof child === "object" && child.type) countRefs(child, n);
16558
17298
  }
16559
17299
  };
16560
- countRefs(body);
17300
+ countRefs(body, null);
16561
17301
 
16562
17302
  // Only auto-fix if all references are covered by the detected dot notation accesses
16563
17303
  const canAutoFix = allRefs.length === accesses.length;
@@ -17872,28 +18612,178 @@ const componentPropsInlineType = {
17872
18612
  * );
17873
18613
  *
17874
18614
  * ✗ Bad:
17875
- * // Returns SVG but doesn't end with "Icon"
17876
- * export const Success = ({ className }: { className?: string }) => (
17877
- * <svg className={className}>...</svg>
17878
- * );
18615
+ * // Returns SVG but doesn't end with "Icon"
18616
+ * export const Success = ({ className }: { className?: string }) => (
18617
+ * <svg className={className}>...</svg>
18618
+ * );
18619
+ *
18620
+ * // Ends with "Icon" but doesn't return SVG
18621
+ * export const ButtonIcon = ({ children }: { children: ReactNode }) => (
18622
+ * <button>{children}</button>
18623
+ * );
18624
+ */
18625
+ const svgComponentIconNaming = {
18626
+ create(context) {
18627
+ // Get the component name from node
18628
+ const getComponentNameHandler = (node) => {
18629
+ // Arrow function: const Name = () => ...
18630
+ if (node.parent && node.parent.type === "VariableDeclarator" && node.parent.id && node.parent.id.type === "Identifier") {
18631
+ return node.parent.id.name;
18632
+ }
18633
+
18634
+ // Function declaration: function Name() { ... }
18635
+ if (node.id && node.id.type === "Identifier") {
18636
+ return node.id.name;
18637
+ }
18638
+
18639
+ return null;
18640
+ };
18641
+
18642
+ // Check if component name starts with uppercase (React component convention)
18643
+ const isReactComponentNameHandler = (name) => name && /^[A-Z]/.test(name);
18644
+
18645
+ // Check if name ends with "Icon"
18646
+ const hasIconSuffixHandler = (name) => name && name.endsWith("Icon");
18647
+
18648
+ // Check if the return value is purely an SVG element
18649
+ const returnsSvgOnlyHandler = (node) => {
18650
+ const body = node.body;
18651
+
18652
+ if (!body) return false;
18653
+
18654
+ // Arrow function with expression body: () => <svg>...</svg>
18655
+ if (body.type === "JSXElement") {
18656
+ return body.openingElement && body.openingElement.name && body.openingElement.name.name === "svg";
18657
+ }
18658
+
18659
+ // Arrow function with parenthesized expression: () => (<svg>...</svg>)
18660
+ if (body.type === "ParenthesizedExpression" && body.expression) {
18661
+ if (body.expression.type === "JSXElement") {
18662
+ return body.expression.openingElement && body.expression.openingElement.name && body.expression.openingElement.name.name === "svg";
18663
+ }
18664
+ }
18665
+
18666
+ // Block body with return statement: () => { return <svg>...</svg>; }
18667
+ if (body.type === "BlockStatement") {
18668
+ // Find all return statements
18669
+ const returnStatements = body.body.filter((stmt) => stmt.type === "ReturnStatement" && stmt.argument);
18670
+
18671
+ // Should have exactly one return statement for a simple SVG component
18672
+ if (returnStatements.length === 1) {
18673
+ const returnArg = returnStatements[0].argument;
18674
+
18675
+ if (returnArg.type === "JSXElement") {
18676
+ return returnArg.openingElement && returnArg.openingElement.name && returnArg.openingElement.name.name === "svg";
18677
+ }
18678
+
18679
+ // Parenthesized: return (<svg>...</svg>);
18680
+ if (returnArg.type === "ParenthesizedExpression" && returnArg.expression && returnArg.expression.type === "JSXElement") {
18681
+ return returnArg.expression.openingElement && returnArg.expression.openingElement.name && returnArg.expression.openingElement.name.name === "svg";
18682
+ }
18683
+ }
18684
+ }
18685
+
18686
+ return false;
18687
+ };
18688
+
18689
+ const checkFunctionHandler = (node) => {
18690
+ const componentName = getComponentNameHandler(node);
18691
+
18692
+ // Only check React components (PascalCase)
18693
+ if (!isReactComponentNameHandler(componentName)) return;
18694
+
18695
+ const returnsSvg = returnsSvgOnlyHandler(node);
18696
+ const hasIconSuffix = hasIconSuffixHandler(componentName);
18697
+
18698
+ // Case 1: Returns SVG but doesn't end with "Icon"
18699
+ if (returnsSvg && !hasIconSuffix) {
18700
+ context.report({
18701
+ message: `Component "${componentName}" returns an SVG element and should end with "Icon" suffix (e.g., "${componentName}Icon")`,
18702
+ node: node.parent && node.parent.type === "VariableDeclarator" ? node.parent.id : node.id || node,
18703
+ });
18704
+ }
18705
+
18706
+ // Case 2: Ends with "Icon" but doesn't return SVG
18707
+ if (hasIconSuffix && !returnsSvg) {
18708
+ context.report({
18709
+ message: `Component "${componentName}" has "Icon" suffix but doesn't return an SVG element. Either rename it or make it return an SVG.`,
18710
+ node: node.parent && node.parent.type === "VariableDeclarator" ? node.parent.id : node.id || node,
18711
+ });
18712
+ }
18713
+ };
18714
+
18715
+ return {
18716
+ ArrowFunctionExpression: checkFunctionHandler,
18717
+ FunctionDeclaration: checkFunctionHandler,
18718
+ FunctionExpression: checkFunctionHandler,
18719
+ };
18720
+ },
18721
+ meta: {
18722
+ docs: { description: "Enforce SVG components to have 'Icon' suffix and vice versa" },
18723
+ fixable: null,
18724
+ schema: [],
18725
+ type: "suggestion",
18726
+ },
18727
+ };
18728
+
18729
+ /**
18730
+ * ───────────────────────────────────────────────────────────────
18731
+ * Rule: Folder Component Suffix
18732
+ * ───────────────────────────────────────────────────────────────
18733
+ *
18734
+ * Description:
18735
+ * Enforces naming conventions for components based on folder location:
18736
+ * - Components in "views" folder must end with "View" suffix
18737
+ * - Components in "pages" folder must end with "Page" suffix
18738
+ *
18739
+ * ✓ Good:
18740
+ * // In views/dashboard-view.tsx:
18741
+ * export const DashboardView = () => <div>Dashboard</div>;
18742
+ *
18743
+ * // In pages/home-page.tsx:
18744
+ * export const HomePage = () => <div>Home</div>;
18745
+ *
18746
+ * ✗ Bad:
18747
+ * // In views/dashboard.tsx:
18748
+ * export const Dashboard = () => <div>Dashboard</div>; // Should be "DashboardView"
17879
18749
  *
17880
- * // Ends with "Icon" but doesn't return SVG
17881
- * export const ButtonIcon = ({ children }: { children: ReactNode }) => (
17882
- * <button>{children}</button>
17883
- * );
18750
+ * // In pages/home.tsx:
18751
+ * export const Home = () => <div>Home</div>; // Should be "HomePage"
17884
18752
  */
17885
- const svgComponentIconNaming = {
18753
+ const folderComponentSuffix = {
17886
18754
  create(context) {
18755
+ const filename = context.filename || context.getFilename();
18756
+ const normalizedFilename = filename.replace(/\\/g, "/");
18757
+
18758
+ // Folder-to-suffix mapping
18759
+ const folderSuffixMap = {
18760
+ pages: "Page",
18761
+ views: "View",
18762
+ };
18763
+
18764
+ // Check which folder the file is in
18765
+ const getFolderSuffixHandler = () => {
18766
+ for (const [folder, suffix] of Object.entries(folderSuffixMap)) {
18767
+ const pattern = new RegExp(`/${folder}/[^/]+\\.(jsx?|tsx?)$`);
18768
+
18769
+ if (pattern.test(normalizedFilename)) {
18770
+ return { folder, suffix };
18771
+ }
18772
+ }
18773
+
18774
+ return null;
18775
+ };
18776
+
17887
18777
  // Get the component name from node
17888
18778
  const getComponentNameHandler = (node) => {
17889
18779
  // Arrow function: const Name = () => ...
17890
18780
  if (node.parent && node.parent.type === "VariableDeclarator" && node.parent.id && node.parent.id.type === "Identifier") {
17891
- return node.parent.id.name;
18781
+ return { name: node.parent.id.name, identifierNode: node.parent.id };
17892
18782
  }
17893
18783
 
17894
18784
  // Function declaration: function Name() { ... }
17895
18785
  if (node.id && node.id.type === "Identifier") {
17896
- return node.id.name;
18786
+ return { name: node.id.name, identifierNode: node.id };
17897
18787
  }
17898
18788
 
17899
18789
  return null;
@@ -17902,72 +18792,115 @@ const svgComponentIconNaming = {
17902
18792
  // Check if component name starts with uppercase (React component convention)
17903
18793
  const isReactComponentNameHandler = (name) => name && /^[A-Z]/.test(name);
17904
18794
 
17905
- // Check if name ends with "Icon"
17906
- const hasIconSuffixHandler = (name) => name && name.endsWith("Icon");
17907
-
17908
- // Check if the return value is purely an SVG element
17909
- const returnsSvgOnlyHandler = (node) => {
18795
+ // Check if the function returns JSX
18796
+ const returnsJsxHandler = (node) => {
17910
18797
  const body = node.body;
17911
18798
 
17912
18799
  if (!body) return false;
17913
18800
 
17914
- // Arrow function with expression body: () => <svg>...</svg>
17915
- if (body.type === "JSXElement") {
17916
- return body.openingElement && body.openingElement.name && body.openingElement.name.name === "svg";
18801
+ // Arrow function with expression body: () => <div>...</div>
18802
+ if (body.type === "JSXElement" || body.type === "JSXFragment") {
18803
+ return true;
17917
18804
  }
17918
18805
 
17919
- // Arrow function with parenthesized expression: () => (<svg>...</svg>)
18806
+ // Parenthesized expression
17920
18807
  if (body.type === "ParenthesizedExpression" && body.expression) {
17921
- if (body.expression.type === "JSXElement") {
17922
- return body.expression.openingElement && body.expression.openingElement.name && body.expression.openingElement.name.name === "svg";
18808
+ if (body.expression.type === "JSXElement" || body.expression.type === "JSXFragment") {
18809
+ return true;
17923
18810
  }
17924
18811
  }
17925
18812
 
17926
- // Block body with return statement: () => { return <svg>...</svg>; }
18813
+ // Block body with return statement
17927
18814
  if (body.type === "BlockStatement") {
17928
- // Find all return statements
17929
- const returnStatements = body.body.filter((stmt) => stmt.type === "ReturnStatement" && stmt.argument);
17930
-
17931
- // Should have exactly one return statement for a simple SVG component
17932
- if (returnStatements.length === 1) {
17933
- const returnArg = returnStatements[0].argument;
18815
+ const hasJsxReturn = body.body.some((stmt) => {
18816
+ if (stmt.type === "ReturnStatement" && stmt.argument) {
18817
+ const arg = stmt.argument;
17934
18818
 
17935
- if (returnArg.type === "JSXElement") {
17936
- return returnArg.openingElement && returnArg.openingElement.name && returnArg.openingElement.name.name === "svg";
18819
+ return arg.type === "JSXElement" || arg.type === "JSXFragment"
18820
+ || (arg.type === "ParenthesizedExpression" && arg.expression
18821
+ && (arg.expression.type === "JSXElement" || arg.expression.type === "JSXFragment"));
17937
18822
  }
17938
18823
 
17939
- // Parenthesized: return (<svg>...</svg>);
17940
- if (returnArg.type === "ParenthesizedExpression" && returnArg.expression && returnArg.expression.type === "JSXElement") {
17941
- return returnArg.expression.openingElement && returnArg.expression.openingElement.name && returnArg.expression.openingElement.name.name === "svg";
17942
- }
17943
- }
18824
+ return false;
18825
+ });
18826
+
18827
+ return hasJsxReturn;
17944
18828
  }
17945
18829
 
17946
18830
  return false;
17947
18831
  };
17948
18832
 
17949
18833
  const checkFunctionHandler = (node) => {
17950
- const componentName = getComponentNameHandler(node);
18834
+ const folderInfo = getFolderSuffixHandler();
18835
+
18836
+ // Not in a folder that requires specific suffix
18837
+ if (!folderInfo) return;
18838
+
18839
+ const componentInfo = getComponentNameHandler(node);
18840
+
18841
+ if (!componentInfo) return;
18842
+
18843
+ const { name, identifierNode } = componentInfo;
17951
18844
 
17952
18845
  // Only check React components (PascalCase)
17953
- if (!isReactComponentNameHandler(componentName)) return;
18846
+ if (!isReactComponentNameHandler(name)) return;
17954
18847
 
17955
- const returnsSvg = returnsSvgOnlyHandler(node);
17956
- const hasIconSuffix = hasIconSuffixHandler(componentName);
18848
+ // Only check functions that return JSX
18849
+ if (!returnsJsxHandler(node)) return;
17957
18850
 
17958
- // Case 1: Returns SVG but doesn't end with "Icon"
17959
- if (returnsSvg && !hasIconSuffix) {
17960
- context.report({
17961
- message: `Component "${componentName}" returns an SVG element and should end with "Icon" suffix (e.g., "${componentName}Icon")`,
17962
- node: node.parent && node.parent.type === "VariableDeclarator" ? node.parent.id : node.id || node,
17963
- });
17964
- }
18851
+ const { folder, suffix } = folderInfo;
18852
+
18853
+ // Check if component name ends with the required suffix
18854
+ if (!name.endsWith(suffix)) {
18855
+ const newName = `${name}${suffix}`;
17965
18856
 
17966
- // Case 2: Ends with "Icon" but doesn't return SVG
17967
- if (hasIconSuffix && !returnsSvg) {
17968
18857
  context.report({
17969
- message: `Component "${componentName}" has "Icon" suffix but doesn't return an SVG element. Either rename it or make it return an SVG.`,
17970
- node: node.parent && node.parent.type === "VariableDeclarator" ? node.parent.id : node.id || node,
18858
+ fix(fixer) {
18859
+ const scope = context.sourceCode
18860
+ ? context.sourceCode.getScope(node)
18861
+ : context.getScope();
18862
+
18863
+ // Find the variable in scope
18864
+ const findVariableHandler = (s, varName) => {
18865
+ const v = s.variables.find((variable) => variable.name === varName);
18866
+
18867
+ if (v) return v;
18868
+ if (s.upper) return findVariableHandler(s.upper, varName);
18869
+
18870
+ return null;
18871
+ };
18872
+
18873
+ const variable = findVariableHandler(scope, name);
18874
+
18875
+ if (!variable) return fixer.replaceText(identifierNode, newName);
18876
+
18877
+ const fixes = [];
18878
+ const fixedRanges = new Set();
18879
+
18880
+ // Fix definition
18881
+ variable.defs.forEach((def) => {
18882
+ const rangeKey = `${def.name.range[0]}-${def.name.range[1]}`;
18883
+
18884
+ if (!fixedRanges.has(rangeKey)) {
18885
+ fixedRanges.add(rangeKey);
18886
+ fixes.push(fixer.replaceText(def.name, newName));
18887
+ }
18888
+ });
18889
+
18890
+ // Fix all references
18891
+ variable.references.forEach((ref) => {
18892
+ const rangeKey = `${ref.identifier.range[0]}-${ref.identifier.range[1]}`;
18893
+
18894
+ if (!fixedRanges.has(rangeKey)) {
18895
+ fixedRanges.add(rangeKey);
18896
+ fixes.push(fixer.replaceText(ref.identifier, newName));
18897
+ }
18898
+ });
18899
+
18900
+ return fixes;
18901
+ },
18902
+ message: `Component "${name}" in "${folder}" folder must end with "${suffix}" suffix (e.g., "${newName}")`,
18903
+ node: identifierNode,
17971
18904
  });
17972
18905
  }
17973
18906
  };
@@ -17979,8 +18912,8 @@ const svgComponentIconNaming = {
17979
18912
  };
17980
18913
  },
17981
18914
  meta: {
17982
- docs: { description: "Enforce SVG components to have 'Icon' suffix and vice versa" },
17983
- fixable: null,
18915
+ docs: { description: "Enforce components in 'views' folder end with 'View' and components in 'pages' folder end with 'Page'" },
18916
+ fixable: "code",
17984
18917
  schema: [],
17985
18918
  type: "suggestion",
17986
18919
  },
@@ -18238,10 +19171,32 @@ const noInlineTypeDefinitions = {
18238
19171
  const typeFormat = {
18239
19172
  create(context) {
18240
19173
  const sourceCode = context.sourceCode || context.getSourceCode();
19174
+ const options = context.options[0] || {};
19175
+ const minUnionMembersForMultiline = options.minUnionMembersForMultiline !== undefined ? options.minUnionMembersForMultiline : 5;
18241
19176
 
18242
19177
  const pascalCaseRegex = /^[A-Z][a-zA-Z0-9]*$/;
18243
19178
  const camelCaseRegex = /^[a-z][a-zA-Z0-9]*$/;
18244
19179
 
19180
+ // Convert PascalCase/SCREAMING_SNAKE_CASE/snake_case to camelCase
19181
+ const toCamelCaseHandler = (name) => {
19182
+ // Handle SCREAMING_SNAKE_CASE (e.g., USER_NAME -> userName)
19183
+ if (/^[A-Z][A-Z0-9_]*$/.test(name)) {
19184
+ return name.toLowerCase().replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
19185
+ }
19186
+
19187
+ // Handle snake_case (e.g., user_name -> userName)
19188
+ if (/_/.test(name)) {
19189
+ return name.toLowerCase().replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
19190
+ }
19191
+
19192
+ // Handle PascalCase (e.g., UserName -> userName)
19193
+ if (/^[A-Z]/.test(name)) {
19194
+ return name[0].toLowerCase() + name.slice(1);
19195
+ }
19196
+
19197
+ return name;
19198
+ };
19199
+
18245
19200
  const checkTypeLiteralHandler = (declarationNode, typeLiteralNode, members) => {
18246
19201
  if (members.length === 0) return;
18247
19202
 
@@ -18300,13 +19255,46 @@ const typeFormat = {
18300
19255
  const propName = member.key.name;
18301
19256
 
18302
19257
  if (!camelCaseRegex.test(propName)) {
19258
+ const fixedName = toCamelCaseHandler(propName);
19259
+
18303
19260
  context.report({
18304
- message: `Type property "${propName}" must be camelCase`,
19261
+ fix: (fixer) => fixer.replaceText(member.key, fixedName),
19262
+ message: `Type property "${propName}" must be camelCase. Use "${fixedName}" instead.`,
18305
19263
  node: member.key,
18306
19264
  });
18307
19265
  }
18308
19266
  }
18309
19267
 
19268
+ // Collapse single-member nested object types to one line
19269
+ if (member.type === "TSPropertySignature" && member.typeAnnotation?.typeAnnotation?.type === "TSTypeLiteral") {
19270
+ const nestedType = member.typeAnnotation.typeAnnotation;
19271
+
19272
+ if (nestedType.members && nestedType.members.length === 1) {
19273
+ const nestedOpenBrace = sourceCode.getFirstToken(nestedType);
19274
+ const nestedCloseBrace = sourceCode.getLastToken(nestedType);
19275
+ const isNestedMultiLine = nestedOpenBrace.loc.end.line !== nestedCloseBrace.loc.start.line;
19276
+
19277
+ if (isNestedMultiLine) {
19278
+ const nestedMember = nestedType.members[0];
19279
+ let nestedMemberText = sourceCode.getText(nestedMember).trim();
19280
+
19281
+ // Remove trailing punctuation
19282
+ if (nestedMemberText.endsWith(",") || nestedMemberText.endsWith(";")) {
19283
+ nestedMemberText = nestedMemberText.slice(0, -1);
19284
+ }
19285
+
19286
+ context.report({
19287
+ fix: (fixer) => fixer.replaceTextRange(
19288
+ [nestedOpenBrace.range[0], nestedCloseBrace.range[1]],
19289
+ `{ ${nestedMemberText} }`,
19290
+ ),
19291
+ message: "Single property nested object type should be on one line",
19292
+ node: nestedType,
19293
+ });
19294
+ }
19295
+ }
19296
+ }
19297
+
18310
19298
  // Check for space before ? in optional properties
18311
19299
  if (member.type === "TSPropertySignature" && member.optional) {
18312
19300
  const keyToken = sourceCode.getFirstToken(member);
@@ -18674,13 +19662,166 @@ const typeFormat = {
18674
19662
  }
18675
19663
  });
18676
19664
  }
19665
+
19666
+ // Check union types formatting (e.g., "a" | "b" | "c")
19667
+ if (node.typeAnnotation && node.typeAnnotation.type === "TSUnionType") {
19668
+ const unionType = node.typeAnnotation;
19669
+ const types = unionType.types;
19670
+ const minMembersForMultiline = minUnionMembersForMultiline;
19671
+
19672
+ // Get line info
19673
+ const typeLine = sourceCode.lines[node.loc.start.line - 1];
19674
+ const baseIndent = typeLine.match(/^\s*/)[0];
19675
+ const memberIndent = baseIndent + " ";
19676
+
19677
+ // Get the = token
19678
+ const equalToken = sourceCode.getTokenAfter(node.id);
19679
+ const firstType = types[0];
19680
+ const lastType = types[types.length - 1];
19681
+
19682
+ // Check if currently on single line
19683
+ const isCurrentlySingleLine = firstType.loc.start.line === lastType.loc.end.line &&
19684
+ equalToken.loc.end.line === firstType.loc.start.line;
19685
+
19686
+ // Check if currently properly multiline (= on its own conceptually, first type on new line)
19687
+ const isFirstTypeOnNewLine = firstType.loc.start.line > equalToken.loc.end.line;
19688
+
19689
+ if (types.length >= minMembersForMultiline) {
19690
+ // Should be multiline format
19691
+ // Check if needs reformatting
19692
+ let needsReformat = false;
19693
+
19694
+ // Check if first type is on new line after =
19695
+ if (!isFirstTypeOnNewLine) {
19696
+ needsReformat = true;
19697
+ }
19698
+
19699
+ // Check if each type is on its own line
19700
+ if (!needsReformat) {
19701
+ for (let i = 1; i < types.length; i++) {
19702
+ if (types[i].loc.start.line === types[i - 1].loc.end.line) {
19703
+ needsReformat = true;
19704
+ break;
19705
+ }
19706
+ }
19707
+ }
19708
+
19709
+ // Check proper indentation and | placement
19710
+ if (!needsReformat) {
19711
+ for (let i = 1; i < types.length; i++) {
19712
+ const pipeToken = sourceCode.getTokenBefore(types[i]);
19713
+
19714
+ if (pipeToken && pipeToken.value === "|") {
19715
+ // | should be at start of line (after indent)
19716
+ if (pipeToken.loc.start.line !== types[i].loc.start.line) {
19717
+ needsReformat = true;
19718
+ break;
19719
+ }
19720
+ }
19721
+ }
19722
+ }
19723
+
19724
+ if (needsReformat) {
19725
+ // Build the correct multiline format
19726
+ const formattedTypes = types.map((type, index) => {
19727
+ const typeText = sourceCode.getText(type);
19728
+
19729
+ if (index === 0) {
19730
+ return memberIndent + typeText;
19731
+ }
19732
+
19733
+ return memberIndent + "| " + typeText;
19734
+ }).join("\n");
19735
+
19736
+ const newTypeText = `= \n${formattedTypes}`;
19737
+
19738
+ context.report({
19739
+ fix: (fixer) => fixer.replaceTextRange(
19740
+ [equalToken.range[0], lastType.range[1]],
19741
+ newTypeText,
19742
+ ),
19743
+ message: `Union type with ${types.length} members should be multiline with each member on its own line`,
19744
+ node: unionType,
19745
+ });
19746
+ }
19747
+ } else {
19748
+ // Should be single line format (less than 5 members)
19749
+ if (!isCurrentlySingleLine) {
19750
+ // Build single line format
19751
+ const typeTexts = types.map((type) => sourceCode.getText(type));
19752
+ const singleLineText = `= ${typeTexts.join(" | ")}`;
19753
+
19754
+ context.report({
19755
+ fix: (fixer) => fixer.replaceTextRange(
19756
+ [equalToken.range[0], lastType.range[1]],
19757
+ singleLineText,
19758
+ ),
19759
+ message: `Union type with ${types.length} members should be on a single line`,
19760
+ node: unionType,
19761
+ });
19762
+ }
19763
+ }
19764
+ }
19765
+ },
19766
+ // Handle inline type literals (e.g., in function parameters)
19767
+ TSTypeLiteral(node) {
19768
+ // Skip if already handled by TSTypeAliasDeclaration or TSAsExpression
19769
+ if (node.parent?.type === "TSTypeAliasDeclaration") return;
19770
+ if (node.parent?.type === "TSAsExpression") return;
19771
+
19772
+ // Check for single-member nested object types that should be collapsed
19773
+ if (node.members) {
19774
+ node.members.forEach((member) => {
19775
+ if (member.type === "TSPropertySignature" && member.typeAnnotation?.typeAnnotation?.type === "TSTypeLiteral") {
19776
+ const nestedType = member.typeAnnotation.typeAnnotation;
19777
+
19778
+ if (nestedType.members && nestedType.members.length === 1) {
19779
+ const nestedOpenBrace = sourceCode.getFirstToken(nestedType);
19780
+ const nestedCloseBrace = sourceCode.getLastToken(nestedType);
19781
+ const isNestedMultiLine = nestedOpenBrace.loc.end.line !== nestedCloseBrace.loc.start.line;
19782
+
19783
+ if (isNestedMultiLine) {
19784
+ const nestedMember = nestedType.members[0];
19785
+ let nestedMemberText = sourceCode.getText(nestedMember).trim();
19786
+
19787
+ // Remove trailing punctuation
19788
+ if (nestedMemberText.endsWith(",") || nestedMemberText.endsWith(";")) {
19789
+ nestedMemberText = nestedMemberText.slice(0, -1);
19790
+ }
19791
+
19792
+ context.report({
19793
+ fix: (fixer) => fixer.replaceTextRange(
19794
+ [nestedOpenBrace.range[0], nestedCloseBrace.range[1]],
19795
+ `{ ${nestedMemberText} }`,
19796
+ ),
19797
+ message: "Single property nested object type should be on one line",
19798
+ node: nestedType,
19799
+ });
19800
+ }
19801
+ }
19802
+ }
19803
+ });
19804
+ }
18677
19805
  },
18678
19806
  };
18679
19807
  },
18680
19808
  meta: {
18681
- docs: { description: "Enforce type naming (PascalCase + Type suffix), camelCase properties, proper formatting, and trailing commas" },
19809
+ docs: { description: "Enforce type naming (PascalCase + Type suffix), camelCase properties, proper formatting, union type formatting, and trailing commas" },
18682
19810
  fixable: "code",
18683
- schema: [],
19811
+ schema: [
19812
+ {
19813
+ additionalProperties: false,
19814
+ properties: {
19815
+ minUnionMembersForMultiline: {
19816
+ default: 5,
19817
+ description: "Minimum number of union members to require multiline format",
19818
+ minimum: 2,
19819
+ type: "integer",
19820
+ },
19821
+ },
19822
+ type: "object",
19823
+ },
19824
+ ],
18684
19825
  type: "suggestion",
18685
19826
  },
18686
19827
  };
@@ -20293,14 +21434,27 @@ const enumFormat = {
20293
21434
  });
20294
21435
  }
20295
21436
 
21437
+ // Convert camelCase/PascalCase to UPPER_SNAKE_CASE
21438
+ const toUpperSnakeCaseHandler = (name) => {
21439
+ // Insert underscore before each uppercase letter (except the first)
21440
+ // Then convert to uppercase
21441
+ return name
21442
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
21443
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
21444
+ .toUpperCase();
21445
+ };
21446
+
20296
21447
  members.forEach((member, index) => {
20297
21448
  // Check member name is UPPER_CASE
20298
21449
  if (member.id && member.id.type === "Identifier") {
20299
21450
  const memberName = member.id.name;
20300
21451
 
20301
21452
  if (!upperCaseRegex.test(memberName)) {
21453
+ const fixedName = toUpperSnakeCaseHandler(memberName);
21454
+
20302
21455
  context.report({
20303
- message: `Enum member "${memberName}" must be UPPER_CASE (e.g., ${memberName.toUpperCase()})`,
21456
+ fix: (fixer) => fixer.replaceText(member.id, fixedName),
21457
+ message: `Enum member "${memberName}" must be UPPER_CASE (e.g., ${fixedName})`,
20304
21458
  node: member.id,
20305
21459
  });
20306
21460
  }
@@ -20453,6 +21607,26 @@ const interfaceFormat = {
20453
21607
  const pascalCaseRegex = /^[A-Z][a-zA-Z0-9]*$/;
20454
21608
  const camelCaseRegex = /^[a-z][a-zA-Z0-9]*$/;
20455
21609
 
21610
+ // Convert PascalCase/SCREAMING_SNAKE_CASE/snake_case to camelCase
21611
+ const toCamelCaseHandler = (name) => {
21612
+ // Handle SCREAMING_SNAKE_CASE (e.g., USER_NAME -> userName)
21613
+ if (/^[A-Z][A-Z0-9_]*$/.test(name)) {
21614
+ return name.toLowerCase().replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
21615
+ }
21616
+
21617
+ // Handle snake_case (e.g., user_name -> userName)
21618
+ if (/_/.test(name)) {
21619
+ return name.toLowerCase().replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
21620
+ }
21621
+
21622
+ // Handle PascalCase (e.g., UserName -> userName)
21623
+ if (/^[A-Z]/.test(name)) {
21624
+ return name[0].toLowerCase() + name.slice(1);
21625
+ }
21626
+
21627
+ return name;
21628
+ };
21629
+
20456
21630
  return {
20457
21631
  TSInterfaceDeclaration(node) {
20458
21632
  const interfaceName = node.id.name;
@@ -20530,17 +21704,41 @@ const interfaceFormat = {
20530
21704
  }
20531
21705
 
20532
21706
  // For single member, should be on one line without trailing punctuation
21707
+ // But skip if the property has a nested object type with 2+ members
20533
21708
  if (members.length === 1) {
20534
21709
  const member = members[0];
20535
- const memberText = sourceCode.getText(member);
20536
21710
  const isMultiLine = openBraceToken.loc.end.line !== closeBraceToken.loc.start.line;
20537
21711
 
20538
- if (isMultiLine) {
20539
- // Collapse to single line without trailing punctuation
20540
- let cleanText = memberText.trim();
21712
+ // Check if property has nested object type
21713
+ const nestedType = member.typeAnnotation?.typeAnnotation;
21714
+ const hasNestedType = nestedType?.type === "TSTypeLiteral";
21715
+ const hasMultiMemberNestedType = hasNestedType && nestedType.members?.length >= 2;
21716
+ const hasSingleMemberNestedType = hasNestedType && nestedType.members?.length === 1;
20541
21717
 
20542
- if (cleanText.endsWith(",") || cleanText.endsWith(";")) {
20543
- cleanText = cleanText.slice(0, -1);
21718
+ if (isMultiLine && !hasMultiMemberNestedType) {
21719
+ // Build the collapsed text, handling nested types specially
21720
+ let cleanText;
21721
+
21722
+ if (hasSingleMemberNestedType) {
21723
+ // Collapse nested type first, then build the member text
21724
+ const nestedMember = nestedType.members[0];
21725
+ let nestedMemberText = sourceCode.getText(nestedMember).trim();
21726
+
21727
+ if (nestedMemberText.endsWith(",") || nestedMemberText.endsWith(";")) {
21728
+ nestedMemberText = nestedMemberText.slice(0, -1);
21729
+ }
21730
+
21731
+ // Build: propName: { nestedProp: type }
21732
+ const propName = member.key.name;
21733
+ const optionalMark = member.optional ? "?" : "";
21734
+
21735
+ cleanText = `${propName}${optionalMark}: { ${nestedMemberText} }`;
21736
+ } else {
21737
+ cleanText = sourceCode.getText(member).trim();
21738
+
21739
+ if (cleanText.endsWith(",") || cleanText.endsWith(";")) {
21740
+ cleanText = cleanText.slice(0, -1);
21741
+ }
20544
21742
  }
20545
21743
 
20546
21744
  const newInterfaceText = `{ ${cleanText} }`;
@@ -20558,6 +21756,8 @@ const interfaceFormat = {
20558
21756
  }
20559
21757
 
20560
21758
  // Check for trailing comma/semicolon in single-line single member
21759
+ const memberText = sourceCode.getText(member);
21760
+
20561
21761
  if (memberText.trimEnd().endsWith(",") || memberText.trimEnd().endsWith(";")) {
20562
21762
  const punctIndex = Math.max(memberText.lastIndexOf(","), memberText.lastIndexOf(";"));
20563
21763
 
@@ -20589,13 +21789,46 @@ const interfaceFormat = {
20589
21789
  const propName = member.key.name;
20590
21790
 
20591
21791
  if (!camelCaseRegex.test(propName)) {
21792
+ const fixedName = toCamelCaseHandler(propName);
21793
+
20592
21794
  context.report({
20593
- message: `Interface property "${propName}" must be camelCase`,
21795
+ fix: (fixer) => fixer.replaceText(member.key, fixedName),
21796
+ message: `Interface property "${propName}" must be camelCase. Use "${fixedName}" instead.`,
20594
21797
  node: member.key,
20595
21798
  });
20596
21799
  }
20597
21800
  }
20598
21801
 
21802
+ // Collapse single-member nested object types to one line
21803
+ if (member.type === "TSPropertySignature" && member.typeAnnotation?.typeAnnotation?.type === "TSTypeLiteral") {
21804
+ const nestedType = member.typeAnnotation.typeAnnotation;
21805
+
21806
+ if (nestedType.members && nestedType.members.length === 1) {
21807
+ const nestedOpenBrace = sourceCode.getFirstToken(nestedType);
21808
+ const nestedCloseBrace = sourceCode.getLastToken(nestedType);
21809
+ const isNestedMultiLine = nestedOpenBrace.loc.end.line !== nestedCloseBrace.loc.start.line;
21810
+
21811
+ if (isNestedMultiLine) {
21812
+ const nestedMember = nestedType.members[0];
21813
+ let nestedMemberText = sourceCode.getText(nestedMember).trim();
21814
+
21815
+ // Remove trailing punctuation
21816
+ if (nestedMemberText.endsWith(",") || nestedMemberText.endsWith(";")) {
21817
+ nestedMemberText = nestedMemberText.slice(0, -1);
21818
+ }
21819
+
21820
+ context.report({
21821
+ fix: (fixer) => fixer.replaceTextRange(
21822
+ [nestedOpenBrace.range[0], nestedCloseBrace.range[1]],
21823
+ `{ ${nestedMemberText} }`,
21824
+ ),
21825
+ message: "Single property nested object type should be on one line",
21826
+ node: nestedType,
21827
+ });
21828
+ }
21829
+ }
21830
+ }
21831
+
20599
21832
  // Check for space before ? in optional properties
20600
21833
  if (member.type === "TSPropertySignature" && member.optional) {
20601
21834
  const keyToken = sourceCode.getFirstToken(member);
@@ -20841,6 +22074,7 @@ export default {
20841
22074
  // Component rules
20842
22075
  "component-props-destructure": componentPropsDestructure,
20843
22076
  "component-props-inline-type": componentPropsInlineType,
22077
+ "folder-component-suffix": folderComponentSuffix,
20844
22078
  "svg-component-icon-naming": svgComponentIconNaming,
20845
22079
 
20846
22080
  // React rules
@@ -20871,6 +22105,7 @@ export default {
20871
22105
  // Hook rules
20872
22106
  "hook-callback-format": hookCallbackFormat,
20873
22107
  "hook-deps-per-line": hookDepsPerLine,
22108
+ "use-state-naming-convention": useStateNamingConvention,
20874
22109
 
20875
22110
  // Import/Export rules
20876
22111
  "absolute-imports-only": absoluteImportsOnly,
@@ -20912,6 +22147,7 @@ export default {
20912
22147
  "enum-format": enumFormat,
20913
22148
  "interface-format": interfaceFormat,
20914
22149
  "no-inline-type-definitions": noInlineTypeDefinitions,
22150
+ "prop-naming-convention": propNamingConvention,
20915
22151
  "type-annotation-spacing": typeAnnotationSpacing,
20916
22152
  "type-format": typeFormat,
20917
22153
  "typescript-definition-location": typescriptDefinitionLocation,