@sarj/eslint-plugin 16.19.1 → 16.19.2
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/dist/index.cjs +47 -9
- package/dist/index.js +47 -9
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -52,7 +52,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
52
52
|
// package.json
|
|
53
53
|
var package_default = {
|
|
54
54
|
name: "@sarj/eslint-plugin",
|
|
55
|
-
version: "16.19.
|
|
55
|
+
version: "16.19.2",
|
|
56
56
|
packageManager: "npm@12.2.0",
|
|
57
57
|
description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
|
|
58
58
|
type: "module",
|
|
@@ -14349,13 +14349,14 @@ var no_duplicate_test_case_default = createRule({
|
|
|
14349
14349
|
// src/rules/no-tautological-expect.ts
|
|
14350
14350
|
var import_utils80 = require("@typescript-eslint/utils");
|
|
14351
14351
|
var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
|
|
14352
|
-
summary: "Disallow supported assertions
|
|
14353
|
-
rationale: "An assertion
|
|
14352
|
+
summary: "Disallow supported assertions fixed by literal setup or immutable self identity.",
|
|
14353
|
+
rationale: "An assertion fixed by literal setup or one immutable binding's identity cannot distinguish a wrong result from the intended behavior.",
|
|
14354
14354
|
remediation: "Assert on a value produced by the behavior under test against an independently specified expectation, or remove only the redundant assertion while preserving coverage.",
|
|
14355
14355
|
category: "testing",
|
|
14356
14356
|
limitations: [
|
|
14357
14357
|
"Only supported `expect` matcher calls in recognized test files are inspected, including imported aliases, static string messages on imported Vitest/Playwright expect calls, and a single `.not` on zero-argument matchers. Local or type-only bindings, regular expressions, async modifiers and unsupported coercions are excluded; failing constant assertions are not tautologies.",
|
|
14358
|
-
"
|
|
14358
|
+
"The literal proof resolves scalar const bindings and aliases through lexical scope; mutable bindings, objects, arrays, destructuring, runtime initializers and forward references are excluded from that proof.",
|
|
14359
|
+
"Positive toBe assertions on the same initialized, unreassigned local identifier const binding also have fixed identity, including runtime results. Different bindings, imported live values, property/getter accesses and deep-equality reflexivity remain excluded from this identity proof.",
|
|
14359
14360
|
"Expected-side runtime dependence, including a complete inline snapshot computed from its actual value, remains manual review; independent static interpolation and intentional partial masking are valid.",
|
|
14360
14361
|
"Direct framework expect.extend registrations exclude replaced built-in matchers; opaque registrations exclude all supported matchers, and nonempty custom equality testers exclude deep equality. Direct synchronous expect callbacks observed by a throwing matcher retain their assertion contracts. Cross-file or indirect matcher customization is not inferred."
|
|
14361
14362
|
],
|
|
@@ -14477,6 +14478,26 @@ var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
|
|
|
14477
14478
|
focusPath: "src/assertion.test.ts",
|
|
14478
14479
|
expectedCount: 0,
|
|
14479
14480
|
public: true
|
|
14481
|
+
},
|
|
14482
|
+
{
|
|
14483
|
+
id: "self-identity-oracle",
|
|
14484
|
+
scenarioId: "immutable-identity",
|
|
14485
|
+
title: "A returned value's identity with itself cannot reject a wrong result",
|
|
14486
|
+
outcome: "match",
|
|
14487
|
+
files: [{ path: "src/service.test.ts", source: "import {expect} from 'vitest'; const result = serviceStatus(); expect(result).toBe(result);" }],
|
|
14488
|
+
focusPath: "src/service.test.ts",
|
|
14489
|
+
expectedCount: 1,
|
|
14490
|
+
public: true
|
|
14491
|
+
},
|
|
14492
|
+
{
|
|
14493
|
+
id: "independent-result-oracle",
|
|
14494
|
+
scenarioId: "immutable-identity",
|
|
14495
|
+
title: "An independent expected state can reject a wrong result",
|
|
14496
|
+
outcome: "no-match",
|
|
14497
|
+
files: [{ path: "src/service.test.ts", source: "import {expect} from 'vitest'; const result = serviceStatus(); expect(result.status).toBe('ready');" }],
|
|
14498
|
+
focusPath: "src/service.test.ts",
|
|
14499
|
+
expectedCount: 0,
|
|
14500
|
+
public: true
|
|
14480
14501
|
}
|
|
14481
14502
|
]
|
|
14482
14503
|
};
|
|
@@ -14628,6 +14649,15 @@ function collectCustomization(node, source, customized) {
|
|
|
14628
14649
|
customized.add(name ?? "*");
|
|
14629
14650
|
}
|
|
14630
14651
|
}
|
|
14652
|
+
function hasConstSelfIdentity(actual, expected, source) {
|
|
14653
|
+
actual = unwrapExpression(actual);
|
|
14654
|
+
expected = unwrapExpression(expected);
|
|
14655
|
+
if (actual.type !== import_utils80.AST_NODE_TYPES.Identifier || expected.type !== import_utils80.AST_NODE_TYPES.Identifier) return false;
|
|
14656
|
+
const variable = import_utils80.ASTUtils.findVariable(source.getScope(actual), actual.name);
|
|
14657
|
+
if (variable?.defs.length !== 1 || import_utils80.ASTUtils.findVariable(source.getScope(expected), expected.name) !== variable) return false;
|
|
14658
|
+
const definition = variable.defs[0];
|
|
14659
|
+
return definition?.node.type === import_utils80.AST_NODE_TYPES.VariableDeclarator && definition.node.id.type === import_utils80.AST_NODE_TYPES.Identifier && definition.node.parent.kind === "const" && definition.node.init !== null && definition.node.init.range[1] <= Math.min(actual.range[0], expected.range[0]) && !variable.references.some((reference) => reference.isWrite() && !reference.init);
|
|
14660
|
+
}
|
|
14631
14661
|
var no_tautological_expect_default = createRule({
|
|
14632
14662
|
name: "no-tautological-expect",
|
|
14633
14663
|
documentation: NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION,
|
|
@@ -14638,7 +14668,7 @@ var no_tautological_expect_default = createRule({
|
|
|
14638
14668
|
},
|
|
14639
14669
|
schema: [],
|
|
14640
14670
|
messages: {
|
|
14641
|
-
tautologicalComparison: "`expect({{operand}}).{{matcher}}({{operand}})` is fixed by
|
|
14671
|
+
tautologicalComparison: "`expect({{operand}}).{{matcher}}({{operand}})` is fixed by {{reason}} and does not observe behavior. Assert against an independent expectation or remove only the redundant assertion, preserving the action and other coverage.",
|
|
14642
14672
|
tautologicalMatcher: "`expect({{operand}}).{{matcher}}()` is statically known to pass. Assert on a produced value or remove only the redundant assertion, preserving other coverage."
|
|
14643
14673
|
}
|
|
14644
14674
|
},
|
|
@@ -14659,10 +14689,19 @@ var no_tautological_expect_default = createRule({
|
|
|
14659
14689
|
const matcher = import_utils80.ASTUtils.getPropertyName(callee);
|
|
14660
14690
|
if (matcher === null || !ZERO_ARG_MATCHERS.has(matcher) && !EQUALITY_MATCHERS.has(matcher) || customized.has("*") || customized.has(matcher)) return;
|
|
14661
14691
|
const assertion = expectOperand(callee, context.sourceCode);
|
|
14662
|
-
if (assertion === null) return;
|
|
14692
|
+
if (assertion === null || isExceptionContract(node, context.sourceCode)) return;
|
|
14663
14693
|
const writtenOperand = assertion.operand;
|
|
14694
|
+
const writtenExpected = node.arguments[0];
|
|
14695
|
+
if (!assertion.negated && matcher === "toBe" && node.arguments.length === 1 && writtenExpected !== void 0 && hasConstSelfIdentity(writtenOperand, writtenExpected, context.sourceCode)) {
|
|
14696
|
+
context.report({
|
|
14697
|
+
node,
|
|
14698
|
+
messageId: "tautologicalComparison",
|
|
14699
|
+
data: { operand: preview2(writtenOperand), matcher, reason: "the same immutable binding" }
|
|
14700
|
+
});
|
|
14701
|
+
return;
|
|
14702
|
+
}
|
|
14664
14703
|
const operand = resolveLiteral(writtenOperand, context.sourceCode);
|
|
14665
|
-
if (operand === null
|
|
14704
|
+
if (operand === null) {
|
|
14666
14705
|
return;
|
|
14667
14706
|
}
|
|
14668
14707
|
const writtenMatcher = `${assertion.negated ? "not." : ""}${matcher}`;
|
|
@@ -14674,7 +14713,6 @@ var no_tautological_expect_default = createRule({
|
|
|
14674
14713
|
});
|
|
14675
14714
|
return;
|
|
14676
14715
|
}
|
|
14677
|
-
const writtenExpected = node.arguments[0];
|
|
14678
14716
|
const expected = writtenExpected === void 0 ? null : resolveLiteral(writtenExpected, context.sourceCode);
|
|
14679
14717
|
if (assertion.negated || !EQUALITY_MATCHERS.has(matcher) || node.arguments.length !== 1 || expected === null) {
|
|
14680
14718
|
return;
|
|
@@ -14688,7 +14726,7 @@ var no_tautological_expect_default = createRule({
|
|
|
14688
14726
|
context.report({
|
|
14689
14727
|
node,
|
|
14690
14728
|
messageId: "tautologicalComparison",
|
|
14691
|
-
data: { operand: preview2(writtenOperand), matcher }
|
|
14729
|
+
data: { operand: preview2(writtenOperand), matcher, reason: "literal setup" }
|
|
14692
14730
|
});
|
|
14693
14731
|
}
|
|
14694
14732
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// package.json
|
|
2
2
|
var package_default = {
|
|
3
3
|
name: "@sarj/eslint-plugin",
|
|
4
|
-
version: "16.19.
|
|
4
|
+
version: "16.19.2",
|
|
5
5
|
packageManager: "npm@12.2.0",
|
|
6
6
|
description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
|
|
7
7
|
type: "module",
|
|
@@ -14330,13 +14330,14 @@ var no_duplicate_test_case_default = createRule({
|
|
|
14330
14330
|
// src/rules/no-tautological-expect.ts
|
|
14331
14331
|
import { AST_NODE_TYPES as AST_NODE_TYPES65, ASTUtils as ASTUtils40 } from "@typescript-eslint/utils";
|
|
14332
14332
|
var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
|
|
14333
|
-
summary: "Disallow supported assertions
|
|
14334
|
-
rationale: "An assertion
|
|
14333
|
+
summary: "Disallow supported assertions fixed by literal setup or immutable self identity.",
|
|
14334
|
+
rationale: "An assertion fixed by literal setup or one immutable binding's identity cannot distinguish a wrong result from the intended behavior.",
|
|
14335
14335
|
remediation: "Assert on a value produced by the behavior under test against an independently specified expectation, or remove only the redundant assertion while preserving coverage.",
|
|
14336
14336
|
category: "testing",
|
|
14337
14337
|
limitations: [
|
|
14338
14338
|
"Only supported `expect` matcher calls in recognized test files are inspected, including imported aliases, static string messages on imported Vitest/Playwright expect calls, and a single `.not` on zero-argument matchers. Local or type-only bindings, regular expressions, async modifiers and unsupported coercions are excluded; failing constant assertions are not tautologies.",
|
|
14339
|
-
"
|
|
14339
|
+
"The literal proof resolves scalar const bindings and aliases through lexical scope; mutable bindings, objects, arrays, destructuring, runtime initializers and forward references are excluded from that proof.",
|
|
14340
|
+
"Positive toBe assertions on the same initialized, unreassigned local identifier const binding also have fixed identity, including runtime results. Different bindings, imported live values, property/getter accesses and deep-equality reflexivity remain excluded from this identity proof.",
|
|
14340
14341
|
"Expected-side runtime dependence, including a complete inline snapshot computed from its actual value, remains manual review; independent static interpolation and intentional partial masking are valid.",
|
|
14341
14342
|
"Direct framework expect.extend registrations exclude replaced built-in matchers; opaque registrations exclude all supported matchers, and nonempty custom equality testers exclude deep equality. Direct synchronous expect callbacks observed by a throwing matcher retain their assertion contracts. Cross-file or indirect matcher customization is not inferred."
|
|
14342
14343
|
],
|
|
@@ -14458,6 +14459,26 @@ var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
|
|
|
14458
14459
|
focusPath: "src/assertion.test.ts",
|
|
14459
14460
|
expectedCount: 0,
|
|
14460
14461
|
public: true
|
|
14462
|
+
},
|
|
14463
|
+
{
|
|
14464
|
+
id: "self-identity-oracle",
|
|
14465
|
+
scenarioId: "immutable-identity",
|
|
14466
|
+
title: "A returned value's identity with itself cannot reject a wrong result",
|
|
14467
|
+
outcome: "match",
|
|
14468
|
+
files: [{ path: "src/service.test.ts", source: "import {expect} from 'vitest'; const result = serviceStatus(); expect(result).toBe(result);" }],
|
|
14469
|
+
focusPath: "src/service.test.ts",
|
|
14470
|
+
expectedCount: 1,
|
|
14471
|
+
public: true
|
|
14472
|
+
},
|
|
14473
|
+
{
|
|
14474
|
+
id: "independent-result-oracle",
|
|
14475
|
+
scenarioId: "immutable-identity",
|
|
14476
|
+
title: "An independent expected state can reject a wrong result",
|
|
14477
|
+
outcome: "no-match",
|
|
14478
|
+
files: [{ path: "src/service.test.ts", source: "import {expect} from 'vitest'; const result = serviceStatus(); expect(result.status).toBe('ready');" }],
|
|
14479
|
+
focusPath: "src/service.test.ts",
|
|
14480
|
+
expectedCount: 0,
|
|
14481
|
+
public: true
|
|
14461
14482
|
}
|
|
14462
14483
|
]
|
|
14463
14484
|
};
|
|
@@ -14609,6 +14630,15 @@ function collectCustomization(node, source, customized) {
|
|
|
14609
14630
|
customized.add(name ?? "*");
|
|
14610
14631
|
}
|
|
14611
14632
|
}
|
|
14633
|
+
function hasConstSelfIdentity(actual, expected, source) {
|
|
14634
|
+
actual = unwrapExpression(actual);
|
|
14635
|
+
expected = unwrapExpression(expected);
|
|
14636
|
+
if (actual.type !== AST_NODE_TYPES65.Identifier || expected.type !== AST_NODE_TYPES65.Identifier) return false;
|
|
14637
|
+
const variable = ASTUtils40.findVariable(source.getScope(actual), actual.name);
|
|
14638
|
+
if (variable?.defs.length !== 1 || ASTUtils40.findVariable(source.getScope(expected), expected.name) !== variable) return false;
|
|
14639
|
+
const definition = variable.defs[0];
|
|
14640
|
+
return definition?.node.type === AST_NODE_TYPES65.VariableDeclarator && definition.node.id.type === AST_NODE_TYPES65.Identifier && definition.node.parent.kind === "const" && definition.node.init !== null && definition.node.init.range[1] <= Math.min(actual.range[0], expected.range[0]) && !variable.references.some((reference) => reference.isWrite() && !reference.init);
|
|
14641
|
+
}
|
|
14612
14642
|
var no_tautological_expect_default = createRule({
|
|
14613
14643
|
name: "no-tautological-expect",
|
|
14614
14644
|
documentation: NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION,
|
|
@@ -14619,7 +14649,7 @@ var no_tautological_expect_default = createRule({
|
|
|
14619
14649
|
},
|
|
14620
14650
|
schema: [],
|
|
14621
14651
|
messages: {
|
|
14622
|
-
tautologicalComparison: "`expect({{operand}}).{{matcher}}({{operand}})` is fixed by
|
|
14652
|
+
tautologicalComparison: "`expect({{operand}}).{{matcher}}({{operand}})` is fixed by {{reason}} and does not observe behavior. Assert against an independent expectation or remove only the redundant assertion, preserving the action and other coverage.",
|
|
14623
14653
|
tautologicalMatcher: "`expect({{operand}}).{{matcher}}()` is statically known to pass. Assert on a produced value or remove only the redundant assertion, preserving other coverage."
|
|
14624
14654
|
}
|
|
14625
14655
|
},
|
|
@@ -14640,10 +14670,19 @@ var no_tautological_expect_default = createRule({
|
|
|
14640
14670
|
const matcher = ASTUtils40.getPropertyName(callee);
|
|
14641
14671
|
if (matcher === null || !ZERO_ARG_MATCHERS.has(matcher) && !EQUALITY_MATCHERS.has(matcher) || customized.has("*") || customized.has(matcher)) return;
|
|
14642
14672
|
const assertion = expectOperand(callee, context.sourceCode);
|
|
14643
|
-
if (assertion === null) return;
|
|
14673
|
+
if (assertion === null || isExceptionContract(node, context.sourceCode)) return;
|
|
14644
14674
|
const writtenOperand = assertion.operand;
|
|
14675
|
+
const writtenExpected = node.arguments[0];
|
|
14676
|
+
if (!assertion.negated && matcher === "toBe" && node.arguments.length === 1 && writtenExpected !== void 0 && hasConstSelfIdentity(writtenOperand, writtenExpected, context.sourceCode)) {
|
|
14677
|
+
context.report({
|
|
14678
|
+
node,
|
|
14679
|
+
messageId: "tautologicalComparison",
|
|
14680
|
+
data: { operand: preview2(writtenOperand), matcher, reason: "the same immutable binding" }
|
|
14681
|
+
});
|
|
14682
|
+
return;
|
|
14683
|
+
}
|
|
14645
14684
|
const operand = resolveLiteral(writtenOperand, context.sourceCode);
|
|
14646
|
-
if (operand === null
|
|
14685
|
+
if (operand === null) {
|
|
14647
14686
|
return;
|
|
14648
14687
|
}
|
|
14649
14688
|
const writtenMatcher = `${assertion.negated ? "not." : ""}${matcher}`;
|
|
@@ -14655,7 +14694,6 @@ var no_tautological_expect_default = createRule({
|
|
|
14655
14694
|
});
|
|
14656
14695
|
return;
|
|
14657
14696
|
}
|
|
14658
|
-
const writtenExpected = node.arguments[0];
|
|
14659
14697
|
const expected = writtenExpected === void 0 ? null : resolveLiteral(writtenExpected, context.sourceCode);
|
|
14660
14698
|
if (assertion.negated || !EQUALITY_MATCHERS.has(matcher) || node.arguments.length !== 1 || expected === null) {
|
|
14661
14699
|
return;
|
|
@@ -14669,7 +14707,7 @@ var no_tautological_expect_default = createRule({
|
|
|
14669
14707
|
context.report({
|
|
14670
14708
|
node,
|
|
14671
14709
|
messageId: "tautologicalComparison",
|
|
14672
|
-
data: { operand: preview2(writtenOperand), matcher }
|
|
14710
|
+
data: { operand: preview2(writtenOperand), matcher, reason: "literal setup" }
|
|
14673
14711
|
});
|
|
14674
14712
|
}
|
|
14675
14713
|
};
|