@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
|
@@ -13,10 +13,10 @@ exports.enforceCallableTypes = (0, createRule_1.createRule)({
|
|
|
13
13
|
},
|
|
14
14
|
schema: [],
|
|
15
15
|
messages: {
|
|
16
|
-
missingPropsType: '
|
|
17
|
-
missingResponseType: '
|
|
18
|
-
unusedPropsType: 'Props
|
|
19
|
-
unusedResponseType: 'Response
|
|
16
|
+
missingPropsType: 'Callable functions must export a Props type to describe the request payload. Without Props the callable accepts any data and loses compile-time validation; export `type Props = { ... }` and use it in `CallableRequest<Props>` so request.data stays typed.',
|
|
17
|
+
missingResponseType: 'Callable functions must export a Response type to document what the function returns. Without Response the callable can return any shape and break clients; export `type Response = ...` and return that shape from the handler.',
|
|
18
|
+
unusedPropsType: 'Props is exported but never used in the onCall handler. An unused Props type lets the request payload drift from the code that reads it; annotate the handler parameter as `CallableRequest<Props>` or remove Props if the callable does not accept data.',
|
|
19
|
+
unusedResponseType: 'Response is exported but never used in the callable return type. Without applying Response, the callable can return any payload and clients lose a stable contract; return Response (or Promise<Response>) from the handler or remove the unused type.',
|
|
20
20
|
},
|
|
21
21
|
},
|
|
22
22
|
defaultOptions: [],
|
|
@@ -19,37 +19,144 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
19
19
|
defaultOptions: [],
|
|
20
20
|
create(context) {
|
|
21
21
|
function isFunction(node) {
|
|
22
|
-
return (node.type === utils_1.
|
|
23
|
-
node.type === utils_1.
|
|
22
|
+
return (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
23
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression);
|
|
24
|
+
}
|
|
25
|
+
function isInsideUseCallback(node) {
|
|
26
|
+
let current = node.parent;
|
|
27
|
+
while (current) {
|
|
28
|
+
if (current.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
29
|
+
const { callee } = current;
|
|
30
|
+
const isDirectUseCallback = callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
31
|
+
callee.name === 'useCallback';
|
|
32
|
+
const isMemberUseCallback = callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
33
|
+
!callee.computed &&
|
|
34
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
35
|
+
callee.property.name === 'useCallback';
|
|
36
|
+
if (isDirectUseCallback || isMemberUseCallback) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
current = current.parent;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
function collectBoundNames(param, out) {
|
|
45
|
+
if (param.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
46
|
+
out.push(param.name);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (param.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
|
|
50
|
+
collectBoundNames(param.left, out);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (param.type === utils_1.AST_NODE_TYPES.RestElement) {
|
|
54
|
+
if (param.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
55
|
+
out.push(param.argument.name);
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (param.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
60
|
+
for (const property of param.properties) {
|
|
61
|
+
if (property.type === utils_1.AST_NODE_TYPES.RestElement) {
|
|
62
|
+
collectBoundNames(property, out);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (property.type !== utils_1.AST_NODE_TYPES.Property)
|
|
66
|
+
continue;
|
|
67
|
+
collectBoundNames(property.value, out);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (param.type === utils_1.AST_NODE_TYPES.ArrayPattern) {
|
|
72
|
+
for (const element of param.elements) {
|
|
73
|
+
if (element)
|
|
74
|
+
collectBoundNames(element, out);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function getParentFunctionParams(node) {
|
|
79
|
+
let current = node.parent;
|
|
80
|
+
while (current) {
|
|
81
|
+
if (isFunction(current)) {
|
|
82
|
+
const params = [];
|
|
83
|
+
for (const param of current.params) {
|
|
84
|
+
collectBoundNames(param, params);
|
|
85
|
+
}
|
|
86
|
+
return params;
|
|
87
|
+
}
|
|
88
|
+
current = current.parent;
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
function referencesParentScopeVariables(functionNode, parentParams) {
|
|
93
|
+
const scopeManager = context.getSourceCode().scopeManager;
|
|
94
|
+
if (!scopeManager)
|
|
95
|
+
return false;
|
|
96
|
+
const scope = scopeManager.acquire(functionNode);
|
|
97
|
+
if (!scope)
|
|
98
|
+
return false;
|
|
99
|
+
// "through" holds references that are not resolved within the function scope
|
|
100
|
+
for (const ref of scope.through) {
|
|
101
|
+
const { identifier } = ref;
|
|
102
|
+
const parent = identifier.parent;
|
|
103
|
+
const isPropertyKey = parent &&
|
|
104
|
+
((parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
105
|
+
!parent.computed &&
|
|
106
|
+
parent.property === identifier) ||
|
|
107
|
+
(parent.type === utils_1.AST_NODE_TYPES.Property &&
|
|
108
|
+
parent.key === identifier &&
|
|
109
|
+
parent.kind === 'init'));
|
|
110
|
+
if (isPropertyKey) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (parentParams.includes(identifier.name)) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
24
118
|
}
|
|
25
119
|
function containsFunction(node) {
|
|
26
120
|
if (isFunction(node)) {
|
|
27
121
|
return true;
|
|
28
122
|
}
|
|
29
|
-
if (node.type === utils_1.
|
|
123
|
+
if (node.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
30
124
|
return node.properties.some((prop) => {
|
|
31
|
-
if (prop.type === utils_1.
|
|
32
|
-
'value' in prop) {
|
|
125
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property && 'value' in prop) {
|
|
33
126
|
return containsFunction(prop.value);
|
|
34
127
|
}
|
|
128
|
+
if (prop.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
129
|
+
return containsFunction(prop.argument);
|
|
130
|
+
}
|
|
35
131
|
return false;
|
|
36
132
|
});
|
|
37
133
|
}
|
|
38
|
-
if (node.type === utils_1.
|
|
134
|
+
if (node.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
39
135
|
return node.elements.some((element) => element && containsFunction(element));
|
|
40
136
|
}
|
|
137
|
+
// Check ternary expressions (conditional expressions)
|
|
138
|
+
if (node.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
|
|
139
|
+
return (containsFunction(node.consequent) || containsFunction(node.alternate));
|
|
140
|
+
}
|
|
141
|
+
// Check logical expressions (&&, ||)
|
|
142
|
+
if (node.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
|
|
143
|
+
return containsFunction(node.left) || containsFunction(node.right);
|
|
144
|
+
}
|
|
41
145
|
return false;
|
|
42
146
|
}
|
|
43
147
|
function hasJSXWithFunction(node) {
|
|
44
|
-
if (node.type === utils_1.
|
|
148
|
+
if (node.type === utils_1.AST_NODE_TYPES.JSXElement) {
|
|
45
149
|
return node.openingElement.attributes.some((attr) => {
|
|
46
|
-
if (attr.type === utils_1.
|
|
47
|
-
attr.value) {
|
|
48
|
-
if (attr.value.type ===
|
|
49
|
-
utils_1.TSESTree.AST_NODE_TYPES.JSXExpressionContainer) {
|
|
150
|
+
if (attr.type === utils_1.AST_NODE_TYPES.JSXAttribute && attr.value) {
|
|
151
|
+
if (attr.value.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer) {
|
|
50
152
|
return containsFunction(attr.value.expression);
|
|
51
153
|
}
|
|
52
154
|
}
|
|
155
|
+
else if (attr.type === utils_1.AST_NODE_TYPES.JSXSpreadAttribute &&
|
|
156
|
+
attr.argument.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
157
|
+
// Only inspect literal objects to avoid heavy/static-unsafe analysis on identifiers.
|
|
158
|
+
return containsFunction(attr.argument);
|
|
159
|
+
}
|
|
53
160
|
return false;
|
|
54
161
|
});
|
|
55
162
|
}
|
|
@@ -57,19 +164,37 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
57
164
|
}
|
|
58
165
|
function checkJSXAttribute(node) {
|
|
59
166
|
if (!node.value ||
|
|
60
|
-
node.value.type !== utils_1.
|
|
167
|
+
node.value.type !== utils_1.AST_NODE_TYPES.JSXExpressionContainer) {
|
|
61
168
|
return;
|
|
62
169
|
}
|
|
63
170
|
const { expression } = node.value;
|
|
64
171
|
// Skip if the prop is already wrapped in useCallback or useMemo
|
|
65
|
-
if (expression.type === utils_1.
|
|
66
|
-
expression.callee.type === utils_1.
|
|
172
|
+
if (expression.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
173
|
+
expression.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
67
174
|
(expression.callee.name === 'useCallback' ||
|
|
68
175
|
expression.callee.name === 'useMemo')) {
|
|
69
176
|
return;
|
|
70
177
|
}
|
|
71
178
|
// Check for direct inline functions
|
|
72
179
|
if (isFunction(expression)) {
|
|
180
|
+
// Skip reporting if this callback is inside a useCallback and references parent scope variables
|
|
181
|
+
const isInUseCallback = isInsideUseCallback(expression);
|
|
182
|
+
const parentParams = getParentFunctionParams(expression);
|
|
183
|
+
const referencesParentVars = referencesParentScopeVariables(expression, parentParams);
|
|
184
|
+
if (isInUseCallback && referencesParentVars) {
|
|
185
|
+
// Skip reporting - this is a nested callback that needs access to parent scope
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
context.report({
|
|
189
|
+
node,
|
|
190
|
+
messageId: 'enforceCallback',
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// Check for ternary expressions and logical expressions containing functions
|
|
195
|
+
if ((expression.type === utils_1.AST_NODE_TYPES.ConditionalExpression ||
|
|
196
|
+
expression.type === utils_1.AST_NODE_TYPES.LogicalExpression) &&
|
|
197
|
+
containsFunction(expression)) {
|
|
73
198
|
context.report({
|
|
74
199
|
node,
|
|
75
200
|
messageId: 'enforceCallback',
|
|
@@ -77,12 +202,12 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
77
202
|
return;
|
|
78
203
|
}
|
|
79
204
|
// Check for objects/arrays/JSX elements containing functions
|
|
80
|
-
if ((expression.type === utils_1.
|
|
81
|
-
expression.type === utils_1.
|
|
82
|
-
expression.type === utils_1.
|
|
205
|
+
if ((expression.type === utils_1.AST_NODE_TYPES.ObjectExpression ||
|
|
206
|
+
expression.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
|
|
207
|
+
expression.type === utils_1.AST_NODE_TYPES.JSXElement) &&
|
|
83
208
|
(containsFunction(expression) || hasJSXWithFunction(expression))) {
|
|
84
209
|
// Skip reporting if this is a JSX element and we already reported an inline function
|
|
85
|
-
if (expression.type === utils_1.
|
|
210
|
+
if (expression.type === utils_1.AST_NODE_TYPES.JSXElement &&
|
|
86
211
|
hasJSXWithFunction(expression)) {
|
|
87
212
|
return;
|
|
88
213
|
}
|
|
@@ -15,7 +15,7 @@ exports.enforceCentralizedMockFirestore = (0, createRule_1.createRule)({
|
|
|
15
15
|
fixable: 'code',
|
|
16
16
|
schema: [],
|
|
17
17
|
messages: {
|
|
18
|
-
useCentralizedMockFirestore: '
|
|
18
|
+
useCentralizedMockFirestore: 'This file defines or re-exports a local mockFirestore instead of importing the shared one from "{{requiredPath}}". Local mocks drift from the canonical behavior and hide API changes across suites. To fix this, import mockFirestore from the centralized test util so fixes happen in one place.',
|
|
19
19
|
},
|
|
20
20
|
},
|
|
21
21
|
defaultOptions: [],
|
|
@@ -145,6 +145,9 @@ exports.enforceCentralizedMockFirestore = (0, createRule_1.createRule)({
|
|
|
145
145
|
context.report({
|
|
146
146
|
node: Array.from(mockFirestoreNodes)[0],
|
|
147
147
|
messageId: 'useCentralizedMockFirestore',
|
|
148
|
+
data: {
|
|
149
|
+
requiredPath: MOCK_FIRESTORE_PATH,
|
|
150
|
+
},
|
|
148
151
|
fix(fixer) {
|
|
149
152
|
// Instead of trying to modify the code incrementally, we'll generate the entire fixed code
|
|
150
153
|
const originalText = sourceCode.getText();
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
type MessageIds = 'missingConsoleError' | 'missingConsoleWarn' | 'missingConsoleBoth';
|
|
2
|
+
export declare const enforceConsoleError: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
3
|
+
export {};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.enforceConsoleError = void 0;
|
|
4
|
+
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
6
|
+
exports.enforceConsoleError = (0, createRule_1.createRule)({
|
|
7
|
+
name: 'enforce-console-error',
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'problem',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Enforce proper logging for useAlertDialog based on severity. When severity is "error", console.error must be included. When severity is "warning", console.warn must be included. This ensures all user-facing errors and warnings are properly logged to monitoring systems.',
|
|
12
|
+
recommended: 'error',
|
|
13
|
+
},
|
|
14
|
+
messages: {
|
|
15
|
+
missingConsoleError: 'useAlertDialog with severity "error" requires a console.error statement in the same function scope for proper monitoring.',
|
|
16
|
+
missingConsoleWarn: 'useAlertDialog with severity "warning" requires a console.warn statement in the same function scope for proper monitoring.',
|
|
17
|
+
missingConsoleBoth: 'useAlertDialog with dynamic severity requires both console.error and console.warn statements in the same function scope for proper monitoring.',
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
},
|
|
21
|
+
defaultOptions: [],
|
|
22
|
+
create(context) {
|
|
23
|
+
// Track all open calls and console calls in the entire file
|
|
24
|
+
const openCalls = [];
|
|
25
|
+
const consoleCalls = [];
|
|
26
|
+
let hasUseAlertDialog = false;
|
|
27
|
+
const functionScopeStack = [];
|
|
28
|
+
let currentFunctionScope = null;
|
|
29
|
+
// Track renamed open functions from useAlertDialog destructuring
|
|
30
|
+
const openFunctionNames = new Set();
|
|
31
|
+
// Track aliased useAlertDialog function names
|
|
32
|
+
const useAlertDialogNames = new Set(['useAlertDialog']);
|
|
33
|
+
function isUseAlertDialogCall(node) {
|
|
34
|
+
return (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
35
|
+
useAlertDialogNames.has(node.callee.name));
|
|
36
|
+
}
|
|
37
|
+
function isConsoleCall(node, method) {
|
|
38
|
+
return (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
39
|
+
node.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
40
|
+
node.callee.object.name === 'console' &&
|
|
41
|
+
node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
42
|
+
node.callee.property.name === method);
|
|
43
|
+
}
|
|
44
|
+
function getSeverityFromObjectExpression(node) {
|
|
45
|
+
for (const prop of node.properties) {
|
|
46
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property) {
|
|
47
|
+
// Handle both computed and non-computed properties
|
|
48
|
+
let isSeverityProperty = false;
|
|
49
|
+
if (!prop.computed &&
|
|
50
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
51
|
+
prop.key.name === 'severity') {
|
|
52
|
+
isSeverityProperty = true;
|
|
53
|
+
}
|
|
54
|
+
else if (prop.computed &&
|
|
55
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
56
|
+
prop.key.value === 'severity') {
|
|
57
|
+
isSeverityProperty = true;
|
|
58
|
+
}
|
|
59
|
+
if (isSeverityProperty) {
|
|
60
|
+
if (prop.value.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
61
|
+
typeof prop.value.value === 'string') {
|
|
62
|
+
return prop.value.value;
|
|
63
|
+
}
|
|
64
|
+
// If severity is not a literal, treat as dynamic
|
|
65
|
+
return 'dynamic';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const enterFunction = (node) => {
|
|
72
|
+
functionScopeStack.push(node);
|
|
73
|
+
currentFunctionScope = functionScopeStack[functionScopeStack.length - 1];
|
|
74
|
+
};
|
|
75
|
+
const exitFunction = () => {
|
|
76
|
+
functionScopeStack.pop();
|
|
77
|
+
currentFunctionScope =
|
|
78
|
+
functionScopeStack[functionScopeStack.length - 1] ?? null;
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
ImportDeclaration(node) {
|
|
82
|
+
// Track aliased imports of useAlertDialog
|
|
83
|
+
const importPath = String(node.source.value);
|
|
84
|
+
const isAlertDialogImport = importPath === '../useAlertDialog' ||
|
|
85
|
+
importPath === './useAlertDialog' ||
|
|
86
|
+
importPath === 'useAlertDialog' ||
|
|
87
|
+
importPath.endsWith('/useAlertDialog') ||
|
|
88
|
+
importPath.endsWith('/useAlertDialog/index') ||
|
|
89
|
+
importPath === '@/hooks/useAlertDialog' ||
|
|
90
|
+
importPath === 'src/hooks/useAlertDialog';
|
|
91
|
+
if (!isAlertDialogImport)
|
|
92
|
+
return;
|
|
93
|
+
for (const specifier of node.specifiers) {
|
|
94
|
+
// Named imports
|
|
95
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
96
|
+
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
97
|
+
specifier.imported.name === 'useAlertDialog' &&
|
|
98
|
+
specifier.local.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
99
|
+
useAlertDialogNames.add(specifier.local.name);
|
|
100
|
+
}
|
|
101
|
+
// Default or namespace imports still imply useAlertDialog is available via the local name
|
|
102
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
|
|
103
|
+
specifier.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
|
|
104
|
+
if (specifier.local.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
105
|
+
useAlertDialogNames.add(specifier.local.name);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
'Program:exit'() {
|
|
111
|
+
// Only check if we have useAlertDialog in the file
|
|
112
|
+
if (!hasUseAlertDialog)
|
|
113
|
+
return;
|
|
114
|
+
// Group open calls by their containing function
|
|
115
|
+
const functionGroups = new Map();
|
|
116
|
+
// Group open calls by function
|
|
117
|
+
openCalls.forEach(({ node, severity, functionScope }) => {
|
|
118
|
+
if (!functionGroups.has(functionScope)) {
|
|
119
|
+
functionGroups.set(functionScope, {
|
|
120
|
+
openCalls: [],
|
|
121
|
+
consoleCalls: [],
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
functionGroups.get(functionScope).openCalls.push({ node, severity });
|
|
125
|
+
});
|
|
126
|
+
// Group console calls by function
|
|
127
|
+
consoleCalls.forEach(({ node, method, functionScope }) => {
|
|
128
|
+
if (!functionGroups.has(functionScope)) {
|
|
129
|
+
functionGroups.set(functionScope, {
|
|
130
|
+
openCalls: [],
|
|
131
|
+
consoleCalls: [],
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
functionGroups
|
|
135
|
+
.get(functionScope)
|
|
136
|
+
.consoleCalls.push({ node, method });
|
|
137
|
+
});
|
|
138
|
+
// Check each function for violations
|
|
139
|
+
functionGroups.forEach((group) => {
|
|
140
|
+
const hasError = group.openCalls.some((call) => call.severity === 'error');
|
|
141
|
+
const hasWarning = group.openCalls.some((call) => call.severity === 'warning');
|
|
142
|
+
const hasDynamic = group.openCalls.some((call) => call.severity === 'dynamic');
|
|
143
|
+
const hasConsoleError = group.consoleCalls.some((call) => call.method === 'error');
|
|
144
|
+
const hasConsoleWarn = group.consoleCalls.some((call) => call.method === 'warn');
|
|
145
|
+
const needsConsoleError = hasError || hasDynamic;
|
|
146
|
+
const needsConsoleWarn = hasWarning || hasDynamic;
|
|
147
|
+
if (hasDynamic && (!hasConsoleError || !hasConsoleWarn)) {
|
|
148
|
+
const dynamicCall = group.openCalls.find((call) => call.severity === 'dynamic');
|
|
149
|
+
if (dynamicCall) {
|
|
150
|
+
context.report({
|
|
151
|
+
node: dynamicCall.node,
|
|
152
|
+
messageId: 'missingConsoleBoth',
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
if (needsConsoleError && !hasConsoleError) {
|
|
158
|
+
const errorCall = group.openCalls.find((call) => call.severity === 'error');
|
|
159
|
+
if (errorCall) {
|
|
160
|
+
context.report({
|
|
161
|
+
node: errorCall.node,
|
|
162
|
+
messageId: 'missingConsoleError',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (needsConsoleWarn && !hasConsoleWarn) {
|
|
167
|
+
const warningCall = group.openCalls.find((call) => call.severity === 'warning');
|
|
168
|
+
if (warningCall) {
|
|
169
|
+
context.report({
|
|
170
|
+
node: warningCall.node,
|
|
171
|
+
messageId: 'missingConsoleWarn',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
FunctionDeclaration: enterFunction,
|
|
179
|
+
FunctionExpression: enterFunction,
|
|
180
|
+
ArrowFunctionExpression: enterFunction,
|
|
181
|
+
'FunctionDeclaration:exit': exitFunction,
|
|
182
|
+
'FunctionExpression:exit': exitFunction,
|
|
183
|
+
'ArrowFunctionExpression:exit': exitFunction,
|
|
184
|
+
VariableDeclarator(node) {
|
|
185
|
+
// Track destructuring of open functions from useAlertDialog
|
|
186
|
+
if (node.init &&
|
|
187
|
+
node.init.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
188
|
+
isUseAlertDialogCall(node.init) &&
|
|
189
|
+
node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
190
|
+
// Look for destructured open properties
|
|
191
|
+
for (const prop of node.id.properties) {
|
|
192
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
193
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
194
|
+
prop.key.name === 'open') {
|
|
195
|
+
if (prop.value.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
196
|
+
// Track the renamed function name
|
|
197
|
+
openFunctionNames.add(prop.value.name);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
CallExpression(node) {
|
|
204
|
+
// Track useAlertDialog calls
|
|
205
|
+
if (isUseAlertDialogCall(node)) {
|
|
206
|
+
hasUseAlertDialog = true;
|
|
207
|
+
}
|
|
208
|
+
// Track open method calls (both member expressions and direct calls)
|
|
209
|
+
const isOpenCall = ((node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
210
|
+
node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
211
|
+
node.callee.property.name === 'open') ||
|
|
212
|
+
(node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
213
|
+
(node.callee.name === 'open' ||
|
|
214
|
+
openFunctionNames.has(node.callee.name)))) &&
|
|
215
|
+
node.arguments.length > 0;
|
|
216
|
+
if (isOpenCall) {
|
|
217
|
+
const firstArg = node.arguments[0];
|
|
218
|
+
if (firstArg.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
219
|
+
const severity = getSeverityFromObjectExpression(firstArg);
|
|
220
|
+
if (severity && currentFunctionScope) {
|
|
221
|
+
openCalls.push({
|
|
222
|
+
node,
|
|
223
|
+
severity,
|
|
224
|
+
functionScope: currentFunctionScope,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Track console calls
|
|
230
|
+
if (currentFunctionScope) {
|
|
231
|
+
if (isConsoleCall(node, 'error')) {
|
|
232
|
+
consoleCalls.push({
|
|
233
|
+
node,
|
|
234
|
+
method: 'error',
|
|
235
|
+
functionScope: currentFunctionScope,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (isConsoleCall(node, 'warn')) {
|
|
239
|
+
consoleCalls.push({
|
|
240
|
+
node,
|
|
241
|
+
method: 'warn',
|
|
242
|
+
functionScope: currentFunctionScope,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
//# sourceMappingURL=enforce-console-error.js.map
|
|
@@ -16,58 +16,39 @@ exports.enforceCssMediaQueries = (0, createRule_1.createRule)({
|
|
|
16
16
|
recommended: 'error',
|
|
17
17
|
},
|
|
18
18
|
messages: {
|
|
19
|
-
enforceCssMediaQueries: '
|
|
19
|
+
enforceCssMediaQueries: 'Responsive breakpoint "{{source}}" uses JavaScript media detection. JavaScript breakpoints attach resize listeners inside the render path, causing avoidable re-renders and drifting from the single CSS breakpoint source of truth. Move this breakpoint into CSS (@media or container queries) and drive layout changes through class names or CSS-driven props instead of runtime hooks.',
|
|
20
20
|
},
|
|
21
21
|
schema: [],
|
|
22
22
|
},
|
|
23
23
|
defaultOptions: [],
|
|
24
24
|
create(context) {
|
|
25
|
+
const reportUsage = (node, source) => context.report({
|
|
26
|
+
node,
|
|
27
|
+
messageId: 'enforceCssMediaQueries',
|
|
28
|
+
data: { source },
|
|
29
|
+
});
|
|
25
30
|
return {
|
|
26
|
-
//
|
|
31
|
+
// Only react-responsive is handled at the declaration level to avoid duplicates.
|
|
27
32
|
ImportDeclaration(node) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
32
|
-
specifier.imported.name === 'useMediaQuery')) {
|
|
33
|
-
context.report({
|
|
34
|
-
node,
|
|
35
|
-
messageId: 'enforceCssMediaQueries',
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
// Check for react-responsive imports
|
|
39
|
-
if (node.source.value === 'react-responsive' ||
|
|
40
|
-
node.source.value.includes('react-responsive/')) {
|
|
41
|
-
context.report({
|
|
42
|
-
node,
|
|
43
|
-
messageId: 'enforceCssMediaQueries',
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
// Check for useMobile import from hooks/useMobile
|
|
47
|
-
if (node.source.value.includes('hooks/useMobile') &&
|
|
48
|
-
node.specifiers.some((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
49
|
-
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
50
|
-
specifier.imported.name === 'useMobile')) {
|
|
51
|
-
context.report({
|
|
52
|
-
node,
|
|
53
|
-
messageId: 'enforceCssMediaQueries',
|
|
54
|
-
});
|
|
33
|
+
if (node.source.value !== 'react-responsive' &&
|
|
34
|
+
!node.source.value.includes('react-responsive/')) {
|
|
35
|
+
return;
|
|
55
36
|
}
|
|
37
|
+
reportUsage(node, `react-responsive import "${String(node.source.value)}"`);
|
|
56
38
|
},
|
|
57
|
-
//
|
|
39
|
+
// Handle specific specifiers to avoid duplicate diagnostics.
|
|
58
40
|
ImportSpecifier(node) {
|
|
59
|
-
if (node.parent &&
|
|
60
|
-
node.
|
|
61
|
-
node.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
});
|
|
41
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
42
|
+
node.imported.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
43
|
+
if (node.parent.source.value === '@mui/material' &&
|
|
44
|
+
node.imported.name === 'useMediaQuery') {
|
|
45
|
+
reportUsage(node, 'useMediaQuery import from @mui/material');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (node.imported.name === 'useMobile') {
|
|
49
|
+
reportUsage(node, `useMobile import from ${String(node.parent.source.value)}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
71
52
|
}
|
|
72
53
|
},
|
|
73
54
|
// Check for useMediaQuery and useMobile calls
|
|
@@ -75,10 +56,7 @@ exports.enforceCssMediaQueries = (0, createRule_1.createRule)({
|
|
|
75
56
|
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
76
57
|
(node.callee.name === 'useMediaQuery' ||
|
|
77
58
|
node.callee.name === 'useMobile')) {
|
|
78
|
-
|
|
79
|
-
node,
|
|
80
|
-
messageId: 'enforceCssMediaQueries',
|
|
81
|
-
});
|
|
59
|
+
reportUsage(node, `${node.callee.name} call`);
|
|
82
60
|
}
|
|
83
61
|
},
|
|
84
62
|
};
|