@blumintinc/eslint-plugin-blumint 1.12.5 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -99
- package/lib/index.js +111 -20
- package/lib/rules/array-methods-this-context.js +20 -2
- package/lib/rules/avoid-utils-directory.js +11 -2
- package/lib/rules/dynamic-https-errors.d.ts +3 -1
- package/lib/rules/dynamic-https-errors.js +48 -10
- package/lib/rules/enforce-assert-safe-object-key.d.ts +8 -0
- package/lib/rules/{enforce-assertSafe-object-key.js → enforce-assert-safe-object-key.js} +51 -95
- package/lib/rules/enforce-assert-throws.d.ts +3 -1
- package/lib/rules/enforce-assert-throws.js +493 -51
- package/lib/rules/enforce-boolean-naming-prefixes.d.ts +3 -1
- package/lib/rules/enforce-boolean-naming-prefixes.js +832 -39
- package/lib/rules/enforce-callable-types.js +4 -4
- package/lib/rules/enforce-callback-memo.js +143 -18
- package/lib/rules/enforce-centralized-mock-firestore.js +4 -1
- package/lib/rules/enforce-console-error.d.ts +3 -0
- package/lib/rules/enforce-console-error.js +250 -0
- package/lib/rules/enforce-css-media-queries.js +24 -46
- package/lib/rules/enforce-dynamic-firebase-imports.js +100 -19
- package/lib/rules/enforce-dynamic-imports.js +1 -1
- package/lib/rules/enforce-exported-function-types.js +3 -3
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +212 -0
- package/lib/rules/enforce-firestore-doc-ref-generic.js +335 -0
- package/lib/rules/enforce-firestore-facade.js +347 -157
- package/lib/rules/enforce-firestore-rules-get-access.d.ts +3 -0
- package/lib/rules/enforce-firestore-rules-get-access.js +109 -0
- package/lib/rules/enforce-global-constants.d.ts +4 -1
- package/lib/rules/enforce-global-constants.js +240 -12
- package/lib/rules/enforce-memoize-async.d.ts +2 -1
- package/lib/rules/enforce-memoize-async.js +96 -27
- package/lib/rules/enforce-memoize-getters.d.ts +3 -0
- package/lib/rules/enforce-memoize-getters.js +123 -0
- package/lib/rules/enforce-microdiff.js +2 -1
- package/lib/rules/enforce-positive-naming.js +5 -7
- package/lib/rules/enforce-props-argument-name.d.ts +2 -7
- package/lib/rules/enforce-props-argument-name.js +165 -151
- package/lib/rules/enforce-props-naming-consistency.d.ts +2 -0
- package/lib/rules/enforce-props-naming-consistency.js +145 -0
- package/lib/rules/enforce-querykey-ts.d.ts +9 -0
- package/lib/rules/enforce-querykey-ts.js +274 -0
- package/lib/rules/enforce-singular-type-names.js +9 -5
- package/lib/rules/enforce-typescript-markdown-code-blocks.d.ts +1 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.js +96 -0
- package/lib/rules/enforce-verb-noun-naming.js +85 -10
- package/lib/rules/extract-global-constants.js +57 -45
- package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
- package/lib/rules/fast-deep-equal-over-microdiff.js +356 -78
- package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
- package/lib/rules/firestore-transaction-reads-before-writes.js +231 -0
- package/lib/rules/generic-starts-with-t.js +8 -2
- package/lib/rules/memoize-root-level-hocs.d.ts +7 -0
- package/lib/rules/memoize-root-level-hocs.js +242 -0
- package/lib/rules/no-array-length-in-deps.d.ts +12 -0
- package/lib/rules/no-array-length-in-deps.js +308 -0
- package/lib/rules/no-async-array-filter.js +5 -2
- package/lib/rules/no-complex-cloud-params.js +15 -2
- package/lib/rules/no-compositing-layer-props.js +4 -2
- package/lib/rules/no-entire-object-hook-deps.d.ts +3 -1
- package/lib/rules/no-entire-object-hook-deps.js +317 -63
- package/lib/rules/no-excessive-parent-chain.d.ts +5 -0
- package/lib/rules/no-excessive-parent-chain.js +273 -0
- package/lib/rules/no-firestore-object-arrays.js +345 -30
- package/lib/rules/no-hungarian.js +13 -13
- package/lib/rules/no-margin-properties.d.ts +7 -1
- package/lib/rules/no-margin-properties.js +16 -3
- package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
- package/lib/rules/no-misleading-boolean-prefixes.js +327 -0
- package/lib/rules/no-overridable-method-calls-in-constructor.d.ts +1 -0
- package/lib/rules/no-overridable-method-calls-in-constructor.js +260 -0
- package/lib/rules/no-passthrough-getters.d.ts +4 -0
- package/lib/rules/no-passthrough-getters.js +170 -0
- package/lib/rules/no-redundant-usecallback-wrapper.d.ts +8 -0
- package/lib/rules/no-redundant-usecallback-wrapper.js +315 -0
- package/lib/rules/no-restricted-properties-fix.d.ts +12 -0
- package/lib/rules/no-restricted-properties-fix.js +172 -0
- package/lib/rules/no-separate-loading-state.d.ts +8 -0
- package/lib/rules/no-separate-loading-state.js +142 -0
- package/lib/rules/no-stale-state-across-await.d.ts +1 -0
- package/lib/rules/no-stale-state-across-await.js +185 -0
- package/lib/rules/no-static-constants-in-dynamic-files.d.ts +3 -0
- package/lib/rules/no-static-constants-in-dynamic-files.js +149 -0
- package/lib/rules/no-try-catch-already-exists-in-transaction.d.ts +1 -0
- package/lib/rules/no-try-catch-already-exists-in-transaction.js +342 -0
- package/lib/rules/no-undefined-null-passthrough.js +166 -19
- package/lib/rules/no-unnecessary-verb-suffix.js +0 -2
- package/lib/rules/no-unsafe-firestore-spread.js +8 -5
- package/lib/rules/no-unused-props.d.ts +3 -0
- package/lib/rules/no-unused-props.js +213 -78
- package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
- package/lib/rules/optimize-object-boolean-conditions.js +234 -0
- package/lib/rules/parallelize-async-operations.d.ts +8 -0
- package/lib/rules/parallelize-async-operations.js +375 -0
- package/lib/rules/prefer-batch-operations.js +199 -33
- package/lib/rules/prefer-block-comments-for-declarations.js +13 -3
- package/lib/rules/prefer-document-flattening.d.ts +4 -0
- package/lib/rules/prefer-document-flattening.js +239 -0
- package/lib/rules/prefer-field-paths-in-transforms.d.ts +7 -0
- package/lib/rules/prefer-field-paths-in-transforms.js +250 -0
- package/lib/rules/prefer-fragment-component.js +3 -3
- package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
- package/lib/rules/prefer-global-router-state-key.js +230 -31
- package/lib/rules/prefer-next-dynamic.d.ts +9 -0
- package/lib/rules/prefer-next-dynamic.js +366 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +430 -0
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +6 -0
- package/lib/rules/prefer-nullish-coalescing-override.js +182 -0
- package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
- package/lib/rules/prefer-params-over-parent-id.js +228 -0
- package/lib/rules/prefer-settings-object.js +23 -15
- package/lib/rules/prefer-type-alias-over-typeof-constant.d.ts +3 -0
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +259 -0
- package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
- package/lib/rules/prefer-url-tostring-over-tojson.js +166 -0
- package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
- package/lib/rules/prefer-use-deep-compare-memo.js +428 -0
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +24 -1
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
- package/lib/rules/react-usememo-should-be-component.d.ts +2 -1
- package/lib/rules/react-usememo-should-be-component.js +144 -78
- package/lib/rules/require-dynamic-firebase-imports.js +2 -1
- package/lib/rules/require-image-optimized.js +1 -1
- package/lib/rules/require-memo.js +11 -1
- package/lib/rules/require-usememo-object-literals.js +7 -1
- package/lib/rules/semantic-function-prefixes.js +38 -18
- package/lib/rules/sync-onwrite-name-func.js +5 -1
- package/lib/rules/test-file-location-enforcement.d.ts +1 -0
- package/lib/rules/test-file-location-enforcement.js +57 -0
- package/lib/rules/use-custom-link.js +6 -1
- package/lib/rules/use-custom-router.js +6 -1
- package/lib/rules/use-latest-callback.d.ts +3 -0
- package/lib/rules/use-latest-callback.js +270 -0
- package/lib/utils/ASTHelpers.js +10 -0
- package/lib/utils/createRule.js +1 -1
- package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
- package/lib/utils/ruleTester.d.ts +1 -0
- package/lib/utils/ruleTester.js +4 -1
- package/package.json +21 -14
- package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
- package/lib/rules/no-restricted-imports-dynamic.d.ts +0 -25
- package/lib/rules/no-restricted-imports-dynamic.js +0 -213
|
@@ -19,17 +19,177 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
19
19
|
},
|
|
20
20
|
defaultOptions: [],
|
|
21
21
|
create(context) {
|
|
22
|
+
const filename = context.filename ??
|
|
23
|
+
context.getFilename();
|
|
24
|
+
const ruleSettings = (context.settings && context.settings['no-unused-props']) ?? {};
|
|
25
|
+
const reactLikeExtensions = (ruleSettings.reactLikeExtensions ??
|
|
26
|
+
['.tsx']).map((ext) => ext.toLowerCase());
|
|
27
|
+
const fileExtension = filename.includes('.')
|
|
28
|
+
? filename.slice(filename.lastIndexOf('.')).toLowerCase()
|
|
29
|
+
: '';
|
|
30
|
+
const isTsxFile = reactLikeExtensions.includes(fileExtension);
|
|
31
|
+
let hasJsxInFile = false;
|
|
32
|
+
const shouldCheckFile = () => isTsxFile || hasJsxInFile;
|
|
33
|
+
const UTILITY_TYPES = new Set([
|
|
34
|
+
'Pick',
|
|
35
|
+
'Omit',
|
|
36
|
+
'Partial',
|
|
37
|
+
'Required',
|
|
38
|
+
'Record',
|
|
39
|
+
'Exclude',
|
|
40
|
+
'Extract',
|
|
41
|
+
'NonNullable',
|
|
42
|
+
'ReturnType',
|
|
43
|
+
'InstanceType',
|
|
44
|
+
'ThisType',
|
|
45
|
+
]);
|
|
22
46
|
const propsTypes = new Map();
|
|
23
|
-
const usedProps = new Map();
|
|
24
47
|
// Track which spread types have been used in a component
|
|
25
48
|
const usedSpreadTypes = new Map();
|
|
49
|
+
const componentsToCheck = [];
|
|
26
50
|
let currentComponent = null;
|
|
51
|
+
const clearState = () => {
|
|
52
|
+
propsTypes.clear();
|
|
53
|
+
usedSpreadTypes.clear();
|
|
54
|
+
componentsToCheck.length = 0;
|
|
55
|
+
currentComponent = null;
|
|
56
|
+
};
|
|
57
|
+
const isGenericTypeSpread = (prop) => prop.startsWith('...') && prop.length === 4 && /^\.\.\.[A-Z]$/.test(prop);
|
|
58
|
+
const hasRestSpreadUsage = (used, restUsed) => {
|
|
59
|
+
if (restUsed) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
for (const usedProp of used) {
|
|
63
|
+
if (usedProp.startsWith('...')) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
};
|
|
69
|
+
const isAnyPropFromSpreadTypeUsed = (spreadTypeName, used) => {
|
|
70
|
+
const spreadTypeProps = usedSpreadTypes.get(spreadTypeName);
|
|
71
|
+
if (!spreadTypeProps) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
for (const spreadProp of spreadTypeProps) {
|
|
75
|
+
if (used.has(spreadProp)) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
};
|
|
81
|
+
const shouldSkipSpreadType = (prop, hasRestSpread, used) => {
|
|
82
|
+
if (hasRestSpread) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
const spreadTypeName = prop.substring(3);
|
|
86
|
+
if (UTILITY_TYPES.has(spreadTypeName)) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return isAnyPropFromSpreadTypeUsed(spreadTypeName, used);
|
|
90
|
+
};
|
|
91
|
+
const shouldSkipPropFromSpreadType = (prop, currentTypeName, used) => {
|
|
92
|
+
for (const [spreadType, props] of usedSpreadTypes.entries()) {
|
|
93
|
+
if (spreadType === currentTypeName)
|
|
94
|
+
continue;
|
|
95
|
+
if (!props.has(prop)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
for (const spreadProp of props) {
|
|
99
|
+
if (used.has(spreadProp)) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
};
|
|
106
|
+
const reportUnusedProps = (typeName, used, restUsed) => {
|
|
107
|
+
const propsType = propsTypes.get(typeName);
|
|
108
|
+
if (!propsType) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const hasRestSpread = hasRestSpreadUsage(used, restUsed);
|
|
112
|
+
Object.keys(propsType).forEach((prop) => {
|
|
113
|
+
if (!used.has(prop)) {
|
|
114
|
+
// For imported types (props that start with '...'), only report if there's no rest spread operator
|
|
115
|
+
// This allows imported types to be used without being flagged when properly forwarded
|
|
116
|
+
// Don't report unused props if:
|
|
117
|
+
// 1. It's a spread type and there's a rest spread operator, OR
|
|
118
|
+
// 2. It's a property from a spread type and any property from that spread type is used, OR
|
|
119
|
+
// 3. It's a spread type and any of its properties are used in the component
|
|
120
|
+
let shouldReport = true;
|
|
121
|
+
// Skip reporting for generic type parameters (T, K, etc.)
|
|
122
|
+
if (isGenericTypeSpread(prop)) {
|
|
123
|
+
shouldReport = false;
|
|
124
|
+
}
|
|
125
|
+
else if (prop.startsWith('...')) {
|
|
126
|
+
shouldReport = !shouldSkipSpreadType(prop, hasRestSpread, used);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
shouldReport = !shouldSkipPropFromSpreadType(prop, typeName, used);
|
|
130
|
+
}
|
|
131
|
+
if (shouldReport) {
|
|
132
|
+
context.report({
|
|
133
|
+
node: propsType[prop],
|
|
134
|
+
messageId: 'unusedProp',
|
|
135
|
+
data: { propName: prop },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
};
|
|
27
141
|
return {
|
|
28
142
|
TSTypeAliasDeclaration(node) {
|
|
29
143
|
if (node.id.name.endsWith('Props')) {
|
|
30
144
|
const props = {};
|
|
31
145
|
// Track which properties come from which spread type
|
|
32
146
|
const spreadTypeProps = {};
|
|
147
|
+
const addBaseTypeProps = (typeNode, shouldInclude = () => true) => {
|
|
148
|
+
if (typeNode.type !== utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
typeNode.members.forEach((member) => {
|
|
152
|
+
if (member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
|
|
153
|
+
member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
154
|
+
shouldInclude(member.key.name)) {
|
|
155
|
+
props[member.key.name] = member.key;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
const handleOmitType = (baseType, omittedProps) => {
|
|
160
|
+
if (baseType.type !== utils_1.AST_NODE_TYPES.TSTypeReference ||
|
|
161
|
+
baseType.typeName.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const baseTypeName = baseType.typeName.name;
|
|
165
|
+
const omittedPropNames = new Set();
|
|
166
|
+
if (omittedProps.type === utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
167
|
+
omittedProps.types.forEach((type) => {
|
|
168
|
+
if (type.type === utils_1.AST_NODE_TYPES.TSLiteralType &&
|
|
169
|
+
type.literal.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
170
|
+
typeof type.literal.value === 'string') {
|
|
171
|
+
omittedPropNames.add(type.literal.value);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
else if (omittedProps.type === utils_1.AST_NODE_TYPES.TSLiteralType &&
|
|
176
|
+
omittedProps.literal.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
177
|
+
typeof omittedProps.literal.value === 'string') {
|
|
178
|
+
omittedPropNames.add(omittedProps.literal.value);
|
|
179
|
+
}
|
|
180
|
+
const scope = context.getScope();
|
|
181
|
+
const variable = scope.variables.find((v) => v.name === baseTypeName);
|
|
182
|
+
if (variable &&
|
|
183
|
+
variable.defs[0]?.node.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
184
|
+
addBaseTypeProps(variable.defs[0].node.typeAnnotation, (name) => !omittedPropNames.has(name));
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
props[`...${baseTypeName}`] = baseType.typeName;
|
|
188
|
+
if (!spreadTypeProps[baseTypeName]) {
|
|
189
|
+
spreadTypeProps[baseTypeName] = [];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
33
193
|
function extractProps(typeNode) {
|
|
34
194
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
35
195
|
typeNode.members.forEach((member) => {
|
|
@@ -81,6 +241,13 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
81
241
|
}
|
|
82
242
|
}
|
|
83
243
|
}
|
|
244
|
+
else if (typeName.name === 'Omit' &&
|
|
245
|
+
type.typeParameters &&
|
|
246
|
+
type.typeParameters.params.length === 2) {
|
|
247
|
+
// Handle Omit utility type in intersection
|
|
248
|
+
const [baseType, omittedProps] = type.typeParameters.params;
|
|
249
|
+
handleOmitType(baseType, omittedProps);
|
|
250
|
+
}
|
|
84
251
|
else {
|
|
85
252
|
// For referenced types in intersections, we need to find their type declaration
|
|
86
253
|
const scope = context.getScope();
|
|
@@ -111,10 +278,9 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
111
278
|
}
|
|
112
279
|
else if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
113
280
|
if (typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
114
|
-
// List of TypeScript utility types that transform other types
|
|
115
|
-
const utilityTypes = ['Pick', 'Omit', 'Partial', 'Required', 'Record', 'Exclude', 'Extract', 'NonNullable', 'ReturnType', 'InstanceType', 'ThisType'];
|
|
116
281
|
// Skip checking for utility type parameters (T, K, etc.) as they're not actual props
|
|
117
|
-
if (typeNode.typeName.name.length === 1 &&
|
|
282
|
+
if (typeNode.typeName.name.length === 1 &&
|
|
283
|
+
/^[A-Z]$/.test(typeNode.typeName.name)) {
|
|
118
284
|
// This is likely a generic type parameter (T, K, etc.), not a real type
|
|
119
285
|
// Skip it to avoid false positives
|
|
120
286
|
return;
|
|
@@ -157,20 +323,37 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
157
323
|
}
|
|
158
324
|
}
|
|
159
325
|
}
|
|
326
|
+
else if (typeNode.typeName.name === 'Omit' &&
|
|
327
|
+
typeNode.typeParameters &&
|
|
328
|
+
typeNode.typeParameters.params.length === 2) {
|
|
329
|
+
// Handle Omit<T, K> utility type
|
|
330
|
+
const [baseType, omittedProps] = typeNode.typeParameters.params;
|
|
331
|
+
handleOmitType(baseType, omittedProps);
|
|
332
|
+
}
|
|
160
333
|
else if (
|
|
161
334
|
// Handle other utility types like Required, Partial, etc.
|
|
162
|
-
|
|
335
|
+
UTILITY_TYPES.has(typeNode.typeName.name) &&
|
|
163
336
|
typeNode.typeParameters) {
|
|
164
|
-
// For utility types like Required<T
|
|
337
|
+
// For utility types like Required<T>, Partial<T>, we need to handle the base type
|
|
165
338
|
const baseType = typeNode.typeParameters.params[0];
|
|
166
339
|
if (baseType.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
167
340
|
baseType.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
168
|
-
// Mark the base type as used via the utility type
|
|
169
341
|
const baseTypeName = baseType.typeName.name;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
342
|
+
// Find the base type definition
|
|
343
|
+
const scope = context.getScope();
|
|
344
|
+
const variable = scope.variables.find((v) => v.name === baseTypeName);
|
|
345
|
+
if (variable &&
|
|
346
|
+
variable.defs[0]?.node.type ===
|
|
347
|
+
utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
348
|
+
// For Partial<T>, Required<T>, etc., add all properties from the base type
|
|
349
|
+
addBaseTypeProps(variable.defs[0].node.typeAnnotation);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
// If we can't find the base type definition, treat it as a spread type
|
|
353
|
+
props[`...${baseTypeName}`] = baseType.typeName;
|
|
354
|
+
if (!spreadTypeProps[baseTypeName]) {
|
|
355
|
+
spreadTypeProps[baseTypeName] = [];
|
|
356
|
+
}
|
|
174
357
|
}
|
|
175
358
|
}
|
|
176
359
|
}
|
|
@@ -216,8 +399,8 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
216
399
|
utils_1.AST_NODE_TYPES.Identifier) {
|
|
217
400
|
const typeName = param.typeAnnotation.typeAnnotation.typeName.name;
|
|
218
401
|
if (typeName.endsWith('Props')) {
|
|
219
|
-
currentComponent = { node, typeName };
|
|
220
402
|
const used = new Set();
|
|
403
|
+
let restUsed = false;
|
|
221
404
|
param.properties.forEach((prop) => {
|
|
222
405
|
if (prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
223
406
|
prop.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
@@ -227,6 +410,7 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
227
410
|
prop.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
228
411
|
// Handle rest spread operator {...rest}
|
|
229
412
|
// When a rest operator is used, all remaining props are considered used
|
|
413
|
+
restUsed = true;
|
|
230
414
|
const propsType = propsTypes.get(typeName);
|
|
231
415
|
if (propsType) {
|
|
232
416
|
Object.keys(propsType).forEach((key) => {
|
|
@@ -237,7 +421,7 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
237
421
|
}
|
|
238
422
|
}
|
|
239
423
|
});
|
|
240
|
-
|
|
424
|
+
currentComponent = { node, typeName, used, restUsed };
|
|
241
425
|
}
|
|
242
426
|
}
|
|
243
427
|
}
|
|
@@ -245,74 +429,25 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
245
429
|
},
|
|
246
430
|
'VariableDeclaration:exit'(node) {
|
|
247
431
|
if (currentComponent?.node === node) {
|
|
248
|
-
const { typeName } = currentComponent;
|
|
249
|
-
|
|
250
|
-
const used = usedProps.get(typeName);
|
|
251
|
-
if (propsType && used) {
|
|
252
|
-
Object.keys(propsType).forEach((prop) => {
|
|
253
|
-
if (!used.has(prop)) {
|
|
254
|
-
// For imported types (props that start with '...'), only report if there's no rest spread operator
|
|
255
|
-
// This allows imported types to be used without being flagged when properly forwarded
|
|
256
|
-
const hasRestSpread = Array.from(used.values()).some((usedProp) => usedProp.startsWith('...'));
|
|
257
|
-
// Don't report unused props if:
|
|
258
|
-
// 1. It's a spread type and there's a rest spread operator, OR
|
|
259
|
-
// 2. It's a property from a spread type and any property from that spread type is used, OR
|
|
260
|
-
// 3. It's a spread type and any of its properties are used in the component
|
|
261
|
-
let shouldReport = true;
|
|
262
|
-
// Skip reporting for generic type parameters (T, K, etc.)
|
|
263
|
-
if (prop.startsWith('...') && prop.length === 4 && /^\.\.\.([A-Z])$/.test(prop)) {
|
|
264
|
-
// This is a generic type parameter like ...T, ...K, etc.
|
|
265
|
-
shouldReport = false;
|
|
266
|
-
}
|
|
267
|
-
else if (prop.startsWith('...') && hasRestSpread) {
|
|
268
|
-
shouldReport = false;
|
|
269
|
-
}
|
|
270
|
-
else if (prop.startsWith('...')) {
|
|
271
|
-
// For spread types like "...GroupInfoBasic", check if any properties from this type are used
|
|
272
|
-
const spreadTypeName = prop.substring(3); // Remove the "..." prefix
|
|
273
|
-
// Get the properties that belong to this spread type
|
|
274
|
-
const spreadTypeProps = usedSpreadTypes.get(spreadTypeName);
|
|
275
|
-
if (spreadTypeProps) {
|
|
276
|
-
// Check if any property from this spread type is being used in the component
|
|
277
|
-
const anyPropFromSpreadTypeUsed = Array.from(spreadTypeProps).some((spreadProp) => used.has(spreadProp));
|
|
278
|
-
if (anyPropFromSpreadTypeUsed) {
|
|
279
|
-
shouldReport = false;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
else {
|
|
284
|
-
// Check if this prop might be from a spread type that has other properties being used
|
|
285
|
-
for (const [spreadType, props] of usedSpreadTypes.entries()) {
|
|
286
|
-
// Skip the current props type
|
|
287
|
-
if (spreadType === typeName)
|
|
288
|
-
continue;
|
|
289
|
-
// If this prop is from a spread type
|
|
290
|
-
if (props.has(prop)) {
|
|
291
|
-
// Check if any other prop from this spread type is being used
|
|
292
|
-
const anyPropFromSpreadTypeUsed = Array.from(props).some((spreadProp) => used.has(spreadProp));
|
|
293
|
-
if (anyPropFromSpreadTypeUsed) {
|
|
294
|
-
shouldReport = false;
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
if (shouldReport) {
|
|
301
|
-
context.report({
|
|
302
|
-
node: propsType[prop],
|
|
303
|
-
messageId: 'unusedProp',
|
|
304
|
-
data: { propName: prop },
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
// Reset state for this component
|
|
311
|
-
propsTypes.delete(typeName);
|
|
312
|
-
usedProps.delete(typeName);
|
|
432
|
+
const { typeName, used, restUsed } = currentComponent;
|
|
433
|
+
componentsToCheck.push({ typeName, used, restUsed });
|
|
313
434
|
currentComponent = null;
|
|
314
435
|
}
|
|
315
436
|
},
|
|
437
|
+
JSXElement() {
|
|
438
|
+
hasJsxInFile = true;
|
|
439
|
+
},
|
|
440
|
+
JSXFragment() {
|
|
441
|
+
hasJsxInFile = true;
|
|
442
|
+
},
|
|
443
|
+
'Program:exit'() {
|
|
444
|
+
if (!shouldCheckFile()) {
|
|
445
|
+
clearState();
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
componentsToCheck.forEach(({ typeName, used, restUsed }) => reportUnusedProps(typeName, used, restUsed));
|
|
449
|
+
clearState();
|
|
450
|
+
},
|
|
316
451
|
};
|
|
317
452
|
},
|
|
318
453
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const optimizeObjectBooleanConditions: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"extractBooleanCondition", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.optimizeObjectBooleanConditions = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const HOOK_NAMES = new Set(['useEffect', 'useCallback', 'useMemo']);
|
|
7
|
+
function isHookCall(node) {
|
|
8
|
+
const callee = node.callee;
|
|
9
|
+
return ((callee.type === utils_1.AST_NODE_TYPES.Identifier && HOOK_NAMES.has(callee.name)) ||
|
|
10
|
+
(callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
11
|
+
!callee.computed &&
|
|
12
|
+
callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
13
|
+
callee.object.name === 'React' &&
|
|
14
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
15
|
+
HOOK_NAMES.has(callee.property.name)));
|
|
16
|
+
}
|
|
17
|
+
function generateBooleanVariableName(objectName, conditionType, isNegated) {
|
|
18
|
+
const base = capitalize(objectName);
|
|
19
|
+
if (conditionType === 'existence') {
|
|
20
|
+
return isNegated ? `is${base}Missing` : `has${base}`;
|
|
21
|
+
}
|
|
22
|
+
if (conditionType === 'keyCount') {
|
|
23
|
+
return isNegated ? `is${base}Empty` : `has${base}`;
|
|
24
|
+
}
|
|
25
|
+
// complex: default to hasX to avoid over-guessing semantics
|
|
26
|
+
return `has${base}`;
|
|
27
|
+
}
|
|
28
|
+
function capitalize(str) {
|
|
29
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
30
|
+
}
|
|
31
|
+
function isObjectExistenceCheck(node) {
|
|
32
|
+
// Check for patterns like !obj
|
|
33
|
+
if (node.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
|
|
34
|
+
// !obj
|
|
35
|
+
if (node.operator === '!' && node.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
// !!obj
|
|
39
|
+
if (node.operator === '!' &&
|
|
40
|
+
node.argument.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
|
|
41
|
+
node.argument.operator === '!' &&
|
|
42
|
+
node.argument.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
function isObjectKeyCountCheck(node) {
|
|
49
|
+
// Check for patterns like Object.keys(obj).length === 0 or Object.keys(obj).length > 0
|
|
50
|
+
if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
|
51
|
+
const { left, operator, right } = node;
|
|
52
|
+
// Check if left side is Object.keys(obj).length
|
|
53
|
+
if (left.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
54
|
+
left.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
55
|
+
left.property.name === 'length' &&
|
|
56
|
+
left.object.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
57
|
+
const callExpr = left.object;
|
|
58
|
+
if (callExpr.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
59
|
+
callExpr.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
60
|
+
callExpr.callee.object.name === 'Object' &&
|
|
61
|
+
callExpr.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
62
|
+
callExpr.callee.property.name === 'keys' &&
|
|
63
|
+
callExpr.arguments.length === 1 &&
|
|
64
|
+
callExpr.arguments[0].type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
65
|
+
// Check if right side is a number
|
|
66
|
+
if (right.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
67
|
+
typeof right.value === 'number' &&
|
|
68
|
+
['===', '!==', '>', '<', '>=', '<='].includes(operator)) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
function isComplexBooleanExpression(node) {
|
|
77
|
+
// Check for complex expressions involving objects
|
|
78
|
+
if (node.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
|
|
79
|
+
return (containsObjectCondition(node.left) || containsObjectCondition(node.right));
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
function containsObjectCondition(node) {
|
|
84
|
+
return (isObjectExistenceCheck(node) ||
|
|
85
|
+
isObjectKeyCountCheck(node) ||
|
|
86
|
+
isComplexBooleanExpression(node));
|
|
87
|
+
}
|
|
88
|
+
function extractObjectName(node) {
|
|
89
|
+
if (node.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
|
|
90
|
+
if (node.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
91
|
+
return node.argument.name;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
|
95
|
+
const { left } = node;
|
|
96
|
+
if (left.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
97
|
+
left.object.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
98
|
+
left.object.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
99
|
+
left.object.arguments.length === 1 &&
|
|
100
|
+
left.object.arguments[0].type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
101
|
+
return left.object.arguments[0].name;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (node.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
|
|
105
|
+
// For complex expressions, try to extract from the first object condition found
|
|
106
|
+
const leftObj = extractObjectName(node.left);
|
|
107
|
+
if (leftObj)
|
|
108
|
+
return leftObj;
|
|
109
|
+
return extractObjectName(node.right);
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
function analyzeBooleanCondition(node, context) {
|
|
114
|
+
const sourceCode = context.getSourceCode();
|
|
115
|
+
const expression = sourceCode.getText(node);
|
|
116
|
+
if (isObjectExistenceCheck(node)) {
|
|
117
|
+
const objectName = extractObjectName(node);
|
|
118
|
+
if (objectName) {
|
|
119
|
+
const isNegated = node.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
|
|
120
|
+
node.operator === '!' &&
|
|
121
|
+
!(node.argument.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
|
|
122
|
+
node.argument.operator === '!');
|
|
123
|
+
return {
|
|
124
|
+
type: 'existence',
|
|
125
|
+
objectName,
|
|
126
|
+
expression,
|
|
127
|
+
suggestedName: generateBooleanVariableName(objectName, 'existence', isNegated),
|
|
128
|
+
node,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (isObjectKeyCountCheck(node)) {
|
|
133
|
+
const objectName = extractObjectName(node);
|
|
134
|
+
if (objectName) {
|
|
135
|
+
const isNegated = expression.includes('=== 0') || expression.includes('<= 0');
|
|
136
|
+
return {
|
|
137
|
+
type: 'keyCount',
|
|
138
|
+
objectName,
|
|
139
|
+
expression,
|
|
140
|
+
suggestedName: generateBooleanVariableName(objectName, 'keyCount', isNegated),
|
|
141
|
+
node,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else if (isComplexBooleanExpression(node)) {
|
|
146
|
+
const objectName = extractObjectName(node);
|
|
147
|
+
if (objectName) {
|
|
148
|
+
return {
|
|
149
|
+
type: 'complex',
|
|
150
|
+
objectName,
|
|
151
|
+
expression,
|
|
152
|
+
suggestedName: generateBooleanVariableName(objectName, 'complex', false),
|
|
153
|
+
node,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
function findBooleanConditionsInDependencies(depsArray, context) {
|
|
160
|
+
const patterns = [];
|
|
161
|
+
for (const element of depsArray.elements) {
|
|
162
|
+
if (!element)
|
|
163
|
+
continue; // Skip holes in array
|
|
164
|
+
const pattern = analyzeBooleanCondition(element, context);
|
|
165
|
+
if (pattern) {
|
|
166
|
+
patterns.push(pattern);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return patterns;
|
|
170
|
+
}
|
|
171
|
+
function isAlreadyBooleanVariable(node) {
|
|
172
|
+
// Check if the dependency is already a boolean variable (starts with is, has, can, etc.)
|
|
173
|
+
if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
174
|
+
const name = node.name;
|
|
175
|
+
const booleanPrefixes = [
|
|
176
|
+
'is',
|
|
177
|
+
'has',
|
|
178
|
+
'can',
|
|
179
|
+
'should',
|
|
180
|
+
'will',
|
|
181
|
+
'was',
|
|
182
|
+
'were',
|
|
183
|
+
];
|
|
184
|
+
return booleanPrefixes.some((prefix) => name.toLowerCase().startsWith(prefix.toLowerCase()));
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
exports.optimizeObjectBooleanConditions = (0, createRule_1.createRule)({
|
|
189
|
+
name: 'optimize-object-boolean-conditions',
|
|
190
|
+
meta: {
|
|
191
|
+
type: 'suggestion',
|
|
192
|
+
docs: {
|
|
193
|
+
description: 'Detects and suggests optimizations for boolean conditions formed over objects in React hook dependencies. Suggests extracting boolean conditions into separate variables to reduce unnecessary re-computations when objects change frequently but the boolean condition changes less frequently.',
|
|
194
|
+
recommended: 'error',
|
|
195
|
+
},
|
|
196
|
+
schema: [],
|
|
197
|
+
messages: {
|
|
198
|
+
extractBooleanCondition: 'Extract boolean condition "{{expression}}" into a separate variable "{{suggestedName}}" to optimize hook re-runs. The condition depends on object "{{objectName}}" which may change frequently, but the boolean result changes less often.',
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
defaultOptions: [],
|
|
202
|
+
create(context) {
|
|
203
|
+
return {
|
|
204
|
+
CallExpression(node) {
|
|
205
|
+
if (!isHookCall(node)) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
// Get the dependency array argument (last argument for hooks)
|
|
209
|
+
const depsArg = node.arguments[node.arguments.length - 1];
|
|
210
|
+
if (!depsArg || depsArg.type !== utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// Find boolean conditions in dependencies
|
|
214
|
+
const patterns = findBooleanConditionsInDependencies(depsArg, context);
|
|
215
|
+
for (const pattern of patterns) {
|
|
216
|
+
// Skip if it's already a boolean variable
|
|
217
|
+
if (isAlreadyBooleanVariable(pattern.node)) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
context.report({
|
|
221
|
+
node: pattern.node,
|
|
222
|
+
messageId: 'extractBooleanCondition',
|
|
223
|
+
data: {
|
|
224
|
+
expression: pattern.expression,
|
|
225
|
+
suggestedName: pattern.suggestedName,
|
|
226
|
+
objectName: pattern.objectName,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
//# sourceMappingURL=optimize-object-boolean-conditions.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type Options = [
|
|
3
|
+
{
|
|
4
|
+
sideEffectPatterns?: Array<string | RegExp>;
|
|
5
|
+
}
|
|
6
|
+
];
|
|
7
|
+
export declare const parallelizeAsyncOperations: TSESLint.RuleModule<"parallelizeAsyncOperations", Options, TSESLint.RuleListener>;
|
|
8
|
+
export {};
|