@blumintinc/eslint-plugin-blumint 1.20.95 → 1.20.97
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 +1 -1
- package/lib/index.js +1 -1
- package/lib/rules/enforce-boolean-naming-prefixes.js +38 -3
- package/lib/rules/enforce-css-media-queries.d.ts +2 -1
- package/lib/rules/enforce-css-media-queries.js +203 -8
- package/lib/rules/enforce-exported-function-types.js +209 -6
- package/lib/rules/enforce-firestore-doc-ref-generic.js +71 -18
- package/lib/rules/enforce-mui-rounded-icons.d.ts +2 -1
- package/lib/rules/enforce-mui-rounded-icons.js +194 -13
- package/lib/rules/enforce-positive-naming.js +112 -24
- package/lib/rules/generic-starts-with-t.js +49 -0
- package/lib/rules/global-const-style.js +101 -51
- package/lib/rules/no-always-true-false-conditions.js +231 -62
- package/lib/rules/no-unnecessary-verb-suffix.js +52 -5
- package/lib/rules/parallelize-loop-awaits.d.ts +1 -0
- package/lib/rules/parallelize-loop-awaits.js +233 -43
- package/lib/rules/prefer-getter-over-parameterless-method.d.ts +2 -1
- package/lib/rules/prefer-getter-over-parameterless-method.js +259 -0
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +63 -4
- package/lib/rules/require-https-error.js +6 -2
- package/lib/rules/use-custom-link.js +45 -1
- package/lib/rules/use-custom-memo.js +30 -0
- package/lib/rules/use-custom-router.js +32 -2
- package/lib/utils/harvestRuleTesterCases.d.ts +48 -0
- package/lib/utils/harvestRuleTesterCases.js +168 -0
- package/package.json +1 -1
- package/release-manifest.json +151 -0
|
@@ -152,6 +152,232 @@ function hasOverloadSignatures(node) {
|
|
|
152
152
|
return !hasBody && key.name === targetName;
|
|
153
153
|
});
|
|
154
154
|
}
|
|
155
|
+
function getClassOf(node) {
|
|
156
|
+
const classBody = node.parent;
|
|
157
|
+
if (!classBody || classBody.type !== utils_1.AST_NODE_TYPES.ClassBody) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const classNode = classBody.parent;
|
|
161
|
+
if (classNode &&
|
|
162
|
+
(classNode.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
|
|
163
|
+
classNode.type === utils_1.AST_NODE_TYPES.ClassExpression)) {
|
|
164
|
+
return classNode;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* A member typed as a function is as binding as a method signature: a getter
|
|
170
|
+
* returning the function's *result* is not assignable to the function type, so
|
|
171
|
+
* such a member is a contract match too.
|
|
172
|
+
*/
|
|
173
|
+
function isFunctionTypeNode(node) {
|
|
174
|
+
if (!node)
|
|
175
|
+
return false;
|
|
176
|
+
switch (node.type) {
|
|
177
|
+
case utils_1.AST_NODE_TYPES.TSFunctionType:
|
|
178
|
+
case utils_1.AST_NODE_TYPES.TSConstructorType:
|
|
179
|
+
return true;
|
|
180
|
+
case utils_1.AST_NODE_TYPES.TSUnionType:
|
|
181
|
+
case utils_1.AST_NODE_TYPES.TSIntersectionType:
|
|
182
|
+
return node.types.some(isFunctionTypeNode);
|
|
183
|
+
default:
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function knownMemberName(key, computed) {
|
|
188
|
+
if (!key || computed)
|
|
189
|
+
return null;
|
|
190
|
+
if (key.type === utils_1.AST_NODE_TYPES.Identifier)
|
|
191
|
+
return key.name;
|
|
192
|
+
if (key.type === utils_1.AST_NODE_TYPES.Literal && typeof key.value === 'string') {
|
|
193
|
+
return key.value;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
function findVariableInScopeChain(scope, name) {
|
|
198
|
+
let current = scope;
|
|
199
|
+
while (current) {
|
|
200
|
+
const variable = current.variables.find((entry) => entry.name === name);
|
|
201
|
+
if (variable) {
|
|
202
|
+
return variable;
|
|
203
|
+
}
|
|
204
|
+
current = current.upper;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
function collectTypeElementMembers(elements, out) {
|
|
209
|
+
for (const element of elements) {
|
|
210
|
+
if (element.type === utils_1.AST_NODE_TYPES.TSMethodSignature) {
|
|
211
|
+
// A `get`/`set` signature already describes property access, so a getter
|
|
212
|
+
// implementation satisfies it — only call signatures bind.
|
|
213
|
+
if (element.kind === 'get' || element.kind === 'set')
|
|
214
|
+
continue;
|
|
215
|
+
const name = knownMemberName(element.key, element.computed);
|
|
216
|
+
if (name)
|
|
217
|
+
out.instance.add(name);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (element.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
|
|
221
|
+
isFunctionTypeNode(element.typeAnnotation?.typeAnnotation)) {
|
|
222
|
+
const name = knownMemberName(element.key, element.computed);
|
|
223
|
+
if (name)
|
|
224
|
+
out.instance.add(name);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function collectClassContractMembers(classNode, out) {
|
|
229
|
+
for (const member of classNode.body.body) {
|
|
230
|
+
if (member.type === utils_1.AST_NODE_TYPES.StaticBlock)
|
|
231
|
+
continue;
|
|
232
|
+
const isStatic = member.static ?? false;
|
|
233
|
+
const target = isStatic ? out.staticSide : out.instance;
|
|
234
|
+
if (member.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
|
|
235
|
+
member.type === utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition) {
|
|
236
|
+
if (member.kind !== 'method')
|
|
237
|
+
continue;
|
|
238
|
+
const name = knownMemberName(member.key, member.computed);
|
|
239
|
+
if (name)
|
|
240
|
+
target.add(name);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if ((member.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
|
|
244
|
+
member.type === utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition) &&
|
|
245
|
+
isFunctionTypeNode(member.typeAnnotation?.typeAnnotation)) {
|
|
246
|
+
const name = knownMemberName(member.key, member.computed);
|
|
247
|
+
if (name)
|
|
248
|
+
target.add(name);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function collectContractFromName(name, scope, seen, out) {
|
|
253
|
+
if (seen.has(name))
|
|
254
|
+
return;
|
|
255
|
+
seen.add(name);
|
|
256
|
+
const variable = findVariableInScopeChain(scope, name);
|
|
257
|
+
// No binding at all (a global/ambient type) or a binding with no declaration
|
|
258
|
+
// node (a lib type) leaves the contract unknowable from this file.
|
|
259
|
+
if (!variable || variable.defs.length === 0) {
|
|
260
|
+
out.resolved = false;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
for (const def of variable.defs) {
|
|
264
|
+
const declaration = def.node;
|
|
265
|
+
switch (declaration.type) {
|
|
266
|
+
case utils_1.AST_NODE_TYPES.TSInterfaceDeclaration:
|
|
267
|
+
collectContractFromInterface(declaration, scope, seen, out);
|
|
268
|
+
break;
|
|
269
|
+
case utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration:
|
|
270
|
+
collectContractFromTypeNode(declaration.typeAnnotation, scope, seen, out);
|
|
271
|
+
break;
|
|
272
|
+
case utils_1.AST_NODE_TYPES.ClassDeclaration:
|
|
273
|
+
case utils_1.AST_NODE_TYPES.ClassExpression:
|
|
274
|
+
collectContractFromClass(declaration, scope, seen, out);
|
|
275
|
+
break;
|
|
276
|
+
default:
|
|
277
|
+
// An import binding, an enum, a value — nothing whose members this file
|
|
278
|
+
// can enumerate.
|
|
279
|
+
out.resolved = false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function collectContractFromHeritage(heritage, scope, seen, out) {
|
|
284
|
+
// A qualified reference (`ns.Type`) names a declaration this lookup cannot
|
|
285
|
+
// reach, so the contract stays unresolved.
|
|
286
|
+
if (heritage.expression.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
287
|
+
out.resolved = false;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
collectContractFromName(heritage.expression.name, scope, seen, out);
|
|
291
|
+
}
|
|
292
|
+
function collectContractFromInterface(declaration, scope, seen, out) {
|
|
293
|
+
collectTypeElementMembers(declaration.body.body, out);
|
|
294
|
+
for (const heritage of [
|
|
295
|
+
...(declaration.extends ?? []),
|
|
296
|
+
...(declaration.implements ?? []),
|
|
297
|
+
]) {
|
|
298
|
+
collectContractFromHeritage(heritage, scope, seen, out);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function collectContractFromClass(declaration, scope, seen, out) {
|
|
302
|
+
collectClassContractMembers(declaration, out);
|
|
303
|
+
if (declaration.superClass) {
|
|
304
|
+
if (declaration.superClass.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
305
|
+
collectContractFromName(declaration.superClass.name, scope, seen, out);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
// A mixin call or member expression base class: unknowable members.
|
|
309
|
+
out.resolved = false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (const heritage of declaration.implements ?? []) {
|
|
313
|
+
collectContractFromHeritage(heritage, scope, seen, out);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function collectContractFromTypeNode(typeNode, scope, seen, out) {
|
|
317
|
+
switch (typeNode.type) {
|
|
318
|
+
case utils_1.AST_NODE_TYPES.TSTypeLiteral:
|
|
319
|
+
collectTypeElementMembers(typeNode.members, out);
|
|
320
|
+
return;
|
|
321
|
+
case utils_1.AST_NODE_TYPES.TSIntersectionType:
|
|
322
|
+
for (const part of typeNode.types) {
|
|
323
|
+
collectContractFromTypeNode(part, scope, seen, out);
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
case utils_1.AST_NODE_TYPES.TSTypeReference:
|
|
327
|
+
if (typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
328
|
+
collectContractFromName(typeNode.typeName.name, scope, seen, out);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
out.resolved = false;
|
|
332
|
+
return;
|
|
333
|
+
default:
|
|
334
|
+
// Mapped, conditional, indexed-access and utility-type shapes describe
|
|
335
|
+
// members this rule cannot enumerate syntactically.
|
|
336
|
+
out.resolved = false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Resolves what the class's `extends`/`implements` clauses oblige its members
|
|
341
|
+
* to look like, using only declarations visible in the file being linted.
|
|
342
|
+
*/
|
|
343
|
+
function resolveHeritageContract(classNode, scope) {
|
|
344
|
+
const heritageNames = [];
|
|
345
|
+
let hasOpaqueHeritage = false;
|
|
346
|
+
if (classNode.superClass) {
|
|
347
|
+
if (classNode.superClass.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
348
|
+
heritageNames.push(classNode.superClass.name);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
hasOpaqueHeritage = true;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
for (const implemented of classNode.implements ?? []) {
|
|
355
|
+
if (implemented.expression.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
356
|
+
heritageNames.push(implemented.expression.name);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
hasOpaqueHeritage = true;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (!heritageNames.length && !hasOpaqueHeritage) {
|
|
363
|
+
return { status: 'none' };
|
|
364
|
+
}
|
|
365
|
+
if (hasOpaqueHeritage) {
|
|
366
|
+
return { status: 'unresolvable' };
|
|
367
|
+
}
|
|
368
|
+
const members = {
|
|
369
|
+
instance: new Set(),
|
|
370
|
+
staticSide: new Set(),
|
|
371
|
+
resolved: true,
|
|
372
|
+
};
|
|
373
|
+
const seen = new Set();
|
|
374
|
+
for (const name of heritageNames) {
|
|
375
|
+
collectContractFromName(name, scope, seen, members);
|
|
376
|
+
}
|
|
377
|
+
return members.resolved
|
|
378
|
+
? { status: 'resolved', members }
|
|
379
|
+
: { status: 'unresolvable' };
|
|
380
|
+
}
|
|
155
381
|
exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
156
382
|
name: 'prefer-getter-over-parameterless-method',
|
|
157
383
|
meta: {
|
|
@@ -535,6 +761,37 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
535
761
|
}
|
|
536
762
|
}
|
|
537
763
|
}
|
|
764
|
+
const heritageContracts = new WeakMap();
|
|
765
|
+
/**
|
|
766
|
+
* A method that satisfies an `implements` clause or overrides a base-class
|
|
767
|
+
* member cannot become a getter: the heritage type declares a *method*, so
|
|
768
|
+
* the conversion the rule prescribes is a TS2416/TS2417 compile error. When
|
|
769
|
+
* every heritage reference resolves in-file the exemption is per method —
|
|
770
|
+
* only names the contract declares are spared. When any reference leaves
|
|
771
|
+
* the file (an import, a global, a third-party `.d.ts`, a mixin call) the
|
|
772
|
+
* contract's members are unknowable, so no method of that class can be
|
|
773
|
+
* proven safe and the whole class is spared: a false negative is preferable
|
|
774
|
+
* to prescribing a remedy that does not compile.
|
|
775
|
+
*/
|
|
776
|
+
function isConstrainedByHeritage(node) {
|
|
777
|
+
const classNode = getClassOf(node);
|
|
778
|
+
if (!classNode)
|
|
779
|
+
return false;
|
|
780
|
+
let contract = heritageContracts.get(classNode);
|
|
781
|
+
if (!contract) {
|
|
782
|
+
contract = resolveHeritageContract(classNode, context.getScope());
|
|
783
|
+
heritageContracts.set(classNode, contract);
|
|
784
|
+
}
|
|
785
|
+
if (contract.status === 'none')
|
|
786
|
+
return false;
|
|
787
|
+
if (contract.status === 'unresolvable')
|
|
788
|
+
return true;
|
|
789
|
+
const name = node.key.name;
|
|
790
|
+
const isStatic = node.static ?? false;
|
|
791
|
+
return isStatic
|
|
792
|
+
? contract.members.staticSide.has(name)
|
|
793
|
+
: contract.members.instance.has(name);
|
|
794
|
+
}
|
|
538
795
|
const callUsedNamesByClass = new WeakMap();
|
|
539
796
|
const callUsedNamesInFile = new Set();
|
|
540
797
|
const candidates = [];
|
|
@@ -594,6 +851,8 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
594
851
|
return;
|
|
595
852
|
if (!node.value.body)
|
|
596
853
|
return;
|
|
854
|
+
if (isConstrainedByHeritage(node))
|
|
855
|
+
return;
|
|
597
856
|
const name = node.key.name;
|
|
598
857
|
if (ignoredMethods.has(name))
|
|
599
858
|
return;
|
|
@@ -100,7 +100,8 @@ function collectReferencedTypeNames(node, acc = new Set()) {
|
|
|
100
100
|
collectReferencedTypeNames(mapped.typeAnnotation, acc);
|
|
101
101
|
if (mapped.nameType)
|
|
102
102
|
collectReferencedTypeNames(mapped.nameType, acc);
|
|
103
|
-
if (mapped.typeParameter &&
|
|
103
|
+
if (mapped.typeParameter &&
|
|
104
|
+
mapped.typeParameter.constraint) {
|
|
104
105
|
collectReferencedTypeNames(mapped.typeParameter.constraint, acc);
|
|
105
106
|
}
|
|
106
107
|
break;
|
|
@@ -162,6 +163,61 @@ function collectReferencedTypeNames(node, acc = new Set()) {
|
|
|
162
163
|
}
|
|
163
164
|
return acc;
|
|
164
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Type-level operators that keep a `typeof` query in the position where the
|
|
168
|
+
* alias derives its own type from the constant, rather than moving the query
|
|
169
|
+
* into a member, parameter or declaration slot. Unions, intersections, `keyof`,
|
|
170
|
+
* element extraction, array wrappers and utility application all still describe
|
|
171
|
+
* "this alias IS the constant's type", which is the remedy this rule asks for.
|
|
172
|
+
*
|
|
173
|
+
* Utility application belongs here for a convergence reason: the remedy for a
|
|
174
|
+
* reported `function f(x: Readonly<typeof CONST>)` is to name that type once as
|
|
175
|
+
* `type T = Readonly<typeof CONST>`. Reporting the extracted alias too would
|
|
176
|
+
* make the remedy its own violation, leaving that class of code with nowhere to
|
|
177
|
+
* land.
|
|
178
|
+
*/
|
|
179
|
+
function isAliasDerivationWrapper(parent, child) {
|
|
180
|
+
switch (parent.type) {
|
|
181
|
+
case utils_1.AST_NODE_TYPES.TSUnionType:
|
|
182
|
+
case utils_1.AST_NODE_TYPES.TSIntersectionType:
|
|
183
|
+
case utils_1.AST_NODE_TYPES.TSTypeOperator:
|
|
184
|
+
case utils_1.AST_NODE_TYPES.TSArrayType:
|
|
185
|
+
case utils_1.AST_NODE_TYPES.TSTypeReference:
|
|
186
|
+
case utils_1.AST_NODE_TYPES.TSTypeParameterInstantiation:
|
|
187
|
+
return true;
|
|
188
|
+
case utils_1.AST_NODE_TYPES.TSIndexedAccessType:
|
|
189
|
+
// Only the object side derives from the constant. `Foo[typeof KEY]` reads
|
|
190
|
+
// the constant as a lookup key, which is a consumer position like any
|
|
191
|
+
// other annotation.
|
|
192
|
+
return parent.objectType === child;
|
|
193
|
+
default:
|
|
194
|
+
return isParenthesizedType(parent);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* True when the query defines the alias itself — `type T = typeof CONST` and the
|
|
199
|
+
* derivation wrappers around it, such as `keyof typeof CONST`,
|
|
200
|
+
* `(typeof CONST)[number]` or `Readonly<typeof CONST>` (Issues #1117, #1175).
|
|
201
|
+
* A query sitting in a member, index signature, function-type parameter,
|
|
202
|
+
* conditional branch, mapped-type constraint or type-parameter default inside the
|
|
203
|
+
* alias body is a use site and stays reportable, matching what the same shape
|
|
204
|
+
* does outside an alias.
|
|
205
|
+
*/
|
|
206
|
+
function isCanonicalAliasDerivation(node) {
|
|
207
|
+
let child = node;
|
|
208
|
+
let parent = node.parent;
|
|
209
|
+
while (parent) {
|
|
210
|
+
if (parent.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
211
|
+
return parent.typeAnnotation === child;
|
|
212
|
+
}
|
|
213
|
+
if (!isAliasDerivationWrapper(parent, child)) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
child = parent;
|
|
217
|
+
parent = parent.parent;
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
165
221
|
/** Collects module-level consts and type aliases for quick lookup */
|
|
166
222
|
function collectTopLevelContext(program) {
|
|
167
223
|
const topLevelConstInitByName = new Map();
|
|
@@ -292,9 +348,12 @@ exports.preferTypeAliasOverTypeofConstant = (0, createRule_1.createRule)({
|
|
|
292
348
|
if (!collected)
|
|
293
349
|
return;
|
|
294
350
|
const ancestors = ASTHelpers_1.ASTHelpers.getAncestors(context, node);
|
|
295
|
-
// Skip
|
|
296
|
-
//
|
|
297
|
-
|
|
351
|
+
// Skip the alias definition itself (Issue #1117, #1175), which is the
|
|
352
|
+
// canonical way to name the constant's type. The exemption covers the
|
|
353
|
+
// declared type of the alias and its derivation wrappers only; a query
|
|
354
|
+
// buried in an alias member is a use site and reports like the same
|
|
355
|
+
// shape written on an interface or a parameter (Issue #1680).
|
|
356
|
+
if (isCanonicalAliasDerivation(node)) {
|
|
298
357
|
return;
|
|
299
358
|
}
|
|
300
359
|
// Skip `keyof typeof X` as it's a canonical way to derive a union of keys from a constant object.
|
|
@@ -26,8 +26,12 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
26
26
|
},
|
|
27
27
|
schema: [],
|
|
28
28
|
messages: {
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// The remedy names the module SHAPE, never a package name: the proprietary
|
|
30
|
+
// wrapper lives inside the consuming repository (a shared
|
|
31
|
+
// util/errors/HttpsError), so prescribing an npm specifier sends the
|
|
32
|
+
// reader to install something that does not exist (issue #1685).
|
|
33
|
+
useHttpsError: 'Throwing "{{constructorName}}" in Cloud Functions returns a generic 500 and drops the structured status code clients rely on. Throw the proprietary HttpsError instead so responses include the correct status, sanitized message, and logging context. Import it from the proprietary HttpsError module this codebase owns, such as a shared util/errors/HttpsError, rather than from firebase-admin.',
|
|
34
|
+
useProprietaryHttpsError: '{{reference}} comes from {{source}} and bypasses our proprietary HttpsError wrapper, so responses skip standardized status codes, logging, and client-safe payloads. Import HttpsError from the proprietary HttpsError module this codebase owns, such as a shared util/errors/HttpsError, and throw that to keep errors consistent.',
|
|
31
35
|
},
|
|
32
36
|
},
|
|
33
37
|
defaultOptions: [],
|
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.useCustomLink = void 0;
|
|
4
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
const LINK_MODULE_PATH = 'src/components/Link';
|
|
6
|
+
/**
|
|
7
|
+
* MUI's documented Next.js integration names the component that adapts
|
|
8
|
+
* `next/link` for the wrapper `NextLinkComposed`, and `src/components/Link`
|
|
9
|
+
* renders it. Keying on the basename alone — rather than a full path — matches
|
|
10
|
+
* wherever the integration component is colocated.
|
|
11
|
+
*/
|
|
12
|
+
const INTEGRATION_COMPONENT = 'NextLinkComposed';
|
|
13
|
+
const SOURCE_EXTENSION = /\.(?:ts|tsx|js|jsx)$/;
|
|
14
|
+
/**
|
|
15
|
+
* The two modules that implement the wrapper are the ones that must keep
|
|
16
|
+
* importing `next/link`: `src/components/Link` is the module the fixer points
|
|
17
|
+
* every other import at, and it renders `NextLinkComposed`. Rewriting either
|
|
18
|
+
* one manufactures a cycle — `Link` importing itself, or
|
|
19
|
+
* `Link → NextLinkComposed → Link` — so the wrapper evaluates circularly and
|
|
20
|
+
* every consumer of it breaks.
|
|
21
|
+
*
|
|
22
|
+
* The linted path is matched by suffix because it reaches the rule in whatever
|
|
23
|
+
* form the caller used — absolute (`/repo/src/components/Link.tsx`) or
|
|
24
|
+
* project-relative (`src/components/Link.tsx`). The suffix has to land on a
|
|
25
|
+
* path-segment boundary, otherwise `notsrc/components/Link.tsx` — an unrelated
|
|
26
|
+
* module — would be exempted too, and the integration component is matched by
|
|
27
|
+
* basename equality so that `MyNextLinkComposed.tsx` stays reportable.
|
|
28
|
+
*/
|
|
29
|
+
const isWrapperImplementation = (filename) => {
|
|
30
|
+
const normalized = filename.replace(/\\/g, '/').replace(SOURCE_EXTENSION, '');
|
|
31
|
+
const basename = normalized.slice(normalized.lastIndexOf('/') + 1);
|
|
32
|
+
if (basename === INTEGRATION_COMPONENT) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (!normalized.endsWith(LINK_MODULE_PATH)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const suffixStart = normalized.length - LINK_MODULE_PATH.length;
|
|
39
|
+
return suffixStart === 0 || normalized[suffixStart - 1] === '/';
|
|
40
|
+
};
|
|
5
41
|
exports.useCustomLink = (0, createRule_1.createRule)({
|
|
6
42
|
name: 'use-custom-link',
|
|
7
43
|
meta: {
|
|
@@ -18,6 +54,14 @@ exports.useCustomLink = (0, createRule_1.createRule)({
|
|
|
18
54
|
},
|
|
19
55
|
defaultOptions: [],
|
|
20
56
|
create(context) {
|
|
57
|
+
// A processor hands the rule a virtual filename for an extracted code block;
|
|
58
|
+
// the physical path is the one that identifies the module on disk.
|
|
59
|
+
const filename = context.getPhysicalFilename
|
|
60
|
+
? context.getPhysicalFilename()
|
|
61
|
+
: context.getFilename();
|
|
62
|
+
if (isWrapperImplementation(filename)) {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
21
65
|
return {
|
|
22
66
|
ImportDeclaration(node) {
|
|
23
67
|
if (node.source.value === 'next/link') {
|
|
@@ -41,7 +85,7 @@ exports.useCustomLink = (0, createRule_1.createRule)({
|
|
|
41
85
|
defaultAsSpecifier?.local?.name ||
|
|
42
86
|
'Link';
|
|
43
87
|
// Create the new import statement
|
|
44
|
-
const newImport = `import ${localName} from '
|
|
88
|
+
const newImport = `import ${localName} from '${LINK_MODULE_PATH}';`;
|
|
45
89
|
return fixer.replaceText(node, newImport);
|
|
46
90
|
},
|
|
47
91
|
});
|
|
@@ -4,6 +4,28 @@ exports.useCustomMemo = void 0;
|
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
6
|
const MEMO_MODULE = `'src/util/memo'`;
|
|
7
|
+
/** `MEMO_MODULE` carries the quotes the fixer emits; a path never does. */
|
|
8
|
+
const MEMO_MODULE_PATH = MEMO_MODULE.slice(1, -1);
|
|
9
|
+
const SOURCE_EXTENSION = /\.(?:ts|tsx|js|jsx)$/;
|
|
10
|
+
/**
|
|
11
|
+
* The module the fixer points every `memo` import at is the one module that must
|
|
12
|
+
* keep importing `memo` from `react`: rewriting it makes it import itself, and a
|
|
13
|
+
* self-import evaluates circularly, so the wrapper exports `undefined` and every
|
|
14
|
+
* consumer of it breaks.
|
|
15
|
+
*
|
|
16
|
+
* The linted path is matched by suffix because it reaches the rule in whatever
|
|
17
|
+
* form the caller used — absolute (`/repo/src/util/memo.ts`) or project-relative
|
|
18
|
+
* (`src/util/memo.ts`). The suffix has to land on a path-segment boundary,
|
|
19
|
+
* otherwise `notsrc/util/memo.ts` — an unrelated module — would be exempted too.
|
|
20
|
+
*/
|
|
21
|
+
const isMemoModule = (filename) => {
|
|
22
|
+
const normalized = filename.replace(/\\/g, '/').replace(SOURCE_EXTENSION, '');
|
|
23
|
+
if (!normalized.endsWith(MEMO_MODULE_PATH)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const suffixStart = normalized.length - MEMO_MODULE_PATH.length;
|
|
27
|
+
return suffixStart === 0 || normalized[suffixStart - 1] === '/';
|
|
28
|
+
};
|
|
7
29
|
const isComment = (token) => token.type === utils_1.AST_TOKEN_TYPES.Line || token.type === utils_1.AST_TOKEN_TYPES.Block;
|
|
8
30
|
const isMemoSpecifier = (specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
9
31
|
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
@@ -162,6 +184,14 @@ exports.useCustomMemo = (0, createRule_1.createRule)({
|
|
|
162
184
|
},
|
|
163
185
|
defaultOptions: [],
|
|
164
186
|
create(context) {
|
|
187
|
+
// A processor hands the rule a virtual filename for an extracted code block;
|
|
188
|
+
// the physical path is the one that identifies the module on disk.
|
|
189
|
+
const filename = context.getPhysicalFilename
|
|
190
|
+
? context.getPhysicalFilename()
|
|
191
|
+
: context.getFilename();
|
|
192
|
+
if (isMemoModule(filename)) {
|
|
193
|
+
return {};
|
|
194
|
+
}
|
|
165
195
|
return {
|
|
166
196
|
ImportDeclaration(node) {
|
|
167
197
|
if (node.source.value !== 'react') {
|
|
@@ -3,6 +3,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.useCustomRouter = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const ROUTER_MODULE_PATH = 'src/hooks/routing/useRouter';
|
|
7
|
+
const SOURCE_EXTENSION = /\.(?:ts|tsx|js|jsx)$/;
|
|
8
|
+
/**
|
|
9
|
+
* The module the fixer points every `useRouter` import at is the one module that
|
|
10
|
+
* must keep importing `useRouter` from `next/router`: rewriting it makes it
|
|
11
|
+
* import itself, and a self-import evaluates circularly, so the wrapper exports
|
|
12
|
+
* `undefined` and every consumer of it breaks.
|
|
13
|
+
*
|
|
14
|
+
* The linted path is matched by suffix because it reaches the rule in whatever
|
|
15
|
+
* form the caller used — absolute (`/repo/src/hooks/routing/useRouter.ts`) or
|
|
16
|
+
* project-relative (`src/hooks/routing/useRouter.ts`). The suffix has to land on
|
|
17
|
+
* a path-segment boundary, otherwise `notsrc/hooks/routing/useRouter.ts` — an
|
|
18
|
+
* unrelated module — would be exempted too.
|
|
19
|
+
*/
|
|
20
|
+
const isRouterModule = (filename) => {
|
|
21
|
+
const normalized = filename.replace(/\\/g, '/').replace(SOURCE_EXTENSION, '');
|
|
22
|
+
if (!normalized.endsWith(ROUTER_MODULE_PATH)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const suffixStart = normalized.length - ROUTER_MODULE_PATH.length;
|
|
26
|
+
return suffixStart === 0 || normalized[suffixStart - 1] === '/';
|
|
27
|
+
};
|
|
6
28
|
exports.useCustomRouter = (0, createRule_1.createRule)({
|
|
7
29
|
name: 'use-custom-router',
|
|
8
30
|
meta: {
|
|
@@ -19,6 +41,14 @@ exports.useCustomRouter = (0, createRule_1.createRule)({
|
|
|
19
41
|
},
|
|
20
42
|
defaultOptions: [],
|
|
21
43
|
create(context) {
|
|
44
|
+
// A processor hands the rule a virtual filename for an extracted code block;
|
|
45
|
+
// the physical path is the one that identifies the module on disk.
|
|
46
|
+
const filename = context.getPhysicalFilename
|
|
47
|
+
? context.getPhysicalFilename()
|
|
48
|
+
: context.getFilename();
|
|
49
|
+
if (isRouterModule(filename)) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
22
52
|
return {
|
|
23
53
|
ImportDeclaration(node) {
|
|
24
54
|
if (node.source.value === 'next/router') {
|
|
@@ -45,7 +75,7 @@ exports.useCustomRouter = (0, createRule_1.createRule)({
|
|
|
45
75
|
.map((s) => s.local.name !== s.imported.name
|
|
46
76
|
? `useRouter as ${s.local.name}`
|
|
47
77
|
: 'useRouter')
|
|
48
|
-
.join(', ')} } from '
|
|
78
|
+
.join(', ')} } from '${ROUTER_MODULE_PATH}';`);
|
|
49
79
|
}
|
|
50
80
|
else {
|
|
51
81
|
// Create a new import for useRouter and keep other imports
|
|
@@ -53,7 +83,7 @@ exports.useCustomRouter = (0, createRule_1.createRule)({
|
|
|
53
83
|
.map((s) => s.local.name !== s.imported.name
|
|
54
84
|
? `useRouter as ${s.local.name}`
|
|
55
85
|
: 'useRouter')
|
|
56
|
-
.join(', ')} } from '
|
|
86
|
+
.join(', ')} } from '${ROUTER_MODULE_PATH}';\n`;
|
|
57
87
|
const otherImports = `import { ${otherSpecifiers
|
|
58
88
|
.map((s) => s.local.name)
|
|
59
89
|
.join(', ')} } from 'next/router';`;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collects every `RuleTester` case the suite declares WITHOUT executing any of
|
|
3
|
+
* them, by shadowing `run` on the shared tester instances and then loading each
|
|
4
|
+
* suite for its declarations alone.
|
|
5
|
+
*
|
|
6
|
+
* A guard that wants to exercise fixtures rather than documented snippets has no
|
|
7
|
+
* other way in. `src/tests/*.test.ts` call `RuleTester.run` at module scope, so
|
|
8
|
+
* importing one normally re-executes it — measured at 2350 tests, 2 minutes and
|
|
9
|
+
* 48 cross-file side-effect failures, which is why
|
|
10
|
+
* `recommended-config-fix-closure.test.ts` reads docs fenced blocks instead.
|
|
11
|
+
* Shadowing `run` before the load turns each of those calls into a declaration
|
|
12
|
+
* capture, so the cases are collected at the cost of loading the module and
|
|
13
|
+
* nothing more.
|
|
14
|
+
*
|
|
15
|
+
* The fixtures are worth reaching precisely because they are not the docs: a
|
|
16
|
+
* rule's `valid` list is written to sit on its carve-out boundaries, which is
|
|
17
|
+
* where a sibling fixer destroys an exemption. Every finding of that class
|
|
18
|
+
* (#1595-#1599, #1603, #1677-#1682) came from this corpus; the docs corpus
|
|
19
|
+
* caught none of them.
|
|
20
|
+
*/
|
|
21
|
+
/** A single `ruleTester.run(name, rule, tests)` call, captured but not run. */
|
|
22
|
+
export type HarvestedSuite = {
|
|
23
|
+
/** The display name the suite passed to `run`. */
|
|
24
|
+
name: string;
|
|
25
|
+
/** Which shared tester export declared it, which fixes the parser. */
|
|
26
|
+
tester: string;
|
|
27
|
+
/** Basename of the declaring file, so a finding is reproducible by hand. */
|
|
28
|
+
file: string;
|
|
29
|
+
/**
|
|
30
|
+
* The rule object itself. Callers resolve a rule NAME from this by identity
|
|
31
|
+
* against the plugin's own map rather than from `name`: 100 of the suites
|
|
32
|
+
* pass a display name that is not a rule name (`requireMemo`,
|
|
33
|
+
* `prefer-next-dynamic (JSX scenarios)`, `no-hungarian-phone-number-test`),
|
|
34
|
+
* and name-keyed matching silently drops every one of them.
|
|
35
|
+
*/
|
|
36
|
+
rule: unknown;
|
|
37
|
+
valid: readonly unknown[];
|
|
38
|
+
invalid: readonly unknown[];
|
|
39
|
+
};
|
|
40
|
+
export type HarvestResult = {
|
|
41
|
+
suites: HarvestedSuite[];
|
|
42
|
+
/** Files that threw while loading, `basename: message`. */
|
|
43
|
+
failures: string[];
|
|
44
|
+
/** Non-vacuity accounting: a silent drop here would fake a clean sweep. */
|
|
45
|
+
filesLoaded: number;
|
|
46
|
+
filesSkipped: number;
|
|
47
|
+
};
|
|
48
|
+
export declare function harvestRuleTesterCases(): HarvestResult;
|