@blumintinc/eslint-plugin-blumint 1.20.71 → 1.20.72
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/lib/index.js +1 -1
- package/lib/rules/enforce-firestore-doc-ref-generic.js +76 -18
- package/lib/rules/enforce-verb-noun-naming.js +173 -0
- package/lib/rules/no-explicit-return-type.js +28 -5
- package/lib/rules/no-unnecessary-verb-suffix.js +32 -5
- package/package.json +1 -1
- package/release-manifest.json +38 -0
package/lib/index.js
CHANGED
|
@@ -277,23 +277,53 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
277
277
|
member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
278
278
|
member.key.name === property.name);
|
|
279
279
|
if (getter) {
|
|
280
|
-
|
|
281
|
-
if (getter.value.returnType) {
|
|
282
|
-
return hasCollectionReferenceType(getter.value.returnType.typeAnnotation);
|
|
283
|
-
}
|
|
284
|
-
// Check return statement to infer type
|
|
285
|
-
if (getter.value.body &&
|
|
286
|
-
getter.value.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
287
|
-
const returnStmt = getter.value.body.body.find((stmt) => stmt.type === utils_1.AST_NODE_TYPES.ReturnStatement);
|
|
288
|
-
if (returnStmt?.argument?.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
289
|
-
return checkMemberExpressionForCollectionReference(returnStmt.argument);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
280
|
+
return yieldsTypedCollectionReference(getter.value);
|
|
292
281
|
}
|
|
293
282
|
}
|
|
294
283
|
}
|
|
295
284
|
return false;
|
|
296
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Guards the return-expression inference below against a class whose
|
|
288
|
+
* members refer to one another, such as `get a() { return this.a; }` or a
|
|
289
|
+
* pair of mutually recursive methods. Following the expression is otherwise
|
|
290
|
+
* unbounded, and a cycle is legal input that must terminate rather than
|
|
291
|
+
* exhaust the stack.
|
|
292
|
+
*/
|
|
293
|
+
const inferenceInProgress = new Set();
|
|
294
|
+
/**
|
|
295
|
+
* Reports whether a class member provably hands back a typed
|
|
296
|
+
* CollectionReference.
|
|
297
|
+
*
|
|
298
|
+
* The explicit return annotation is authoritative where it exists, but it
|
|
299
|
+
* cannot be the only evidence read: `no-explicit-return-type` ships in the
|
|
300
|
+
* same recommended config and is fixable, so a single `eslint --fix` pass
|
|
301
|
+
* deletes it. The schema the annotation described still lives in the
|
|
302
|
+
* expression the member returns, and that expression is what the fixer
|
|
303
|
+
* leaves behind, so it is read as the fallback.
|
|
304
|
+
*/
|
|
305
|
+
function yieldsTypedCollectionReference(fn) {
|
|
306
|
+
if (fn.returnType) {
|
|
307
|
+
return hasCollectionReferenceType(fn.returnType.typeAnnotation);
|
|
308
|
+
}
|
|
309
|
+
if (!fn.body || inferenceInProgress.has(fn)) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
inferenceInProgress.add(fn);
|
|
313
|
+
try {
|
|
314
|
+
if (fn.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
315
|
+
return isTypedCollectionReference(fn.body);
|
|
316
|
+
}
|
|
317
|
+
// Only top-level returns are read; a return nested inside another
|
|
318
|
+
// function belongs to that one.
|
|
319
|
+
return fn.body.body.some((statement) => statement.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
|
|
320
|
+
!!statement.argument &&
|
|
321
|
+
isTypedCollectionReference(statement.argument));
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
inferenceInProgress.delete(fn);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
297
327
|
function checkIdentifierForCollectionReference(node) {
|
|
298
328
|
// Check function parameters
|
|
299
329
|
const functionParam = findFunctionParameter(node);
|
|
@@ -374,12 +404,9 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
374
404
|
if (obj.type === utils_1.AST_NODE_TYPES.ThisExpression) {
|
|
375
405
|
const classNode = findParentClass(node);
|
|
376
406
|
if (classNode) {
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
!!member.value.returnType);
|
|
381
|
-
if (method?.value.returnType) {
|
|
382
|
-
return hasCollectionReferenceType(method.value.returnType.typeAnnotation);
|
|
407
|
+
const callee = findClassCallable(classNode, property.name);
|
|
408
|
+
if (callee) {
|
|
409
|
+
return yieldsTypedCollectionReference(callee);
|
|
383
410
|
}
|
|
384
411
|
}
|
|
385
412
|
}
|
|
@@ -387,6 +414,37 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
387
414
|
}
|
|
388
415
|
return false;
|
|
389
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Resolves `this.<name>()` to the function the call runs, covering both a
|
|
419
|
+
* method declaration and a property holding a function expression. The
|
|
420
|
+
* member is matched by name alone: requiring a return annotation here would
|
|
421
|
+
* make the resolution disappear the moment `no-explicit-return-type`
|
|
422
|
+
* strips it, even though the returned expression is unchanged.
|
|
423
|
+
*/
|
|
424
|
+
function findClassCallable(classNode, name) {
|
|
425
|
+
for (const member of classNode.body.body) {
|
|
426
|
+
if (member.type === utils_1.AST_NODE_TYPES.MethodDefinition &&
|
|
427
|
+
member.kind === 'method' &&
|
|
428
|
+
member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
429
|
+
member.key.name === name) {
|
|
430
|
+
return member.value;
|
|
431
|
+
}
|
|
432
|
+
if (member.type === utils_1.AST_NODE_TYPES.PropertyDefinition &&
|
|
433
|
+
member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
434
|
+
member.key.name === name) {
|
|
435
|
+
// An annotated property is already resolved by the member-expression
|
|
436
|
+
// path, which reads the annotation rather than the initializer.
|
|
437
|
+
if (member.typeAnnotation || !member.value) {
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
const initializer = member.value;
|
|
441
|
+
const isFunction = initializer.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
442
|
+
initializer.type === utils_1.AST_NODE_TYPES.FunctionExpression;
|
|
443
|
+
return isFunction ? initializer : undefined;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
390
448
|
function getTypeOfMemberExpression(node) {
|
|
391
449
|
if (node.object.type === utils_1.AST_NODE_TYPES.ThisExpression &&
|
|
392
450
|
node.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
@@ -3767,6 +3767,154 @@ const ALLOWLIST = {
|
|
|
3767
3767
|
'zoom',
|
|
3768
3768
|
]),
|
|
3769
3769
|
};
|
|
3770
|
+
/** Higher-order components that only ever wrap another component. */
|
|
3771
|
+
const COMPONENT_WRAPPERS = new Set(['memo', 'forwardRef']);
|
|
3772
|
+
/** Element factories a component uses when it renders without JSX syntax. */
|
|
3773
|
+
const ELEMENT_FACTORIES = new Set(['createElement', 'cloneElement']);
|
|
3774
|
+
/** A hook call is named `useX`; a PascalCase caller of one is a component. */
|
|
3775
|
+
const HOOK_CALL = /^use[A-Z]/;
|
|
3776
|
+
/**
|
|
3777
|
+
* A nested function owns its own returns and its own hook calls, so evidence
|
|
3778
|
+
* gathered about the enclosing function must stop at its boundary.
|
|
3779
|
+
*/
|
|
3780
|
+
const FUNCTION_LIKE_TYPES = new Set([
|
|
3781
|
+
utils_1.AST_NODE_TYPES.FunctionDeclaration,
|
|
3782
|
+
utils_1.AST_NODE_TYPES.FunctionExpression,
|
|
3783
|
+
utils_1.AST_NODE_TYPES.ArrowFunctionExpression,
|
|
3784
|
+
]);
|
|
3785
|
+
/**
|
|
3786
|
+
* Visits every descendant of `node` that belongs to the same function scope.
|
|
3787
|
+
* Nested functions are handed to `visit` but not descended into.
|
|
3788
|
+
*/
|
|
3789
|
+
function forEachNodeInOwnScope(node, visit) {
|
|
3790
|
+
for (const key of Object.keys(node)) {
|
|
3791
|
+
if (key === 'parent') {
|
|
3792
|
+
continue;
|
|
3793
|
+
}
|
|
3794
|
+
const value = node[key];
|
|
3795
|
+
const children = Array.isArray(value) ? value : [value];
|
|
3796
|
+
for (const child of children) {
|
|
3797
|
+
if (!ASTHelpers_1.ASTHelpers.isNode(child)) {
|
|
3798
|
+
continue;
|
|
3799
|
+
}
|
|
3800
|
+
visit(child);
|
|
3801
|
+
if (!FUNCTION_LIKE_TYPES.has(child.type)) {
|
|
3802
|
+
forEachNodeInOwnScope(child, visit);
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
/**
|
|
3808
|
+
* The name of a call to `name(...)` or `React.name(...)`. Member calls are
|
|
3809
|
+
* confined to the React namespace so `document.createElement(...)` — an ordinary
|
|
3810
|
+
* DOM call — is not mistaken for a render.
|
|
3811
|
+
*/
|
|
3812
|
+
function calleeName(callee) {
|
|
3813
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
3814
|
+
return callee.name;
|
|
3815
|
+
}
|
|
3816
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
3817
|
+
!callee.computed &&
|
|
3818
|
+
callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
3819
|
+
callee.object.name === 'React' &&
|
|
3820
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
3821
|
+
return callee.property.name;
|
|
3822
|
+
}
|
|
3823
|
+
return undefined;
|
|
3824
|
+
}
|
|
3825
|
+
/** A generator yields values rather than rendering, so it is never a component. */
|
|
3826
|
+
function isGeneratorFunction(node) {
|
|
3827
|
+
return node.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression && node.generator;
|
|
3828
|
+
}
|
|
3829
|
+
/**
|
|
3830
|
+
* Whether a returned expression is something React renders. `null`/`undefined`
|
|
3831
|
+
* are the "render nothing" case a component reaches through an early return,
|
|
3832
|
+
* and `createElement` is how a component renders from a file that cannot hold
|
|
3833
|
+
* JSX syntax.
|
|
3834
|
+
*/
|
|
3835
|
+
function isRenderableValue(value) {
|
|
3836
|
+
if (!value) {
|
|
3837
|
+
return false;
|
|
3838
|
+
}
|
|
3839
|
+
switch (value.type) {
|
|
3840
|
+
case utils_1.AST_NODE_TYPES.Literal:
|
|
3841
|
+
// Matched on `raw` because a regex literal an environment cannot compile
|
|
3842
|
+
// also carries `value === null`.
|
|
3843
|
+
return value.raw === 'null';
|
|
3844
|
+
case utils_1.AST_NODE_TYPES.Identifier:
|
|
3845
|
+
return value.name === 'undefined';
|
|
3846
|
+
case utils_1.AST_NODE_TYPES.JSXElement:
|
|
3847
|
+
case utils_1.AST_NODE_TYPES.JSXFragment:
|
|
3848
|
+
return true;
|
|
3849
|
+
case utils_1.AST_NODE_TYPES.CallExpression: {
|
|
3850
|
+
const name = calleeName(value.callee);
|
|
3851
|
+
return !!name && ELEMENT_FACTORIES.has(name);
|
|
3852
|
+
}
|
|
3853
|
+
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
3854
|
+
case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
|
|
3855
|
+
case utils_1.AST_NODE_TYPES.TSNonNullExpression:
|
|
3856
|
+
case utils_1.AST_NODE_TYPES.TSTypeAssertion:
|
|
3857
|
+
return isRenderableValue(value.expression);
|
|
3858
|
+
case utils_1.AST_NODE_TYPES.ConditionalExpression:
|
|
3859
|
+
// Every branch has to render, otherwise `cond ? value : null` — an
|
|
3860
|
+
// ordinary lookup-with-fallback — would read as a component.
|
|
3861
|
+
return (isRenderableValue(value.consequent) &&
|
|
3862
|
+
isRenderableValue(value.alternate));
|
|
3863
|
+
default:
|
|
3864
|
+
return false;
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
/**
|
|
3868
|
+
* Whether every value the function returns is renderable, and it returns at
|
|
3869
|
+
* least once. Demanding *every* return keeps a helper that merely falls back to
|
|
3870
|
+
* `null` on one branch out of the component exemption, and demanding one return
|
|
3871
|
+
* keeps a function that returns nothing at all out of it.
|
|
3872
|
+
*/
|
|
3873
|
+
function rendersEveryReturn(node) {
|
|
3874
|
+
if (node.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
3875
|
+
return isRenderableValue(node.body);
|
|
3876
|
+
}
|
|
3877
|
+
const returned = [];
|
|
3878
|
+
forEachNodeInOwnScope(node.body, (child) => {
|
|
3879
|
+
if (child.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
|
|
3880
|
+
returned.push(child.argument);
|
|
3881
|
+
}
|
|
3882
|
+
});
|
|
3883
|
+
return returned.length > 0 && returned.every(isRenderableValue);
|
|
3884
|
+
}
|
|
3885
|
+
function callsReactHook(node) {
|
|
3886
|
+
let found = false;
|
|
3887
|
+
forEachNodeInOwnScope(node.body, (child) => {
|
|
3888
|
+
if (found || child.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
3889
|
+
return;
|
|
3890
|
+
}
|
|
3891
|
+
const name = calleeName(child.callee);
|
|
3892
|
+
found = !!name && HOOK_CALL.test(name);
|
|
3893
|
+
});
|
|
3894
|
+
return found;
|
|
3895
|
+
}
|
|
3896
|
+
/**
|
|
3897
|
+
* Whether a reference to the function names it as a component: rendered as a
|
|
3898
|
+
* JSX element, or handed to a higher-order component.
|
|
3899
|
+
*/
|
|
3900
|
+
function isComponentReference(identifier) {
|
|
3901
|
+
const parent = identifier.parent;
|
|
3902
|
+
if (!parent) {
|
|
3903
|
+
return false;
|
|
3904
|
+
}
|
|
3905
|
+
if (parent.type === utils_1.AST_NODE_TYPES.JSXOpeningElement ||
|
|
3906
|
+
parent.type === utils_1.AST_NODE_TYPES.JSXClosingElement ||
|
|
3907
|
+
parent.type === utils_1.AST_NODE_TYPES.JSXMemberExpression) {
|
|
3908
|
+
return true;
|
|
3909
|
+
}
|
|
3910
|
+
if (parent.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
3911
|
+
const name = calleeName(parent.callee);
|
|
3912
|
+
return (!!name &&
|
|
3913
|
+
COMPONENT_WRAPPERS.has(name) &&
|
|
3914
|
+
parent.arguments.some((argument) => argument === identifier));
|
|
3915
|
+
}
|
|
3916
|
+
return false;
|
|
3917
|
+
}
|
|
3770
3918
|
exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
3771
3919
|
name: 'enforce-verb-noun-naming',
|
|
3772
3920
|
meta: {
|
|
@@ -3960,6 +4108,19 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
3960
4108
|
}
|
|
3961
4109
|
return false;
|
|
3962
4110
|
}
|
|
4111
|
+
/**
|
|
4112
|
+
* Whether the file names the function as a component somewhere other than
|
|
4113
|
+
* its declaration — `<MyComponent />`, `memo(MyComponent)`. Resolved through
|
|
4114
|
+
* scope analysis, which records JSX element names as references.
|
|
4115
|
+
*/
|
|
4116
|
+
function isUsedAsReactComponent(node, functionName) {
|
|
4117
|
+
const scope = ASTHelpers_1.ASTHelpers.getScope(context, node);
|
|
4118
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, functionName);
|
|
4119
|
+
if (!variable) {
|
|
4120
|
+
return false;
|
|
4121
|
+
}
|
|
4122
|
+
return variable.references.some((reference) => isComponentReference(reference.identifier));
|
|
4123
|
+
}
|
|
3963
4124
|
function isReactComponent(node) {
|
|
3964
4125
|
if (node.type !== utils_1.AST_NODE_TYPES.FunctionDeclaration &&
|
|
3965
4126
|
node.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
@@ -3985,6 +4146,18 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
3985
4146
|
if (isJsxFile || returnsJsx || hasReactType) {
|
|
3986
4147
|
return true;
|
|
3987
4148
|
}
|
|
4149
|
+
// The annotation cannot be the only evidence a `.ts` file offers, because
|
|
4150
|
+
// `no-explicit-return-type --fix` — shipped in the same recommended config
|
|
4151
|
+
// — deletes it, leaving a component indistinguishable from a helper and
|
|
4152
|
+
// demanding a rename that would break every JSX call site. A component
|
|
4153
|
+
// is therefore also recognised by what it renders, by the hooks it calls,
|
|
4154
|
+
// and by how the rest of the file uses it.
|
|
4155
|
+
if (!isGeneratorFunction(node) &&
|
|
4156
|
+
(rendersEveryReturn(node) ||
|
|
4157
|
+
callsReactHook(node) ||
|
|
4158
|
+
isUsedAsReactComponent(node, functionName))) {
|
|
4159
|
+
return true;
|
|
4160
|
+
}
|
|
3988
4161
|
}
|
|
3989
4162
|
// If we have explicit React type, still treat it as a component to avoid false naming violations.
|
|
3990
4163
|
if (hasReactType) {
|
|
@@ -34,16 +34,22 @@ function describeClassMethod(node) {
|
|
|
34
34
|
return 'class method';
|
|
35
35
|
}
|
|
36
36
|
function describeMethodSignature(node) {
|
|
37
|
+
// A method signature is equally non-inferable in either container, but naming
|
|
38
|
+
// the wrong one sends the reader looking for an `interface` keyword that the
|
|
39
|
+
// source does not contain.
|
|
40
|
+
const kind = node.parent?.type === utils_1.AST_NODE_TYPES.TSTypeLiteral
|
|
41
|
+
? 'type literal method'
|
|
42
|
+
: 'interface method';
|
|
37
43
|
if (!node.computed &&
|
|
38
44
|
(node.key.type === utils_1.AST_NODE_TYPES.Identifier ||
|
|
39
45
|
(node.key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
40
46
|
typeof node.key.value === 'string'))) {
|
|
41
47
|
const name = getNameFromIdentifierOrLiteral(node.key);
|
|
42
48
|
if (name) {
|
|
43
|
-
return
|
|
49
|
+
return `${kind} "${name}"`;
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
|
-
return
|
|
52
|
+
return kind;
|
|
47
53
|
}
|
|
48
54
|
function describeFunctionDeclaration(node) {
|
|
49
55
|
if (node.id?.name) {
|
|
@@ -449,12 +455,29 @@ function participatesInReturnCycle(name, graph) {
|
|
|
449
455
|
}
|
|
450
456
|
return false;
|
|
451
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* The sibling members of a method signature's container. An interface body and a
|
|
460
|
+
* type literal declare exactly the same members — `interface X { f(): void }` and
|
|
461
|
+
* `type X = { f(): void }` differ only in the keyword that introduces them, and
|
|
462
|
+
* `prefer-type-over-interface` (also fixable, also in the recommended config)
|
|
463
|
+
* rewrites the first into the second. A member's inferability cannot depend on
|
|
464
|
+
* which keyword declared its container, so both are read here.
|
|
465
|
+
*/
|
|
466
|
+
function signatureContainerMembers(container) {
|
|
467
|
+
if (container?.type === utils_1.AST_NODE_TYPES.TSInterfaceBody) {
|
|
468
|
+
return container.body;
|
|
469
|
+
}
|
|
470
|
+
if (container?.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
471
|
+
return container.members;
|
|
472
|
+
}
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
452
475
|
function isOverloadedFunction(node) {
|
|
453
476
|
if (!node.parent)
|
|
454
477
|
return false;
|
|
455
478
|
if (node.type === utils_1.AST_NODE_TYPES.TSMethodSignature) {
|
|
456
|
-
const
|
|
457
|
-
if (
|
|
479
|
+
const members = signatureContainerMembers(node.parent);
|
|
480
|
+
if (!members)
|
|
458
481
|
return false;
|
|
459
482
|
if (node.computed)
|
|
460
483
|
return false;
|
|
@@ -464,7 +487,7 @@ function isOverloadedFunction(node) {
|
|
|
464
487
|
: undefined;
|
|
465
488
|
if (!methodName)
|
|
466
489
|
return false;
|
|
467
|
-
return (
|
|
490
|
+
return (members.filter((member) => member.type === utils_1.AST_NODE_TYPES.TSMethodSignature &&
|
|
468
491
|
!member.computed &&
|
|
469
492
|
(member.key.type === utils_1.AST_NODE_TYPES.Identifier ||
|
|
470
493
|
member.key.type === utils_1.AST_NODE_TYPES.Literal) &&
|
|
@@ -277,6 +277,19 @@ function enclosingFunction(node) {
|
|
|
277
277
|
function declaresCheckedReturnType(fn) {
|
|
278
278
|
return (fn !== null && checksExcessProperties(fn.returnType?.typeAnnotation ?? null));
|
|
279
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* Assertion wrappers, which never change the runtime value they wrap. An
|
|
282
|
+
* `as const`, `as T`, `satisfies T` or `!` therefore leaves the literal beneath
|
|
283
|
+
* it the same object, still checked by whatever declared type sits OUTSIDE the
|
|
284
|
+
* wrapper. Mirrors the set `no-firestore-object-arrays` unwraps for the same
|
|
285
|
+
* reason.
|
|
286
|
+
*/
|
|
287
|
+
const EXPRESSION_ASSERTION_TYPES = new Set([
|
|
288
|
+
utils_1.AST_NODE_TYPES.TSAsExpression,
|
|
289
|
+
utils_1.AST_NODE_TYPES.TSSatisfiesExpression,
|
|
290
|
+
utils_1.AST_NODE_TYPES.TSNonNullExpression,
|
|
291
|
+
utils_1.AST_NODE_TYPES.TSTypeAssertion,
|
|
292
|
+
]);
|
|
280
293
|
/**
|
|
281
294
|
* Reports whether `node` sits inside a value whose shape TypeScript checks
|
|
282
295
|
* against a declared type — a type-annotated variable or class field, a
|
|
@@ -294,8 +307,13 @@ function declaresCheckedReturnType(fn) {
|
|
|
294
307
|
* that shape's sole way to declare the contract it imitates.
|
|
295
308
|
*
|
|
296
309
|
* The walk climbs object/array containers so an outer signal covers nested
|
|
297
|
-
* members, and
|
|
298
|
-
*
|
|
310
|
+
* members, and climbs THROUGH assertion wrappers, which change no runtime value
|
|
311
|
+
* and so cannot detach a literal from the declared type it is assigned to
|
|
312
|
+
* (#1597) — `enforce-object-literal-as-const` ships in the same recommended
|
|
313
|
+
* config and appends `as const` to exactly these literals by `--fix`. An
|
|
314
|
+
* assertion is transparent, never itself a signal: `{...} as T` still reports,
|
|
315
|
+
* because an `as` clause does not reject undeclared members the way an
|
|
316
|
+
* annotation does. The walk stops at anything else.
|
|
299
317
|
*/
|
|
300
318
|
function hasConformanceSignal(node) {
|
|
301
319
|
let current = node;
|
|
@@ -304,10 +322,19 @@ function hasConformanceSignal(node) {
|
|
|
304
322
|
if (!parent) {
|
|
305
323
|
return false;
|
|
306
324
|
}
|
|
325
|
+
// A `satisfies` clause both asserts and checks, so it is answered before
|
|
326
|
+
// the wrapper is stepped over; an unchecked one (`satisfies any`) proves
|
|
327
|
+
// nothing on its own and the walk continues past it to the outer context.
|
|
328
|
+
if (parent.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression &&
|
|
329
|
+
parent.expression === current &&
|
|
330
|
+
checksExcessProperties(parent.typeAnnotation)) {
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
if (EXPRESSION_ASSERTION_TYPES.has(parent.type)) {
|
|
334
|
+
current = parent;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
307
337
|
switch (parent.type) {
|
|
308
|
-
case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
|
|
309
|
-
return (parent.expression === current &&
|
|
310
|
-
checksExcessProperties(parent.typeAnnotation));
|
|
311
338
|
case utils_1.AST_NODE_TYPES.VariableDeclarator:
|
|
312
339
|
return (parent.init === current &&
|
|
313
340
|
checksExcessProperties(declaredTypeNode(parent.id)));
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,42 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.72",
|
|
4
|
+
"date": "2026-08-02T03:33:22.821Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-firestore-doc-ref-generic",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1595
|
|
11
|
+
],
|
|
12
|
+
"summary": "infer collection schema from the returned expression, not only the annotation (closes #1595)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "enforce-verb-noun-naming",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1596
|
|
19
|
+
],
|
|
20
|
+
"summary": "recognize a React component without its return annotation (closes #1596)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "no-explicit-return-type",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
1598
|
|
27
|
+
],
|
|
28
|
+
"summary": "see overload siblings inside a type literal, not only an interface body (closes #1598)"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "no-unnecessary-verb-suffix",
|
|
32
|
+
"changeType": "fix",
|
|
33
|
+
"issues": [
|
|
34
|
+
1597
|
|
35
|
+
],
|
|
36
|
+
"summary": "climb through assertion wrappers when seeking a conformance signal (closes #1597)"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
2
40
|
{
|
|
3
41
|
"version": "1.20.71",
|
|
4
42
|
"date": "2026-08-02T01:25:22.489Z",
|