@sarj/eslint-plugin 16.15.1 → 16.15.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 +111 -29
- package/dist/index.js +111 -29
- 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.15.
|
|
55
|
+
version: "16.15.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",
|
|
@@ -9071,14 +9071,19 @@ var NO_BARE_RETURN_FROM_TEST_CATCH_DOCUMENTATION = {
|
|
|
9071
9071
|
remediation: "Rethrow the error, assert on it, or use the runner's explicit skip mechanism when the capability is optional.",
|
|
9072
9072
|
category: "testing",
|
|
9073
9073
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
|
|
9074
|
-
limitations: ["Only bare returns owned by
|
|
9074
|
+
limitations: ["Only bare returns owned by an inline supported test callback and followed lexically by a framework assertion are reported, including each/for tables and runIf/skipIf builders. Custom builders and indirect callback references are excluded. A runner skip suppresses the finding only when it is an unconditional earlier statement in the return's block. Vitest imports and unshadowed runner globals support TestContext.skip on the first ordinary-test parameter or second for-table parameter, including a directly destructured skip alias; each-table data arguments, reassigned bindings, overwritten skip methods and dynamic skip conditions are excluded."],
|
|
9075
9075
|
examples: [
|
|
9076
9076
|
{ id: "rethrow", title: "Preserve the failure", outcome: "no-match", files: [{ path: "src/codec.test.ts", source: "test('decodes', () => { try { decode(); } catch (error) { throw error; } expect(result()).toBe('ok'); });" }], focusPath: "src/codec.test.ts", expectedCount: 0, public: true },
|
|
9077
|
-
{ id: "bare-return", title: "Do not silently pass", outcome: "match", files: [{ path: "src/codec.test.ts", source: "test('decodes', () => { try { decode(); } catch { return; } expect(result()).toBe('ok'); });" }], focusPath: "src/codec.test.ts", expectedCount: 1, public: true }
|
|
9077
|
+
{ id: "bare-return", title: "Do not silently pass", outcome: "match", files: [{ path: "src/codec.test.ts", source: "test('decodes', () => { try { decode(); } catch { return; } expect(result()).toBe('ok'); });" }], focusPath: "src/codec.test.ts", expectedCount: 1, public: true },
|
|
9078
|
+
{ id: "table-bare-return", scenarioId: "table-catch", title: "Case tables must preserve failures", outcome: "match", files: [{ path: "src/codec.test.ts", source: "test.each(['valid', 'invalid'])('decodes %s', (input) => { try { decode(input); } catch { return; } expect(result()).toBe('ok'); });" }], focusPath: "src/codec.test.ts", expectedCount: 1, public: true },
|
|
9079
|
+
{ id: "table-rethrow", scenarioId: "table-catch", title: "Each table case propagates its failure", outcome: "no-match", files: [{ path: "src/codec.test.ts", source: "test.each(['valid', 'invalid'])('decodes %s', (input) => { try { decode(input); } catch (error) { throw error; } expect(result()).toBe('ok'); });" }], focusPath: "src/codec.test.ts", expectedCount: 0, public: true },
|
|
9080
|
+
{ id: "table-context-skip", scenarioId: "context-skip", title: "A Vitest context skip preserves an optional table case", outcome: "no-match", files: [{ path: "src/codec.test.ts", source: "import {test, expect} from 'vitest'; test.for(['optional'])('decodes %s', (input, context) => { try { decode(input); } catch { context.skip('codec unavailable'); return; } expect(result()).toBe('ok'); });" }], focusPath: "src/codec.test.ts", expectedCount: 0, public: true },
|
|
9081
|
+
{ id: "table-context-false-skip", scenarioId: "context-skip", title: "A false skip condition does not preserve the failure", outcome: "match", files: [{ path: "src/codec.test.ts", source: "import {test, expect} from 'vitest'; test.for(['optional'])('decodes %s', (input, context) => { try { decode(input); } catch { context.skip(false); return; } expect(result()).toBe('ok'); });" }], focusPath: "src/codec.test.ts", expectedCount: 1, public: true }
|
|
9078
9082
|
]
|
|
9079
9083
|
};
|
|
9080
9084
|
var TEST_NAMES = /* @__PURE__ */ new Set(["it", "test"]);
|
|
9081
9085
|
var TEST_MODIFIERS2 = /* @__PURE__ */ new Set(["concurrent", "fails", "only", "sequential", "skip"]);
|
|
9086
|
+
var TEST_BUILDERS = /* @__PURE__ */ new Set(["each", "for", "runIf", "skipIf"]);
|
|
9082
9087
|
var ASSERTION_NAMES = /* @__PURE__ */ new Set(["assert", "assertType", "expect", "expectTypeOf"]);
|
|
9083
9088
|
var FUNCTION_TYPES3 = /* @__PURE__ */ new Set([import_utils51.AST_NODE_TYPES.ArrowFunctionExpression, import_utils51.AST_NODE_TYPES.FunctionExpression, import_utils51.AST_NODE_TYPES.FunctionDeclaration]);
|
|
9084
9089
|
function staticMemberName3(node) {
|
|
@@ -9102,6 +9107,12 @@ function isDirectTestCallback(node, context) {
|
|
|
9102
9107
|
function testRoot(callee) {
|
|
9103
9108
|
callee = unwrapExpression(callee);
|
|
9104
9109
|
if (callee.type === import_utils51.AST_NODE_TYPES.Identifier) return callee;
|
|
9110
|
+
if (callee.type === import_utils51.AST_NODE_TYPES.CallExpression || callee.type === import_utils51.AST_NODE_TYPES.TaggedTemplateExpression) {
|
|
9111
|
+
const builder = unwrapExpression(callee.type === import_utils51.AST_NODE_TYPES.CallExpression ? callee.callee : callee.tag);
|
|
9112
|
+
if (builder.type !== import_utils51.AST_NODE_TYPES.MemberExpression) return null;
|
|
9113
|
+
const name = staticMemberName3(builder);
|
|
9114
|
+
return name !== null && (callee.type === import_utils51.AST_NODE_TYPES.TaggedTemplateExpression ? name === "each" || name === "for" : TEST_BUILDERS.has(name)) ? testRoot(builder.object) : null;
|
|
9115
|
+
}
|
|
9105
9116
|
if (callee.type !== import_utils51.AST_NODE_TYPES.MemberExpression) return null;
|
|
9106
9117
|
const modifier = staticMemberName3(callee);
|
|
9107
9118
|
return modifier !== null && TEST_MODIFIERS2.has(modifier) ? testRoot(callee.object) : null;
|
|
@@ -9125,11 +9136,67 @@ function isExplicitSkip(node, context) {
|
|
|
9125
9136
|
const root = rootIdentifier2(unwrappedNodeCallee.object);
|
|
9126
9137
|
return root !== null && TEST_NAMES.has(runtimeTestFrameworkName(context.sourceCode, root, "test") ?? "");
|
|
9127
9138
|
}
|
|
9128
|
-
function
|
|
9139
|
+
function isUnconditionalContextSkip(node, owner, context) {
|
|
9140
|
+
if (node.type !== import_utils51.AST_NODE_TYPES.CallExpression || node.optional) return false;
|
|
9141
|
+
const first = node.arguments[0] === void 0 ? void 0 : unwrapExpression(node.arguments[0]);
|
|
9142
|
+
const note = first?.type === import_utils51.AST_NODE_TYPES.Literal && typeof first.value === "string" || first?.type === import_utils51.AST_NODE_TYPES.TemplateLiteral && first.expressions.length === 0;
|
|
9143
|
+
const always = first?.type === import_utils51.AST_NODE_TYPES.Literal && first.value === true;
|
|
9144
|
+
if (node.arguments.length !== 0 && !(node.arguments.length === 1 && note) && !(node.arguments.length <= 2 && always)) return false;
|
|
9145
|
+
const parameter = testContextParameter(owner, context);
|
|
9146
|
+
const callee = unwrapExpression(node.callee);
|
|
9147
|
+
if (parameter?.type === import_utils51.AST_NODE_TYPES.Identifier && callee.type === import_utils51.AST_NODE_TYPES.MemberExpression && !callee.optional && staticMemberName3(callee) === "skip") {
|
|
9148
|
+
const receiver = unwrapExpression(callee.object);
|
|
9149
|
+
return receiver.type === import_utils51.AST_NODE_TYPES.Identifier && isStableParameterReference(receiver, parameter, context, true);
|
|
9150
|
+
}
|
|
9151
|
+
if (parameter?.type !== import_utils51.AST_NODE_TYPES.ObjectPattern || callee.type !== import_utils51.AST_NODE_TYPES.Identifier) return false;
|
|
9152
|
+
return parameter.properties.some((property) => property.type === import_utils51.AST_NODE_TYPES.Property && import_utils51.ASTUtils.getPropertyName(property) === "skip" && property.value.type === import_utils51.AST_NODE_TYPES.Identifier && isStableParameterReference(callee, property.value, context, false));
|
|
9153
|
+
}
|
|
9154
|
+
function testContextParameter(owner, context) {
|
|
9155
|
+
const call = directArgumentCall(owner);
|
|
9156
|
+
if (call === null) return null;
|
|
9157
|
+
const root = testRoot(call.callee);
|
|
9158
|
+
if (root === null) return null;
|
|
9159
|
+
const binding = import_utils51.ASTUtils.findVariable(context.sourceCode.getScope(root), root.name);
|
|
9160
|
+
const definition = binding?.defs[0];
|
|
9161
|
+
const runnerGlobal = binding === null || binding.defs.length === 0;
|
|
9162
|
+
const vitestImport = binding?.defs.length === 1 && definition?.node.type === import_utils51.AST_NODE_TYPES.ImportSpecifier && definition.node.parent.type === import_utils51.AST_NODE_TYPES.ImportDeclaration && definition.node.parent.source.value === "vitest";
|
|
9163
|
+
if (!runnerGlobal && !vitestImport) return null;
|
|
9164
|
+
const position = contextParameterPosition(call.callee);
|
|
9165
|
+
return position === null ? null : owner.params[position] ?? null;
|
|
9166
|
+
}
|
|
9167
|
+
function contextParameterPosition(callee) {
|
|
9168
|
+
callee = unwrapExpression(callee);
|
|
9169
|
+
if (callee.type === import_utils51.AST_NODE_TYPES.Identifier) return 0;
|
|
9170
|
+
if (callee.type === import_utils51.AST_NODE_TYPES.MemberExpression) return contextParameterPosition(callee.object);
|
|
9171
|
+
if (callee.type !== import_utils51.AST_NODE_TYPES.CallExpression && callee.type !== import_utils51.AST_NODE_TYPES.TaggedTemplateExpression) return null;
|
|
9172
|
+
const builder = unwrapExpression(callee.type === import_utils51.AST_NODE_TYPES.CallExpression ? callee.callee : callee.tag);
|
|
9173
|
+
if (builder.type !== import_utils51.AST_NODE_TYPES.MemberExpression) return null;
|
|
9174
|
+
const name = staticMemberName3(builder);
|
|
9175
|
+
const inherited = contextParameterPosition(builder.object);
|
|
9176
|
+
return name === "each" || inherited === null ? null : name === "for" ? 1 : inherited;
|
|
9177
|
+
}
|
|
9178
|
+
function isStableParameterReference(identifier, parameter, context, memberReceiver) {
|
|
9179
|
+
const source = context.sourceCode;
|
|
9180
|
+
const binding = import_utils51.ASTUtils.findVariable(source.getScope(parameter), parameter.name);
|
|
9181
|
+
if (binding === null || binding !== import_utils51.ASTUtils.findVariable(source.getScope(identifier), identifier.name)) return false;
|
|
9182
|
+
return binding.references.every((reference) => {
|
|
9183
|
+
if (reference.isWrite() && !reference.init) return false;
|
|
9184
|
+
if (!memberReceiver) return true;
|
|
9185
|
+
const receiver = outerExpression(reference.identifier);
|
|
9186
|
+
const member = receiver.parent;
|
|
9187
|
+
if (member?.type !== import_utils51.AST_NODE_TYPES.MemberExpression || member.object !== receiver) return true;
|
|
9188
|
+
const name = staticMemberName3(member);
|
|
9189
|
+
if (name !== null && name !== "skip") return true;
|
|
9190
|
+
const use = outerExpression(member);
|
|
9191
|
+
const parent = use.parent;
|
|
9192
|
+
return !(parent.type === import_utils51.AST_NODE_TYPES.AssignmentExpression && parent.left === use) && !(parent.type === import_utils51.AST_NODE_TYPES.UpdateExpression && parent.argument === use) && !(parent.type === import_utils51.AST_NODE_TYPES.UnaryExpression && parent.operator === "delete" && parent.argument === use);
|
|
9193
|
+
});
|
|
9194
|
+
}
|
|
9195
|
+
function hasDominatingExplicitSkip(node, owner, context) {
|
|
9129
9196
|
const block = node.parent;
|
|
9130
9197
|
if (block?.type !== import_utils51.AST_NODE_TYPES.BlockStatement) return false;
|
|
9131
9198
|
return block.body.some(
|
|
9132
|
-
(candidate2) => candidate2.range[1] <= node.range[0] && candidate2.type === import_utils51.AST_NODE_TYPES.ExpressionStatement && isExplicitSkip(candidate2.expression, context)
|
|
9199
|
+
(candidate2) => candidate2.range[1] <= node.range[0] && candidate2.type === import_utils51.AST_NODE_TYPES.ExpressionStatement && (isExplicitSkip(candidate2.expression, context) || isUnconditionalContextSkip(candidate2.expression, owner, context))
|
|
9133
9200
|
);
|
|
9134
9201
|
}
|
|
9135
9202
|
var no_bare_return_from_test_catch_default = createRule({
|
|
@@ -9163,7 +9230,7 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
9163
9230
|
if (parameter?.type === import_utils51.AST_NODE_TYPES.Identifier && returnBlock?.type === import_utils51.AST_NODE_TYPES.BlockStatement) {
|
|
9164
9231
|
const errorBinding = import_utils51.ASTUtils.findVariable(context.sourceCode.getScope(parameter), parameter.name);
|
|
9165
9232
|
const assertedError = returnBlock.body.some((statement) => {
|
|
9166
|
-
if (statement.range[1]
|
|
9233
|
+
if (statement.range[1] > node.range[0] || statement.type !== import_utils51.AST_NODE_TYPES.ExpressionStatement) return false;
|
|
9167
9234
|
const expression = statement.expression;
|
|
9168
9235
|
if (expression.type !== import_utils51.AST_NODE_TYPES.CallExpression || !isAssertion(expression, context)) return false;
|
|
9169
9236
|
const root = rootIdentifier2(expression.callee);
|
|
@@ -9176,7 +9243,7 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
9176
9243
|
});
|
|
9177
9244
|
if (assertedError) return;
|
|
9178
9245
|
}
|
|
9179
|
-
if (hasDominatingExplicitSkip(node, context)) return;
|
|
9246
|
+
if (hasDominatingExplicitSkip(node, owner, context)) return;
|
|
9180
9247
|
if (!walkOwnScope(owner.body, (current) => current.range[0] > node.range[1] && isAssertion(current, context))) return;
|
|
9181
9248
|
context.report({ node, messageId: "bareReturnFromTestCatch" });
|
|
9182
9249
|
}
|
|
@@ -13309,7 +13376,7 @@ var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
|
|
|
13309
13376
|
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.",
|
|
13310
13377
|
category: "testing",
|
|
13311
13378
|
limitations: [
|
|
13312
|
-
"Only
|
|
13379
|
+
"Only supported `expect` matcher calls in recognized test files are inspected, including imported aliases 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.",
|
|
13313
13380
|
"Scalar const bindings and aliases resolve through lexical scope; mutable bindings, objects, arrays, destructuring, runtime initializers and forward references are excluded.",
|
|
13314
13381
|
"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."
|
|
13315
13382
|
],
|
|
@@ -13342,6 +13409,26 @@ var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
|
|
|
13342
13409
|
expectedCount: 1,
|
|
13343
13410
|
public: true
|
|
13344
13411
|
},
|
|
13412
|
+
{
|
|
13413
|
+
id: "negated-setup-oracle",
|
|
13414
|
+
scenarioId: "negated-setup",
|
|
13415
|
+
title: "Negating a fixed matcher still does not observe the application",
|
|
13416
|
+
outcome: "match",
|
|
13417
|
+
files: [{ path: "src/service.test.ts", source: "import {expect as verify} from 'vitest'; it('works', () => { const ready = false; verify(ready).not.toBeTruthy(); });" }],
|
|
13418
|
+
focusPath: "src/service.test.ts",
|
|
13419
|
+
expectedCount: 1,
|
|
13420
|
+
public: true
|
|
13421
|
+
},
|
|
13422
|
+
{
|
|
13423
|
+
id: "negated-produced-oracle",
|
|
13424
|
+
scenarioId: "negated-setup",
|
|
13425
|
+
title: "A negated assertion can observe a runtime result",
|
|
13426
|
+
outcome: "no-match",
|
|
13427
|
+
files: [{ path: "src/service.test.ts", source: "import {expect as verify} from 'vitest'; it('works', () => { verify(serviceStatus()).not.toBeNull(); });" }],
|
|
13428
|
+
focusPath: "src/service.test.ts",
|
|
13429
|
+
expectedCount: 0,
|
|
13430
|
+
public: true
|
|
13431
|
+
},
|
|
13345
13432
|
{
|
|
13346
13433
|
id: "produced-scalar-oracle",
|
|
13347
13434
|
scenarioId: "setup-only",
|
|
@@ -13435,13 +13522,16 @@ function passesZeroArgumentMatcher(node, matcher) {
|
|
|
13435
13522
|
return false;
|
|
13436
13523
|
}
|
|
13437
13524
|
}
|
|
13438
|
-
function expectOperand(callee) {
|
|
13439
|
-
|
|
13525
|
+
function expectOperand(callee, sourceCode) {
|
|
13526
|
+
let receiver = unwrapExpression(callee.object);
|
|
13527
|
+
const negated = receiver.type === import_utils74.AST_NODE_TYPES.MemberExpression && import_utils74.ASTUtils.getPropertyName(receiver) === "not";
|
|
13528
|
+
if (negated && receiver.type === import_utils74.AST_NODE_TYPES.MemberExpression) receiver = unwrapExpression(receiver.object);
|
|
13440
13529
|
const expectCallee = receiver.type === import_utils74.AST_NODE_TYPES.CallExpression ? unwrapExpression(receiver.callee) : null;
|
|
13441
|
-
if (receiver.type !== import_utils74.AST_NODE_TYPES.CallExpression || expectCallee?.type !== import_utils74.AST_NODE_TYPES.Identifier || expectCallee
|
|
13530
|
+
if (receiver.type !== import_utils74.AST_NODE_TYPES.CallExpression || expectCallee?.type !== import_utils74.AST_NODE_TYPES.Identifier || runtimeTestFrameworkName(sourceCode, expectCallee, "assertion") !== "expect" || receiver.arguments.length !== 1) {
|
|
13442
13531
|
return null;
|
|
13443
13532
|
}
|
|
13444
|
-
|
|
13533
|
+
const operand = receiver.arguments[0];
|
|
13534
|
+
return operand === void 0 ? null : { operand, negated };
|
|
13445
13535
|
}
|
|
13446
13536
|
var no_tautological_expect_default = createRule({
|
|
13447
13537
|
name: "no-tautological-expect",
|
|
@@ -13472,33 +13562,25 @@ var no_tautological_expect_default = createRule({
|
|
|
13472
13562
|
if (callee.type !== import_utils74.AST_NODE_TYPES.MemberExpression) return;
|
|
13473
13563
|
const matcher = import_utils74.ASTUtils.getPropertyName(callee);
|
|
13474
13564
|
if (matcher === null) return;
|
|
13475
|
-
const
|
|
13476
|
-
if (
|
|
13477
|
-
const
|
|
13478
|
-
|
|
13479
|
-
const variable = import_utils74.ASTUtils.findVariable(context.sourceCode.getScope(expectIdentifier), expectIdentifier.name);
|
|
13480
|
-
if (variable !== null && variable.defs.some((definition) => {
|
|
13481
|
-
if (definition.node.type !== import_utils74.AST_NODE_TYPES.ImportSpecifier) return true;
|
|
13482
|
-
const declaration = definition.node.parent;
|
|
13483
|
-
const imported = definition.node.imported;
|
|
13484
|
-
return declaration.type !== import_utils74.AST_NODE_TYPES.ImportDeclaration || !["vitest", "@jest/globals", "@playwright/test", "bun:test"].includes(String(declaration.source.value)) || (imported.type === import_utils74.AST_NODE_TYPES.Identifier ? imported.name : imported.value) !== "expect";
|
|
13485
|
-
})) return;
|
|
13486
|
-
const writtenOperand = expectOperand(callee);
|
|
13487
|
-
const operand = writtenOperand === null ? null : resolveLiteral(writtenOperand, context.sourceCode);
|
|
13565
|
+
const assertion = expectOperand(callee, context.sourceCode);
|
|
13566
|
+
if (assertion === null) return;
|
|
13567
|
+
const writtenOperand = assertion.operand;
|
|
13568
|
+
const operand = resolveLiteral(writtenOperand, context.sourceCode);
|
|
13488
13569
|
if (operand === null) {
|
|
13489
13570
|
return;
|
|
13490
13571
|
}
|
|
13491
|
-
|
|
13572
|
+
const writtenMatcher = `${assertion.negated ? "not." : ""}${matcher}`;
|
|
13573
|
+
if (ZERO_ARG_MATCHERS.has(matcher) && node.arguments.length === 0 && passesZeroArgumentMatcher(operand, matcher) !== assertion.negated) {
|
|
13492
13574
|
context.report({
|
|
13493
13575
|
node,
|
|
13494
13576
|
messageId: "tautologicalMatcher",
|
|
13495
|
-
data: { operand: preview2(writtenOperand
|
|
13577
|
+
data: { operand: preview2(writtenOperand), matcher: writtenMatcher }
|
|
13496
13578
|
});
|
|
13497
13579
|
return;
|
|
13498
13580
|
}
|
|
13499
13581
|
const writtenExpected = node.arguments[0];
|
|
13500
13582
|
const expected = writtenExpected === void 0 ? null : resolveLiteral(writtenExpected, context.sourceCode);
|
|
13501
|
-
if (!EQUALITY_MATCHERS.has(matcher) || node.arguments.length !== 1 || expected === null) {
|
|
13583
|
+
if (assertion.negated || !EQUALITY_MATCHERS.has(matcher) || node.arguments.length !== 1 || expected === null) {
|
|
13502
13584
|
return;
|
|
13503
13585
|
}
|
|
13504
13586
|
if (matcher === "toBe" && (isStructuralLiteral(operand) || isStructuralLiteral(expected))) {
|
|
@@ -13510,7 +13592,7 @@ var no_tautological_expect_default = createRule({
|
|
|
13510
13592
|
context.report({
|
|
13511
13593
|
node,
|
|
13512
13594
|
messageId: "tautologicalComparison",
|
|
13513
|
-
data: { operand: preview2(writtenOperand
|
|
13595
|
+
data: { operand: preview2(writtenOperand), matcher }
|
|
13514
13596
|
});
|
|
13515
13597
|
}
|
|
13516
13598
|
};
|
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.15.
|
|
4
|
+
version: "16.15.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",
|
|
@@ -9052,14 +9052,19 @@ var NO_BARE_RETURN_FROM_TEST_CATCH_DOCUMENTATION = {
|
|
|
9052
9052
|
remediation: "Rethrow the error, assert on it, or use the runner's explicit skip mechanism when the capability is optional.",
|
|
9053
9053
|
category: "testing",
|
|
9054
9054
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
|
|
9055
|
-
limitations: ["Only bare returns owned by
|
|
9055
|
+
limitations: ["Only bare returns owned by an inline supported test callback and followed lexically by a framework assertion are reported, including each/for tables and runIf/skipIf builders. Custom builders and indirect callback references are excluded. A runner skip suppresses the finding only when it is an unconditional earlier statement in the return's block. Vitest imports and unshadowed runner globals support TestContext.skip on the first ordinary-test parameter or second for-table parameter, including a directly destructured skip alias; each-table data arguments, reassigned bindings, overwritten skip methods and dynamic skip conditions are excluded."],
|
|
9056
9056
|
examples: [
|
|
9057
9057
|
{ id: "rethrow", title: "Preserve the failure", outcome: "no-match", files: [{ path: "src/codec.test.ts", source: "test('decodes', () => { try { decode(); } catch (error) { throw error; } expect(result()).toBe('ok'); });" }], focusPath: "src/codec.test.ts", expectedCount: 0, public: true },
|
|
9058
|
-
{ id: "bare-return", title: "Do not silently pass", outcome: "match", files: [{ path: "src/codec.test.ts", source: "test('decodes', () => { try { decode(); } catch { return; } expect(result()).toBe('ok'); });" }], focusPath: "src/codec.test.ts", expectedCount: 1, public: true }
|
|
9058
|
+
{ id: "bare-return", title: "Do not silently pass", outcome: "match", files: [{ path: "src/codec.test.ts", source: "test('decodes', () => { try { decode(); } catch { return; } expect(result()).toBe('ok'); });" }], focusPath: "src/codec.test.ts", expectedCount: 1, public: true },
|
|
9059
|
+
{ id: "table-bare-return", scenarioId: "table-catch", title: "Case tables must preserve failures", outcome: "match", files: [{ path: "src/codec.test.ts", source: "test.each(['valid', 'invalid'])('decodes %s', (input) => { try { decode(input); } catch { return; } expect(result()).toBe('ok'); });" }], focusPath: "src/codec.test.ts", expectedCount: 1, public: true },
|
|
9060
|
+
{ id: "table-rethrow", scenarioId: "table-catch", title: "Each table case propagates its failure", outcome: "no-match", files: [{ path: "src/codec.test.ts", source: "test.each(['valid', 'invalid'])('decodes %s', (input) => { try { decode(input); } catch (error) { throw error; } expect(result()).toBe('ok'); });" }], focusPath: "src/codec.test.ts", expectedCount: 0, public: true },
|
|
9061
|
+
{ id: "table-context-skip", scenarioId: "context-skip", title: "A Vitest context skip preserves an optional table case", outcome: "no-match", files: [{ path: "src/codec.test.ts", source: "import {test, expect} from 'vitest'; test.for(['optional'])('decodes %s', (input, context) => { try { decode(input); } catch { context.skip('codec unavailable'); return; } expect(result()).toBe('ok'); });" }], focusPath: "src/codec.test.ts", expectedCount: 0, public: true },
|
|
9062
|
+
{ id: "table-context-false-skip", scenarioId: "context-skip", title: "A false skip condition does not preserve the failure", outcome: "match", files: [{ path: "src/codec.test.ts", source: "import {test, expect} from 'vitest'; test.for(['optional'])('decodes %s', (input, context) => { try { decode(input); } catch { context.skip(false); return; } expect(result()).toBe('ok'); });" }], focusPath: "src/codec.test.ts", expectedCount: 1, public: true }
|
|
9059
9063
|
]
|
|
9060
9064
|
};
|
|
9061
9065
|
var TEST_NAMES = /* @__PURE__ */ new Set(["it", "test"]);
|
|
9062
9066
|
var TEST_MODIFIERS2 = /* @__PURE__ */ new Set(["concurrent", "fails", "only", "sequential", "skip"]);
|
|
9067
|
+
var TEST_BUILDERS = /* @__PURE__ */ new Set(["each", "for", "runIf", "skipIf"]);
|
|
9063
9068
|
var ASSERTION_NAMES = /* @__PURE__ */ new Set(["assert", "assertType", "expect", "expectTypeOf"]);
|
|
9064
9069
|
var FUNCTION_TYPES3 = /* @__PURE__ */ new Set([AST_NODE_TYPES43.ArrowFunctionExpression, AST_NODE_TYPES43.FunctionExpression, AST_NODE_TYPES43.FunctionDeclaration]);
|
|
9065
9070
|
function staticMemberName3(node) {
|
|
@@ -9083,6 +9088,12 @@ function isDirectTestCallback(node, context) {
|
|
|
9083
9088
|
function testRoot(callee) {
|
|
9084
9089
|
callee = unwrapExpression(callee);
|
|
9085
9090
|
if (callee.type === AST_NODE_TYPES43.Identifier) return callee;
|
|
9091
|
+
if (callee.type === AST_NODE_TYPES43.CallExpression || callee.type === AST_NODE_TYPES43.TaggedTemplateExpression) {
|
|
9092
|
+
const builder = unwrapExpression(callee.type === AST_NODE_TYPES43.CallExpression ? callee.callee : callee.tag);
|
|
9093
|
+
if (builder.type !== AST_NODE_TYPES43.MemberExpression) return null;
|
|
9094
|
+
const name = staticMemberName3(builder);
|
|
9095
|
+
return name !== null && (callee.type === AST_NODE_TYPES43.TaggedTemplateExpression ? name === "each" || name === "for" : TEST_BUILDERS.has(name)) ? testRoot(builder.object) : null;
|
|
9096
|
+
}
|
|
9086
9097
|
if (callee.type !== AST_NODE_TYPES43.MemberExpression) return null;
|
|
9087
9098
|
const modifier = staticMemberName3(callee);
|
|
9088
9099
|
return modifier !== null && TEST_MODIFIERS2.has(modifier) ? testRoot(callee.object) : null;
|
|
@@ -9106,11 +9117,67 @@ function isExplicitSkip(node, context) {
|
|
|
9106
9117
|
const root = rootIdentifier2(unwrappedNodeCallee.object);
|
|
9107
9118
|
return root !== null && TEST_NAMES.has(runtimeTestFrameworkName(context.sourceCode, root, "test") ?? "");
|
|
9108
9119
|
}
|
|
9109
|
-
function
|
|
9120
|
+
function isUnconditionalContextSkip(node, owner, context) {
|
|
9121
|
+
if (node.type !== AST_NODE_TYPES43.CallExpression || node.optional) return false;
|
|
9122
|
+
const first = node.arguments[0] === void 0 ? void 0 : unwrapExpression(node.arguments[0]);
|
|
9123
|
+
const note = first?.type === AST_NODE_TYPES43.Literal && typeof first.value === "string" || first?.type === AST_NODE_TYPES43.TemplateLiteral && first.expressions.length === 0;
|
|
9124
|
+
const always = first?.type === AST_NODE_TYPES43.Literal && first.value === true;
|
|
9125
|
+
if (node.arguments.length !== 0 && !(node.arguments.length === 1 && note) && !(node.arguments.length <= 2 && always)) return false;
|
|
9126
|
+
const parameter = testContextParameter(owner, context);
|
|
9127
|
+
const callee = unwrapExpression(node.callee);
|
|
9128
|
+
if (parameter?.type === AST_NODE_TYPES43.Identifier && callee.type === AST_NODE_TYPES43.MemberExpression && !callee.optional && staticMemberName3(callee) === "skip") {
|
|
9129
|
+
const receiver = unwrapExpression(callee.object);
|
|
9130
|
+
return receiver.type === AST_NODE_TYPES43.Identifier && isStableParameterReference(receiver, parameter, context, true);
|
|
9131
|
+
}
|
|
9132
|
+
if (parameter?.type !== AST_NODE_TYPES43.ObjectPattern || callee.type !== AST_NODE_TYPES43.Identifier) return false;
|
|
9133
|
+
return parameter.properties.some((property) => property.type === AST_NODE_TYPES43.Property && ASTUtils22.getPropertyName(property) === "skip" && property.value.type === AST_NODE_TYPES43.Identifier && isStableParameterReference(callee, property.value, context, false));
|
|
9134
|
+
}
|
|
9135
|
+
function testContextParameter(owner, context) {
|
|
9136
|
+
const call = directArgumentCall(owner);
|
|
9137
|
+
if (call === null) return null;
|
|
9138
|
+
const root = testRoot(call.callee);
|
|
9139
|
+
if (root === null) return null;
|
|
9140
|
+
const binding = ASTUtils22.findVariable(context.sourceCode.getScope(root), root.name);
|
|
9141
|
+
const definition = binding?.defs[0];
|
|
9142
|
+
const runnerGlobal = binding === null || binding.defs.length === 0;
|
|
9143
|
+
const vitestImport = binding?.defs.length === 1 && definition?.node.type === AST_NODE_TYPES43.ImportSpecifier && definition.node.parent.type === AST_NODE_TYPES43.ImportDeclaration && definition.node.parent.source.value === "vitest";
|
|
9144
|
+
if (!runnerGlobal && !vitestImport) return null;
|
|
9145
|
+
const position = contextParameterPosition(call.callee);
|
|
9146
|
+
return position === null ? null : owner.params[position] ?? null;
|
|
9147
|
+
}
|
|
9148
|
+
function contextParameterPosition(callee) {
|
|
9149
|
+
callee = unwrapExpression(callee);
|
|
9150
|
+
if (callee.type === AST_NODE_TYPES43.Identifier) return 0;
|
|
9151
|
+
if (callee.type === AST_NODE_TYPES43.MemberExpression) return contextParameterPosition(callee.object);
|
|
9152
|
+
if (callee.type !== AST_NODE_TYPES43.CallExpression && callee.type !== AST_NODE_TYPES43.TaggedTemplateExpression) return null;
|
|
9153
|
+
const builder = unwrapExpression(callee.type === AST_NODE_TYPES43.CallExpression ? callee.callee : callee.tag);
|
|
9154
|
+
if (builder.type !== AST_NODE_TYPES43.MemberExpression) return null;
|
|
9155
|
+
const name = staticMemberName3(builder);
|
|
9156
|
+
const inherited = contextParameterPosition(builder.object);
|
|
9157
|
+
return name === "each" || inherited === null ? null : name === "for" ? 1 : inherited;
|
|
9158
|
+
}
|
|
9159
|
+
function isStableParameterReference(identifier, parameter, context, memberReceiver) {
|
|
9160
|
+
const source = context.sourceCode;
|
|
9161
|
+
const binding = ASTUtils22.findVariable(source.getScope(parameter), parameter.name);
|
|
9162
|
+
if (binding === null || binding !== ASTUtils22.findVariable(source.getScope(identifier), identifier.name)) return false;
|
|
9163
|
+
return binding.references.every((reference) => {
|
|
9164
|
+
if (reference.isWrite() && !reference.init) return false;
|
|
9165
|
+
if (!memberReceiver) return true;
|
|
9166
|
+
const receiver = outerExpression(reference.identifier);
|
|
9167
|
+
const member = receiver.parent;
|
|
9168
|
+
if (member?.type !== AST_NODE_TYPES43.MemberExpression || member.object !== receiver) return true;
|
|
9169
|
+
const name = staticMemberName3(member);
|
|
9170
|
+
if (name !== null && name !== "skip") return true;
|
|
9171
|
+
const use = outerExpression(member);
|
|
9172
|
+
const parent = use.parent;
|
|
9173
|
+
return !(parent.type === AST_NODE_TYPES43.AssignmentExpression && parent.left === use) && !(parent.type === AST_NODE_TYPES43.UpdateExpression && parent.argument === use) && !(parent.type === AST_NODE_TYPES43.UnaryExpression && parent.operator === "delete" && parent.argument === use);
|
|
9174
|
+
});
|
|
9175
|
+
}
|
|
9176
|
+
function hasDominatingExplicitSkip(node, owner, context) {
|
|
9110
9177
|
const block = node.parent;
|
|
9111
9178
|
if (block?.type !== AST_NODE_TYPES43.BlockStatement) return false;
|
|
9112
9179
|
return block.body.some(
|
|
9113
|
-
(candidate2) => candidate2.range[1] <= node.range[0] && candidate2.type === AST_NODE_TYPES43.ExpressionStatement && isExplicitSkip(candidate2.expression, context)
|
|
9180
|
+
(candidate2) => candidate2.range[1] <= node.range[0] && candidate2.type === AST_NODE_TYPES43.ExpressionStatement && (isExplicitSkip(candidate2.expression, context) || isUnconditionalContextSkip(candidate2.expression, owner, context))
|
|
9114
9181
|
);
|
|
9115
9182
|
}
|
|
9116
9183
|
var no_bare_return_from_test_catch_default = createRule({
|
|
@@ -9144,7 +9211,7 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
9144
9211
|
if (parameter?.type === AST_NODE_TYPES43.Identifier && returnBlock?.type === AST_NODE_TYPES43.BlockStatement) {
|
|
9145
9212
|
const errorBinding = ASTUtils22.findVariable(context.sourceCode.getScope(parameter), parameter.name);
|
|
9146
9213
|
const assertedError = returnBlock.body.some((statement) => {
|
|
9147
|
-
if (statement.range[1]
|
|
9214
|
+
if (statement.range[1] > node.range[0] || statement.type !== AST_NODE_TYPES43.ExpressionStatement) return false;
|
|
9148
9215
|
const expression = statement.expression;
|
|
9149
9216
|
if (expression.type !== AST_NODE_TYPES43.CallExpression || !isAssertion(expression, context)) return false;
|
|
9150
9217
|
const root = rootIdentifier2(expression.callee);
|
|
@@ -9157,7 +9224,7 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
9157
9224
|
});
|
|
9158
9225
|
if (assertedError) return;
|
|
9159
9226
|
}
|
|
9160
|
-
if (hasDominatingExplicitSkip(node, context)) return;
|
|
9227
|
+
if (hasDominatingExplicitSkip(node, owner, context)) return;
|
|
9161
9228
|
if (!walkOwnScope(owner.body, (current) => current.range[0] > node.range[1] && isAssertion(current, context))) return;
|
|
9162
9229
|
context.report({ node, messageId: "bareReturnFromTestCatch" });
|
|
9163
9230
|
}
|
|
@@ -13290,7 +13357,7 @@ var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
|
|
|
13290
13357
|
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.",
|
|
13291
13358
|
category: "testing",
|
|
13292
13359
|
limitations: [
|
|
13293
|
-
"Only
|
|
13360
|
+
"Only supported `expect` matcher calls in recognized test files are inspected, including imported aliases 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.",
|
|
13294
13361
|
"Scalar const bindings and aliases resolve through lexical scope; mutable bindings, objects, arrays, destructuring, runtime initializers and forward references are excluded.",
|
|
13295
13362
|
"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."
|
|
13296
13363
|
],
|
|
@@ -13323,6 +13390,26 @@ var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
|
|
|
13323
13390
|
expectedCount: 1,
|
|
13324
13391
|
public: true
|
|
13325
13392
|
},
|
|
13393
|
+
{
|
|
13394
|
+
id: "negated-setup-oracle",
|
|
13395
|
+
scenarioId: "negated-setup",
|
|
13396
|
+
title: "Negating a fixed matcher still does not observe the application",
|
|
13397
|
+
outcome: "match",
|
|
13398
|
+
files: [{ path: "src/service.test.ts", source: "import {expect as verify} from 'vitest'; it('works', () => { const ready = false; verify(ready).not.toBeTruthy(); });" }],
|
|
13399
|
+
focusPath: "src/service.test.ts",
|
|
13400
|
+
expectedCount: 1,
|
|
13401
|
+
public: true
|
|
13402
|
+
},
|
|
13403
|
+
{
|
|
13404
|
+
id: "negated-produced-oracle",
|
|
13405
|
+
scenarioId: "negated-setup",
|
|
13406
|
+
title: "A negated assertion can observe a runtime result",
|
|
13407
|
+
outcome: "no-match",
|
|
13408
|
+
files: [{ path: "src/service.test.ts", source: "import {expect as verify} from 'vitest'; it('works', () => { verify(serviceStatus()).not.toBeNull(); });" }],
|
|
13409
|
+
focusPath: "src/service.test.ts",
|
|
13410
|
+
expectedCount: 0,
|
|
13411
|
+
public: true
|
|
13412
|
+
},
|
|
13326
13413
|
{
|
|
13327
13414
|
id: "produced-scalar-oracle",
|
|
13328
13415
|
scenarioId: "setup-only",
|
|
@@ -13416,13 +13503,16 @@ function passesZeroArgumentMatcher(node, matcher) {
|
|
|
13416
13503
|
return false;
|
|
13417
13504
|
}
|
|
13418
13505
|
}
|
|
13419
|
-
function expectOperand(callee) {
|
|
13420
|
-
|
|
13506
|
+
function expectOperand(callee, sourceCode) {
|
|
13507
|
+
let receiver = unwrapExpression(callee.object);
|
|
13508
|
+
const negated = receiver.type === AST_NODE_TYPES60.MemberExpression && ASTUtils36.getPropertyName(receiver) === "not";
|
|
13509
|
+
if (negated && receiver.type === AST_NODE_TYPES60.MemberExpression) receiver = unwrapExpression(receiver.object);
|
|
13421
13510
|
const expectCallee = receiver.type === AST_NODE_TYPES60.CallExpression ? unwrapExpression(receiver.callee) : null;
|
|
13422
|
-
if (receiver.type !== AST_NODE_TYPES60.CallExpression || expectCallee?.type !== AST_NODE_TYPES60.Identifier || expectCallee
|
|
13511
|
+
if (receiver.type !== AST_NODE_TYPES60.CallExpression || expectCallee?.type !== AST_NODE_TYPES60.Identifier || runtimeTestFrameworkName(sourceCode, expectCallee, "assertion") !== "expect" || receiver.arguments.length !== 1) {
|
|
13423
13512
|
return null;
|
|
13424
13513
|
}
|
|
13425
|
-
|
|
13514
|
+
const operand = receiver.arguments[0];
|
|
13515
|
+
return operand === void 0 ? null : { operand, negated };
|
|
13426
13516
|
}
|
|
13427
13517
|
var no_tautological_expect_default = createRule({
|
|
13428
13518
|
name: "no-tautological-expect",
|
|
@@ -13453,33 +13543,25 @@ var no_tautological_expect_default = createRule({
|
|
|
13453
13543
|
if (callee.type !== AST_NODE_TYPES60.MemberExpression) return;
|
|
13454
13544
|
const matcher = ASTUtils36.getPropertyName(callee);
|
|
13455
13545
|
if (matcher === null) return;
|
|
13456
|
-
const
|
|
13457
|
-
if (
|
|
13458
|
-
const
|
|
13459
|
-
|
|
13460
|
-
const variable = ASTUtils36.findVariable(context.sourceCode.getScope(expectIdentifier), expectIdentifier.name);
|
|
13461
|
-
if (variable !== null && variable.defs.some((definition) => {
|
|
13462
|
-
if (definition.node.type !== AST_NODE_TYPES60.ImportSpecifier) return true;
|
|
13463
|
-
const declaration = definition.node.parent;
|
|
13464
|
-
const imported = definition.node.imported;
|
|
13465
|
-
return declaration.type !== AST_NODE_TYPES60.ImportDeclaration || !["vitest", "@jest/globals", "@playwright/test", "bun:test"].includes(String(declaration.source.value)) || (imported.type === AST_NODE_TYPES60.Identifier ? imported.name : imported.value) !== "expect";
|
|
13466
|
-
})) return;
|
|
13467
|
-
const writtenOperand = expectOperand(callee);
|
|
13468
|
-
const operand = writtenOperand === null ? null : resolveLiteral(writtenOperand, context.sourceCode);
|
|
13546
|
+
const assertion = expectOperand(callee, context.sourceCode);
|
|
13547
|
+
if (assertion === null) return;
|
|
13548
|
+
const writtenOperand = assertion.operand;
|
|
13549
|
+
const operand = resolveLiteral(writtenOperand, context.sourceCode);
|
|
13469
13550
|
if (operand === null) {
|
|
13470
13551
|
return;
|
|
13471
13552
|
}
|
|
13472
|
-
|
|
13553
|
+
const writtenMatcher = `${assertion.negated ? "not." : ""}${matcher}`;
|
|
13554
|
+
if (ZERO_ARG_MATCHERS.has(matcher) && node.arguments.length === 0 && passesZeroArgumentMatcher(operand, matcher) !== assertion.negated) {
|
|
13473
13555
|
context.report({
|
|
13474
13556
|
node,
|
|
13475
13557
|
messageId: "tautologicalMatcher",
|
|
13476
|
-
data: { operand: preview2(writtenOperand
|
|
13558
|
+
data: { operand: preview2(writtenOperand), matcher: writtenMatcher }
|
|
13477
13559
|
});
|
|
13478
13560
|
return;
|
|
13479
13561
|
}
|
|
13480
13562
|
const writtenExpected = node.arguments[0];
|
|
13481
13563
|
const expected = writtenExpected === void 0 ? null : resolveLiteral(writtenExpected, context.sourceCode);
|
|
13482
|
-
if (!EQUALITY_MATCHERS.has(matcher) || node.arguments.length !== 1 || expected === null) {
|
|
13564
|
+
if (assertion.negated || !EQUALITY_MATCHERS.has(matcher) || node.arguments.length !== 1 || expected === null) {
|
|
13483
13565
|
return;
|
|
13484
13566
|
}
|
|
13485
13567
|
if (matcher === "toBe" && (isStructuralLiteral(operand) || isStructuralLiteral(expected))) {
|
|
@@ -13491,7 +13573,7 @@ var no_tautological_expect_default = createRule({
|
|
|
13491
13573
|
context.report({
|
|
13492
13574
|
node,
|
|
13493
13575
|
messageId: "tautologicalComparison",
|
|
13494
|
-
data: { operand: preview2(writtenOperand
|
|
13576
|
+
data: { operand: preview2(writtenOperand), matcher }
|
|
13495
13577
|
});
|
|
13496
13578
|
}
|
|
13497
13579
|
};
|