@blumintinc/eslint-plugin-blumint 1.12.5 → 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 -20
- 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-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
- package/lib/rules/no-restricted-imports-dynamic.d.ts +0 -25
- package/lib/rules/no-restricted-imports-dynamic.js +0 -213
|
@@ -7,11 +7,11 @@ exports.preferUseMemoOverUseEffectUseState = (0, createRule_1.createRule)({
|
|
|
7
7
|
meta: {
|
|
8
8
|
type: 'suggestion',
|
|
9
9
|
docs: {
|
|
10
|
-
description: 'Prefer useMemo over useEffect + useState for pure computations
|
|
10
|
+
description: 'Prefer useMemo over useEffect + useState for pure computations to avoid extra render cycles and stale derived state.',
|
|
11
11
|
recommended: 'error',
|
|
12
12
|
},
|
|
13
13
|
messages: {
|
|
14
|
-
preferUseMemo: '
|
|
14
|
+
preferUseMemo: 'Derived state "{{stateName}}" is computed inside useEffect and copied into React state even though the value comes from a pure calculation. That extra render cycle and state indirection make components re-render more and risk stale snapshots when dependencies change. Compute the value with useMemo (or inline in render) and read it directly instead of mirroring it into state.',
|
|
15
15
|
},
|
|
16
16
|
schema: [],
|
|
17
17
|
},
|
|
@@ -84,7 +84,8 @@ exports.preferUseMemoOverUseEffectUseState = (0, createRule_1.createRule)({
|
|
|
84
84
|
if (initialValue &&
|
|
85
85
|
isIdentifierReference(initialValue) &&
|
|
86
86
|
isIdentifierReference(setterArgument) &&
|
|
87
|
-
initialValue.name ===
|
|
87
|
+
initialValue.name ===
|
|
88
|
+
setterArgument.name) {
|
|
88
89
|
return true;
|
|
89
90
|
}
|
|
90
91
|
// If the initial value is a function that references a prop and the setter argument
|
|
@@ -111,7 +112,9 @@ exports.preferUseMemoOverUseEffectUseState = (0, createRule_1.createRule)({
|
|
|
111
112
|
const stateName = node.id.elements[0].name;
|
|
112
113
|
const setterName = node.id.elements[1].name;
|
|
113
114
|
const initialValue = node.init.arguments[0] || null;
|
|
114
|
-
|
|
115
|
+
// keep a reference to the defining identifier node
|
|
116
|
+
const defId = node.id.elements[1];
|
|
117
|
+
stateSetters.set(setterName, { stateName, initialValue, defId });
|
|
115
118
|
}
|
|
116
119
|
}
|
|
117
120
|
},
|
|
@@ -133,7 +136,20 @@ exports.preferUseMemoOverUseEffectUseState = (0, createRule_1.createRule)({
|
|
|
133
136
|
statement.expression.callee.type === 'Identifier') {
|
|
134
137
|
const setterName = statement.expression.callee.name;
|
|
135
138
|
const stateInfo = stateSetters.get(setterName);
|
|
136
|
-
|
|
139
|
+
// Verify the setter identifier resolves to the tracked binding
|
|
140
|
+
const calleeId = statement.expression
|
|
141
|
+
.callee;
|
|
142
|
+
const scope = context.getScope();
|
|
143
|
+
const variable = scope.set.get(calleeId.name) ||
|
|
144
|
+
scope.upper?.set.get(calleeId.name);
|
|
145
|
+
const defNode = variable?.defs?.[0]?.name;
|
|
146
|
+
if (!stateInfo ||
|
|
147
|
+
!defNode ||
|
|
148
|
+
defNode.type !== 'Identifier' ||
|
|
149
|
+
defNode.name !== stateInfo.defId.name) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (statement.expression.arguments.length === 1) {
|
|
137
153
|
const computation = statement.expression.arguments[0];
|
|
138
154
|
// Skip if this is a state synchronization pattern
|
|
139
155
|
if (isStateSynchronization(stateInfo.initialValue, computation)) {
|
|
@@ -145,6 +161,9 @@ exports.preferUseMemoOverUseEffectUseState = (0, createRule_1.createRule)({
|
|
|
145
161
|
context.report({
|
|
146
162
|
node,
|
|
147
163
|
messageId: 'preferUseMemo',
|
|
164
|
+
data: {
|
|
165
|
+
stateName: stateInfo.stateName,
|
|
166
|
+
},
|
|
148
167
|
});
|
|
149
168
|
}
|
|
150
169
|
}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
export declare const reactUseMemoShouldBeComponent: TSESLint.RuleModule<"useMemoShouldBeComponent", [], TSESLint.RuleListener>;
|
|
@@ -3,6 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.reactUseMemoShouldBeComponent = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const ARRAY_RETURNING_METHODS = new Set([
|
|
7
|
+
'map',
|
|
8
|
+
'flatMap',
|
|
9
|
+
'filter',
|
|
10
|
+
'concat',
|
|
11
|
+
'slice',
|
|
12
|
+
]);
|
|
6
13
|
/**
|
|
7
14
|
* Checks if a node is a JSX element or fragment
|
|
8
15
|
*/
|
|
@@ -24,6 +31,99 @@ const isJsxElement = (node) => {
|
|
|
24
31
|
return (node.type === utils_1.AST_NODE_TYPES.JSXElement ||
|
|
25
32
|
node.type === utils_1.AST_NODE_TYPES.JSXFragment);
|
|
26
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* True if the resolved variable is referenced inside a JSX prop (attribute or spread).
|
|
36
|
+
*/
|
|
37
|
+
const isUsedAsComponentProp = (context, variableName, node) => {
|
|
38
|
+
// Find containing function for proper scope acquisition
|
|
39
|
+
let current = node;
|
|
40
|
+
let fn;
|
|
41
|
+
while (current?.parent) {
|
|
42
|
+
current = current.parent;
|
|
43
|
+
if (current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
44
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
45
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
46
|
+
fn = current;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const sourceCode = context.getSourceCode();
|
|
51
|
+
const scopeManager = sourceCode.scopeManager;
|
|
52
|
+
const scope = scopeManager &&
|
|
53
|
+
((fn && scopeManager.acquire(fn)) || scopeManager.acquire(node));
|
|
54
|
+
if (!scopeManager || !scope) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
const variable = scope.variables.find((v) => v.name === variableName) ??
|
|
58
|
+
scope.upper?.variables.find((v) => v.name === variableName) ??
|
|
59
|
+
null;
|
|
60
|
+
if (!variable) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
for (const ref of variable.references) {
|
|
64
|
+
let p = ref.identifier.parent ?? null;
|
|
65
|
+
while (p) {
|
|
66
|
+
if (p.type === utils_1.AST_NODE_TYPES.JSXAttribute ||
|
|
67
|
+
p.type === utils_1.AST_NODE_TYPES.JSXSpreadAttribute) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
p = p.parent;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
};
|
|
75
|
+
const isUsedAsJsxChild = (context, variableName, node) => {
|
|
76
|
+
// Traverse the nearest function (or program) to see if the identifier appears
|
|
77
|
+
// as a JSX child expression.
|
|
78
|
+
const sourceCode = context.getSourceCode();
|
|
79
|
+
let container = sourceCode.ast;
|
|
80
|
+
let current = node;
|
|
81
|
+
while (current?.parent) {
|
|
82
|
+
current = current.parent;
|
|
83
|
+
if (current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
84
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
85
|
+
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
86
|
+
container = current;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
let found = false;
|
|
91
|
+
const visited = new Set();
|
|
92
|
+
const visit = (n) => {
|
|
93
|
+
if (found || visited.has(n))
|
|
94
|
+
return;
|
|
95
|
+
visited.add(n);
|
|
96
|
+
if (n.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
|
|
97
|
+
n.expression.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
98
|
+
n.expression.name === variableName &&
|
|
99
|
+
n.parent &&
|
|
100
|
+
(n.parent.type === utils_1.AST_NODE_TYPES.JSXElement ||
|
|
101
|
+
n.parent.type === utils_1.AST_NODE_TYPES.JSXFragment) &&
|
|
102
|
+
n.parent.children.includes(n)) {
|
|
103
|
+
found = true;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
for (const key of Object.keys(n)) {
|
|
107
|
+
if (key === 'parent')
|
|
108
|
+
continue;
|
|
109
|
+
const value = n[key];
|
|
110
|
+
if (!value)
|
|
111
|
+
continue;
|
|
112
|
+
if (Array.isArray(value)) {
|
|
113
|
+
for (const child of value) {
|
|
114
|
+
if (child && typeof child.type === 'string') {
|
|
115
|
+
visit(child);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (value && typeof value.type === 'string') {
|
|
120
|
+
visit(value);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
visit(container);
|
|
125
|
+
return found;
|
|
126
|
+
};
|
|
27
127
|
/**
|
|
28
128
|
* Checks if a variable is used multiple times in a component
|
|
29
129
|
*/
|
|
@@ -149,28 +249,13 @@ const containsJsxInFunction = (node) => {
|
|
|
149
249
|
if (body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
150
250
|
return containsJsxInBlockStatement(body);
|
|
151
251
|
}
|
|
152
|
-
// Check for array methods returning JSX
|
|
252
|
+
// Check for array methods returning arrays (do not flag arrays of JSX)
|
|
153
253
|
if (body.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
154
254
|
if (body.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
155
255
|
body.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
'filter',
|
|
160
|
-
'find',
|
|
161
|
-
'findIndex',
|
|
162
|
-
'some',
|
|
163
|
-
'every',
|
|
164
|
-
'reduce',
|
|
165
|
-
];
|
|
166
|
-
if (arrayMethods.includes(body.callee.property.name) &&
|
|
167
|
-
body.arguments.length > 0) {
|
|
168
|
-
const callback = body.arguments[0];
|
|
169
|
-
if ((callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
170
|
-
callback.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
|
|
171
|
-
containsJsxInFunction(callback)) {
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
256
|
+
if (ARRAY_RETURNING_METHODS.has(body.callee.property.name)) {
|
|
257
|
+
// Returning an array (even if its elements are JSX) should not be flagged
|
|
258
|
+
return false;
|
|
174
259
|
}
|
|
175
260
|
}
|
|
176
261
|
// Check for IIFE
|
|
@@ -239,70 +324,27 @@ const containsJsxInExpression = (node) => {
|
|
|
239
324
|
}
|
|
240
325
|
return containsJsxInObject(node);
|
|
241
326
|
case utils_1.AST_NODE_TYPES.CallExpression:
|
|
242
|
-
//
|
|
327
|
+
// Do not flag arrays of JSX: common array-producing patterns
|
|
243
328
|
if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
244
|
-
node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
245
|
-
node.callee.property.name
|
|
246
|
-
|
|
247
|
-
const callback = node.arguments[0];
|
|
248
|
-
if (callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
249
|
-
callback.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
250
|
-
// Check if the callback returns an object with both JSX and non-JSX properties
|
|
251
|
-
if (callback.body.type !== utils_1.AST_NODE_TYPES.BlockStatement &&
|
|
252
|
-
callback.body.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
253
|
-
let hasNonJsxProperties = false;
|
|
254
|
-
let hasJsxProperties = false;
|
|
255
|
-
for (const property of callback.body.properties) {
|
|
256
|
-
if (property.type === utils_1.AST_NODE_TYPES.Property && property.value) {
|
|
257
|
-
if (isJsxElement(property.value)) {
|
|
258
|
-
hasJsxProperties = true;
|
|
259
|
-
}
|
|
260
|
-
else if (property.value.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
261
|
-
hasNonJsxProperties = true;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
// If the object has both JSX and non-JSX properties, it's likely a data object
|
|
266
|
-
if (hasNonJsxProperties && hasJsxProperties) {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
// For other cases, check if the function returns JSX directly
|
|
271
|
-
return containsJsxInFunction(callback);
|
|
329
|
+
node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
330
|
+
if (ARRAY_RETURNING_METHODS.has(node.callee.property.name)) {
|
|
331
|
+
return false;
|
|
272
332
|
}
|
|
273
333
|
}
|
|
334
|
+
// Array.from(...) returns an array as well
|
|
335
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
336
|
+
node.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
337
|
+
node.callee.object.name === 'Array' &&
|
|
338
|
+
node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
339
|
+
node.callee.property.name === 'from') {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
274
342
|
// Check if it's an IIFE
|
|
275
343
|
if (node.callee.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
276
344
|
node.callee.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
277
345
|
return containsJsxInFunction(node.callee);
|
|
278
346
|
}
|
|
279
|
-
//
|
|
280
|
-
if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
281
|
-
node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
282
|
-
const arrayMethods = [
|
|
283
|
-
'filter',
|
|
284
|
-
'find',
|
|
285
|
-
'findIndex',
|
|
286
|
-
'some',
|
|
287
|
-
'every',
|
|
288
|
-
'reduce',
|
|
289
|
-
];
|
|
290
|
-
if (arrayMethods.includes(node.callee.property.name) &&
|
|
291
|
-
node.arguments.length > 0) {
|
|
292
|
-
const callback = node.arguments[0];
|
|
293
|
-
if (callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
294
|
-
callback.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
295
|
-
return containsJsxInFunction(callback);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
// Check arguments for JSX
|
|
300
|
-
for (const arg of node.arguments) {
|
|
301
|
-
if (arg.type !== utils_1.AST_NODE_TYPES.SpreadElement &&
|
|
302
|
-
containsJsxInExpression(arg)) {
|
|
303
|
-
return true;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
347
|
+
// Do not flag based on JSX in arguments of non-IIFE calls
|
|
306
348
|
return false;
|
|
307
349
|
case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
|
|
308
350
|
case utils_1.AST_NODE_TYPES.FunctionExpression:
|
|
@@ -371,8 +413,15 @@ const containsJsxInBlockStatement = (node) => {
|
|
|
371
413
|
// Check variable declarations for JSX
|
|
372
414
|
if (statement.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
373
415
|
for (const declarator of statement.declarations) {
|
|
374
|
-
if (declarator.init
|
|
375
|
-
return
|
|
416
|
+
if (declarator.init) {
|
|
417
|
+
// Ignore nested function expressions (helpers/callbacks) even if they return JSX
|
|
418
|
+
if (declarator.init.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
419
|
+
declarator.init.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (containsJsxInExpression(declarator.init)) {
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
376
425
|
}
|
|
377
426
|
}
|
|
378
427
|
}
|
|
@@ -441,7 +490,7 @@ exports.reactUseMemoShouldBeComponent = (0, createRule_1.createRule)({
|
|
|
441
490
|
},
|
|
442
491
|
schema: [],
|
|
443
492
|
messages: {
|
|
444
|
-
useMemoShouldBeComponent: 'useMemo
|
|
493
|
+
useMemoShouldBeComponent: 'useMemo result "{{memoName}}" returns JSX, which hides a component inside a memoized value. JSX needs component identity so React can manage props, state, and dev tooling; wrapping it in useMemo bypasses that boundary and makes reuse/debugging harder. Extract this JSX into its own component and memoize it with React.memo if stability is required.',
|
|
445
494
|
},
|
|
446
495
|
},
|
|
447
496
|
defaultOptions: [],
|
|
@@ -454,15 +503,32 @@ exports.reactUseMemoShouldBeComponent = (0, createRule_1.createRule)({
|
|
|
454
503
|
node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
455
504
|
node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
456
505
|
const variableName = node.parent.id.name;
|
|
506
|
+
// Only report when the memoized value is used as a JSX child
|
|
507
|
+
if (!isUsedAsJsxChild(context, variableName, node)) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
457
510
|
// Check if the variable is used multiple times in the component
|
|
458
511
|
if (isUsedMultipleTimes(variableName, node)) {
|
|
459
512
|
// If the variable is used multiple times, allow it
|
|
460
513
|
return;
|
|
461
514
|
}
|
|
515
|
+
// Check if the variable is used as a prop value in a JSX element
|
|
516
|
+
if (isUsedAsComponentProp(context, variableName, node)) {
|
|
517
|
+
// If the variable is used as a prop value, allow it
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
462
520
|
}
|
|
521
|
+
const memoName = node.parent &&
|
|
522
|
+
node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
523
|
+
node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier
|
|
524
|
+
? node.parent.id.name
|
|
525
|
+
: 'useMemo return value';
|
|
463
526
|
context.report({
|
|
464
527
|
node,
|
|
465
528
|
messageId: 'useMemoShouldBeComponent',
|
|
529
|
+
data: {
|
|
530
|
+
memoName,
|
|
531
|
+
},
|
|
466
532
|
});
|
|
467
533
|
}
|
|
468
534
|
},
|
|
@@ -14,7 +14,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
14
14
|
fixable: 'code',
|
|
15
15
|
schema: [],
|
|
16
16
|
messages: {
|
|
17
|
-
requireDynamicImport: 'Firebase
|
|
17
|
+
requireDynamicImport: 'Static Firebase import from "{{importSource}}" keeps the Firebase SDK in the initial bundle and blocks route-level code splitting. Use an `await import(\'{{importSource}}\')` dynamic import so Firebase loads only on the code path that needs it; type-only imports remain allowed.',
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
defaultOptions: [],
|
|
@@ -72,6 +72,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
72
72
|
context.report({
|
|
73
73
|
node,
|
|
74
74
|
messageId: 'requireDynamicImport',
|
|
75
|
+
data: { importSource },
|
|
75
76
|
fix(fixer) {
|
|
76
77
|
const dynamicImport = createDynamicImport(node);
|
|
77
78
|
return fixer.replaceText(node, dynamicImport);
|
|
@@ -24,7 +24,7 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
24
24
|
},
|
|
25
25
|
],
|
|
26
26
|
messages: {
|
|
27
|
-
useImageOptimized: 'Use ImageOptimized
|
|
27
|
+
useImageOptimized: 'Use ImageOptimized from {{ componentPath }} instead of {{ component }}. The shared wrapper handles responsive sizing, lazy loading, and blur placeholders so images stay optimized and do not hurt Core Web Vitals. Replace this usage with ImageOptimized to send the asset through the optimization pipeline.',
|
|
28
28
|
},
|
|
29
29
|
},
|
|
30
30
|
defaultOptions: [{ componentPath: 'src/components/image/ImageOptimized' }],
|
|
@@ -66,9 +66,17 @@ function checkFunction(context, node) {
|
|
|
66
66
|
isUnmemoizedExportedFunctionComponent,
|
|
67
67
|
].map((fn) => fn(parentNode, node));
|
|
68
68
|
if (results.some((result) => !!result)) {
|
|
69
|
+
const componentName = (node.type === 'FunctionDeclaration' && node.id?.name) ||
|
|
70
|
+
(parentNode.type === 'VariableDeclarator' &&
|
|
71
|
+
parentNode.id.type === 'Identifier' &&
|
|
72
|
+
parentNode.id.name) ||
|
|
73
|
+
'component';
|
|
69
74
|
context.report({
|
|
70
75
|
node,
|
|
71
76
|
messageId: 'requireMemo',
|
|
77
|
+
data: {
|
|
78
|
+
name: componentName,
|
|
79
|
+
},
|
|
72
80
|
fix: results[2] || results[1]
|
|
73
81
|
? function fix(fixer) {
|
|
74
82
|
const sourceCode = context.getSourceCode();
|
|
@@ -158,7 +166,9 @@ exports.requireMemo = {
|
|
|
158
166
|
recommended: 'error',
|
|
159
167
|
},
|
|
160
168
|
messages: {
|
|
161
|
-
requireMemo: 'Component
|
|
169
|
+
requireMemo: 'Component "{{name}}" renders JSX with props but is not wrapped in memo(). ' +
|
|
170
|
+
'Without memo the component function is recreated on every parent render, breaking referential equality and causing avoidable child re-renders. ' +
|
|
171
|
+
'Wrap the component with memo from util/memo so callers receive a stable reference; rename to "{{name}}Unmemoized" if it must stay un-memoized.',
|
|
162
172
|
},
|
|
163
173
|
schema: [],
|
|
164
174
|
fixable: 'code',
|
|
@@ -11,7 +11,7 @@ exports.requireUseMemoObjectLiterals = (0, createRule_1.createRule)({
|
|
|
11
11
|
recommended: 'error',
|
|
12
12
|
},
|
|
13
13
|
messages: {
|
|
14
|
-
requireUseMemo: 'Inline
|
|
14
|
+
requireUseMemo: 'Inline {{literalType}} literal passed to {{componentName}} prop "{{propName}}" is recreated on every render, producing a new reference that forces child components to re-render even when values stay the same. Wrap the literal in useMemo (or hoist a memoized constant) so the prop reference remains stable between renders.',
|
|
15
15
|
},
|
|
16
16
|
schema: [],
|
|
17
17
|
},
|
|
@@ -46,9 +46,15 @@ exports.requireUseMemoObjectLiterals = (0, createRule_1.createRule)({
|
|
|
46
46
|
? jsxElement.name.name
|
|
47
47
|
: '';
|
|
48
48
|
if (elementName && /^[A-Z]/.test(elementName)) {
|
|
49
|
+
const literalType = expression.type === 'ObjectExpression' ? 'object' : 'array';
|
|
49
50
|
context.report({
|
|
50
51
|
node: expression,
|
|
51
52
|
messageId: 'requireUseMemo',
|
|
53
|
+
data: {
|
|
54
|
+
literalType,
|
|
55
|
+
propName: typeof propName === 'string' ? propName : 'prop',
|
|
56
|
+
componentName: elementName,
|
|
57
|
+
},
|
|
52
58
|
});
|
|
53
59
|
}
|
|
54
60
|
}
|
|
@@ -24,17 +24,43 @@ const SUGGESTED_ALTERNATIVES = {
|
|
|
24
24
|
process: ['transform', 'sanitize', 'compute'],
|
|
25
25
|
do: ['execute', 'perform', 'apply'],
|
|
26
26
|
};
|
|
27
|
+
function extractFirstWord(name) {
|
|
28
|
+
let firstWord = name;
|
|
29
|
+
for (let i = 1; i < name.length; i++) {
|
|
30
|
+
const currentChar = name[i];
|
|
31
|
+
const prevChar = name[i - 1];
|
|
32
|
+
// lowercase -> Uppercase (camel/Pascal boundary)
|
|
33
|
+
if (prevChar >= 'a' &&
|
|
34
|
+
prevChar <= 'z' &&
|
|
35
|
+
currentChar >= 'A' &&
|
|
36
|
+
currentChar <= 'Z') {
|
|
37
|
+
firstWord = name.substring(0, i);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
// UPPERCASE acronym -> lowercase word start (e.g., XML|Http)
|
|
41
|
+
if (prevChar >= 'A' &&
|
|
42
|
+
prevChar <= 'Z' &&
|
|
43
|
+
currentChar >= 'a' &&
|
|
44
|
+
currentChar <= 'z') {
|
|
45
|
+
if (i > 1) {
|
|
46
|
+
firstWord = name.substring(0, i - 1);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return firstWord;
|
|
52
|
+
}
|
|
27
53
|
exports.semanticFunctionPrefixes = (0, createRule_1.createRule)({
|
|
28
54
|
name: 'semantic-function-prefixes',
|
|
29
55
|
meta: {
|
|
30
56
|
type: 'suggestion',
|
|
31
57
|
docs: {
|
|
32
|
-
description: '
|
|
58
|
+
description: 'Require semantic function prefixes instead of generic verbs so callers know whether a function fetches data, transforms input, or mutates state',
|
|
33
59
|
recommended: 'error',
|
|
34
60
|
},
|
|
35
61
|
schema: [],
|
|
36
62
|
messages: {
|
|
37
|
-
avoidGenericPrefix: '
|
|
63
|
+
avoidGenericPrefix: 'Function "{{functionName}}" starts with the generic prefix "{{prefix}}", which hides whether it fetches remote data, transforms input, or mutates state. Use a semantic verb such as {{alternatives}} to describe the operation and set caller expectations.',
|
|
38
64
|
},
|
|
39
65
|
},
|
|
40
66
|
defaultOptions: [],
|
|
@@ -54,20 +80,17 @@ exports.semanticFunctionPrefixes = (0, createRule_1.createRule)({
|
|
|
54
80
|
if (NEXTJS_DATA_FUNCTIONS.has(methodName))
|
|
55
81
|
return;
|
|
56
82
|
// Extract first word from PascalCase/camelCase
|
|
57
|
-
|
|
58
|
-
for (let i = 1; i < methodName.length; i++) {
|
|
59
|
-
if (methodName[i] >= 'A' && methodName[i] <= 'Z') {
|
|
60
|
-
firstWord = methodName.substring(0, i);
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
83
|
+
const firstWord = extractFirstWord(methodName);
|
|
64
84
|
// Check for disallowed prefixes
|
|
85
|
+
// Only flag if the disallowed word is used as a prefix (not the entire name)
|
|
65
86
|
for (const prefix of DISALLOWED_PREFIXES) {
|
|
66
|
-
if (firstWord.toLowerCase() === prefix.toLowerCase()
|
|
87
|
+
if (firstWord.toLowerCase() === prefix.toLowerCase() &&
|
|
88
|
+
firstWord.length < methodName.length) {
|
|
67
89
|
context.report({
|
|
68
90
|
node: node.key,
|
|
69
91
|
messageId: 'avoidGenericPrefix',
|
|
70
92
|
data: {
|
|
93
|
+
functionName: methodName,
|
|
71
94
|
prefix,
|
|
72
95
|
alternatives: SUGGESTED_ALTERNATIVES[prefix].join(', '),
|
|
73
96
|
},
|
|
@@ -99,20 +122,17 @@ exports.semanticFunctionPrefixes = (0, createRule_1.createRule)({
|
|
|
99
122
|
if (NEXTJS_DATA_FUNCTIONS.has(functionName))
|
|
100
123
|
return;
|
|
101
124
|
// Extract first word from PascalCase/camelCase
|
|
102
|
-
|
|
103
|
-
for (let i = 1; i < functionName.length; i++) {
|
|
104
|
-
if (functionName[i] >= 'A' && functionName[i] <= 'Z') {
|
|
105
|
-
firstWord = functionName.substring(0, i);
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
125
|
+
const firstWord = extractFirstWord(functionName);
|
|
109
126
|
// Check for disallowed prefixes
|
|
127
|
+
// Only flag if the disallowed word is used as a prefix (not the entire name)
|
|
110
128
|
for (const prefix of DISALLOWED_PREFIXES) {
|
|
111
|
-
if (firstWord.toLowerCase() === prefix.toLowerCase()
|
|
129
|
+
if (firstWord.toLowerCase() === prefix.toLowerCase() &&
|
|
130
|
+
firstWord.length < functionName.length) {
|
|
112
131
|
context.report({
|
|
113
132
|
node: node.id || node,
|
|
114
133
|
messageId: 'avoidGenericPrefix',
|
|
115
134
|
data: {
|
|
135
|
+
functionName,
|
|
116
136
|
prefix,
|
|
117
137
|
alternatives: SUGGESTED_ALTERNATIVES[prefix].join(', '),
|
|
118
138
|
},
|
|
@@ -14,7 +14,7 @@ exports.syncOnwriteNameFunc = (0, createRule_1.createRule)({
|
|
|
14
14
|
fixable: 'code',
|
|
15
15
|
schema: [],
|
|
16
16
|
messages: {
|
|
17
|
-
mismatchedName: 'The name field
|
|
17
|
+
mismatchedName: 'OnWrite handler name "{{nameValue}}" does not match func reference "{{funcName}}". The name field is what gets registered for deploys, logs, and alerts, so a mismatch hides which function is actually running. Rename the "name" value to "{{funcName}}" or point "func" to a function named "{{nameValue}}" so the trigger label and implementation stay in sync.',
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
defaultOptions: [],
|
|
@@ -68,6 +68,10 @@ exports.syncOnwriteNameFunc = (0, createRule_1.createRule)({
|
|
|
68
68
|
context.report({
|
|
69
69
|
node: nameProperty,
|
|
70
70
|
messageId: 'mismatchedName',
|
|
71
|
+
data: {
|
|
72
|
+
nameValue,
|
|
73
|
+
funcName,
|
|
74
|
+
},
|
|
71
75
|
fix(fixer) {
|
|
72
76
|
return fixer.replaceText(value, `'${funcName}'`);
|
|
73
77
|
},
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const testFileLocationEnforcement: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"misplacedTestFile", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.testFileLocationEnforcement = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const createRule_1 = require("../utils/createRule");
|
|
10
|
+
const TEST_FILE_PATTERN = /\.test\.tsx?$/i;
|
|
11
|
+
const SUPPORTED_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
|
|
12
|
+
exports.testFileLocationEnforcement = (0, createRule_1.createRule)({
|
|
13
|
+
name: 'test-file-location-enforcement',
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'suggestion',
|
|
16
|
+
docs: {
|
|
17
|
+
description: 'Enforce colocating *.test.ts(x) files with the code they cover.',
|
|
18
|
+
recommended: 'error',
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
messages: {
|
|
22
|
+
misplacedTestFile: 'Test file "{{testFile}}" is not colocated with its subject. Keep tests in the same directory as {{expectedNames}} so refactors move code and coverage together and engineers can find the implementation without searching separate test folders.',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultOptions: [],
|
|
26
|
+
create(context) {
|
|
27
|
+
return {
|
|
28
|
+
Program(node) {
|
|
29
|
+
const filename = context.getFilename();
|
|
30
|
+
if (filename === '<input>' ||
|
|
31
|
+
filename === '<text>' ||
|
|
32
|
+
filename.includes('node_modules') ||
|
|
33
|
+
!TEST_FILE_PATTERN.test(filename)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const directory = path_1.default.dirname(filename);
|
|
37
|
+
const testFileName = path_1.default.basename(filename);
|
|
38
|
+
const baseName = testFileName.replace(TEST_FILE_PATTERN, '');
|
|
39
|
+
const candidates = SUPPORTED_EXTENSIONS.map((extension) => path_1.default.join(directory, `${baseName}${extension}`));
|
|
40
|
+
const hasSibling = candidates.some((candidate) => fs_1.default.existsSync(candidate));
|
|
41
|
+
if (hasSibling) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const relativePath = path_1.default.isAbsolute(filename)
|
|
45
|
+
? path_1.default.relative(process.cwd(), filename) || filename
|
|
46
|
+
: filename;
|
|
47
|
+
const expectedNames = SUPPORTED_EXTENSIONS.map((extension) => `"${baseName}${extension}"`).join(' or ');
|
|
48
|
+
context.report({
|
|
49
|
+
node,
|
|
50
|
+
messageId: 'misplacedTestFile',
|
|
51
|
+
data: { testFile: relativePath, expectedNames },
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
//# sourceMappingURL=test-file-location-enforcement.js.map
|
|
@@ -13,7 +13,7 @@ exports.useCustomLink = (0, createRule_1.createRule)({
|
|
|
13
13
|
fixable: 'code',
|
|
14
14
|
schema: [],
|
|
15
15
|
messages: {
|
|
16
|
-
useCustomLink: 'Import
|
|
16
|
+
useCustomLink: 'Import "{{localName}}" from src/components/Link instead of next/link. The custom Link wraps Next.js navigation with design system defaults and analytics hooks; importing next/link bypasses those wrappers and leads to inconsistent styling and missing instrumentation. Replace the import source with src/components/Link so routing uses the shared wrapper.',
|
|
17
17
|
},
|
|
18
18
|
},
|
|
19
19
|
defaultOptions: [],
|
|
@@ -30,6 +30,11 @@ exports.useCustomLink = (0, createRule_1.createRule)({
|
|
|
30
30
|
context.report({
|
|
31
31
|
node,
|
|
32
32
|
messageId: 'useCustomLink',
|
|
33
|
+
data: {
|
|
34
|
+
localName: defaultSpecifier?.local?.name ||
|
|
35
|
+
defaultAsSpecifier?.local?.name ||
|
|
36
|
+
'Link',
|
|
37
|
+
},
|
|
33
38
|
fix(fixer) {
|
|
34
39
|
// Get the local name of the imported Link component
|
|
35
40
|
const localName = defaultSpecifier?.local?.name ||
|