@blumintinc/eslint-plugin-blumint 1.16.2 → 1.17.1
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/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 +18 -26
- 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/react-memoize-literals.js +106 -1
- 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,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.preferFlatTransformEachKeys = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
// Propagation strategy shape signals: an object literal with one of these
|
|
7
|
+
// properties (besides transformEach) is treated as a propagation strategy.
|
|
8
|
+
const STRATEGY_SHAPE_KEYS = new Set([
|
|
9
|
+
'resolveAll',
|
|
10
|
+
'queryResolveAll',
|
|
11
|
+
'numericFieldPathConfig',
|
|
12
|
+
'upsert',
|
|
13
|
+
'sourceDeletionOverride',
|
|
14
|
+
]);
|
|
15
|
+
function isObjectExpression(node) {
|
|
16
|
+
return !!node && node.type === utils_1.AST_NODE_TYPES.ObjectExpression;
|
|
17
|
+
}
|
|
18
|
+
function isProperty(node) {
|
|
19
|
+
return node.type === utils_1.AST_NODE_TYPES.Property;
|
|
20
|
+
}
|
|
21
|
+
// Return the string name of a non-computed property key, or null.
|
|
22
|
+
function getStaticKeyName(prop) {
|
|
23
|
+
if (prop.computed)
|
|
24
|
+
return null;
|
|
25
|
+
if (prop.key.type === utils_1.AST_NODE_TYPES.Identifier)
|
|
26
|
+
return prop.key.name;
|
|
27
|
+
if (prop.key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
28
|
+
typeof prop.key.value === 'string')
|
|
29
|
+
return prop.key.value;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
// A key is a "dot-notation key" if it contains a dot (e.g., 'foo.bar').
|
|
33
|
+
// Such keys represent leaf paths — their values can be any shape.
|
|
34
|
+
function isDotNotationKey(keyName) {
|
|
35
|
+
return keyName.includes('.');
|
|
36
|
+
}
|
|
37
|
+
// Determine whether an object-expression property key is a computed key.
|
|
38
|
+
// Computed keys (bracket notation, including template literals) represent
|
|
39
|
+
// dynamic leaf paths and are therefore exempt from the flat-key requirement.
|
|
40
|
+
// Template literal keys always have `computed: true` in the AST, so a single
|
|
41
|
+
// check on `prop.computed` is sufficient.
|
|
42
|
+
function isComputedKey(prop) {
|
|
43
|
+
return prop.computed;
|
|
44
|
+
}
|
|
45
|
+
// Find a direct Property child by name inside an ObjectExpression.
|
|
46
|
+
function findProp(obj, name) {
|
|
47
|
+
for (const p of obj.properties) {
|
|
48
|
+
if (!isProperty(p))
|
|
49
|
+
continue;
|
|
50
|
+
const k = getStaticKeyName(p);
|
|
51
|
+
if (k === name)
|
|
52
|
+
return p;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
// Determine whether an ObjectExpression looks like a propagation strategy:
|
|
57
|
+
// it has a `transformEach` property AND at least one of the known strategy
|
|
58
|
+
// shape keys.
|
|
59
|
+
function isStrategyObject(obj) {
|
|
60
|
+
let hasTransformEach = false;
|
|
61
|
+
let hasStrategyKey = false;
|
|
62
|
+
for (const p of obj.properties) {
|
|
63
|
+
if (!isProperty(p))
|
|
64
|
+
continue;
|
|
65
|
+
const k = getStaticKeyName(p);
|
|
66
|
+
if (!k)
|
|
67
|
+
continue;
|
|
68
|
+
if (k === 'transformEach')
|
|
69
|
+
hasTransformEach = true;
|
|
70
|
+
if (STRATEGY_SHAPE_KEYS.has(k))
|
|
71
|
+
hasStrategyKey = true;
|
|
72
|
+
}
|
|
73
|
+
return hasTransformEach && hasStrategyKey;
|
|
74
|
+
}
|
|
75
|
+
// Return true if the strategy uses resolveSelf (deleted-source short-circuit,
|
|
76
|
+
// so nested objects are safe). Checks: resolveAll's value is an Identifier
|
|
77
|
+
// named 'resolveSelf'.
|
|
78
|
+
function usesResolveSelf(obj) {
|
|
79
|
+
const resolveAllProp = findProp(obj, 'resolveAll');
|
|
80
|
+
if (!resolveAllProp)
|
|
81
|
+
return false;
|
|
82
|
+
const val = resolveAllProp.value;
|
|
83
|
+
return val.type === utils_1.AST_NODE_TYPES.Identifier && val.name === 'resolveSelf';
|
|
84
|
+
}
|
|
85
|
+
// Extract the effective "data" object from a return value:
|
|
86
|
+
// - If the returned object has an `afterData` property, return its value
|
|
87
|
+
// (since the outer object is a structural wrapper, not a propagation map).
|
|
88
|
+
// - Otherwise return the object itself.
|
|
89
|
+
function getDataObject(obj) {
|
|
90
|
+
const afterDataProp = findProp(obj, 'afterData');
|
|
91
|
+
if (afterDataProp) {
|
|
92
|
+
const val = afterDataProp.value;
|
|
93
|
+
if (isObjectExpression(val))
|
|
94
|
+
return val;
|
|
95
|
+
// afterData exists but its value isn't a literal (e.g. a variable) — skip.
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return obj;
|
|
99
|
+
}
|
|
100
|
+
// Check whether a variable-binding return follows the pattern:
|
|
101
|
+
// const result = { ... };
|
|
102
|
+
// return result;
|
|
103
|
+
// Returns the initialiser ObjectExpression if found, otherwise null.
|
|
104
|
+
function resolveVariableBinding(returnArg, funcBody) {
|
|
105
|
+
if (returnArg.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
106
|
+
return null;
|
|
107
|
+
const varName = returnArg.name;
|
|
108
|
+
// Walk the statements in the function body looking for a single const/let
|
|
109
|
+
// initialisation whose id matches varName.
|
|
110
|
+
for (const stmt of funcBody.body) {
|
|
111
|
+
if (stmt.type !== utils_1.AST_NODE_TYPES.VariableDeclaration)
|
|
112
|
+
continue;
|
|
113
|
+
for (const decl of stmt.declarations) {
|
|
114
|
+
if (decl.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
115
|
+
decl.id.name === varName &&
|
|
116
|
+
decl.init &&
|
|
117
|
+
isObjectExpression(decl.init)) {
|
|
118
|
+
return decl.init;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
// Report nested-object properties in a data object (the actual propagation
|
|
125
|
+
// map, after afterData unwrapping). A property is flagged when:
|
|
126
|
+
// - its key is a simple non-dot-notation identifier/string
|
|
127
|
+
// - its value is an ObjectExpression
|
|
128
|
+
function checkDataObject(dataObj, report) {
|
|
129
|
+
for (const prop of dataObj.properties) {
|
|
130
|
+
if (!isProperty(prop))
|
|
131
|
+
continue;
|
|
132
|
+
if (isComputedKey(prop))
|
|
133
|
+
continue;
|
|
134
|
+
const keyName = getStaticKeyName(prop);
|
|
135
|
+
if (keyName === null)
|
|
136
|
+
continue;
|
|
137
|
+
// Dot-notation string keys represent leaf paths — their values may be
|
|
138
|
+
// any shape (the value IS the leaf data).
|
|
139
|
+
if (isDotNotationKey(keyName))
|
|
140
|
+
continue;
|
|
141
|
+
// Flag when the value is a nested object literal.
|
|
142
|
+
if (isObjectExpression(prop.value)) {
|
|
143
|
+
report(prop);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Analyse a function body (BlockStatement) for return statements that produce
|
|
148
|
+
// nested objects, and report each violation.
|
|
149
|
+
function analyzeBlockBody(body, report) {
|
|
150
|
+
for (const stmt of body.body) {
|
|
151
|
+
if (stmt.type !== utils_1.AST_NODE_TYPES.ReturnStatement)
|
|
152
|
+
continue;
|
|
153
|
+
if (!stmt.argument)
|
|
154
|
+
continue;
|
|
155
|
+
let retObj = null;
|
|
156
|
+
if (isObjectExpression(stmt.argument)) {
|
|
157
|
+
retObj = stmt.argument;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// Try single-binding pattern: const x = {...}; return x;
|
|
161
|
+
retObj = resolveVariableBinding(stmt.argument, body);
|
|
162
|
+
}
|
|
163
|
+
if (!retObj)
|
|
164
|
+
continue;
|
|
165
|
+
const dataObj = getDataObject(retObj);
|
|
166
|
+
if (!dataObj)
|
|
167
|
+
continue;
|
|
168
|
+
checkDataObject(dataObj, report);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
exports.preferFlatTransformEachKeys = (0, createRule_1.createRule)({
|
|
172
|
+
name: 'prefer-flat-transform-each-keys',
|
|
173
|
+
meta: {
|
|
174
|
+
type: 'suggestion',
|
|
175
|
+
docs: {
|
|
176
|
+
description: 'Warn when transformEach in a propagation strategy returns nested object values instead of flat dot-notation keys.',
|
|
177
|
+
recommended: 'warn',
|
|
178
|
+
},
|
|
179
|
+
fixable: undefined,
|
|
180
|
+
schema: [],
|
|
181
|
+
messages: {
|
|
182
|
+
preferFlatTransformEachKeys: "Avoid returning nested objects from transformEach. Use flat dot-notation keys instead (e.g., 'parent.child.key' instead of { parent: { child: { key } } }). Nested objects cause FieldValue.delete() at the parent key on source deletion, wiping the entire sub-tree instead of just the propagated fields. If nesting is intentional (e.g., wiping an entire sub-tree), disable this rule with a comment explaining why.",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
defaultOptions: [],
|
|
186
|
+
create(context) {
|
|
187
|
+
// Core detection is purely syntactic and works without type info.
|
|
188
|
+
// The parserServices guard below mirrors no-entire-object-hook-deps.ts so
|
|
189
|
+
// the rule degrades gracefully in test environments without parserOptions.project.
|
|
190
|
+
// Currently only the syntactic path is exercised; type-aware enhancement
|
|
191
|
+
// (e.g. resolving identifier types) can be wired in here when needed.
|
|
192
|
+
void context.parserServices; // accessed for future type-aware enhancement
|
|
193
|
+
return {
|
|
194
|
+
// Visit every ObjectExpression. If it looks like a propagation strategy,
|
|
195
|
+
// find the transformEach function and check its return values.
|
|
196
|
+
ObjectExpression(node) {
|
|
197
|
+
if (!isStrategyObject(node))
|
|
198
|
+
return;
|
|
199
|
+
if (usesResolveSelf(node))
|
|
200
|
+
return;
|
|
201
|
+
const transformEachProp = findProp(node, 'transformEach');
|
|
202
|
+
if (!transformEachProp)
|
|
203
|
+
return;
|
|
204
|
+
const fn = transformEachProp.value;
|
|
205
|
+
const report = (violatingNode) => {
|
|
206
|
+
context.report({
|
|
207
|
+
node: violatingNode,
|
|
208
|
+
messageId: 'preferFlatTransformEachKeys',
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
if (fn.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
212
|
+
fn.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
213
|
+
const body = fn.body;
|
|
214
|
+
if (isObjectExpression(body)) {
|
|
215
|
+
// Arrow function with implicit return: () => ({ ... })
|
|
216
|
+
const dataObj = getDataObject(body);
|
|
217
|
+
if (dataObj)
|
|
218
|
+
checkDataObject(dataObj, report);
|
|
219
|
+
}
|
|
220
|
+
else if (body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
221
|
+
analyzeBlockBody(body, report);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
//# sourceMappingURL=prefer-flat-transform-each-keys.js.map
|
|
@@ -34,10 +34,17 @@ const DEFAULT_IGNORED_METHODS = [
|
|
|
34
34
|
'parse',
|
|
35
35
|
'stringify',
|
|
36
36
|
];
|
|
37
|
+
/**
|
|
38
|
+
* Builder/factory terminal methods whose external callers (in other files)
|
|
39
|
+
* would break if converted to getters. These are imperative actions, not
|
|
40
|
+
* computed properties — per origin issue #990 edge case #4.
|
|
41
|
+
*/
|
|
42
|
+
const DEFAULT_FACTORY_METHODS = ['build', 'create', 'make'];
|
|
37
43
|
const SIDE_EFFECT_TAGS = ['sideEffect', 'sideEffects', 'mutates'];
|
|
38
44
|
const DEFAULT_OPTIONS = {
|
|
39
45
|
stripPrefixes: DEFAULT_PREFIXES,
|
|
40
46
|
ignoredMethods: DEFAULT_IGNORED_METHODS,
|
|
47
|
+
factoryMethods: DEFAULT_FACTORY_METHODS,
|
|
41
48
|
ignoreAsync: true,
|
|
42
49
|
ignoreVoidReturn: true,
|
|
43
50
|
ignoreAbstract: true,
|
|
@@ -166,6 +173,10 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
166
173
|
type: 'array',
|
|
167
174
|
items: { type: 'string' },
|
|
168
175
|
},
|
|
176
|
+
factoryMethods: {
|
|
177
|
+
type: 'array',
|
|
178
|
+
items: { type: 'string' },
|
|
179
|
+
},
|
|
169
180
|
ignoreAsync: {
|
|
170
181
|
type: 'boolean',
|
|
171
182
|
},
|
|
@@ -199,8 +210,10 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
199
210
|
...userOptions,
|
|
200
211
|
stripPrefixes: userOptions.stripPrefixes ?? DEFAULT_OPTIONS.stripPrefixes,
|
|
201
212
|
ignoredMethods: userOptions.ignoredMethods ?? DEFAULT_OPTIONS.ignoredMethods,
|
|
213
|
+
factoryMethods: userOptions.factoryMethods ?? DEFAULT_OPTIONS.factoryMethods,
|
|
202
214
|
};
|
|
203
215
|
const ignoredMethods = new Set(config.ignoredMethods);
|
|
216
|
+
const factoryMethods = new Set(config.factoryMethods);
|
|
204
217
|
const prefixList = config.stripPrefixes;
|
|
205
218
|
const sourceCode = context.getSourceCode();
|
|
206
219
|
const ignoredTraversalKeys = new Set(['parent', 'loc', 'range']);
|
|
@@ -357,6 +370,28 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
357
370
|
}
|
|
358
371
|
return false;
|
|
359
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* Returns true when the method body contains a ThrowStatement that is
|
|
375
|
+
* directly in the method's own scope (not inside a nested function/arrow).
|
|
376
|
+
* A method that can throw at the top level is an imperative assertion, not
|
|
377
|
+
* a computed property, so it must not become a getter.
|
|
378
|
+
*/
|
|
379
|
+
function containsTopLevelThrow(body) {
|
|
380
|
+
const stack = [...body.body];
|
|
381
|
+
while (stack.length) {
|
|
382
|
+
const current = stack.pop();
|
|
383
|
+
// Do not descend into nested function-like scopes — a throw there is
|
|
384
|
+
// that nested function's concern, not the method's.
|
|
385
|
+
if (isFunctionLikeNode(current)) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (current.type === utils_1.AST_NODE_TYPES.ThrowStatement) {
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
pushChildNodes(current, stack);
|
|
392
|
+
}
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
360
395
|
function suggestName(name) {
|
|
361
396
|
for (const prefix of prefixList) {
|
|
362
397
|
if (name.startsWith(prefix) &&
|
|
@@ -562,6 +597,11 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
562
597
|
const name = node.key.name;
|
|
563
598
|
if (ignoredMethods.has(name))
|
|
564
599
|
return;
|
|
600
|
+
// Factory/builder terminal methods are imperative actions (issue #990 #4).
|
|
601
|
+
// Exemption takes precedence over stripPrefixes so names like "build"
|
|
602
|
+
// are never prefix-stripped into a getter candidate.
|
|
603
|
+
if (factoryMethods.has(name))
|
|
604
|
+
return;
|
|
565
605
|
if (hasOverloadSignatures(node))
|
|
566
606
|
return;
|
|
567
607
|
const body = node.value.body;
|
|
@@ -572,6 +612,10 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
572
612
|
return;
|
|
573
613
|
if (config.respectJsDocSideEffects && hasSideEffectTag(node))
|
|
574
614
|
return;
|
|
615
|
+
// A method that throws at the top level is an imperative assertion or
|
|
616
|
+
// command, not a computed property — getters must be pure/non-throwing.
|
|
617
|
+
if (containsTopLevelThrow(body))
|
|
618
|
+
return;
|
|
575
619
|
const sideEffectReason = analyzeMutations(body);
|
|
576
620
|
const suggestedName = suggestName(name);
|
|
577
621
|
candidates.push({ node, sideEffectReason, suggestedName });
|