@blumintinc/eslint-plugin-blumint 1.12.6 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -99
- package/lib/index.js +111 -17
- package/lib/rules/array-methods-this-context.js +20 -2
- package/lib/rules/avoid-utils-directory.js +11 -2
- package/lib/rules/dynamic-https-errors.d.ts +3 -1
- package/lib/rules/dynamic-https-errors.js +48 -10
- package/lib/rules/enforce-assert-safe-object-key.d.ts +8 -0
- package/lib/rules/{enforce-assertSafe-object-key.js → enforce-assert-safe-object-key.js} +51 -95
- package/lib/rules/enforce-assert-throws.d.ts +3 -1
- package/lib/rules/enforce-assert-throws.js +493 -51
- package/lib/rules/enforce-boolean-naming-prefixes.d.ts +3 -1
- package/lib/rules/enforce-boolean-naming-prefixes.js +832 -39
- package/lib/rules/enforce-callable-types.js +4 -4
- package/lib/rules/enforce-callback-memo.js +143 -18
- package/lib/rules/enforce-centralized-mock-firestore.js +4 -1
- package/lib/rules/enforce-console-error.d.ts +3 -0
- package/lib/rules/enforce-console-error.js +250 -0
- package/lib/rules/enforce-css-media-queries.js +24 -46
- package/lib/rules/enforce-dynamic-firebase-imports.js +100 -19
- package/lib/rules/enforce-dynamic-imports.js +1 -1
- package/lib/rules/enforce-exported-function-types.js +3 -3
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +212 -0
- package/lib/rules/enforce-firestore-doc-ref-generic.js +335 -0
- package/lib/rules/enforce-firestore-facade.js +347 -157
- package/lib/rules/enforce-firestore-rules-get-access.d.ts +3 -0
- package/lib/rules/enforce-firestore-rules-get-access.js +109 -0
- package/lib/rules/enforce-global-constants.d.ts +4 -1
- package/lib/rules/enforce-global-constants.js +240 -12
- package/lib/rules/enforce-memoize-async.d.ts +2 -1
- package/lib/rules/enforce-memoize-async.js +96 -27
- package/lib/rules/enforce-memoize-getters.d.ts +3 -0
- package/lib/rules/enforce-memoize-getters.js +123 -0
- package/lib/rules/enforce-microdiff.js +2 -1
- package/lib/rules/enforce-positive-naming.js +5 -7
- package/lib/rules/enforce-props-argument-name.d.ts +2 -7
- package/lib/rules/enforce-props-argument-name.js +165 -151
- package/lib/rules/enforce-props-naming-consistency.d.ts +2 -0
- package/lib/rules/enforce-props-naming-consistency.js +145 -0
- package/lib/rules/enforce-querykey-ts.d.ts +9 -0
- package/lib/rules/enforce-querykey-ts.js +274 -0
- package/lib/rules/enforce-singular-type-names.js +9 -5
- package/lib/rules/enforce-typescript-markdown-code-blocks.d.ts +1 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.js +96 -0
- package/lib/rules/enforce-verb-noun-naming.js +85 -10
- package/lib/rules/extract-global-constants.js +57 -45
- package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
- package/lib/rules/fast-deep-equal-over-microdiff.js +356 -78
- package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
- package/lib/rules/firestore-transaction-reads-before-writes.js +231 -0
- package/lib/rules/generic-starts-with-t.js +8 -2
- package/lib/rules/memoize-root-level-hocs.d.ts +7 -0
- package/lib/rules/memoize-root-level-hocs.js +242 -0
- package/lib/rules/no-array-length-in-deps.d.ts +12 -0
- package/lib/rules/no-array-length-in-deps.js +308 -0
- package/lib/rules/no-async-array-filter.js +5 -2
- package/lib/rules/no-complex-cloud-params.js +15 -2
- package/lib/rules/no-compositing-layer-props.js +4 -2
- package/lib/rules/no-entire-object-hook-deps.d.ts +3 -1
- package/lib/rules/no-entire-object-hook-deps.js +317 -63
- package/lib/rules/no-excessive-parent-chain.d.ts +5 -0
- package/lib/rules/no-excessive-parent-chain.js +273 -0
- package/lib/rules/no-firestore-object-arrays.js +345 -30
- package/lib/rules/no-hungarian.js +13 -13
- package/lib/rules/no-margin-properties.d.ts +7 -1
- package/lib/rules/no-margin-properties.js +16 -3
- package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
- package/lib/rules/no-misleading-boolean-prefixes.js +327 -0
- package/lib/rules/no-overridable-method-calls-in-constructor.d.ts +1 -0
- package/lib/rules/no-overridable-method-calls-in-constructor.js +260 -0
- package/lib/rules/no-passthrough-getters.d.ts +4 -0
- package/lib/rules/no-passthrough-getters.js +170 -0
- package/lib/rules/no-redundant-usecallback-wrapper.d.ts +8 -0
- package/lib/rules/no-redundant-usecallback-wrapper.js +315 -0
- package/lib/rules/no-restricted-properties-fix.d.ts +12 -0
- package/lib/rules/no-restricted-properties-fix.js +172 -0
- package/lib/rules/no-separate-loading-state.d.ts +8 -0
- package/lib/rules/no-separate-loading-state.js +142 -0
- package/lib/rules/no-stale-state-across-await.d.ts +1 -0
- package/lib/rules/no-stale-state-across-await.js +185 -0
- package/lib/rules/no-static-constants-in-dynamic-files.d.ts +3 -0
- package/lib/rules/no-static-constants-in-dynamic-files.js +149 -0
- package/lib/rules/no-try-catch-already-exists-in-transaction.d.ts +1 -0
- package/lib/rules/no-try-catch-already-exists-in-transaction.js +342 -0
- package/lib/rules/no-type-assertion-returns.js +6 -0
- package/lib/rules/no-undefined-null-passthrough.js +166 -19
- package/lib/rules/no-unnecessary-verb-suffix.js +0 -2
- package/lib/rules/no-unsafe-firestore-spread.js +8 -5
- package/lib/rules/no-unused-props.d.ts +3 -0
- package/lib/rules/no-unused-props.js +213 -78
- package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
- package/lib/rules/optimize-object-boolean-conditions.js +234 -0
- package/lib/rules/parallelize-async-operations.d.ts +8 -0
- package/lib/rules/parallelize-async-operations.js +375 -0
- package/lib/rules/prefer-batch-operations.js +199 -33
- package/lib/rules/prefer-block-comments-for-declarations.js +13 -3
- package/lib/rules/prefer-document-flattening.d.ts +4 -0
- package/lib/rules/prefer-document-flattening.js +239 -0
- package/lib/rules/prefer-field-paths-in-transforms.d.ts +7 -0
- package/lib/rules/prefer-field-paths-in-transforms.js +250 -0
- package/lib/rules/prefer-fragment-component.js +3 -3
- package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
- package/lib/rules/prefer-global-router-state-key.js +230 -31
- package/lib/rules/prefer-next-dynamic.d.ts +9 -0
- package/lib/rules/prefer-next-dynamic.js +366 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +430 -0
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +6 -0
- package/lib/rules/prefer-nullish-coalescing-override.js +182 -0
- package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
- package/lib/rules/prefer-params-over-parent-id.js +228 -0
- package/lib/rules/prefer-settings-object.js +23 -15
- package/lib/rules/prefer-type-alias-over-typeof-constant.d.ts +3 -0
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +259 -0
- package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
- package/lib/rules/prefer-url-tostring-over-tojson.js +166 -0
- package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
- package/lib/rules/prefer-use-deep-compare-memo.js +428 -0
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +24 -1
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
- package/lib/rules/react-usememo-should-be-component.d.ts +2 -1
- package/lib/rules/react-usememo-should-be-component.js +144 -78
- package/lib/rules/require-dynamic-firebase-imports.js +2 -1
- package/lib/rules/require-image-optimized.js +1 -1
- package/lib/rules/require-memo.js +11 -1
- package/lib/rules/require-usememo-object-literals.js +7 -1
- package/lib/rules/semantic-function-prefixes.js +38 -18
- package/lib/rules/sync-onwrite-name-func.js +5 -1
- package/lib/rules/test-file-location-enforcement.d.ts +1 -0
- package/lib/rules/test-file-location-enforcement.js +57 -0
- package/lib/rules/use-custom-link.js +6 -1
- package/lib/rules/use-custom-router.js +6 -1
- package/lib/rules/use-latest-callback.d.ts +3 -0
- package/lib/rules/use-latest-callback.js +270 -0
- package/lib/utils/ASTHelpers.js +10 -0
- package/lib/utils/createRule.js +1 -1
- package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
- package/lib/utils/ruleTester.d.ts +1 -0
- package/lib/utils/ruleTester.js +4 -1
- package/package.json +21 -14
- package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
|
@@ -14,43 +14,111 @@ const PRIMITIVE_TYPES = new Set([
|
|
|
14
14
|
'GeoPoint',
|
|
15
15
|
]);
|
|
16
16
|
const isInFirestoreTypesDirectory = (filename) => {
|
|
17
|
-
|
|
17
|
+
if (!filename || filename === '<input>')
|
|
18
|
+
return false;
|
|
19
|
+
const normalized = filename.replace(/\\/g, '/');
|
|
20
|
+
if (normalized.includes('/node_modules/') || normalized.includes('/dist/')) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
// Match typical mono/multi-repo layouts
|
|
24
|
+
return normalized.includes('/types/firestore/');
|
|
18
25
|
};
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
const getRightmostIdentifierName = (name) => {
|
|
27
|
+
let cur = name;
|
|
28
|
+
// Walk rightwards until we reach an Identifier
|
|
29
|
+
while (cur.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
30
|
+
cur = cur.right;
|
|
31
|
+
}
|
|
32
|
+
return cur.name;
|
|
33
|
+
};
|
|
34
|
+
const isArrayGenericReference = (node) => {
|
|
35
|
+
return (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
36
|
+
(node.typeName.name === 'Array' || node.typeName.name === 'ReadonlyArray'));
|
|
37
|
+
};
|
|
38
|
+
const isParenthesizedType = (node) => {
|
|
39
|
+
if (!node || typeof node !== 'object')
|
|
40
|
+
return false;
|
|
41
|
+
const candidate = node;
|
|
42
|
+
return (candidate.type === 'TSParenthesizedType' &&
|
|
43
|
+
candidate.typeAnnotation !== undefined);
|
|
44
|
+
};
|
|
45
|
+
const unwrapArrayElementType = (node) => {
|
|
46
|
+
let current = node;
|
|
47
|
+
// Fixpoint loop: peel wrappers in any order until none remain
|
|
48
|
+
// Cap unwrap iterations to prevent accidental non-terminating edits if new cases are added.
|
|
49
|
+
// Wrappers are finite; 10 is generous but safe.
|
|
50
|
+
for (let i = 0; i < 10; i++) {
|
|
51
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSTypeOperator &&
|
|
52
|
+
current.operator === 'readonly') {
|
|
53
|
+
current = current
|
|
54
|
+
.typeAnnotation;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (isParenthesizedType(current)) {
|
|
58
|
+
const paren = current;
|
|
59
|
+
current = paren.typeAnnotation;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSArrayType) {
|
|
63
|
+
current = current.elementType;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
67
|
+
isArrayGenericReference(current) &&
|
|
68
|
+
current.typeParameters &&
|
|
69
|
+
current.typeParameters.params.length > 0) {
|
|
70
|
+
current = current.typeParameters.params[0];
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
return current;
|
|
76
|
+
};
|
|
77
|
+
// Determine whether this type node appears in a Firestore model type context
|
|
78
|
+
// i.e., within an interface or type alias declaration, and not within variable/function annotations
|
|
79
|
+
const isInModelTypeContext = (node) => {
|
|
80
|
+
let current = node.parent;
|
|
81
|
+
while (current) {
|
|
82
|
+
// Hard stop for non-type declaration contexts
|
|
83
|
+
if (current.type === utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
84
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
85
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
86
|
+
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
87
|
+
current.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
|
|
88
|
+
current.type === utils_1.AST_NODE_TYPES.TSMethodSignature ||
|
|
89
|
+
current.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
|
|
90
|
+
current.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
|
|
91
|
+
current.type === utils_1.AST_NODE_TYPES.TSParameterProperty) {
|
|
40
92
|
return false;
|
|
93
|
+
}
|
|
94
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration ||
|
|
95
|
+
current.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
current = current.parent;
|
|
41
99
|
}
|
|
100
|
+
return false;
|
|
42
101
|
};
|
|
43
102
|
exports.noFirestoreObjectArrays = (0, createRule_1.createRule)({
|
|
44
103
|
name: 'no-firestore-object-arrays',
|
|
45
104
|
meta: {
|
|
46
105
|
type: 'problem',
|
|
47
106
|
docs: {
|
|
48
|
-
description: 'Disallow arrays of
|
|
107
|
+
description: 'Disallow arrays of object types in Firestore models. Prefer Record maps keyed by id with an index field, or subcollections/arrays of IDs.',
|
|
49
108
|
recommended: 'warn',
|
|
109
|
+
requiresTypeChecking: false,
|
|
50
110
|
},
|
|
51
111
|
schema: [],
|
|
52
112
|
messages: {
|
|
53
|
-
noObjectArrays:
|
|
113
|
+
noObjectArrays: `Arrays of objects are problematic in Firestore:
|
|
114
|
+
- Not queryable
|
|
115
|
+
- Destructive updates
|
|
116
|
+
- Concurrency risks
|
|
117
|
+
|
|
118
|
+
Prefer:
|
|
119
|
+
- Record<string, T> keyed by id with an index field for order (use toMap/toArr)
|
|
120
|
+
- Subcollections
|
|
121
|
+
- Arrays of IDs where appropriate`,
|
|
54
122
|
},
|
|
55
123
|
},
|
|
56
124
|
defaultOptions: [],
|
|
@@ -58,9 +126,246 @@ exports.noFirestoreObjectArrays = (0, createRule_1.createRule)({
|
|
|
58
126
|
if (!isInFirestoreTypesDirectory(context.getFilename())) {
|
|
59
127
|
return {};
|
|
60
128
|
}
|
|
129
|
+
const sourceCode = context.getSourceCode();
|
|
130
|
+
// Collect alias/interface/enum information within this file to refine object vs primitive classification
|
|
131
|
+
const aliasNameToType = new Map();
|
|
132
|
+
const interfaceNames = new Set();
|
|
133
|
+
const enumNames = new Set();
|
|
134
|
+
const visitNode = (n) => {
|
|
135
|
+
switch (n.type) {
|
|
136
|
+
case utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration: {
|
|
137
|
+
aliasNameToType.set(n.id.name, n.typeAnnotation);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case utils_1.AST_NODE_TYPES.TSInterfaceDeclaration: {
|
|
141
|
+
interfaceNames.add(n.id.name);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case utils_1.AST_NODE_TYPES.TSEnumDeclaration: {
|
|
145
|
+
enumNames.add(n.id.name);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case utils_1.AST_NODE_TYPES.ExportNamedDeclaration: {
|
|
149
|
+
if (n.declaration)
|
|
150
|
+
visitNode(n.declaration);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case utils_1.AST_NODE_TYPES.ExportDefaultDeclaration: {
|
|
154
|
+
if (n.declaration) {
|
|
155
|
+
visitNode(n.declaration);
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case utils_1.AST_NODE_TYPES.TSExportAssignment: {
|
|
160
|
+
if (n.expression) {
|
|
161
|
+
visitNode(n.expression);
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case utils_1.AST_NODE_TYPES.TSModuleDeclaration: {
|
|
166
|
+
const body = n.body || null;
|
|
167
|
+
if (body && body.type === utils_1.AST_NODE_TYPES.TSModuleBlock) {
|
|
168
|
+
for (const stmt of body.body) {
|
|
169
|
+
visitNode(stmt);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else if (body && body.type === utils_1.AST_NODE_TYPES.TSModuleDeclaration) {
|
|
173
|
+
visitNode(body);
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
default: {
|
|
178
|
+
// Only traverse a shallow subset we care about
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
for (const stmt of sourceCode.ast.body) {
|
|
184
|
+
visitNode(stmt);
|
|
185
|
+
}
|
|
186
|
+
const seenAlias = new Set();
|
|
187
|
+
const visitingAliases = new Set();
|
|
188
|
+
const isPrimitiveLikeAlias = (name, recursionDepth) => {
|
|
189
|
+
if (seenAlias.has(name))
|
|
190
|
+
return true;
|
|
191
|
+
if (recursionDepth > 50)
|
|
192
|
+
return false;
|
|
193
|
+
if (PRIMITIVE_TYPES.has(name))
|
|
194
|
+
return true;
|
|
195
|
+
if (enumNames.has(name))
|
|
196
|
+
return true; // enums are non-object primitives for our purposes
|
|
197
|
+
const aliased = aliasNameToType.get(name);
|
|
198
|
+
if (!aliased)
|
|
199
|
+
return false;
|
|
200
|
+
if (visitingAliases.has(name)) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
visitingAliases.add(name);
|
|
204
|
+
seenAlias.add(name);
|
|
205
|
+
// Determine if the alias ultimately resolves to only primitive-like/literal types
|
|
206
|
+
const node = aliased;
|
|
207
|
+
const result = isPrimitiveLikeTypeNode(node, recursionDepth + 1);
|
|
208
|
+
visitingAliases.delete(name);
|
|
209
|
+
if (result) {
|
|
210
|
+
seenAlias.add(name);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
// Remove optimistic marking when resolution fails
|
|
214
|
+
seenAlias.delete(name);
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
};
|
|
218
|
+
const isPrimitiveLikeTypeNode = (node, recursionDepth = 0) => {
|
|
219
|
+
if (recursionDepth > 25) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
if (isParenthesizedType(node)) {
|
|
223
|
+
const paren = node;
|
|
224
|
+
return isPrimitiveLikeTypeNode(paren.typeAnnotation, recursionDepth + 1);
|
|
225
|
+
}
|
|
226
|
+
switch (node.type) {
|
|
227
|
+
case utils_1.AST_NODE_TYPES.TSStringKeyword:
|
|
228
|
+
case utils_1.AST_NODE_TYPES.TSNumberKeyword:
|
|
229
|
+
case utils_1.AST_NODE_TYPES.TSBooleanKeyword:
|
|
230
|
+
case utils_1.AST_NODE_TYPES.TSNullKeyword:
|
|
231
|
+
case utils_1.AST_NODE_TYPES.TSUndefinedKeyword:
|
|
232
|
+
case utils_1.AST_NODE_TYPES.TSNeverKeyword:
|
|
233
|
+
return true;
|
|
234
|
+
case utils_1.AST_NODE_TYPES.TSAnyKeyword:
|
|
235
|
+
case utils_1.AST_NODE_TYPES.TSUnknownKeyword:
|
|
236
|
+
return false;
|
|
237
|
+
case utils_1.AST_NODE_TYPES.TSLiteralType:
|
|
238
|
+
return true; // string/number/boolean literals
|
|
239
|
+
case utils_1.AST_NODE_TYPES.TSTypeReference: {
|
|
240
|
+
// Allow known primitive-like references and enums or primitive-like aliases
|
|
241
|
+
const ref = node;
|
|
242
|
+
if (ref.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
243
|
+
const name = ref.typeName.name;
|
|
244
|
+
if (PRIMITIVE_TYPES.has(name) || enumNames.has(name))
|
|
245
|
+
return true;
|
|
246
|
+
if (seenAlias.has(name))
|
|
247
|
+
return true;
|
|
248
|
+
return isPrimitiveLikeAlias(name, recursionDepth + 1);
|
|
249
|
+
}
|
|
250
|
+
if (ref.typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
251
|
+
const name = getRightmostIdentifierName(ref.typeName);
|
|
252
|
+
if (PRIMITIVE_TYPES.has(name) || enumNames.has(name))
|
|
253
|
+
return true;
|
|
254
|
+
if (seenAlias.has(name))
|
|
255
|
+
return true;
|
|
256
|
+
return isPrimitiveLikeAlias(name, recursionDepth + 1);
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
case utils_1.AST_NODE_TYPES.TSUnionType: {
|
|
261
|
+
return node.types.every((t) => isPrimitiveLikeTypeNode(t, recursionDepth + 1));
|
|
262
|
+
}
|
|
263
|
+
case utils_1.AST_NODE_TYPES.TSTypeOperator: {
|
|
264
|
+
// Treat keyof/unique symbol/etc as primitive-like to avoid false positives
|
|
265
|
+
if (node.operator !== 'readonly')
|
|
266
|
+
return true;
|
|
267
|
+
return isPrimitiveLikeTypeNode(node
|
|
268
|
+
.typeAnnotation, recursionDepth + 1);
|
|
269
|
+
}
|
|
270
|
+
case utils_1.AST_NODE_TYPES.TSTemplateLiteralType: {
|
|
271
|
+
// Template literal types behave like strings
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
default:
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
const isObjectType = (node) => {
|
|
279
|
+
if (isParenthesizedType(node)) {
|
|
280
|
+
const paren = node;
|
|
281
|
+
return isObjectType(paren.typeAnnotation);
|
|
282
|
+
}
|
|
283
|
+
switch (node.type) {
|
|
284
|
+
case utils_1.AST_NODE_TYPES.TSTypeLiteral:
|
|
285
|
+
return true;
|
|
286
|
+
case utils_1.AST_NODE_TYPES.TSTypeReference: {
|
|
287
|
+
const tn = node.typeName;
|
|
288
|
+
if (tn.type !== utils_1.AST_NODE_TYPES.Identifier &&
|
|
289
|
+
tn.type !== utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
290
|
+
// Unsupported reference (e.g., ThisType) — do not assume object to avoid false positives
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
const refName = tn.type === utils_1.AST_NODE_TYPES.Identifier
|
|
294
|
+
? tn.name
|
|
295
|
+
: getRightmostIdentifierName(tn);
|
|
296
|
+
if (PRIMITIVE_TYPES.has(refName))
|
|
297
|
+
return false;
|
|
298
|
+
if (interfaceNames.has(refName))
|
|
299
|
+
return true;
|
|
300
|
+
if (enumNames.has(refName))
|
|
301
|
+
return false;
|
|
302
|
+
if (aliasNameToType.has(refName)) {
|
|
303
|
+
return !isPrimitiveLikeAlias(refName, 0);
|
|
304
|
+
}
|
|
305
|
+
// Unknown reference: do not assume object to avoid false positives
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
case utils_1.AST_NODE_TYPES.TSIntersectionType:
|
|
309
|
+
case utils_1.AST_NODE_TYPES.TSUnionType:
|
|
310
|
+
return node.types.some(isObjectType);
|
|
311
|
+
case utils_1.AST_NODE_TYPES.TSMappedType:
|
|
312
|
+
return true;
|
|
313
|
+
case utils_1.AST_NODE_TYPES.TSIndexedAccessType:
|
|
314
|
+
// Treat indexed access as object-like to align with existing tests
|
|
315
|
+
return true;
|
|
316
|
+
case utils_1.AST_NODE_TYPES.TSTypeOperator:
|
|
317
|
+
if (node.operator === 'readonly') {
|
|
318
|
+
return isObjectType(node
|
|
319
|
+
.typeAnnotation);
|
|
320
|
+
}
|
|
321
|
+
return false;
|
|
322
|
+
case utils_1.AST_NODE_TYPES.TSAnyKeyword:
|
|
323
|
+
case utils_1.AST_NODE_TYPES.TSUnknownKeyword:
|
|
324
|
+
return true;
|
|
325
|
+
default:
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
const skipReadonlyAndParens = (parent) => {
|
|
330
|
+
let currentParent = parent;
|
|
331
|
+
while (currentParent) {
|
|
332
|
+
if (currentParent.type === utils_1.AST_NODE_TYPES.TSTypeOperator &&
|
|
333
|
+
currentParent.operator === 'readonly') {
|
|
334
|
+
currentParent = currentParent
|
|
335
|
+
.parent;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (isParenthesizedType(currentParent)) {
|
|
339
|
+
const paren = currentParent;
|
|
340
|
+
currentParent = paren.parent;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
return currentParent;
|
|
346
|
+
};
|
|
347
|
+
const isImmediatelyWrappedByArraySyntax = (node) => {
|
|
348
|
+
const parent = skipReadonlyAndParens(node.parent);
|
|
349
|
+
if (!parent)
|
|
350
|
+
return false;
|
|
351
|
+
if (parent.type === utils_1.AST_NODE_TYPES.TSArrayType)
|
|
352
|
+
return true;
|
|
353
|
+
if (parent.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
354
|
+
isArrayGenericReference(parent))
|
|
355
|
+
return true;
|
|
356
|
+
return false;
|
|
357
|
+
};
|
|
61
358
|
return {
|
|
62
359
|
TSArrayType(node) {
|
|
63
|
-
|
|
360
|
+
// Only consider arrays within model type/interface declarations
|
|
361
|
+
if (!isInModelTypeContext(node)) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (isImmediatelyWrappedByArraySyntax(node)) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const base = unwrapArrayElementType(node.elementType);
|
|
368
|
+
if (isObjectType(base)) {
|
|
64
369
|
context.report({
|
|
65
370
|
node,
|
|
66
371
|
messageId: 'noObjectArrays',
|
|
@@ -68,12 +373,22 @@ exports.noFirestoreObjectArrays = (0, createRule_1.createRule)({
|
|
|
68
373
|
}
|
|
69
374
|
},
|
|
70
375
|
TSTypeReference(node) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
376
|
+
if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
377
|
+
(node.typeName.name === 'Array' ||
|
|
378
|
+
node.typeName.name === 'ReadonlyArray') &&
|
|
74
379
|
node.typeParameters) {
|
|
75
|
-
|
|
76
|
-
if (
|
|
380
|
+
// Only consider arrays within model type/interface declarations
|
|
381
|
+
if (!isInModelTypeContext(node)) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (isImmediatelyWrappedByArraySyntax(node)) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const elementType = node.typeParameters.params[0] ?? null;
|
|
388
|
+
if (!elementType)
|
|
389
|
+
return;
|
|
390
|
+
const base = unwrapArrayElementType(elementType);
|
|
391
|
+
if (isObjectType(base)) {
|
|
77
392
|
context.report({
|
|
78
393
|
node,
|
|
79
394
|
messageId: 'noObjectArrays',
|
|
@@ -46,11 +46,7 @@ const ALLOWED_SUFFIXES = [
|
|
|
46
46
|
'Displayed',
|
|
47
47
|
];
|
|
48
48
|
// Common compound nouns that should not be flagged as Hungarian notation
|
|
49
|
-
const ALLOWED_COMPOUND_NOUNS = [
|
|
50
|
-
'PhoneNumber',
|
|
51
|
-
'EmailAddress',
|
|
52
|
-
'PostalCode',
|
|
53
|
-
];
|
|
49
|
+
const ALLOWED_COMPOUND_NOUNS = ['PhoneNumber', 'EmailAddress', 'PostalCode'];
|
|
54
50
|
// Common built-in JavaScript prototype methods
|
|
55
51
|
const BUILT_IN_METHODS = new Set([
|
|
56
52
|
// String methods
|
|
@@ -225,11 +221,12 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
225
221
|
if (variableName.includes(compoundNoun)) {
|
|
226
222
|
// Check if it's a prefix like "strPhoneNumber" (which should be flagged)
|
|
227
223
|
const prefix = variableName.substring(0, variableName.indexOf(compoundNoun));
|
|
228
|
-
if (TYPE_MARKERS.some(marker => prefix.toLowerCase() === marker.toLowerCase())) {
|
|
224
|
+
if (TYPE_MARKERS.some((marker) => prefix.toLowerCase() === marker.toLowerCase())) {
|
|
229
225
|
// This is a type marker prefix + compound noun, so it should be flagged
|
|
230
226
|
return true;
|
|
231
227
|
}
|
|
232
|
-
// Otherwise
|
|
228
|
+
// Otherwise treat the compound noun as an allowed descriptive phrase
|
|
229
|
+
// (e.g., userPhoneNumber is acceptable even though it contains "Number")
|
|
233
230
|
return false;
|
|
234
231
|
}
|
|
235
232
|
}
|
|
@@ -281,7 +278,8 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
281
278
|
// Check if it's a suffix with proper boundary (e.g., userString, itemArray)
|
|
282
279
|
if (normalizedVarName.endsWith(normalizedMarker) &&
|
|
283
280
|
normalizedVarName.length > normalizedMarker.length &&
|
|
284
|
-
/[A-Z0-9]/.test(variableName[variableName.length - normalizedMarker.length - 1])
|
|
281
|
+
(/[A-Z0-9]/.test(variableName[variableName.length - normalizedMarker.length - 1]) ||
|
|
282
|
+
/[A-Z]/.test(variableName[variableName.length - normalizedMarker.length]))) {
|
|
285
283
|
return true;
|
|
286
284
|
}
|
|
287
285
|
// Check for word boundaries to avoid matching substrings
|
|
@@ -290,17 +288,19 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
290
288
|
if (markerIndex === -1) {
|
|
291
289
|
return false;
|
|
292
290
|
}
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
const suffix = variableName.at(markerIndex + normalizedMarker.length);
|
|
291
|
+
const preMarkerPrefix = markerIndex > 0 ? variableName[markerIndex - 1] : '';
|
|
292
|
+
const suffix = variableName[markerIndex + normalizedMarker.length] ?? '';
|
|
296
293
|
// Ensure we have proper word boundaries
|
|
297
294
|
// A word boundary is defined by:
|
|
298
295
|
// 1. Start of string OR underscore OR capital letter before the marker
|
|
299
296
|
// 2. End of string OR underscore OR capital letter after the marker
|
|
300
|
-
const hasStartBoundary = markerIndex === 0 ||
|
|
297
|
+
const hasStartBoundary = markerIndex === 0 ||
|
|
298
|
+
preMarkerPrefix === '_' ||
|
|
299
|
+
/[A-Z]/.test(preMarkerPrefix) ||
|
|
300
|
+
/[A-Z]/.test(variableName[markerIndex]);
|
|
301
301
|
const hasEndBoundary = markerIndex + normalizedMarker.length === normalizedVarName.length ||
|
|
302
302
|
suffix === '_' ||
|
|
303
|
-
/[A-Z]/.test(suffix
|
|
303
|
+
/[A-Z]/.test(suffix);
|
|
304
304
|
return hasStartBoundary && hasEndBoundary;
|
|
305
305
|
});
|
|
306
306
|
}
|
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
type Options = [
|
|
2
|
+
{
|
|
3
|
+
autofix?: boolean;
|
|
4
|
+
}
|
|
5
|
+
];
|
|
6
|
+
export declare const noMarginProperties: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noMarginProperties", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
7
|
+
export {};
|
|
@@ -40,16 +40,29 @@ exports.noMarginProperties = (0, createRule_1.createRule)({
|
|
|
40
40
|
type: 'suggestion',
|
|
41
41
|
docs: {
|
|
42
42
|
description: 'Discourage using margin properties (margin, marginLeft, marginRight, marginTop, marginBottom, mx, my, etc.) for spacing in MUI components. Instead, prefer defining spacing with padding, gap, or the spacing prop for more predictable layouts.',
|
|
43
|
-
recommended: '
|
|
43
|
+
recommended: 'warn',
|
|
44
44
|
},
|
|
45
|
-
schema: [
|
|
45
|
+
schema: [
|
|
46
|
+
{
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
autofix: {
|
|
50
|
+
type: 'boolean',
|
|
51
|
+
default: false,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
additionalProperties: false,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
46
57
|
messages: {
|
|
47
58
|
noMarginProperties: 'Avoid using {{property}} for spacing in MUI components. Use padding, gap, or the spacing prop instead for more predictable layouts. See https://www.youtube.com/watch?v=KVQMoEFUee8 for more details.',
|
|
48
59
|
},
|
|
49
60
|
},
|
|
50
|
-
defaultOptions: [],
|
|
61
|
+
defaultOptions: [{ autofix: false }],
|
|
51
62
|
create(context) {
|
|
52
63
|
const seenNodes = new WeakSet();
|
|
64
|
+
// Note: autofix option is available but not currently implemented
|
|
65
|
+
// Future implementation can use: const { autofix = false } = _options;
|
|
53
66
|
function checkProperty(propertyName) {
|
|
54
67
|
const normalizedName = normalizePropertyName(propertyName);
|
|
55
68
|
return MARGIN_PROPERTIES.has(normalizedName);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type Options = [
|
|
2
|
+
{
|
|
3
|
+
prefixes?: string[];
|
|
4
|
+
}
|
|
5
|
+
];
|
|
6
|
+
export declare const noMisleadingBooleanPrefixes: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"nonBooleanReturn", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
7
|
+
export {};
|