@blumintinc/eslint-plugin-blumint 1.13.0 → 1.14.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 +170 -139
- package/lib/index.js +190 -1
- package/lib/rules/array-methods-this-context.js +1 -1
- package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
- package/lib/rules/consistent-callback-naming.js +1 -1
- package/lib/rules/dynamic-https-errors.js +3 -4
- package/lib/rules/enforce-assert-safe-object-key.js +11 -11
- package/lib/rules/enforce-boolean-naming-prefixes.js +11 -23
- package/lib/rules/enforce-callback-memo.js +1 -1
- package/lib/rules/enforce-centralized-mock-firestore.js +1 -1
- package/lib/rules/enforce-console-error.js +35 -7
- package/lib/rules/enforce-dynamic-file-naming.d.ts +2 -0
- package/lib/rules/enforce-dynamic-file-naming.js +53 -28
- package/lib/rules/enforce-early-destructuring.d.ts +2 -0
- package/lib/rules/enforce-early-destructuring.js +980 -0
- package/lib/rules/enforce-empty-object-check.d.ts +11 -0
- package/lib/rules/enforce-empty-object-check.js +502 -0
- package/lib/rules/enforce-exported-function-types.js +1 -1
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +100 -50
- package/lib/rules/enforce-firestore-doc-ref-generic.js +15 -3
- package/lib/rules/enforce-firestore-facade.js +4 -2
- package/lib/rules/enforce-firestore-set-merge.js +3 -4
- package/lib/rules/enforce-global-constants.js +1 -1
- package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
- package/lib/rules/enforce-memoize-async.js +2 -6
- package/lib/rules/enforce-memoize-getters.js +39 -15
- package/lib/rules/enforce-microdiff.js +1 -1
- package/lib/rules/enforce-object-literal-as-const.js +4 -4
- package/lib/rules/enforce-positive-naming.js +4 -0
- package/lib/rules/enforce-props-argument-name.js +2 -2
- package/lib/rules/enforce-safe-stringify.js +1 -1
- package/lib/rules/enforce-serializable-params.js +3 -3
- package/lib/rules/enforce-singular-type-names.js +1 -1
- package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
- package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
- package/lib/rules/enforce-storage-context.d.ts +9 -0
- package/lib/rules/enforce-storage-context.js +646 -0
- package/lib/rules/enforce-timestamp-now.js +23 -7
- package/lib/rules/enforce-transform-memoization.d.ts +4 -0
- package/lib/rules/enforce-transform-memoization.js +416 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.js +7 -3
- package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
- package/lib/rules/enforce-unique-cursor-headers.js +365 -0
- package/lib/rules/enforce-verb-noun-naming.js +5 -4
- package/lib/rules/ensure-pointer-events-none.js +28 -2
- package/lib/rules/export-if-in-doubt.js +27 -4
- package/lib/rules/extract-global-constants.js +40 -13
- package/lib/rules/fast-deep-equal-over-microdiff.js +29 -39
- package/lib/rules/firestore-transaction-reads-before-writes.js +50 -4
- package/lib/rules/flatten-push-calls.d.ts +2 -0
- package/lib/rules/flatten-push-calls.js +428 -0
- package/lib/rules/global-const-style.js +50 -19
- package/lib/rules/jsdoc-above-field.d.ts +11 -0
- package/lib/rules/jsdoc-above-field.js +208 -0
- package/lib/rules/key-only-outermost-element.js +21 -3
- package/lib/rules/logical-top-to-bottom-grouping.d.ts +5 -0
- package/lib/rules/logical-top-to-bottom-grouping.js +1258 -0
- package/lib/rules/memo-compare-deeply-complex-props.d.ts +3 -0
- package/lib/rules/memo-compare-deeply-complex-props.js +788 -0
- package/lib/rules/memo-nested-react-components.d.ts +8 -0
- package/lib/rules/memo-nested-react-components.js +410 -0
- package/lib/rules/no-always-true-false-conditions.d.ts +2 -1
- package/lib/rules/no-always-true-false-conditions.js +68 -8
- package/lib/rules/no-array-length-in-deps.js +9 -3
- package/lib/rules/no-async-foreach.js +210 -12
- package/lib/rules/no-circular-references.js +18 -17
- package/lib/rules/no-class-instance-destructuring.js +55 -12
- package/lib/rules/no-complex-cloud-params.js +7 -3
- package/lib/rules/no-compositing-layer-props.js +2 -2
- package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
- package/lib/rules/no-console-error.d.ts +9 -0
- package/lib/rules/no-console-error.js +525 -0
- package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
- package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
- package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
- package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
- package/lib/rules/no-entire-object-hook-deps.js +4 -6
- package/lib/rules/no-excessive-parent-chain.js +1 -1
- package/lib/rules/no-explicit-return-type.d.ts +2 -1
- package/lib/rules/no-explicit-return-type.js +215 -26
- package/lib/rules/no-filter-without-return.js +5 -1
- package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
- package/lib/rules/no-firestore-jest-mock.js +126 -8
- package/lib/rules/no-firestore-object-arrays.js +67 -12
- package/lib/rules/no-handler-suffix.d.ts +12 -0
- package/lib/rules/no-handler-suffix.js +305 -0
- package/lib/rules/no-hungarian.js +1 -1
- package/lib/rules/no-inline-component-prop.d.ts +10 -0
- package/lib/rules/no-inline-component-prop.js +456 -0
- package/lib/rules/no-jsx-in-hooks.js +6 -1
- package/lib/rules/no-jsx-whitespace-literal.js +8 -2
- package/lib/rules/no-margin-properties.js +6 -6
- package/lib/rules/no-memoize-on-static.js +9 -1
- package/lib/rules/no-misleading-boolean-prefixes.js +7 -3
- package/lib/rules/no-misused-switch-case.js +22 -1
- package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
- package/lib/rules/no-mixed-firestore-transactions.js +296 -34
- package/lib/rules/no-mock-firebase-admin.js +5 -2
- package/lib/rules/no-object-values-on-strings.js +10 -2
- package/lib/rules/no-overridable-method-calls-in-constructor.js +29 -17
- package/lib/rules/no-passthrough-getters.js +38 -2
- package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
- package/lib/rules/no-redundant-annotation-assertion.js +402 -0
- package/lib/rules/no-redundant-param-types.js +16 -6
- package/lib/rules/no-redundant-this-params.d.ts +3 -0
- package/lib/rules/no-redundant-this-params.js +459 -0
- package/lib/rules/no-redundant-usecallback-wrapper.js +16 -3
- package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
- package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
- package/lib/rules/no-restricted-properties-fix.js +11 -10
- package/lib/rules/no-separate-loading-state.js +7 -16
- package/lib/rules/no-stale-state-across-await.js +1 -1
- package/lib/rules/no-type-assertion-returns.js +43 -49
- package/lib/rules/no-undefined-null-passthrough.js +61 -31
- package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
- package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
- package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
- package/lib/rules/no-unnecessary-destructuring-rename.js +347 -0
- package/lib/rules/no-unnecessary-destructuring.js +14 -4
- package/lib/rules/no-unnecessary-verb-suffix.js +2 -1
- package/lib/rules/no-unpinned-dependencies.js +36 -5
- package/lib/rules/no-unused-props.d.ts +2 -2
- package/lib/rules/no-unused-props.js +295 -91
- package/lib/rules/no-unused-usestate.js +6 -2
- package/lib/rules/no-useless-fragment.js +28 -2
- package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
- package/lib/rules/no-useless-usememo-primitives.js +393 -0
- package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
- package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
- package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
- package/lib/rules/omit-index-html.d.ts +2 -1
- package/lib/rules/omit-index-html.js +62 -7
- package/lib/rules/optimize-object-boolean-conditions.js +6 -4
- package/lib/rules/parallelize-async-operations.js +24 -5
- package/lib/rules/prefer-batch-operations.d.ts +1 -3
- package/lib/rules/prefer-batch-operations.js +32 -5
- package/lib/rules/prefer-block-comments-for-declarations.js +9 -4
- package/lib/rules/prefer-clone-deep.js +3 -3
- package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
- package/lib/rules/prefer-destructuring-no-class.js +244 -59
- package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
- package/lib/rules/prefer-docsetter-setall.js +243 -0
- package/lib/rules/prefer-field-paths-in-transforms.js +4 -3
- package/lib/rules/prefer-fragment-component.js +1 -1
- package/lib/rules/prefer-fragment-shorthand.js +2 -1
- package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
- package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
- package/lib/rules/prefer-global-router-state-key.js +88 -13
- package/lib/rules/prefer-memoized-props.d.ts +3 -0
- package/lib/rules/prefer-memoized-props.js +445 -0
- package/lib/rules/prefer-next-dynamic.js +60 -69
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +9 -4
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +2 -1
- package/lib/rules/prefer-nullish-coalescing-override.js +11 -5
- package/lib/rules/prefer-params-over-parent-id.js +220 -171
- package/lib/rules/prefer-settings-object.js +2 -2
- package/lib/rules/prefer-type-over-interface.js +7 -2
- package/lib/rules/prefer-url-tostring-over-tojson.js +6 -3
- package/lib/rules/prefer-use-deep-compare-memo.js +8 -5
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +1 -1
- package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
- package/lib/rules/prevent-children-clobber.d.ts +2 -0
- package/lib/rules/prevent-children-clobber.js +536 -0
- package/lib/rules/react-memoize-literals.d.ts +4 -0
- package/lib/rules/react-memoize-literals.js +495 -0
- package/lib/rules/react-usememo-should-be-component.js +2 -2
- package/lib/rules/require-hooks-default-params.js +17 -16
- package/lib/rules/require-https-error-cause.d.ts +4 -0
- package/lib/rules/require-https-error-cause.js +136 -0
- package/lib/rules/require-https-error.js +43 -21
- package/lib/rules/require-image-optimized.js +1 -1
- package/lib/rules/require-memo.js +1 -1
- package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
- package/lib/rules/require-memoize-jsx-returners.js +485 -0
- package/lib/rules/require-usememo-object-literals.js +2 -3
- package/lib/rules/test-file-location-enforcement.js +1 -1
- package/lib/rules/use-latest-callback.js +6 -5
- package/lib/rules/vertically-group-related-functions.d.ts +16 -0
- package/lib/rules/vertically-group-related-functions.js +480 -0
- package/lib/utils/ASTHelpers.d.ts +13 -1
- package/lib/utils/ASTHelpers.js +24 -0
- package/lib/utils/getMethodName.d.ts +27 -0
- package/lib/utils/getMethodName.js +35 -0
- package/lib/utils/tsTypeClassifier.d.ts +30 -0
- package/lib/utils/tsTypeClassifier.js +149 -0
- package/package.json +3 -1
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noConsoleError = void 0;
|
|
4
|
+
const minimatch_1 = require("minimatch");
|
|
5
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
6
|
+
const createRule_1 = require("../utils/createRule");
|
|
7
|
+
const defaultIgnorePatterns = [
|
|
8
|
+
'**/__tests__/**',
|
|
9
|
+
'**/__mocks__/**',
|
|
10
|
+
'**/__playwright__/**',
|
|
11
|
+
'**/scripts/**',
|
|
12
|
+
'**/electron/**',
|
|
13
|
+
'**/node_modules/**',
|
|
14
|
+
'**/dist/**',
|
|
15
|
+
'**/build/**',
|
|
16
|
+
'**/.next/**',
|
|
17
|
+
'**/coverage/**',
|
|
18
|
+
];
|
|
19
|
+
const normalizeFilename = (filename) => filename.replace(/\\/g, '/');
|
|
20
|
+
const isUseAlertDialogImportPath = (importPath) => {
|
|
21
|
+
const normalizedImportPath = normalizeFilename(importPath);
|
|
22
|
+
if (normalizedImportPath === '../useAlertDialog' ||
|
|
23
|
+
normalizedImportPath === './useAlertDialog' ||
|
|
24
|
+
normalizedImportPath === 'useAlertDialog' ||
|
|
25
|
+
normalizedImportPath === '@/hooks/useAlertDialog' ||
|
|
26
|
+
normalizedImportPath === 'src/hooks/useAlertDialog') {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return (normalizedImportPath.endsWith('/useAlertDialog') ||
|
|
30
|
+
normalizedImportPath.endsWith('/useAlertDialog/index'));
|
|
31
|
+
};
|
|
32
|
+
const createFileIgnorePredicate = (options) => {
|
|
33
|
+
const ignorePatterns = [
|
|
34
|
+
...defaultIgnorePatterns,
|
|
35
|
+
...(options?.ignorePatterns ?? []),
|
|
36
|
+
];
|
|
37
|
+
return (filename) => {
|
|
38
|
+
const normalizedFilename = normalizeFilename(filename);
|
|
39
|
+
return (normalizedFilename &&
|
|
40
|
+
!normalizedFilename.startsWith('<') &&
|
|
41
|
+
ignorePatterns.some((pattern) => (0, minimatch_1.minimatch)(normalizedFilename, pattern, { dot: true })));
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
const unwrapChainExpression = (node) => {
|
|
45
|
+
if (!node)
|
|
46
|
+
return null;
|
|
47
|
+
if (node.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
|
48
|
+
return node.expression;
|
|
49
|
+
}
|
|
50
|
+
return node;
|
|
51
|
+
};
|
|
52
|
+
const findVariable = (scope, name) => {
|
|
53
|
+
let current = scope;
|
|
54
|
+
while (current) {
|
|
55
|
+
const variable = current.variables.find((v) => v.name === name);
|
|
56
|
+
if (variable) {
|
|
57
|
+
return variable;
|
|
58
|
+
}
|
|
59
|
+
current = current.upper;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
};
|
|
63
|
+
const getResolvedVariable = (scope, identifier) => {
|
|
64
|
+
const ref = scope.references.find((r) => r.identifier === identifier);
|
|
65
|
+
if (ref?.resolved) {
|
|
66
|
+
return ref.resolved;
|
|
67
|
+
}
|
|
68
|
+
// Fallback for cases where scope analysis might not have linked it yet or for shadowed built-ins
|
|
69
|
+
return findVariable(scope, identifier.name);
|
|
70
|
+
};
|
|
71
|
+
const getScopeForNode = (context, node) => {
|
|
72
|
+
const sourceCode = context.getSourceCode();
|
|
73
|
+
const sourceCodeWithScope = sourceCode;
|
|
74
|
+
if (typeof sourceCodeWithScope.getScope === 'function') {
|
|
75
|
+
return sourceCodeWithScope.getScope(node) ?? context.getScope();
|
|
76
|
+
}
|
|
77
|
+
return context.getScope();
|
|
78
|
+
};
|
|
79
|
+
const getDeclaredVariablesForNode = (context, node) => {
|
|
80
|
+
const sourceCodeWithDeclaredVariables = context.getSourceCode();
|
|
81
|
+
if (typeof sourceCodeWithDeclaredVariables.getDeclaredVariables === 'function') {
|
|
82
|
+
return sourceCodeWithDeclaredVariables.getDeclaredVariables(node);
|
|
83
|
+
}
|
|
84
|
+
return context.getDeclaredVariables(node);
|
|
85
|
+
};
|
|
86
|
+
const isErrorKey = (key, computed) => {
|
|
87
|
+
if (!computed && key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
88
|
+
return key.name === 'error';
|
|
89
|
+
}
|
|
90
|
+
if (computed) {
|
|
91
|
+
if (key.type === utils_1.AST_NODE_TYPES.Literal && key.value === 'error') {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (key.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
|
|
95
|
+
key.expressions.length === 0 &&
|
|
96
|
+
key.quasis.length === 1 &&
|
|
97
|
+
key.quasis[0].value.raw === 'error') {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
};
|
|
103
|
+
const getLocalNameFromPattern = (target) => {
|
|
104
|
+
if (target.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
105
|
+
return target.name;
|
|
106
|
+
}
|
|
107
|
+
if (target.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
|
|
108
|
+
target.left.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
109
|
+
return target.left.name;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
};
|
|
113
|
+
const findDeclaredVariableByName = (name, variables) => variables.find((variable) => variable.name === name);
|
|
114
|
+
/**
|
|
115
|
+
* Tracks aliases of `console` and `console.error` to detect indirect calls.
|
|
116
|
+
*/
|
|
117
|
+
class AliasTracker {
|
|
118
|
+
constructor(context) {
|
|
119
|
+
this.context = context;
|
|
120
|
+
this.consoleAliases = new Set();
|
|
121
|
+
this.errorAliases = new Set();
|
|
122
|
+
}
|
|
123
|
+
getScope(node) {
|
|
124
|
+
return getScopeForNode(this.context, node);
|
|
125
|
+
}
|
|
126
|
+
markConsole(variable) {
|
|
127
|
+
if (!variable)
|
|
128
|
+
return;
|
|
129
|
+
this.consoleAliases.add(variable);
|
|
130
|
+
this.errorAliases.delete(variable);
|
|
131
|
+
}
|
|
132
|
+
markError(variable) {
|
|
133
|
+
if (!variable)
|
|
134
|
+
return;
|
|
135
|
+
this.errorAliases.add(variable);
|
|
136
|
+
this.consoleAliases.delete(variable);
|
|
137
|
+
}
|
|
138
|
+
untrack(variable) {
|
|
139
|
+
if (!variable)
|
|
140
|
+
return;
|
|
141
|
+
this.consoleAliases.delete(variable);
|
|
142
|
+
this.errorAliases.delete(variable);
|
|
143
|
+
}
|
|
144
|
+
isConsoleObject(expression, scope) {
|
|
145
|
+
const unwrapped = unwrapChainExpression(expression);
|
|
146
|
+
if (!unwrapped || unwrapped.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
const variable = getResolvedVariable(scope, unwrapped);
|
|
150
|
+
if (variable) {
|
|
151
|
+
if (unwrapped.name === 'console' &&
|
|
152
|
+
variable.scope.type === 'global' &&
|
|
153
|
+
variable.defs.length === 0) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
return this.consoleAliases.has(variable);
|
|
157
|
+
}
|
|
158
|
+
return unwrapped.name === 'console';
|
|
159
|
+
}
|
|
160
|
+
isConsoleErrorMemberExpression(member, scope) {
|
|
161
|
+
return (this.isConsoleObject(member.object, scope) &&
|
|
162
|
+
isErrorKey(member.property, member.computed ?? false));
|
|
163
|
+
}
|
|
164
|
+
isErrorAlias(identifier, scope) {
|
|
165
|
+
const variable = getResolvedVariable(scope, identifier);
|
|
166
|
+
return Boolean(variable && this.errorAliases.has(variable));
|
|
167
|
+
}
|
|
168
|
+
isConsoleErrorCall(node, scope) {
|
|
169
|
+
const callee = unwrapChainExpression(node.callee);
|
|
170
|
+
if (!callee)
|
|
171
|
+
return false;
|
|
172
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
173
|
+
return this.isConsoleErrorMemberExpression(callee, scope);
|
|
174
|
+
}
|
|
175
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
176
|
+
return this.isErrorAlias(callee, scope);
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
isDirectGlobalConsoleErrorCall(node, scope) {
|
|
181
|
+
const callee = unwrapChainExpression(node.callee);
|
|
182
|
+
if (!callee || callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
if (callee.computed ||
|
|
186
|
+
callee.property.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
187
|
+
callee.property.name !== 'error') {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const obj = unwrapChainExpression(callee.object);
|
|
191
|
+
if (!obj ||
|
|
192
|
+
obj.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
193
|
+
obj.name !== 'console') {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
const variable = getResolvedVariable(scope, obj);
|
|
197
|
+
return !variable || variable.scope.type === 'global';
|
|
198
|
+
}
|
|
199
|
+
handleVariableDeclarator(node) {
|
|
200
|
+
if (!node.init)
|
|
201
|
+
return;
|
|
202
|
+
const scope = this.getScope(node);
|
|
203
|
+
const init = unwrapChainExpression(node.init);
|
|
204
|
+
const declaredVariables = getDeclaredVariablesForNode(this.context, node);
|
|
205
|
+
const getDeclaredVar = (name) => findDeclaredVariableByName(name, declaredVariables) ?? null;
|
|
206
|
+
if (node.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
207
|
+
init &&
|
|
208
|
+
this.isConsoleObject(init, scope)) {
|
|
209
|
+
this.markConsole(getDeclaredVar(node.id.name));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (node.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
213
|
+
init &&
|
|
214
|
+
init.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
215
|
+
this.isConsoleErrorMemberExpression(init, scope)) {
|
|
216
|
+
this.markError(getDeclaredVar(node.id.name));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
|
|
220
|
+
init &&
|
|
221
|
+
this.isConsoleObject(init, scope)) {
|
|
222
|
+
for (const prop of node.id.properties) {
|
|
223
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
224
|
+
isErrorKey(prop.key, prop.computed ?? false)) {
|
|
225
|
+
const localName = getLocalNameFromPattern(prop.value);
|
|
226
|
+
if (localName)
|
|
227
|
+
this.markError(getDeclaredVar(localName));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
handleAssignmentExpression(node) {
|
|
233
|
+
const scope = this.getScope(node);
|
|
234
|
+
const right = unwrapChainExpression(node.right);
|
|
235
|
+
if (!right)
|
|
236
|
+
return;
|
|
237
|
+
if (node.left.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
238
|
+
const variable = getResolvedVariable(scope, node.left);
|
|
239
|
+
if (this.isConsoleObject(right, scope)) {
|
|
240
|
+
this.markConsole(variable);
|
|
241
|
+
}
|
|
242
|
+
else if (right.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
243
|
+
this.isConsoleErrorMemberExpression(right, scope)) {
|
|
244
|
+
this.markError(variable);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
this.untrack(variable);
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (node.left.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
252
|
+
const errorProps = node.left.properties.filter((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
253
|
+
isErrorKey(prop.key, prop.computed ?? false));
|
|
254
|
+
const localNames = errorProps
|
|
255
|
+
.map((prop) => getLocalNameFromPattern(prop.value))
|
|
256
|
+
.filter((name) => Boolean(name));
|
|
257
|
+
const isConsole = this.isConsoleObject(right, scope);
|
|
258
|
+
for (const name of localNames) {
|
|
259
|
+
const v = findVariable(scope, name);
|
|
260
|
+
if (isConsole)
|
|
261
|
+
this.markError(v);
|
|
262
|
+
else
|
|
263
|
+
this.untrack(v);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Tracks `useAlertDialog` hook usage and its `open` function to allow
|
|
270
|
+
* `console.error` calls when an error dialog is being shown.
|
|
271
|
+
*/
|
|
272
|
+
class UseAlertDialogTracker {
|
|
273
|
+
constructor(context) {
|
|
274
|
+
this.context = context;
|
|
275
|
+
this.hookVariables = new Set();
|
|
276
|
+
this.instanceVariables = new Set();
|
|
277
|
+
this.openVariables = new Set();
|
|
278
|
+
this.hasUseAlertDialogCall = false;
|
|
279
|
+
}
|
|
280
|
+
getScope(node) {
|
|
281
|
+
return getScopeForNode(this.context, node);
|
|
282
|
+
}
|
|
283
|
+
static hasErrorSeverity(node) {
|
|
284
|
+
for (const prop of node.properties) {
|
|
285
|
+
if (prop.type !== utils_1.AST_NODE_TYPES.Property)
|
|
286
|
+
continue;
|
|
287
|
+
const isSeverityProperty = (!prop.computed &&
|
|
288
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
289
|
+
prop.key.name === 'severity') ||
|
|
290
|
+
(prop.computed &&
|
|
291
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
292
|
+
prop.key.value === 'severity');
|
|
293
|
+
if (!isSeverityProperty)
|
|
294
|
+
continue;
|
|
295
|
+
if (prop.value.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
296
|
+
typeof prop.value.value === 'string') {
|
|
297
|
+
return prop.value.value === 'error';
|
|
298
|
+
}
|
|
299
|
+
// Non-literal severities cannot be verified at lint time, so treat them
|
|
300
|
+
// conservatively as non-error to prevent false negatives.
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
trackImport(node) {
|
|
306
|
+
const importPath = String(node.source.value);
|
|
307
|
+
if (!isUseAlertDialogImportPath(importPath))
|
|
308
|
+
return;
|
|
309
|
+
for (const specifier of node.specifiers) {
|
|
310
|
+
for (const v of getDeclaredVariablesForNode(this.context, specifier)) {
|
|
311
|
+
this.hookVariables.add(v);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
trackVariableDeclarator(node) {
|
|
316
|
+
if (!node.init)
|
|
317
|
+
return;
|
|
318
|
+
const scope = this.getScope(node);
|
|
319
|
+
// const dialog = useAlertDialog(...) OR const { open } = useAlertDialog(...)
|
|
320
|
+
if (node.init.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
321
|
+
this.isHookCall(node.init)) {
|
|
322
|
+
this.hasUseAlertDialogCall = true;
|
|
323
|
+
if (node.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
324
|
+
const v = findDeclaredVariableByName(node.id.name, getDeclaredVariablesForNode(this.context, node));
|
|
325
|
+
if (v)
|
|
326
|
+
this.instanceVariables.add(v);
|
|
327
|
+
}
|
|
328
|
+
else if (node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
329
|
+
this.trackDestructuredOpen(node.id, node);
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// const { open } = dialog
|
|
334
|
+
if (node.init.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
335
|
+
node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
336
|
+
const initVar = getResolvedVariable(scope, node.init);
|
|
337
|
+
if (initVar && this.instanceVariables.has(initVar)) {
|
|
338
|
+
this.trackDestructuredOpen(node.id, node);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
trackDestructuredOpen(pattern, node) {
|
|
343
|
+
const declaredVariables = getDeclaredVariablesForNode(this.context, node);
|
|
344
|
+
for (const prop of pattern.properties) {
|
|
345
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
346
|
+
!prop.computed &&
|
|
347
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
348
|
+
prop.key.name === 'open') {
|
|
349
|
+
const localName = getLocalNameFromPattern(prop.value);
|
|
350
|
+
if (localName) {
|
|
351
|
+
const v = findDeclaredVariableByName(localName, declaredVariables);
|
|
352
|
+
if (v)
|
|
353
|
+
this.openVariables.add(v);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
isHookCall(node) {
|
|
359
|
+
const callee = unwrapChainExpression(node.callee);
|
|
360
|
+
if (!callee || callee.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
361
|
+
return false;
|
|
362
|
+
const v = getResolvedVariable(this.getScope(callee), callee);
|
|
363
|
+
if (v && this.hookVariables.has(v))
|
|
364
|
+
return true;
|
|
365
|
+
return ((v === null || v.scope.type === 'global') &&
|
|
366
|
+
callee.name === 'useAlertDialog');
|
|
367
|
+
}
|
|
368
|
+
isOpenCall(node) {
|
|
369
|
+
const callee = unwrapChainExpression(node.callee);
|
|
370
|
+
if (!callee)
|
|
371
|
+
return false;
|
|
372
|
+
const scope = this.getScope(node);
|
|
373
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
374
|
+
const v = getResolvedVariable(scope, callee);
|
|
375
|
+
return v !== null && this.openVariables.has(v);
|
|
376
|
+
}
|
|
377
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
378
|
+
if (callee.computed ||
|
|
379
|
+
callee.property.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
380
|
+
callee.property.name !== 'open') {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
const obj = unwrapChainExpression(callee.object);
|
|
384
|
+
if (obj && obj.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
385
|
+
const v = getResolvedVariable(scope, obj);
|
|
386
|
+
return v !== null && this.instanceVariables.has(v);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Manages function scopes and pending `console.error` calls that may be
|
|
394
|
+
* allowed if a `useAlertDialog` call with error severity follows.
|
|
395
|
+
*/
|
|
396
|
+
class PendingCallTracker {
|
|
397
|
+
constructor() {
|
|
398
|
+
this.functionScopeStack = [];
|
|
399
|
+
this.functionsWithErrorDialogOpen = new WeakSet();
|
|
400
|
+
this.pendingConsoleErrorCalls = new Map();
|
|
401
|
+
}
|
|
402
|
+
pushScope(node) {
|
|
403
|
+
this.functionScopeStack.push(node);
|
|
404
|
+
}
|
|
405
|
+
popScope() {
|
|
406
|
+
return this.functionScopeStack.pop() ?? null;
|
|
407
|
+
}
|
|
408
|
+
getCurrentScope() {
|
|
409
|
+
return this.functionScopeStack[this.functionScopeStack.length - 1] ?? null;
|
|
410
|
+
}
|
|
411
|
+
markFunctionWithErrorDialog(node) {
|
|
412
|
+
this.functionsWithErrorDialogOpen.add(node);
|
|
413
|
+
}
|
|
414
|
+
queueCall(scope, node) {
|
|
415
|
+
const pending = this.pendingConsoleErrorCalls.get(scope) ?? [];
|
|
416
|
+
pending.push(node);
|
|
417
|
+
this.pendingConsoleErrorCalls.set(scope, pending);
|
|
418
|
+
}
|
|
419
|
+
flushForScope(scope, reportFn, allowWithUseAlertDialog, hasUseAlertDialogCall) {
|
|
420
|
+
const pending = this.pendingConsoleErrorCalls.get(scope);
|
|
421
|
+
if (!pending)
|
|
422
|
+
return;
|
|
423
|
+
const shouldAllow = allowWithUseAlertDialog &&
|
|
424
|
+
hasUseAlertDialogCall &&
|
|
425
|
+
scope !== null &&
|
|
426
|
+
this.functionsWithErrorDialogOpen.has(scope);
|
|
427
|
+
if (!shouldAllow) {
|
|
428
|
+
for (const node of pending) {
|
|
429
|
+
reportFn(node);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
this.pendingConsoleErrorCalls.delete(scope);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
exports.noConsoleError = (0, createRule_1.createRule)({
|
|
436
|
+
name: 'no-console-error',
|
|
437
|
+
meta: {
|
|
438
|
+
type: 'problem',
|
|
439
|
+
docs: {
|
|
440
|
+
description: 'Disallow console.error so errors flow through structured handling (HttpsError/useErrorAlert on frontend, structured loggers on backend).',
|
|
441
|
+
recommended: 'warn',
|
|
442
|
+
},
|
|
443
|
+
schema: [
|
|
444
|
+
{
|
|
445
|
+
type: 'object',
|
|
446
|
+
properties: {
|
|
447
|
+
ignorePatterns: {
|
|
448
|
+
type: 'array',
|
|
449
|
+
items: { type: 'string' },
|
|
450
|
+
},
|
|
451
|
+
allowWithUseAlertDialog: { type: 'boolean' },
|
|
452
|
+
},
|
|
453
|
+
additionalProperties: false,
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
messages: {
|
|
457
|
+
noConsoleError: 'console.error bypasses the structured error pipeline → failures are harder to monitor, debug, and surface consistently to users → use structured error handling instead (frontend: throw HttpsError or route through useErrorAlert; backend: log with a structured logger such as firebase-functions/v2 logger and propagate/convert the error so monitoring captures it).',
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
defaultOptions: [{}],
|
|
461
|
+
create(context, [options]) {
|
|
462
|
+
const shouldIgnoreFile = createFileIgnorePredicate(options);
|
|
463
|
+
const filename = context.filename ??
|
|
464
|
+
context.getFilename();
|
|
465
|
+
if (shouldIgnoreFile(filename))
|
|
466
|
+
return {};
|
|
467
|
+
const allowWithUseAlertDialog = options.allowWithUseAlertDialog === true;
|
|
468
|
+
const aliasTracker = new AliasTracker(context);
|
|
469
|
+
const dialogTracker = new UseAlertDialogTracker(context);
|
|
470
|
+
const pendingTracker = new PendingCallTracker();
|
|
471
|
+
const report = (node) => {
|
|
472
|
+
context.report({ node, messageId: 'noConsoleError' });
|
|
473
|
+
};
|
|
474
|
+
const onFunctionEnter = (node) => {
|
|
475
|
+
pendingTracker.pushScope(node);
|
|
476
|
+
};
|
|
477
|
+
const onFunctionExit = (node) => {
|
|
478
|
+
pendingTracker.flushForScope(node, report, allowWithUseAlertDialog, dialogTracker.hasUseAlertDialogCall);
|
|
479
|
+
pendingTracker.popScope();
|
|
480
|
+
};
|
|
481
|
+
return {
|
|
482
|
+
ImportDeclaration(node) {
|
|
483
|
+
dialogTracker.trackImport(node);
|
|
484
|
+
},
|
|
485
|
+
FunctionDeclaration: onFunctionEnter,
|
|
486
|
+
FunctionExpression: onFunctionEnter,
|
|
487
|
+
ArrowFunctionExpression: onFunctionEnter,
|
|
488
|
+
'FunctionDeclaration:exit': onFunctionExit,
|
|
489
|
+
'FunctionExpression:exit': onFunctionExit,
|
|
490
|
+
'ArrowFunctionExpression:exit': onFunctionExit,
|
|
491
|
+
'Program:exit'() {
|
|
492
|
+
pendingTracker.flushForScope(null, report, allowWithUseAlertDialog, dialogTracker.hasUseAlertDialogCall);
|
|
493
|
+
},
|
|
494
|
+
VariableDeclarator(node) {
|
|
495
|
+
dialogTracker.trackVariableDeclarator(node);
|
|
496
|
+
aliasTracker.handleVariableDeclarator(node);
|
|
497
|
+
},
|
|
498
|
+
AssignmentExpression(node) {
|
|
499
|
+
aliasTracker.handleAssignmentExpression(node);
|
|
500
|
+
},
|
|
501
|
+
CallExpression(node) {
|
|
502
|
+
if (dialogTracker.isOpenCall(node) &&
|
|
503
|
+
node.arguments[0]?.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
|
|
504
|
+
UseAlertDialogTracker.hasErrorSeverity(node.arguments[0])) {
|
|
505
|
+
const scope = pendingTracker.getCurrentScope();
|
|
506
|
+
if (scope)
|
|
507
|
+
pendingTracker.markFunctionWithErrorDialog(scope);
|
|
508
|
+
}
|
|
509
|
+
const scope = getScopeForNode(context, node);
|
|
510
|
+
if (!aliasTracker.isConsoleErrorCall(node, scope))
|
|
511
|
+
return;
|
|
512
|
+
// Queueing is limited to direct global `console.error(...)` calls because deferral relies on
|
|
513
|
+
// linear control flow. Aliased or destructured references can escape the current scope
|
|
514
|
+
// (stored, passed, invoked later), so they are reported immediately to avoid missing non-local calls.
|
|
515
|
+
if (allowWithUseAlertDialog &&
|
|
516
|
+
aliasTracker.isDirectGlobalConsoleErrorCall(node, scope)) {
|
|
517
|
+
pendingTracker.queueCall(pendingTracker.getCurrentScope(), node);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
report(node);
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
//# sourceMappingURL=no-console-error.js.map
|