@blumintinc/eslint-plugin-blumint 1.1.9 → 1.2.1
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 +3 -0
- package/lib/index.js +7 -1
- package/lib/rules/dynamic-https-errors.js +1 -1
- package/lib/rules/enforce-callback-memo.js +21 -3
- package/lib/rules/enforce-exported-function-types.d.ts +3 -0
- package/lib/rules/enforce-exported-function-types.js +249 -0
- package/lib/rules/enforce-firestore-doc-ref-generic.d.ts +10 -0
- package/lib/rules/enforce-firestore-doc-ref-generic.js +134 -0
- package/lib/rules/enforce-memoize-async.d.ts +1 -0
- package/lib/rules/enforce-memoize-async.js +70 -0
- package/lib/rules/export-if-in-doubt.js +1 -1
- package/lib/rules/global-const-style.d.ts +2 -4
- package/lib/rules/global-const-style.js +44 -22
- package/lib/rules/no-compositing-layer-props.d.ts +1 -0
- package/lib/rules/no-compositing-layer-props.js +161 -0
- package/lib/rules/no-entire-object-hook-deps.js +68 -60
- package/lib/rules/no-unused-props.js +33 -8
- package/lib/rules/no-useless-fragment.js +1 -1
- package/lib/rules/prefer-fragment-shorthand.js +1 -1
- package/lib/rules/prefer-settings-object.d.ts +11 -0
- package/lib/rules/prefer-settings-object.js +129 -0
- package/lib/rules/prefer-type-over-interface.js +1 -1
- package/lib/rules/require-https-error.js +32 -23
- package/lib/rules/require-image-overlayed.js +2 -0
- package/lib/rules/use-custom-memo.d.ts +1 -0
- package/lib/rules/use-custom-memo.js +65 -0
- package/lib/utils/ASTHelpers.js +14 -0
- package/package.json +1 -1
|
@@ -21,7 +21,8 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
21
21
|
defaultOptions: [],
|
|
22
22
|
create(context) {
|
|
23
23
|
// Check if the file is a TypeScript file
|
|
24
|
-
const isTypeScript = context.getFilename().endsWith('.ts') ||
|
|
24
|
+
const isTypeScript = context.getFilename().endsWith('.ts') ||
|
|
25
|
+
context.getFilename().endsWith('.tsx');
|
|
25
26
|
return {
|
|
26
27
|
VariableDeclaration(node) {
|
|
27
28
|
// Only check top-level const declarations
|
|
@@ -33,7 +34,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
36
|
// Skip if any declaration is a function component, arrow function, forwardRef, or memo
|
|
36
|
-
const shouldSkip = node.declarations.some(declaration => {
|
|
37
|
+
const shouldSkip = node.declarations.some((declaration) => {
|
|
37
38
|
if (declaration.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
38
39
|
return false;
|
|
39
40
|
}
|
|
@@ -44,7 +45,8 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
44
45
|
return false;
|
|
45
46
|
}
|
|
46
47
|
// Skip function components (uppercase name + arrow function)
|
|
47
|
-
if (/^[A-Z]/.test(name) &&
|
|
48
|
+
if (/^[A-Z]/.test(name) &&
|
|
49
|
+
init.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
48
50
|
return true;
|
|
49
51
|
}
|
|
50
52
|
// Skip any arrow function
|
|
@@ -84,26 +86,20 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
84
86
|
init.type === utils_1.AST_NODE_TYPES.NewExpression) {
|
|
85
87
|
return;
|
|
86
88
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const newName = name
|
|
94
|
-
.replace(/([A-Z])/g, '_$1')
|
|
95
|
-
.toUpperCase()
|
|
96
|
-
.replace(/^_/, '');
|
|
97
|
-
return fixer.replaceText(declaration.id, newName);
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
}
|
|
89
|
+
const sourceCode = context.getSourceCode();
|
|
90
|
+
const initText = sourceCode.getText(init);
|
|
91
|
+
const typeAnnotation = declaration.id.typeAnnotation;
|
|
92
|
+
const typeText = typeAnnotation
|
|
93
|
+
? sourceCode.getText(typeAnnotation)
|
|
94
|
+
: '';
|
|
101
95
|
// Only check for as const in TypeScript files
|
|
102
96
|
if (isTypeScript) {
|
|
103
97
|
const isAsConstExpression = (node) => {
|
|
104
98
|
if (node.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
105
|
-
return (node.typeAnnotation?.type ===
|
|
106
|
-
|
|
99
|
+
return (node.typeAnnotation?.type ===
|
|
100
|
+
utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
101
|
+
node.typeAnnotation?.typeName
|
|
102
|
+
?.name === 'const');
|
|
107
103
|
}
|
|
108
104
|
return false;
|
|
109
105
|
};
|
|
@@ -112,6 +108,15 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
112
108
|
if (isAsConstExpression(node)) {
|
|
113
109
|
return false;
|
|
114
110
|
}
|
|
111
|
+
// Handle type assertions
|
|
112
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
|
|
113
|
+
node.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
114
|
+
return shouldHaveAsConst(node.expression);
|
|
115
|
+
}
|
|
116
|
+
// Skip if there's an explicit type annotation
|
|
117
|
+
if (declaration.id.typeAnnotation) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
115
120
|
// Check if it's a literal, array, or object that should have as const
|
|
116
121
|
return (node.type === utils_1.AST_NODE_TYPES.Literal ||
|
|
117
122
|
node.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
|
|
@@ -119,16 +124,33 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
119
124
|
};
|
|
120
125
|
if (shouldHaveAsConst(init)) {
|
|
121
126
|
context.report({
|
|
122
|
-
node:
|
|
127
|
+
node: declaration,
|
|
123
128
|
messageId: 'asConst',
|
|
124
129
|
fix(fixer) {
|
|
125
|
-
const sourceCode = context.getSourceCode();
|
|
126
|
-
const initText = sourceCode.getText(init);
|
|
127
130
|
return fixer.replaceText(init, `${initText} as const`);
|
|
128
131
|
},
|
|
129
132
|
});
|
|
130
133
|
}
|
|
131
134
|
}
|
|
135
|
+
// Check for UPPER_SNAKE_CASE
|
|
136
|
+
if (!isUpperSnakeCase(name)) {
|
|
137
|
+
const newName = name
|
|
138
|
+
.replace(/([A-Z])/g, '_$1')
|
|
139
|
+
.toUpperCase()
|
|
140
|
+
.replace(/^_/, '');
|
|
141
|
+
context.report({
|
|
142
|
+
node: declaration,
|
|
143
|
+
messageId: 'upperSnakeCase',
|
|
144
|
+
fix(fixer) {
|
|
145
|
+
if (typeAnnotation) {
|
|
146
|
+
return fixer.replaceText(declaration, `${newName}${typeText} = ${initText}`);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
return fixer.replaceText(declaration.id, newName);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
132
154
|
});
|
|
133
155
|
},
|
|
134
156
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noCompositingLayerProps: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"compositingLayer", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noCompositingLayerProps = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
// Convert camelCase to kebab-case
|
|
7
|
+
function toKebabCase(str) {
|
|
8
|
+
return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
9
|
+
}
|
|
10
|
+
// Normalize property name to kebab-case for consistent lookup
|
|
11
|
+
function normalizePropertyName(name) {
|
|
12
|
+
// If already contains hyphens, assume it's kebab-case
|
|
13
|
+
if (name.includes('-'))
|
|
14
|
+
return name.toLowerCase();
|
|
15
|
+
// Convert camelCase to kebab-case
|
|
16
|
+
return toKebabCase(name).toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
const COMPOSITING_PROPERTIES = new Set([
|
|
19
|
+
'filter',
|
|
20
|
+
'backdrop-filter',
|
|
21
|
+
'will-change',
|
|
22
|
+
'transform',
|
|
23
|
+
'perspective',
|
|
24
|
+
'backface-visibility',
|
|
25
|
+
'contain',
|
|
26
|
+
'mix-blend-mode',
|
|
27
|
+
'opacity',
|
|
28
|
+
]);
|
|
29
|
+
const COMPOSITING_VALUES = new Set([
|
|
30
|
+
'translate3d',
|
|
31
|
+
'scale3d',
|
|
32
|
+
'translateZ',
|
|
33
|
+
'transparent',
|
|
34
|
+
]);
|
|
35
|
+
exports.noCompositingLayerProps = (0, createRule_1.createRule)({
|
|
36
|
+
name: 'no-compositing-layer-props',
|
|
37
|
+
meta: {
|
|
38
|
+
type: 'suggestion',
|
|
39
|
+
docs: {
|
|
40
|
+
description: 'Warn when using CSS properties that trigger compositing layers',
|
|
41
|
+
recommended: 'error',
|
|
42
|
+
},
|
|
43
|
+
schema: [],
|
|
44
|
+
messages: {
|
|
45
|
+
compositingLayer: '{{property}} may trigger a new compositing layer which can impact performance. Consider using alternative properties or add an eslint-disable comment if the layer promotion is intentional.',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
defaultOptions: [],
|
|
49
|
+
create(context) {
|
|
50
|
+
const seenNodes = new WeakSet();
|
|
51
|
+
function checkPropertyValue(value) {
|
|
52
|
+
return (COMPOSITING_VALUES.has(value) ||
|
|
53
|
+
value.includes('translate3d') ||
|
|
54
|
+
value.includes('scale3d') ||
|
|
55
|
+
value.includes('translateZ'));
|
|
56
|
+
}
|
|
57
|
+
function checkProperty(propertyName, propertyValue) {
|
|
58
|
+
const normalizedName = normalizePropertyName(propertyName);
|
|
59
|
+
if (COMPOSITING_PROPERTIES.has(normalizedName)) {
|
|
60
|
+
// Special case for opacity - only warn if it's animated or fractional
|
|
61
|
+
if (normalizedName === 'opacity') {
|
|
62
|
+
if (!propertyValue)
|
|
63
|
+
return false;
|
|
64
|
+
const numValue = Number.parseFloat(propertyValue);
|
|
65
|
+
if (Number.isNaN(numValue))
|
|
66
|
+
return false;
|
|
67
|
+
return numValue > 0 && numValue < 1;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (propertyValue && checkPropertyValue(propertyValue)) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
function isStyleContext(node) {
|
|
77
|
+
let current = node;
|
|
78
|
+
while (current?.parent) {
|
|
79
|
+
// Check for JSX style attribute
|
|
80
|
+
if (current.parent.type === utils_1.AST_NODE_TYPES.JSXAttribute &&
|
|
81
|
+
current.parent.name.type === utils_1.AST_NODE_TYPES.JSXIdentifier &&
|
|
82
|
+
current.parent.name.name === 'style') {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
// Check for style-related variable names or properties
|
|
86
|
+
if (current.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
87
|
+
current.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
88
|
+
/style/i.test(current.id.name)) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
// Check for style-related object property assignments
|
|
92
|
+
if (current.parent.type === utils_1.AST_NODE_TYPES.Property &&
|
|
93
|
+
current.parent.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
94
|
+
/style/i.test(current.parent.key.name)) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
// Skip if we're in a TypeScript type definition
|
|
98
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration ||
|
|
99
|
+
current.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration ||
|
|
100
|
+
current.type === utils_1.AST_NODE_TYPES.TSPropertySignature) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
current = current.parent;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
function checkNode(node) {
|
|
108
|
+
// Skip if we've already processed this node
|
|
109
|
+
if (seenNodes.has(node))
|
|
110
|
+
return;
|
|
111
|
+
seenNodes.add(node);
|
|
112
|
+
// Skip if not in a style context
|
|
113
|
+
if (!isStyleContext(node))
|
|
114
|
+
return;
|
|
115
|
+
let propertyName = '';
|
|
116
|
+
let propertyValue = '';
|
|
117
|
+
// Get property name
|
|
118
|
+
if (node.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
119
|
+
propertyName = node.key.name;
|
|
120
|
+
}
|
|
121
|
+
else if (node.key.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
122
|
+
propertyName = String(node.key.value);
|
|
123
|
+
}
|
|
124
|
+
// Get property value if it's a string literal or numeric literal
|
|
125
|
+
if (node.value.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
126
|
+
propertyValue = String(node.value.value);
|
|
127
|
+
}
|
|
128
|
+
if (checkProperty(propertyName, propertyValue)) {
|
|
129
|
+
context.report({
|
|
130
|
+
node,
|
|
131
|
+
messageId: 'compositingLayer',
|
|
132
|
+
data: {
|
|
133
|
+
property: propertyName,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
// Handle object literal properties (inline styles)
|
|
140
|
+
Property(node) {
|
|
141
|
+
if (node.parent?.type !== utils_1.AST_NODE_TYPES.ObjectExpression)
|
|
142
|
+
return;
|
|
143
|
+
checkNode(node);
|
|
144
|
+
},
|
|
145
|
+
// Handle JSX style attributes
|
|
146
|
+
JSXAttribute(node) {
|
|
147
|
+
if (node.name.name !== 'style')
|
|
148
|
+
return;
|
|
149
|
+
if (node.value?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
|
|
150
|
+
node.value.expression.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
151
|
+
node.value.expression.properties.forEach((prop) => {
|
|
152
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property) {
|
|
153
|
+
checkNode(prop);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
//# sourceMappingURL=no-compositing-layer-props.js.map
|
|
@@ -3,11 +3,41 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.noEntireObjectHookDeps = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const typescript_1 = require("typescript");
|
|
6
7
|
const HOOK_NAMES = new Set(['useEffect', 'useCallback', 'useMemo']);
|
|
7
8
|
function isHookCall(node) {
|
|
8
9
|
const callee = node.callee;
|
|
9
|
-
return (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
10
|
-
|
|
10
|
+
return (callee.type === utils_1.AST_NODE_TYPES.Identifier && HOOK_NAMES.has(callee.name));
|
|
11
|
+
}
|
|
12
|
+
function isArrayOrPrimitive(checker, esTreeNode, nodeMap) {
|
|
13
|
+
const tsNode = nodeMap.get(esTreeNode);
|
|
14
|
+
if (!tsNode)
|
|
15
|
+
return false;
|
|
16
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
17
|
+
// Check if it's a primitive type
|
|
18
|
+
if (type.flags &
|
|
19
|
+
(typescript_1.TypeFlags.String |
|
|
20
|
+
typescript_1.TypeFlags.Number |
|
|
21
|
+
typescript_1.TypeFlags.Boolean |
|
|
22
|
+
typescript_1.TypeFlags.Null |
|
|
23
|
+
typescript_1.TypeFlags.Undefined |
|
|
24
|
+
typescript_1.TypeFlags.Void |
|
|
25
|
+
typescript_1.TypeFlags.Never |
|
|
26
|
+
typescript_1.TypeFlags.Any |
|
|
27
|
+
typescript_1.TypeFlags.Unknown |
|
|
28
|
+
typescript_1.TypeFlags.BigInt |
|
|
29
|
+
typescript_1.TypeFlags.ESSymbol)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
// Check if it's an array type
|
|
33
|
+
const typeNode = checker.typeToTypeNode(type, undefined, undefined);
|
|
34
|
+
if (type.symbol?.name === 'Array' ||
|
|
35
|
+
type.symbol?.escapedName === 'Array' ||
|
|
36
|
+
(typeNode && ((0, typescript_1.isArrayTypeNode)(typeNode) || (0, typescript_1.isTupleTypeNode)(typeNode)))) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
// If it's not a primitive or array, and has properties, it's an object
|
|
40
|
+
return false;
|
|
11
41
|
}
|
|
12
42
|
function getObjectUsagesInHook(hookBody, objectName) {
|
|
13
43
|
const usages = new Set();
|
|
@@ -25,75 +55,44 @@ function getObjectUsagesInHook(hookBody, objectName) {
|
|
|
25
55
|
parts.unshift(current.property.name);
|
|
26
56
|
current = current.object;
|
|
27
57
|
}
|
|
28
|
-
if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
29
|
-
|
|
58
|
+
if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
59
|
+
current.name === objectName) {
|
|
30
60
|
return parts.join('.');
|
|
31
61
|
}
|
|
32
62
|
return null;
|
|
33
63
|
}
|
|
34
64
|
function visit(node) {
|
|
35
|
-
if (visited.has(node))
|
|
65
|
+
if (visited.has(node))
|
|
36
66
|
return;
|
|
37
|
-
}
|
|
38
67
|
visited.add(node);
|
|
39
|
-
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
usages.add(
|
|
68
|
+
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
69
|
+
const path = buildAccessPath(node);
|
|
70
|
+
if (path) {
|
|
71
|
+
usages.add(`${objectName}.${path}`);
|
|
43
72
|
}
|
|
44
73
|
}
|
|
45
|
-
// Visit
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
node.arguments.forEach(arg => visit(arg));
|
|
57
|
-
break;
|
|
58
|
-
case utils_1.AST_NODE_TYPES.MemberExpression:
|
|
59
|
-
visit(node.object);
|
|
60
|
-
if (!node.computed) {
|
|
61
|
-
visit(node.property);
|
|
62
|
-
}
|
|
63
|
-
break;
|
|
64
|
-
case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
|
|
65
|
-
case utils_1.AST_NODE_TYPES.FunctionExpression:
|
|
66
|
-
if (node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
67
|
-
visit(node.body);
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
visit(node.body);
|
|
74
|
+
// Visit all child nodes
|
|
75
|
+
for (const key in node) {
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
+
const child = node[key];
|
|
78
|
+
if (child && typeof child === 'object') {
|
|
79
|
+
if (Array.isArray(child)) {
|
|
80
|
+
child.forEach((item) => {
|
|
81
|
+
if (item && typeof item === 'object') {
|
|
82
|
+
visit(item);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
71
85
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (node.argument) {
|
|
75
|
-
visit(node.argument);
|
|
86
|
+
else if ('type' in child) {
|
|
87
|
+
visit(child);
|
|
76
88
|
}
|
|
77
|
-
|
|
78
|
-
case utils_1.AST_NODE_TYPES.ConditionalExpression:
|
|
79
|
-
visit(node.test);
|
|
80
|
-
visit(node.consequent);
|
|
81
|
-
visit(node.alternate);
|
|
82
|
-
break;
|
|
83
|
-
case utils_1.AST_NODE_TYPES.LogicalExpression:
|
|
84
|
-
case utils_1.AST_NODE_TYPES.BinaryExpression:
|
|
85
|
-
visit(node.left);
|
|
86
|
-
visit(node.right);
|
|
87
|
-
break;
|
|
88
|
-
case utils_1.AST_NODE_TYPES.TemplateLiteral:
|
|
89
|
-
node.expressions.forEach(expr => visit(expr));
|
|
90
|
-
break;
|
|
89
|
+
}
|
|
91
90
|
}
|
|
92
91
|
}
|
|
93
92
|
visit(hookBody);
|
|
94
93
|
// Filter out intermediate paths
|
|
95
94
|
const paths = Array.from(usages);
|
|
96
|
-
const filteredPaths = paths.filter(path => !paths.some(otherPath => otherPath !== path && otherPath.startsWith(path + '.')));
|
|
95
|
+
const filteredPaths = paths.filter((path) => !paths.some((otherPath) => otherPath !== path && otherPath.startsWith(path + '.')));
|
|
97
96
|
return new Set(filteredPaths);
|
|
98
97
|
}
|
|
99
98
|
exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
@@ -112,6 +111,13 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
112
111
|
},
|
|
113
112
|
defaultOptions: [],
|
|
114
113
|
create(context) {
|
|
114
|
+
const parserServices = context.parserServices;
|
|
115
|
+
// Check if we have access to TypeScript services
|
|
116
|
+
if (!parserServices?.program || !parserServices?.esTreeNodeToTSNodeMap) {
|
|
117
|
+
throw new Error('You have to enable the `project` setting in parser options to use this rule');
|
|
118
|
+
}
|
|
119
|
+
const checker = parserServices.program.getTypeChecker();
|
|
120
|
+
const nodeMap = parserServices.esTreeNodeToTSNodeMap;
|
|
115
121
|
return {
|
|
116
122
|
CallExpression(node) {
|
|
117
123
|
if (!isHookCall(node)) {
|
|
@@ -119,8 +125,7 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
119
125
|
}
|
|
120
126
|
// Get the dependency array argument
|
|
121
127
|
const depsArg = node.arguments[node.arguments.length - 1];
|
|
122
|
-
if (!depsArg ||
|
|
123
|
-
depsArg.type !== utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
128
|
+
if (!depsArg || depsArg.type !== utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
124
129
|
return;
|
|
125
130
|
}
|
|
126
131
|
// Get the hook callback function
|
|
@@ -131,10 +136,13 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
131
136
|
return;
|
|
132
137
|
}
|
|
133
138
|
// Check each dependency in the array
|
|
134
|
-
depsArg.elements.forEach(element => {
|
|
135
|
-
if (element &&
|
|
136
|
-
element.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
139
|
+
depsArg.elements.forEach((element) => {
|
|
140
|
+
if (element && element.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
137
141
|
const objectName = element.name;
|
|
142
|
+
// Skip if the dependency is an array or primitive type
|
|
143
|
+
if (isArrayOrPrimitive(checker, element, nodeMap)) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
138
146
|
const usages = getObjectUsagesInHook(callbackArg.body, objectName);
|
|
139
147
|
// If we found specific field usages and the entire object is in deps
|
|
140
148
|
if (usages.size > 0) {
|
|
@@ -25,16 +25,28 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
25
25
|
return {
|
|
26
26
|
TSTypeAliasDeclaration(node) {
|
|
27
27
|
if (node.id.name.endsWith('Props')) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
member.
|
|
33
|
-
|
|
28
|
+
const props = {};
|
|
29
|
+
function extractProps(typeNode) {
|
|
30
|
+
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
31
|
+
typeNode.members.forEach((member) => {
|
|
32
|
+
if (member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
|
|
33
|
+
member.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
34
|
+
props[member.key.name] = member.key;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
else if (typeNode.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
|
|
39
|
+
typeNode.types.forEach(extractProps);
|
|
40
|
+
}
|
|
41
|
+
else if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
42
|
+
// For referenced types like FormControlLabelProps, we need to track that these props should be forwarded
|
|
43
|
+
if (typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
44
|
+
props[`...${typeNode.typeName.name}`] = typeNode.typeName;
|
|
34
45
|
}
|
|
35
|
-
}
|
|
36
|
-
propsTypes.set(node.id.name, props);
|
|
46
|
+
}
|
|
37
47
|
}
|
|
48
|
+
extractProps(node.typeAnnotation);
|
|
49
|
+
propsTypes.set(node.id.name, props);
|
|
38
50
|
}
|
|
39
51
|
},
|
|
40
52
|
VariableDeclaration(node) {
|
|
@@ -56,6 +68,19 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
56
68
|
prop.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
57
69
|
used.add(prop.key.name);
|
|
58
70
|
}
|
|
71
|
+
else if (prop.type === utils_1.AST_NODE_TYPES.RestElement &&
|
|
72
|
+
prop.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
73
|
+
// Handle rest spread operator {...rest}
|
|
74
|
+
// When a rest operator is used, all remaining props are considered used
|
|
75
|
+
const propsType = propsTypes.get(typeName);
|
|
76
|
+
if (propsType) {
|
|
77
|
+
Object.keys(propsType).forEach((key) => {
|
|
78
|
+
if (key.startsWith('...')) {
|
|
79
|
+
used.add(key);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
59
84
|
});
|
|
60
85
|
usedProps.set(typeName, used);
|
|
61
86
|
}
|
|
@@ -35,7 +35,7 @@ exports.noUselessFragment = {
|
|
|
35
35
|
type: 'suggestion',
|
|
36
36
|
docs: {
|
|
37
37
|
description: 'Prevent unnecessary use of React fragments',
|
|
38
|
-
recommended: '
|
|
38
|
+
recommended: 'error',
|
|
39
39
|
},
|
|
40
40
|
messages: {
|
|
41
41
|
noUselessFragment: 'React fragment is unnecessary when wrapping a single child',
|
|
@@ -28,7 +28,7 @@ exports.preferFragmentShorthand = {
|
|
|
28
28
|
type: 'suggestion',
|
|
29
29
|
docs: {
|
|
30
30
|
description: 'Prefer <> shorthand for <React.Fragment>',
|
|
31
|
-
recommended: '
|
|
31
|
+
recommended: 'error',
|
|
32
32
|
},
|
|
33
33
|
messages: {
|
|
34
34
|
preferShorthand: 'Use <> shorthand for <React.Fragment>, unless a key is required for an iterator',
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type MessageIds = 'tooManyParams' | 'sameTypeParams';
|
|
2
|
+
type Options = [
|
|
3
|
+
{
|
|
4
|
+
minimumParameters?: number;
|
|
5
|
+
checkSameTypeParameters?: boolean;
|
|
6
|
+
ignoreBoundMethods?: boolean;
|
|
7
|
+
ignoreVariadicFunctions?: boolean;
|
|
8
|
+
}
|
|
9
|
+
];
|
|
10
|
+
export declare const preferSettingsObject: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
11
|
+
export {};
|