@blumintinc/eslint-plugin-blumint 1.2.0 → 1.3.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 +40 -37
- package/lib/index.js +22 -13
- package/lib/rules/dynamic-https-errors.js +1 -1
- package/lib/rules/enforce-callback-memo.js +21 -3
- 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-firestore-set-merge.d.ts +1 -0
- package/lib/rules/enforce-firestore-set-merge.js +106 -0
- package/lib/rules/enforce-mock-firestore.d.ts +3 -0
- package/lib/rules/enforce-mock-firestore.js +76 -0
- package/lib/rules/enforce-verb-noun-naming.d.ts +1 -0
- package/lib/rules/enforce-verb-noun-naming.js +110 -0
- package/lib/rules/export-if-in-doubt.js +1 -1
- package/lib/rules/extract-global-constants.js +31 -2
- package/lib/rules/global-const-style.d.ts +2 -4
- package/lib/rules/global-const-style.js +48 -22
- package/lib/rules/no-compositing-layer-props.js +38 -4
- package/lib/rules/no-entire-object-hook-deps.js +68 -60
- package/lib/rules/no-explicit-return-type.d.ts +11 -0
- package/lib/rules/no-explicit-return-type.js +190 -0
- 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-type-over-interface.js +1 -1
- package/lib/rules/require-https-error.js +32 -23
- package/lib/rules/require-image-optimized.d.ts +7 -0
- package/lib/rules/require-image-optimized.js +82 -0
- package/lib/rules/semantic-function-prefixes.d.ts +1 -0
- package/lib/rules/semantic-function-prefixes.js +74 -0
- package/package.json +2 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.enforceVerbNounNaming = void 0;
|
|
7
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
8
|
+
const createRule_1 = require("../utils/createRule");
|
|
9
|
+
const compromise_1 = __importDefault(require("compromise"));
|
|
10
|
+
const PREPOSITIONS = ['to', 'from', 'with', 'by', 'at', 'of'];
|
|
11
|
+
exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
12
|
+
name: 'enforce-verb-noun-naming',
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'suggestion',
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Enforce verb phrases for functions and methods',
|
|
17
|
+
recommended: 'error',
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
messages: {
|
|
21
|
+
functionVerbPhrase: 'Function names should start with a verb phrase (e.g., fetchData, processRequest)',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultOptions: [],
|
|
25
|
+
create(context) {
|
|
26
|
+
function extractFirstWord(name) {
|
|
27
|
+
const firstChar = name.charAt(0);
|
|
28
|
+
const rest = name.slice(1);
|
|
29
|
+
const words = rest.split(/(?=[A-Z])/);
|
|
30
|
+
return firstChar + words[0];
|
|
31
|
+
}
|
|
32
|
+
function toSentence(name) {
|
|
33
|
+
return name.split(/(?=[A-Z])/).join(' ');
|
|
34
|
+
}
|
|
35
|
+
function getPossibleTags(sentence) {
|
|
36
|
+
const doc = (0, compromise_1.default)(sentence);
|
|
37
|
+
const terms = doc.terms().json();
|
|
38
|
+
if (terms.length === 0 || !terms[0].terms || !terms[0].terms[0].tags)
|
|
39
|
+
return [];
|
|
40
|
+
const tags = terms[0].terms[0].tags;
|
|
41
|
+
return tags;
|
|
42
|
+
}
|
|
43
|
+
function isVerbPhrase(name) {
|
|
44
|
+
const firstWord = extractFirstWord(name);
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
|
+
if (PREPOSITIONS.includes(firstWord.toLowerCase())) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
const tags = getPossibleTags(toSentence(name));
|
|
50
|
+
const isVerb = tags.includes('Verb');
|
|
51
|
+
const isPreposition = tags.includes('Preposition');
|
|
52
|
+
const isConjunction = tags.includes('Conjunction');
|
|
53
|
+
return isVerb || isPreposition || isConjunction;
|
|
54
|
+
}
|
|
55
|
+
function isJsxReturnFunction(node) {
|
|
56
|
+
if (node.type !== utils_1.AST_NODE_TYPES.FunctionDeclaration &&
|
|
57
|
+
node.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
58
|
+
node.type !== utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
// Check if function returns JSX
|
|
62
|
+
const sourceCode = context.getSourceCode();
|
|
63
|
+
const text = sourceCode.getText(node);
|
|
64
|
+
return text.includes('return <') || text.includes('=> <');
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
FunctionDeclaration(node) {
|
|
68
|
+
if (!node.id)
|
|
69
|
+
return;
|
|
70
|
+
if (isJsxReturnFunction(node)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!isVerbPhrase(node.id.name)) {
|
|
74
|
+
context.report({
|
|
75
|
+
node: node.id,
|
|
76
|
+
messageId: 'functionVerbPhrase',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
VariableDeclarator(node) {
|
|
81
|
+
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
82
|
+
return;
|
|
83
|
+
// Only check if it's a function
|
|
84
|
+
if (node.init?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
85
|
+
node.init?.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
86
|
+
if (isJsxReturnFunction(node.init)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!isVerbPhrase(node.id.name)) {
|
|
90
|
+
context.report({
|
|
91
|
+
node: node.id,
|
|
92
|
+
messageId: 'functionVerbPhrase',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
MethodDefinition(node) {
|
|
98
|
+
if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
99
|
+
return;
|
|
100
|
+
if (!isVerbPhrase(node.key.name)) {
|
|
101
|
+
context.report({
|
|
102
|
+
node: node.key,
|
|
103
|
+
messageId: 'functionVerbPhrase',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
//# sourceMappingURL=enforce-verb-noun-naming.js.map
|
|
@@ -9,7 +9,7 @@ exports.exportIfInDoubt = (0, createRule_1.createRule)({
|
|
|
9
9
|
type: 'suggestion',
|
|
10
10
|
docs: {
|
|
11
11
|
description: 'All top-level const definitions, type definitions, and functions should be exported',
|
|
12
|
-
recommended: '
|
|
12
|
+
recommended: 'error',
|
|
13
13
|
},
|
|
14
14
|
schema: [],
|
|
15
15
|
messages: {
|
|
@@ -19,13 +19,42 @@ function isFunctionDefinition(node) {
|
|
|
19
19
|
return (node?.type === 'FunctionExpression' ||
|
|
20
20
|
node?.type === 'ArrowFunctionExpression');
|
|
21
21
|
}
|
|
22
|
+
function isImmutableValue(node) {
|
|
23
|
+
if (!node)
|
|
24
|
+
return false;
|
|
25
|
+
switch (node.type) {
|
|
26
|
+
case 'Literal':
|
|
27
|
+
return true;
|
|
28
|
+
case 'TemplateLiteral':
|
|
29
|
+
return node.expressions.length === 0;
|
|
30
|
+
case 'UnaryExpression':
|
|
31
|
+
return isImmutableValue(node.argument);
|
|
32
|
+
case 'BinaryExpression':
|
|
33
|
+
if (node.left.type === 'PrivateIdentifier')
|
|
34
|
+
return false;
|
|
35
|
+
return isImmutableValue(node.left) && isImmutableValue(node.right);
|
|
36
|
+
default:
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
22
40
|
function isMutableValue(node) {
|
|
23
41
|
if (!node)
|
|
24
42
|
return false;
|
|
25
|
-
// Check for
|
|
26
|
-
if (node.type === '
|
|
43
|
+
// Check for object expressions (always mutable)
|
|
44
|
+
if (node.type === 'ObjectExpression') {
|
|
27
45
|
return true;
|
|
28
46
|
}
|
|
47
|
+
// Check array literals - mutable if empty or if they contain mutable values
|
|
48
|
+
if (node.type === 'ArrayExpression') {
|
|
49
|
+
// Empty arrays are mutable since they can be modified later
|
|
50
|
+
if (node.elements.length === 0)
|
|
51
|
+
return true;
|
|
52
|
+
// Arrays with spread elements are mutable
|
|
53
|
+
if (node.elements.some(element => !element || element.type === 'SpreadElement'))
|
|
54
|
+
return true;
|
|
55
|
+
// Arrays with non-immutable values are mutable
|
|
56
|
+
return node.elements.some(element => !isImmutableValue(element));
|
|
57
|
+
}
|
|
29
58
|
// Check for new expressions (e.g., new Map(), new Set())
|
|
30
59
|
if (node.type === 'NewExpression') {
|
|
31
60
|
return true;
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<
|
|
3
|
-
VariableDeclaration(node: TSESTree.VariableDeclaration): void;
|
|
4
|
-
}>;
|
|
1
|
+
type MessageIds = 'upperSnakeCase' | 'asConst';
|
|
2
|
+
declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
5
3
|
export default _default;
|
|
@@ -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,23 +108,53 @@ 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
|
|
121
|
+
// Skip regular expressions as they are already immutable
|
|
122
|
+
if (node.type === utils_1.AST_NODE_TYPES.Literal && 'regex' in node) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
116
125
|
return (node.type === utils_1.AST_NODE_TYPES.Literal ||
|
|
117
126
|
node.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
|
|
118
127
|
node.type === utils_1.AST_NODE_TYPES.ObjectExpression);
|
|
119
128
|
};
|
|
120
129
|
if (shouldHaveAsConst(init)) {
|
|
121
130
|
context.report({
|
|
122
|
-
node:
|
|
131
|
+
node: declaration,
|
|
123
132
|
messageId: 'asConst',
|
|
124
133
|
fix(fixer) {
|
|
125
|
-
const sourceCode = context.getSourceCode();
|
|
126
|
-
const initText = sourceCode.getText(init);
|
|
127
134
|
return fixer.replaceText(init, `${initText} as const`);
|
|
128
135
|
},
|
|
129
136
|
});
|
|
130
137
|
}
|
|
131
138
|
}
|
|
139
|
+
// Check for UPPER_SNAKE_CASE
|
|
140
|
+
if (!isUpperSnakeCase(name)) {
|
|
141
|
+
const newName = name
|
|
142
|
+
.replace(/([A-Z])/g, '_$1')
|
|
143
|
+
.toUpperCase()
|
|
144
|
+
.replace(/^_/, '');
|
|
145
|
+
context.report({
|
|
146
|
+
node: declaration,
|
|
147
|
+
messageId: 'upperSnakeCase',
|
|
148
|
+
fix(fixer) {
|
|
149
|
+
if (typeAnnotation) {
|
|
150
|
+
return fixer.replaceText(declaration, `${newName}${typeText} = ${initText}`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
return fixer.replaceText(declaration.id, newName);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
132
158
|
});
|
|
133
159
|
},
|
|
134
160
|
};
|
|
@@ -5,7 +5,7 @@ const utils_1 = require("@typescript-eslint/utils");
|
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
6
|
// Convert camelCase to kebab-case
|
|
7
7
|
function toKebabCase(str) {
|
|
8
|
-
return str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
|
|
8
|
+
return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
9
9
|
}
|
|
10
10
|
// Normalize property name to kebab-case for consistent lookup
|
|
11
11
|
function normalizePropertyName(name) {
|
|
@@ -38,7 +38,7 @@ exports.noCompositingLayerProps = (0, createRule_1.createRule)({
|
|
|
38
38
|
type: 'suggestion',
|
|
39
39
|
docs: {
|
|
40
40
|
description: 'Warn when using CSS properties that trigger compositing layers',
|
|
41
|
-
recommended: '
|
|
41
|
+
recommended: 'error',
|
|
42
42
|
},
|
|
43
43
|
schema: [],
|
|
44
44
|
messages: {
|
|
@@ -49,10 +49,10 @@ exports.noCompositingLayerProps = (0, createRule_1.createRule)({
|
|
|
49
49
|
create(context) {
|
|
50
50
|
const seenNodes = new WeakSet();
|
|
51
51
|
function checkPropertyValue(value) {
|
|
52
|
-
return COMPOSITING_VALUES.has(value) ||
|
|
52
|
+
return (COMPOSITING_VALUES.has(value) ||
|
|
53
53
|
value.includes('translate3d') ||
|
|
54
54
|
value.includes('scale3d') ||
|
|
55
|
-
value.includes('translateZ');
|
|
55
|
+
value.includes('translateZ'));
|
|
56
56
|
}
|
|
57
57
|
function checkProperty(propertyName, propertyValue) {
|
|
58
58
|
const normalizedName = normalizePropertyName(propertyName);
|
|
@@ -73,11 +73,45 @@ exports.noCompositingLayerProps = (0, createRule_1.createRule)({
|
|
|
73
73
|
}
|
|
74
74
|
return false;
|
|
75
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
|
+
}
|
|
76
107
|
function checkNode(node) {
|
|
77
108
|
// Skip if we've already processed this node
|
|
78
109
|
if (seenNodes.has(node))
|
|
79
110
|
return;
|
|
80
111
|
seenNodes.add(node);
|
|
112
|
+
// Skip if not in a style context
|
|
113
|
+
if (!isStyleContext(node))
|
|
114
|
+
return;
|
|
81
115
|
let propertyName = '';
|
|
82
116
|
let propertyValue = '';
|
|
83
117
|
// Get property name
|
|
@@ -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) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type Options = [
|
|
2
|
+
{
|
|
3
|
+
allowRecursiveFunctions?: boolean;
|
|
4
|
+
allowOverloadedFunctions?: boolean;
|
|
5
|
+
allowInterfaceMethodSignatures?: boolean;
|
|
6
|
+
allowAbstractMethodSignatures?: boolean;
|
|
7
|
+
allowDtsFiles?: boolean;
|
|
8
|
+
}
|
|
9
|
+
];
|
|
10
|
+
export declare const noExplicitReturnType: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noExplicitReturnType", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
11
|
+
export {};
|