eslint-plugin-code-style 1.11.4 → 1.13.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,
@@ -7818,16 +7825,6 @@ const jsxChildrenOnNewLine = {
7818
7825
  message: "JSX child should be on its own line",
7819
7826
  node: firstChild,
7820
7827
  });
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
7828
  }
7832
7829
 
7833
7830
  // Check if closing tag is on same line as last child
@@ -8558,6 +8555,350 @@ const jsxPropNamingConvention = {
8558
8555
  },
8559
8556
  };
8560
8557
 
8558
+ /**
8559
+ * ───────────────────────────────────────────────────────────────
8560
+ * Rule: Prop Naming Convention
8561
+ * ───────────────────────────────────────────────────────────────
8562
+ *
8563
+ * Description:
8564
+ * Enforces naming conventions for boolean and callback/method props:
8565
+ * - Boolean props must start with: is, has, with, without (followed by capital letter)
8566
+ * - Callback props must start with: on (followed by capital letter)
8567
+ *
8568
+ * Applies to: interfaces, type aliases, inline types, and nested object types
8569
+ * at any nesting level. Does NOT apply to JSX element attributes.
8570
+ *
8571
+ * Options:
8572
+ * { booleanPrefixes: ["is", "has"] } - Replace default prefixes entirely
8573
+ * { extendBooleanPrefixes: ["should", "can"] } - Add to default prefixes
8574
+ * { allowPastVerbBoolean: false } - Allow past verb booleans (disabled, selected, checked, opened, etc.)
8575
+ * { allowContinuousVerbBoolean: false } - Allow continuous verb booleans (loading, saving, closing, etc.)
8576
+ * { callbackPrefix: "on" } - Required prefix for callbacks
8577
+ * { allowActionSuffix: false } - Allow "xxxAction" pattern for callbacks
8578
+ *
8579
+ * ✓ Good:
8580
+ * interface PropsInterface {
8581
+ * isLoading: boolean,
8582
+ * hasError: boolean,
8583
+ * onClick: () => void,
8584
+ * onSubmit: (data: Data) => void,
8585
+ * config: {
8586
+ * isEnabled: boolean,
8587
+ * onToggle: () => void,
8588
+ * },
8589
+ * }
8590
+ *
8591
+ * ✗ Bad:
8592
+ * interface PropsInterface {
8593
+ * loading: boolean, // Should be isLoading
8594
+ * error: boolean, // Should be hasError
8595
+ * click: () => void, // Should be onClick
8596
+ * handleSubmit: () => void, // Should be onSubmit
8597
+ * config: {
8598
+ * enabled: boolean, // Should be isEnabled (nested)
8599
+ * toggle: () => void, // Should be onToggle (nested)
8600
+ * },
8601
+ * }
8602
+ *
8603
+ * ✓ Good (with allowPastVerbBoolean: true):
8604
+ * interface PropsInterface {
8605
+ * disabled: boolean, // Past verb - allowed
8606
+ * selected: boolean, // Past verb - allowed
8607
+ * checked: boolean, // Past verb - allowed
8608
+ * }
8609
+ *
8610
+ * ✓ Good (with allowContinuousVerbBoolean: true):
8611
+ * interface PropsInterface {
8612
+ * loading: boolean, // Continuous verb - allowed
8613
+ * saving: boolean, // Continuous verb - allowed
8614
+ * fetching: boolean, // Continuous verb - allowed
8615
+ * }
8616
+ */
8617
+ const propNamingConvention = {
8618
+ create(context) {
8619
+ const options = context.options[0] || {};
8620
+
8621
+ // Boolean prefixes handling (like module-index-exports pattern)
8622
+ const defaultBooleanPrefixes = ["is", "has", "with", "without"];
8623
+ const booleanPrefixes = options.booleanPrefixes || [
8624
+ ...defaultBooleanPrefixes,
8625
+ ...(options.extendBooleanPrefixes || []),
8626
+ ];
8627
+
8628
+ const allowPastVerbBoolean = options.allowPastVerbBoolean || false;
8629
+ const allowContinuousVerbBoolean = options.allowContinuousVerbBoolean || false;
8630
+ const callbackPrefix = options.callbackPrefix || "on";
8631
+ const allowActionSuffix = options.allowActionSuffix || false;
8632
+
8633
+ // Pattern to check if name starts with valid boolean prefix followed by capital letter
8634
+ const booleanPrefixPattern = new RegExp(`^(${booleanPrefixes.join("|")})[A-Z]`);
8635
+
8636
+ // Pattern for callback prefix
8637
+ const callbackPrefixPattern = new RegExp(`^${callbackPrefix}[A-Z]`);
8638
+
8639
+ // Pattern for past verb booleans (ends with -ed: disabled, selected, checked, opened, closed, etc.)
8640
+ const pastVerbPattern = /^[a-z]+ed$/;
8641
+
8642
+ // Pattern for continuous verb booleans (ends with -ing: loading, saving, closing, etc.)
8643
+ const continuousVerbPattern = /^[a-z]+ing$/;
8644
+
8645
+ // Words that suggest "has" prefix instead of "is"
8646
+ const hasKeywords = [
8647
+ "children", "content", "data", "error", "errors", "items",
8648
+ "permission", "permissions", "value", "values",
8649
+ ];
8650
+
8651
+ // Convert name to appropriate boolean prefix
8652
+ const toBooleanNameHandler = (name) => {
8653
+ const lowerName = name.toLowerCase();
8654
+ const prefix = hasKeywords.some((k) => lowerName.includes(k)) ? "has" : "is";
8655
+
8656
+ return prefix + name[0].toUpperCase() + name.slice(1);
8657
+ };
8658
+
8659
+ // Convert name to callback format (add "on" prefix)
8660
+ const toCallbackNameHandler = (name) => {
8661
+ // Handle "handleXxx" pattern -> "onXxx"
8662
+ if (name.startsWith("handle") && name.length > 6) {
8663
+ const rest = name.slice(6);
8664
+
8665
+ return callbackPrefix + rest[0].toUpperCase() + rest.slice(1);
8666
+ }
8667
+
8668
+ // Handle "xxxHandler" pattern -> "onXxx"
8669
+ if (name.endsWith("Handler") && name.length > 7) {
8670
+ const rest = name.slice(0, -7);
8671
+
8672
+ return callbackPrefix + rest[0].toUpperCase() + rest.slice(1);
8673
+ }
8674
+
8675
+ // Simple case: just add "on" prefix
8676
+ return callbackPrefix + name[0].toUpperCase() + name.slice(1);
8677
+ };
8678
+
8679
+ // Check if type annotation indicates boolean
8680
+ const isBooleanTypeHandler = (typeAnnotation) => {
8681
+ if (!typeAnnotation) return false;
8682
+ const type = typeAnnotation.typeAnnotation;
8683
+
8684
+ if (!type) return false;
8685
+ if (type.type === "TSBooleanKeyword") return true;
8686
+ // Check for union with boolean (e.g., boolean | undefined)
8687
+ if (type.type === "TSUnionType") {
8688
+ return type.types.some((t) => t.type === "TSBooleanKeyword");
8689
+ }
8690
+
8691
+ return false;
8692
+ };
8693
+
8694
+ // React event handler type names
8695
+ const reactEventHandlerTypes = [
8696
+ "MouseEventHandler",
8697
+ "ChangeEventHandler",
8698
+ "FormEventHandler",
8699
+ "KeyboardEventHandler",
8700
+ "FocusEventHandler",
8701
+ "TouchEventHandler",
8702
+ "PointerEventHandler",
8703
+ "DragEventHandler",
8704
+ "WheelEventHandler",
8705
+ "AnimationEventHandler",
8706
+ "TransitionEventHandler",
8707
+ "ClipboardEventHandler",
8708
+ "CompositionEventHandler",
8709
+ "UIEventHandler",
8710
+ "ScrollEventHandler",
8711
+ "EventHandler",
8712
+ ];
8713
+
8714
+ // Check if type annotation indicates function/callback
8715
+ const isCallbackTypeHandler = (typeAnnotation) => {
8716
+ if (!typeAnnotation) return false;
8717
+ const type = typeAnnotation.typeAnnotation;
8718
+
8719
+ if (!type) return false;
8720
+ if (type.type === "TSFunctionType") return true;
8721
+ if (type.type === "TSTypeReference") {
8722
+ const typeName = type.typeName?.name;
8723
+
8724
+ // Check for Function, VoidFunction, or React event handler types
8725
+ if (typeName === "Function" || typeName === "VoidFunction") return true;
8726
+ if (reactEventHandlerTypes.includes(typeName)) return true;
8727
+ }
8728
+
8729
+ // Check for union with function (e.g., (() => void) | undefined)
8730
+ if (type.type === "TSUnionType") {
8731
+ return type.types.some((t) =>
8732
+ t.type === "TSFunctionType" ||
8733
+ (t.type === "TSTypeReference" && (
8734
+ t.typeName?.name === "Function" ||
8735
+ t.typeName?.name === "VoidFunction" ||
8736
+ reactEventHandlerTypes.includes(t.typeName?.name)
8737
+ )));
8738
+ }
8739
+
8740
+ return false;
8741
+ };
8742
+
8743
+ // Check if type annotation is a nested object type (TSTypeLiteral)
8744
+ const isNestedObjectTypeHandler = (typeAnnotation) => {
8745
+ if (!typeAnnotation) return false;
8746
+ const type = typeAnnotation.typeAnnotation;
8747
+
8748
+ if (!type) return false;
8749
+
8750
+ return type.type === "TSTypeLiteral";
8751
+ };
8752
+
8753
+ // Check if name is a valid boolean prop name
8754
+ const isValidBooleanNameHandler = (name) => {
8755
+ // Starts with valid prefix
8756
+ if (booleanPrefixPattern.test(name)) return true;
8757
+
8758
+ // Allow past verb booleans if option is enabled (disabled, selected, checked, etc.)
8759
+ if (allowPastVerbBoolean && pastVerbPattern.test(name)) return true;
8760
+
8761
+ // Allow continuous verb booleans if option is enabled (loading, saving, etc.)
8762
+ if (allowContinuousVerbBoolean && continuousVerbPattern.test(name)) return true;
8763
+
8764
+ return false;
8765
+ };
8766
+
8767
+ // Check if name is a valid callback prop name
8768
+ const isValidCallbackNameHandler = (name) => {
8769
+ // Starts with "on" prefix
8770
+ if (callbackPrefixPattern.test(name)) return true;
8771
+
8772
+ // Allow "xxxAction" suffix if option is enabled
8773
+ if (allowActionSuffix && name.endsWith("Action") && name.length > 6) return true;
8774
+
8775
+ return false;
8776
+ };
8777
+
8778
+ // Check a property signature (interface/type member) - recursive for nested types
8779
+ const checkPropertySignatureHandler = (member) => {
8780
+ if (member.type !== "TSPropertySignature") return;
8781
+ if (!member.key || member.key.type !== "Identifier") return;
8782
+
8783
+ const propName = member.key.name;
8784
+
8785
+ // Skip private properties (starting with _)
8786
+ if (propName.startsWith("_")) return;
8787
+
8788
+ // Check for nested object types and recursively check their members
8789
+ if (isNestedObjectTypeHandler(member.typeAnnotation)) {
8790
+ const nestedType = member.typeAnnotation.typeAnnotation;
8791
+
8792
+ if (nestedType && nestedType.members) {
8793
+ nestedType.members.forEach(checkPropertySignatureHandler);
8794
+ }
8795
+
8796
+ return;
8797
+ }
8798
+
8799
+ // Check boolean props
8800
+ if (isBooleanTypeHandler(member.typeAnnotation)) {
8801
+ if (!isValidBooleanNameHandler(propName)) {
8802
+ const suggestedName = toBooleanNameHandler(propName);
8803
+
8804
+ context.report({
8805
+ fix: (fixer) => fixer.replaceText(member.key, suggestedName),
8806
+ message: `Boolean prop "${propName}" should start with a valid prefix (${booleanPrefixes.join(", ")}). Use "${suggestedName}" instead.`,
8807
+ node: member.key,
8808
+ });
8809
+ }
8810
+
8811
+ return;
8812
+ }
8813
+
8814
+ // Check callback props
8815
+ if (isCallbackTypeHandler(member.typeAnnotation)) {
8816
+ if (!isValidCallbackNameHandler(propName)) {
8817
+ const suggestedName = toCallbackNameHandler(propName);
8818
+
8819
+ context.report({
8820
+ fix: (fixer) => fixer.replaceText(member.key, suggestedName),
8821
+ message: `Callback prop "${propName}" should start with "${callbackPrefix}" prefix. Use "${suggestedName}" instead.`,
8822
+ node: member.key,
8823
+ });
8824
+ }
8825
+ }
8826
+ };
8827
+
8828
+ // Check members of a type literal (inline types, type aliases)
8829
+ const checkTypeLiteralHandler = (node) => {
8830
+ if (!node.members) return;
8831
+ node.members.forEach(checkPropertySignatureHandler);
8832
+ };
8833
+
8834
+ return {
8835
+ // Interface declarations
8836
+ TSInterfaceDeclaration(node) {
8837
+ if (!node.body || !node.body.body) return;
8838
+ node.body.body.forEach(checkPropertySignatureHandler);
8839
+ },
8840
+
8841
+ // Type alias declarations with object type
8842
+ TSTypeAliasDeclaration(node) {
8843
+ if (node.typeAnnotation?.type === "TSTypeLiteral") {
8844
+ checkTypeLiteralHandler(node.typeAnnotation);
8845
+ }
8846
+ },
8847
+
8848
+ // Inline type literals (e.g., in function parameters)
8849
+ TSTypeLiteral(node) {
8850
+ // Skip if already handled by TSTypeAliasDeclaration
8851
+ if (node.parent?.type === "TSTypeAliasDeclaration") return;
8852
+ checkTypeLiteralHandler(node);
8853
+ },
8854
+ };
8855
+ },
8856
+ meta: {
8857
+ docs: { description: "Enforce naming conventions: boolean props must start with is/has/with/without, callback props must start with on" },
8858
+ fixable: "code",
8859
+ schema: [
8860
+ {
8861
+ additionalProperties: false,
8862
+ properties: {
8863
+ allowActionSuffix: {
8864
+ default: false,
8865
+ description: "Allow 'xxxAction' pattern for callback props (e.g., submitAction, copyAction)",
8866
+ type: "boolean",
8867
+ },
8868
+ allowContinuousVerbBoolean: {
8869
+ default: false,
8870
+ description: "Allow continuous verb boolean props without prefix (e.g., loading, saving, fetching, closing)",
8871
+ type: "boolean",
8872
+ },
8873
+ allowPastVerbBoolean: {
8874
+ default: false,
8875
+ description: "Allow past verb boolean props without prefix (e.g., disabled, selected, checked, opened)",
8876
+ type: "boolean",
8877
+ },
8878
+ booleanPrefixes: {
8879
+ description: "Replace default boolean prefixes entirely. If not provided, defaults are used with extendBooleanPrefixes",
8880
+ items: { type: "string" },
8881
+ type: "array",
8882
+ },
8883
+ callbackPrefix: {
8884
+ default: "on",
8885
+ description: "Required prefix for callback props",
8886
+ type: "string",
8887
+ },
8888
+ extendBooleanPrefixes: {
8889
+ default: [],
8890
+ description: "Add additional prefixes to the defaults (is, has, with, without)",
8891
+ items: { type: "string" },
8892
+ type: "array",
8893
+ },
8894
+ },
8895
+ type: "object",
8896
+ },
8897
+ ],
8898
+ type: "suggestion",
8899
+ },
8900
+ };
8901
+
8561
8902
  /**
8562
8903
  * ───────────────────────────────────────────────────────────────
8563
8904
  * Rule: JSX Simple Element On One Line
@@ -11141,39 +11482,42 @@ const noEmptyLinesInFunctionParams = {
11141
11482
  const firstParam = params[0];
11142
11483
  const lastParam = params[params.length - 1];
11143
11484
 
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
- }
11485
+ // Find opening paren - must be WITHIN this function's range (not from an outer call expression)
11486
+ const tokenBeforeFirstParam = sourceCode.getTokenBefore(firstParam);
11487
+ // Check that the ( is within this function's range (not from .map( or similar)
11488
+ const hasParenAroundParams = tokenBeforeFirstParam
11489
+ && tokenBeforeFirstParam.value === "("
11490
+ && tokenBeforeFirstParam.range[0] >= node.range[0];
11491
+
11492
+ // Only check open/close paren spacing if params are wrapped in parentheses
11493
+ if (hasParenAroundParams) {
11494
+ const openParen = tokenBeforeFirstParam;
11495
+ const closeParen = sourceCode.getTokenAfter(lastParam);
11496
+
11497
+ // Verify closeParen is actually a ) right after lastParam AND within this function's range
11498
+ if (closeParen && closeParen.value === ")" && closeParen.range[1] <= (node.body ? node.body.range[0] : node.range[1])) {
11499
+ if (firstParam.loc.start.line - openParen.loc.end.line > 1) {
11500
+ context.report({
11501
+ fix: (fixer) => fixer.replaceTextRange(
11502
+ [openParen.range[1], firstParam.range[0]],
11503
+ "\n" + " ".repeat(firstParam.loc.start.column),
11504
+ ),
11505
+ message: "No empty line after opening parenthesis in function parameters",
11506
+ node: firstParam,
11507
+ });
11508
+ }
11167
11509
 
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
- });
11510
+ if (closeParen.loc.start.line - lastParam.loc.end.line > 1) {
11511
+ context.report({
11512
+ fix: (fixer) => fixer.replaceTextRange(
11513
+ [lastParam.range[1], closeParen.range[0]],
11514
+ "\n" + " ".repeat(closeParen.loc.start.column),
11515
+ ),
11516
+ message: "No empty line before closing parenthesis in function parameters",
11517
+ node: lastParam,
11518
+ });
11519
+ }
11520
+ }
11177
11521
  }
11178
11522
 
11179
11523
  for (let i = 0; i < params.length - 1; i += 1) {
@@ -14561,7 +14905,7 @@ const noHardcodedStrings = {
14561
14905
  "textDecoration", // SVG
14562
14906
  "transform", // SVG
14563
14907
  "translate",
14564
- "type",
14908
+ // "type" removed - should use enums for input/button types to prevent typos
14565
14909
  "vectorEffect", // SVG
14566
14910
  "useMap",
14567
14911
  "value",
@@ -14646,8 +14990,8 @@ const noHardcodedStrings = {
14646
14990
  /^[a-zA-Z]+\d*[_a-zA-Z0-9]*(_[a-zA-Z0-9]+)+$/,
14647
14991
  // Color names (CSS named colors used in SVG)
14648
14992
  /^(white|black|red|green|blue|yellow|orange|purple|pink|brown|gray|grey|cyan|magenta|transparent)$/i,
14649
- // CSS cursor values
14650
- /^(auto|default|none|context-menu|help|pointer|progress|wait|cell|crosshair|text|vertical-text|alias|copy|move|no-drop|not-allowed|grab|grabbing|all-scroll|col-resize|row-resize|n-resize|e-resize|s-resize|w-resize|ne-resize|nw-resize|se-resize|sw-resize|ew-resize|ns-resize|nesw-resize|nwse-resize|zoom-in|zoom-out)$/,
14993
+ // CSS cursor values (excluding "text" as it conflicts with input type)
14994
+ /^(auto|default|none|context-menu|help|pointer|progress|wait|cell|crosshair|vertical-text|alias|copy|move|no-drop|not-allowed|grab|grabbing|all-scroll|col-resize|row-resize|n-resize|e-resize|s-resize|w-resize|ne-resize|nw-resize|se-resize|sw-resize|ew-resize|ns-resize|nesw-resize|nwse-resize|zoom-in|zoom-out)$/,
14651
14995
  // CSS display/visibility values
14652
14996
  /^(block|inline|inline-block|flex|inline-flex|grid|inline-grid|flow-root|contents|table|table-row|table-cell|list-item|none|visible|hidden|collapse)$/,
14653
14997
  // CSS position values
@@ -14801,9 +15145,6 @@ const noHardcodedStrings = {
14801
15145
  });
14802
15146
  };
14803
15147
 
14804
- // UI component patterns - only ignored in JSX attributes, not in logic
14805
- const uiComponentPattern = /^(primary|secondary|tertiary|ghost|outline|link|muted|danger|warning|info|success|error|default|subtle|solid|soft|plain|flat|elevated|filled|tonal|text|contained|standard|xs|sm|md|lg|xl|2xl|3xl|4xl|5xl|xxs|xxl|small|medium|large|tiny|huge|compact|comfortable|spacious|left|right|center|top|bottom|start|end|middle|baseline|stretch|between|around|evenly|horizontal|vertical|row|column|inline|block|flex|grid|auto|none|hidden|visible|static|relative|absolute|fixed|sticky|on|off|hover|focus|click|blur|always|never)$/;
14806
-
14807
15148
  // HTML input types - standard browser input types, not hardcoded strings
14808
15149
  const htmlInputTypes = new Set([
14809
15150
  "button",
@@ -14923,21 +15264,6 @@ const noHardcodedStrings = {
14923
15264
  return false;
14924
15265
  };
14925
15266
 
14926
- // Check if string is in a default parameter for input type
14927
- const isInputTypeDefaultParamHandler = (node) => {
14928
- // Check if we're in an AssignmentPattern (default param)
14929
- if (node.parent && node.parent.type === "AssignmentPattern") {
14930
- const assignPattern = node.parent;
14931
-
14932
- // Check if the parameter name is "type"
14933
- if (assignPattern.left && assignPattern.left.type === "Identifier" && assignPattern.left.name === "type") {
14934
- return true;
14935
- }
14936
- }
14937
-
14938
- return false;
14939
- };
14940
-
14941
15267
  // Check if this is a module-level exported string that should be flagged
14942
15268
  const isExportedHardcodedStringHandler = (node) => {
14943
15269
  let current = node.parent;
@@ -14995,19 +15321,23 @@ const noHardcodedStrings = {
14995
15321
  const isSingleWord = !/\s/.test(str) && str.length <= 30;
14996
15322
  const isAllLowercase = /^[a-z_]+$/.test(str);
14997
15323
 
15324
+ // For JSX attributes (type, variant, etc.), prefer enums to prevent typos
15325
+ const isJsxAttribute = context.includes("attribute");
15326
+
14998
15327
  if (isSingleWord && isAllLowercase) {
14999
- return `Hardcoded data keyword or enum "${truncatedStr}"${contextPart} should be imported from @/data or @/enums (e.g., import { StatusEnum } from "@/enums")`;
15328
+ if (isJsxAttribute) {
15329
+ return `Hardcoded "${truncatedStr}"${contextPart} should be imported from @/enums (preferred) or @/data to prevent typos (e.g., import { InputTypeEnum } from "@/enums")`;
15330
+ }
15331
+
15332
+ return `Hardcoded "${truncatedStr}"${contextPart} should be imported from @/enums (preferred) or @/data (e.g., import { StatusEnum } from "@/enums")`;
15000
15333
  }
15001
15334
 
15002
15335
  // UI string: starts with capital, has spaces, or multiple words
15003
- return `Hardcoded UI string "${truncatedStr}"${contextPart} should be imported from @/strings or @/constants or @/@strings or @/@constants (e.g., import { strings } from "@/strings")`;
15336
+ return `Hardcoded UI string "${truncatedStr}"${contextPart} should be imported from @/strings or @/constants (e.g., import { strings } from "@/strings")`;
15004
15337
  };
15005
15338
 
15006
15339
  // Check if a string matches any ignore pattern
15007
15340
  const shouldIgnoreStringHandler = (str) => {
15008
- // Skip HTML input types (text, password, email, etc.)
15009
- if (isHtmlInputTypeHandler(str)) return true;
15010
-
15011
15341
  // Skip Tailwind/CSS class strings
15012
15342
  if (isTailwindClassStringHandler(str)) return true;
15013
15343
 
@@ -15279,9 +15609,6 @@ const noHardcodedStrings = {
15279
15609
  if (node.value.type === "Literal" && typeof node.value.value === "string") {
15280
15610
  const str = node.value.value;
15281
15611
 
15282
- // Skip UI component patterns in JSX attributes (variant, size, position props)
15283
- if (uiComponentPattern.test(str)) return;
15284
-
15285
15612
  if (shouldIgnoreStringHandler(str)) return;
15286
15613
 
15287
15614
  // Check if it looks like user-facing text
@@ -15303,9 +15630,6 @@ const noHardcodedStrings = {
15303
15630
  if (expression.type === "Literal" && typeof expression.value === "string") {
15304
15631
  const str = expression.value;
15305
15632
 
15306
- // Skip UI component patterns in JSX attributes (variant, size, position props)
15307
- if (uiComponentPattern.test(str)) return;
15308
-
15309
15633
  if (shouldIgnoreStringHandler(str)) return;
15310
15634
 
15311
15635
  if (!/[a-zA-Z]/.test(str)) return;
@@ -15328,9 +15652,6 @@ const noHardcodedStrings = {
15328
15652
  // Skip if inside a style object (style={{ transform: "..." }})
15329
15653
  if (isInsideStyleObjectHandler(node)) return;
15330
15654
 
15331
- // Skip input type default params (e.g., type = "text")
15332
- if (isInputTypeDefaultParamHandler(node)) return;
15333
-
15334
15655
  // Check for exported hardcoded strings (e.g., export const tokenKey = "auth_token")
15335
15656
  // These should be flagged even at module level, regardless of whether the value
15336
15657
  // looks "technical" - the point is exposing hardcoded strings in exports
@@ -15749,7 +16070,7 @@ const variableNamingConvention = {
15749
16070
 
15750
16071
  const name = node.key.name;
15751
16072
 
15752
- if (name.startsWith("_") || constantRegex.test(name) || allowedIdentifiers.includes(name)) return;
16073
+ if (name.startsWith("_") || allowedIdentifiers.includes(name)) return;
15753
16074
 
15754
16075
  // Allow PascalCase for properties that hold component references
15755
16076
  // e.g., Icon: AdminPanelSettingsIcon, FormComponent: UpdateEventForm
@@ -15769,8 +16090,11 @@ const variableNamingConvention = {
15769
16090
  if (name.startsWith("Mui")) return;
15770
16091
 
15771
16092
  if (!camelCaseRegex.test(name)) {
16093
+ const camelCaseName = toCamelCaseHandler(name);
16094
+
15772
16095
  context.report({
15773
- message: `Property "${name}" should be camelCase`,
16096
+ fix: (fixer) => fixer.replaceText(node.key, camelCaseName),
16097
+ message: `Property "${name}" should be camelCase (e.g., ${camelCaseName} instead of ${name})`,
15774
16098
  node: node.key,
15775
16099
  });
15776
16100
  }
@@ -16563,24 +16887,52 @@ const functionObjectDestructure = {
16563
16887
  if (param.type !== "Identifier") return;
16564
16888
 
16565
16889
  const paramName = param.name;
16890
+
16891
+ // Check if param is used in a spread operation - skip because destructuring would break it
16892
+ let usedInSpread = false;
16893
+ const checkSpread = (n, parent) => {
16894
+ if (!n || typeof n !== "object") return;
16895
+ if (n.type === "SpreadElement" && n.argument && n.argument.type === "Identifier" && n.argument.name === paramName) {
16896
+ usedInSpread = true;
16897
+
16898
+ return;
16899
+ }
16900
+ for (const key of Object.keys(n)) {
16901
+ if (key === "parent") continue;
16902
+ const child = n[key];
16903
+ if (Array.isArray(child)) child.forEach((c) => checkSpread(c, n));
16904
+ else if (child && typeof child === "object" && child.type) checkSpread(child, n);
16905
+ }
16906
+ };
16907
+ checkSpread(body, null);
16908
+
16909
+ if (usedInSpread) return;
16910
+
16566
16911
  const accesses = findObjectAccessesHandler(body, paramName);
16567
16912
 
16568
16913
  if (accesses.length > 0) {
16569
16914
  const accessedProps = [...new Set(accesses.map((a) => a.property))];
16570
16915
 
16571
- // Count all references to paramName in the body to check if it's used beyond dot notation
16916
+ // Count all actual references to paramName (excluding object property keys)
16572
16917
  const allRefs = [];
16573
- const countRefs = (n) => {
16918
+ const countRefs = (n, parent) => {
16574
16919
  if (!n || typeof n !== "object") return;
16575
- if (n.type === "Identifier" && n.name === paramName) allRefs.push(n);
16920
+ if (n.type === "Identifier" && n.name === paramName) {
16921
+ // Exclude object property keys (non-computed)
16922
+ const isPropertyKey = parent && parent.type === "Property" && parent.key === n && !parent.computed;
16923
+
16924
+ if (!isPropertyKey) {
16925
+ allRefs.push(n);
16926
+ }
16927
+ }
16576
16928
  for (const key of Object.keys(n)) {
16577
16929
  if (key === "parent") continue;
16578
16930
  const child = n[key];
16579
- if (Array.isArray(child)) child.forEach(countRefs);
16580
- else if (child && typeof child === "object" && child.type) countRefs(child);
16931
+ if (Array.isArray(child)) child.forEach((c) => countRefs(c, n));
16932
+ else if (child && typeof child === "object" && child.type) countRefs(child, n);
16581
16933
  }
16582
16934
  };
16583
- countRefs(body);
16935
+ countRefs(body, null);
16584
16936
 
16585
16937
  // Only auto-fix if all references are covered by the detected dot notation accesses
16586
16938
  const canAutoFix = allRefs.length === accesses.length;
@@ -18009,6 +18361,153 @@ const svgComponentIconNaming = {
18009
18361
  },
18010
18362
  };
18011
18363
 
18364
+ /**
18365
+ * ───────────────────────────────────────────────────────────────
18366
+ * Rule: Folder Component Suffix
18367
+ * ───────────────────────────────────────────────────────────────
18368
+ *
18369
+ * Description:
18370
+ * Enforces naming conventions for components based on folder location:
18371
+ * - Components in "views" folder must end with "View" suffix
18372
+ * - Components in "pages" folder must end with "Page" suffix
18373
+ *
18374
+ * ✓ Good:
18375
+ * // In views/dashboard-view.tsx:
18376
+ * export const DashboardView = () => <div>Dashboard</div>;
18377
+ *
18378
+ * // In pages/home-page.tsx:
18379
+ * export const HomePage = () => <div>Home</div>;
18380
+ *
18381
+ * ✗ Bad:
18382
+ * // In views/dashboard.tsx:
18383
+ * export const Dashboard = () => <div>Dashboard</div>; // Should be "DashboardView"
18384
+ *
18385
+ * // In pages/home.tsx:
18386
+ * export const Home = () => <div>Home</div>; // Should be "HomePage"
18387
+ */
18388
+ const folderComponentSuffix = {
18389
+ create(context) {
18390
+ const filename = context.filename || context.getFilename();
18391
+ const normalizedFilename = filename.replace(/\\/g, "/");
18392
+
18393
+ // Folder-to-suffix mapping
18394
+ const folderSuffixMap = {
18395
+ pages: "Page",
18396
+ views: "View",
18397
+ };
18398
+
18399
+ // Check which folder the file is in
18400
+ const getFolderSuffixHandler = () => {
18401
+ for (const [folder, suffix] of Object.entries(folderSuffixMap)) {
18402
+ const pattern = new RegExp(`/${folder}/[^/]+\\.(jsx?|tsx?)$`);
18403
+
18404
+ if (pattern.test(normalizedFilename)) {
18405
+ return { folder, suffix };
18406
+ }
18407
+ }
18408
+
18409
+ return null;
18410
+ };
18411
+
18412
+ // Get the component name from node
18413
+ const getComponentNameHandler = (node) => {
18414
+ // Arrow function: const Name = () => ...
18415
+ if (node.parent && node.parent.type === "VariableDeclarator" && node.parent.id && node.parent.id.type === "Identifier") {
18416
+ return { name: node.parent.id.name, identifierNode: node.parent.id };
18417
+ }
18418
+
18419
+ // Function declaration: function Name() { ... }
18420
+ if (node.id && node.id.type === "Identifier") {
18421
+ return { name: node.id.name, identifierNode: node.id };
18422
+ }
18423
+
18424
+ return null;
18425
+ };
18426
+
18427
+ // Check if component name starts with uppercase (React component convention)
18428
+ const isReactComponentNameHandler = (name) => name && /^[A-Z]/.test(name);
18429
+
18430
+ // Check if the function returns JSX
18431
+ const returnsJsxHandler = (node) => {
18432
+ const body = node.body;
18433
+
18434
+ if (!body) return false;
18435
+
18436
+ // Arrow function with expression body: () => <div>...</div>
18437
+ if (body.type === "JSXElement" || body.type === "JSXFragment") {
18438
+ return true;
18439
+ }
18440
+
18441
+ // Parenthesized expression
18442
+ if (body.type === "ParenthesizedExpression" && body.expression) {
18443
+ if (body.expression.type === "JSXElement" || body.expression.type === "JSXFragment") {
18444
+ return true;
18445
+ }
18446
+ }
18447
+
18448
+ // Block body with return statement
18449
+ if (body.type === "BlockStatement") {
18450
+ const hasJsxReturn = body.body.some((stmt) => {
18451
+ if (stmt.type === "ReturnStatement" && stmt.argument) {
18452
+ const arg = stmt.argument;
18453
+
18454
+ return arg.type === "JSXElement" || arg.type === "JSXFragment"
18455
+ || (arg.type === "ParenthesizedExpression" && arg.expression
18456
+ && (arg.expression.type === "JSXElement" || arg.expression.type === "JSXFragment"));
18457
+ }
18458
+
18459
+ return false;
18460
+ });
18461
+
18462
+ return hasJsxReturn;
18463
+ }
18464
+
18465
+ return false;
18466
+ };
18467
+
18468
+ const checkFunctionHandler = (node) => {
18469
+ const folderInfo = getFolderSuffixHandler();
18470
+
18471
+ // Not in a folder that requires specific suffix
18472
+ if (!folderInfo) return;
18473
+
18474
+ const componentInfo = getComponentNameHandler(node);
18475
+
18476
+ if (!componentInfo) return;
18477
+
18478
+ const { name, identifierNode } = componentInfo;
18479
+
18480
+ // Only check React components (PascalCase)
18481
+ if (!isReactComponentNameHandler(name)) return;
18482
+
18483
+ // Only check functions that return JSX
18484
+ if (!returnsJsxHandler(node)) return;
18485
+
18486
+ const { folder, suffix } = folderInfo;
18487
+
18488
+ // Check if component name ends with the required suffix
18489
+ if (!name.endsWith(suffix)) {
18490
+ context.report({
18491
+ message: `Component "${name}" in "${folder}" folder must end with "${suffix}" suffix (e.g., "${name}${suffix}")`,
18492
+ node: identifierNode,
18493
+ });
18494
+ }
18495
+ };
18496
+
18497
+ return {
18498
+ ArrowFunctionExpression: checkFunctionHandler,
18499
+ FunctionDeclaration: checkFunctionHandler,
18500
+ FunctionExpression: checkFunctionHandler,
18501
+ };
18502
+ },
18503
+ meta: {
18504
+ docs: { description: "Enforce components in 'views' folder end with 'View' and components in 'pages' folder end with 'Page'" },
18505
+ fixable: null,
18506
+ schema: [],
18507
+ type: "suggestion",
18508
+ },
18509
+ };
18510
+
18012
18511
  /**
18013
18512
  * ───────────────────────────────────────────────────────────────
18014
18513
  * Rule: No Inline Type Definitions
@@ -18261,10 +18760,32 @@ const noInlineTypeDefinitions = {
18261
18760
  const typeFormat = {
18262
18761
  create(context) {
18263
18762
  const sourceCode = context.sourceCode || context.getSourceCode();
18763
+ const options = context.options[0] || {};
18764
+ const minUnionMembersForMultiline = options.minUnionMembersForMultiline !== undefined ? options.minUnionMembersForMultiline : 5;
18264
18765
 
18265
18766
  const pascalCaseRegex = /^[A-Z][a-zA-Z0-9]*$/;
18266
18767
  const camelCaseRegex = /^[a-z][a-zA-Z0-9]*$/;
18267
18768
 
18769
+ // Convert PascalCase/SCREAMING_SNAKE_CASE/snake_case to camelCase
18770
+ const toCamelCaseHandler = (name) => {
18771
+ // Handle SCREAMING_SNAKE_CASE (e.g., USER_NAME -> userName)
18772
+ if (/^[A-Z][A-Z0-9_]*$/.test(name)) {
18773
+ return name.toLowerCase().replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
18774
+ }
18775
+
18776
+ // Handle snake_case (e.g., user_name -> userName)
18777
+ if (/_/.test(name)) {
18778
+ return name.toLowerCase().replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
18779
+ }
18780
+
18781
+ // Handle PascalCase (e.g., UserName -> userName)
18782
+ if (/^[A-Z]/.test(name)) {
18783
+ return name[0].toLowerCase() + name.slice(1);
18784
+ }
18785
+
18786
+ return name;
18787
+ };
18788
+
18268
18789
  const checkTypeLiteralHandler = (declarationNode, typeLiteralNode, members) => {
18269
18790
  if (members.length === 0) return;
18270
18791
 
@@ -18323,13 +18844,46 @@ const typeFormat = {
18323
18844
  const propName = member.key.name;
18324
18845
 
18325
18846
  if (!camelCaseRegex.test(propName)) {
18847
+ const fixedName = toCamelCaseHandler(propName);
18848
+
18326
18849
  context.report({
18327
- message: `Type property "${propName}" must be camelCase`,
18850
+ fix: (fixer) => fixer.replaceText(member.key, fixedName),
18851
+ message: `Type property "${propName}" must be camelCase. Use "${fixedName}" instead.`,
18328
18852
  node: member.key,
18329
18853
  });
18330
18854
  }
18331
18855
  }
18332
18856
 
18857
+ // Collapse single-member nested object types to one line
18858
+ if (member.type === "TSPropertySignature" && member.typeAnnotation?.typeAnnotation?.type === "TSTypeLiteral") {
18859
+ const nestedType = member.typeAnnotation.typeAnnotation;
18860
+
18861
+ if (nestedType.members && nestedType.members.length === 1) {
18862
+ const nestedOpenBrace = sourceCode.getFirstToken(nestedType);
18863
+ const nestedCloseBrace = sourceCode.getLastToken(nestedType);
18864
+ const isNestedMultiLine = nestedOpenBrace.loc.end.line !== nestedCloseBrace.loc.start.line;
18865
+
18866
+ if (isNestedMultiLine) {
18867
+ const nestedMember = nestedType.members[0];
18868
+ let nestedMemberText = sourceCode.getText(nestedMember).trim();
18869
+
18870
+ // Remove trailing punctuation
18871
+ if (nestedMemberText.endsWith(",") || nestedMemberText.endsWith(";")) {
18872
+ nestedMemberText = nestedMemberText.slice(0, -1);
18873
+ }
18874
+
18875
+ context.report({
18876
+ fix: (fixer) => fixer.replaceTextRange(
18877
+ [nestedOpenBrace.range[0], nestedCloseBrace.range[1]],
18878
+ `{ ${nestedMemberText} }`,
18879
+ ),
18880
+ message: "Single property nested object type should be on one line",
18881
+ node: nestedType,
18882
+ });
18883
+ }
18884
+ }
18885
+ }
18886
+
18333
18887
  // Check for space before ? in optional properties
18334
18888
  if (member.type === "TSPropertySignature" && member.optional) {
18335
18889
  const keyToken = sourceCode.getFirstToken(member);
@@ -18697,13 +19251,166 @@ const typeFormat = {
18697
19251
  }
18698
19252
  });
18699
19253
  }
19254
+
19255
+ // Check union types formatting (e.g., "a" | "b" | "c")
19256
+ if (node.typeAnnotation && node.typeAnnotation.type === "TSUnionType") {
19257
+ const unionType = node.typeAnnotation;
19258
+ const types = unionType.types;
19259
+ const minMembersForMultiline = minUnionMembersForMultiline;
19260
+
19261
+ // Get line info
19262
+ const typeLine = sourceCode.lines[node.loc.start.line - 1];
19263
+ const baseIndent = typeLine.match(/^\s*/)[0];
19264
+ const memberIndent = baseIndent + " ";
19265
+
19266
+ // Get the = token
19267
+ const equalToken = sourceCode.getTokenAfter(node.id);
19268
+ const firstType = types[0];
19269
+ const lastType = types[types.length - 1];
19270
+
19271
+ // Check if currently on single line
19272
+ const isCurrentlySingleLine = firstType.loc.start.line === lastType.loc.end.line &&
19273
+ equalToken.loc.end.line === firstType.loc.start.line;
19274
+
19275
+ // Check if currently properly multiline (= on its own conceptually, first type on new line)
19276
+ const isFirstTypeOnNewLine = firstType.loc.start.line > equalToken.loc.end.line;
19277
+
19278
+ if (types.length >= minMembersForMultiline) {
19279
+ // Should be multiline format
19280
+ // Check if needs reformatting
19281
+ let needsReformat = false;
19282
+
19283
+ // Check if first type is on new line after =
19284
+ if (!isFirstTypeOnNewLine) {
19285
+ needsReformat = true;
19286
+ }
19287
+
19288
+ // Check if each type is on its own line
19289
+ if (!needsReformat) {
19290
+ for (let i = 1; i < types.length; i++) {
19291
+ if (types[i].loc.start.line === types[i - 1].loc.end.line) {
19292
+ needsReformat = true;
19293
+ break;
19294
+ }
19295
+ }
19296
+ }
19297
+
19298
+ // Check proper indentation and | placement
19299
+ if (!needsReformat) {
19300
+ for (let i = 1; i < types.length; i++) {
19301
+ const pipeToken = sourceCode.getTokenBefore(types[i]);
19302
+
19303
+ if (pipeToken && pipeToken.value === "|") {
19304
+ // | should be at start of line (after indent)
19305
+ if (pipeToken.loc.start.line !== types[i].loc.start.line) {
19306
+ needsReformat = true;
19307
+ break;
19308
+ }
19309
+ }
19310
+ }
19311
+ }
19312
+
19313
+ if (needsReformat) {
19314
+ // Build the correct multiline format
19315
+ const formattedTypes = types.map((type, index) => {
19316
+ const typeText = sourceCode.getText(type);
19317
+
19318
+ if (index === 0) {
19319
+ return memberIndent + typeText;
19320
+ }
19321
+
19322
+ return memberIndent + "| " + typeText;
19323
+ }).join("\n");
19324
+
19325
+ const newTypeText = `= \n${formattedTypes}`;
19326
+
19327
+ context.report({
19328
+ fix: (fixer) => fixer.replaceTextRange(
19329
+ [equalToken.range[0], lastType.range[1]],
19330
+ newTypeText,
19331
+ ),
19332
+ message: `Union type with ${types.length} members should be multiline with each member on its own line`,
19333
+ node: unionType,
19334
+ });
19335
+ }
19336
+ } else {
19337
+ // Should be single line format (less than 5 members)
19338
+ if (!isCurrentlySingleLine) {
19339
+ // Build single line format
19340
+ const typeTexts = types.map((type) => sourceCode.getText(type));
19341
+ const singleLineText = `= ${typeTexts.join(" | ")}`;
19342
+
19343
+ context.report({
19344
+ fix: (fixer) => fixer.replaceTextRange(
19345
+ [equalToken.range[0], lastType.range[1]],
19346
+ singleLineText,
19347
+ ),
19348
+ message: `Union type with ${types.length} members should be on a single line`,
19349
+ node: unionType,
19350
+ });
19351
+ }
19352
+ }
19353
+ }
19354
+ },
19355
+ // Handle inline type literals (e.g., in function parameters)
19356
+ TSTypeLiteral(node) {
19357
+ // Skip if already handled by TSTypeAliasDeclaration or TSAsExpression
19358
+ if (node.parent?.type === "TSTypeAliasDeclaration") return;
19359
+ if (node.parent?.type === "TSAsExpression") return;
19360
+
19361
+ // Check for single-member nested object types that should be collapsed
19362
+ if (node.members) {
19363
+ node.members.forEach((member) => {
19364
+ if (member.type === "TSPropertySignature" && member.typeAnnotation?.typeAnnotation?.type === "TSTypeLiteral") {
19365
+ const nestedType = member.typeAnnotation.typeAnnotation;
19366
+
19367
+ if (nestedType.members && nestedType.members.length === 1) {
19368
+ const nestedOpenBrace = sourceCode.getFirstToken(nestedType);
19369
+ const nestedCloseBrace = sourceCode.getLastToken(nestedType);
19370
+ const isNestedMultiLine = nestedOpenBrace.loc.end.line !== nestedCloseBrace.loc.start.line;
19371
+
19372
+ if (isNestedMultiLine) {
19373
+ const nestedMember = nestedType.members[0];
19374
+ let nestedMemberText = sourceCode.getText(nestedMember).trim();
19375
+
19376
+ // Remove trailing punctuation
19377
+ if (nestedMemberText.endsWith(",") || nestedMemberText.endsWith(";")) {
19378
+ nestedMemberText = nestedMemberText.slice(0, -1);
19379
+ }
19380
+
19381
+ context.report({
19382
+ fix: (fixer) => fixer.replaceTextRange(
19383
+ [nestedOpenBrace.range[0], nestedCloseBrace.range[1]],
19384
+ `{ ${nestedMemberText} }`,
19385
+ ),
19386
+ message: "Single property nested object type should be on one line",
19387
+ node: nestedType,
19388
+ });
19389
+ }
19390
+ }
19391
+ }
19392
+ });
19393
+ }
18700
19394
  },
18701
19395
  };
18702
19396
  },
18703
19397
  meta: {
18704
- docs: { description: "Enforce type naming (PascalCase + Type suffix), camelCase properties, proper formatting, and trailing commas" },
19398
+ docs: { description: "Enforce type naming (PascalCase + Type suffix), camelCase properties, proper formatting, union type formatting, and trailing commas" },
18705
19399
  fixable: "code",
18706
- schema: [],
19400
+ schema: [
19401
+ {
19402
+ additionalProperties: false,
19403
+ properties: {
19404
+ minUnionMembersForMultiline: {
19405
+ default: 5,
19406
+ description: "Minimum number of union members to require multiline format",
19407
+ minimum: 2,
19408
+ type: "integer",
19409
+ },
19410
+ },
19411
+ type: "object",
19412
+ },
19413
+ ],
18707
19414
  type: "suggestion",
18708
19415
  },
18709
19416
  };
@@ -20316,14 +21023,27 @@ const enumFormat = {
20316
21023
  });
20317
21024
  }
20318
21025
 
21026
+ // Convert camelCase/PascalCase to UPPER_SNAKE_CASE
21027
+ const toUpperSnakeCaseHandler = (name) => {
21028
+ // Insert underscore before each uppercase letter (except the first)
21029
+ // Then convert to uppercase
21030
+ return name
21031
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
21032
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
21033
+ .toUpperCase();
21034
+ };
21035
+
20319
21036
  members.forEach((member, index) => {
20320
21037
  // Check member name is UPPER_CASE
20321
21038
  if (member.id && member.id.type === "Identifier") {
20322
21039
  const memberName = member.id.name;
20323
21040
 
20324
21041
  if (!upperCaseRegex.test(memberName)) {
21042
+ const fixedName = toUpperSnakeCaseHandler(memberName);
21043
+
20325
21044
  context.report({
20326
- message: `Enum member "${memberName}" must be UPPER_CASE (e.g., ${memberName.toUpperCase()})`,
21045
+ fix: (fixer) => fixer.replaceText(member.id, fixedName),
21046
+ message: `Enum member "${memberName}" must be UPPER_CASE (e.g., ${fixedName})`,
20327
21047
  node: member.id,
20328
21048
  });
20329
21049
  }
@@ -20476,6 +21196,26 @@ const interfaceFormat = {
20476
21196
  const pascalCaseRegex = /^[A-Z][a-zA-Z0-9]*$/;
20477
21197
  const camelCaseRegex = /^[a-z][a-zA-Z0-9]*$/;
20478
21198
 
21199
+ // Convert PascalCase/SCREAMING_SNAKE_CASE/snake_case to camelCase
21200
+ const toCamelCaseHandler = (name) => {
21201
+ // Handle SCREAMING_SNAKE_CASE (e.g., USER_NAME -> userName)
21202
+ if (/^[A-Z][A-Z0-9_]*$/.test(name)) {
21203
+ return name.toLowerCase().replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
21204
+ }
21205
+
21206
+ // Handle snake_case (e.g., user_name -> userName)
21207
+ if (/_/.test(name)) {
21208
+ return name.toLowerCase().replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
21209
+ }
21210
+
21211
+ // Handle PascalCase (e.g., UserName -> userName)
21212
+ if (/^[A-Z]/.test(name)) {
21213
+ return name[0].toLowerCase() + name.slice(1);
21214
+ }
21215
+
21216
+ return name;
21217
+ };
21218
+
20479
21219
  return {
20480
21220
  TSInterfaceDeclaration(node) {
20481
21221
  const interfaceName = node.id.name;
@@ -20553,17 +21293,41 @@ const interfaceFormat = {
20553
21293
  }
20554
21294
 
20555
21295
  // For single member, should be on one line without trailing punctuation
21296
+ // But skip if the property has a nested object type with 2+ members
20556
21297
  if (members.length === 1) {
20557
21298
  const member = members[0];
20558
- const memberText = sourceCode.getText(member);
20559
21299
  const isMultiLine = openBraceToken.loc.end.line !== closeBraceToken.loc.start.line;
20560
21300
 
20561
- if (isMultiLine) {
20562
- // Collapse to single line without trailing punctuation
20563
- let cleanText = memberText.trim();
21301
+ // Check if property has nested object type
21302
+ const nestedType = member.typeAnnotation?.typeAnnotation;
21303
+ const hasNestedType = nestedType?.type === "TSTypeLiteral";
21304
+ const hasMultiMemberNestedType = hasNestedType && nestedType.members?.length >= 2;
21305
+ const hasSingleMemberNestedType = hasNestedType && nestedType.members?.length === 1;
21306
+
21307
+ if (isMultiLine && !hasMultiMemberNestedType) {
21308
+ // Build the collapsed text, handling nested types specially
21309
+ let cleanText;
21310
+
21311
+ if (hasSingleMemberNestedType) {
21312
+ // Collapse nested type first, then build the member text
21313
+ const nestedMember = nestedType.members[0];
21314
+ let nestedMemberText = sourceCode.getText(nestedMember).trim();
21315
+
21316
+ if (nestedMemberText.endsWith(",") || nestedMemberText.endsWith(";")) {
21317
+ nestedMemberText = nestedMemberText.slice(0, -1);
21318
+ }
21319
+
21320
+ // Build: propName: { nestedProp: type }
21321
+ const propName = member.key.name;
21322
+ const optionalMark = member.optional ? "?" : "";
21323
+
21324
+ cleanText = `${propName}${optionalMark}: { ${nestedMemberText} }`;
21325
+ } else {
21326
+ cleanText = sourceCode.getText(member).trim();
20564
21327
 
20565
- if (cleanText.endsWith(",") || cleanText.endsWith(";")) {
20566
- cleanText = cleanText.slice(0, -1);
21328
+ if (cleanText.endsWith(",") || cleanText.endsWith(";")) {
21329
+ cleanText = cleanText.slice(0, -1);
21330
+ }
20567
21331
  }
20568
21332
 
20569
21333
  const newInterfaceText = `{ ${cleanText} }`;
@@ -20581,6 +21345,8 @@ const interfaceFormat = {
20581
21345
  }
20582
21346
 
20583
21347
  // Check for trailing comma/semicolon in single-line single member
21348
+ const memberText = sourceCode.getText(member);
21349
+
20584
21350
  if (memberText.trimEnd().endsWith(",") || memberText.trimEnd().endsWith(";")) {
20585
21351
  const punctIndex = Math.max(memberText.lastIndexOf(","), memberText.lastIndexOf(";"));
20586
21352
 
@@ -20612,13 +21378,46 @@ const interfaceFormat = {
20612
21378
  const propName = member.key.name;
20613
21379
 
20614
21380
  if (!camelCaseRegex.test(propName)) {
21381
+ const fixedName = toCamelCaseHandler(propName);
21382
+
20615
21383
  context.report({
20616
- message: `Interface property "${propName}" must be camelCase`,
21384
+ fix: (fixer) => fixer.replaceText(member.key, fixedName),
21385
+ message: `Interface property "${propName}" must be camelCase. Use "${fixedName}" instead.`,
20617
21386
  node: member.key,
20618
21387
  });
20619
21388
  }
20620
21389
  }
20621
21390
 
21391
+ // Collapse single-member nested object types to one line
21392
+ if (member.type === "TSPropertySignature" && member.typeAnnotation?.typeAnnotation?.type === "TSTypeLiteral") {
21393
+ const nestedType = member.typeAnnotation.typeAnnotation;
21394
+
21395
+ if (nestedType.members && nestedType.members.length === 1) {
21396
+ const nestedOpenBrace = sourceCode.getFirstToken(nestedType);
21397
+ const nestedCloseBrace = sourceCode.getLastToken(nestedType);
21398
+ const isNestedMultiLine = nestedOpenBrace.loc.end.line !== nestedCloseBrace.loc.start.line;
21399
+
21400
+ if (isNestedMultiLine) {
21401
+ const nestedMember = nestedType.members[0];
21402
+ let nestedMemberText = sourceCode.getText(nestedMember).trim();
21403
+
21404
+ // Remove trailing punctuation
21405
+ if (nestedMemberText.endsWith(",") || nestedMemberText.endsWith(";")) {
21406
+ nestedMemberText = nestedMemberText.slice(0, -1);
21407
+ }
21408
+
21409
+ context.report({
21410
+ fix: (fixer) => fixer.replaceTextRange(
21411
+ [nestedOpenBrace.range[0], nestedCloseBrace.range[1]],
21412
+ `{ ${nestedMemberText} }`,
21413
+ ),
21414
+ message: "Single property nested object type should be on one line",
21415
+ node: nestedType,
21416
+ });
21417
+ }
21418
+ }
21419
+ }
21420
+
20622
21421
  // Check for space before ? in optional properties
20623
21422
  if (member.type === "TSPropertySignature" && member.optional) {
20624
21423
  const keyToken = sourceCode.getFirstToken(member);
@@ -20864,6 +21663,7 @@ export default {
20864
21663
  // Component rules
20865
21664
  "component-props-destructure": componentPropsDestructure,
20866
21665
  "component-props-inline-type": componentPropsInlineType,
21666
+ "folder-component-suffix": folderComponentSuffix,
20867
21667
  "svg-component-icon-naming": svgComponentIconNaming,
20868
21668
 
20869
21669
  // React rules
@@ -20935,6 +21735,7 @@ export default {
20935
21735
  "enum-format": enumFormat,
20936
21736
  "interface-format": interfaceFormat,
20937
21737
  "no-inline-type-definitions": noInlineTypeDefinitions,
21738
+ "prop-naming-convention": propNamingConvention,
20938
21739
  "type-annotation-spacing": typeAnnotationSpacing,
20939
21740
  "type-format": typeFormat,
20940
21741
  "typescript-definition-location": typescriptDefinitionLocation,