@sarj/eslint-plugin 2.13.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1
- package/dist/index.cjs +116 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +116 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,10 +12,41 @@ import sarj from "@sarj/eslint-plugin";
|
|
|
12
12
|
export default [...sarj.configs.recommended];
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
45 rules. Each rule's source under `src/rules/` carries its own `@fileoverview` rationale plus `meta.docs.description` + `meta.messages` — read the file for the full reasoning, including the false positives it deliberately does not fire on.
|
|
16
16
|
|
|
17
17
|
Presets: `recommended` (warn-first), `strict` (every rule at error), `style-guide` (formatting/naming subset).
|
|
18
18
|
|
|
19
|
+
## New in 2.14.0 — `no-tautological-expect`
|
|
20
|
+
|
|
21
|
+
The TS half of SARJ057. An `expect(...)` whose operands are all literals has
|
|
22
|
+
already decided its outcome before the test runs — `expect(true).toBe(true)`
|
|
23
|
+
passes if you delete the module under test.
|
|
24
|
+
|
|
25
|
+
Python has caught the assertion-*free* test since 0.15.0 (`SARJ043
|
|
26
|
+
zero-assertion-test`) and had no TypeScript counterpart, which is exactly how
|
|
27
|
+
`expect(true).toBe(true); // placeholder` survived in an internal suite named for
|
|
28
|
+
the behaviour it was supposed to check: the file *has* an assertion, so nothing
|
|
29
|
+
was looking at it.
|
|
30
|
+
|
|
31
|
+
| Rule | What it catches | Preset |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `no-tautological-expect` | `expect(<literal>).toBe/toEqual/toStrictEqual(<textually identical literal>)`, and `expect(<literal>).toBeDefined()/toBeTruthy()/toBeNull()/…` — a zero-argument matcher on a literal receiver. | warn / error |
|
|
34
|
+
|
|
35
|
+
**The narrowness is the rule.** The obvious generalisation — "flag a comparison
|
|
36
|
+
of a thing with itself" — measures ~95% false positives:
|
|
37
|
+
`expect(hash([o])).toEqual(hash([o]))` is a *determinism* test,
|
|
38
|
+
`expect(memo(x)).toBe(memo(x))` a *memoization* test, `expect(a).toEqual(a)` on a
|
|
39
|
+
value with custom equality a *reflexivity* test. All three can genuinely fail. So
|
|
40
|
+
an identifier, member-expression or call operand is never enough: both sides must
|
|
41
|
+
be literals, and textually identical ones. A modified chain (`.not`, `.resolves`,
|
|
42
|
+
`.rejects`), a spread, an interpolated template literal, and two *different*
|
|
43
|
+
literals are all left alone.
|
|
44
|
+
|
|
45
|
+
Measured before shipping: **3 hits across 5,819 `.ts`/`.tsx` files** (1,003 of
|
|
46
|
+
them test files, where the rule is active) — six internal repos plus `got`,
|
|
47
|
+
`hono`, `swr` and `trpc`. 3 true positives, **0 false positives**; every hit is
|
|
48
|
+
an abandoned placeholder.
|
|
49
|
+
|
|
19
50
|
## New in 2.13.0 — the anti-comment-verbosity family
|
|
20
51
|
|
|
21
52
|
From a 37,918-comment, nine-repo measurement study. All three are
|
package/dist/index.cjs
CHANGED
|
@@ -7160,6 +7160,107 @@ var trailing_value_narration_default = import_utils49.ESLintUtils.RuleCreator(
|
|
|
7160
7160
|
}
|
|
7161
7161
|
});
|
|
7162
7162
|
|
|
7163
|
+
// src/rules/no-tautological-expect.ts
|
|
7164
|
+
var import_utils50 = require("@typescript-eslint/utils");
|
|
7165
|
+
var EQUALITY_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
|
|
7166
|
+
var ZERO_ARG_MATCHERS = /* @__PURE__ */ new Set([
|
|
7167
|
+
"toBeDefined",
|
|
7168
|
+
"toBeUndefined",
|
|
7169
|
+
"toBeNull",
|
|
7170
|
+
"toBeTruthy",
|
|
7171
|
+
"toBeFalsy",
|
|
7172
|
+
"toBeNaN"
|
|
7173
|
+
]);
|
|
7174
|
+
var OPERAND_PREVIEW_CHARS = 40;
|
|
7175
|
+
var NUMERIC_SIGNS = /* @__PURE__ */ new Set(["-", "+"]);
|
|
7176
|
+
function isLiteral(node) {
|
|
7177
|
+
switch (node.type) {
|
|
7178
|
+
case import_utils50.AST_NODE_TYPES.Literal:
|
|
7179
|
+
return true;
|
|
7180
|
+
case import_utils50.AST_NODE_TYPES.TemplateLiteral:
|
|
7181
|
+
return node.expressions.length === 0;
|
|
7182
|
+
case import_utils50.AST_NODE_TYPES.UnaryExpression:
|
|
7183
|
+
return NUMERIC_SIGNS.has(node.operator) && isLiteral(node.argument);
|
|
7184
|
+
case import_utils50.AST_NODE_TYPES.ArrayExpression:
|
|
7185
|
+
return node.elements.every((element) => element !== null && isLiteral(element));
|
|
7186
|
+
case import_utils50.AST_NODE_TYPES.ObjectExpression:
|
|
7187
|
+
return node.properties.every(
|
|
7188
|
+
(property) => property.type === import_utils50.AST_NODE_TYPES.Property && !property.computed && isLiteral(property.value)
|
|
7189
|
+
);
|
|
7190
|
+
default:
|
|
7191
|
+
return false;
|
|
7192
|
+
}
|
|
7193
|
+
}
|
|
7194
|
+
function expectOperand(callee) {
|
|
7195
|
+
const receiver = callee.object;
|
|
7196
|
+
if (receiver.type !== import_utils50.AST_NODE_TYPES.CallExpression || receiver.callee.type !== import_utils50.AST_NODE_TYPES.Identifier || receiver.callee.name !== "expect" || receiver.arguments.length !== 1) {
|
|
7197
|
+
return null;
|
|
7198
|
+
}
|
|
7199
|
+
return receiver.arguments[0] ?? null;
|
|
7200
|
+
}
|
|
7201
|
+
var no_tautological_expect_default = import_utils50.ESLintUtils.RuleCreator(
|
|
7202
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
7203
|
+
)({
|
|
7204
|
+
name: "no-tautological-expect",
|
|
7205
|
+
meta: {
|
|
7206
|
+
type: "problem",
|
|
7207
|
+
docs: {
|
|
7208
|
+
description: "Disallow an assertion whose operands are all literals; its outcome is fixed before the code runs, so it can never fail."
|
|
7209
|
+
},
|
|
7210
|
+
schema: [],
|
|
7211
|
+
messages: {
|
|
7212
|
+
tautologicalComparison: "`expect({{operand}}).{{matcher}}({{operand}})` compares a literal with an identical literal \u2014 it passes even if the code under test is deleted. Assert on a value the code produced, or delete the test.",
|
|
7213
|
+
tautologicalMatcher: "`expect({{operand}}).{{matcher}}()` asserts on a literal, so its outcome is fixed before the code runs. Assert on a value the code produced, or delete the test."
|
|
7214
|
+
}
|
|
7215
|
+
},
|
|
7216
|
+
defaultOptions: [],
|
|
7217
|
+
create(context) {
|
|
7218
|
+
if (!isTestFile(context.filename)) {
|
|
7219
|
+
return {};
|
|
7220
|
+
}
|
|
7221
|
+
const preview2 = (node) => {
|
|
7222
|
+
const text = context.sourceCode.getText(node).replaceAll(/\s+/gu, " ");
|
|
7223
|
+
return text.length > OPERAND_PREVIEW_CHARS ? `${text.slice(0, OPERAND_PREVIEW_CHARS)}\u2026` : text;
|
|
7224
|
+
};
|
|
7225
|
+
return {
|
|
7226
|
+
CallExpression(node) {
|
|
7227
|
+
const callee = node.callee;
|
|
7228
|
+
if (callee.type !== import_utils50.AST_NODE_TYPES.MemberExpression || callee.computed) {
|
|
7229
|
+
return;
|
|
7230
|
+
}
|
|
7231
|
+
if (callee.property.type !== import_utils50.AST_NODE_TYPES.Identifier) {
|
|
7232
|
+
return;
|
|
7233
|
+
}
|
|
7234
|
+
const matcher = callee.property.name;
|
|
7235
|
+
const operand = expectOperand(callee);
|
|
7236
|
+
if (operand === null || !isLiteral(operand)) {
|
|
7237
|
+
return;
|
|
7238
|
+
}
|
|
7239
|
+
if (ZERO_ARG_MATCHERS.has(matcher) && node.arguments.length === 0) {
|
|
7240
|
+
context.report({
|
|
7241
|
+
node,
|
|
7242
|
+
messageId: "tautologicalMatcher",
|
|
7243
|
+
data: { operand: preview2(operand), matcher }
|
|
7244
|
+
});
|
|
7245
|
+
return;
|
|
7246
|
+
}
|
|
7247
|
+
const expected = node.arguments[0];
|
|
7248
|
+
if (!EQUALITY_MATCHERS.has(matcher) || node.arguments.length !== 1 || expected === void 0 || !isLiteral(expected)) {
|
|
7249
|
+
return;
|
|
7250
|
+
}
|
|
7251
|
+
if (context.sourceCode.getText(operand) !== context.sourceCode.getText(expected)) {
|
|
7252
|
+
return;
|
|
7253
|
+
}
|
|
7254
|
+
context.report({
|
|
7255
|
+
node,
|
|
7256
|
+
messageId: "tautologicalComparison",
|
|
7257
|
+
data: { operand: preview2(operand), matcher }
|
|
7258
|
+
});
|
|
7259
|
+
}
|
|
7260
|
+
};
|
|
7261
|
+
}
|
|
7262
|
+
});
|
|
7263
|
+
|
|
7163
7264
|
// src/index.ts
|
|
7164
7265
|
var rules = {
|
|
7165
7266
|
"enforce-file-structure": enforce_file_structure_default,
|
|
@@ -7205,12 +7306,13 @@ var rules = {
|
|
|
7205
7306
|
"prefer-module-level-constant": prefer_module_level_constant_default,
|
|
7206
7307
|
"jsdoc-restates-signature": jsdoc_restates_signature_default,
|
|
7207
7308
|
"no-restated-comment": no_restated_comment_default,
|
|
7208
|
-
"trailing-value-narration": trailing_value_narration_default
|
|
7309
|
+
"trailing-value-narration": trailing_value_narration_default,
|
|
7310
|
+
"no-tautological-expect": no_tautological_expect_default
|
|
7209
7311
|
};
|
|
7210
7312
|
var plugin = {
|
|
7211
7313
|
meta: {
|
|
7212
7314
|
name: "@sarj/eslint-plugin",
|
|
7213
|
-
version: "2.
|
|
7315
|
+
version: "2.14.0"
|
|
7214
7316
|
},
|
|
7215
7317
|
rules,
|
|
7216
7318
|
configs: {
|
|
@@ -7277,7 +7379,15 @@ var plugin = {
|
|
|
7277
7379
|
// wrong deletion is silent information loss.
|
|
7278
7380
|
"@sarj/no-restated-comment": "warn",
|
|
7279
7381
|
"@sarj/jsdoc-restates-signature": "warn",
|
|
7280
|
-
"@sarj/trailing-value-narration": "warn"
|
|
7382
|
+
"@sarj/trailing-value-narration": "warn",
|
|
7383
|
+
// The TS half of SARJ057 (2026-07). Python has caught the
|
|
7384
|
+
// assertion-FREE test since 0.15.0 (SARJ043) and had no TS
|
|
7385
|
+
// counterpart, which is how `expect(true).toBe(true); // placeholder`
|
|
7386
|
+
// survived in internal-automations: the file HAS an assertion.
|
|
7387
|
+
// Measured across 5,819 .ts/.tsx files (1,003 of them test files) in
|
|
7388
|
+
// six internal repos plus got / hono / swr / trpc: 3 hits, 3 true
|
|
7389
|
+
// positives, 0 false positives.
|
|
7390
|
+
"@sarj/no-tautological-expect": "warn"
|
|
7281
7391
|
}
|
|
7282
7392
|
},
|
|
7283
7393
|
strict: {
|
|
@@ -7345,7 +7455,9 @@ var plugin = {
|
|
|
7345
7455
|
// for the measured hit counts and false-positive rates.
|
|
7346
7456
|
"@sarj/no-restated-comment": "error",
|
|
7347
7457
|
"@sarj/jsdoc-restates-signature": "error",
|
|
7348
|
-
"@sarj/trailing-value-narration": "error"
|
|
7458
|
+
"@sarj/trailing-value-narration": "error",
|
|
7459
|
+
// TS half of SARJ057 — see the `recommended` block for the measurement.
|
|
7460
|
+
"@sarj/no-tautological-expect": "error"
|
|
7349
7461
|
}
|
|
7350
7462
|
}
|
|
7351
7463
|
}
|