@blumintinc/eslint-plugin-blumint 1.8.1 → 1.8.2
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/lib/index.js +1 -1
- package/lib/rules/enforce-assertSafe-object-key.js +1 -1
- package/lib/rules/enforce-id-capitalization.js +43 -3
- package/lib/rules/enforce-object-literal-as-const.js +37 -0
- package/lib/rules/enforce-positive-naming.js +87 -163
- package/lib/rules/enforce-singular-type-names.js +1 -1
- package/lib/rules/no-type-assertion-returns.js +143 -128
- package/lib/rules/no-unused-props.js +100 -7
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -26,7 +26,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
26
26
|
const addAssertSafeImport = (fixer) => {
|
|
27
27
|
const program = context.getSourceCode().ast;
|
|
28
28
|
const firstImport = program.body.find((node) => node.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
29
|
-
const importStatement = "import { assertSafe } from '
|
|
29
|
+
const importStatement = "import { assertSafe } from 'functions/src/util/assertSafe';\n";
|
|
30
30
|
if (firstImport) {
|
|
31
31
|
return fixer.insertTextBefore(firstImport, importStatement);
|
|
32
32
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.enforceIdCapitalization = void 0;
|
|
4
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
6
|
/**
|
|
6
7
|
* This rule ensures consistency in user-facing text by enforcing the use of "ID"
|
|
7
8
|
* instead of "id" when referring to identifiers in UI labels, instructions,
|
|
@@ -26,12 +27,51 @@ exports.enforceIdCapitalization = (0, createRule_1.createRule)({
|
|
|
26
27
|
// Regular expression to match standalone "id" surrounded by whitespace or punctuation
|
|
27
28
|
// This ensures we only match "id" as a word, not as part of another word
|
|
28
29
|
const idRegex = /(^|\s|[.,;:!?'"()\[\]{}])id(\s|$|[.,;:!?'"()\[\]{}])/g;
|
|
30
|
+
/**
|
|
31
|
+
* Check if a node is in a context that should be excluded from the rule
|
|
32
|
+
* (e.g., parameter names, property names, type definitions)
|
|
33
|
+
*/
|
|
34
|
+
function isExcludedContext(node) {
|
|
35
|
+
// Check if the node is a property of an object pattern (destructuring)
|
|
36
|
+
if (node.parent &&
|
|
37
|
+
(node.parent.type === utils_1.AST_NODE_TYPES.Property ||
|
|
38
|
+
node.parent.type === utils_1.AST_NODE_TYPES.PropertyDefinition) &&
|
|
39
|
+
node.parent.key === node) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
// Check if the node is a parameter name
|
|
43
|
+
if (node.parent &&
|
|
44
|
+
(node.parent.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
45
|
+
node.parent.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
46
|
+
node.parent.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) &&
|
|
47
|
+
node.parent.params.includes(node)) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
// Check if the node is part of a type definition
|
|
51
|
+
if (node.parent &&
|
|
52
|
+
(node.parent.type === utils_1.AST_NODE_TYPES.TSPropertySignature ||
|
|
53
|
+
node.parent.type === utils_1.AST_NODE_TYPES.TSParameterProperty ||
|
|
54
|
+
node.parent.type === utils_1.AST_NODE_TYPES.TSTypeAnnotation ||
|
|
55
|
+
node.parent.type === utils_1.AST_NODE_TYPES.TSTypeReference)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
// Check if the node is a variable name
|
|
59
|
+
if (node.parent &&
|
|
60
|
+
node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
61
|
+
node.parent.id === node) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
29
66
|
/**
|
|
30
67
|
* Check if a string contains "id" as a standalone word and report if found
|
|
31
68
|
*/
|
|
32
69
|
function checkForIdInString(node, value) {
|
|
33
70
|
if (typeof value !== 'string')
|
|
34
71
|
return;
|
|
72
|
+
// Skip checking if the node is in an excluded context
|
|
73
|
+
if (isExcludedContext(node))
|
|
74
|
+
return;
|
|
35
75
|
// Reset the regex lastIndex to ensure consistent behavior
|
|
36
76
|
idRegex.lastIndex = 0;
|
|
37
77
|
// Check if the string contains "id" as a standalone word
|
|
@@ -45,13 +85,13 @@ exports.enforceIdCapitalization = (0, createRule_1.createRule)({
|
|
|
45
85
|
node,
|
|
46
86
|
messageId: 'enforceIdCapitalization',
|
|
47
87
|
fix: (fixer) => {
|
|
48
|
-
if (node.type ===
|
|
88
|
+
if (node.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
49
89
|
return fixer.replaceText(node, JSON.stringify(fixedText));
|
|
50
90
|
}
|
|
51
|
-
else if (node.type ===
|
|
91
|
+
else if (node.type === utils_1.AST_NODE_TYPES.JSXText) {
|
|
52
92
|
return fixer.replaceText(node, fixedText);
|
|
53
93
|
}
|
|
54
|
-
else if (node.type ===
|
|
94
|
+
else if (node.type === utils_1.AST_NODE_TYPES.TemplateElement) {
|
|
55
95
|
return fixer.replaceText(node, fixedText);
|
|
56
96
|
}
|
|
57
97
|
return fixer.replaceText(node, JSON.stringify(fixedText));
|
|
@@ -18,6 +18,36 @@ exports.enforceObjectLiteralAsConst = (0, createRule_1.createRule)({
|
|
|
18
18
|
},
|
|
19
19
|
defaultOptions: [],
|
|
20
20
|
create(context) {
|
|
21
|
+
/**
|
|
22
|
+
* Checks if the node is inside a React hook like useMemo
|
|
23
|
+
*/
|
|
24
|
+
function isInsideReactHook(ancestors) {
|
|
25
|
+
for (let i = 0; i < ancestors.length; i++) {
|
|
26
|
+
const ancestor = ancestors[i];
|
|
27
|
+
// Check for CallExpression (function call)
|
|
28
|
+
if (ancestor.type === 'CallExpression') {
|
|
29
|
+
const callee = ancestor.callee;
|
|
30
|
+
// Check if it's a hook like useMemo, useCallback, etc.
|
|
31
|
+
if (callee.type === 'Identifier' &&
|
|
32
|
+
(callee.name === 'useMemo' ||
|
|
33
|
+
callee.name === 'useCallback' ||
|
|
34
|
+
callee.name.startsWith('use'))) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Checks if an array contains object literals that might be used as component props
|
|
43
|
+
*/
|
|
44
|
+
function isArrayWithObjectLiterals(node) {
|
|
45
|
+
if (node.type !== 'ArrayExpression') {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
// Check if any element in the array is an object literal
|
|
49
|
+
return node.elements.some((elem) => elem !== null && elem.type === 'ObjectExpression');
|
|
50
|
+
}
|
|
21
51
|
return {
|
|
22
52
|
ReturnStatement(node) {
|
|
23
53
|
// Skip if there's no argument in the return statement
|
|
@@ -66,6 +96,13 @@ exports.enforceObjectLiteralAsConst = (0, createRule_1.createRule)({
|
|
|
66
96
|
argument.elements.some((elem) => elem !== null && elem.type === 'SpreadElement'))) {
|
|
67
97
|
return;
|
|
68
98
|
}
|
|
99
|
+
// Skip arrays with object literals inside React hooks (likely component props)
|
|
100
|
+
if (isInsideReactHook(ancestors) &&
|
|
101
|
+
isArrayWithObjectLiterals(argument.type === 'TSAsExpression'
|
|
102
|
+
? argument.expression
|
|
103
|
+
: argument)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
69
106
|
// Report the issue and provide a fix
|
|
70
107
|
context.report({
|
|
71
108
|
node,
|
|
@@ -3,60 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.enforcePositiveNaming = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
-
// Common negative prefixes
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
'incomplete',
|
|
12
|
-
'unpaid',
|
|
13
|
-
'inactive',
|
|
14
|
-
'disallowed',
|
|
15
|
-
'unauthorized',
|
|
16
|
-
'unverified',
|
|
17
|
-
'prevent',
|
|
18
|
-
'avoid',
|
|
19
|
-
'block',
|
|
20
|
-
'deny',
|
|
21
|
-
'reject',
|
|
22
|
-
'exclude',
|
|
23
|
-
'fail',
|
|
24
|
-
'missing',
|
|
25
|
-
'forbidden',
|
|
26
|
-
'impossible',
|
|
27
|
-
'error',
|
|
28
|
-
'broken',
|
|
29
|
-
'banned',
|
|
30
|
-
'restricted',
|
|
31
|
-
'limitation',
|
|
32
|
-
'limitation',
|
|
33
|
-
'blocked',
|
|
34
|
-
'prohibited',
|
|
35
|
-
];
|
|
36
|
-
// Whitelist of acceptable negative terms (technical terms, common patterns)
|
|
37
|
-
const ALLOWED_NEGATIVE_TERMS = new Set([
|
|
38
|
-
'isNaN',
|
|
39
|
-
'isNull',
|
|
40
|
-
'isUndefined',
|
|
41
|
-
'isEmpty',
|
|
42
|
-
'isOffline',
|
|
43
|
-
'isMissing',
|
|
44
|
-
'isOutOfStock',
|
|
45
|
-
'isOutOfBounds',
|
|
46
|
-
'isOffPeak',
|
|
47
|
-
'isNoticeable',
|
|
48
|
-
'hasNotification',
|
|
49
|
-
'isNoteworthy',
|
|
50
|
-
'isNone',
|
|
51
|
-
'isNegative',
|
|
52
|
-
'isNeutral',
|
|
53
|
-
'isNotification',
|
|
54
|
-
'isNote',
|
|
55
|
-
'hasNote',
|
|
56
|
-
]);
|
|
57
|
-
// Map of negative terms to suggested positive alternatives
|
|
58
|
-
const POSITIVE_ALTERNATIVES = {
|
|
59
|
-
// Prefixes
|
|
6
|
+
// Common negative prefixes for boolean variables
|
|
7
|
+
const BOOLEAN_NEGATIVE_PREFIXES = ['not', 'no', 'non', 'un', 'in', 'dis'];
|
|
8
|
+
// Map of negative boolean terms to suggested positive alternatives
|
|
9
|
+
const BOOLEAN_POSITIVE_ALTERNATIVES = {
|
|
10
|
+
// Boolean prefixes
|
|
60
11
|
'isNot': ['is'],
|
|
61
12
|
'isUn': ['is'],
|
|
62
13
|
'isDis': ['is'],
|
|
@@ -68,54 +19,13 @@ const POSITIVE_ALTERNATIVES = {
|
|
|
68
19
|
'shouldNot': ['should'],
|
|
69
20
|
'willNot': ['will'],
|
|
70
21
|
'doesNot': ['does'],
|
|
71
|
-
// Words
|
|
72
|
-
'isInvalid': ['isValid'],
|
|
73
|
-
'isDisabled': ['isEnabled'],
|
|
74
|
-
'isIncomplete': ['isComplete'],
|
|
75
|
-
'isUnpaid': ['isPaid', 'hasPaid'],
|
|
76
|
-
'isInactive': ['isActive'],
|
|
77
|
-
'isDisallowed': ['isAllowed'],
|
|
78
|
-
'isUnauthorized': ['isAuthorized'],
|
|
79
|
-
'isUnverified': ['isVerified'],
|
|
80
|
-
'isImpossible': ['isPossible'],
|
|
81
|
-
'isError': ['isSuccess', 'isValid'],
|
|
82
|
-
'isBroken': ['isWorking', 'isFunctional'],
|
|
83
|
-
'isBanned': ['isAllowed', 'isPermitted'],
|
|
84
|
-
'isRestricted': ['isAllowed', 'isAccessible'],
|
|
85
|
-
'isBlocked': ['isAllowed', 'isAccessible'],
|
|
86
|
-
'isProhibited': ['isAllowed', 'isPermitted'],
|
|
87
|
-
'prevent': ['allow', 'enable'],
|
|
88
|
-
'avoid': ['use', 'prefer'],
|
|
89
|
-
'block': ['allow', 'permit'],
|
|
90
|
-
'deny': ['allow', 'grant'],
|
|
91
|
-
'reject': ['accept', 'approve'],
|
|
92
|
-
'exclude': ['include'],
|
|
93
|
-
'fail': ['succeed', 'pass'],
|
|
94
|
-
'missing': ['present', 'available'],
|
|
95
|
-
'forbidden': ['allowed', 'permitted'],
|
|
96
|
-
'disabled': ['enabled'],
|
|
97
|
-
'incomplete': ['complete'],
|
|
98
|
-
'invalid': ['valid'],
|
|
99
|
-
'disallowed': ['allowed'],
|
|
100
|
-
'unauthorized': ['authorized'],
|
|
101
|
-
'unverified': ['verified'],
|
|
102
|
-
'inactive': ['active'],
|
|
103
|
-
'disabledFeatures': ['enabledFeatures'],
|
|
104
|
-
'inactiveUsers': ['activeUsers'],
|
|
105
|
-
'disableFeature': ['enableFeature'],
|
|
106
|
-
'disableAccount': ['enableAccount'],
|
|
107
|
-
'blockUser': ['allowUser'],
|
|
108
|
-
'preventAccess': ['allowAccess', 'enableAccess'],
|
|
109
|
-
'restrictAccess': ['allowAccess', 'grantAccess'],
|
|
110
|
-
'limitations': ['capabilities', 'features'],
|
|
111
|
-
'limitation': ['capability', 'feature'],
|
|
112
22
|
};
|
|
113
23
|
exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
114
24
|
name: 'enforce-positive-naming',
|
|
115
25
|
meta: {
|
|
116
26
|
type: 'suggestion',
|
|
117
27
|
docs: {
|
|
118
|
-
description: 'Enforce positive
|
|
28
|
+
description: 'Enforce positive naming for boolean variables and avoid negations',
|
|
119
29
|
recommended: 'error',
|
|
120
30
|
},
|
|
121
31
|
schema: [],
|
|
@@ -141,72 +51,43 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
141
51
|
return {};
|
|
142
52
|
}
|
|
143
53
|
/**
|
|
144
|
-
* Check if a name has negative
|
|
54
|
+
* Check if a name has boolean negative naming
|
|
145
55
|
*/
|
|
146
|
-
function
|
|
147
|
-
// Skip checking if the name is in the whitelist
|
|
148
|
-
if (ALLOWED_NEGATIVE_TERMS.has(name)) {
|
|
149
|
-
return { isNegative: false, alternatives: [] };
|
|
150
|
-
}
|
|
56
|
+
function hasBooleanNegativeNaming(name) {
|
|
151
57
|
// Check for exact matches in our alternatives map first
|
|
152
|
-
if (
|
|
153
|
-
return { isNegative: true, alternatives:
|
|
58
|
+
if (BOOLEAN_POSITIVE_ALTERNATIVES[name]) {
|
|
59
|
+
return { isNegative: true, alternatives: BOOLEAN_POSITIVE_ALTERNATIVES[name] };
|
|
154
60
|
}
|
|
155
|
-
// Check for negative prefixes
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
61
|
+
// Check for negative prefixes in boolean-like variables
|
|
62
|
+
if (name.startsWith('is') || name.startsWith('has') || name.startsWith('can') ||
|
|
63
|
+
name.startsWith('should') || name.startsWith('will') || name.startsWith('does')) {
|
|
64
|
+
for (const prefix of BOOLEAN_NEGATIVE_PREFIXES) {
|
|
65
|
+
// Check for patterns like isNot, hasNo, canNot, etc.
|
|
66
|
+
const prefixPatterns = [
|
|
67
|
+
{ pattern: new RegExp(`^is${prefix}`, 'i'), key: `is${prefix.charAt(0).toUpperCase() + prefix.slice(1)}` },
|
|
68
|
+
{ pattern: new RegExp(`^has${prefix}`, 'i'), key: `has${prefix.charAt(0).toUpperCase() + prefix.slice(1)}` },
|
|
69
|
+
{ pattern: new RegExp(`^can${prefix}`, 'i'), key: `can${prefix.charAt(0).toUpperCase() + prefix.slice(1)}` },
|
|
70
|
+
{ pattern: new RegExp(`^should${prefix}`, 'i'), key: `should${prefix.charAt(0).toUpperCase() + prefix.slice(1)}` },
|
|
71
|
+
{ pattern: new RegExp(`^will${prefix}`, 'i'), key: `will${prefix.charAt(0).toUpperCase() + prefix.slice(1)}` },
|
|
72
|
+
{ pattern: new RegExp(`^does${prefix}`, 'i'), key: `does${prefix.charAt(0).toUpperCase() + prefix.slice(1)}` },
|
|
73
|
+
];
|
|
74
|
+
for (const { pattern, key } of prefixPatterns) {
|
|
75
|
+
if (pattern.test(name)) {
|
|
76
|
+
// If we have a direct match for this pattern (like isNotVerified -> isVerified)
|
|
77
|
+
const directMatch = BOOLEAN_POSITIVE_ALTERNATIVES[name];
|
|
78
|
+
if (directMatch) {
|
|
79
|
+
return { isNegative: true, alternatives: directMatch };
|
|
80
|
+
}
|
|
81
|
+
const alternatives = BOOLEAN_POSITIVE_ALTERNATIVES[key] || [];
|
|
82
|
+
if (alternatives.length > 0) {
|
|
83
|
+
// Suggest the positive version with the rest of the name
|
|
84
|
+
const restOfName = name.replace(pattern, '');
|
|
85
|
+
const suggestedAlternatives = alternatives.map(alt => `${alt}${restOfName.charAt(0).toUpperCase() + restOfName.slice(1)}`);
|
|
86
|
+
return { isNegative: true, alternatives: suggestedAlternatives };
|
|
87
|
+
}
|
|
88
|
+
return { isNegative: true, alternatives: ['a positive alternative'] };
|
|
179
89
|
}
|
|
180
|
-
return { isNegative: true, alternatives: ['a positive alternative'] };
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
// Check for exact negative words
|
|
185
|
-
for (const word of NEGATIVE_WORDS) {
|
|
186
|
-
if (name.toLowerCase() === word.toLowerCase()) {
|
|
187
|
-
const alternatives = POSITIVE_ALTERNATIVES[word] || [];
|
|
188
|
-
return {
|
|
189
|
-
isNegative: true,
|
|
190
|
-
alternatives: alternatives.length ? alternatives : ['a positive alternative']
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
// Check for negative words in camelCase/PascalCase
|
|
195
|
-
for (const word of NEGATIVE_WORDS) {
|
|
196
|
-
// Match the word at the beginning or after a capital letter
|
|
197
|
-
const pattern = new RegExp(`(^|[A-Z])${word}($|[A-Z])`, 'i');
|
|
198
|
-
if (pattern.test(name)) {
|
|
199
|
-
// For compound names like isInvalid, check if we have a specific suggestion
|
|
200
|
-
const exactMatch = `is${word.charAt(0).toUpperCase() + word.slice(1)}`;
|
|
201
|
-
if (POSITIVE_ALTERNATIVES[exactMatch]) {
|
|
202
|
-
return { isNegative: true, alternatives: POSITIVE_ALTERNATIVES[exactMatch] };
|
|
203
90
|
}
|
|
204
|
-
// Otherwise suggest general alternatives for the negative word
|
|
205
|
-
const alternatives = POSITIVE_ALTERNATIVES[word] || [];
|
|
206
|
-
return {
|
|
207
|
-
isNegative: true,
|
|
208
|
-
alternatives: alternatives.length ? alternatives : ['a positive alternative']
|
|
209
|
-
};
|
|
210
91
|
}
|
|
211
92
|
}
|
|
212
93
|
return { isNegative: false, alternatives: [] };
|
|
@@ -220,14 +101,41 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
220
101
|
}
|
|
221
102
|
return String(alternatives);
|
|
222
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Check if a node is likely to be a boolean
|
|
106
|
+
*/
|
|
107
|
+
function isBooleanLike(node) {
|
|
108
|
+
// Check if the node has a boolean type annotation
|
|
109
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSTypeAnnotation &&
|
|
110
|
+
node.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSBooleanKeyword) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
// Check if the node is initialized with a boolean literal
|
|
114
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
115
|
+
node.parent.init?.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
116
|
+
typeof node.parent.init.value === 'boolean') {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
// Check if the node has a name that suggests it's a boolean
|
|
120
|
+
if (node.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
121
|
+
(node.name.startsWith('is') || node.name.startsWith('has') ||
|
|
122
|
+
node.name.startsWith('can') || node.name.startsWith('should') ||
|
|
123
|
+
node.name.startsWith('will') || node.name.startsWith('does'))) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
223
128
|
/**
|
|
224
129
|
* Check variable declarations for negative naming
|
|
225
130
|
*/
|
|
226
131
|
function checkVariableDeclaration(node) {
|
|
227
132
|
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
228
133
|
return;
|
|
134
|
+
// Only check boolean-like variables
|
|
135
|
+
if (!isBooleanLike(node.id) && !isBooleanLike(node))
|
|
136
|
+
return;
|
|
229
137
|
const variableName = node.id.name;
|
|
230
|
-
const { isNegative, alternatives } =
|
|
138
|
+
const { isNegative, alternatives } = hasBooleanNegativeNaming(variableName);
|
|
231
139
|
if (isNegative) {
|
|
232
140
|
context.report({
|
|
233
141
|
node: node.id,
|
|
@@ -263,7 +171,10 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
263
171
|
}
|
|
264
172
|
if (!functionName)
|
|
265
173
|
return;
|
|
266
|
-
|
|
174
|
+
// Only check boolean-returning functions
|
|
175
|
+
if (!isBooleanLike(node.id || node))
|
|
176
|
+
return;
|
|
177
|
+
const { isNegative, alternatives } = hasBooleanNegativeNaming(functionName);
|
|
267
178
|
if (isNegative) {
|
|
268
179
|
context.report({
|
|
269
180
|
node: node.id || node,
|
|
@@ -281,8 +192,11 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
281
192
|
function checkMethodDefinition(node) {
|
|
282
193
|
if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
283
194
|
return;
|
|
195
|
+
// Only check boolean-returning methods
|
|
196
|
+
if (!isBooleanLike(node.key))
|
|
197
|
+
return;
|
|
284
198
|
const methodName = node.key.name;
|
|
285
|
-
const { isNegative, alternatives } =
|
|
199
|
+
const { isNegative, alternatives } = hasBooleanNegativeNaming(methodName);
|
|
286
200
|
if (isNegative) {
|
|
287
201
|
context.report({
|
|
288
202
|
node: node.key,
|
|
@@ -300,8 +214,11 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
300
214
|
function checkProperty(node) {
|
|
301
215
|
if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
302
216
|
return;
|
|
217
|
+
// Only check boolean properties
|
|
218
|
+
if (!isBooleanLike(node.key))
|
|
219
|
+
return;
|
|
303
220
|
const propertyName = node.key.name;
|
|
304
|
-
const { isNegative, alternatives } =
|
|
221
|
+
const { isNegative, alternatives } = hasBooleanNegativeNaming(propertyName);
|
|
305
222
|
if (isNegative) {
|
|
306
223
|
context.report({
|
|
307
224
|
node: node.key,
|
|
@@ -319,8 +236,12 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
319
236
|
function checkPropertySignature(node) {
|
|
320
237
|
if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
321
238
|
return;
|
|
239
|
+
// Only check boolean properties
|
|
240
|
+
if (!isBooleanLike(node.key) &&
|
|
241
|
+
!(node.typeAnnotation?.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSBooleanKeyword))
|
|
242
|
+
return;
|
|
322
243
|
const propertyName = node.key.name;
|
|
323
|
-
const { isNegative, alternatives } =
|
|
244
|
+
const { isNegative, alternatives } = hasBooleanNegativeNaming(propertyName);
|
|
324
245
|
if (isNegative) {
|
|
325
246
|
context.report({
|
|
326
247
|
node: node.key,
|
|
@@ -338,8 +259,11 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
338
259
|
function checkParameter(node) {
|
|
339
260
|
if (node.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
340
261
|
return;
|
|
262
|
+
// Only check boolean parameters
|
|
263
|
+
if (!isBooleanLike(node))
|
|
264
|
+
return;
|
|
341
265
|
const paramName = node.name;
|
|
342
|
-
const { isNegative, alternatives } =
|
|
266
|
+
const { isNegative, alternatives } = hasBooleanNegativeNaming(paramName);
|
|
343
267
|
if (isNegative) {
|
|
344
268
|
context.report({
|
|
345
269
|
node,
|
|
@@ -38,7 +38,7 @@ exports.enforceSingularTypeNames = (0, createRule_1.createRule)({
|
|
|
38
38
|
if (name.length < 3)
|
|
39
39
|
return false;
|
|
40
40
|
// Skip checking if name ends with 'Props' or 'Params'
|
|
41
|
-
if (name.endsWith('Props') || name.endsWith('Params'))
|
|
41
|
+
if (name.endsWith('Props') || name.endsWith('Params') || name.endsWith('Options') || name.endsWith('Settings'))
|
|
42
42
|
return false;
|
|
43
43
|
// Skip checking if name is already singular according to pluralize
|
|
44
44
|
if (pluralize.isSingular(name))
|
|
@@ -22,6 +22,54 @@ function isAsConstAssertion(node) {
|
|
|
22
22
|
function isTypePredicate(node) {
|
|
23
23
|
return node.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypePredicate;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Checks if a node is inside a return statement
|
|
27
|
+
*/
|
|
28
|
+
function isInsideReturnStatement(node) {
|
|
29
|
+
let current = node;
|
|
30
|
+
while (current?.parent) {
|
|
31
|
+
if (current.parent.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
current = current.parent;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Checks if a node is inside a conditional statement
|
|
40
|
+
*/
|
|
41
|
+
function isInsideConditionalStatement(node) {
|
|
42
|
+
let current = node;
|
|
43
|
+
while (current?.parent) {
|
|
44
|
+
if (current.parent.type === utils_1.AST_NODE_TYPES.IfStatement ||
|
|
45
|
+
current.parent.type === utils_1.AST_NODE_TYPES.WhileStatement ||
|
|
46
|
+
current.parent.type === utils_1.AST_NODE_TYPES.DoWhileStatement ||
|
|
47
|
+
current.parent.type === utils_1.AST_NODE_TYPES.ForStatement ||
|
|
48
|
+
current.parent.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
|
|
49
|
+
// If we're in a conditional statement but not in a return statement, it's valid
|
|
50
|
+
return !isInsideReturnStatement(current.parent);
|
|
51
|
+
}
|
|
52
|
+
current = current.parent;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Checks if a type assertion is used to access a property
|
|
58
|
+
*/
|
|
59
|
+
function isPropertyAccess(node) {
|
|
60
|
+
return (node.parent?.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
61
|
+
node.parent.object === node);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Checks if a type assertion is used within an array includes check
|
|
65
|
+
*/
|
|
66
|
+
function isArrayIncludesCheck(node) {
|
|
67
|
+
return (node.parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
68
|
+
node.parent.arguments.includes(node) &&
|
|
69
|
+
node.parent.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
70
|
+
node.parent.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
71
|
+
node.parent.callee.property.name === 'includes');
|
|
72
|
+
}
|
|
25
73
|
exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
|
|
26
74
|
name: 'no-type-assertion-returns',
|
|
27
75
|
meta: {
|
|
@@ -50,6 +98,94 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
|
|
|
50
98
|
defaultOptions: [defaultOptions],
|
|
51
99
|
create(context, [options]) {
|
|
52
100
|
const mergedOptions = { ...defaultOptions, ...options };
|
|
101
|
+
/**
|
|
102
|
+
* Common function to check if a type assertion should be allowed
|
|
103
|
+
*/
|
|
104
|
+
function shouldAllowTypeAssertion(node) {
|
|
105
|
+
// If the parent is a return statement, we already handle it in ReturnStatement
|
|
106
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
// If the parent is an arrow function, we already handle it in ArrowFunctionExpression
|
|
110
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
// If the parent is a variable declarator, this is a variable declaration with type assertion
|
|
114
|
+
// which is a valid pattern and should not be flagged
|
|
115
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
// Allow type assertions within conditional statements (if, while, do-while, for)
|
|
119
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.IfStatement ||
|
|
120
|
+
node.parent?.type === utils_1.AST_NODE_TYPES.WhileStatement ||
|
|
121
|
+
node.parent?.type === utils_1.AST_NODE_TYPES.DoWhileStatement ||
|
|
122
|
+
node.parent?.type === utils_1.AST_NODE_TYPES.ForStatement) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
// Allow type assertions within logical expressions (which are often used in conditions)
|
|
126
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
// Allow type assertions within method calls like array.includes()
|
|
130
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
131
|
+
node.parent.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
// Allow type assertions within array includes checks
|
|
135
|
+
if (isArrayIncludesCheck(node)) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
// Allow type assertions within conditional expressions, but only if they're not part of a return statement
|
|
139
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.ConditionalExpression && !isInsideReturnStatement(node)) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
// Allow type assertions used to access properties in conditional contexts
|
|
143
|
+
if (isPropertyAccess(node) && isInsideConditionalStatement(node)) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Common function to check function return types
|
|
150
|
+
*/
|
|
151
|
+
function checkFunctionReturnType(node) {
|
|
152
|
+
if (!node.returnType)
|
|
153
|
+
return;
|
|
154
|
+
// Allow type predicates if configured
|
|
155
|
+
if (mergedOptions.allowTypePredicates && isTypePredicate(node.returnType)) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// If type predicates are not allowed, report them
|
|
159
|
+
if (!mergedOptions.allowTypePredicates && isTypePredicate(node.returnType)) {
|
|
160
|
+
context.report({
|
|
161
|
+
node: node.returnType,
|
|
162
|
+
messageId: 'useExplicitVariable',
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Check if the function has a return statement with a direct value (not a variable)
|
|
167
|
+
if (node.body && node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
168
|
+
for (const statement of node.body.body) {
|
|
169
|
+
if (statement.type === utils_1.AST_NODE_TYPES.ReturnStatement && statement.argument) {
|
|
170
|
+
// If returning a variable reference, that's fine
|
|
171
|
+
if (statement.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
// If returning an object literal, array literal, or other complex expression
|
|
175
|
+
// without first assigning it to a typed variable, that's a problem
|
|
176
|
+
if (statement.argument.type === utils_1.AST_NODE_TYPES.ObjectExpression ||
|
|
177
|
+
statement.argument.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
|
|
178
|
+
statement.argument.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
179
|
+
context.report({
|
|
180
|
+
node: node.returnType,
|
|
181
|
+
messageId: 'useExplicitVariable',
|
|
182
|
+
});
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
53
189
|
return {
|
|
54
190
|
// Check for return statements with type assertions
|
|
55
191
|
ReturnStatement(node) {
|
|
@@ -84,81 +220,11 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
|
|
|
84
220
|
},
|
|
85
221
|
// Check functions with explicit return types
|
|
86
222
|
FunctionDeclaration(node) {
|
|
87
|
-
|
|
88
|
-
return;
|
|
89
|
-
// Allow type predicates if configured
|
|
90
|
-
if (mergedOptions.allowTypePredicates && isTypePredicate(node.returnType)) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
// If type predicates are not allowed, report them
|
|
94
|
-
if (!mergedOptions.allowTypePredicates && isTypePredicate(node.returnType)) {
|
|
95
|
-
context.report({
|
|
96
|
-
node: node.returnType,
|
|
97
|
-
messageId: 'useExplicitVariable',
|
|
98
|
-
});
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
// Check if the function has a return statement with a direct value (not a variable)
|
|
102
|
-
if (node.body && node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
103
|
-
for (const statement of node.body.body) {
|
|
104
|
-
if (statement.type === utils_1.AST_NODE_TYPES.ReturnStatement && statement.argument) {
|
|
105
|
-
// If returning a variable reference, that's fine
|
|
106
|
-
if (statement.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
// If returning an object literal, array literal, or other complex expression
|
|
110
|
-
// without first assigning it to a typed variable, that's a problem
|
|
111
|
-
if (statement.argument.type === utils_1.AST_NODE_TYPES.ObjectExpression ||
|
|
112
|
-
statement.argument.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
|
|
113
|
-
statement.argument.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
114
|
-
context.report({
|
|
115
|
-
node: node.returnType,
|
|
116
|
-
messageId: 'useExplicitVariable',
|
|
117
|
-
});
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
223
|
+
checkFunctionReturnType(node);
|
|
123
224
|
},
|
|
124
225
|
// Check function expressions with explicit return types
|
|
125
226
|
FunctionExpression(node) {
|
|
126
|
-
|
|
127
|
-
return;
|
|
128
|
-
// Allow type predicates if configured
|
|
129
|
-
if (mergedOptions.allowTypePredicates && isTypePredicate(node.returnType)) {
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
// If type predicates are not allowed, report them
|
|
133
|
-
if (!mergedOptions.allowTypePredicates && isTypePredicate(node.returnType)) {
|
|
134
|
-
context.report({
|
|
135
|
-
node: node.returnType,
|
|
136
|
-
messageId: 'useExplicitVariable',
|
|
137
|
-
});
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
// Check if the function has a return statement with a direct value (not a variable)
|
|
141
|
-
if (node.body && node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
142
|
-
for (const statement of node.body.body) {
|
|
143
|
-
if (statement.type === utils_1.AST_NODE_TYPES.ReturnStatement && statement.argument) {
|
|
144
|
-
// If returning a variable reference, that's fine
|
|
145
|
-
if (statement.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
// If returning an object literal, array literal, or other complex expression
|
|
149
|
-
// without first assigning it to a typed variable, that's a problem
|
|
150
|
-
if (statement.argument.type === utils_1.AST_NODE_TYPES.ObjectExpression ||
|
|
151
|
-
statement.argument.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
|
|
152
|
-
statement.argument.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
153
|
-
context.report({
|
|
154
|
-
node: node.returnType,
|
|
155
|
-
messageId: 'useExplicitVariable',
|
|
156
|
-
});
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
227
|
+
checkFunctionReturnType(node);
|
|
162
228
|
},
|
|
163
229
|
// Check arrow functions
|
|
164
230
|
ArrowFunctionExpression(node) {
|
|
@@ -207,40 +273,7 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
|
|
|
207
273
|
}
|
|
208
274
|
else if (node.returnType) {
|
|
209
275
|
// For arrow functions with block bodies
|
|
210
|
-
|
|
211
|
-
if (mergedOptions.allowTypePredicates && isTypePredicate(node.returnType)) {
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
// If type predicates are not allowed, report them
|
|
215
|
-
if (!mergedOptions.allowTypePredicates && isTypePredicate(node.returnType)) {
|
|
216
|
-
context.report({
|
|
217
|
-
node: node.returnType,
|
|
218
|
-
messageId: 'useExplicitVariable',
|
|
219
|
-
});
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
// Check if the function has a return statement with a direct value (not a variable)
|
|
223
|
-
if (node.body && node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
224
|
-
for (const statement of node.body.body) {
|
|
225
|
-
if (statement.type === utils_1.AST_NODE_TYPES.ReturnStatement && statement.argument) {
|
|
226
|
-
// If returning a variable reference, that's fine
|
|
227
|
-
if (statement.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
// If returning an object literal, array literal, or other complex expression
|
|
231
|
-
// without first assigning it to a typed variable, that's a problem
|
|
232
|
-
if (statement.argument.type === utils_1.AST_NODE_TYPES.ObjectExpression ||
|
|
233
|
-
statement.argument.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
|
|
234
|
-
statement.argument.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
235
|
-
context.report({
|
|
236
|
-
node: node.returnType,
|
|
237
|
-
messageId: 'useExplicitVariable',
|
|
238
|
-
});
|
|
239
|
-
break;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
276
|
+
checkFunctionReturnType(node);
|
|
244
277
|
}
|
|
245
278
|
},
|
|
246
279
|
// Check for array methods with type assertions
|
|
@@ -253,17 +286,8 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
|
|
|
253
286
|
if (mergedOptions.allowAsConst && isAsConstAssertion(node)) {
|
|
254
287
|
return;
|
|
255
288
|
}
|
|
256
|
-
//
|
|
257
|
-
if (node
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
// If the parent is an arrow function, we already handle it in ArrowFunctionExpression
|
|
261
|
-
if (node.parent?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
// If the parent is a variable declarator, this is a variable declaration with type assertion
|
|
265
|
-
// which is a valid pattern and should not be flagged
|
|
266
|
-
if (node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
289
|
+
// Use the common function to check if the type assertion should be allowed
|
|
290
|
+
if (shouldAllowTypeAssertion(node)) {
|
|
267
291
|
return;
|
|
268
292
|
}
|
|
269
293
|
// For standalone type assertions in expressions
|
|
@@ -274,17 +298,8 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
|
|
|
274
298
|
},
|
|
275
299
|
// Check for type assertions using angle bracket syntax
|
|
276
300
|
TSTypeAssertion(node) {
|
|
277
|
-
//
|
|
278
|
-
if (node
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
// If the parent is an arrow function, we already handle it in ArrowFunctionExpression
|
|
282
|
-
if (node.parent?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
// If the parent is a variable declarator, this is a variable declaration with type assertion
|
|
286
|
-
// which is a valid pattern and should not be flagged
|
|
287
|
-
if (node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
301
|
+
// Use the common function to check if the type assertion should be allowed
|
|
302
|
+
if (shouldAllowTypeAssertion(node)) {
|
|
288
303
|
return;
|
|
289
304
|
}
|
|
290
305
|
// For standalone type assertions in expressions
|
|
@@ -21,11 +21,15 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
21
21
|
create(context) {
|
|
22
22
|
const propsTypes = new Map();
|
|
23
23
|
const usedProps = new Map();
|
|
24
|
+
// Track which spread types have been used in a component
|
|
25
|
+
const usedSpreadTypes = new Map();
|
|
24
26
|
let currentComponent = null;
|
|
25
27
|
return {
|
|
26
28
|
TSTypeAliasDeclaration(node) {
|
|
27
29
|
if (node.id.name.endsWith('Props')) {
|
|
28
30
|
const props = {};
|
|
31
|
+
// Track which properties come from which spread type
|
|
32
|
+
const spreadTypeProps = {};
|
|
29
33
|
function extractProps(typeNode) {
|
|
30
34
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
31
35
|
typeNode.members.forEach((member) => {
|
|
@@ -45,6 +49,7 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
45
49
|
const [baseType, pickedProps] = type.typeParameters.params;
|
|
46
50
|
if (baseType.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
47
51
|
baseType.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
52
|
+
const baseTypeName = baseType.typeName.name;
|
|
48
53
|
// Extract the picked properties from the union type
|
|
49
54
|
if (pickedProps.type === utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
50
55
|
pickedProps.types.forEach((t) => {
|
|
@@ -52,7 +57,13 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
52
57
|
t.literal.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
53
58
|
typeof t.literal.value === 'string') {
|
|
54
59
|
// Add each picked property as a regular prop
|
|
55
|
-
|
|
60
|
+
const propName = t.literal.value;
|
|
61
|
+
props[propName] = t.literal;
|
|
62
|
+
// Track that this prop comes from the base type
|
|
63
|
+
if (!spreadTypeProps[baseTypeName]) {
|
|
64
|
+
spreadTypeProps[baseTypeName] = [];
|
|
65
|
+
}
|
|
66
|
+
spreadTypeProps[baseTypeName].push(propName);
|
|
56
67
|
}
|
|
57
68
|
});
|
|
58
69
|
}
|
|
@@ -60,7 +71,13 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
60
71
|
pickedProps.literal.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
61
72
|
typeof pickedProps.literal.value === 'string') {
|
|
62
73
|
// Single property pick
|
|
63
|
-
|
|
74
|
+
const propName = pickedProps.literal.value;
|
|
75
|
+
props[propName] = pickedProps.literal;
|
|
76
|
+
// Track that this prop comes from the base type
|
|
77
|
+
if (!spreadTypeProps[baseTypeName]) {
|
|
78
|
+
spreadTypeProps[baseTypeName] = [];
|
|
79
|
+
}
|
|
80
|
+
spreadTypeProps[baseTypeName].push(propName);
|
|
64
81
|
}
|
|
65
82
|
}
|
|
66
83
|
}
|
|
@@ -74,7 +91,13 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
74
91
|
else {
|
|
75
92
|
// If we can't find the type declaration, it's likely an imported type
|
|
76
93
|
// Mark it as a forwarded prop
|
|
77
|
-
|
|
94
|
+
const spreadTypeName = typeName.name;
|
|
95
|
+
props[`...${spreadTypeName}`] = typeName;
|
|
96
|
+
// For imported types, we need to track individual properties that might be used
|
|
97
|
+
// from this spread type, even if we don't know what they are yet
|
|
98
|
+
if (!spreadTypeProps[spreadTypeName]) {
|
|
99
|
+
spreadTypeProps[spreadTypeName] = [];
|
|
100
|
+
}
|
|
78
101
|
}
|
|
79
102
|
}
|
|
80
103
|
}
|
|
@@ -91,6 +114,7 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
91
114
|
const [baseType, pickedProps] = typeNode.typeParameters.params;
|
|
92
115
|
if (baseType.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
93
116
|
baseType.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
117
|
+
const baseTypeName = baseType.typeName.name;
|
|
94
118
|
// Extract the picked properties from the union type
|
|
95
119
|
if (pickedProps.type === utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
96
120
|
pickedProps.types.forEach((type) => {
|
|
@@ -98,7 +122,13 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
98
122
|
type.literal.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
99
123
|
typeof type.literal.value === 'string') {
|
|
100
124
|
// Add each picked property as a regular prop
|
|
101
|
-
|
|
125
|
+
const propName = type.literal.value;
|
|
126
|
+
props[propName] = type.literal;
|
|
127
|
+
// Track that this prop comes from the base type
|
|
128
|
+
if (!spreadTypeProps[baseTypeName]) {
|
|
129
|
+
spreadTypeProps[baseTypeName] = [];
|
|
130
|
+
}
|
|
131
|
+
spreadTypeProps[baseTypeName].push(propName);
|
|
102
132
|
}
|
|
103
133
|
});
|
|
104
134
|
}
|
|
@@ -106,19 +136,44 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
106
136
|
pickedProps.literal.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
107
137
|
typeof pickedProps.literal.value === 'string') {
|
|
108
138
|
// Single property pick
|
|
109
|
-
|
|
139
|
+
const propName = pickedProps.literal.value;
|
|
140
|
+
props[propName] = pickedProps.literal;
|
|
141
|
+
// Track that this prop comes from the base type
|
|
142
|
+
if (!spreadTypeProps[baseTypeName]) {
|
|
143
|
+
spreadTypeProps[baseTypeName] = [];
|
|
144
|
+
}
|
|
145
|
+
spreadTypeProps[baseTypeName].push(propName);
|
|
110
146
|
}
|
|
111
147
|
}
|
|
112
148
|
}
|
|
113
149
|
else {
|
|
114
150
|
// For referenced types like FormControlLabelProps, we need to track that these props should be forwarded
|
|
115
|
-
|
|
151
|
+
const spreadTypeName = typeNode.typeName.name;
|
|
152
|
+
props[`...${spreadTypeName}`] = typeNode.typeName;
|
|
153
|
+
// For imported types, we need to track individual properties that might be used
|
|
154
|
+
// from this spread type, even if we don't know what they are yet
|
|
155
|
+
if (!spreadTypeProps[spreadTypeName]) {
|
|
156
|
+
spreadTypeProps[spreadTypeName] = [];
|
|
157
|
+
}
|
|
116
158
|
}
|
|
117
159
|
}
|
|
118
160
|
}
|
|
119
161
|
}
|
|
120
162
|
extractProps(node.typeAnnotation);
|
|
121
163
|
propsTypes.set(node.id.name, props);
|
|
164
|
+
// Store the mapping of spread types to their properties
|
|
165
|
+
const typeName = node.id.name;
|
|
166
|
+
usedSpreadTypes.set(typeName, new Set(Object.keys(spreadTypeProps)));
|
|
167
|
+
// Store the spread type properties for later reference
|
|
168
|
+
for (const [spreadType, propNames] of Object.entries(spreadTypeProps)) {
|
|
169
|
+
// Create a map entry for this spread type if it doesn't exist
|
|
170
|
+
if (!usedSpreadTypes.has(spreadType)) {
|
|
171
|
+
usedSpreadTypes.set(spreadType, new Set());
|
|
172
|
+
}
|
|
173
|
+
// Add the property names to the spread type's set
|
|
174
|
+
const spreadTypeSet = usedSpreadTypes.get(spreadType);
|
|
175
|
+
propNames.forEach(prop => spreadTypeSet.add(prop));
|
|
176
|
+
}
|
|
122
177
|
}
|
|
123
178
|
},
|
|
124
179
|
VariableDeclaration(node) {
|
|
@@ -171,7 +226,45 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
171
226
|
// For imported types (props that start with '...'), only report if there's no rest spread operator
|
|
172
227
|
// This allows imported types to be used without being flagged when properly forwarded
|
|
173
228
|
const hasRestSpread = Array.from(used.values()).some(usedProp => usedProp.startsWith('...'));
|
|
174
|
-
|
|
229
|
+
// Don't report unused props if:
|
|
230
|
+
// 1. It's a spread type and there's a rest spread operator, OR
|
|
231
|
+
// 2. It's a property from a spread type and any property from that spread type is used, OR
|
|
232
|
+
// 3. It's a spread type and any of its properties are used in the component
|
|
233
|
+
let shouldReport = true;
|
|
234
|
+
if (prop.startsWith('...') && hasRestSpread) {
|
|
235
|
+
shouldReport = false;
|
|
236
|
+
}
|
|
237
|
+
else if (prop.startsWith('...')) {
|
|
238
|
+
// For spread types like "...GroupInfoBasic", check if any properties from this type are used
|
|
239
|
+
const spreadTypeName = prop.substring(3); // Remove the "..." prefix
|
|
240
|
+
// Get the properties that belong to this spread type
|
|
241
|
+
const spreadTypeProps = usedSpreadTypes.get(spreadTypeName);
|
|
242
|
+
if (spreadTypeProps) {
|
|
243
|
+
// Check if any property from this spread type is being used in the component
|
|
244
|
+
const anyPropFromSpreadTypeUsed = Array.from(spreadTypeProps).some(spreadProp => used.has(spreadProp));
|
|
245
|
+
if (anyPropFromSpreadTypeUsed) {
|
|
246
|
+
shouldReport = false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
// Check if this prop might be from a spread type that has other properties being used
|
|
252
|
+
for (const [spreadType, props] of usedSpreadTypes.entries()) {
|
|
253
|
+
// Skip the current props type
|
|
254
|
+
if (spreadType === typeName)
|
|
255
|
+
continue;
|
|
256
|
+
// If this prop is from a spread type
|
|
257
|
+
if (props.has(prop)) {
|
|
258
|
+
// Check if any other prop from this spread type is being used
|
|
259
|
+
const anyPropFromSpreadTypeUsed = Array.from(props).some(spreadProp => used.has(spreadProp));
|
|
260
|
+
if (anyPropFromSpreadTypeUsed) {
|
|
261
|
+
shouldReport = false;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (shouldReport) {
|
|
175
268
|
context.report({
|
|
176
269
|
node: propsType[prop],
|
|
177
270
|
messageId: 'unusedProp',
|