@blumintinc/eslint-plugin-blumint 1.12.1 → 1.12.3
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 +7 -1
- package/lib/rules/enforce-boolean-naming-prefixes.js +41 -1
- package/lib/rules/enforce-firestore-facade.js +88 -6
- package/lib/rules/enforce-microdiff.js +24 -15
- package/lib/rules/enforce-positive-naming.js +34 -3
- package/lib/rules/no-explicit-return-type.js +3 -2
- package/lib/rules/no-hungarian.js +43 -6
- package/lib/rules/no-type-assertion-returns.js +15 -0
- package/lib/rules/no-undefined-null-passthrough.d.ts +2 -0
- package/lib/rules/no-undefined-null-passthrough.js +254 -0
- package/lib/rules/no-unused-props.js +31 -1
- package/lib/rules/prefer-block-comments-for-declarations.d.ts +6 -0
- package/lib/rules/prefer-block-comments-for-declarations.js +118 -0
- package/lib/rules/prefer-settings-object.js +38 -2
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +29 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -103,10 +103,12 @@ const enforce_dynamic_file_naming_1 = __importDefault(require("./rules/enforce-d
|
|
|
103
103
|
const prefer_usecallback_over_usememo_for_functions_1 = __importDefault(require("./rules/prefer-usecallback-over-usememo-for-functions"));
|
|
104
104
|
const no_margin_properties_1 = require("./rules/no-margin-properties");
|
|
105
105
|
const enforce_boolean_naming_prefixes_1 = require("./rules/enforce-boolean-naming-prefixes");
|
|
106
|
+
const prefer_block_comments_for_declarations_1 = require("./rules/prefer-block-comments-for-declarations");
|
|
107
|
+
const no_undefined_null_passthrough_1 = require("./rules/no-undefined-null-passthrough");
|
|
106
108
|
module.exports = {
|
|
107
109
|
meta: {
|
|
108
110
|
name: '@blumintinc/eslint-plugin-blumint',
|
|
109
|
-
version: '1.12.
|
|
111
|
+
version: '1.12.3',
|
|
110
112
|
},
|
|
111
113
|
parseOptions: {
|
|
112
114
|
ecmaVersion: 2020,
|
|
@@ -115,6 +117,7 @@ module.exports = {
|
|
|
115
117
|
recommended: {
|
|
116
118
|
plugins: ['@blumintinc/blumint'],
|
|
117
119
|
rules: {
|
|
120
|
+
'@blumintinc/blumint/prefer-block-comments-for-declarations': 'error',
|
|
118
121
|
'@blumintinc/blumint/key-only-outermost-element': 'error',
|
|
119
122
|
'@blumintinc/blumint/avoid-utils-directory': 'error',
|
|
120
123
|
'@blumintinc/blumint/enforce-firestore-path-utils': 'error',
|
|
@@ -221,10 +224,12 @@ module.exports = {
|
|
|
221
224
|
'@blumintinc/blumint/prefer-usecallback-over-usememo-for-functions': 'error',
|
|
222
225
|
'@blumintinc/blumint/no-margin-properties': 'error',
|
|
223
226
|
'@blumintinc/blumint/enforce-boolean-naming-prefixes': 'error',
|
|
227
|
+
'@blumintinc/blumint/no-undefined-null-passthrough': 'error',
|
|
224
228
|
},
|
|
225
229
|
},
|
|
226
230
|
},
|
|
227
231
|
rules: {
|
|
232
|
+
'prefer-block-comments-for-declarations': prefer_block_comments_for_declarations_1.preferBlockCommentsForDeclarations,
|
|
228
233
|
'key-only-outermost-element': key_only_outermost_element_1.keyOnlyOutermostElement,
|
|
229
234
|
'array-methods-this-context': array_methods_this_context_1.arrayMethodsThisContext,
|
|
230
235
|
'class-methods-read-top-to-bottom': class_methods_read_top_to_bottom_1.classMethodsReadTopToBottom,
|
|
@@ -325,6 +330,7 @@ module.exports = {
|
|
|
325
330
|
'prefer-usecallback-over-usememo-for-functions': prefer_usecallback_over_usememo_for_functions_1.default,
|
|
326
331
|
'no-margin-properties': no_margin_properties_1.noMarginProperties,
|
|
327
332
|
'enforce-boolean-naming-prefixes': enforce_boolean_naming_prefixes_1.enforceBooleanNamingPrefixes,
|
|
333
|
+
'no-undefined-null-passthrough': no_undefined_null_passthrough_1.noUndefinedNullPassthrough,
|
|
328
334
|
},
|
|
329
335
|
};
|
|
330
336
|
//# sourceMappingURL=index.js.map
|
|
@@ -20,6 +20,7 @@ const DEFAULT_BOOLEAN_PREFIXES = [
|
|
|
20
20
|
'supports',
|
|
21
21
|
'needs',
|
|
22
22
|
'asserts',
|
|
23
|
+
'includes',
|
|
23
24
|
];
|
|
24
25
|
exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
25
26
|
name: 'enforce-boolean-naming-prefixes',
|
|
@@ -119,9 +120,48 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
119
120
|
['===', '!==', '==', '!=', '>', '<', '>=', '<='].includes(node.init.operator)) {
|
|
120
121
|
return true;
|
|
121
122
|
}
|
|
122
|
-
// Check for logical expressions (
|
|
123
|
+
// Check for logical expressions (&&)
|
|
123
124
|
if (node.init.type === utils_1.AST_NODE_TYPES.LogicalExpression &&
|
|
124
125
|
node.init.operator === '&&') {
|
|
126
|
+
// Check if the right side is a method call that might return a non-boolean value
|
|
127
|
+
const rightSide = node.init.right;
|
|
128
|
+
if (rightSide.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
129
|
+
// If the method name doesn't suggest it returns a boolean, don't flag it
|
|
130
|
+
if (rightSide.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
131
|
+
rightSide.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
132
|
+
const methodName = rightSide.callee.property.name;
|
|
133
|
+
// Check if the method name suggests it returns a boolean
|
|
134
|
+
const isBooleanMethod = approvedPrefixes.some((prefix) => methodName.toLowerCase().startsWith(prefix.toLowerCase()));
|
|
135
|
+
// If the method name suggests it returns a boolean (starts with a boolean prefix or contains 'boolean' or 'enabled'),
|
|
136
|
+
// then the variable should be treated as a boolean
|
|
137
|
+
if (isBooleanMethod ||
|
|
138
|
+
methodName.toLowerCase().includes('boolean') ||
|
|
139
|
+
methodName.toLowerCase().includes('enabled') ||
|
|
140
|
+
methodName.toLowerCase().includes('auth') ||
|
|
141
|
+
methodName.toLowerCase().includes('valid') ||
|
|
142
|
+
methodName.toLowerCase().includes('check')) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
// For methods like getVolume(), getData(), etc., assume they return non-boolean values
|
|
146
|
+
if (methodName.toLowerCase().startsWith('get') ||
|
|
147
|
+
methodName.toLowerCase().startsWith('fetch') ||
|
|
148
|
+
methodName.toLowerCase().startsWith('retrieve') ||
|
|
149
|
+
methodName.toLowerCase().startsWith('load') ||
|
|
150
|
+
methodName.toLowerCase().startsWith('read')) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// For property access like user.isAuthenticated, treat as boolean
|
|
156
|
+
if (rightSide.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
157
|
+
rightSide.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
158
|
+
const propertyName = rightSide.property.name;
|
|
159
|
+
const isBooleanProperty = approvedPrefixes.some((prefix) => propertyName.toLowerCase().startsWith(prefix.toLowerCase()));
|
|
160
|
+
if (isBooleanProperty) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Default to true for other cases with && to avoid false negatives
|
|
125
165
|
return true;
|
|
126
166
|
}
|
|
127
167
|
// Special case for logical OR (||) - only consider it boolean if:
|
|
@@ -7,6 +7,68 @@ const FIRESTORE_METHODS = new Set(['get', 'set', 'update', 'delete']);
|
|
|
7
7
|
const isMemberExpression = (node) => {
|
|
8
8
|
return node.type === utils_1.AST_NODE_TYPES.MemberExpression;
|
|
9
9
|
};
|
|
10
|
+
// Track variables that are assigned realtimeDb references
|
|
11
|
+
const realtimeDbRefVariables = new Set();
|
|
12
|
+
const realtimeDbChildVariables = new Set();
|
|
13
|
+
const isRealtimeDbRefAssignment = (node) => {
|
|
14
|
+
if (node.type !== utils_1.AST_NODE_TYPES.VariableDeclarator)
|
|
15
|
+
return false;
|
|
16
|
+
const init = node.init;
|
|
17
|
+
if (!init)
|
|
18
|
+
return false;
|
|
19
|
+
// Check for direct realtimeDb.ref() assignments
|
|
20
|
+
// e.g., const ref = realtimeDb.ref(path);
|
|
21
|
+
if (init.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
22
|
+
isMemberExpression(init.callee) &&
|
|
23
|
+
isIdentifier(init.callee.property) &&
|
|
24
|
+
init.callee.property.name === 'ref' &&
|
|
25
|
+
isIdentifier(init.callee.object) &&
|
|
26
|
+
(init.callee.object.name === 'realtimeDb' || init.callee.object.name.includes('realtimeDb'))) {
|
|
27
|
+
if (isIdentifier(node.id)) {
|
|
28
|
+
realtimeDbRefVariables.add(node.id.name);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Check for child() method calls on realtimeDb refs
|
|
33
|
+
// e.g., const childRef = parentRef.child('path');
|
|
34
|
+
if (init.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
35
|
+
isMemberExpression(init.callee) &&
|
|
36
|
+
isIdentifier(init.callee.property) &&
|
|
37
|
+
init.callee.property.name === 'child' &&
|
|
38
|
+
isIdentifier(init.callee.object) &&
|
|
39
|
+
realtimeDbRefVariables.has(init.callee.object.name)) {
|
|
40
|
+
if (isIdentifier(node.id)) {
|
|
41
|
+
realtimeDbChildVariables.add(node.id.name);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
};
|
|
47
|
+
const isRealtimeDbReference = (node) => {
|
|
48
|
+
// Check if it's a direct realtimeDb.ref() call
|
|
49
|
+
if (node.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
50
|
+
isMemberExpression(node.callee) &&
|
|
51
|
+
isIdentifier(node.callee.property) &&
|
|
52
|
+
node.callee.property.name === 'ref' &&
|
|
53
|
+
isIdentifier(node.callee.object) &&
|
|
54
|
+
(node.callee.object.name === 'realtimeDb' || node.callee.object.name.includes('realtimeDb'))) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
// Check if it's a variable that holds a realtimeDb reference
|
|
58
|
+
if (isIdentifier(node)) {
|
|
59
|
+
return realtimeDbRefVariables.has(node.name) || realtimeDbChildVariables.has(node.name);
|
|
60
|
+
}
|
|
61
|
+
// Check if it's a child() call on a realtimeDb reference
|
|
62
|
+
if (node.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
63
|
+
isMemberExpression(node.callee) &&
|
|
64
|
+
isIdentifier(node.callee.property) &&
|
|
65
|
+
node.callee.property.name === 'child' &&
|
|
66
|
+
isIdentifier(node.callee.object) &&
|
|
67
|
+
(realtimeDbRefVariables.has(node.callee.object.name) || realtimeDbChildVariables.has(node.callee.object.name))) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
};
|
|
10
72
|
const isFirestoreMethodCall = (node) => {
|
|
11
73
|
if (!isMemberExpression(node.callee))
|
|
12
74
|
return false;
|
|
@@ -24,11 +86,19 @@ const isFirestoreMethodCall = (node) => {
|
|
|
24
86
|
name.includes('Tx')) {
|
|
25
87
|
return false;
|
|
26
88
|
}
|
|
89
|
+
// Skip if it's a realtimeDb reference variable
|
|
90
|
+
if (realtimeDbRefVariables.has(name) || realtimeDbChildVariables.has(name)) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
27
93
|
// Check for batch or transaction
|
|
28
94
|
if (/batch|transaction/i.test(name)) {
|
|
29
95
|
return true;
|
|
30
96
|
}
|
|
31
97
|
}
|
|
98
|
+
// Check if the method is called on a realtimeDb reference
|
|
99
|
+
if (isRealtimeDbReference(object)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
32
102
|
// Handle type assertions (as in the bug report)
|
|
33
103
|
if (object.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
34
104
|
return true;
|
|
@@ -41,10 +111,12 @@ const isFirestoreMethodCall = (node) => {
|
|
|
41
111
|
const callee = current.callee;
|
|
42
112
|
if (isMemberExpression(callee)) {
|
|
43
113
|
const property = callee.property;
|
|
44
|
-
if (isIdentifier(property)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
114
|
+
if (isIdentifier(property)) {
|
|
115
|
+
// Check for Firestore methods
|
|
116
|
+
if (property.name === 'doc' || property.name === 'collection') {
|
|
117
|
+
foundDocOrCollection = true;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
48
120
|
}
|
|
49
121
|
}
|
|
50
122
|
}
|
|
@@ -63,8 +135,11 @@ const isFirestoreMethodCall = (node) => {
|
|
|
63
135
|
if (!foundDocOrCollection && isIdentifier(object)) {
|
|
64
136
|
const name = object.name;
|
|
65
137
|
// If the variable name contains 'doc' or 'ref', it's likely a Firestore reference
|
|
66
|
-
|
|
67
|
-
|
|
138
|
+
// But exclude realtimeDb references
|
|
139
|
+
if ((name.toLowerCase().includes('doc') || name.toLowerCase().includes('ref')) &&
|
|
140
|
+
!name.includes('realtimeDb') &&
|
|
141
|
+
!realtimeDbRefVariables.has(name) &&
|
|
142
|
+
!realtimeDbChildVariables.has(name)) {
|
|
68
143
|
return true;
|
|
69
144
|
}
|
|
70
145
|
}
|
|
@@ -94,7 +169,14 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
|
94
169
|
},
|
|
95
170
|
defaultOptions: [],
|
|
96
171
|
create(context) {
|
|
172
|
+
// Clear the sets at the beginning of each file analysis
|
|
173
|
+
realtimeDbRefVariables.clear();
|
|
174
|
+
realtimeDbChildVariables.clear();
|
|
97
175
|
return {
|
|
176
|
+
// Track variable declarations that are assigned realtimeDb references
|
|
177
|
+
VariableDeclarator(node) {
|
|
178
|
+
isRealtimeDbRefAssignment(node);
|
|
179
|
+
},
|
|
98
180
|
CallExpression(node) {
|
|
99
181
|
if (!isFirestoreMethodCall(node))
|
|
100
182
|
return;
|
|
@@ -71,8 +71,8 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
71
71
|
'fast-diff',
|
|
72
72
|
'diff',
|
|
73
73
|
'deep-object-diff',
|
|
74
|
-
'fast-deep-equal'
|
|
75
|
-
|
|
74
|
+
// Removed 'fast-deep-equal' and 'fast-deep-equal/es6' from this list
|
|
75
|
+
// as they are allowed alternatives to microdiff
|
|
76
76
|
].includes(importSource)) {
|
|
77
77
|
// Track imported function names and their sources
|
|
78
78
|
node.specifiers.forEach((specifier) => {
|
|
@@ -84,13 +84,7 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
84
84
|
const localName = specifier.local.name;
|
|
85
85
|
importedFunctions.set(localName, importSource);
|
|
86
86
|
}
|
|
87
|
-
|
|
88
|
-
importSource === 'fast-deep-equal/es6') {
|
|
89
|
-
// Handle default imports for fast-deep-equal
|
|
90
|
-
if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
|
|
91
|
-
importedFunctions.set(specifier.local.name, importSource);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
87
|
+
// Removed the fast-deep-equal handling since it's now allowed
|
|
94
88
|
});
|
|
95
89
|
// Report all competing diffing libraries right away
|
|
96
90
|
context.report({
|
|
@@ -117,12 +111,22 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
117
111
|
}
|
|
118
112
|
return false;
|
|
119
113
|
});
|
|
120
|
-
if (hasDiffImport
|
|
121
|
-
importSource === 'fast-deep-equal' ||
|
|
122
|
-
importSource === 'fast-deep-equal/es6') {
|
|
114
|
+
if (hasDiffImport) {
|
|
123
115
|
importedDiffLibraries.set(importSource, node);
|
|
124
116
|
}
|
|
125
117
|
}
|
|
118
|
+
// Special handling for fast-deep-equal: track it but don't report it
|
|
119
|
+
if (importSource === 'fast-deep-equal' ||
|
|
120
|
+
importSource === 'fast-deep-equal/es6') {
|
|
121
|
+
// Track imported function names for later reference
|
|
122
|
+
node.specifiers.forEach((specifier) => {
|
|
123
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
|
|
124
|
+
importedFunctions.set(specifier.local.name, importSource);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
// Add to importedDiffLibraries for tracking but don't report
|
|
128
|
+
importedDiffLibraries.set(importSource, node);
|
|
129
|
+
}
|
|
126
130
|
},
|
|
127
131
|
// Check for usage of other diffing libraries
|
|
128
132
|
CallExpression(node) {
|
|
@@ -137,7 +141,13 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
137
141
|
// Check if this is a function we specifically imported from a diff library
|
|
138
142
|
if (importedFunctions.has(name)) {
|
|
139
143
|
usedImportNames.add(name);
|
|
140
|
-
//
|
|
144
|
+
// Get the source of the imported function
|
|
145
|
+
const importSource = importedFunctions.get(name);
|
|
146
|
+
// Skip reporting if it's from fast-deep-equal
|
|
147
|
+
if (importSource === 'fast-deep-equal' || importSource === 'fast-deep-equal/es6') {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Report it if it's from any other tracked library
|
|
141
151
|
reportedNodes.add(node);
|
|
142
152
|
context.report({
|
|
143
153
|
node,
|
|
@@ -154,8 +164,7 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
154
164
|
'fastDiff',
|
|
155
165
|
'diffArrays',
|
|
156
166
|
'detailedDiff',
|
|
157
|
-
'fastDeepEqual'
|
|
158
|
-
'isEqual',
|
|
167
|
+
// Removed 'fastDeepEqual' and 'isEqual' as they are allowed alternatives
|
|
159
168
|
].includes(name);
|
|
160
169
|
if (isDiffFunction) {
|
|
161
170
|
// Track this import name as used
|
|
@@ -824,6 +824,26 @@ const DIS_EXCEPTIONS = [
|
|
|
824
824
|
'disaster',
|
|
825
825
|
'disasters',
|
|
826
826
|
'disastrous',
|
|
827
|
+
// Added common technical terms containing 'dis' that are not negative
|
|
828
|
+
'display',
|
|
829
|
+
'displayed',
|
|
830
|
+
'displaying',
|
|
831
|
+
'displays',
|
|
832
|
+
'dispatch',
|
|
833
|
+
'dispatched',
|
|
834
|
+
'dispatches',
|
|
835
|
+
'dispatching',
|
|
836
|
+
'discover',
|
|
837
|
+
'discovered',
|
|
838
|
+
'discovering',
|
|
839
|
+
'discovers',
|
|
840
|
+
'discovery',
|
|
841
|
+
'dismiss',
|
|
842
|
+
'dismissed',
|
|
843
|
+
'dismissing',
|
|
844
|
+
'dismissal',
|
|
845
|
+
'disk',
|
|
846
|
+
'disks',
|
|
827
847
|
];
|
|
828
848
|
// Words that contain negative prefixes but should be treated as valid
|
|
829
849
|
const EXCEPTION_WORDS = [
|
|
@@ -920,6 +940,10 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
920
940
|
* Check if a name has boolean negative naming
|
|
921
941
|
*/
|
|
922
942
|
function hasBooleanNegativeNaming(name) {
|
|
943
|
+
// Safety check: if name is undefined or null, return not negative
|
|
944
|
+
if (!name) {
|
|
945
|
+
return { isNegative: false, alternatives: [] };
|
|
946
|
+
}
|
|
923
947
|
// Check for exact matches in our alternatives map first
|
|
924
948
|
if (BOOLEAN_POSITIVE_ALTERNATIVES[name]) {
|
|
925
949
|
return {
|
|
@@ -930,9 +954,12 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
930
954
|
// Split the name into words
|
|
931
955
|
const words = splitNameIntoWords(name);
|
|
932
956
|
// Check if this follows the pattern IS_NOT_SOMETHING or HAS_NO_SOMETHING
|
|
933
|
-
|
|
934
|
-
if (
|
|
935
|
-
|
|
957
|
+
// Make sure words[1] exists before trying to access it
|
|
958
|
+
if (words.length > 1) {
|
|
959
|
+
const secondWord = words[1].toLowerCase();
|
|
960
|
+
if (EXCEPTION_WORDS.some((exception) => secondWord === exception.toLowerCase())) {
|
|
961
|
+
return { isNegative: false, alternatives: [] };
|
|
962
|
+
}
|
|
936
963
|
}
|
|
937
964
|
const nameLowercase = name.toLowerCase();
|
|
938
965
|
// Check for negative prefixes in boolean-like variables
|
|
@@ -1193,6 +1220,7 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
1193
1220
|
* Check TSPropertySignature for negative naming (in interfaces)
|
|
1194
1221
|
*/
|
|
1195
1222
|
function checkPropertySignature(node) {
|
|
1223
|
+
// Skip non-identifier keys (like computed properties)
|
|
1196
1224
|
if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
1197
1225
|
return;
|
|
1198
1226
|
// Only check boolean properties
|
|
@@ -1200,7 +1228,10 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
1200
1228
|
!(node.typeAnnotation?.typeAnnotation.type ===
|
|
1201
1229
|
utils_1.AST_NODE_TYPES.TSBooleanKeyword))
|
|
1202
1230
|
return;
|
|
1231
|
+
// Ensure we have a valid property name
|
|
1203
1232
|
const propertyName = node.key.name;
|
|
1233
|
+
if (!propertyName)
|
|
1234
|
+
return;
|
|
1204
1235
|
const { isNegative, alternatives } = hasBooleanNegativeNaming(propertyName);
|
|
1205
1236
|
if (isNegative) {
|
|
1206
1237
|
context.report({
|
|
@@ -210,8 +210,9 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
210
210
|
MethodDefinition(node) {
|
|
211
211
|
if (!node.value.returnType)
|
|
212
212
|
return;
|
|
213
|
-
if (
|
|
214
|
-
|
|
213
|
+
if (isTypeGuardFunction(node.value) ||
|
|
214
|
+
(mergedOptions.allowAbstractMethodSignatures &&
|
|
215
|
+
isInterfaceOrAbstractMethodSignature(node))) {
|
|
215
216
|
return;
|
|
216
217
|
}
|
|
217
218
|
context.report({
|
|
@@ -45,6 +45,12 @@ const ALLOWED_SUFFIXES = [
|
|
|
45
45
|
'Display',
|
|
46
46
|
'Displayed',
|
|
47
47
|
];
|
|
48
|
+
// Common compound nouns that should not be flagged as Hungarian notation
|
|
49
|
+
const ALLOWED_COMPOUND_NOUNS = [
|
|
50
|
+
'PhoneNumber',
|
|
51
|
+
'EmailAddress',
|
|
52
|
+
'PostalCode',
|
|
53
|
+
];
|
|
48
54
|
// Common built-in JavaScript prototype methods
|
|
49
55
|
const BUILT_IN_METHODS = new Set([
|
|
50
56
|
// String methods
|
|
@@ -208,6 +214,25 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
208
214
|
const checkedIdentifiers = new Set();
|
|
209
215
|
// Check if a variable name contains a type marker with proper word boundaries
|
|
210
216
|
function hasTypeMarker(variableName) {
|
|
217
|
+
// Check if the variable name is exactly one of the allowed compound nouns
|
|
218
|
+
// or if it contains one of the allowed compound nouns but is not a prefix like "strPhoneNumber"
|
|
219
|
+
for (const compoundNoun of ALLOWED_COMPOUND_NOUNS) {
|
|
220
|
+
// If the variable name is exactly the compound noun (case-insensitive)
|
|
221
|
+
if (variableName.toLowerCase() === compoundNoun.toLowerCase()) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
// If the variable name contains the compound noun
|
|
225
|
+
if (variableName.includes(compoundNoun)) {
|
|
226
|
+
// Check if it's a prefix like "strPhoneNumber" (which should be flagged)
|
|
227
|
+
const prefix = variableName.substring(0, variableName.indexOf(compoundNoun));
|
|
228
|
+
if (TYPE_MARKERS.some(marker => prefix.toLowerCase() === marker.toLowerCase())) {
|
|
229
|
+
// This is a type marker prefix + compound noun, so it should be flagged
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
// Otherwise, it's a valid use of the compound noun
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
211
236
|
// Check if the variable name ends with one of the allowed descriptive suffixes
|
|
212
237
|
if (ALLOWED_SUFFIXES.some((suffix) => variableName.endsWith(suffix) &&
|
|
213
238
|
variableName.length > suffix.length &&
|
|
@@ -216,8 +241,12 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
216
241
|
}
|
|
217
242
|
const normalizedVarName = variableName.toLowerCase();
|
|
218
243
|
// Handle SCREAMING_SNAKE_CASE separately
|
|
219
|
-
if (variableName === variableName.toUpperCase()
|
|
220
|
-
|
|
244
|
+
if (variableName === variableName.toUpperCase()) {
|
|
245
|
+
// Special case for all-caps variables without underscores (like BREAKPOINTS)
|
|
246
|
+
// These should not be flagged as Hungarian notation
|
|
247
|
+
if (!variableName.includes('_')) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
221
250
|
return TYPE_MARKERS.some((marker) => {
|
|
222
251
|
const markerUpper = marker.toUpperCase();
|
|
223
252
|
// Check if it's a prefix (PREFIX_REST)
|
|
@@ -232,6 +261,7 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
232
261
|
}
|
|
233
262
|
// Check if it's in the middle (PART_MARKER_PART)
|
|
234
263
|
const parts = variableName.split('_');
|
|
264
|
+
// Only consider exact matches for parts, not substrings
|
|
235
265
|
return parts.some((part) => part === markerUpper);
|
|
236
266
|
});
|
|
237
267
|
}
|
|
@@ -254,6 +284,8 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
254
284
|
/[A-Z0-9]/.test(variableName[variableName.length - normalizedMarker.length - 1])) {
|
|
255
285
|
return true;
|
|
256
286
|
}
|
|
287
|
+
// Check for word boundaries to avoid matching substrings
|
|
288
|
+
// For example, avoid matching "int" in "points" or "str" in "stream"
|
|
257
289
|
const markerIndex = normalizedVarName.indexOf(normalizedMarker);
|
|
258
290
|
if (markerIndex === -1) {
|
|
259
291
|
return false;
|
|
@@ -261,10 +293,15 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
261
293
|
const markerPrefix = variableName.at(markerIndex);
|
|
262
294
|
const preMarkerPrefix = variableName.at(markerIndex - 1);
|
|
263
295
|
const suffix = variableName.at(markerIndex + normalizedMarker.length);
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
296
|
+
// Ensure we have proper word boundaries
|
|
297
|
+
// A word boundary is defined by:
|
|
298
|
+
// 1. Start of string OR underscore OR capital letter before the marker
|
|
299
|
+
// 2. End of string OR underscore OR capital letter after the marker
|
|
300
|
+
const hasStartBoundary = markerIndex === 0 || preMarkerPrefix === '_' || /[A-Z]/.test(markerPrefix || '');
|
|
301
|
+
const hasEndBoundary = markerIndex + normalizedMarker.length === normalizedVarName.length ||
|
|
302
|
+
suffix === '_' ||
|
|
303
|
+
/[A-Z]/.test(suffix || '');
|
|
304
|
+
return hasStartBoundary && hasEndBoundary;
|
|
268
305
|
});
|
|
269
306
|
}
|
|
270
307
|
// Check if the identifier is a built-in method or imported from an external module
|
|
@@ -193,12 +193,23 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
|
|
|
193
193
|
}
|
|
194
194
|
return false;
|
|
195
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Check if the current file is a .f.ts file
|
|
198
|
+
*/
|
|
199
|
+
function isFunctionFile(context) {
|
|
200
|
+
const filename = context.getFilename();
|
|
201
|
+
return filename.endsWith('.f.ts');
|
|
202
|
+
}
|
|
196
203
|
/**
|
|
197
204
|
* Common function to check function return types
|
|
198
205
|
*/
|
|
199
206
|
function checkFunctionReturnType(node) {
|
|
200
207
|
if (!node.returnType)
|
|
201
208
|
return;
|
|
209
|
+
// Skip checking return types in .f.ts files
|
|
210
|
+
if (isFunctionFile(context)) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
202
213
|
// Allow type predicates if configured
|
|
203
214
|
if (mergedOptions.allowTypePredicates &&
|
|
204
215
|
isTypePredicate(node.returnType)) {
|
|
@@ -283,6 +294,10 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
|
|
|
283
294
|
if (node.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
284
295
|
// Check for explicit return type
|
|
285
296
|
if (node.returnType) {
|
|
297
|
+
// Skip checking return types in .f.ts files
|
|
298
|
+
if (isFunctionFile(context)) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
286
301
|
// Allow type predicates if configured
|
|
287
302
|
if (mergedOptions.allowTypePredicates &&
|
|
288
303
|
isTypePredicate(node.returnType)) {
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noUndefinedNullPassthrough = void 0;
|
|
4
|
+
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
exports.noUndefinedNullPassthrough = (0, createRule_1.createRule)({
|
|
6
|
+
create(context) {
|
|
7
|
+
return {
|
|
8
|
+
FunctionDeclaration(node) {
|
|
9
|
+
// Only apply to functions with exactly one parameter
|
|
10
|
+
if (node.params.length !== 1) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
// Skip React hooks (functions starting with 'use')
|
|
14
|
+
if (node.id &&
|
|
15
|
+
node.id.type === 'Identifier' &&
|
|
16
|
+
node.id.name.startsWith('use')) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
checkFunctionBody(node.body, node.params[0], context);
|
|
20
|
+
},
|
|
21
|
+
ArrowFunctionExpression(node) {
|
|
22
|
+
// Only apply to arrow functions with exactly one parameter
|
|
23
|
+
if (node.params.length !== 1) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Skip if the function is part of a variable declaration that starts with 'use' (React hook)
|
|
27
|
+
const parent = node.parent;
|
|
28
|
+
if (parent &&
|
|
29
|
+
parent.type === 'VariableDeclarator' &&
|
|
30
|
+
parent.id.type === 'Identifier' &&
|
|
31
|
+
parent.id.name.startsWith('use')) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// For arrow functions with block body
|
|
35
|
+
if (node.body.type === 'BlockStatement') {
|
|
36
|
+
checkFunctionBody(node.body, node.params[0], context);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// For arrow functions with expression body (implicit return)
|
|
40
|
+
checkImplicitReturn(node, context);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
FunctionExpression(node) {
|
|
44
|
+
// Only apply to functions with exactly one parameter
|
|
45
|
+
if (node.params.length !== 1) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Skip if the function is part of a variable declaration that starts with 'use' (React hook)
|
|
49
|
+
const parent = node.parent;
|
|
50
|
+
if (parent &&
|
|
51
|
+
parent.type === 'VariableDeclarator' &&
|
|
52
|
+
parent.id.type === 'Identifier' &&
|
|
53
|
+
parent.id.name.startsWith('use')) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
checkFunctionBody(node.body, node.params[0], context);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
name: 'no-undefined-null-passthrough',
|
|
61
|
+
meta: {
|
|
62
|
+
type: 'suggestion',
|
|
63
|
+
docs: {
|
|
64
|
+
description: 'Avoid functions that return undefined or null when their single argument is undefined or null',
|
|
65
|
+
recommended: 'error',
|
|
66
|
+
},
|
|
67
|
+
schema: [],
|
|
68
|
+
messages: {
|
|
69
|
+
unexpected: 'Avoid functions that return undefined or null when their single argument is undefined or null. Move the null/undefined check to the caller instead.',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
defaultOptions: [],
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Check function body for early returns when parameter is null/undefined
|
|
76
|
+
*/
|
|
77
|
+
function checkFunctionBody(body, param, context) {
|
|
78
|
+
// Get the parameter name
|
|
79
|
+
let paramName = null;
|
|
80
|
+
if (param.type === 'Identifier') {
|
|
81
|
+
paramName = param.name;
|
|
82
|
+
}
|
|
83
|
+
else if (param.type === 'AssignmentPattern' &&
|
|
84
|
+
param.left.type === 'Identifier') {
|
|
85
|
+
paramName = param.left.name;
|
|
86
|
+
}
|
|
87
|
+
if (!paramName)
|
|
88
|
+
return;
|
|
89
|
+
// Look for early returns based on parameter being null/undefined
|
|
90
|
+
for (const statement of body.body) {
|
|
91
|
+
if (statement.type === 'IfStatement') {
|
|
92
|
+
const test = statement.test;
|
|
93
|
+
// Check for patterns like: if (!param) return;
|
|
94
|
+
// or if (param === null) return;
|
|
95
|
+
// or if (param === undefined) return;
|
|
96
|
+
if (isNullUndefinedCheck(test, paramName)) {
|
|
97
|
+
// Check if the consequent is a block statement with a return
|
|
98
|
+
if (statement.consequent.type === 'BlockStatement') {
|
|
99
|
+
for (const consequentStmt of statement.consequent.body) {
|
|
100
|
+
if (consequentStmt.type === 'ReturnStatement' &&
|
|
101
|
+
(!consequentStmt.argument ||
|
|
102
|
+
isNullOrUndefinedLiteral(consequentStmt.argument))) {
|
|
103
|
+
// Check if there's a transformation in the function
|
|
104
|
+
const hasTransformation = checkForTransformation(body, paramName);
|
|
105
|
+
if (!hasTransformation) {
|
|
106
|
+
context.report({
|
|
107
|
+
node: statement,
|
|
108
|
+
messageId: 'unexpected',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Check if the consequent is a direct return statement
|
|
116
|
+
else if (statement.consequent.type === 'ReturnStatement' &&
|
|
117
|
+
(!statement.consequent.argument ||
|
|
118
|
+
isNullOrUndefinedLiteral(statement.consequent.argument))) {
|
|
119
|
+
// Check if there's a transformation in the function
|
|
120
|
+
const hasTransformation = checkForTransformation(body, paramName);
|
|
121
|
+
if (!hasTransformation) {
|
|
122
|
+
context.report({
|
|
123
|
+
node: statement,
|
|
124
|
+
messageId: 'unexpected',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Check if the function body contains a transformation of the parameter
|
|
135
|
+
*/
|
|
136
|
+
function checkForTransformation(body, paramName) {
|
|
137
|
+
if (!paramName)
|
|
138
|
+
return false;
|
|
139
|
+
// Look for return statements that call functions with the parameter
|
|
140
|
+
for (const statement of body.body) {
|
|
141
|
+
if (statement.type === 'ReturnStatement' && statement.argument) {
|
|
142
|
+
// Check for return transformData(data) pattern
|
|
143
|
+
if (statement.argument.type === 'CallExpression' &&
|
|
144
|
+
statement.argument.arguments.some(arg => arg.type === 'Identifier' && arg.name === paramName)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Check arrow functions with expression bodies (implicit returns)
|
|
153
|
+
*/
|
|
154
|
+
function checkImplicitReturn(node, context) {
|
|
155
|
+
// Get the parameter name
|
|
156
|
+
let paramName = null;
|
|
157
|
+
if (node.params[0].type === 'Identifier') {
|
|
158
|
+
paramName = node.params[0].name;
|
|
159
|
+
}
|
|
160
|
+
else if (node.params[0].type === 'AssignmentPattern' &&
|
|
161
|
+
node.params[0].left.type === 'Identifier') {
|
|
162
|
+
paramName = node.params[0].left.name;
|
|
163
|
+
}
|
|
164
|
+
if (!paramName)
|
|
165
|
+
return;
|
|
166
|
+
// Check for patterns like: (param) => param ? param.value : null
|
|
167
|
+
if (node.body.type === 'ConditionalExpression') {
|
|
168
|
+
const test = node.body.test;
|
|
169
|
+
if (isParameterReference(test, paramName) &&
|
|
170
|
+
isNullOrUndefinedLiteral(node.body.alternate)) {
|
|
171
|
+
context.report({
|
|
172
|
+
node,
|
|
173
|
+
messageId: 'unexpected',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else if (node.body.type === 'LogicalExpression') {
|
|
178
|
+
// Check for (param) => param && doSomething(param)
|
|
179
|
+
if (node.body.operator === '&&' &&
|
|
180
|
+
isParameterReference(node.body.left, paramName)) {
|
|
181
|
+
context.report({
|
|
182
|
+
node,
|
|
183
|
+
messageId: 'unexpected',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else if (node.body.type === 'Identifier' && node.body.name === paramName) {
|
|
188
|
+
// Check for (param) => param
|
|
189
|
+
context.report({
|
|
190
|
+
node,
|
|
191
|
+
messageId: 'unexpected',
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Check if an expression is testing if a parameter is null or undefined
|
|
197
|
+
*/
|
|
198
|
+
function isNullUndefinedCheck(node, paramName) {
|
|
199
|
+
// Check for !param
|
|
200
|
+
if (node.type === 'UnaryExpression' &&
|
|
201
|
+
node.operator === '!' &&
|
|
202
|
+
node.argument.type === 'Identifier' &&
|
|
203
|
+
node.argument.name === paramName) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
// Check for param === null, param === undefined, etc.
|
|
207
|
+
if (node.type === 'BinaryExpression' &&
|
|
208
|
+
(node.operator === '===' || node.operator === '==' ||
|
|
209
|
+
node.operator === '!==' || node.operator === '!=')) {
|
|
210
|
+
const left = node.left;
|
|
211
|
+
const right = node.right;
|
|
212
|
+
// param === null/undefined
|
|
213
|
+
if (left.type === 'Identifier' &&
|
|
214
|
+
left.name === paramName &&
|
|
215
|
+
isNullOrUndefinedLiteral(right)) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
// null/undefined === param
|
|
219
|
+
if (right.type === 'Identifier' &&
|
|
220
|
+
right.name === paramName &&
|
|
221
|
+
left.type !== 'PrivateIdentifier' && // Ensure left is not a PrivateIdentifier
|
|
222
|
+
isNullOrUndefinedLiteral(left)) {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Check for param === null || param === undefined
|
|
227
|
+
if (node.type === 'LogicalExpression' &&
|
|
228
|
+
node.operator === '||') {
|
|
229
|
+
return (isNullUndefinedCheck(node.left, paramName) ||
|
|
230
|
+
isNullUndefinedCheck(node.right, paramName));
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Check if a node is a null or undefined literal
|
|
236
|
+
*/
|
|
237
|
+
function isNullOrUndefinedLiteral(node) {
|
|
238
|
+
if (node.type === 'Literal' && node.value === null) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
if (node.type === 'Identifier' && node.name === 'undefined') {
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Check if a node is a reference to the parameter
|
|
248
|
+
*/
|
|
249
|
+
function isParameterReference(node, paramName) {
|
|
250
|
+
if (!paramName)
|
|
251
|
+
return false;
|
|
252
|
+
return node.type === 'Identifier' && node.name === paramName;
|
|
253
|
+
}
|
|
254
|
+
//# sourceMappingURL=no-undefined-null-passthrough.js.map
|
|
@@ -111,6 +111,14 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
111
111
|
}
|
|
112
112
|
else if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
113
113
|
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
|
+
// Skip checking for utility type parameters (T, K, etc.) as they're not actual props
|
|
117
|
+
if (typeNode.typeName.name.length === 1 && /^[A-Z]$/.test(typeNode.typeName.name)) {
|
|
118
|
+
// This is likely a generic type parameter (T, K, etc.), not a real type
|
|
119
|
+
// Skip it to avoid false positives
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
114
122
|
if (typeNode.typeName.name === 'Pick' &&
|
|
115
123
|
typeNode.typeParameters) {
|
|
116
124
|
// Handle Pick utility type
|
|
@@ -149,6 +157,23 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
149
157
|
}
|
|
150
158
|
}
|
|
151
159
|
}
|
|
160
|
+
else if (
|
|
161
|
+
// Handle other utility types like Required, Partial, etc.
|
|
162
|
+
utilityTypes.includes(typeNode.typeName.name) &&
|
|
163
|
+
typeNode.typeParameters) {
|
|
164
|
+
// For utility types like Required<T, K>, we need to handle the base type
|
|
165
|
+
const baseType = typeNode.typeParameters.params[0];
|
|
166
|
+
if (baseType.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
167
|
+
baseType.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
168
|
+
// Mark the base type as used via the utility type
|
|
169
|
+
const baseTypeName = baseType.typeName.name;
|
|
170
|
+
props[`...${baseTypeName}`] = baseType.typeName;
|
|
171
|
+
// For utility types, we need to track individual properties that might be used
|
|
172
|
+
if (!spreadTypeProps[baseTypeName]) {
|
|
173
|
+
spreadTypeProps[baseTypeName] = [];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
152
177
|
else {
|
|
153
178
|
// For referenced types like FormControlLabelProps, we need to track that these props should be forwarded
|
|
154
179
|
const spreadTypeName = typeNode.typeName.name;
|
|
@@ -234,7 +259,12 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
234
259
|
// 2. It's a property from a spread type and any property from that spread type is used, OR
|
|
235
260
|
// 3. It's a spread type and any of its properties are used in the component
|
|
236
261
|
let shouldReport = true;
|
|
237
|
-
|
|
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) {
|
|
238
268
|
shouldReport = false;
|
|
239
269
|
}
|
|
240
270
|
else if (prop.startsWith('...')) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
/**
|
|
3
|
+
* Rule to enforce the use of block comments (/** *\/) instead of single-line comments (//)
|
|
4
|
+
* for all declarations, including type declarations, variable declarations, and function declarations.
|
|
5
|
+
*/
|
|
6
|
+
export declare const preferBlockCommentsForDeclarations: TSESLint.RuleModule<'preferBlockComment', never[]>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.preferBlockCommentsForDeclarations = void 0;
|
|
4
|
+
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
/**
|
|
6
|
+
* Rule to enforce the use of block comments (/** *\/) instead of single-line comments (//)
|
|
7
|
+
* for all declarations, including type declarations, variable declarations, and function declarations.
|
|
8
|
+
*/
|
|
9
|
+
exports.preferBlockCommentsForDeclarations = (0, createRule_1.createRule)({
|
|
10
|
+
create(context) {
|
|
11
|
+
/**
|
|
12
|
+
* Check if a comment is a line comment that should be converted to a block comment
|
|
13
|
+
*/
|
|
14
|
+
const isLineCommentBeforeDeclaration = (comment, node) => {
|
|
15
|
+
// Only process line comments
|
|
16
|
+
if (comment.type !== 'Line') {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
// Check if the comment is directly before the node
|
|
20
|
+
const commentLine = comment.loc.end.line;
|
|
21
|
+
const nodeLine = node.loc.start.line;
|
|
22
|
+
return commentLine === nodeLine - 1;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Check if a node is inside a function body
|
|
26
|
+
*/
|
|
27
|
+
const isInsideFunctionBody = (node) => {
|
|
28
|
+
let parent = node.parent;
|
|
29
|
+
while (parent) {
|
|
30
|
+
if (parent.type === 'BlockStatement' &&
|
|
31
|
+
(parent.parent?.type === 'FunctionDeclaration' ||
|
|
32
|
+
parent.parent?.type === 'FunctionExpression' ||
|
|
33
|
+
parent.parent?.type === 'ArrowFunctionExpression' ||
|
|
34
|
+
parent.parent?.type === 'MethodDefinition')) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
parent = parent.parent;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Process a node that might have a declaration comment
|
|
43
|
+
*/
|
|
44
|
+
const checkNodeForLineComments = (node) => {
|
|
45
|
+
// Skip nodes inside function bodies
|
|
46
|
+
if (isInsideFunctionBody(node)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const sourceCode = context.getSourceCode();
|
|
50
|
+
const comments = sourceCode.getCommentsBefore(node);
|
|
51
|
+
// Find the closest comment to the node
|
|
52
|
+
const lastComment = comments[comments.length - 1];
|
|
53
|
+
if (lastComment && isLineCommentBeforeDeclaration(lastComment, node)) {
|
|
54
|
+
context.report({
|
|
55
|
+
loc: lastComment.loc,
|
|
56
|
+
messageId: 'preferBlockComment',
|
|
57
|
+
fix: (fixer) => {
|
|
58
|
+
const commentText = lastComment.value.trim();
|
|
59
|
+
return fixer.replaceText(lastComment, `/** ${commentText} */`);
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
// Check function declarations
|
|
66
|
+
FunctionDeclaration(node) {
|
|
67
|
+
checkNodeForLineComments(node);
|
|
68
|
+
},
|
|
69
|
+
// Check variable declarations
|
|
70
|
+
VariableDeclaration(node) {
|
|
71
|
+
checkNodeForLineComments(node);
|
|
72
|
+
},
|
|
73
|
+
// Check type declarations
|
|
74
|
+
TSTypeAliasDeclaration(node) {
|
|
75
|
+
checkNodeForLineComments(node);
|
|
76
|
+
},
|
|
77
|
+
// Check interface declarations
|
|
78
|
+
TSInterfaceDeclaration(node) {
|
|
79
|
+
checkNodeForLineComments(node);
|
|
80
|
+
},
|
|
81
|
+
// Check class declarations
|
|
82
|
+
ClassDeclaration(node) {
|
|
83
|
+
checkNodeForLineComments(node);
|
|
84
|
+
},
|
|
85
|
+
// Check property declarations in interfaces and classes
|
|
86
|
+
TSPropertySignature(node) {
|
|
87
|
+
checkNodeForLineComments(node);
|
|
88
|
+
},
|
|
89
|
+
// Check class properties
|
|
90
|
+
PropertyDefinition(node) {
|
|
91
|
+
checkNodeForLineComments(node);
|
|
92
|
+
},
|
|
93
|
+
// Check method declarations
|
|
94
|
+
MethodDefinition(node) {
|
|
95
|
+
checkNodeForLineComments(node);
|
|
96
|
+
},
|
|
97
|
+
// Check enum declarations
|
|
98
|
+
TSEnumDeclaration(node) {
|
|
99
|
+
checkNodeForLineComments(node);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
name: 'prefer-block-comments-for-declarations',
|
|
104
|
+
meta: {
|
|
105
|
+
type: 'suggestion',
|
|
106
|
+
docs: {
|
|
107
|
+
description: 'Enforce the use of block comments for declarations',
|
|
108
|
+
recommended: 'error',
|
|
109
|
+
},
|
|
110
|
+
fixable: 'code',
|
|
111
|
+
schema: [],
|
|
112
|
+
messages: {
|
|
113
|
+
preferBlockComment: 'Use block comments (/** */) instead of line comments (//) for declarations to improve IDE support.',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
defaultOptions: [],
|
|
117
|
+
});
|
|
118
|
+
//# sourceMappingURL=prefer-block-comments-for-declarations.js.map
|
|
@@ -55,18 +55,36 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
55
55
|
// For destructured parameters, use the type annotation name
|
|
56
56
|
const typeNode = param.typeAnnotation.typeAnnotation;
|
|
57
57
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
58
|
-
|
|
58
|
+
// Include type parameters in the type signature to differentiate generic types
|
|
59
|
+
const typeName = typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier
|
|
59
60
|
? typeNode.typeName.name
|
|
60
61
|
: 'unknown';
|
|
62
|
+
// If there are type parameters, include them in the type signature
|
|
63
|
+
if (typeNode.typeParameters && typeNode.typeParameters.params.length > 0) {
|
|
64
|
+
const typeParams = typeNode.typeParameters.params
|
|
65
|
+
.map(param => param.type)
|
|
66
|
+
.join('_');
|
|
67
|
+
return `${typeName}<${typeParams}>`;
|
|
68
|
+
}
|
|
69
|
+
return typeName;
|
|
61
70
|
}
|
|
62
71
|
return typeNode.type;
|
|
63
72
|
}
|
|
64
73
|
if (param.type === utils_1.AST_NODE_TYPES.Identifier && param.typeAnnotation) {
|
|
65
74
|
const typeNode = param.typeAnnotation.typeAnnotation;
|
|
66
75
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
67
|
-
|
|
76
|
+
// Include type parameters in the type signature to differentiate generic types
|
|
77
|
+
const typeName = typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier
|
|
68
78
|
? typeNode.typeName.name
|
|
69
79
|
: 'unknown';
|
|
80
|
+
// If there are type parameters, include them in the type signature
|
|
81
|
+
if (typeNode.typeParameters && typeNode.typeParameters.params.length > 0) {
|
|
82
|
+
const typeParams = typeNode.typeParameters.params
|
|
83
|
+
.map(param => param.type)
|
|
84
|
+
.join('_');
|
|
85
|
+
return `${typeName}<${typeParams}>`;
|
|
86
|
+
}
|
|
87
|
+
return typeName;
|
|
70
88
|
}
|
|
71
89
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSStringKeyword)
|
|
72
90
|
return 'string';
|
|
@@ -258,6 +276,24 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
258
276
|
if (hasABPattern(node.params))
|
|
259
277
|
return true;
|
|
260
278
|
}
|
|
279
|
+
// Check if the function is a handler with transaction parameter
|
|
280
|
+
// This is a common pattern in Firebase/Firestore handlers
|
|
281
|
+
if ((node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
282
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
283
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) &&
|
|
284
|
+
node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
285
|
+
node.parent.id?.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
286
|
+
const functionName = node.parent.id.name;
|
|
287
|
+
// Check if the function name or its type annotation suggests it's a handler with transaction
|
|
288
|
+
if (functionName.includes('Transaction') ||
|
|
289
|
+
functionName.includes('WithTransaction') ||
|
|
290
|
+
(node.parent.id.typeAnnotation?.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
291
|
+
node.parent.id.typeAnnotation.typeAnnotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
292
|
+
(node.parent.id.typeAnnotation.typeAnnotation.typeName.name.includes('Transaction') ||
|
|
293
|
+
node.parent.id.typeAnnotation.typeAnnotation.typeName.name.includes('WithTransaction')))) {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
261
297
|
return false;
|
|
262
298
|
}
|
|
263
299
|
function checkFunction(node) {
|
|
@@ -73,6 +73,31 @@ exports.preferUseMemoOverUseEffectUseState = (0, createRule_1.createRule)({
|
|
|
73
73
|
// Allow basic expressions and literals
|
|
74
74
|
return true;
|
|
75
75
|
};
|
|
76
|
+
// Helper to check if a node is a reference to a prop or variable
|
|
77
|
+
const isIdentifierReference = (node) => {
|
|
78
|
+
return node.type === 'Identifier';
|
|
79
|
+
};
|
|
80
|
+
// Helper to check if this is a state synchronization pattern
|
|
81
|
+
const isStateSynchronization = (initialValue, setterArgument) => {
|
|
82
|
+
// If the initial value is a reference to a prop/variable and the setter argument
|
|
83
|
+
// is the same reference, this is likely state synchronization
|
|
84
|
+
if (initialValue &&
|
|
85
|
+
isIdentifierReference(initialValue) &&
|
|
86
|
+
isIdentifierReference(setterArgument) &&
|
|
87
|
+
initialValue.name === setterArgument.name) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
// If the initial value is a function that references a prop and the setter argument
|
|
91
|
+
// is that same prop, this is likely state synchronization
|
|
92
|
+
if (initialValue &&
|
|
93
|
+
initialValue.type === 'ArrowFunctionExpression' &&
|
|
94
|
+
initialValue.body.type === 'Identifier' &&
|
|
95
|
+
isIdentifierReference(setterArgument) &&
|
|
96
|
+
initialValue.body.name === setterArgument.name) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
};
|
|
76
101
|
return {
|
|
77
102
|
// Track useState declarations
|
|
78
103
|
VariableDeclarator(node) {
|
|
@@ -110,6 +135,10 @@ exports.preferUseMemoOverUseEffectUseState = (0, createRule_1.createRule)({
|
|
|
110
135
|
const stateInfo = stateSetters.get(setterName);
|
|
111
136
|
if (stateInfo && statement.expression.arguments.length === 1) {
|
|
112
137
|
const computation = statement.expression.arguments[0];
|
|
138
|
+
// Skip if this is a state synchronization pattern
|
|
139
|
+
if (isStateSynchronization(stateInfo.initialValue, computation)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
113
142
|
// Check if the computation is pure
|
|
114
143
|
if (isPureComputation(computation)) {
|
|
115
144
|
// Report the issue but without autofixing
|