@sarj/eslint-plugin 16.15.0 → 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 +157 -46
- package/dist/index.js +157 -46
- package/dist/rule-examples.cjs +33 -0
- package/dist/rule-examples.js +33 -0
- 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",
|
|
@@ -3906,10 +3906,13 @@ var NO_CLIENT_SIDE_DATA_FETCHING_DOCUMENTATION = {
|
|
|
3906
3906
|
examples: [
|
|
3907
3907
|
{
|
|
3908
3908
|
id: "effect-without-fetch",
|
|
3909
|
-
title: "
|
|
3909
|
+
title: "Load data while rendering on the server",
|
|
3910
3910
|
outcome: "no-match",
|
|
3911
|
-
files: [
|
|
3912
|
-
|
|
3911
|
+
files: [
|
|
3912
|
+
{ path: "app/users/page.tsx", source: "import { loadUsers } from './users-client';\nexport default async function Users() { const users = await loadUsers(); return <pre>{JSON.stringify(users)}</pre>; }" },
|
|
3913
|
+
{ path: "app/users/users-client.ts", source: "export async function loadUsers(): Promise<unknown> { const response = await fetch('https://api.example.com/users'); return response.json(); }" }
|
|
3914
|
+
],
|
|
3915
|
+
focusPath: "app/users/page.tsx",
|
|
3913
3916
|
expectedCount: 0,
|
|
3914
3917
|
public: true
|
|
3915
3918
|
},
|
|
@@ -3917,7 +3920,7 @@ var NO_CLIENT_SIDE_DATA_FETCHING_DOCUMENTATION = {
|
|
|
3917
3920
|
id: "fetch-inside-effect",
|
|
3918
3921
|
title: "An effect starts a data request",
|
|
3919
3922
|
outcome: "match",
|
|
3920
|
-
files: [{ path: "src/users.tsx", source: "useEffect(() => { fetch('/api/users'); }, []);" }],
|
|
3923
|
+
files: [{ path: "src/users.tsx", source: "'use client'; import { useEffect } from 'react';\nexport function Users() { useEffect(() => { fetch('/api/users'); }, []); return null; }" }],
|
|
3921
3924
|
focusPath: "src/users.tsx",
|
|
3922
3925
|
expectedCount: 1,
|
|
3923
3926
|
public: true
|
|
@@ -6058,11 +6061,11 @@ var NO_FAT_TRY_BLOCKS_DOCUMENTATION = {
|
|
|
6058
6061
|
examples: [
|
|
6059
6062
|
{
|
|
6060
6063
|
id: "focused-try-block",
|
|
6061
|
-
title: "
|
|
6064
|
+
title: "Keep only operations with the same recovery policy in the try block",
|
|
6062
6065
|
outcome: "no-match",
|
|
6063
6066
|
files: [{
|
|
6064
6067
|
path: "src/load.ts",
|
|
6065
|
-
source: "function f() { try { const a = one(); const b = two(); const c = three(); } catch (error) { handle(error); } finish(); }"
|
|
6068
|
+
source: "function f() { try { const a = one(); const b = two(); const c = three(); } catch (error) { handle(error); } const d = four(); finish(); }"
|
|
6066
6069
|
}],
|
|
6067
6070
|
focusPath: "src/load.ts",
|
|
6068
6071
|
expectedCount: 0,
|
|
@@ -7850,7 +7853,7 @@ var NO_JSON_STRINGIFY_OBJECT_EQUALITY_DOCUMENTATION = {
|
|
|
7850
7853
|
id: "domain-comparator",
|
|
7851
7854
|
title: "Use an explicit structural comparator",
|
|
7852
7855
|
outcome: "no-match",
|
|
7853
|
-
files: [{ path: "src/compare.ts", source: "
|
|
7856
|
+
files: [{ path: "src/compare.ts", source: "type RecordState = { id: string; state: string };\nfunction sameRecord(actual: RecordState, expected: RecordState): boolean { return actual.id === expected.id && actual.state === expected.state; }\nconst same = sameRecord({ id: '1', state: 'ready' }, { state: 'ready', id: '1' });" }],
|
|
7854
7857
|
focusPath: "src/compare.ts",
|
|
7855
7858
|
expectedCount: 0,
|
|
7856
7859
|
public: true
|
|
@@ -9068,14 +9071,19 @@ var NO_BARE_RETURN_FROM_TEST_CATCH_DOCUMENTATION = {
|
|
|
9068
9071
|
remediation: "Rethrow the error, assert on it, or use the runner's explicit skip mechanism when the capability is optional.",
|
|
9069
9072
|
category: "testing",
|
|
9070
9073
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
|
|
9071
|
-
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."],
|
|
9072
9075
|
examples: [
|
|
9073
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 },
|
|
9074
|
-
{ 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 }
|
|
9075
9082
|
]
|
|
9076
9083
|
};
|
|
9077
9084
|
var TEST_NAMES = /* @__PURE__ */ new Set(["it", "test"]);
|
|
9078
9085
|
var TEST_MODIFIERS2 = /* @__PURE__ */ new Set(["concurrent", "fails", "only", "sequential", "skip"]);
|
|
9086
|
+
var TEST_BUILDERS = /* @__PURE__ */ new Set(["each", "for", "runIf", "skipIf"]);
|
|
9079
9087
|
var ASSERTION_NAMES = /* @__PURE__ */ new Set(["assert", "assertType", "expect", "expectTypeOf"]);
|
|
9080
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]);
|
|
9081
9089
|
function staticMemberName3(node) {
|
|
@@ -9099,6 +9107,12 @@ function isDirectTestCallback(node, context) {
|
|
|
9099
9107
|
function testRoot(callee) {
|
|
9100
9108
|
callee = unwrapExpression(callee);
|
|
9101
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
|
+
}
|
|
9102
9116
|
if (callee.type !== import_utils51.AST_NODE_TYPES.MemberExpression) return null;
|
|
9103
9117
|
const modifier = staticMemberName3(callee);
|
|
9104
9118
|
return modifier !== null && TEST_MODIFIERS2.has(modifier) ? testRoot(callee.object) : null;
|
|
@@ -9122,11 +9136,67 @@ function isExplicitSkip(node, context) {
|
|
|
9122
9136
|
const root = rootIdentifier2(unwrappedNodeCallee.object);
|
|
9123
9137
|
return root !== null && TEST_NAMES.has(runtimeTestFrameworkName(context.sourceCode, root, "test") ?? "");
|
|
9124
9138
|
}
|
|
9125
|
-
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) {
|
|
9126
9196
|
const block = node.parent;
|
|
9127
9197
|
if (block?.type !== import_utils51.AST_NODE_TYPES.BlockStatement) return false;
|
|
9128
9198
|
return block.body.some(
|
|
9129
|
-
(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))
|
|
9130
9200
|
);
|
|
9131
9201
|
}
|
|
9132
9202
|
var no_bare_return_from_test_catch_default = createRule({
|
|
@@ -9160,7 +9230,7 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
9160
9230
|
if (parameter?.type === import_utils51.AST_NODE_TYPES.Identifier && returnBlock?.type === import_utils51.AST_NODE_TYPES.BlockStatement) {
|
|
9161
9231
|
const errorBinding = import_utils51.ASTUtils.findVariable(context.sourceCode.getScope(parameter), parameter.name);
|
|
9162
9232
|
const assertedError = returnBlock.body.some((statement) => {
|
|
9163
|
-
if (statement.range[1]
|
|
9233
|
+
if (statement.range[1] > node.range[0] || statement.type !== import_utils51.AST_NODE_TYPES.ExpressionStatement) return false;
|
|
9164
9234
|
const expression = statement.expression;
|
|
9165
9235
|
if (expression.type !== import_utils51.AST_NODE_TYPES.CallExpression || !isAssertion(expression, context)) return false;
|
|
9166
9236
|
const root = rootIdentifier2(expression.callee);
|
|
@@ -9173,7 +9243,7 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
9173
9243
|
});
|
|
9174
9244
|
if (assertedError) return;
|
|
9175
9245
|
}
|
|
9176
|
-
if (hasDominatingExplicitSkip(node, context)) return;
|
|
9246
|
+
if (hasDominatingExplicitSkip(node, owner, context)) return;
|
|
9177
9247
|
if (!walkOwnScope(owner.body, (current) => current.range[0] > node.range[1] && isAssertion(current, context))) return;
|
|
9178
9248
|
context.report({ node, messageId: "bareReturnFromTestCatch" });
|
|
9179
9249
|
}
|
|
@@ -10578,8 +10648,9 @@ var NO_RESTRICTED_LIBRARY_LOAD_DOCUMENTATION = {
|
|
|
10578
10648
|
category: "architecture",
|
|
10579
10649
|
limitations: ["Only literal dynamic imports, unshadowed CommonJS loads/resolution calls, and runtime TypeScript import-equals declarations are checked; erased type imports are excluded. A configured restriction list is required."],
|
|
10580
10650
|
examples: [
|
|
10581
|
-
{ id: "
|
|
10582
|
-
{ id: "runtime-load", title: "Do not load a restricted library at runtime", outcome: "match", files: [{ path: "src/client.ts", source: "const client = require('axios');" }], focusPath: "src/client.ts", expectedCount: 1, public: true, ruleOptions: [{ libraries: [{ id: "LIB101", module: "axios", replacement: "Ky" }] }] }
|
|
10651
|
+
{ id: "configured-replacement", title: "Use the configured library replacement", outcome: "no-match", files: [{ path: "src/client.ts", source: "import ky from 'ky'; const client = ky;" }], focusPath: "src/client.ts", expectedCount: 0, public: true, ruleOptions: [{ libraries: [{ id: "LIB101", module: "axios", replacement: "Ky" }] }] },
|
|
10652
|
+
{ id: "runtime-load", title: "Do not load a restricted library at runtime", outcome: "match", files: [{ path: "src/client.ts", source: "const client = require('axios');" }], focusPath: "src/client.ts", expectedCount: 1, public: true, ruleOptions: [{ libraries: [{ id: "LIB101", module: "axios", replacement: "Ky" }] }] },
|
|
10653
|
+
{ id: "static-import-owned-upstream", title: "Leave static restrictions to the owning import rule", outcome: "no-match", files: [{ path: "src/client.ts", source: "import axios from 'axios';" }], focusPath: "src/client.ts", expectedCount: 0, ruleOptions: [{ libraries: [{ id: "LIB101", module: "axios", replacement: "Ky" }] }] }
|
|
10583
10654
|
]
|
|
10584
10655
|
};
|
|
10585
10656
|
function staticModule(node) {
|
|
@@ -13305,7 +13376,7 @@ var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
|
|
|
13305
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.",
|
|
13306
13377
|
category: "testing",
|
|
13307
13378
|
limitations: [
|
|
13308
|
-
"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.",
|
|
13309
13380
|
"Scalar const bindings and aliases resolve through lexical scope; mutable bindings, objects, arrays, destructuring, runtime initializers and forward references are excluded.",
|
|
13310
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."
|
|
13311
13382
|
],
|
|
@@ -13338,6 +13409,26 @@ var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
|
|
|
13338
13409
|
expectedCount: 1,
|
|
13339
13410
|
public: true
|
|
13340
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
|
+
},
|
|
13341
13432
|
{
|
|
13342
13433
|
id: "produced-scalar-oracle",
|
|
13343
13434
|
scenarioId: "setup-only",
|
|
@@ -13431,13 +13522,16 @@ function passesZeroArgumentMatcher(node, matcher) {
|
|
|
13431
13522
|
return false;
|
|
13432
13523
|
}
|
|
13433
13524
|
}
|
|
13434
|
-
function expectOperand(callee) {
|
|
13435
|
-
|
|
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);
|
|
13436
13529
|
const expectCallee = receiver.type === import_utils74.AST_NODE_TYPES.CallExpression ? unwrapExpression(receiver.callee) : null;
|
|
13437
|
-
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) {
|
|
13438
13531
|
return null;
|
|
13439
13532
|
}
|
|
13440
|
-
|
|
13533
|
+
const operand = receiver.arguments[0];
|
|
13534
|
+
return operand === void 0 ? null : { operand, negated };
|
|
13441
13535
|
}
|
|
13442
13536
|
var no_tautological_expect_default = createRule({
|
|
13443
13537
|
name: "no-tautological-expect",
|
|
@@ -13468,33 +13562,25 @@ var no_tautological_expect_default = createRule({
|
|
|
13468
13562
|
if (callee.type !== import_utils74.AST_NODE_TYPES.MemberExpression) return;
|
|
13469
13563
|
const matcher = import_utils74.ASTUtils.getPropertyName(callee);
|
|
13470
13564
|
if (matcher === null) return;
|
|
13471
|
-
const
|
|
13472
|
-
if (
|
|
13473
|
-
const
|
|
13474
|
-
|
|
13475
|
-
const variable = import_utils74.ASTUtils.findVariable(context.sourceCode.getScope(expectIdentifier), expectIdentifier.name);
|
|
13476
|
-
if (variable !== null && variable.defs.some((definition) => {
|
|
13477
|
-
if (definition.node.type !== import_utils74.AST_NODE_TYPES.ImportSpecifier) return true;
|
|
13478
|
-
const declaration = definition.node.parent;
|
|
13479
|
-
const imported = definition.node.imported;
|
|
13480
|
-
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";
|
|
13481
|
-
})) return;
|
|
13482
|
-
const writtenOperand = expectOperand(callee);
|
|
13483
|
-
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);
|
|
13484
13569
|
if (operand === null) {
|
|
13485
13570
|
return;
|
|
13486
13571
|
}
|
|
13487
|
-
|
|
13572
|
+
const writtenMatcher = `${assertion.negated ? "not." : ""}${matcher}`;
|
|
13573
|
+
if (ZERO_ARG_MATCHERS.has(matcher) && node.arguments.length === 0 && passesZeroArgumentMatcher(operand, matcher) !== assertion.negated) {
|
|
13488
13574
|
context.report({
|
|
13489
13575
|
node,
|
|
13490
13576
|
messageId: "tautologicalMatcher",
|
|
13491
|
-
data: { operand: preview2(writtenOperand
|
|
13577
|
+
data: { operand: preview2(writtenOperand), matcher: writtenMatcher }
|
|
13492
13578
|
});
|
|
13493
13579
|
return;
|
|
13494
13580
|
}
|
|
13495
13581
|
const writtenExpected = node.arguments[0];
|
|
13496
13582
|
const expected = writtenExpected === void 0 ? null : resolveLiteral(writtenExpected, context.sourceCode);
|
|
13497
|
-
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) {
|
|
13498
13584
|
return;
|
|
13499
13585
|
}
|
|
13500
13586
|
if (matcher === "toBe" && (isStructuralLiteral(operand) || isStructuralLiteral(expected))) {
|
|
@@ -13506,7 +13592,7 @@ var no_tautological_expect_default = createRule({
|
|
|
13506
13592
|
context.report({
|
|
13507
13593
|
node,
|
|
13508
13594
|
messageId: "tautologicalComparison",
|
|
13509
|
-
data: { operand: preview2(writtenOperand
|
|
13595
|
+
data: { operand: preview2(writtenOperand), matcher }
|
|
13510
13596
|
});
|
|
13511
13597
|
}
|
|
13512
13598
|
};
|
|
@@ -14084,7 +14170,7 @@ var NO_DECLARATION_COMMENT_WALL_DOCUMENTATION = {
|
|
|
14084
14170
|
id: "uncommented-members",
|
|
14085
14171
|
title: "Let clear member names stand alone",
|
|
14086
14172
|
outcome: "no-match",
|
|
14087
|
-
files: [{ path: "src/status.ts", source: "enum Status { Pending = 'pending',
|
|
14173
|
+
files: [{ path: "src/status.ts", source: "enum Status { Pending = 'pending', Finished = 'finished', Failed = 'failed' }" }],
|
|
14088
14174
|
focusPath: "src/status.ts",
|
|
14089
14175
|
expectedCount: 0,
|
|
14090
14176
|
public: true
|
|
@@ -14375,7 +14461,7 @@ var NO_TYPE_MEMBER_COMMENT_WALL_DOCUMENTATION = {
|
|
|
14375
14461
|
id: "uncommented-members",
|
|
14376
14462
|
title: "Let clear member names and types stand alone",
|
|
14377
14463
|
outcome: "no-match",
|
|
14378
|
-
files: [{ path: "src/credentials.ts", source: "interface Credentials { host
|
|
14464
|
+
files: [{ path: "src/credentials.ts", source: "interface Credentials { host?: string; port?: number; username?: string; password?: string; }" }],
|
|
14379
14465
|
focusPath: "src/credentials.ts",
|
|
14380
14466
|
expectedCount: 0,
|
|
14381
14467
|
public: true
|
|
@@ -14964,12 +15050,37 @@ var NO_ZOD_NATIVE_ENUM_DOCUMENTATION = {
|
|
|
14964
15050
|
},
|
|
14965
15051
|
{
|
|
14966
15052
|
id: "zod-native-enum",
|
|
14967
|
-
title: "
|
|
15053
|
+
title: "Declare literal values instead of an enum-like object",
|
|
14968
15054
|
outcome: "match",
|
|
14969
15055
|
files: [{ path: "src/status.ts", source: 'import { z } from "zod"; const S = z.nativeEnum({ Active: "active", Inactive: "inactive" });' }],
|
|
14970
15056
|
focusPath: "src/status.ts",
|
|
14971
15057
|
expectedCount: 1,
|
|
14972
15058
|
public: true
|
|
15059
|
+
},
|
|
15060
|
+
{
|
|
15061
|
+
id: "imported-typescript-enum",
|
|
15062
|
+
scenarioId: "imported-enum",
|
|
15063
|
+
title: "Do not hide an imported TypeScript enum behind z.enum",
|
|
15064
|
+
outcome: "match",
|
|
15065
|
+
files: [
|
|
15066
|
+
{ path: "src/status-values.ts", source: "export enum Status { Active = 'active', Inactive = 'inactive' }" },
|
|
15067
|
+
{ path: "src/status.ts", source: "import { z } from 'zod'; import { Status } from './status-values.js';\nexport const StatusSchema = z.enum(Status);" }
|
|
15068
|
+
],
|
|
15069
|
+
installedDependencies: [{ module: "zod", version: "4.6.7" }],
|
|
15070
|
+
focusPath: "src/status.ts",
|
|
15071
|
+
expectedCount: 1,
|
|
15072
|
+
public: true
|
|
15073
|
+
},
|
|
15074
|
+
{
|
|
15075
|
+
id: "schema-owned-string-values",
|
|
15076
|
+
scenarioId: "imported-enum",
|
|
15077
|
+
title: "Let the schema own its literal value domain",
|
|
15078
|
+
outcome: "no-match",
|
|
15079
|
+
files: [{ path: "src/status.ts", source: "import { z } from 'zod';\nexport const StatusSchema = z.enum(['active', 'inactive']);" }],
|
|
15080
|
+
installedDependencies: [{ module: "zod", version: "4.6.7" }],
|
|
15081
|
+
focusPath: "src/status.ts",
|
|
15082
|
+
expectedCount: 0,
|
|
15083
|
+
public: true
|
|
14973
15084
|
}
|
|
14974
15085
|
]
|
|
14975
15086
|
};
|
|
@@ -17882,7 +17993,7 @@ var PREFER_MODULE_LEVEL_REFINED_SCHEMA_DOCUMENTATION = {
|
|
|
17882
17993
|
outcome: "no-match",
|
|
17883
17994
|
files: [{
|
|
17884
17995
|
path: "src/options.ts",
|
|
17885
|
-
source: "import { z } from 'zod'; const
|
|
17996
|
+
source: "import { z } from 'zod'; const NameSchema = z.string().trim().min(1).max(128); export function parse(value: unknown) { return NameSchema.parse(value); }"
|
|
17886
17997
|
}],
|
|
17887
17998
|
focusPath: "src/options.ts",
|
|
17888
17999
|
expectedCount: 0,
|
|
@@ -19517,7 +19628,7 @@ var PREFER_SCHEMA_FOR_API_PAYLOAD_DOCUMENTATION = {
|
|
|
19517
19628
|
"Test fixtures, generated clients and recognized local-file reads are excluded. This is bounded local analysis, not a general control-flow or mutation proof."
|
|
19518
19629
|
],
|
|
19519
19630
|
examples: [
|
|
19520
|
-
{ id: "validated-payload", title: "Validate before property access", outcome: "no-match", files: [{ path: "src/client.ts", source: "
|
|
19631
|
+
{ id: "validated-payload", title: "Validate before property access", outcome: "no-match", files: [{ path: "src/client.ts", source: "import { z } from 'zod';\nconst UserSchema = z.object({ id: z.string() });\nasync function load(response: Response) { const body = UserSchema.parse(await response.json()); return body.id; }" }], installedDependencies: [{ module: "zod", version: "4.6.7" }], focusPath: "src/client.ts", expectedCount: 0, public: true },
|
|
19521
19632
|
{ id: "unvalidated-payload", title: "Do not trust response JSON directly", outcome: "match", files: [{ path: "src/client.ts", source: "async function load(response: Response) { const body = await response.json(); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 1, public: true }
|
|
19522
19633
|
]
|
|
19523
19634
|
};
|
|
@@ -20193,8 +20304,8 @@ var PREFER_SWITCH_FOR_REPEATED_EQUALITY_DOCUMENTATION = {
|
|
|
20193
20304
|
"The rule deliberately ignores compound predicates, loose equality, ranges, and chains that compare different discriminants."
|
|
20194
20305
|
],
|
|
20195
20306
|
examples: [
|
|
20196
|
-
{ id: "switch-dispatch", title: "Make finite dispatch explicit", outcome: "no-match", files: [{ path: "src/render.ts", source: "switch (kind) { case 'a': return
|
|
20197
|
-
{ id: "repeated-equality", title: "Avoid repeating the discriminant", outcome: "match", files: [{ path: "src/render.ts", source: "function render(kind: string) { if (kind === 'a') return
|
|
20307
|
+
{ id: "switch-dispatch", title: "Make finite dispatch explicit", outcome: "no-match", files: [{ path: "src/render.ts", source: "function render(kind: string) { switch (kind) { case 'a': return 'alpha'; case 'b': return 'beta'; case 'c': return 'gamma'; default: return 'unknown'; } }" }], focusPath: "src/render.ts", expectedCount: 0, public: true },
|
|
20308
|
+
{ id: "repeated-equality", title: "Avoid repeating the discriminant", outcome: "match", files: [{ path: "src/render.ts", source: "function render(kind: string) { if (kind === 'a') return 'alpha'; else if (kind === 'b') return 'beta'; else if (kind === 'c') return 'gamma'; return 'unknown'; }" }], focusPath: "src/render.ts", expectedCount: 1, public: true },
|
|
20198
20309
|
{ id: "escaped-middle", scenarioId: "native-discriminant", title: "Use native identifier identity", outcome: "match", files: [{ path: "src/dispatch.ts", source: `function probe(kind){if(kind==='a')return 1;else if(\\u006bind==='b')return 2;else if(kind==='c')return 3;return 0;}
|
|
20199
20310
|
console.log(JSON.stringify(["a","b","c","d"].map(probe)));` }], focusPath: "src/dispatch.ts", expectedCount: 1, public: true },
|
|
20200
20311
|
{ id: "different-discriminants", scenarioId: "native-discriminant", title: "Use native identifier identity", outcome: "no-match", files: [{ path: "src/dispatch.ts", source: `function probe(kind){const other="other";if(kind==='a')return 1;else if(other==='b')return 2;else if(kind==='c')return 3;return 0;}
|
|
@@ -22732,7 +22843,7 @@ var REQUIRE_ASSERT_NEVER_DOCUMENTATION = {
|
|
|
22732
22843
|
category: "correctness",
|
|
22733
22844
|
limitations: ["Requires type information and singleton case values covering the current union. The helper must accept never to provide a compile-time check; its spelling alone is not a proof of that contract. Review the desired runtime behavior for unexpected external values."],
|
|
22734
22845
|
examples: [
|
|
22735
|
-
{ id: "assert-never-default", title: "Make the default exhaustive", outcome: "no-match", files: [{ path: "src/render.ts", source: "
|
|
22846
|
+
{ id: "assert-never-default", title: "Make the default exhaustive", outcome: "no-match", files: [{ path: "src/render.ts", source: "function assertNever(value: never): never { throw new Error(`Unexpected kind: ${String(value)}`); }\ndeclare const kind: 'a' | 'b';\nswitch (kind) { case 'a': break; case 'b': break; default: assertNever(kind); }" }], focusPath: "src/render.ts", expectedCount: 0, public: true },
|
|
22736
22847
|
{ id: "empty-default", title: "Do not leave an exhaustive default empty", outcome: "match", files: [{ path: "src/render.ts", source: "declare const kind: 'a' | 'b';\nswitch (kind) { case 'a': break; case 'b': break; default: }" }], focusPath: "src/render.ts", expectedCount: 1, public: true }
|
|
22737
22848
|
]
|
|
22738
22849
|
};
|
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",
|
|
@@ -3867,10 +3867,13 @@ var NO_CLIENT_SIDE_DATA_FETCHING_DOCUMENTATION = {
|
|
|
3867
3867
|
examples: [
|
|
3868
3868
|
{
|
|
3869
3869
|
id: "effect-without-fetch",
|
|
3870
|
-
title: "
|
|
3870
|
+
title: "Load data while rendering on the server",
|
|
3871
3871
|
outcome: "no-match",
|
|
3872
|
-
files: [
|
|
3873
|
-
|
|
3872
|
+
files: [
|
|
3873
|
+
{ path: "app/users/page.tsx", source: "import { loadUsers } from './users-client';\nexport default async function Users() { const users = await loadUsers(); return <pre>{JSON.stringify(users)}</pre>; }" },
|
|
3874
|
+
{ path: "app/users/users-client.ts", source: "export async function loadUsers(): Promise<unknown> { const response = await fetch('https://api.example.com/users'); return response.json(); }" }
|
|
3875
|
+
],
|
|
3876
|
+
focusPath: "app/users/page.tsx",
|
|
3874
3877
|
expectedCount: 0,
|
|
3875
3878
|
public: true
|
|
3876
3879
|
},
|
|
@@ -3878,7 +3881,7 @@ var NO_CLIENT_SIDE_DATA_FETCHING_DOCUMENTATION = {
|
|
|
3878
3881
|
id: "fetch-inside-effect",
|
|
3879
3882
|
title: "An effect starts a data request",
|
|
3880
3883
|
outcome: "match",
|
|
3881
|
-
files: [{ path: "src/users.tsx", source: "useEffect(() => { fetch('/api/users'); }, []);" }],
|
|
3884
|
+
files: [{ path: "src/users.tsx", source: "'use client'; import { useEffect } from 'react';\nexport function Users() { useEffect(() => { fetch('/api/users'); }, []); return null; }" }],
|
|
3882
3885
|
focusPath: "src/users.tsx",
|
|
3883
3886
|
expectedCount: 1,
|
|
3884
3887
|
public: true
|
|
@@ -6019,11 +6022,11 @@ var NO_FAT_TRY_BLOCKS_DOCUMENTATION = {
|
|
|
6019
6022
|
examples: [
|
|
6020
6023
|
{
|
|
6021
6024
|
id: "focused-try-block",
|
|
6022
|
-
title: "
|
|
6025
|
+
title: "Keep only operations with the same recovery policy in the try block",
|
|
6023
6026
|
outcome: "no-match",
|
|
6024
6027
|
files: [{
|
|
6025
6028
|
path: "src/load.ts",
|
|
6026
|
-
source: "function f() { try { const a = one(); const b = two(); const c = three(); } catch (error) { handle(error); } finish(); }"
|
|
6029
|
+
source: "function f() { try { const a = one(); const b = two(); const c = three(); } catch (error) { handle(error); } const d = four(); finish(); }"
|
|
6027
6030
|
}],
|
|
6028
6031
|
focusPath: "src/load.ts",
|
|
6029
6032
|
expectedCount: 0,
|
|
@@ -7818,7 +7821,7 @@ var NO_JSON_STRINGIFY_OBJECT_EQUALITY_DOCUMENTATION = {
|
|
|
7818
7821
|
id: "domain-comparator",
|
|
7819
7822
|
title: "Use an explicit structural comparator",
|
|
7820
7823
|
outcome: "no-match",
|
|
7821
|
-
files: [{ path: "src/compare.ts", source: "
|
|
7824
|
+
files: [{ path: "src/compare.ts", source: "type RecordState = { id: string; state: string };\nfunction sameRecord(actual: RecordState, expected: RecordState): boolean { return actual.id === expected.id && actual.state === expected.state; }\nconst same = sameRecord({ id: '1', state: 'ready' }, { state: 'ready', id: '1' });" }],
|
|
7822
7825
|
focusPath: "src/compare.ts",
|
|
7823
7826
|
expectedCount: 0,
|
|
7824
7827
|
public: true
|
|
@@ -9049,14 +9052,19 @@ var NO_BARE_RETURN_FROM_TEST_CATCH_DOCUMENTATION = {
|
|
|
9049
9052
|
remediation: "Rethrow the error, assert on it, or use the runner's explicit skip mechanism when the capability is optional.",
|
|
9050
9053
|
category: "testing",
|
|
9051
9054
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
|
|
9052
|
-
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."],
|
|
9053
9056
|
examples: [
|
|
9054
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 },
|
|
9055
|
-
{ 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 }
|
|
9056
9063
|
]
|
|
9057
9064
|
};
|
|
9058
9065
|
var TEST_NAMES = /* @__PURE__ */ new Set(["it", "test"]);
|
|
9059
9066
|
var TEST_MODIFIERS2 = /* @__PURE__ */ new Set(["concurrent", "fails", "only", "sequential", "skip"]);
|
|
9067
|
+
var TEST_BUILDERS = /* @__PURE__ */ new Set(["each", "for", "runIf", "skipIf"]);
|
|
9060
9068
|
var ASSERTION_NAMES = /* @__PURE__ */ new Set(["assert", "assertType", "expect", "expectTypeOf"]);
|
|
9061
9069
|
var FUNCTION_TYPES3 = /* @__PURE__ */ new Set([AST_NODE_TYPES43.ArrowFunctionExpression, AST_NODE_TYPES43.FunctionExpression, AST_NODE_TYPES43.FunctionDeclaration]);
|
|
9062
9070
|
function staticMemberName3(node) {
|
|
@@ -9080,6 +9088,12 @@ function isDirectTestCallback(node, context) {
|
|
|
9080
9088
|
function testRoot(callee) {
|
|
9081
9089
|
callee = unwrapExpression(callee);
|
|
9082
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
|
+
}
|
|
9083
9097
|
if (callee.type !== AST_NODE_TYPES43.MemberExpression) return null;
|
|
9084
9098
|
const modifier = staticMemberName3(callee);
|
|
9085
9099
|
return modifier !== null && TEST_MODIFIERS2.has(modifier) ? testRoot(callee.object) : null;
|
|
@@ -9103,11 +9117,67 @@ function isExplicitSkip(node, context) {
|
|
|
9103
9117
|
const root = rootIdentifier2(unwrappedNodeCallee.object);
|
|
9104
9118
|
return root !== null && TEST_NAMES.has(runtimeTestFrameworkName(context.sourceCode, root, "test") ?? "");
|
|
9105
9119
|
}
|
|
9106
|
-
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) {
|
|
9107
9177
|
const block = node.parent;
|
|
9108
9178
|
if (block?.type !== AST_NODE_TYPES43.BlockStatement) return false;
|
|
9109
9179
|
return block.body.some(
|
|
9110
|
-
(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))
|
|
9111
9181
|
);
|
|
9112
9182
|
}
|
|
9113
9183
|
var no_bare_return_from_test_catch_default = createRule({
|
|
@@ -9141,7 +9211,7 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
9141
9211
|
if (parameter?.type === AST_NODE_TYPES43.Identifier && returnBlock?.type === AST_NODE_TYPES43.BlockStatement) {
|
|
9142
9212
|
const errorBinding = ASTUtils22.findVariable(context.sourceCode.getScope(parameter), parameter.name);
|
|
9143
9213
|
const assertedError = returnBlock.body.some((statement) => {
|
|
9144
|
-
if (statement.range[1]
|
|
9214
|
+
if (statement.range[1] > node.range[0] || statement.type !== AST_NODE_TYPES43.ExpressionStatement) return false;
|
|
9145
9215
|
const expression = statement.expression;
|
|
9146
9216
|
if (expression.type !== AST_NODE_TYPES43.CallExpression || !isAssertion(expression, context)) return false;
|
|
9147
9217
|
const root = rootIdentifier2(expression.callee);
|
|
@@ -9154,7 +9224,7 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
9154
9224
|
});
|
|
9155
9225
|
if (assertedError) return;
|
|
9156
9226
|
}
|
|
9157
|
-
if (hasDominatingExplicitSkip(node, context)) return;
|
|
9227
|
+
if (hasDominatingExplicitSkip(node, owner, context)) return;
|
|
9158
9228
|
if (!walkOwnScope(owner.body, (current) => current.range[0] > node.range[1] && isAssertion(current, context))) return;
|
|
9159
9229
|
context.report({ node, messageId: "bareReturnFromTestCatch" });
|
|
9160
9230
|
}
|
|
@@ -10559,8 +10629,9 @@ var NO_RESTRICTED_LIBRARY_LOAD_DOCUMENTATION = {
|
|
|
10559
10629
|
category: "architecture",
|
|
10560
10630
|
limitations: ["Only literal dynamic imports, unshadowed CommonJS loads/resolution calls, and runtime TypeScript import-equals declarations are checked; erased type imports are excluded. A configured restriction list is required."],
|
|
10561
10631
|
examples: [
|
|
10562
|
-
{ id: "
|
|
10563
|
-
{ id: "runtime-load", title: "Do not load a restricted library at runtime", outcome: "match", files: [{ path: "src/client.ts", source: "const client = require('axios');" }], focusPath: "src/client.ts", expectedCount: 1, public: true, ruleOptions: [{ libraries: [{ id: "LIB101", module: "axios", replacement: "Ky" }] }] }
|
|
10632
|
+
{ id: "configured-replacement", title: "Use the configured library replacement", outcome: "no-match", files: [{ path: "src/client.ts", source: "import ky from 'ky'; const client = ky;" }], focusPath: "src/client.ts", expectedCount: 0, public: true, ruleOptions: [{ libraries: [{ id: "LIB101", module: "axios", replacement: "Ky" }] }] },
|
|
10633
|
+
{ id: "runtime-load", title: "Do not load a restricted library at runtime", outcome: "match", files: [{ path: "src/client.ts", source: "const client = require('axios');" }], focusPath: "src/client.ts", expectedCount: 1, public: true, ruleOptions: [{ libraries: [{ id: "LIB101", module: "axios", replacement: "Ky" }] }] },
|
|
10634
|
+
{ id: "static-import-owned-upstream", title: "Leave static restrictions to the owning import rule", outcome: "no-match", files: [{ path: "src/client.ts", source: "import axios from 'axios';" }], focusPath: "src/client.ts", expectedCount: 0, ruleOptions: [{ libraries: [{ id: "LIB101", module: "axios", replacement: "Ky" }] }] }
|
|
10564
10635
|
]
|
|
10565
10636
|
};
|
|
10566
10637
|
function staticModule(node) {
|
|
@@ -13286,7 +13357,7 @@ var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
|
|
|
13286
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.",
|
|
13287
13358
|
category: "testing",
|
|
13288
13359
|
limitations: [
|
|
13289
|
-
"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.",
|
|
13290
13361
|
"Scalar const bindings and aliases resolve through lexical scope; mutable bindings, objects, arrays, destructuring, runtime initializers and forward references are excluded.",
|
|
13291
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."
|
|
13292
13363
|
],
|
|
@@ -13319,6 +13390,26 @@ var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
|
|
|
13319
13390
|
expectedCount: 1,
|
|
13320
13391
|
public: true
|
|
13321
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
|
+
},
|
|
13322
13413
|
{
|
|
13323
13414
|
id: "produced-scalar-oracle",
|
|
13324
13415
|
scenarioId: "setup-only",
|
|
@@ -13412,13 +13503,16 @@ function passesZeroArgumentMatcher(node, matcher) {
|
|
|
13412
13503
|
return false;
|
|
13413
13504
|
}
|
|
13414
13505
|
}
|
|
13415
|
-
function expectOperand(callee) {
|
|
13416
|
-
|
|
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);
|
|
13417
13510
|
const expectCallee = receiver.type === AST_NODE_TYPES60.CallExpression ? unwrapExpression(receiver.callee) : null;
|
|
13418
|
-
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) {
|
|
13419
13512
|
return null;
|
|
13420
13513
|
}
|
|
13421
|
-
|
|
13514
|
+
const operand = receiver.arguments[0];
|
|
13515
|
+
return operand === void 0 ? null : { operand, negated };
|
|
13422
13516
|
}
|
|
13423
13517
|
var no_tautological_expect_default = createRule({
|
|
13424
13518
|
name: "no-tautological-expect",
|
|
@@ -13449,33 +13543,25 @@ var no_tautological_expect_default = createRule({
|
|
|
13449
13543
|
if (callee.type !== AST_NODE_TYPES60.MemberExpression) return;
|
|
13450
13544
|
const matcher = ASTUtils36.getPropertyName(callee);
|
|
13451
13545
|
if (matcher === null) return;
|
|
13452
|
-
const
|
|
13453
|
-
if (
|
|
13454
|
-
const
|
|
13455
|
-
|
|
13456
|
-
const variable = ASTUtils36.findVariable(context.sourceCode.getScope(expectIdentifier), expectIdentifier.name);
|
|
13457
|
-
if (variable !== null && variable.defs.some((definition) => {
|
|
13458
|
-
if (definition.node.type !== AST_NODE_TYPES60.ImportSpecifier) return true;
|
|
13459
|
-
const declaration = definition.node.parent;
|
|
13460
|
-
const imported = definition.node.imported;
|
|
13461
|
-
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";
|
|
13462
|
-
})) return;
|
|
13463
|
-
const writtenOperand = expectOperand(callee);
|
|
13464
|
-
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);
|
|
13465
13550
|
if (operand === null) {
|
|
13466
13551
|
return;
|
|
13467
13552
|
}
|
|
13468
|
-
|
|
13553
|
+
const writtenMatcher = `${assertion.negated ? "not." : ""}${matcher}`;
|
|
13554
|
+
if (ZERO_ARG_MATCHERS.has(matcher) && node.arguments.length === 0 && passesZeroArgumentMatcher(operand, matcher) !== assertion.negated) {
|
|
13469
13555
|
context.report({
|
|
13470
13556
|
node,
|
|
13471
13557
|
messageId: "tautologicalMatcher",
|
|
13472
|
-
data: { operand: preview2(writtenOperand
|
|
13558
|
+
data: { operand: preview2(writtenOperand), matcher: writtenMatcher }
|
|
13473
13559
|
});
|
|
13474
13560
|
return;
|
|
13475
13561
|
}
|
|
13476
13562
|
const writtenExpected = node.arguments[0];
|
|
13477
13563
|
const expected = writtenExpected === void 0 ? null : resolveLiteral(writtenExpected, context.sourceCode);
|
|
13478
|
-
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) {
|
|
13479
13565
|
return;
|
|
13480
13566
|
}
|
|
13481
13567
|
if (matcher === "toBe" && (isStructuralLiteral(operand) || isStructuralLiteral(expected))) {
|
|
@@ -13487,7 +13573,7 @@ var no_tautological_expect_default = createRule({
|
|
|
13487
13573
|
context.report({
|
|
13488
13574
|
node,
|
|
13489
13575
|
messageId: "tautologicalComparison",
|
|
13490
|
-
data: { operand: preview2(writtenOperand
|
|
13576
|
+
data: { operand: preview2(writtenOperand), matcher }
|
|
13491
13577
|
});
|
|
13492
13578
|
}
|
|
13493
13579
|
};
|
|
@@ -14065,7 +14151,7 @@ var NO_DECLARATION_COMMENT_WALL_DOCUMENTATION = {
|
|
|
14065
14151
|
id: "uncommented-members",
|
|
14066
14152
|
title: "Let clear member names stand alone",
|
|
14067
14153
|
outcome: "no-match",
|
|
14068
|
-
files: [{ path: "src/status.ts", source: "enum Status { Pending = 'pending',
|
|
14154
|
+
files: [{ path: "src/status.ts", source: "enum Status { Pending = 'pending', Finished = 'finished', Failed = 'failed' }" }],
|
|
14069
14155
|
focusPath: "src/status.ts",
|
|
14070
14156
|
expectedCount: 0,
|
|
14071
14157
|
public: true
|
|
@@ -14356,7 +14442,7 @@ var NO_TYPE_MEMBER_COMMENT_WALL_DOCUMENTATION = {
|
|
|
14356
14442
|
id: "uncommented-members",
|
|
14357
14443
|
title: "Let clear member names and types stand alone",
|
|
14358
14444
|
outcome: "no-match",
|
|
14359
|
-
files: [{ path: "src/credentials.ts", source: "interface Credentials { host
|
|
14445
|
+
files: [{ path: "src/credentials.ts", source: "interface Credentials { host?: string; port?: number; username?: string; password?: string; }" }],
|
|
14360
14446
|
focusPath: "src/credentials.ts",
|
|
14361
14447
|
expectedCount: 0,
|
|
14362
14448
|
public: true
|
|
@@ -14952,12 +15038,37 @@ var NO_ZOD_NATIVE_ENUM_DOCUMENTATION = {
|
|
|
14952
15038
|
},
|
|
14953
15039
|
{
|
|
14954
15040
|
id: "zod-native-enum",
|
|
14955
|
-
title: "
|
|
15041
|
+
title: "Declare literal values instead of an enum-like object",
|
|
14956
15042
|
outcome: "match",
|
|
14957
15043
|
files: [{ path: "src/status.ts", source: 'import { z } from "zod"; const S = z.nativeEnum({ Active: "active", Inactive: "inactive" });' }],
|
|
14958
15044
|
focusPath: "src/status.ts",
|
|
14959
15045
|
expectedCount: 1,
|
|
14960
15046
|
public: true
|
|
15047
|
+
},
|
|
15048
|
+
{
|
|
15049
|
+
id: "imported-typescript-enum",
|
|
15050
|
+
scenarioId: "imported-enum",
|
|
15051
|
+
title: "Do not hide an imported TypeScript enum behind z.enum",
|
|
15052
|
+
outcome: "match",
|
|
15053
|
+
files: [
|
|
15054
|
+
{ path: "src/status-values.ts", source: "export enum Status { Active = 'active', Inactive = 'inactive' }" },
|
|
15055
|
+
{ path: "src/status.ts", source: "import { z } from 'zod'; import { Status } from './status-values.js';\nexport const StatusSchema = z.enum(Status);" }
|
|
15056
|
+
],
|
|
15057
|
+
installedDependencies: [{ module: "zod", version: "4.6.7" }],
|
|
15058
|
+
focusPath: "src/status.ts",
|
|
15059
|
+
expectedCount: 1,
|
|
15060
|
+
public: true
|
|
15061
|
+
},
|
|
15062
|
+
{
|
|
15063
|
+
id: "schema-owned-string-values",
|
|
15064
|
+
scenarioId: "imported-enum",
|
|
15065
|
+
title: "Let the schema own its literal value domain",
|
|
15066
|
+
outcome: "no-match",
|
|
15067
|
+
files: [{ path: "src/status.ts", source: "import { z } from 'zod';\nexport const StatusSchema = z.enum(['active', 'inactive']);" }],
|
|
15068
|
+
installedDependencies: [{ module: "zod", version: "4.6.7" }],
|
|
15069
|
+
focusPath: "src/status.ts",
|
|
15070
|
+
expectedCount: 0,
|
|
15071
|
+
public: true
|
|
14961
15072
|
}
|
|
14962
15073
|
]
|
|
14963
15074
|
};
|
|
@@ -17879,7 +17990,7 @@ var PREFER_MODULE_LEVEL_REFINED_SCHEMA_DOCUMENTATION = {
|
|
|
17879
17990
|
outcome: "no-match",
|
|
17880
17991
|
files: [{
|
|
17881
17992
|
path: "src/options.ts",
|
|
17882
|
-
source: "import { z } from 'zod'; const
|
|
17993
|
+
source: "import { z } from 'zod'; const NameSchema = z.string().trim().min(1).max(128); export function parse(value: unknown) { return NameSchema.parse(value); }"
|
|
17883
17994
|
}],
|
|
17884
17995
|
focusPath: "src/options.ts",
|
|
17885
17996
|
expectedCount: 0,
|
|
@@ -19525,7 +19636,7 @@ var PREFER_SCHEMA_FOR_API_PAYLOAD_DOCUMENTATION = {
|
|
|
19525
19636
|
"Test fixtures, generated clients and recognized local-file reads are excluded. This is bounded local analysis, not a general control-flow or mutation proof."
|
|
19526
19637
|
],
|
|
19527
19638
|
examples: [
|
|
19528
|
-
{ id: "validated-payload", title: "Validate before property access", outcome: "no-match", files: [{ path: "src/client.ts", source: "
|
|
19639
|
+
{ id: "validated-payload", title: "Validate before property access", outcome: "no-match", files: [{ path: "src/client.ts", source: "import { z } from 'zod';\nconst UserSchema = z.object({ id: z.string() });\nasync function load(response: Response) { const body = UserSchema.parse(await response.json()); return body.id; }" }], installedDependencies: [{ module: "zod", version: "4.6.7" }], focusPath: "src/client.ts", expectedCount: 0, public: true },
|
|
19529
19640
|
{ id: "unvalidated-payload", title: "Do not trust response JSON directly", outcome: "match", files: [{ path: "src/client.ts", source: "async function load(response: Response) { const body = await response.json(); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 1, public: true }
|
|
19530
19641
|
]
|
|
19531
19642
|
};
|
|
@@ -20201,8 +20312,8 @@ var PREFER_SWITCH_FOR_REPEATED_EQUALITY_DOCUMENTATION = {
|
|
|
20201
20312
|
"The rule deliberately ignores compound predicates, loose equality, ranges, and chains that compare different discriminants."
|
|
20202
20313
|
],
|
|
20203
20314
|
examples: [
|
|
20204
|
-
{ id: "switch-dispatch", title: "Make finite dispatch explicit", outcome: "no-match", files: [{ path: "src/render.ts", source: "switch (kind) { case 'a': return
|
|
20205
|
-
{ id: "repeated-equality", title: "Avoid repeating the discriminant", outcome: "match", files: [{ path: "src/render.ts", source: "function render(kind: string) { if (kind === 'a') return
|
|
20315
|
+
{ id: "switch-dispatch", title: "Make finite dispatch explicit", outcome: "no-match", files: [{ path: "src/render.ts", source: "function render(kind: string) { switch (kind) { case 'a': return 'alpha'; case 'b': return 'beta'; case 'c': return 'gamma'; default: return 'unknown'; } }" }], focusPath: "src/render.ts", expectedCount: 0, public: true },
|
|
20316
|
+
{ id: "repeated-equality", title: "Avoid repeating the discriminant", outcome: "match", files: [{ path: "src/render.ts", source: "function render(kind: string) { if (kind === 'a') return 'alpha'; else if (kind === 'b') return 'beta'; else if (kind === 'c') return 'gamma'; return 'unknown'; }" }], focusPath: "src/render.ts", expectedCount: 1, public: true },
|
|
20206
20317
|
{ id: "escaped-middle", scenarioId: "native-discriminant", title: "Use native identifier identity", outcome: "match", files: [{ path: "src/dispatch.ts", source: `function probe(kind){if(kind==='a')return 1;else if(\\u006bind==='b')return 2;else if(kind==='c')return 3;return 0;}
|
|
20207
20318
|
console.log(JSON.stringify(["a","b","c","d"].map(probe)));` }], focusPath: "src/dispatch.ts", expectedCount: 1, public: true },
|
|
20208
20319
|
{ id: "different-discriminants", scenarioId: "native-discriminant", title: "Use native identifier identity", outcome: "no-match", files: [{ path: "src/dispatch.ts", source: `function probe(kind){const other="other";if(kind==='a')return 1;else if(other==='b')return 2;else if(kind==='c')return 3;return 0;}
|
|
@@ -22747,7 +22858,7 @@ var REQUIRE_ASSERT_NEVER_DOCUMENTATION = {
|
|
|
22747
22858
|
category: "correctness",
|
|
22748
22859
|
limitations: ["Requires type information and singleton case values covering the current union. The helper must accept never to provide a compile-time check; its spelling alone is not a proof of that contract. Review the desired runtime behavior for unexpected external values."],
|
|
22749
22860
|
examples: [
|
|
22750
|
-
{ id: "assert-never-default", title: "Make the default exhaustive", outcome: "no-match", files: [{ path: "src/render.ts", source: "
|
|
22861
|
+
{ id: "assert-never-default", title: "Make the default exhaustive", outcome: "no-match", files: [{ path: "src/render.ts", source: "function assertNever(value: never): never { throw new Error(`Unexpected kind: ${String(value)}`); }\ndeclare const kind: 'a' | 'b';\nswitch (kind) { case 'a': break; case 'b': break; default: assertNever(kind); }" }], focusPath: "src/render.ts", expectedCount: 0, public: true },
|
|
22751
22862
|
{ id: "empty-default", title: "Do not leave an exhaustive default empty", outcome: "match", files: [{ path: "src/render.ts", source: "declare const kind: 'a' | 'b';\nswitch (kind) { case 'a': break; case 'b': break; default: }" }], focusPath: "src/render.ts", expectedCount: 1, public: true }
|
|
22752
22863
|
]
|
|
22753
22864
|
};
|
package/dist/rule-examples.cjs
CHANGED
|
@@ -48,6 +48,7 @@ async function verifyRuleExamples(rule, setup = {}) {
|
|
|
48
48
|
try {
|
|
49
49
|
for (const file of example.files) {
|
|
50
50
|
const path = fixturePath(root, file.path);
|
|
51
|
+
validateFixtureSyntax(example.id, file, path);
|
|
51
52
|
await (0, import_promises.mkdir)((0, import_node_path.dirname)(path), { recursive: true });
|
|
52
53
|
await (0, import_promises.writeFile)(path, file.source);
|
|
53
54
|
}
|
|
@@ -92,6 +93,14 @@ function fixturePath(root, path) {
|
|
|
92
93
|
if (!resolved.startsWith(root + import_node_path.sep)) throw new Error(`unsafe example path: ${path}`);
|
|
93
94
|
return resolved;
|
|
94
95
|
}
|
|
96
|
+
function validateFixtureSyntax(exampleId, file, path) {
|
|
97
|
+
if (!/\.[cm]?[jt]sx?$/u.test(file.path)) return;
|
|
98
|
+
try {
|
|
99
|
+
parser.parse(file.source, { filePath: path });
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw new Error(`${exampleId}: invalid fixture ${file.path}`, { cause: error });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
95
104
|
async function verifyExample(linter, config, example, root, ruleId) {
|
|
96
105
|
const focus = example.files.find((file) => file.path === example.focusPath);
|
|
97
106
|
if (focus === void 0) throw new Error(`${example.id}: focus file is missing`);
|
|
@@ -100,6 +109,19 @@ async function verifyExample(linter, config, example, root, ruleId) {
|
|
|
100
109
|
if (messages.some((message) => message.fatal || message.ruleId !== ruleId)) {
|
|
101
110
|
throw new Error(`${example.id}: invalid fixture: ${messages.map((message) => message.message).join("; ")}`);
|
|
102
111
|
}
|
|
112
|
+
const lines = linter.getSourceCode().lines;
|
|
113
|
+
if (messages.some((message) => !physicalDiagnosticLocation(message, lines))) {
|
|
114
|
+
throw new Error(`${example.id}: invalid diagnostic location`);
|
|
115
|
+
}
|
|
116
|
+
const locations = messages.map((message) => JSON.stringify([
|
|
117
|
+
message.ruleId,
|
|
118
|
+
message.messageId ?? message.message,
|
|
119
|
+
message.line,
|
|
120
|
+
message.column,
|
|
121
|
+
message.endLine,
|
|
122
|
+
message.endColumn
|
|
123
|
+
]));
|
|
124
|
+
if (new Set(locations).size !== locations.length) throw new Error(`${example.id}: duplicate diagnostic`);
|
|
103
125
|
if (messages.length !== example.expectedCount) {
|
|
104
126
|
throw new Error(`${example.id}: expected ${example.expectedCount} findings, received ${messages.length}`);
|
|
105
127
|
}
|
|
@@ -116,6 +138,17 @@ async function verifyExample(linter, config, example, root, ruleId) {
|
|
|
116
138
|
}
|
|
117
139
|
}
|
|
118
140
|
}
|
|
141
|
+
function physicalDiagnosticLocation(message, lines) {
|
|
142
|
+
if (!physicalPosition(lines, message.line, message.column)) return false;
|
|
143
|
+
if (message.endLine === void 0 && message.endColumn === void 0) return true;
|
|
144
|
+
if (!physicalPosition(lines, message.endLine, message.endColumn)) return false;
|
|
145
|
+
return message.endLine !== void 0 && message.endColumn !== void 0 && (message.endLine > message.line || message.endLine === message.line && message.endColumn >= message.column);
|
|
146
|
+
}
|
|
147
|
+
function physicalPosition(lines, line, column) {
|
|
148
|
+
if (line === void 0 || column === void 0 || !Number.isInteger(line) || !Number.isInteger(column)) return false;
|
|
149
|
+
const text = lines[line - 1];
|
|
150
|
+
return text !== void 0 && column >= 1 && column <= text.length + 1;
|
|
151
|
+
}
|
|
119
152
|
// Annotate the CommonJS export names for ESM import in node:
|
|
120
153
|
0 && (module.exports = {
|
|
121
154
|
verifyRuleExamples
|
package/dist/rule-examples.js
CHANGED
|
@@ -14,6 +14,7 @@ async function verifyRuleExamples(rule, setup = {}) {
|
|
|
14
14
|
try {
|
|
15
15
|
for (const file of example.files) {
|
|
16
16
|
const path = fixturePath(root, file.path);
|
|
17
|
+
validateFixtureSyntax(example.id, file, path);
|
|
17
18
|
await mkdir(dirname(path), { recursive: true });
|
|
18
19
|
await writeFile(path, file.source);
|
|
19
20
|
}
|
|
@@ -58,6 +59,14 @@ function fixturePath(root, path) {
|
|
|
58
59
|
if (!resolved.startsWith(root + sep)) throw new Error(`unsafe example path: ${path}`);
|
|
59
60
|
return resolved;
|
|
60
61
|
}
|
|
62
|
+
function validateFixtureSyntax(exampleId, file, path) {
|
|
63
|
+
if (!/\.[cm]?[jt]sx?$/u.test(file.path)) return;
|
|
64
|
+
try {
|
|
65
|
+
parser.parse(file.source, { filePath: path });
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new Error(`${exampleId}: invalid fixture ${file.path}`, { cause: error });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
61
70
|
async function verifyExample(linter, config, example, root, ruleId) {
|
|
62
71
|
const focus = example.files.find((file) => file.path === example.focusPath);
|
|
63
72
|
if (focus === void 0) throw new Error(`${example.id}: focus file is missing`);
|
|
@@ -66,6 +75,19 @@ async function verifyExample(linter, config, example, root, ruleId) {
|
|
|
66
75
|
if (messages.some((message) => message.fatal || message.ruleId !== ruleId)) {
|
|
67
76
|
throw new Error(`${example.id}: invalid fixture: ${messages.map((message) => message.message).join("; ")}`);
|
|
68
77
|
}
|
|
78
|
+
const lines = linter.getSourceCode().lines;
|
|
79
|
+
if (messages.some((message) => !physicalDiagnosticLocation(message, lines))) {
|
|
80
|
+
throw new Error(`${example.id}: invalid diagnostic location`);
|
|
81
|
+
}
|
|
82
|
+
const locations = messages.map((message) => JSON.stringify([
|
|
83
|
+
message.ruleId,
|
|
84
|
+
message.messageId ?? message.message,
|
|
85
|
+
message.line,
|
|
86
|
+
message.column,
|
|
87
|
+
message.endLine,
|
|
88
|
+
message.endColumn
|
|
89
|
+
]));
|
|
90
|
+
if (new Set(locations).size !== locations.length) throw new Error(`${example.id}: duplicate diagnostic`);
|
|
69
91
|
if (messages.length !== example.expectedCount) {
|
|
70
92
|
throw new Error(`${example.id}: expected ${example.expectedCount} findings, received ${messages.length}`);
|
|
71
93
|
}
|
|
@@ -82,6 +104,17 @@ async function verifyExample(linter, config, example, root, ruleId) {
|
|
|
82
104
|
}
|
|
83
105
|
}
|
|
84
106
|
}
|
|
107
|
+
function physicalDiagnosticLocation(message, lines) {
|
|
108
|
+
if (!physicalPosition(lines, message.line, message.column)) return false;
|
|
109
|
+
if (message.endLine === void 0 && message.endColumn === void 0) return true;
|
|
110
|
+
if (!physicalPosition(lines, message.endLine, message.endColumn)) return false;
|
|
111
|
+
return message.endLine !== void 0 && message.endColumn !== void 0 && (message.endLine > message.line || message.endLine === message.line && message.endColumn >= message.column);
|
|
112
|
+
}
|
|
113
|
+
function physicalPosition(lines, line, column) {
|
|
114
|
+
if (line === void 0 || column === void 0 || !Number.isInteger(line) || !Number.isInteger(column)) return false;
|
|
115
|
+
const text = lines[line - 1];
|
|
116
|
+
return text !== void 0 && column >= 1 && column <= text.length + 1;
|
|
117
|
+
}
|
|
85
118
|
export {
|
|
86
119
|
verifyRuleExamples
|
|
87
120
|
};
|