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/CHANGELOG.md +89 -0
- package/README.md +275 -7
- package/index.d.ts +4 -0
- package/index.js +886 -62
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1709,9 +1709,16 @@ const commentFormat = {
|
|
|
1709
1709
|
const isSingleLine = !value.includes("\n");
|
|
1710
1710
|
|
|
1711
1711
|
if (isSingleLine) {
|
|
1712
|
-
//
|
|
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
|
|
11145
|
-
|
|
11146
|
-
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
if
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
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
|
-
|
|
11169
|
-
|
|
11170
|
-
|
|
11171
|
-
|
|
11172
|
-
|
|
11173
|
-
|
|
11174
|
-
|
|
11175
|
-
|
|
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("_") ||
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
20539
|
-
|
|
20540
|
-
|
|
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
|
-
|
|
20543
|
-
|
|
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
|
-
|
|
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,
|