@blumintinc/eslint-plugin-blumint 1.2.0 → 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/lib/index.js +4 -1
- 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/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.js +38 -4
- 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-type-over-interface.js +1 -1
- package/lib/rules/require-https-error.js +32 -23
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -37,10 +37,11 @@ const avoid_utils_directory_1 = require("./rules/avoid-utils-directory");
|
|
|
37
37
|
const no_entire_object_hook_deps_1 = require("./rules/no-entire-object-hook-deps");
|
|
38
38
|
const enforce_firestore_path_utils_1 = require("./rules/enforce-firestore-path-utils");
|
|
39
39
|
const no_compositing_layer_props_1 = require("./rules/no-compositing-layer-props");
|
|
40
|
+
const enforce_firestore_doc_ref_generic_1 = require("./rules/enforce-firestore-doc-ref-generic");
|
|
40
41
|
module.exports = {
|
|
41
42
|
meta: {
|
|
42
43
|
name: '@blumintinc/eslint-plugin-blumint',
|
|
43
|
-
version: '1.2.
|
|
44
|
+
version: '1.2.1',
|
|
44
45
|
},
|
|
45
46
|
parseOptions: {
|
|
46
47
|
ecmaVersion: 2020,
|
|
@@ -83,6 +84,7 @@ module.exports = {
|
|
|
83
84
|
'@blumintinc/blumint/enforce-safe-stringify': 'error',
|
|
84
85
|
'@blumintinc/blumint/no-entire-object-hook-deps': 'error',
|
|
85
86
|
'@blumintinc/blumint/no-compositing-layer-props': 'warn',
|
|
87
|
+
'@blumintinc/blumint/enforce-firestore-doc-ref-generic': 'error',
|
|
86
88
|
},
|
|
87
89
|
},
|
|
88
90
|
},
|
|
@@ -121,6 +123,7 @@ module.exports = {
|
|
|
121
123
|
'no-entire-object-hook-deps': no_entire_object_hook_deps_1.noEntireObjectHookDeps,
|
|
122
124
|
'enforce-firestore-path-utils': enforce_firestore_path_utils_1.enforceFirestorePathUtils,
|
|
123
125
|
'no-compositing-layer-props': no_compositing_layer_props_1.noCompositingLayerProps,
|
|
126
|
+
'enforce-firestore-doc-ref-generic': enforce_firestore_doc_ref_generic_1.enforceFirestoreDocRefGeneric,
|
|
124
127
|
},
|
|
125
128
|
};
|
|
126
129
|
//# sourceMappingURL=index.js.map
|
|
@@ -20,7 +20,7 @@ exports.dynamicHttpsErrors = (0, createRule_1.createRule)({
|
|
|
20
20
|
type: 'suggestion',
|
|
21
21
|
docs: {
|
|
22
22
|
description: 'Dynamic error details should only be in the third argument of the HttpsError constructor. The second argument is hashed to produce a unique id.',
|
|
23
|
-
recommended: '
|
|
23
|
+
recommended: 'error',
|
|
24
24
|
},
|
|
25
25
|
schema: [],
|
|
26
26
|
messages: {
|
|
@@ -40,6 +40,19 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
40
40
|
}
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
|
+
function hasJSXWithFunction(node) {
|
|
44
|
+
if (node.type === utils_1.TSESTree.AST_NODE_TYPES.JSXElement) {
|
|
45
|
+
return node.openingElement.attributes.some((attr) => {
|
|
46
|
+
if (attr.type === utils_1.TSESTree.AST_NODE_TYPES.JSXAttribute && attr.value) {
|
|
47
|
+
if (attr.value.type === utils_1.TSESTree.AST_NODE_TYPES.JSXExpressionContainer) {
|
|
48
|
+
return containsFunction(attr.value.expression);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
43
56
|
function checkJSXAttribute(node) {
|
|
44
57
|
if (!node.value ||
|
|
45
58
|
node.value.type !== utils_1.TSESTree.AST_NODE_TYPES.JSXExpressionContainer) {
|
|
@@ -61,10 +74,15 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
61
74
|
});
|
|
62
75
|
return;
|
|
63
76
|
}
|
|
64
|
-
// Check for objects/arrays containing functions
|
|
77
|
+
// Check for objects/arrays/JSX elements containing functions
|
|
65
78
|
if ((expression.type === utils_1.TSESTree.AST_NODE_TYPES.ObjectExpression ||
|
|
66
|
-
expression.type === utils_1.TSESTree.AST_NODE_TYPES.ArrayExpression
|
|
67
|
-
|
|
79
|
+
expression.type === utils_1.TSESTree.AST_NODE_TYPES.ArrayExpression ||
|
|
80
|
+
expression.type === utils_1.TSESTree.AST_NODE_TYPES.JSXElement) &&
|
|
81
|
+
(containsFunction(expression) || hasJSXWithFunction(expression))) {
|
|
82
|
+
// Skip reporting if this is a JSX element and we already reported an inline function
|
|
83
|
+
if (expression.type === utils_1.TSESTree.AST_NODE_TYPES.JSXElement && hasJSXWithFunction(expression)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
68
86
|
context.report({
|
|
69
87
|
node,
|
|
70
88
|
messageId: 'enforceMemo',
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Enforce generic argument for Firestore DocumentReference
|
|
3
|
+
* @author BluMint
|
|
4
|
+
*/
|
|
5
|
+
type MessageIds = 'missingGeneric' | 'invalidGeneric';
|
|
6
|
+
/**
|
|
7
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
8
|
+
*/
|
|
9
|
+
export declare const enforceFirestoreDocRefGeneric: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Enforce generic argument for Firestore DocumentReference
|
|
4
|
+
* @author BluMint
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.enforceFirestoreDocRefGeneric = void 0;
|
|
8
|
+
/**
|
|
9
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
10
|
+
*/
|
|
11
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
12
|
+
const createRule_1 = require("../utils/createRule");
|
|
13
|
+
/**
|
|
14
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
15
|
+
*/
|
|
16
|
+
exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
17
|
+
name: 'enforce-firestore-doc-ref-generic',
|
|
18
|
+
meta: {
|
|
19
|
+
type: 'problem',
|
|
20
|
+
docs: {
|
|
21
|
+
description: 'Enforce generic argument for Firestore DocumentReference',
|
|
22
|
+
recommended: 'error',
|
|
23
|
+
requiresTypeChecking: true,
|
|
24
|
+
},
|
|
25
|
+
schema: [],
|
|
26
|
+
messages: {
|
|
27
|
+
missingGeneric: 'DocumentReference must specify a generic type argument',
|
|
28
|
+
invalidGeneric: 'DocumentReference must not use "any" or "{}" as generic type argument',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultOptions: [],
|
|
32
|
+
create(context) {
|
|
33
|
+
const typeCache = new Map();
|
|
34
|
+
function hasInvalidType(node) {
|
|
35
|
+
if (!node)
|
|
36
|
+
return false;
|
|
37
|
+
switch (node.type) {
|
|
38
|
+
case utils_1.AST_NODE_TYPES.TSAnyKeyword:
|
|
39
|
+
return true;
|
|
40
|
+
case utils_1.AST_NODE_TYPES.TSTypeLiteral:
|
|
41
|
+
if (!node.members || node.members.length === 0) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return node.members.some((member) => {
|
|
45
|
+
if (member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
|
|
46
|
+
member.typeAnnotation) {
|
|
47
|
+
return hasInvalidType(member.typeAnnotation.typeAnnotation);
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
});
|
|
51
|
+
case utils_1.AST_NODE_TYPES.TSTypeReference:
|
|
52
|
+
if (node.typeParameters) {
|
|
53
|
+
return node.typeParameters.params.some(hasInvalidType);
|
|
54
|
+
}
|
|
55
|
+
if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
56
|
+
const typeName = node.typeName.name;
|
|
57
|
+
if (typeCache.has(typeName)) {
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
59
|
+
return typeCache.get(typeName);
|
|
60
|
+
}
|
|
61
|
+
// Prevent infinite recursion
|
|
62
|
+
typeCache.set(typeName, false);
|
|
63
|
+
const program = context.getSourceCode().ast;
|
|
64
|
+
const interfaceDecl = program.body.find((n) => n.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration &&
|
|
65
|
+
n.id.name === typeName);
|
|
66
|
+
if (interfaceDecl) {
|
|
67
|
+
const result = interfaceDecl.body.body.some((member) => {
|
|
68
|
+
if (member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
|
|
69
|
+
member.typeAnnotation) {
|
|
70
|
+
return hasInvalidType(member.typeAnnotation.typeAnnotation);
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
});
|
|
74
|
+
typeCache.set(typeName, result);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
case utils_1.AST_NODE_TYPES.TSIntersectionType:
|
|
80
|
+
case utils_1.AST_NODE_TYPES.TSUnionType:
|
|
81
|
+
return node.types.some(hasInvalidType);
|
|
82
|
+
case utils_1.AST_NODE_TYPES.TSTypeOperator:
|
|
83
|
+
if ('typeAnnotation' in node) {
|
|
84
|
+
return hasInvalidType(node.typeAnnotation);
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
case utils_1.AST_NODE_TYPES.TSMappedType:
|
|
88
|
+
if ('typeAnnotation' in node) {
|
|
89
|
+
return hasInvalidType(node.typeAnnotation);
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
case utils_1.AST_NODE_TYPES.TSIndexedAccessType:
|
|
93
|
+
return (hasInvalidType(node.objectType) || hasInvalidType(node.indexType));
|
|
94
|
+
case utils_1.AST_NODE_TYPES.TSConditionalType:
|
|
95
|
+
return (hasInvalidType(node.checkType) ||
|
|
96
|
+
hasInvalidType(node.extendsType) ||
|
|
97
|
+
hasInvalidType(node.trueType) ||
|
|
98
|
+
hasInvalidType(node.falseType));
|
|
99
|
+
case utils_1.AST_NODE_TYPES.TSArrayType:
|
|
100
|
+
return hasInvalidType(node.elementType);
|
|
101
|
+
case utils_1.AST_NODE_TYPES.TSTupleType:
|
|
102
|
+
return node.elementTypes.some(hasInvalidType);
|
|
103
|
+
case utils_1.AST_NODE_TYPES.TSTypeQuery:
|
|
104
|
+
return false;
|
|
105
|
+
default:
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
TSTypeReference(node) {
|
|
111
|
+
if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
112
|
+
node.typeName.name === 'DocumentReference') {
|
|
113
|
+
// Check if generic type argument is missing
|
|
114
|
+
if (!node.typeParameters || node.typeParameters.params.length === 0) {
|
|
115
|
+
context.report({
|
|
116
|
+
node,
|
|
117
|
+
messageId: 'missingGeneric',
|
|
118
|
+
});
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// Check for invalid generic type arguments (any or {}) recursively
|
|
122
|
+
const typeArg = node.typeParameters.params[0];
|
|
123
|
+
if (hasInvalidType(typeArg)) {
|
|
124
|
+
context.report({
|
|
125
|
+
node,
|
|
126
|
+
messageId: 'invalidGeneric',
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
//# sourceMappingURL=enforce-firestore-doc-ref-generic.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: {
|
|
@@ -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,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
|
};
|
|
@@ -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) {
|
|
@@ -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',
|
|
@@ -42,30 +42,39 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
ThrowStatement(node) {
|
|
45
|
+
if (!node.argument) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
45
48
|
const argument = node.argument;
|
|
46
|
-
if (argument
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
49
|
+
if (argument.type !== utils_1.AST_NODE_TYPES.NewExpression) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const callee = argument.callee;
|
|
53
|
+
// Check for direct Error usage
|
|
54
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
55
|
+
callee.name === 'Error') {
|
|
56
|
+
context.report({
|
|
57
|
+
node,
|
|
58
|
+
messageId: 'useHttpsError',
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Check for firebase-admin HttpsError usage
|
|
63
|
+
if (!hasFirebaseAdminImport) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const isHttpsError = callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
67
|
+
callee.name === 'HttpsError';
|
|
68
|
+
const isFirebaseHttpsError = callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
69
|
+
callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
70
|
+
callee.object.name === httpsIdentifier &&
|
|
71
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
72
|
+
callee.property.name === 'HttpsError';
|
|
73
|
+
if (isHttpsError || isFirebaseHttpsError) {
|
|
74
|
+
context.report({
|
|
75
|
+
node,
|
|
76
|
+
messageId: 'useProprietaryHttpsError',
|
|
77
|
+
});
|
|
69
78
|
}
|
|
70
79
|
},
|
|
71
80
|
};
|