eslint-plugin-code-style 1.11.4 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -0
- package/README.md +275 -7
- package/index.d.ts +4 -0
- package/index.js +898 -97
- 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) {
|
|
@@ -14561,7 +14905,7 @@ const noHardcodedStrings = {
|
|
|
14561
14905
|
"textDecoration", // SVG
|
|
14562
14906
|
"transform", // SVG
|
|
14563
14907
|
"translate",
|
|
14564
|
-
"type"
|
|
14908
|
+
// "type" removed - should use enums for input/button types to prevent typos
|
|
14565
14909
|
"vectorEffect", // SVG
|
|
14566
14910
|
"useMap",
|
|
14567
14911
|
"value",
|
|
@@ -14646,8 +14990,8 @@ const noHardcodedStrings = {
|
|
|
14646
14990
|
/^[a-zA-Z]+\d*[_a-zA-Z0-9]*(_[a-zA-Z0-9]+)+$/,
|
|
14647
14991
|
// Color names (CSS named colors used in SVG)
|
|
14648
14992
|
/^(white|black|red|green|blue|yellow|orange|purple|pink|brown|gray|grey|cyan|magenta|transparent)$/i,
|
|
14649
|
-
// CSS cursor values
|
|
14650
|
-
/^(auto|default|none|context-menu|help|pointer|progress|wait|cell|crosshair|
|
|
14993
|
+
// CSS cursor values (excluding "text" as it conflicts with input type)
|
|
14994
|
+
/^(auto|default|none|context-menu|help|pointer|progress|wait|cell|crosshair|vertical-text|alias|copy|move|no-drop|not-allowed|grab|grabbing|all-scroll|col-resize|row-resize|n-resize|e-resize|s-resize|w-resize|ne-resize|nw-resize|se-resize|sw-resize|ew-resize|ns-resize|nesw-resize|nwse-resize|zoom-in|zoom-out)$/,
|
|
14651
14995
|
// CSS display/visibility values
|
|
14652
14996
|
/^(block|inline|inline-block|flex|inline-flex|grid|inline-grid|flow-root|contents|table|table-row|table-cell|list-item|none|visible|hidden|collapse)$/,
|
|
14653
14997
|
// CSS position values
|
|
@@ -14801,9 +15145,6 @@ const noHardcodedStrings = {
|
|
|
14801
15145
|
});
|
|
14802
15146
|
};
|
|
14803
15147
|
|
|
14804
|
-
// UI component patterns - only ignored in JSX attributes, not in logic
|
|
14805
|
-
const uiComponentPattern = /^(primary|secondary|tertiary|ghost|outline|link|muted|danger|warning|info|success|error|default|subtle|solid|soft|plain|flat|elevated|filled|tonal|text|contained|standard|xs|sm|md|lg|xl|2xl|3xl|4xl|5xl|xxs|xxl|small|medium|large|tiny|huge|compact|comfortable|spacious|left|right|center|top|bottom|start|end|middle|baseline|stretch|between|around|evenly|horizontal|vertical|row|column|inline|block|flex|grid|auto|none|hidden|visible|static|relative|absolute|fixed|sticky|on|off|hover|focus|click|blur|always|never)$/;
|
|
14806
|
-
|
|
14807
15148
|
// HTML input types - standard browser input types, not hardcoded strings
|
|
14808
15149
|
const htmlInputTypes = new Set([
|
|
14809
15150
|
"button",
|
|
@@ -14923,21 +15264,6 @@ const noHardcodedStrings = {
|
|
|
14923
15264
|
return false;
|
|
14924
15265
|
};
|
|
14925
15266
|
|
|
14926
|
-
// Check if string is in a default parameter for input type
|
|
14927
|
-
const isInputTypeDefaultParamHandler = (node) => {
|
|
14928
|
-
// Check if we're in an AssignmentPattern (default param)
|
|
14929
|
-
if (node.parent && node.parent.type === "AssignmentPattern") {
|
|
14930
|
-
const assignPattern = node.parent;
|
|
14931
|
-
|
|
14932
|
-
// Check if the parameter name is "type"
|
|
14933
|
-
if (assignPattern.left && assignPattern.left.type === "Identifier" && assignPattern.left.name === "type") {
|
|
14934
|
-
return true;
|
|
14935
|
-
}
|
|
14936
|
-
}
|
|
14937
|
-
|
|
14938
|
-
return false;
|
|
14939
|
-
};
|
|
14940
|
-
|
|
14941
15267
|
// Check if this is a module-level exported string that should be flagged
|
|
14942
15268
|
const isExportedHardcodedStringHandler = (node) => {
|
|
14943
15269
|
let current = node.parent;
|
|
@@ -14995,19 +15321,23 @@ const noHardcodedStrings = {
|
|
|
14995
15321
|
const isSingleWord = !/\s/.test(str) && str.length <= 30;
|
|
14996
15322
|
const isAllLowercase = /^[a-z_]+$/.test(str);
|
|
14997
15323
|
|
|
15324
|
+
// For JSX attributes (type, variant, etc.), prefer enums to prevent typos
|
|
15325
|
+
const isJsxAttribute = context.includes("attribute");
|
|
15326
|
+
|
|
14998
15327
|
if (isSingleWord && isAllLowercase) {
|
|
14999
|
-
|
|
15328
|
+
if (isJsxAttribute) {
|
|
15329
|
+
return `Hardcoded "${truncatedStr}"${contextPart} should be imported from @/enums (preferred) or @/data to prevent typos (e.g., import { InputTypeEnum } from "@/enums")`;
|
|
15330
|
+
}
|
|
15331
|
+
|
|
15332
|
+
return `Hardcoded "${truncatedStr}"${contextPart} should be imported from @/enums (preferred) or @/data (e.g., import { StatusEnum } from "@/enums")`;
|
|
15000
15333
|
}
|
|
15001
15334
|
|
|
15002
15335
|
// UI string: starts with capital, has spaces, or multiple words
|
|
15003
|
-
return `Hardcoded UI string "${truncatedStr}"${contextPart} should be imported from @/strings or @/constants
|
|
15336
|
+
return `Hardcoded UI string "${truncatedStr}"${contextPart} should be imported from @/strings or @/constants (e.g., import { strings } from "@/strings")`;
|
|
15004
15337
|
};
|
|
15005
15338
|
|
|
15006
15339
|
// Check if a string matches any ignore pattern
|
|
15007
15340
|
const shouldIgnoreStringHandler = (str) => {
|
|
15008
|
-
// Skip HTML input types (text, password, email, etc.)
|
|
15009
|
-
if (isHtmlInputTypeHandler(str)) return true;
|
|
15010
|
-
|
|
15011
15341
|
// Skip Tailwind/CSS class strings
|
|
15012
15342
|
if (isTailwindClassStringHandler(str)) return true;
|
|
15013
15343
|
|
|
@@ -15279,9 +15609,6 @@ const noHardcodedStrings = {
|
|
|
15279
15609
|
if (node.value.type === "Literal" && typeof node.value.value === "string") {
|
|
15280
15610
|
const str = node.value.value;
|
|
15281
15611
|
|
|
15282
|
-
// Skip UI component patterns in JSX attributes (variant, size, position props)
|
|
15283
|
-
if (uiComponentPattern.test(str)) return;
|
|
15284
|
-
|
|
15285
15612
|
if (shouldIgnoreStringHandler(str)) return;
|
|
15286
15613
|
|
|
15287
15614
|
// Check if it looks like user-facing text
|
|
@@ -15303,9 +15630,6 @@ const noHardcodedStrings = {
|
|
|
15303
15630
|
if (expression.type === "Literal" && typeof expression.value === "string") {
|
|
15304
15631
|
const str = expression.value;
|
|
15305
15632
|
|
|
15306
|
-
// Skip UI component patterns in JSX attributes (variant, size, position props)
|
|
15307
|
-
if (uiComponentPattern.test(str)) return;
|
|
15308
|
-
|
|
15309
15633
|
if (shouldIgnoreStringHandler(str)) return;
|
|
15310
15634
|
|
|
15311
15635
|
if (!/[a-zA-Z]/.test(str)) return;
|
|
@@ -15328,9 +15652,6 @@ const noHardcodedStrings = {
|
|
|
15328
15652
|
// Skip if inside a style object (style={{ transform: "..." }})
|
|
15329
15653
|
if (isInsideStyleObjectHandler(node)) return;
|
|
15330
15654
|
|
|
15331
|
-
// Skip input type default params (e.g., type = "text")
|
|
15332
|
-
if (isInputTypeDefaultParamHandler(node)) return;
|
|
15333
|
-
|
|
15334
15655
|
// Check for exported hardcoded strings (e.g., export const tokenKey = "auth_token")
|
|
15335
15656
|
// These should be flagged even at module level, regardless of whether the value
|
|
15336
15657
|
// looks "technical" - the point is exposing hardcoded strings in exports
|
|
@@ -15749,7 +16070,7 @@ const variableNamingConvention = {
|
|
|
15749
16070
|
|
|
15750
16071
|
const name = node.key.name;
|
|
15751
16072
|
|
|
15752
|
-
if (name.startsWith("_") ||
|
|
16073
|
+
if (name.startsWith("_") || allowedIdentifiers.includes(name)) return;
|
|
15753
16074
|
|
|
15754
16075
|
// Allow PascalCase for properties that hold component references
|
|
15755
16076
|
// e.g., Icon: AdminPanelSettingsIcon, FormComponent: UpdateEventForm
|
|
@@ -15769,8 +16090,11 @@ const variableNamingConvention = {
|
|
|
15769
16090
|
if (name.startsWith("Mui")) return;
|
|
15770
16091
|
|
|
15771
16092
|
if (!camelCaseRegex.test(name)) {
|
|
16093
|
+
const camelCaseName = toCamelCaseHandler(name);
|
|
16094
|
+
|
|
15772
16095
|
context.report({
|
|
15773
|
-
|
|
16096
|
+
fix: (fixer) => fixer.replaceText(node.key, camelCaseName),
|
|
16097
|
+
message: `Property "${name}" should be camelCase (e.g., ${camelCaseName} instead of ${name})`,
|
|
15774
16098
|
node: node.key,
|
|
15775
16099
|
});
|
|
15776
16100
|
}
|
|
@@ -16563,24 +16887,52 @@ const functionObjectDestructure = {
|
|
|
16563
16887
|
if (param.type !== "Identifier") return;
|
|
16564
16888
|
|
|
16565
16889
|
const paramName = param.name;
|
|
16890
|
+
|
|
16891
|
+
// Check if param is used in a spread operation - skip because destructuring would break it
|
|
16892
|
+
let usedInSpread = false;
|
|
16893
|
+
const checkSpread = (n, parent) => {
|
|
16894
|
+
if (!n || typeof n !== "object") return;
|
|
16895
|
+
if (n.type === "SpreadElement" && n.argument && n.argument.type === "Identifier" && n.argument.name === paramName) {
|
|
16896
|
+
usedInSpread = true;
|
|
16897
|
+
|
|
16898
|
+
return;
|
|
16899
|
+
}
|
|
16900
|
+
for (const key of Object.keys(n)) {
|
|
16901
|
+
if (key === "parent") continue;
|
|
16902
|
+
const child = n[key];
|
|
16903
|
+
if (Array.isArray(child)) child.forEach((c) => checkSpread(c, n));
|
|
16904
|
+
else if (child && typeof child === "object" && child.type) checkSpread(child, n);
|
|
16905
|
+
}
|
|
16906
|
+
};
|
|
16907
|
+
checkSpread(body, null);
|
|
16908
|
+
|
|
16909
|
+
if (usedInSpread) return;
|
|
16910
|
+
|
|
16566
16911
|
const accesses = findObjectAccessesHandler(body, paramName);
|
|
16567
16912
|
|
|
16568
16913
|
if (accesses.length > 0) {
|
|
16569
16914
|
const accessedProps = [...new Set(accesses.map((a) => a.property))];
|
|
16570
16915
|
|
|
16571
|
-
// Count all references to paramName
|
|
16916
|
+
// Count all actual references to paramName (excluding object property keys)
|
|
16572
16917
|
const allRefs = [];
|
|
16573
|
-
const countRefs = (n) => {
|
|
16918
|
+
const countRefs = (n, parent) => {
|
|
16574
16919
|
if (!n || typeof n !== "object") return;
|
|
16575
|
-
if (n.type === "Identifier" && n.name === paramName)
|
|
16920
|
+
if (n.type === "Identifier" && n.name === paramName) {
|
|
16921
|
+
// Exclude object property keys (non-computed)
|
|
16922
|
+
const isPropertyKey = parent && parent.type === "Property" && parent.key === n && !parent.computed;
|
|
16923
|
+
|
|
16924
|
+
if (!isPropertyKey) {
|
|
16925
|
+
allRefs.push(n);
|
|
16926
|
+
}
|
|
16927
|
+
}
|
|
16576
16928
|
for (const key of Object.keys(n)) {
|
|
16577
16929
|
if (key === "parent") continue;
|
|
16578
16930
|
const child = n[key];
|
|
16579
|
-
if (Array.isArray(child)) child.forEach(countRefs);
|
|
16580
|
-
else if (child && typeof child === "object" && child.type) countRefs(child);
|
|
16931
|
+
if (Array.isArray(child)) child.forEach((c) => countRefs(c, n));
|
|
16932
|
+
else if (child && typeof child === "object" && child.type) countRefs(child, n);
|
|
16581
16933
|
}
|
|
16582
16934
|
};
|
|
16583
|
-
countRefs(body);
|
|
16935
|
+
countRefs(body, null);
|
|
16584
16936
|
|
|
16585
16937
|
// Only auto-fix if all references are covered by the detected dot notation accesses
|
|
16586
16938
|
const canAutoFix = allRefs.length === accesses.length;
|
|
@@ -18009,6 +18361,153 @@ const svgComponentIconNaming = {
|
|
|
18009
18361
|
},
|
|
18010
18362
|
};
|
|
18011
18363
|
|
|
18364
|
+
/**
|
|
18365
|
+
* ───────────────────────────────────────────────────────────────
|
|
18366
|
+
* Rule: Folder Component Suffix
|
|
18367
|
+
* ───────────────────────────────────────────────────────────────
|
|
18368
|
+
*
|
|
18369
|
+
* Description:
|
|
18370
|
+
* Enforces naming conventions for components based on folder location:
|
|
18371
|
+
* - Components in "views" folder must end with "View" suffix
|
|
18372
|
+
* - Components in "pages" folder must end with "Page" suffix
|
|
18373
|
+
*
|
|
18374
|
+
* ✓ Good:
|
|
18375
|
+
* // In views/dashboard-view.tsx:
|
|
18376
|
+
* export const DashboardView = () => <div>Dashboard</div>;
|
|
18377
|
+
*
|
|
18378
|
+
* // In pages/home-page.tsx:
|
|
18379
|
+
* export const HomePage = () => <div>Home</div>;
|
|
18380
|
+
*
|
|
18381
|
+
* ✗ Bad:
|
|
18382
|
+
* // In views/dashboard.tsx:
|
|
18383
|
+
* export const Dashboard = () => <div>Dashboard</div>; // Should be "DashboardView"
|
|
18384
|
+
*
|
|
18385
|
+
* // In pages/home.tsx:
|
|
18386
|
+
* export const Home = () => <div>Home</div>; // Should be "HomePage"
|
|
18387
|
+
*/
|
|
18388
|
+
const folderComponentSuffix = {
|
|
18389
|
+
create(context) {
|
|
18390
|
+
const filename = context.filename || context.getFilename();
|
|
18391
|
+
const normalizedFilename = filename.replace(/\\/g, "/");
|
|
18392
|
+
|
|
18393
|
+
// Folder-to-suffix mapping
|
|
18394
|
+
const folderSuffixMap = {
|
|
18395
|
+
pages: "Page",
|
|
18396
|
+
views: "View",
|
|
18397
|
+
};
|
|
18398
|
+
|
|
18399
|
+
// Check which folder the file is in
|
|
18400
|
+
const getFolderSuffixHandler = () => {
|
|
18401
|
+
for (const [folder, suffix] of Object.entries(folderSuffixMap)) {
|
|
18402
|
+
const pattern = new RegExp(`/${folder}/[^/]+\\.(jsx?|tsx?)$`);
|
|
18403
|
+
|
|
18404
|
+
if (pattern.test(normalizedFilename)) {
|
|
18405
|
+
return { folder, suffix };
|
|
18406
|
+
}
|
|
18407
|
+
}
|
|
18408
|
+
|
|
18409
|
+
return null;
|
|
18410
|
+
};
|
|
18411
|
+
|
|
18412
|
+
// Get the component name from node
|
|
18413
|
+
const getComponentNameHandler = (node) => {
|
|
18414
|
+
// Arrow function: const Name = () => ...
|
|
18415
|
+
if (node.parent && node.parent.type === "VariableDeclarator" && node.parent.id && node.parent.id.type === "Identifier") {
|
|
18416
|
+
return { name: node.parent.id.name, identifierNode: node.parent.id };
|
|
18417
|
+
}
|
|
18418
|
+
|
|
18419
|
+
// Function declaration: function Name() { ... }
|
|
18420
|
+
if (node.id && node.id.type === "Identifier") {
|
|
18421
|
+
return { name: node.id.name, identifierNode: node.id };
|
|
18422
|
+
}
|
|
18423
|
+
|
|
18424
|
+
return null;
|
|
18425
|
+
};
|
|
18426
|
+
|
|
18427
|
+
// Check if component name starts with uppercase (React component convention)
|
|
18428
|
+
const isReactComponentNameHandler = (name) => name && /^[A-Z]/.test(name);
|
|
18429
|
+
|
|
18430
|
+
// Check if the function returns JSX
|
|
18431
|
+
const returnsJsxHandler = (node) => {
|
|
18432
|
+
const body = node.body;
|
|
18433
|
+
|
|
18434
|
+
if (!body) return false;
|
|
18435
|
+
|
|
18436
|
+
// Arrow function with expression body: () => <div>...</div>
|
|
18437
|
+
if (body.type === "JSXElement" || body.type === "JSXFragment") {
|
|
18438
|
+
return true;
|
|
18439
|
+
}
|
|
18440
|
+
|
|
18441
|
+
// Parenthesized expression
|
|
18442
|
+
if (body.type === "ParenthesizedExpression" && body.expression) {
|
|
18443
|
+
if (body.expression.type === "JSXElement" || body.expression.type === "JSXFragment") {
|
|
18444
|
+
return true;
|
|
18445
|
+
}
|
|
18446
|
+
}
|
|
18447
|
+
|
|
18448
|
+
// Block body with return statement
|
|
18449
|
+
if (body.type === "BlockStatement") {
|
|
18450
|
+
const hasJsxReturn = body.body.some((stmt) => {
|
|
18451
|
+
if (stmt.type === "ReturnStatement" && stmt.argument) {
|
|
18452
|
+
const arg = stmt.argument;
|
|
18453
|
+
|
|
18454
|
+
return arg.type === "JSXElement" || arg.type === "JSXFragment"
|
|
18455
|
+
|| (arg.type === "ParenthesizedExpression" && arg.expression
|
|
18456
|
+
&& (arg.expression.type === "JSXElement" || arg.expression.type === "JSXFragment"));
|
|
18457
|
+
}
|
|
18458
|
+
|
|
18459
|
+
return false;
|
|
18460
|
+
});
|
|
18461
|
+
|
|
18462
|
+
return hasJsxReturn;
|
|
18463
|
+
}
|
|
18464
|
+
|
|
18465
|
+
return false;
|
|
18466
|
+
};
|
|
18467
|
+
|
|
18468
|
+
const checkFunctionHandler = (node) => {
|
|
18469
|
+
const folderInfo = getFolderSuffixHandler();
|
|
18470
|
+
|
|
18471
|
+
// Not in a folder that requires specific suffix
|
|
18472
|
+
if (!folderInfo) return;
|
|
18473
|
+
|
|
18474
|
+
const componentInfo = getComponentNameHandler(node);
|
|
18475
|
+
|
|
18476
|
+
if (!componentInfo) return;
|
|
18477
|
+
|
|
18478
|
+
const { name, identifierNode } = componentInfo;
|
|
18479
|
+
|
|
18480
|
+
// Only check React components (PascalCase)
|
|
18481
|
+
if (!isReactComponentNameHandler(name)) return;
|
|
18482
|
+
|
|
18483
|
+
// Only check functions that return JSX
|
|
18484
|
+
if (!returnsJsxHandler(node)) return;
|
|
18485
|
+
|
|
18486
|
+
const { folder, suffix } = folderInfo;
|
|
18487
|
+
|
|
18488
|
+
// Check if component name ends with the required suffix
|
|
18489
|
+
if (!name.endsWith(suffix)) {
|
|
18490
|
+
context.report({
|
|
18491
|
+
message: `Component "${name}" in "${folder}" folder must end with "${suffix}" suffix (e.g., "${name}${suffix}")`,
|
|
18492
|
+
node: identifierNode,
|
|
18493
|
+
});
|
|
18494
|
+
}
|
|
18495
|
+
};
|
|
18496
|
+
|
|
18497
|
+
return {
|
|
18498
|
+
ArrowFunctionExpression: checkFunctionHandler,
|
|
18499
|
+
FunctionDeclaration: checkFunctionHandler,
|
|
18500
|
+
FunctionExpression: checkFunctionHandler,
|
|
18501
|
+
};
|
|
18502
|
+
},
|
|
18503
|
+
meta: {
|
|
18504
|
+
docs: { description: "Enforce components in 'views' folder end with 'View' and components in 'pages' folder end with 'Page'" },
|
|
18505
|
+
fixable: null,
|
|
18506
|
+
schema: [],
|
|
18507
|
+
type: "suggestion",
|
|
18508
|
+
},
|
|
18509
|
+
};
|
|
18510
|
+
|
|
18012
18511
|
/**
|
|
18013
18512
|
* ───────────────────────────────────────────────────────────────
|
|
18014
18513
|
* Rule: No Inline Type Definitions
|
|
@@ -18261,10 +18760,32 @@ const noInlineTypeDefinitions = {
|
|
|
18261
18760
|
const typeFormat = {
|
|
18262
18761
|
create(context) {
|
|
18263
18762
|
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
18763
|
+
const options = context.options[0] || {};
|
|
18764
|
+
const minUnionMembersForMultiline = options.minUnionMembersForMultiline !== undefined ? options.minUnionMembersForMultiline : 5;
|
|
18264
18765
|
|
|
18265
18766
|
const pascalCaseRegex = /^[A-Z][a-zA-Z0-9]*$/;
|
|
18266
18767
|
const camelCaseRegex = /^[a-z][a-zA-Z0-9]*$/;
|
|
18267
18768
|
|
|
18769
|
+
// Convert PascalCase/SCREAMING_SNAKE_CASE/snake_case to camelCase
|
|
18770
|
+
const toCamelCaseHandler = (name) => {
|
|
18771
|
+
// Handle SCREAMING_SNAKE_CASE (e.g., USER_NAME -> userName)
|
|
18772
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(name)) {
|
|
18773
|
+
return name.toLowerCase().replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
|
|
18774
|
+
}
|
|
18775
|
+
|
|
18776
|
+
// Handle snake_case (e.g., user_name -> userName)
|
|
18777
|
+
if (/_/.test(name)) {
|
|
18778
|
+
return name.toLowerCase().replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
|
|
18779
|
+
}
|
|
18780
|
+
|
|
18781
|
+
// Handle PascalCase (e.g., UserName -> userName)
|
|
18782
|
+
if (/^[A-Z]/.test(name)) {
|
|
18783
|
+
return name[0].toLowerCase() + name.slice(1);
|
|
18784
|
+
}
|
|
18785
|
+
|
|
18786
|
+
return name;
|
|
18787
|
+
};
|
|
18788
|
+
|
|
18268
18789
|
const checkTypeLiteralHandler = (declarationNode, typeLiteralNode, members) => {
|
|
18269
18790
|
if (members.length === 0) return;
|
|
18270
18791
|
|
|
@@ -18323,13 +18844,46 @@ const typeFormat = {
|
|
|
18323
18844
|
const propName = member.key.name;
|
|
18324
18845
|
|
|
18325
18846
|
if (!camelCaseRegex.test(propName)) {
|
|
18847
|
+
const fixedName = toCamelCaseHandler(propName);
|
|
18848
|
+
|
|
18326
18849
|
context.report({
|
|
18327
|
-
|
|
18850
|
+
fix: (fixer) => fixer.replaceText(member.key, fixedName),
|
|
18851
|
+
message: `Type property "${propName}" must be camelCase. Use "${fixedName}" instead.`,
|
|
18328
18852
|
node: member.key,
|
|
18329
18853
|
});
|
|
18330
18854
|
}
|
|
18331
18855
|
}
|
|
18332
18856
|
|
|
18857
|
+
// Collapse single-member nested object types to one line
|
|
18858
|
+
if (member.type === "TSPropertySignature" && member.typeAnnotation?.typeAnnotation?.type === "TSTypeLiteral") {
|
|
18859
|
+
const nestedType = member.typeAnnotation.typeAnnotation;
|
|
18860
|
+
|
|
18861
|
+
if (nestedType.members && nestedType.members.length === 1) {
|
|
18862
|
+
const nestedOpenBrace = sourceCode.getFirstToken(nestedType);
|
|
18863
|
+
const nestedCloseBrace = sourceCode.getLastToken(nestedType);
|
|
18864
|
+
const isNestedMultiLine = nestedOpenBrace.loc.end.line !== nestedCloseBrace.loc.start.line;
|
|
18865
|
+
|
|
18866
|
+
if (isNestedMultiLine) {
|
|
18867
|
+
const nestedMember = nestedType.members[0];
|
|
18868
|
+
let nestedMemberText = sourceCode.getText(nestedMember).trim();
|
|
18869
|
+
|
|
18870
|
+
// Remove trailing punctuation
|
|
18871
|
+
if (nestedMemberText.endsWith(",") || nestedMemberText.endsWith(";")) {
|
|
18872
|
+
nestedMemberText = nestedMemberText.slice(0, -1);
|
|
18873
|
+
}
|
|
18874
|
+
|
|
18875
|
+
context.report({
|
|
18876
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
18877
|
+
[nestedOpenBrace.range[0], nestedCloseBrace.range[1]],
|
|
18878
|
+
`{ ${nestedMemberText} }`,
|
|
18879
|
+
),
|
|
18880
|
+
message: "Single property nested object type should be on one line",
|
|
18881
|
+
node: nestedType,
|
|
18882
|
+
});
|
|
18883
|
+
}
|
|
18884
|
+
}
|
|
18885
|
+
}
|
|
18886
|
+
|
|
18333
18887
|
// Check for space before ? in optional properties
|
|
18334
18888
|
if (member.type === "TSPropertySignature" && member.optional) {
|
|
18335
18889
|
const keyToken = sourceCode.getFirstToken(member);
|
|
@@ -18697,13 +19251,166 @@ const typeFormat = {
|
|
|
18697
19251
|
}
|
|
18698
19252
|
});
|
|
18699
19253
|
}
|
|
19254
|
+
|
|
19255
|
+
// Check union types formatting (e.g., "a" | "b" | "c")
|
|
19256
|
+
if (node.typeAnnotation && node.typeAnnotation.type === "TSUnionType") {
|
|
19257
|
+
const unionType = node.typeAnnotation;
|
|
19258
|
+
const types = unionType.types;
|
|
19259
|
+
const minMembersForMultiline = minUnionMembersForMultiline;
|
|
19260
|
+
|
|
19261
|
+
// Get line info
|
|
19262
|
+
const typeLine = sourceCode.lines[node.loc.start.line - 1];
|
|
19263
|
+
const baseIndent = typeLine.match(/^\s*/)[0];
|
|
19264
|
+
const memberIndent = baseIndent + " ";
|
|
19265
|
+
|
|
19266
|
+
// Get the = token
|
|
19267
|
+
const equalToken = sourceCode.getTokenAfter(node.id);
|
|
19268
|
+
const firstType = types[0];
|
|
19269
|
+
const lastType = types[types.length - 1];
|
|
19270
|
+
|
|
19271
|
+
// Check if currently on single line
|
|
19272
|
+
const isCurrentlySingleLine = firstType.loc.start.line === lastType.loc.end.line &&
|
|
19273
|
+
equalToken.loc.end.line === firstType.loc.start.line;
|
|
19274
|
+
|
|
19275
|
+
// Check if currently properly multiline (= on its own conceptually, first type on new line)
|
|
19276
|
+
const isFirstTypeOnNewLine = firstType.loc.start.line > equalToken.loc.end.line;
|
|
19277
|
+
|
|
19278
|
+
if (types.length >= minMembersForMultiline) {
|
|
19279
|
+
// Should be multiline format
|
|
19280
|
+
// Check if needs reformatting
|
|
19281
|
+
let needsReformat = false;
|
|
19282
|
+
|
|
19283
|
+
// Check if first type is on new line after =
|
|
19284
|
+
if (!isFirstTypeOnNewLine) {
|
|
19285
|
+
needsReformat = true;
|
|
19286
|
+
}
|
|
19287
|
+
|
|
19288
|
+
// Check if each type is on its own line
|
|
19289
|
+
if (!needsReformat) {
|
|
19290
|
+
for (let i = 1; i < types.length; i++) {
|
|
19291
|
+
if (types[i].loc.start.line === types[i - 1].loc.end.line) {
|
|
19292
|
+
needsReformat = true;
|
|
19293
|
+
break;
|
|
19294
|
+
}
|
|
19295
|
+
}
|
|
19296
|
+
}
|
|
19297
|
+
|
|
19298
|
+
// Check proper indentation and | placement
|
|
19299
|
+
if (!needsReformat) {
|
|
19300
|
+
for (let i = 1; i < types.length; i++) {
|
|
19301
|
+
const pipeToken = sourceCode.getTokenBefore(types[i]);
|
|
19302
|
+
|
|
19303
|
+
if (pipeToken && pipeToken.value === "|") {
|
|
19304
|
+
// | should be at start of line (after indent)
|
|
19305
|
+
if (pipeToken.loc.start.line !== types[i].loc.start.line) {
|
|
19306
|
+
needsReformat = true;
|
|
19307
|
+
break;
|
|
19308
|
+
}
|
|
19309
|
+
}
|
|
19310
|
+
}
|
|
19311
|
+
}
|
|
19312
|
+
|
|
19313
|
+
if (needsReformat) {
|
|
19314
|
+
// Build the correct multiline format
|
|
19315
|
+
const formattedTypes = types.map((type, index) => {
|
|
19316
|
+
const typeText = sourceCode.getText(type);
|
|
19317
|
+
|
|
19318
|
+
if (index === 0) {
|
|
19319
|
+
return memberIndent + typeText;
|
|
19320
|
+
}
|
|
19321
|
+
|
|
19322
|
+
return memberIndent + "| " + typeText;
|
|
19323
|
+
}).join("\n");
|
|
19324
|
+
|
|
19325
|
+
const newTypeText = `= \n${formattedTypes}`;
|
|
19326
|
+
|
|
19327
|
+
context.report({
|
|
19328
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
19329
|
+
[equalToken.range[0], lastType.range[1]],
|
|
19330
|
+
newTypeText,
|
|
19331
|
+
),
|
|
19332
|
+
message: `Union type with ${types.length} members should be multiline with each member on its own line`,
|
|
19333
|
+
node: unionType,
|
|
19334
|
+
});
|
|
19335
|
+
}
|
|
19336
|
+
} else {
|
|
19337
|
+
// Should be single line format (less than 5 members)
|
|
19338
|
+
if (!isCurrentlySingleLine) {
|
|
19339
|
+
// Build single line format
|
|
19340
|
+
const typeTexts = types.map((type) => sourceCode.getText(type));
|
|
19341
|
+
const singleLineText = `= ${typeTexts.join(" | ")}`;
|
|
19342
|
+
|
|
19343
|
+
context.report({
|
|
19344
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
19345
|
+
[equalToken.range[0], lastType.range[1]],
|
|
19346
|
+
singleLineText,
|
|
19347
|
+
),
|
|
19348
|
+
message: `Union type with ${types.length} members should be on a single line`,
|
|
19349
|
+
node: unionType,
|
|
19350
|
+
});
|
|
19351
|
+
}
|
|
19352
|
+
}
|
|
19353
|
+
}
|
|
19354
|
+
},
|
|
19355
|
+
// Handle inline type literals (e.g., in function parameters)
|
|
19356
|
+
TSTypeLiteral(node) {
|
|
19357
|
+
// Skip if already handled by TSTypeAliasDeclaration or TSAsExpression
|
|
19358
|
+
if (node.parent?.type === "TSTypeAliasDeclaration") return;
|
|
19359
|
+
if (node.parent?.type === "TSAsExpression") return;
|
|
19360
|
+
|
|
19361
|
+
// Check for single-member nested object types that should be collapsed
|
|
19362
|
+
if (node.members) {
|
|
19363
|
+
node.members.forEach((member) => {
|
|
19364
|
+
if (member.type === "TSPropertySignature" && member.typeAnnotation?.typeAnnotation?.type === "TSTypeLiteral") {
|
|
19365
|
+
const nestedType = member.typeAnnotation.typeAnnotation;
|
|
19366
|
+
|
|
19367
|
+
if (nestedType.members && nestedType.members.length === 1) {
|
|
19368
|
+
const nestedOpenBrace = sourceCode.getFirstToken(nestedType);
|
|
19369
|
+
const nestedCloseBrace = sourceCode.getLastToken(nestedType);
|
|
19370
|
+
const isNestedMultiLine = nestedOpenBrace.loc.end.line !== nestedCloseBrace.loc.start.line;
|
|
19371
|
+
|
|
19372
|
+
if (isNestedMultiLine) {
|
|
19373
|
+
const nestedMember = nestedType.members[0];
|
|
19374
|
+
let nestedMemberText = sourceCode.getText(nestedMember).trim();
|
|
19375
|
+
|
|
19376
|
+
// Remove trailing punctuation
|
|
19377
|
+
if (nestedMemberText.endsWith(",") || nestedMemberText.endsWith(";")) {
|
|
19378
|
+
nestedMemberText = nestedMemberText.slice(0, -1);
|
|
19379
|
+
}
|
|
19380
|
+
|
|
19381
|
+
context.report({
|
|
19382
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
19383
|
+
[nestedOpenBrace.range[0], nestedCloseBrace.range[1]],
|
|
19384
|
+
`{ ${nestedMemberText} }`,
|
|
19385
|
+
),
|
|
19386
|
+
message: "Single property nested object type should be on one line",
|
|
19387
|
+
node: nestedType,
|
|
19388
|
+
});
|
|
19389
|
+
}
|
|
19390
|
+
}
|
|
19391
|
+
}
|
|
19392
|
+
});
|
|
19393
|
+
}
|
|
18700
19394
|
},
|
|
18701
19395
|
};
|
|
18702
19396
|
},
|
|
18703
19397
|
meta: {
|
|
18704
|
-
docs: { description: "Enforce type naming (PascalCase + Type suffix), camelCase properties, proper formatting, and trailing commas" },
|
|
19398
|
+
docs: { description: "Enforce type naming (PascalCase + Type suffix), camelCase properties, proper formatting, union type formatting, and trailing commas" },
|
|
18705
19399
|
fixable: "code",
|
|
18706
|
-
schema: [
|
|
19400
|
+
schema: [
|
|
19401
|
+
{
|
|
19402
|
+
additionalProperties: false,
|
|
19403
|
+
properties: {
|
|
19404
|
+
minUnionMembersForMultiline: {
|
|
19405
|
+
default: 5,
|
|
19406
|
+
description: "Minimum number of union members to require multiline format",
|
|
19407
|
+
minimum: 2,
|
|
19408
|
+
type: "integer",
|
|
19409
|
+
},
|
|
19410
|
+
},
|
|
19411
|
+
type: "object",
|
|
19412
|
+
},
|
|
19413
|
+
],
|
|
18707
19414
|
type: "suggestion",
|
|
18708
19415
|
},
|
|
18709
19416
|
};
|
|
@@ -20316,14 +21023,27 @@ const enumFormat = {
|
|
|
20316
21023
|
});
|
|
20317
21024
|
}
|
|
20318
21025
|
|
|
21026
|
+
// Convert camelCase/PascalCase to UPPER_SNAKE_CASE
|
|
21027
|
+
const toUpperSnakeCaseHandler = (name) => {
|
|
21028
|
+
// Insert underscore before each uppercase letter (except the first)
|
|
21029
|
+
// Then convert to uppercase
|
|
21030
|
+
return name
|
|
21031
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
21032
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
|
|
21033
|
+
.toUpperCase();
|
|
21034
|
+
};
|
|
21035
|
+
|
|
20319
21036
|
members.forEach((member, index) => {
|
|
20320
21037
|
// Check member name is UPPER_CASE
|
|
20321
21038
|
if (member.id && member.id.type === "Identifier") {
|
|
20322
21039
|
const memberName = member.id.name;
|
|
20323
21040
|
|
|
20324
21041
|
if (!upperCaseRegex.test(memberName)) {
|
|
21042
|
+
const fixedName = toUpperSnakeCaseHandler(memberName);
|
|
21043
|
+
|
|
20325
21044
|
context.report({
|
|
20326
|
-
|
|
21045
|
+
fix: (fixer) => fixer.replaceText(member.id, fixedName),
|
|
21046
|
+
message: `Enum member "${memberName}" must be UPPER_CASE (e.g., ${fixedName})`,
|
|
20327
21047
|
node: member.id,
|
|
20328
21048
|
});
|
|
20329
21049
|
}
|
|
@@ -20476,6 +21196,26 @@ const interfaceFormat = {
|
|
|
20476
21196
|
const pascalCaseRegex = /^[A-Z][a-zA-Z0-9]*$/;
|
|
20477
21197
|
const camelCaseRegex = /^[a-z][a-zA-Z0-9]*$/;
|
|
20478
21198
|
|
|
21199
|
+
// Convert PascalCase/SCREAMING_SNAKE_CASE/snake_case to camelCase
|
|
21200
|
+
const toCamelCaseHandler = (name) => {
|
|
21201
|
+
// Handle SCREAMING_SNAKE_CASE (e.g., USER_NAME -> userName)
|
|
21202
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(name)) {
|
|
21203
|
+
return name.toLowerCase().replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
|
|
21204
|
+
}
|
|
21205
|
+
|
|
21206
|
+
// Handle snake_case (e.g., user_name -> userName)
|
|
21207
|
+
if (/_/.test(name)) {
|
|
21208
|
+
return name.toLowerCase().replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
|
|
21209
|
+
}
|
|
21210
|
+
|
|
21211
|
+
// Handle PascalCase (e.g., UserName -> userName)
|
|
21212
|
+
if (/^[A-Z]/.test(name)) {
|
|
21213
|
+
return name[0].toLowerCase() + name.slice(1);
|
|
21214
|
+
}
|
|
21215
|
+
|
|
21216
|
+
return name;
|
|
21217
|
+
};
|
|
21218
|
+
|
|
20479
21219
|
return {
|
|
20480
21220
|
TSInterfaceDeclaration(node) {
|
|
20481
21221
|
const interfaceName = node.id.name;
|
|
@@ -20553,17 +21293,41 @@ const interfaceFormat = {
|
|
|
20553
21293
|
}
|
|
20554
21294
|
|
|
20555
21295
|
// For single member, should be on one line without trailing punctuation
|
|
21296
|
+
// But skip if the property has a nested object type with 2+ members
|
|
20556
21297
|
if (members.length === 1) {
|
|
20557
21298
|
const member = members[0];
|
|
20558
|
-
const memberText = sourceCode.getText(member);
|
|
20559
21299
|
const isMultiLine = openBraceToken.loc.end.line !== closeBraceToken.loc.start.line;
|
|
20560
21300
|
|
|
20561
|
-
if
|
|
20562
|
-
|
|
20563
|
-
|
|
21301
|
+
// Check if property has nested object type
|
|
21302
|
+
const nestedType = member.typeAnnotation?.typeAnnotation;
|
|
21303
|
+
const hasNestedType = nestedType?.type === "TSTypeLiteral";
|
|
21304
|
+
const hasMultiMemberNestedType = hasNestedType && nestedType.members?.length >= 2;
|
|
21305
|
+
const hasSingleMemberNestedType = hasNestedType && nestedType.members?.length === 1;
|
|
21306
|
+
|
|
21307
|
+
if (isMultiLine && !hasMultiMemberNestedType) {
|
|
21308
|
+
// Build the collapsed text, handling nested types specially
|
|
21309
|
+
let cleanText;
|
|
21310
|
+
|
|
21311
|
+
if (hasSingleMemberNestedType) {
|
|
21312
|
+
// Collapse nested type first, then build the member text
|
|
21313
|
+
const nestedMember = nestedType.members[0];
|
|
21314
|
+
let nestedMemberText = sourceCode.getText(nestedMember).trim();
|
|
21315
|
+
|
|
21316
|
+
if (nestedMemberText.endsWith(",") || nestedMemberText.endsWith(";")) {
|
|
21317
|
+
nestedMemberText = nestedMemberText.slice(0, -1);
|
|
21318
|
+
}
|
|
21319
|
+
|
|
21320
|
+
// Build: propName: { nestedProp: type }
|
|
21321
|
+
const propName = member.key.name;
|
|
21322
|
+
const optionalMark = member.optional ? "?" : "";
|
|
21323
|
+
|
|
21324
|
+
cleanText = `${propName}${optionalMark}: { ${nestedMemberText} }`;
|
|
21325
|
+
} else {
|
|
21326
|
+
cleanText = sourceCode.getText(member).trim();
|
|
20564
21327
|
|
|
20565
|
-
|
|
20566
|
-
|
|
21328
|
+
if (cleanText.endsWith(",") || cleanText.endsWith(";")) {
|
|
21329
|
+
cleanText = cleanText.slice(0, -1);
|
|
21330
|
+
}
|
|
20567
21331
|
}
|
|
20568
21332
|
|
|
20569
21333
|
const newInterfaceText = `{ ${cleanText} }`;
|
|
@@ -20581,6 +21345,8 @@ const interfaceFormat = {
|
|
|
20581
21345
|
}
|
|
20582
21346
|
|
|
20583
21347
|
// Check for trailing comma/semicolon in single-line single member
|
|
21348
|
+
const memberText = sourceCode.getText(member);
|
|
21349
|
+
|
|
20584
21350
|
if (memberText.trimEnd().endsWith(",") || memberText.trimEnd().endsWith(";")) {
|
|
20585
21351
|
const punctIndex = Math.max(memberText.lastIndexOf(","), memberText.lastIndexOf(";"));
|
|
20586
21352
|
|
|
@@ -20612,13 +21378,46 @@ const interfaceFormat = {
|
|
|
20612
21378
|
const propName = member.key.name;
|
|
20613
21379
|
|
|
20614
21380
|
if (!camelCaseRegex.test(propName)) {
|
|
21381
|
+
const fixedName = toCamelCaseHandler(propName);
|
|
21382
|
+
|
|
20615
21383
|
context.report({
|
|
20616
|
-
|
|
21384
|
+
fix: (fixer) => fixer.replaceText(member.key, fixedName),
|
|
21385
|
+
message: `Interface property "${propName}" must be camelCase. Use "${fixedName}" instead.`,
|
|
20617
21386
|
node: member.key,
|
|
20618
21387
|
});
|
|
20619
21388
|
}
|
|
20620
21389
|
}
|
|
20621
21390
|
|
|
21391
|
+
// Collapse single-member nested object types to one line
|
|
21392
|
+
if (member.type === "TSPropertySignature" && member.typeAnnotation?.typeAnnotation?.type === "TSTypeLiteral") {
|
|
21393
|
+
const nestedType = member.typeAnnotation.typeAnnotation;
|
|
21394
|
+
|
|
21395
|
+
if (nestedType.members && nestedType.members.length === 1) {
|
|
21396
|
+
const nestedOpenBrace = sourceCode.getFirstToken(nestedType);
|
|
21397
|
+
const nestedCloseBrace = sourceCode.getLastToken(nestedType);
|
|
21398
|
+
const isNestedMultiLine = nestedOpenBrace.loc.end.line !== nestedCloseBrace.loc.start.line;
|
|
21399
|
+
|
|
21400
|
+
if (isNestedMultiLine) {
|
|
21401
|
+
const nestedMember = nestedType.members[0];
|
|
21402
|
+
let nestedMemberText = sourceCode.getText(nestedMember).trim();
|
|
21403
|
+
|
|
21404
|
+
// Remove trailing punctuation
|
|
21405
|
+
if (nestedMemberText.endsWith(",") || nestedMemberText.endsWith(";")) {
|
|
21406
|
+
nestedMemberText = nestedMemberText.slice(0, -1);
|
|
21407
|
+
}
|
|
21408
|
+
|
|
21409
|
+
context.report({
|
|
21410
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
21411
|
+
[nestedOpenBrace.range[0], nestedCloseBrace.range[1]],
|
|
21412
|
+
`{ ${nestedMemberText} }`,
|
|
21413
|
+
),
|
|
21414
|
+
message: "Single property nested object type should be on one line",
|
|
21415
|
+
node: nestedType,
|
|
21416
|
+
});
|
|
21417
|
+
}
|
|
21418
|
+
}
|
|
21419
|
+
}
|
|
21420
|
+
|
|
20622
21421
|
// Check for space before ? in optional properties
|
|
20623
21422
|
if (member.type === "TSPropertySignature" && member.optional) {
|
|
20624
21423
|
const keyToken = sourceCode.getFirstToken(member);
|
|
@@ -20864,6 +21663,7 @@ export default {
|
|
|
20864
21663
|
// Component rules
|
|
20865
21664
|
"component-props-destructure": componentPropsDestructure,
|
|
20866
21665
|
"component-props-inline-type": componentPropsInlineType,
|
|
21666
|
+
"folder-component-suffix": folderComponentSuffix,
|
|
20867
21667
|
"svg-component-icon-naming": svgComponentIconNaming,
|
|
20868
21668
|
|
|
20869
21669
|
// React rules
|
|
@@ -20935,6 +21735,7 @@ export default {
|
|
|
20935
21735
|
"enum-format": enumFormat,
|
|
20936
21736
|
"interface-format": interfaceFormat,
|
|
20937
21737
|
"no-inline-type-definitions": noInlineTypeDefinitions,
|
|
21738
|
+
"prop-naming-convention": propNamingConvention,
|
|
20938
21739
|
"type-annotation-spacing": typeAnnotationSpacing,
|
|
20939
21740
|
"type-format": typeFormat,
|
|
20940
21741
|
"typescript-definition-location": typescriptDefinitionLocation,
|