@blumintinc/eslint-plugin-blumint 1.16.1 → 1.17.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 +21 -0
- package/lib/index.js +64 -1
- package/lib/rules/enforce-boolean-naming-prefixes.js +30 -14
- package/lib/rules/enforce-cloud-function-id-length.d.ts +5 -0
- package/lib/rules/enforce-cloud-function-id-length.js +104 -0
- package/lib/rules/enforce-is-prefix-validators.d.ts +13 -0
- package/lib/rules/enforce-is-prefix-validators.js +304 -0
- package/lib/rules/enforce-m3-sentence-case.d.ts +12 -0
- package/lib/rules/enforce-m3-sentence-case.js +430 -0
- package/lib/rules/enforce-snapshot-state-narrowing.d.ts +10 -0
- package/lib/rules/enforce-snapshot-state-narrowing.js +281 -0
- package/lib/rules/enforce-types-directory-placement.d.ts +9 -0
- package/lib/rules/enforce-types-directory-placement.js +276 -0
- package/lib/rules/logical-top-to-bottom-grouping.js +87 -19
- package/lib/rules/no-direct-function-state.d.ts +8 -0
- package/lib/rules/no-direct-function-state.js +285 -0
- package/lib/rules/no-fill-template-mutation.d.ts +1 -0
- package/lib/rules/no-fill-template-mutation.js +324 -0
- package/lib/rules/no-hungarian.js +11 -0
- package/lib/rules/no-portal-inside-tooltip.d.ts +10 -0
- package/lib/rules/no-portal-inside-tooltip.js +219 -0
- package/lib/rules/no-redundant-boolean-callback-props.d.ts +11 -0
- package/lib/rules/no-redundant-boolean-callback-props.js +355 -0
- package/lib/rules/no-satisfies-in-frontend-bundle.d.ts +8 -0
- package/lib/rules/no-satisfies-in-frontend-bundle.js +126 -0
- package/lib/rules/no-single-dismiss-dialog-button.d.ts +7 -0
- package/lib/rules/no-single-dismiss-dialog-button.js +127 -0
- package/lib/rules/no-stablehash-react-nodes.d.ts +1 -0
- package/lib/rules/no-stablehash-react-nodes.js +325 -0
- package/lib/rules/parallelize-loop-awaits.d.ts +8 -0
- package/lib/rules/parallelize-loop-awaits.js +582 -0
- package/lib/rules/prefer-flat-transform-each-keys.d.ts +1 -0
- package/lib/rules/prefer-flat-transform-each-keys.js +228 -0
- package/lib/rules/prefer-getter-over-parameterless-method.d.ts +1 -0
- package/lib/rules/prefer-getter-over-parameterless-method.js +44 -0
- package/lib/rules/prefer-spread-over-reassembly.d.ts +5 -0
- package/lib/rules/prefer-spread-over-reassembly.js +401 -0
- package/lib/rules/prefer-sx-prop-over-system-props.d.ts +9 -0
- package/lib/rules/prefer-sx-prop-over-system-props.js +401 -0
- package/lib/rules/prefer-use-base62-id.d.ts +8 -0
- package/lib/rules/prefer-use-base62-id.js +483 -0
- package/lib/rules/prefer-use-theme.d.ts +1 -0
- package/lib/rules/prefer-use-theme.js +206 -0
- package/lib/rules/prefer-utility-function-own-file.d.ts +9 -0
- package/lib/rules/prefer-utility-function-own-file.js +505 -0
- package/lib/rules/require-props-composition.d.ts +10 -0
- package/lib/rules/require-props-composition.js +433 -0
- package/lib/rules/require-server-timestamp-for-firestore-dates.d.ts +9 -0
- package/lib/rules/require-server-timestamp-for-firestore-dates.js +313 -0
- package/package.json +1 -1
- package/release-manifest.json +212 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noStablehashReactNodes = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
/**
|
|
7
|
+
* Module path suffix that identifies the stableHash family of utilities.
|
|
8
|
+
* Matches both absolute and relative import paths that end in util/hash/stableHash.
|
|
9
|
+
*/
|
|
10
|
+
const HASH_MODULE_SUFFIX = 'util/hash/stableHash';
|
|
11
|
+
/**
|
|
12
|
+
* Named exports from the hash module that we care about. sortedHash is included
|
|
13
|
+
* because it wraps stableHash and has the same stringification danger.
|
|
14
|
+
*/
|
|
15
|
+
const HASH_EXPORT_NAMES = new Set(['stableHash', 'sortedHash']);
|
|
16
|
+
/**
|
|
17
|
+
* Type names that unambiguously denote React render trees or BluMint node
|
|
18
|
+
* wrappers. When a variable/parameter carries one of these annotations we know
|
|
19
|
+
* it is a ReactNode and must not be hashed.
|
|
20
|
+
*/
|
|
21
|
+
const REACT_NODE_TYPE_NAMES = new Set([
|
|
22
|
+
'ReactNode',
|
|
23
|
+
'ReactElement',
|
|
24
|
+
'JSX',
|
|
25
|
+
'KeyedNode',
|
|
26
|
+
'OrNode',
|
|
27
|
+
]);
|
|
28
|
+
/**
|
|
29
|
+
* Object-literal property names whose presence signals the object contains a
|
|
30
|
+
* React render tree (either a children prop or a BluMint "Node" field).
|
|
31
|
+
*/
|
|
32
|
+
const NODE_PROP_NAMES = new Set(['children', 'Node']);
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
/**
|
|
37
|
+
* Return true when the import source path ends with the known hash module
|
|
38
|
+
* suffix, handling both absolute paths (functions/src/util/hash/stableHash)
|
|
39
|
+
* and relative variants (../../../util/hash/stableHash).
|
|
40
|
+
*/
|
|
41
|
+
function isHashModulePath(source) {
|
|
42
|
+
return source.endsWith(HASH_MODULE_SUFFIX);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the TSTypeAnnotation / TSTypeReference down to a base type name so
|
|
46
|
+
* we can match against REACT_NODE_TYPE_NAMES without depending on the type
|
|
47
|
+
* checker.
|
|
48
|
+
*
|
|
49
|
+
* Handles:
|
|
50
|
+
* ReactNode → 'ReactNode'
|
|
51
|
+
* React.ReactNode → 'ReactNode' (qualified reference)
|
|
52
|
+
* JSX.Element → 'JSX'
|
|
53
|
+
* OrNode<T> → 'OrNode'
|
|
54
|
+
* KeyedNode[] / readonly KeyedNode[] → 'KeyedNode' (array element)
|
|
55
|
+
*/
|
|
56
|
+
function extractBaseTypeName(typeNode) {
|
|
57
|
+
switch (typeNode.type) {
|
|
58
|
+
case utils_1.AST_NODE_TYPES.TSTypeReference: {
|
|
59
|
+
const name = typeNode.typeName;
|
|
60
|
+
if (name.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
61
|
+
return name.name;
|
|
62
|
+
}
|
|
63
|
+
// Qualified name: React.ReactNode → take the right-hand part
|
|
64
|
+
if (name.type === utils_1.AST_NODE_TYPES.TSQualifiedName &&
|
|
65
|
+
name.right.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
66
|
+
// For JSX.Element we want "JSX" (the left) so we can match the set
|
|
67
|
+
if (name.left.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
68
|
+
name.left.name === 'JSX') {
|
|
69
|
+
return 'JSX';
|
|
70
|
+
}
|
|
71
|
+
return name.right.name;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
case utils_1.AST_NODE_TYPES.TSArrayType:
|
|
76
|
+
return extractBaseTypeName(typeNode.elementType);
|
|
77
|
+
case utils_1.AST_NODE_TYPES.TSTypeOperator:
|
|
78
|
+
// readonly T[]
|
|
79
|
+
if (typeNode.typeAnnotation) {
|
|
80
|
+
return extractBaseTypeName(typeNode.typeAnnotation);
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
default:
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Return true when a TS type annotation (already unwrapped to the inner type
|
|
89
|
+
* node) indicates a ReactNode / ReactElement / KeyedNode / OrNode / JSX.Element.
|
|
90
|
+
*/
|
|
91
|
+
function isReactNodeTypeAnnotation(typeNode) {
|
|
92
|
+
const base = extractBaseTypeName(typeNode);
|
|
93
|
+
if (base === null)
|
|
94
|
+
return false;
|
|
95
|
+
return REACT_NODE_TYPE_NAMES.has(base);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Walk the program body's import declarations to build a mapping from local
|
|
99
|
+
* binding name → { isNamespace: boolean }.
|
|
100
|
+
*
|
|
101
|
+
* isNamespace = true → `import * as Hash from '...'` — we flag Hash.stableHash(...)
|
|
102
|
+
* isNamespace = false → `import { stableHash } from '...'` (possibly aliased)
|
|
103
|
+
*/
|
|
104
|
+
function collectHashBindings(program) {
|
|
105
|
+
const bindings = new Map();
|
|
106
|
+
for (const node of program.body) {
|
|
107
|
+
if (node.type !== utils_1.AST_NODE_TYPES.ImportDeclaration)
|
|
108
|
+
continue;
|
|
109
|
+
if (!isHashModulePath(String(node.source.value)))
|
|
110
|
+
continue;
|
|
111
|
+
for (const spec of node.specifiers) {
|
|
112
|
+
if (spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
|
|
113
|
+
// import * as Hash from '...'
|
|
114
|
+
bindings.set(spec.local.name, { isNamespace: true });
|
|
115
|
+
}
|
|
116
|
+
else if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
|
|
117
|
+
// import { stableHash } or import { stableHash as hash }
|
|
118
|
+
const importedName = spec.imported.type === utils_1.AST_NODE_TYPES.Identifier
|
|
119
|
+
? spec.imported.name
|
|
120
|
+
: spec.imported.value;
|
|
121
|
+
if (HASH_EXPORT_NAMES.has(importedName)) {
|
|
122
|
+
bindings.set(spec.local.name, { isNamespace: false });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return bindings;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Return true when the argument to a stableHash call is a JSX element or
|
|
131
|
+
* JSX fragment.
|
|
132
|
+
*/
|
|
133
|
+
function isJsxNode(node) {
|
|
134
|
+
return (node.type === utils_1.AST_NODE_TYPES.JSXElement ||
|
|
135
|
+
node.type === utils_1.AST_NODE_TYPES.JSXFragment);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Return true when the argument is an object literal that contains a property
|
|
139
|
+
* named `children` or `Node` (shorthand or keyed), signalling it wraps a
|
|
140
|
+
* render-tree value.
|
|
141
|
+
*/
|
|
142
|
+
function objectLiteralContainsNodeProp(node) {
|
|
143
|
+
for (const prop of node.properties) {
|
|
144
|
+
if (prop.type !== utils_1.AST_NODE_TYPES.Property)
|
|
145
|
+
continue;
|
|
146
|
+
const key = prop.key;
|
|
147
|
+
let propName = null;
|
|
148
|
+
if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
149
|
+
propName = key.name;
|
|
150
|
+
}
|
|
151
|
+
else if (key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
152
|
+
typeof key.value === 'string') {
|
|
153
|
+
propName = key.value;
|
|
154
|
+
}
|
|
155
|
+
if (propName !== null && NODE_PROP_NAMES.has(propName)) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Attempt to resolve the type annotation of an identifier from the local
|
|
163
|
+
* scope without the type checker.
|
|
164
|
+
*
|
|
165
|
+
* Strategy (in order):
|
|
166
|
+
* 1. The identifier is a parameter of the immediately enclosing function with
|
|
167
|
+
* a TSTypeAnnotation — read it directly.
|
|
168
|
+
* 2. The identifier is declared as a variable with a type annotation
|
|
169
|
+
* (`const x: ReactNode = ...`).
|
|
170
|
+
*
|
|
171
|
+
* Returns the TSTypeAnnotation typeAnnotation node or null if not found.
|
|
172
|
+
*/
|
|
173
|
+
function resolveAnnotationForIdentifier(identNode) {
|
|
174
|
+
// Walk parents looking for a parameter list or variable declarator that
|
|
175
|
+
// matches the identifier's name.
|
|
176
|
+
let current = identNode.parent;
|
|
177
|
+
while (current) {
|
|
178
|
+
// Function or arrow-function parameter
|
|
179
|
+
if (current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
180
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
181
|
+
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
182
|
+
const fn = current;
|
|
183
|
+
for (const param of fn.params) {
|
|
184
|
+
if (param.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
185
|
+
param.name === identNode.name &&
|
|
186
|
+
param.typeAnnotation?.typeAnnotation) {
|
|
187
|
+
return param.typeAnnotation.typeAnnotation;
|
|
188
|
+
}
|
|
189
|
+
// Handle default param: (node: KeyedNode = ...)
|
|
190
|
+
if (param.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
|
|
191
|
+
param.left.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
192
|
+
param.left.name === identNode.name &&
|
|
193
|
+
param.left.typeAnnotation?.typeAnnotation) {
|
|
194
|
+
const annotation = param.left.typeAnnotation;
|
|
195
|
+
if (annotation) {
|
|
196
|
+
return annotation.typeAnnotation;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Variable declaration in the same scope
|
|
202
|
+
if (current.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
203
|
+
for (const decl of current.declarations) {
|
|
204
|
+
if (decl.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
205
|
+
decl.id.name === identNode.name &&
|
|
206
|
+
decl.id.typeAnnotation?.typeAnnotation) {
|
|
207
|
+
return decl.id.typeAnnotation.typeAnnotation;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
current = current.parent;
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Return true when the argument expression is, or contains, a ReactNode based
|
|
217
|
+
* on purely syntactic (AST-level) evidence:
|
|
218
|
+
*
|
|
219
|
+
* 1. Literal JSX element/fragment.
|
|
220
|
+
* 2. Object literal with a `children` or `Node` property.
|
|
221
|
+
* 3. Identifier whose nearest resolvable TS type annotation names a ReactNode.
|
|
222
|
+
*/
|
|
223
|
+
function argIsReactNode(arg) {
|
|
224
|
+
// Strip as/type-assertion wrappers
|
|
225
|
+
let expr = arg;
|
|
226
|
+
while (expr.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
227
|
+
expr.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
|
|
228
|
+
expr.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
|
|
229
|
+
expr = expr.expression;
|
|
230
|
+
}
|
|
231
|
+
// Case 1: direct JSX
|
|
232
|
+
if (isJsxNode(expr)) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
// Case 2: object literal with node-shaped prop
|
|
236
|
+
if (expr.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
|
|
237
|
+
objectLiteralContainsNodeProp(expr)) {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
// Case 3: identifier with resolvable ReactNode annotation
|
|
241
|
+
if (expr.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
242
|
+
const typeNode = resolveAnnotationForIdentifier(expr);
|
|
243
|
+
if (typeNode && isReactNodeTypeAnnotation(typeNode)) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Rule
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
exports.noStablehashReactNodes = (0, createRule_1.createRule)({
|
|
253
|
+
name: 'no-stablehash-react-nodes',
|
|
254
|
+
meta: {
|
|
255
|
+
type: 'problem',
|
|
256
|
+
docs: {
|
|
257
|
+
description: 'Prevent passing ReactNodes or render-tree values to stableHash(), which deep-stringifies its argument and can freeze the browser.',
|
|
258
|
+
recommended: 'error',
|
|
259
|
+
},
|
|
260
|
+
fixable: undefined,
|
|
261
|
+
schema: [],
|
|
262
|
+
messages: {
|
|
263
|
+
noStableHashReactNode: 'Do not stableHash ReactNodes / KeyedNodes — stableHash() stringifies the full render tree and can freeze the browser. Hash stable keys/ids instead (e.g. stableHash(nodes.map((n) => n.key))).',
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
defaultOptions: [],
|
|
267
|
+
create(context) {
|
|
268
|
+
// Collect hash-function bindings lazily on first CallExpression visit.
|
|
269
|
+
let bindings = null;
|
|
270
|
+
function getBindings() {
|
|
271
|
+
if (bindings === null) {
|
|
272
|
+
const sourceCode = context
|
|
273
|
+
.sourceCode ?? context.getSourceCode();
|
|
274
|
+
bindings = collectHashBindings(sourceCode.ast);
|
|
275
|
+
}
|
|
276
|
+
return bindings;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Determine whether a CallExpression's callee is a tracked stableHash /
|
|
280
|
+
* sortedHash binding and return the first argument if so.
|
|
281
|
+
*/
|
|
282
|
+
function getTrackedCallArg(node) {
|
|
283
|
+
if (node.arguments.length === 0)
|
|
284
|
+
return null;
|
|
285
|
+
const map = getBindings();
|
|
286
|
+
if (map.size === 0)
|
|
287
|
+
return null;
|
|
288
|
+
const callee = node.callee;
|
|
289
|
+
// Direct call: stableHash(arg) or hash(arg) (aliased)
|
|
290
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
291
|
+
const info = map.get(callee.name);
|
|
292
|
+
if (info && !info.isNamespace) {
|
|
293
|
+
return node.arguments[0];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Member expression: Hash.stableHash(arg)
|
|
297
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
298
|
+
!callee.computed &&
|
|
299
|
+
callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
300
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
301
|
+
const objectName = callee.object.name;
|
|
302
|
+
const methodName = callee.property.name;
|
|
303
|
+
const info = map.get(objectName);
|
|
304
|
+
if (info && info.isNamespace && HASH_EXPORT_NAMES.has(methodName)) {
|
|
305
|
+
return node.arguments[0];
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
CallExpression(node) {
|
|
312
|
+
const arg = getTrackedCallArg(node);
|
|
313
|
+
if (arg === null)
|
|
314
|
+
return;
|
|
315
|
+
if (argIsReactNode(arg)) {
|
|
316
|
+
context.report({
|
|
317
|
+
node,
|
|
318
|
+
messageId: 'noStableHashReactNode',
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
//# sourceMappingURL=no-stablehash-react-nodes.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type Options = [
|
|
2
|
+
{
|
|
3
|
+
coordinatorPatterns?: string[];
|
|
4
|
+
rateLimitedPatterns?: string[];
|
|
5
|
+
}
|
|
6
|
+
];
|
|
7
|
+
export declare const parallelizeLoopAwaits: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"parallelizeLoopAwaits", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
8
|
+
export {};
|