ai-deslop 0.4.5 → 0.4.6

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 CHANGED
@@ -61,7 +61,8 @@ export default defineConfig({
61
61
  | --- | --- | --- |
62
62
  | `no-call-only-assertions` | Recommended | Flags tests whose assertions only check mock calls. |
63
63
  | `no-module-mocking` | Recommended | Rejects Vitest and Jest module mocks. |
64
- | `no-poor-substring-tests` | Recommended | Rejects testing that a constant string contains substrings. |
64
+ | `no-poor-substring-tests` | Recommended | Rejects testing that a constant string contains substrings. |
65
+ | `no-repeated-expect-call` | Recommended | Flags adjacent assertions that repeat the same call in test files. Reuse the result instead. No fixer. |
65
66
 
66
67
  ### Opt-in rules
67
68
 
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import { noWidenThenAssertRule } from "./rules/no-widen-then-assert.js";
18
18
  import { requireSafetyCommentForTypeAssertionRule } from "./rules/require-safety-comment-for-type-assertion.js";
19
19
  import { noCallOnlyAssertionsRule } from "./rules/no-call-only-assertions.js";
20
20
  import { noPoorSubstringTestsRule } from "./rules/no-poor-substring-tests.js";
21
+ import { noRepeatedExpectCallRule } from "./rules/no-repeated-expect-call.js";
21
22
  import { noPassThroughTypeAliasRule } from "./rules/no-pass-through-type-alias.js";
22
23
  import { noDuplicateTypesRule } from "./rules/no-duplicate-types.js";
23
24
  import { noDuplicateConstsRule } from "./rules/no-duplicate-consts.js";
@@ -44,6 +45,7 @@ export const recommendedRules = {
44
45
  "deslop/require-safety-comment-for-type-assertion": "error",
45
46
  "deslop/no-call-only-assertions": "error",
46
47
  "deslop/no-poor-substring-tests": "error",
48
+ "deslop/no-repeated-expect-call": "error",
47
49
  "deslop/no-pass-through-type-alias": "error",
48
50
  "typescript/no-deprecated": "error",
49
51
  "typescript/no-require-imports": "error",
@@ -82,6 +84,7 @@ const deslopPlugin = eslintCompatPlugin({
82
84
  "require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule,
83
85
  "no-call-only-assertions": noCallOnlyAssertionsRule,
84
86
  "no-poor-substring-tests": noPoorSubstringTestsRule,
87
+ "no-repeated-expect-call": noRepeatedExpectCallRule,
85
88
  "no-constant-substring-tests": noPoorSubstringTestsRule,
86
89
  "no-pass-through-type-alias": noPassThroughTypeAliasRule,
87
90
  "no-duplicate-types": noDuplicateTypesRule,
@@ -0,0 +1 @@
1
+ export declare const noRepeatedExpectCallRule: import("@oxlint/plugins").Rule;
@@ -0,0 +1,93 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import { resolveVariable } from "../shared/scope.js";
3
+ const TEST_FILE = /(?:^|[/\\])(?:__tests__|tests?)(?:[/\\]|$)|(?:[._](?:test|spec))\.[cm]?[jt]sx?$/i;
4
+ const TEST_MODULES = new Set(["bun:test", "@jest/globals", "vitest"]);
5
+ const CLOSING_TOKENS = new Set([")", "]", "}"]);
6
+ function isExpect(identifier, sourceCode) {
7
+ if (identifier.type !== "Identifier")
8
+ return false;
9
+ const variable = resolveVariable(sourceCode, identifier);
10
+ if (variable?.defs.length) {
11
+ return variable.defs.some((definition) => definition.type === "ImportBinding" &&
12
+ definition.node.type === "ImportSpecifier" &&
13
+ definition.node.imported.type === "Identifier" &&
14
+ definition.node.imported.name === "expect" &&
15
+ definition.parent?.type === "ImportDeclaration" &&
16
+ TEST_MODULES.has(definition.parent.source.value));
17
+ }
18
+ return identifier.name === "expect";
19
+ }
20
+ function assertionCall(statement, sourceCode) {
21
+ if (statement.type !== "ExpressionStatement")
22
+ return null;
23
+ let expression = statement.expression;
24
+ if (expression.type === "AwaitExpression")
25
+ expression = expression.argument;
26
+ if (expression.type !== "CallExpression" || expression.callee.type !== "MemberExpression")
27
+ return null;
28
+ let assertion = expression.callee.object;
29
+ while (assertion.type === "MemberExpression")
30
+ assertion = assertion.object;
31
+ if (assertion.type !== "CallExpression" ||
32
+ assertion.arguments.length !== 1 ||
33
+ !isExpect(assertion.callee, sourceCode))
34
+ return null;
35
+ let call = assertion.arguments[0];
36
+ if (call.type === "AwaitExpression")
37
+ call = call.argument;
38
+ return call.type === "CallExpression" ? call : null;
39
+ }
40
+ function sameCall(left, right, sourceCode) {
41
+ if (sourceCode.getText(left) === sourceCode.getText(right))
42
+ return true;
43
+ const leftTokens = sourceCode.getTokens(left);
44
+ const rightTokens = sourceCode.getTokens(right);
45
+ let leftIndex = 0;
46
+ let rightIndex = 0;
47
+ while (leftIndex < leftTokens.length && rightIndex < rightTokens.length) {
48
+ const leftToken = leftTokens[leftIndex];
49
+ const rightToken = rightTokens[rightIndex];
50
+ if (leftToken.value === "," && CLOSING_TOKENS.has(leftTokens[leftIndex + 1]?.value) &&
51
+ leftTokens[leftIndex - 1]?.value !== "[" && leftTokens[leftIndex - 1]?.value !== ",") {
52
+ leftIndex++;
53
+ continue;
54
+ }
55
+ if (rightToken.value === "," && CLOSING_TOKENS.has(rightTokens[rightIndex + 1]?.value) &&
56
+ rightTokens[rightIndex - 1]?.value !== "[" && rightTokens[rightIndex - 1]?.value !== ",") {
57
+ rightIndex++;
58
+ continue;
59
+ }
60
+ if (leftToken.type !== rightToken.type || leftToken.value !== rightToken.value)
61
+ return false;
62
+ leftIndex++;
63
+ rightIndex++;
64
+ }
65
+ return leftIndex === leftTokens.length && rightIndex === rightTokens.length;
66
+ }
67
+ export const noRepeatedExpectCallRule = defineRule({
68
+ meta: {
69
+ type: "suggestion",
70
+ docs: { description: "Disallow repeated calls across adjacent assertions; call once and reuse the result." },
71
+ schema: [],
72
+ messages: { repeatedCall: "Call once and reuse the result across assertions." },
73
+ },
74
+ createOnce(context) {
75
+ function checkStatements(node) {
76
+ let previous = null;
77
+ for (const statement of node.body) {
78
+ const call = assertionCall(statement, context.sourceCode);
79
+ if (call && previous && sameCall(previous, call, context.sourceCode)) {
80
+ context.report({ node: call, messageId: "repeatedCall" });
81
+ }
82
+ previous = call;
83
+ }
84
+ }
85
+ return {
86
+ before() {
87
+ return TEST_FILE.test(context.filename);
88
+ },
89
+ Program: checkStatements,
90
+ BlockStatement: checkStatements,
91
+ };
92
+ },
93
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-deslop",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "Opinionated Oxlint rules that reject low-evidence, low-signal, AI-slop code patterns.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -14,8 +14,8 @@
14
14
  "files": [
15
15
  "dist"
16
16
  ],
17
- "scripts": {
18
- "bench": "npm run build && node bench.mjs",
17
+ "scripts": {
18
+ "bench": "npm run build && node bench.mjs",
19
19
  "build": "tsc -p tsconfig.build.json",
20
20
  "prepublishOnly": "npm run build",
21
21
  "check": "npm run build && oxlint src && tsc --noEmit",