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