eslint-plugin-code-style 1.11.7 → 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) {
@@ -15726,7 +16070,7 @@ const variableNamingConvention = {
15726
16070
 
15727
16071
  const name = node.key.name;
15728
16072
 
15729
- if (name.startsWith("_") || constantRegex.test(name) || allowedIdentifiers.includes(name)) return;
16073
+ if (name.startsWith("_") || allowedIdentifiers.includes(name)) return;
15730
16074
 
15731
16075
  // Allow PascalCase for properties that hold component references
15732
16076
  // e.g., Icon: AdminPanelSettingsIcon, FormComponent: UpdateEventForm
@@ -15746,8 +16090,11 @@ const variableNamingConvention = {
15746
16090
  if (name.startsWith("Mui")) return;
15747
16091
 
15748
16092
  if (!camelCaseRegex.test(name)) {
16093
+ const camelCaseName = toCamelCaseHandler(name);
16094
+
15749
16095
  context.report({
15750
- 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})`,
15751
16098
  node: node.key,
15752
16099
  });
15753
16100
  }
@@ -16540,24 +16887,52 @@ const functionObjectDestructure = {
16540
16887
  if (param.type !== "Identifier") return;
16541
16888
 
16542
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
+
16543
16911
  const accesses = findObjectAccessesHandler(body, paramName);
16544
16912
 
16545
16913
  if (accesses.length > 0) {
16546
16914
  const accessedProps = [...new Set(accesses.map((a) => a.property))];
16547
16915
 
16548
- // 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)
16549
16917
  const allRefs = [];
16550
- const countRefs = (n) => {
16918
+ const countRefs = (n, parent) => {
16551
16919
  if (!n || typeof n !== "object") return;
16552
- 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
+ }
16553
16928
  for (const key of Object.keys(n)) {
16554
16929
  if (key === "parent") continue;
16555
16930
  const child = n[key];
16556
- if (Array.isArray(child)) child.forEach(countRefs);
16557
- 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);
16558
16933
  }
16559
16934
  };
16560
- countRefs(body);
16935
+ countRefs(body, null);
16561
16936
 
16562
16937
  // Only auto-fix if all references are covered by the detected dot notation accesses
16563
16938
  const canAutoFix = allRefs.length === accesses.length;
@@ -17986,6 +18361,153 @@ const svgComponentIconNaming = {
17986
18361
  },
17987
18362
  };
17988
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
+
17989
18511
  /**
17990
18512
  * ───────────────────────────────────────────────────────────────
17991
18513
  * Rule: No Inline Type Definitions
@@ -18238,10 +18760,32 @@ const noInlineTypeDefinitions = {
18238
18760
  const typeFormat = {
18239
18761
  create(context) {
18240
18762
  const sourceCode = context.sourceCode || context.getSourceCode();
18763
+ const options = context.options[0] || {};
18764
+ const minUnionMembersForMultiline = options.minUnionMembersForMultiline !== undefined ? options.minUnionMembersForMultiline : 5;
18241
18765
 
18242
18766
  const pascalCaseRegex = /^[A-Z][a-zA-Z0-9]*$/;
18243
18767
  const camelCaseRegex = /^[a-z][a-zA-Z0-9]*$/;
18244
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
+
18245
18789
  const checkTypeLiteralHandler = (declarationNode, typeLiteralNode, members) => {
18246
18790
  if (members.length === 0) return;
18247
18791
 
@@ -18300,13 +18844,46 @@ const typeFormat = {
18300
18844
  const propName = member.key.name;
18301
18845
 
18302
18846
  if (!camelCaseRegex.test(propName)) {
18847
+ const fixedName = toCamelCaseHandler(propName);
18848
+
18303
18849
  context.report({
18304
- 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.`,
18305
18852
  node: member.key,
18306
18853
  });
18307
18854
  }
18308
18855
  }
18309
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
+
18310
18887
  // Check for space before ? in optional properties
18311
18888
  if (member.type === "TSPropertySignature" && member.optional) {
18312
18889
  const keyToken = sourceCode.getFirstToken(member);
@@ -18674,13 +19251,166 @@ const typeFormat = {
18674
19251
  }
18675
19252
  });
18676
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
+ }
18677
19394
  },
18678
19395
  };
18679
19396
  },
18680
19397
  meta: {
18681
- 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" },
18682
19399
  fixable: "code",
18683
- 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
+ ],
18684
19414
  type: "suggestion",
18685
19415
  },
18686
19416
  };
@@ -20293,14 +21023,27 @@ const enumFormat = {
20293
21023
  });
20294
21024
  }
20295
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
+
20296
21036
  members.forEach((member, index) => {
20297
21037
  // Check member name is UPPER_CASE
20298
21038
  if (member.id && member.id.type === "Identifier") {
20299
21039
  const memberName = member.id.name;
20300
21040
 
20301
21041
  if (!upperCaseRegex.test(memberName)) {
21042
+ const fixedName = toUpperSnakeCaseHandler(memberName);
21043
+
20302
21044
  context.report({
20303
- 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})`,
20304
21047
  node: member.id,
20305
21048
  });
20306
21049
  }
@@ -20453,6 +21196,26 @@ const interfaceFormat = {
20453
21196
  const pascalCaseRegex = /^[A-Z][a-zA-Z0-9]*$/;
20454
21197
  const camelCaseRegex = /^[a-z][a-zA-Z0-9]*$/;
20455
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
+
20456
21219
  return {
20457
21220
  TSInterfaceDeclaration(node) {
20458
21221
  const interfaceName = node.id.name;
@@ -20530,17 +21293,41 @@ const interfaceFormat = {
20530
21293
  }
20531
21294
 
20532
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
20533
21297
  if (members.length === 1) {
20534
21298
  const member = members[0];
20535
- const memberText = sourceCode.getText(member);
20536
21299
  const isMultiLine = openBraceToken.loc.end.line !== closeBraceToken.loc.start.line;
20537
21300
 
20538
- if (isMultiLine) {
20539
- // Collapse to single line without trailing punctuation
20540
- 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
+ }
20541
21319
 
20542
- if (cleanText.endsWith(",") || cleanText.endsWith(";")) {
20543
- cleanText = cleanText.slice(0, -1);
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();
21327
+
21328
+ if (cleanText.endsWith(",") || cleanText.endsWith(";")) {
21329
+ cleanText = cleanText.slice(0, -1);
21330
+ }
20544
21331
  }
20545
21332
 
20546
21333
  const newInterfaceText = `{ ${cleanText} }`;
@@ -20558,6 +21345,8 @@ const interfaceFormat = {
20558
21345
  }
20559
21346
 
20560
21347
  // Check for trailing comma/semicolon in single-line single member
21348
+ const memberText = sourceCode.getText(member);
21349
+
20561
21350
  if (memberText.trimEnd().endsWith(",") || memberText.trimEnd().endsWith(";")) {
20562
21351
  const punctIndex = Math.max(memberText.lastIndexOf(","), memberText.lastIndexOf(";"));
20563
21352
 
@@ -20589,13 +21378,46 @@ const interfaceFormat = {
20589
21378
  const propName = member.key.name;
20590
21379
 
20591
21380
  if (!camelCaseRegex.test(propName)) {
21381
+ const fixedName = toCamelCaseHandler(propName);
21382
+
20592
21383
  context.report({
20593
- 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.`,
20594
21386
  node: member.key,
20595
21387
  });
20596
21388
  }
20597
21389
  }
20598
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
+
20599
21421
  // Check for space before ? in optional properties
20600
21422
  if (member.type === "TSPropertySignature" && member.optional) {
20601
21423
  const keyToken = sourceCode.getFirstToken(member);
@@ -20841,6 +21663,7 @@ export default {
20841
21663
  // Component rules
20842
21664
  "component-props-destructure": componentPropsDestructure,
20843
21665
  "component-props-inline-type": componentPropsInlineType,
21666
+ "folder-component-suffix": folderComponentSuffix,
20844
21667
  "svg-component-icon-naming": svgComponentIconNaming,
20845
21668
 
20846
21669
  // React rules
@@ -20912,6 +21735,7 @@ export default {
20912
21735
  "enum-format": enumFormat,
20913
21736
  "interface-format": interfaceFormat,
20914
21737
  "no-inline-type-definitions": noInlineTypeDefinitions,
21738
+ "prop-naming-convention": propNamingConvention,
20915
21739
  "type-annotation-spacing": typeAnnotationSpacing,
20916
21740
  "type-format": typeFormat,
20917
21741
  "typescript-definition-location": typescriptDefinitionLocation,