ai-deslop 0.4.3 → 0.4.5

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
@@ -1,75 +1,100 @@
1
- # ai-deslop
2
-
3
- Opinionated Oxlint rules that reject low-evidence, low-signal, AI-slop code patterns.
4
-
5
- Rules are vendored from [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) (generic rules) alongside additional deslop rules, all renamed under the `deslop/*` namespace. The vendored source is MIT licensed; see Credits below.
6
-
7
- ## Install
8
-
9
- ```bash
10
- npm install --save-dev ai-deslop
11
- # @oxlint/plugins comes along as a runtime dependency — keep it in sync with your oxlint version
12
- ```
13
-
14
- Ships compiled JavaScript — works with any oxlint JS-plugin runtime (Bun, Deno, or Node.js).
15
-
16
- ## Usage
17
-
18
- ```ts
19
- import { defineConfig } from "oxlint";
20
- import { configs as deslopConfigs } from "ai-deslop";
21
-
22
- export default defineConfig({
23
- jsPlugins: ["ai-deslop"],
24
- options: deslopConfigs.recommended.options,
25
- rules: deslopConfigs.recommended.rules
26
- });
27
- ```
28
-
29
- The recommended config enables type-aware linting for `typescript/no-deprecated` and `typescript/no-floating-promises`. Install `oxlint-tsgolint` when using it.
30
-
31
- ## Rules
32
-
33
- | Rule | Status | Description |
34
- | --- | --- | --- |
35
- | `no-array-filter-map` | Recommended | Rejects adjacent eager array filter/map passes; allows lazy iterator pipelines. |
36
- | `no-reduce-accumulator-copy` | Recommended | Rejects non-spread accumulator copies inside reducers. Enable native `oxc/no-accumulating-spread` alongside it. |
37
- | `no-chained-type-assertions` | Recommended | Rejects nested `as` / angle-bracket assertions that fabricate evidence. |
38
- | `no-conditional-empty-object-spread` | Recommended | Reports object spreads using a conditional `{}` branch to omit fields. |
39
- | `no-known-value-widening` | Recommended | Rejects known expressions flowing into explicit `unknown`, `object`, or open dictionaries. |
40
- | `no-module-mocking` | Recommended | Rejects Vitest and Jest module mocks. |
41
- | `no-object-parameters` | Recommended | Rejects `object` on function inputs. |
42
- | `no-reflect-apply` | Recommended | Rejects global `Reflect.apply`. |
43
- | `no-reflect-get` | Recommended | Rejects global `Reflect.get`. |
44
- | `no-runtime-typeof` | Recommended | Requires boundary parsing instead of ad hoc `typeof` narrowing. |
45
- | `no-shape-in-symbol-names` | Recommended | Rejects `shape` in locally owned symbol names. |
46
- | `no-unknown-parameters` | Recommended | Rejects `unknown` on function inputs. |
47
- | `no-unknown-returns` | Recommended | Rejects return contracts resolving to `unknown`. |
48
- | `no-unknown-type-aliases` | Recommended | Rejects aliases whose resolved type is `unknown`. |
49
- | `no-unsafe-dictionary-type` | Recommended | Rejects dictionary values based on unsafe escape hatches. |
50
- | `no-widen-then-assert` | Recommended | Rejects widening known evidence before narrowing assertions. |
51
- | `require-safety-comment-for-type-assertion` | Recommended | Requires an invariant justification for non-const assertions. |
52
- | `no-call-only-assertions` | Recommended | Flags tests whose assertions only check mock calls. |
53
- | `no-pass-through-type-alias` | Recommended | Disallows aliases that only rename another type. |
1
+ # ai-deslop
2
+
3
+ Opinionated Oxlint rules that reject low-evidence, low-signal, AI-slop code patterns.
4
+
5
+ Rules are vendored from [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) (generic rules) alongside additional deslop rules, all renamed under the `deslop/*` namespace. The vendored source is MIT licensed; see Credits below.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install --save-dev ai-deslop
11
+ # @oxlint/plugins comes along as a runtime dependency — keep it in sync with your oxlint version
12
+ ```
13
+
14
+ Ships compiled JavaScript — works with any oxlint JS-plugin runtime (Bun, Deno, or Node.js).
15
+
16
+ ## Usage
17
+
18
+ ```ts
19
+ import { defineConfig } from "oxlint";
20
+ import { configs as deslopConfigs } from "ai-deslop";
21
+
22
+ export default defineConfig({
23
+ jsPlugins: ["ai-deslop"],
24
+ options: deslopConfigs.recommended.options,
25
+ rules: deslopConfigs.recommended.rules
26
+ });
27
+ ```
28
+
29
+ ## Rules
30
+
31
+ ### Type safety and evidence
32
+
33
+ | Rule | Status | Description |
34
+ | --- | --- | --- |
35
+ | `no-chained-type-assertions` | Recommended | Rejects nested `as` / angle-bracket assertions that fabricate evidence. |
36
+ | `no-conditional-empty-object-spread` | Recommended | Reports object spreads using a conditional `{}` branch to omit fields. |
37
+ | `no-known-value-widening` | Recommended | Rejects known expressions flowing into explicit `unknown`, `object`, or open dictionaries. |
38
+ | `no-object-parameters` | Recommended | Rejects `object` on function inputs. |
39
+ | `no-runtime-typeof` | Recommended | Requires boundary parsing instead of ad hoc `typeof` narrowing. |
40
+ | `no-unknown-parameters` | Recommended | Rejects `unknown` on function inputs. |
41
+ | `no-unknown-returns` | Recommended | Rejects return contracts resolving to `unknown`. |
42
+ | `no-unknown-type-aliases` | Recommended | Rejects aliases whose resolved type is `unknown`. |
43
+ | `no-unsafe-dictionary-type` | Recommended | Rejects dictionary values based on unsafe escape hatches. |
44
+ | `no-widen-then-assert` | Recommended | Rejects widening known evidence before narrowing assertions. |
45
+ | `require-safety-comment-for-type-assertion` | Recommended | Requires an invariant justification for non-const assertions. |
46
+ | `no-pass-through-type-alias` | Recommended | Disallows aliases that only rename another type. |
47
+
48
+ ### Code quality and APIs
49
+
50
+ | Rule | Status | Description |
51
+ | --- | --- | --- |
52
+ | `no-array-filter-map` | Recommended | Rejects adjacent eager array filter/map passes; allows lazy iterator pipelines. |
53
+ | `no-reduce-accumulator-copy` | Recommended | Rejects non-spread accumulator copies inside reducers. Enable native `oxc/no-accumulating-spread` alongside it. |
54
+ | `no-reflect-apply` | Recommended | Rejects global `Reflect.apply`. |
55
+ | `no-reflect-get` | Recommended | Rejects global `Reflect.get`. |
56
+ | `no-shape-in-symbol-names` | Recommended | Rejects `shape` in locally owned symbol names. |
57
+
58
+ ### Tests
59
+
60
+ | Rule | Status | Description |
61
+ | --- | --- | --- |
62
+ | `no-call-only-assertions` | Recommended | Flags tests whose assertions only check mock calls. |
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. |
65
+
66
+ ### Opt-in rules
67
+
68
+ | Rule | Status | Description |
69
+ | --- | --- | --- |
70
+ | [`no-duplicate-consts`](docs/no-duplicate-consts.md) | Opt-in | Reports all-caps constants with the same static value in different files. |
54
71
  | [`no-duplicate-types`](docs/no-duplicate-types.md) | Opt-in | Reports exact duplicate object contracts. No fixer. |
72
+ | [`no-renamed-imports`](docs/no-renamed-imports.md) | Opt-in | Fixes unnecessary named-import aliases when the original name has no module-level conflict. |
55
73
  | [`canonical-class-names`](docs/tailwind.md) | Opt-in | Fixes non-canonical Tailwind CSS v4 class spellings. |
56
-
57
- | Standard rule | Status | Description |
58
- | --- | --- | --- |
59
- | `typescript/no-deprecated` | Recommended | Disallows using code marked `@deprecated`. Requires type-aware linting. |
60
- | `typescript/no-floating-promises` | Recommended | Disallows floating promises. Requires type-aware linting. |
61
-
62
- ## Guides
63
-
64
- | Guide | Contents |
65
- | --- | --- |
74
+
75
+ ### Standard rules enabled by the recommended config
76
+
77
+ | Standard rule | Status | Description |
78
+ | --- | --- | --- |
79
+ | `typescript/no-deprecated` | Recommended | Disallows using code marked `@deprecated`. Requires type-aware linting. |
80
+ | `typescript/no-require-imports` | Recommended | Disallows CommonJS `require()` imports. |
81
+ | `typescript/no-floating-promises` | Recommended | Disallows floating promises. Requires type-aware linting. |
82
+ | `typescript/no-unnecessary-condition` | Recommended | Disallows always truthy/falsy/nullish conditions. Requires type-aware linting. |
83
+ | `no-unreachable` | Recommended | Disallows unreachable code. |
84
+
85
+ ## Guides
86
+
87
+ | Guide | Contents |
88
+ | --- | --- |
89
+ | [No duplicate consts](docs/no-duplicate-consts.md) | Configuration and exact static-value comparison behavior. |
66
90
  | [No duplicate types](docs/no-duplicate-types.md) | Exact comparison behavior, worker scope, complete TypeScript/JSON examples, and every supported option. |
67
- | [Tailwind canonical class names](docs/tailwind.md) | Tailwind v4 setup, complete TypeScript/JSON examples, and every supported option. |
68
-
69
- ## Credits
70
-
71
- - Generic rules vendored verbatim from [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) (MIT, © Dillon Mulroy), including `src/shared/*`.
72
-
73
- ## License
74
-
75
- MIT
91
+ | [No renamed imports](docs/no-renamed-imports.md) | When named-import aliases are fixed and how conflicts are handled. |
92
+ | [Tailwind canonical class names](docs/tailwind.md) | Tailwind v4 setup, complete TypeScript/JSON examples, and every supported option. |
93
+
94
+ ## Credits
95
+
96
+ - Generic rules vendored verbatim from [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) (MIT, © Dillon Mulroy), including `src/shared/*`.
97
+
98
+ ## License
99
+
100
+ MIT
package/dist/index.js CHANGED
@@ -17,8 +17,11 @@ import { noUnsafeDictionaryTypeRule } from "./rules/no-unsafe-dictionary-type.js
17
17
  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
+ import { noPoorSubstringTestsRule } from "./rules/no-poor-substring-tests.js";
20
21
  import { noPassThroughTypeAliasRule } from "./rules/no-pass-through-type-alias.js";
21
22
  import { noDuplicateTypesRule } from "./rules/no-duplicate-types.js";
23
+ import { noDuplicateConstsRule } from "./rules/no-duplicate-consts.js";
24
+ import { noRenamedImportsRule } from "./rules/no-renamed-imports.js";
22
25
  import { canonicalClassNames } from "./tailwind/rule.js";
23
26
  /** Every `deslop/*` rule, plus selected standard rules, each set to `"error"`. Spread into the `rules` field of an oxlint config. */
24
27
  export const recommendedRules = {
@@ -40,9 +43,13 @@ export const recommendedRules = {
40
43
  "deslop/no-widen-then-assert": "error",
41
44
  "deslop/require-safety-comment-for-type-assertion": "error",
42
45
  "deslop/no-call-only-assertions": "error",
46
+ "deslop/no-poor-substring-tests": "error",
43
47
  "deslop/no-pass-through-type-alias": "error",
44
48
  "typescript/no-deprecated": "error",
49
+ "typescript/no-require-imports": "error",
45
50
  "typescript/no-floating-promises": "error",
51
+ "typescript/no-unnecessary-condition": "error",
52
+ "no-unreachable": "error",
46
53
  };
47
54
  /** ESLint flat-config style configs. `recommended` enables every recommended `deslop/*` rule as `"error"` (excluding opt-in rules), plus standard `typescript/*` rules. The `typescript/*` rules require type-aware linting (`options.typeAware`), so `recommended` sets it. */
48
55
  export const configs = {
@@ -74,8 +81,12 @@ const deslopPlugin = eslintCompatPlugin({
74
81
  "no-widen-then-assert": noWidenThenAssertRule,
75
82
  "require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule,
76
83
  "no-call-only-assertions": noCallOnlyAssertionsRule,
84
+ "no-poor-substring-tests": noPoorSubstringTestsRule,
85
+ "no-constant-substring-tests": noPoorSubstringTestsRule,
77
86
  "no-pass-through-type-alias": noPassThroughTypeAliasRule,
78
87
  "no-duplicate-types": noDuplicateTypesRule,
88
+ "no-duplicate-consts": noDuplicateConstsRule,
89
+ "no-renamed-imports": noRenamedImportsRule,
79
90
  // Opt-in: NOT in `recommendedRules`. Requires a `cssPath` option, so users add it manually.
80
91
  "canonical-class-names": canonicalClassNames,
81
92
  },
@@ -0,0 +1,2 @@
1
+ /** Report static all-caps constants duplicated by name and value in another file. */
2
+ export declare const noDuplicateConstsRule: import("@oxlint/plugins").Rule;
@@ -0,0 +1,68 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ function staticValue(node) {
3
+ if (node.type === "Literal") {
4
+ return JSON.stringify(["literal", typeof node.value, String(node.value)]);
5
+ }
6
+ if (node.type === "TemplateLiteral") {
7
+ return node.expressions.length === 0 ? JSON.stringify(["literal", "string", node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw ?? ""]) : null;
8
+ }
9
+ if (node.type === "UnaryExpression" && (node.operator === "+" || node.operator === "-") && node.argument.type === "Literal" && typeof node.argument.value === "number") {
10
+ return JSON.stringify(["literal", "number", `${node.operator}${node.argument.value}`]);
11
+ }
12
+ if (node.type === "ArrayExpression") {
13
+ const values = node.elements.map((element) => element === null || element.type === "SpreadElement" ? null : staticValue(element));
14
+ return values.some((value) => value === null) ? null : JSON.stringify(["array", values]);
15
+ }
16
+ if (node.type === "ObjectExpression") {
17
+ const properties = [];
18
+ for (const property of node.properties) {
19
+ if (property.type !== "Property" || property.computed || property.method || property.kind !== "init" || property.key.type !== "Identifier" && property.key.type !== "Literal")
20
+ return null;
21
+ const value = staticValue(property.value);
22
+ if (value === null)
23
+ return null;
24
+ const key = property.key.type === "Identifier" ? property.key.name : String(property.key.value);
25
+ properties.push([key, value]);
26
+ }
27
+ return JSON.stringify(["object", properties]);
28
+ }
29
+ return null;
30
+ }
31
+ /** Report static all-caps constants duplicated by name and value in another file. */
32
+ export const noDuplicateConstsRule = defineRule({
33
+ meta: {
34
+ type: "suggestion",
35
+ docs: { description: "Report all-caps constants duplicated by name and static value across files" },
36
+ messages: { duplicate: "{{name}} duplicates the constant in {{firstFile}}." },
37
+ schema: [],
38
+ },
39
+ createOnce(context) {
40
+ const seen = new Map();
41
+ return {
42
+ Program() {
43
+ // LSPs can re-lint a file in the same worker; discard stale entries first.
44
+ for (const [key, constant] of seen)
45
+ if (constant.filename === context.filename)
46
+ seen.delete(key);
47
+ },
48
+ VariableDeclaration(node) {
49
+ if (node.kind !== "const")
50
+ return;
51
+ for (const declaration of node.declarations) {
52
+ if (declaration.id.type !== "Identifier" || !/^[A-Z][A-Z0-9_]*$/.test(declaration.id.name) || declaration.init === null)
53
+ continue;
54
+ const value = staticValue(declaration.init);
55
+ if (value === null)
56
+ continue;
57
+ const key = `${declaration.id.name}:${value}`, first = seen.get(key);
58
+ if (first === undefined) {
59
+ seen.set(key, { filename: context.filename, name: declaration.id.name });
60
+ }
61
+ else if (first.filename !== context.filename) {
62
+ context.report({ node: declaration, messageId: "duplicate", data: { name: declaration.id.name, firstFile: first.filename } });
63
+ }
64
+ }
65
+ },
66
+ };
67
+ },
68
+ });
@@ -106,6 +106,10 @@ export const noDuplicateTypesRule = defineRule({
106
106
  };
107
107
  return {
108
108
  Program() {
109
+ // LSPs can re-lint a file in the same createOnce worker; discard its stale declarations first.
110
+ for (const [key, type] of seen)
111
+ if (type.filename === context.filename)
112
+ seen.delete(key);
109
113
  options = context.options[0] ?? {};
110
114
  zodNamespaces.clear();
111
115
  valibotNamespaces.clear();
@@ -0,0 +1,2 @@
1
+ /** Disallow testing that a constant string contains substrings. */
2
+ export declare const noPoorSubstringTestsRule: import("@oxlint/plugins").Rule;
@@ -0,0 +1,320 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { defineRule } from "@oxlint/plugins";
4
+ import { resolveVariable } from "../shared/scope.js";
5
+ const SUBSTRING_MATCHERS = new Set([
6
+ "toContain",
7
+ "toInclude",
8
+ "toMatch",
9
+ "include",
10
+ "contain",
11
+ "string",
12
+ "match",
13
+ ]);
14
+ const CHAIN_MODIFIERS = new Set([
15
+ "not",
16
+ "resolves",
17
+ "rejects",
18
+ "to",
19
+ "be",
20
+ "have",
21
+ "that",
22
+ "which",
23
+ "and",
24
+ "is",
25
+ "deep",
26
+ "nested",
27
+ "own",
28
+ ]);
29
+ const SUBSTRING_METHODS = new Set([
30
+ "includes",
31
+ "startsWith",
32
+ "endsWith",
33
+ "indexOf",
34
+ "search",
35
+ "match",
36
+ ]);
37
+ const ASSERT_MATCHERS = new Set([
38
+ "include",
39
+ "includes",
40
+ "notInclude",
41
+ "notIncludes",
42
+ "match",
43
+ "notMatch",
44
+ ]);
45
+ const fileContentCache = new Map();
46
+ function isTestFile(filename) {
47
+ if (!filename)
48
+ return false;
49
+ return /(?:^|[/\\])(?:__tests__|tests?)(?:[/\\]|$)|(?:[._](?:test|spec))\.[cm]?[jt]sx?$/i.test(filename);
50
+ }
51
+ function rootIdentifierName(node) {
52
+ let current = node;
53
+ while (true) {
54
+ if (current.type === "Identifier")
55
+ return current.name;
56
+ if (current.type === "MemberExpression") {
57
+ current = current.object;
58
+ }
59
+ else if (current.type === "CallExpression" || current.type === "TaggedTemplateExpression") {
60
+ current = current.type === "CallExpression" ? current.callee : current.tag;
61
+ }
62
+ else {
63
+ return undefined;
64
+ }
65
+ }
66
+ }
67
+ function unwrapExpression(expression) {
68
+ let current = expression;
69
+ while (true) {
70
+ if (current.type === "ParenthesizedExpression" ||
71
+ current.type === "TSAsExpression" ||
72
+ current.type === "TSTypeAssertion" ||
73
+ current.type === "TSNonNullExpression" ||
74
+ current.type === "TSSatisfiesExpression") {
75
+ current = current.expression;
76
+ }
77
+ else {
78
+ break;
79
+ }
80
+ }
81
+ return current;
82
+ }
83
+ function isStableConstVariable(variable, declarator) {
84
+ if (declarator.parent.type !== "VariableDeclaration")
85
+ return false;
86
+ if (declarator.parent.kind === "const")
87
+ return true;
88
+ return variable.references.every((ref) => ref.init || !ref.isWrite());
89
+ }
90
+ function isImportedConstantString(variable, currentFilename) {
91
+ if (!currentFilename || !path.isAbsolute(currentFilename))
92
+ return false;
93
+ const definition = variable.defs[0];
94
+ if (definition?.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration") {
95
+ return false;
96
+ }
97
+ const source = definition.parent.source.value;
98
+ if (typeof source !== "string" || !source.startsWith(".")) {
99
+ return false;
100
+ }
101
+ const dir = path.dirname(currentFilename);
102
+ const candidates = [
103
+ path.resolve(dir, source),
104
+ path.resolve(dir, `${source}.ts`),
105
+ path.resolve(dir, `${source}.tsx`),
106
+ path.resolve(dir, `${source}.js`),
107
+ path.resolve(dir, `${source}.mjs`),
108
+ path.resolve(dir, source, "index.ts"),
109
+ path.resolve(dir, source, "index.js"),
110
+ ];
111
+ for (const candidate of candidates) {
112
+ try {
113
+ let content = fileContentCache.get(candidate);
114
+ if (content === undefined) {
115
+ if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) {
116
+ continue;
117
+ }
118
+ content = fs.readFileSync(candidate, "utf8");
119
+ fileContentCache.set(candidate, content);
120
+ }
121
+ const importedName = definition.node.type === "ImportSpecifier"
122
+ ? definition.node.imported.type === "Identifier"
123
+ ? definition.node.imported.name
124
+ : definition.node.imported.value
125
+ : definition.node.type === "ImportDefaultSpecifier"
126
+ ? "default"
127
+ : null;
128
+ if (!importedName)
129
+ continue;
130
+ const pattern = new RegExp(`export\\s+const\\s+${importedName}\\s*=\\s*(["'\`])`);
131
+ if (pattern.test(content)) {
132
+ return true;
133
+ }
134
+ }
135
+ catch {
136
+ // ignore file read errors
137
+ }
138
+ }
139
+ return false;
140
+ }
141
+ function isConstantString(expression, sourceCode, currentFilename, visited = new Set()) {
142
+ const current = unwrapExpression(expression);
143
+ if (current.type === "Literal" && typeof current.value === "string") {
144
+ return true;
145
+ }
146
+ if (current.type === "TemplateLiteral") {
147
+ if (current.expressions.length === 0) {
148
+ return true;
149
+ }
150
+ return current.expressions.every((expr) => isConstantString(expr, sourceCode, currentFilename, visited));
151
+ }
152
+ if (current.type === "BinaryExpression" && current.operator === "+") {
153
+ return (isConstantString(current.left, sourceCode, currentFilename, visited) &&
154
+ isConstantString(current.right, sourceCode, currentFilename, visited));
155
+ }
156
+ if (current.type === "CallExpression" &&
157
+ current.callee.type === "Identifier" &&
158
+ current.callee.name === "String" &&
159
+ current.arguments.length === 1 &&
160
+ current.arguments[0]?.type !== "SpreadElement") {
161
+ return isConstantString(current.arguments[0], sourceCode, currentFilename, visited);
162
+ }
163
+ if (current.type === "Identifier") {
164
+ const variable = resolveVariable(sourceCode, current);
165
+ if (variable === null || visited.has(variable)) {
166
+ return false;
167
+ }
168
+ if (variable.defs.length === 1) {
169
+ const [def] = variable.defs;
170
+ if (def?.type === "Variable" &&
171
+ def.node.type === "VariableDeclarator" &&
172
+ def.node.init !== null) {
173
+ if (isStableConstVariable(variable, def.node)) {
174
+ visited.add(variable);
175
+ return isConstantString(def.node.init, sourceCode, currentFilename, visited);
176
+ }
177
+ }
178
+ if (def?.type === "ImportBinding") {
179
+ return isImportedConstantString(variable, currentFilename);
180
+ }
181
+ }
182
+ }
183
+ return false;
184
+ }
185
+ function extractExpectSubject(callee) {
186
+ let object = callee.object;
187
+ while (object.type === "MemberExpression" &&
188
+ !object.computed &&
189
+ object.property.type === "Identifier" &&
190
+ CHAIN_MODIFIERS.has(object.property.name)) {
191
+ object = object.object;
192
+ }
193
+ if (object.type === "CallExpression" &&
194
+ rootIdentifierName(object.callee) === "expect" &&
195
+ object.arguments.length > 0 &&
196
+ object.arguments[0]?.type !== "SpreadElement") {
197
+ return object.arguments[0];
198
+ }
199
+ return null;
200
+ }
201
+ function extractSubstringCallSubject(node) {
202
+ const unwrapped = unwrapExpression(node);
203
+ if (unwrapped.type === "CallExpression" &&
204
+ unwrapped.callee.type === "MemberExpression" &&
205
+ !unwrapped.callee.computed &&
206
+ unwrapped.callee.property.type === "Identifier") {
207
+ const method = unwrapped.callee.property.name;
208
+ if (SUBSTRING_METHODS.has(method)) {
209
+ return unwrapped.callee.object;
210
+ }
211
+ if (method === "test" && unwrapped.arguments.length > 0 && unwrapped.arguments[0]?.type !== "SpreadElement") {
212
+ return unwrapped.arguments[0];
213
+ }
214
+ }
215
+ if (unwrapped.type === "BinaryExpression") {
216
+ if (unwrapped.left.type !== "PrivateIdentifier") {
217
+ const leftSubject = extractSubstringCallSubject(unwrapped.left);
218
+ if (leftSubject)
219
+ return leftSubject;
220
+ }
221
+ const rightSubject = extractSubstringCallSubject(unwrapped.right);
222
+ if (rightSubject)
223
+ return rightSubject;
224
+ }
225
+ return null;
226
+ }
227
+ function isExpectStringContaining(node) {
228
+ const unwrapped = unwrapExpression(node);
229
+ if (unwrapped.type === "CallExpression" &&
230
+ unwrapped.callee.type === "MemberExpression" &&
231
+ !unwrapped.callee.computed &&
232
+ unwrapped.callee.property.type === "Identifier" &&
233
+ unwrapped.callee.property.name === "stringContaining" &&
234
+ rootIdentifierName(unwrapped.callee.object) === "expect") {
235
+ return true;
236
+ }
237
+ return false;
238
+ }
239
+ /** Disallow testing that a constant string contains substrings. */
240
+ export const noPoorSubstringTestsRule = defineRule({
241
+ meta: {
242
+ type: "suggestion",
243
+ docs: {
244
+ description: "Disallow testing that a constant string contains substrings; tests must assert on dynamic behavior or verify full contracts.",
245
+ },
246
+ messages: {
247
+ poorSubstringTest: "Testing that a constant string contains a substring provides little signal. Assert against dynamic behavior, or verify the complete contract with an exact match or snapshot instead.",
248
+ },
249
+ schema: [],
250
+ },
251
+ createOnce(context) {
252
+ let shouldCheck = false;
253
+ return {
254
+ Program() {
255
+ shouldCheck = isTestFile(context.filename);
256
+ },
257
+ CallExpression(node) {
258
+ if (!shouldCheck)
259
+ return;
260
+ // Pattern 1: expect(subject).toContain(sub), expect(subject).toMatch(regex), etc.
261
+ if (node.callee.type === "MemberExpression" &&
262
+ !node.callee.computed &&
263
+ node.callee.property.type === "Identifier") {
264
+ const propertyName = node.callee.property.name;
265
+ if (SUBSTRING_MATCHERS.has(propertyName)) {
266
+ const subject = extractExpectSubject(node.callee);
267
+ if (subject && isConstantString(subject, context.sourceCode, context.filename)) {
268
+ context.report({ node, messageId: "poorSubstringTest" });
269
+ return;
270
+ }
271
+ }
272
+ // Pattern 1b: expect(subject).toEqual(expect.stringContaining(sub))
273
+ if (node.arguments.length > 0 &&
274
+ node.arguments[0] !== undefined &&
275
+ node.arguments[0].type !== "SpreadElement" &&
276
+ isExpectStringContaining(node.arguments[0])) {
277
+ const subject = extractExpectSubject(node.callee);
278
+ if (subject && isConstantString(subject, context.sourceCode, context.filename)) {
279
+ context.report({ node, messageId: "poorSubstringTest" });
280
+ return;
281
+ }
282
+ }
283
+ }
284
+ // Pattern 2: expect(subject.includes("sub")), expect(subject.startsWith("sub")), expect(/regex/.test(subject)), etc.
285
+ if (rootIdentifierName(node.callee) === "expect" && node.arguments.length > 0) {
286
+ const firstArg = node.arguments[0];
287
+ if (firstArg && firstArg.type !== "SpreadElement") {
288
+ const subject = extractSubstringCallSubject(firstArg);
289
+ if (subject && isConstantString(subject, context.sourceCode, context.filename)) {
290
+ context.report({ node, messageId: "poorSubstringTest" });
291
+ return;
292
+ }
293
+ }
294
+ }
295
+ // Pattern 3: assert(subject.includes("sub")), assert.ok(subject.includes("sub")), assert.isTrue(subject.includes("sub"))
296
+ if (rootIdentifierName(node.callee) === "assert" &&
297
+ node.arguments.length > 0 &&
298
+ node.arguments[0] !== undefined &&
299
+ node.arguments[0].type !== "SpreadElement") {
300
+ const firstArg = node.arguments[0];
301
+ // 3a: assert.include(subject, "sub"), assert.match(subject, /regex/)
302
+ if (node.callee.type === "MemberExpression" &&
303
+ !node.callee.computed &&
304
+ node.callee.property.type === "Identifier" &&
305
+ ASSERT_MATCHERS.has(node.callee.property.name)) {
306
+ if (isConstantString(firstArg, context.sourceCode, context.filename)) {
307
+ context.report({ node, messageId: "poorSubstringTest" });
308
+ return;
309
+ }
310
+ }
311
+ // 3b: assert(subject.includes("sub")), assert.ok(subject.includes("sub"))
312
+ const subject = extractSubstringCallSubject(firstArg);
313
+ if (subject && isConstantString(subject, context.sourceCode, context.filename)) {
314
+ context.report({ node, messageId: "poorSubstringTest" });
315
+ }
316
+ }
317
+ },
318
+ };
319
+ },
320
+ });
@@ -0,0 +1,2 @@
1
+ /** Disallow named import aliases when the imported name is available in module scope. */
2
+ export declare const noRenamedImportsRule: import("@oxlint/plugins").Rule;
@@ -0,0 +1,53 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ function moduleScopeFor(scope) {
3
+ let current = scope;
4
+ while (current !== null) {
5
+ if (current.type === "module")
6
+ return current;
7
+ current = current.upper;
8
+ }
9
+ return null;
10
+ }
11
+ function importedName(node) {
12
+ return node.imported.type === "Identifier" ? node.imported.name : null;
13
+ }
14
+ /** Disallow named import aliases when the imported name is available in module scope. */
15
+ export const noRenamedImportsRule = defineRule({
16
+ meta: {
17
+ type: "suggestion",
18
+ docs: { description: "Disallow unnecessary aliases for named imports" },
19
+ fixable: "code",
20
+ messages: {
21
+ unnecessaryAlias: "Use {{importedName}} directly instead of renaming this import to {{localName}}.",
22
+ },
23
+ schema: [],
24
+ },
25
+ createOnce(context) {
26
+ return {
27
+ ImportSpecifier(node) {
28
+ const name = importedName(node);
29
+ if (name === null || name === node.local.name)
30
+ return;
31
+ const moduleScope = moduleScopeFor(context.sourceCode.getScope(node));
32
+ if (moduleScope === null || moduleScope.set.has(name))
33
+ return;
34
+ const variable = moduleScope.set.get(node.local.name);
35
+ if (variable === undefined)
36
+ return;
37
+ context.report({
38
+ node,
39
+ messageId: "unnecessaryAlias",
40
+ data: { importedName: name, localName: node.local.name },
41
+ fix(fixer) {
42
+ return [
43
+ fixer.replaceText(node, name),
44
+ ...variable.references
45
+ .filter((reference) => reference.resolved === variable)
46
+ .map((reference) => fixer.replaceText(reference.identifier, name)),
47
+ ];
48
+ },
49
+ });
50
+ },
51
+ };
52
+ },
53
+ });
@@ -1,3 +1,4 @@
1
1
  export declare function resolveCssPath(cssPath: string | undefined, cwd?: string): string;
2
2
  export declare function ensureDesignSystem(cssFile: string): string;
3
+ export declare function getDesignSystemVersion(key: string): number | undefined;
3
4
  export declare function canonicalizeTokens(key: string, tokens: Array<string>, rem: number): Map<string, string>;
@@ -8,6 +8,7 @@ import { createSyncFn } from "synckit";
8
8
  const workerPath = fileURLToPath(new URL("./worker.js", import.meta.url));
9
9
  let syncCall = null;
10
10
  const loadedSystems = new Map();
11
+ const canonicalResults = new Map();
11
12
  function getSyncCall() {
12
13
  if (syncCall === null) {
13
14
  syncCall = createSyncFn(workerPath);
@@ -26,16 +27,31 @@ export function ensureDesignSystem(cssFile) {
26
27
  const cssContent = fs.readFileSync(cssFile, "utf8");
27
28
  getSyncCall()({ type: "load", key: cssFile, cssContent, base: path.dirname(cssFile) });
28
29
  loadedSystems.set(cssFile, stat.mtimeMs);
30
+ canonicalResults.delete(cssFile);
29
31
  return cssFile;
30
32
  }
33
+ export function getDesignSystemVersion(key) {
34
+ return loadedSystems.get(key);
35
+ }
31
36
  export function canonicalizeTokens(key, tokens, rem) {
32
37
  if (tokens.length === 0)
33
38
  return new Map();
34
- const canonical = getSyncCall()({ type: "canonicalize", key, candidates: tokens, rem });
35
- if (!Array.isArray(canonical))
36
- throw new Error("Tailwind canonicalization returned an invalid result");
39
+ let byRem = canonicalResults.get(key);
40
+ if (!byRem)
41
+ canonicalResults.set(key, byRem = new Map());
42
+ let cached = byRem.get(rem);
43
+ if (!cached)
44
+ byRem.set(rem, cached = new Map());
45
+ const missing = [...new Set(tokens.filter(token => !cached.has(token)))];
46
+ if (missing.length > 0) {
47
+ const canonical = getSyncCall()({ type: "canonicalize", key, candidates: missing, rem });
48
+ if (!Array.isArray(canonical))
49
+ throw new Error("Tailwind canonicalization returned an invalid result");
50
+ for (let i = 0; i < missing.length; i++)
51
+ cached.set(missing[i], canonical[i] ?? missing[i]);
52
+ }
37
53
  const result = new Map();
38
- for (let i = 0; i < tokens.length; i++)
39
- result.set(tokens[i], canonical[i] ?? tokens[i]);
54
+ for (const token of tokens)
55
+ result.set(token, cached.get(token));
40
56
  return result;
41
57
  }
@@ -0,0 +1,4 @@
1
+ import type { __unstable__loadDesignSystem } from "@tailwindcss/node";
2
+ type DesignSystem = Awaited<ReturnType<typeof __unstable__loadDesignSystem>>;
3
+ export declare function optimizeDesignSystem(system: DesignSystem): DesignSystem;
4
+ export {};
@@ -0,0 +1,127 @@
1
+ function propertyKey(system, candidates, raw = false) {
2
+ const properties = new Set();
3
+ function visit(node) {
4
+ if (node.kind === "at-rule" && node.name === "@property")
5
+ return;
6
+ if (node.kind === "declaration" && node.value !== undefined && !node.property.startsWith("--tw-"))
7
+ properties.add(node.property);
8
+ if ("nodes" in node)
9
+ for (const child of node.nodes)
10
+ visit(child);
11
+ }
12
+ for (const candidate of candidates) {
13
+ if (!raw || candidate.kind === "arbitrary") {
14
+ for (const { node } of system.compileAstNodes({ ...candidate, variants: [], important: false }))
15
+ visit(node);
16
+ continue;
17
+ }
18
+ // Property discovery needs utility declarations, not selector generation,
19
+ // importance propagation, or Tailwind's expensive declaration sort order.
20
+ const utilities = system.utilities.get(candidate.root) ?? [];
21
+ const isFallback = (utility) => (utility.options?.types?.length ?? 0) > 1 && utility.options.types.includes("any");
22
+ for (const fallback of [false, true]) {
23
+ let accepted = false;
24
+ for (const utility of utilities) {
25
+ if (utility.kind !== candidate.kind || isFallback(utility) !== fallback)
26
+ continue;
27
+ const nodes = utility.compileFn(candidate);
28
+ if (nodes === null && utility.options?.types?.length) {
29
+ accepted = true;
30
+ break;
31
+ }
32
+ if (nodes === null || nodes === undefined)
33
+ continue;
34
+ accepted = true;
35
+ for (const node of nodes)
36
+ visit(node);
37
+ }
38
+ if (accepted)
39
+ break;
40
+ }
41
+ }
42
+ return [...properties].sort().join("\0");
43
+ }
44
+ // Canonicalization only compares equivalent declaration sets. Partition its
45
+ // eager class index without changing the compiler, theme, or candidate parser.
46
+ export function optimizeDesignSystem(system) {
47
+ const parseVariant = system.parseVariant.bind(system);
48
+ const getVariantOrder = system.getVariantOrder.bind(system);
49
+ const seenVariants = new Set();
50
+ let variantVersion = 0;
51
+ let orderVersion = -1;
52
+ let order;
53
+ system.parseVariant = raw => {
54
+ const variant = parseVariant(raw);
55
+ if (variant !== null && !seenVariants.has(variant)) {
56
+ seenVariants.add(variant);
57
+ variantVersion++;
58
+ }
59
+ return variant;
60
+ };
61
+ system.getVariantOrder = () => {
62
+ if (orderVersion !== variantVersion) {
63
+ order = getVariantOrder();
64
+ for (const variant of order.keys())
65
+ seenVariants.add(variant);
66
+ orderVersion = variantVersion;
67
+ }
68
+ return order;
69
+ };
70
+ let groups;
71
+ const scopedSystems = new Map();
72
+ function getGroups() {
73
+ if (groups)
74
+ return groups;
75
+ const built = new Map();
76
+ for (const entry of system.getClassList()) {
77
+ // Match Tailwind's index: quarter-step numeric modifiers use its
78
+ // on-demand fallback rather than the enumerated class signatures.
79
+ const modifiers = entry[1].modifiers.filter((modifier) => {
80
+ const number = Number(modifier);
81
+ return !(number >= 0 && number % 0.25 === 0 && String(number) === modifier);
82
+ });
83
+ const spellings = [entry[0], ...modifiers.map((modifier) => `${entry[0]}/${modifier}`)];
84
+ for (const spelling of spellings) {
85
+ let key;
86
+ const token = system.theme.prefix ? `${system.theme.prefix}:${spelling}` : spelling;
87
+ try {
88
+ key = propertyKey(system, system.parseCandidate(token), true);
89
+ }
90
+ catch {
91
+ continue;
92
+ }
93
+ let group = built.get(key);
94
+ if (!group)
95
+ built.set(key, group = new Set());
96
+ group.add(entry);
97
+ }
98
+ }
99
+ return groups = built;
100
+ }
101
+ return {
102
+ ...system,
103
+ canonicalizeCandidates(tokens, options) {
104
+ return tokens.map(token => {
105
+ const candidates = system.parseCandidate(token);
106
+ if (candidates.every(candidate => candidate.kind === "static"))
107
+ return system.canonicalizeCandidates([token], options)[0] ?? token;
108
+ const keys = new Set([propertyKey(system, candidates)]);
109
+ const withoutModifiers = candidates.map(candidate => candidate.kind === "static" ? candidate : { ...candidate, modifier: null });
110
+ keys.add(propertyKey(system, withoutModifiers));
111
+ const scopeKey = [...keys].sort().join("\x01");
112
+ let scoped = scopedSystems.get(scopeKey);
113
+ if (!scoped) {
114
+ const entries = new Set();
115
+ const index = getGroups();
116
+ for (const key of keys)
117
+ for (const entry of index.get(key) ?? [])
118
+ entries.add(entry);
119
+ const list = [...entries];
120
+ scoped = { ...system, storage: {}, getClassList: () => list };
121
+ scopedSystems.set(scopeKey, scoped);
122
+ }
123
+ return scoped.canonicalizeCandidates([token], options)[0] ?? token;
124
+ });
125
+ },
126
+ };
127
+ }
@@ -1,11 +1,12 @@
1
1
  import { defineRule } from "@oxlint/plugins";
2
2
  import { rewriteClassValue, summarizeChanges, uniqueTokens } from "./candidates.js";
3
- import { canonicalizeTokens, ensureDesignSystem, resolveCssPath } from "./design-system.js";
3
+ import { canonicalizeTokens, ensureDesignSystem, getDesignSystemVersion, resolveCssPath } from "./design-system.js";
4
4
  export const PLUGIN_NAME = "deslop";
5
5
  export const RULE_ID = "canonical-class-names";
6
6
  const DEFAULT_ATTRIBUTES = ["class", "className"];
7
7
  const DEFAULT_CALLEE_FUNCTIONS = ["cn", "clsx", "cva", "twMerge", "tw", "classNames", "cx"];
8
8
  const DEFAULT_ROOT_FONT_SIZE = 16;
9
+ const MAX_CACHED_VALUES = 10_000;
9
10
  const OPTIONS_SCHEMA = {
10
11
  type: "object",
11
12
  properties: {
@@ -34,7 +35,11 @@ export const canonicalClassNames = defineRule({
34
35
  let rem = DEFAULT_ROOT_FONT_SIZE;
35
36
  let cssFile = "";
36
37
  let designKey = null;
38
+ const checkedValues = new Map();
39
+ let checkedVersion;
37
40
  function initFile() {
41
+ const previousRem = rem;
42
+ const previousCssFile = cssFile;
38
43
  const options = (context.options?.[0] ?? {});
39
44
  attributes = new Set(options.attributes ?? DEFAULT_ATTRIBUTES);
40
45
  callees = new Set(options.calleeFunctions ?? DEFAULT_CALLEE_FUNCTIONS);
@@ -46,11 +51,18 @@ export const canonicalClassNames = defineRule({
46
51
  cssFile = options.cssPath ?? "";
47
52
  }
48
53
  designKey = null;
54
+ if (previousRem !== rem || previousCssFile !== cssFile)
55
+ checkedValues.clear();
49
56
  }
50
57
  function getDesignKey() {
51
58
  if (designKey === null) {
52
59
  try {
53
60
  designKey = ensureDesignSystem(cssFile);
61
+ const version = getDesignSystemVersion(designKey);
62
+ if (checkedVersion !== version) {
63
+ checkedValues.clear();
64
+ checkedVersion = version;
65
+ }
54
66
  }
55
67
  catch {
56
68
  designKey = false;
@@ -59,22 +71,29 @@ export const canonicalClassNames = defineRule({
59
71
  return designKey === false ? null : designKey;
60
72
  }
61
73
  function checkStringNode(node, value) {
62
- const tokens = uniqueTokens(value);
63
- if (tokens.length === 0)
74
+ if (!/\S/.test(value))
64
75
  return;
65
76
  const key = getDesignKey();
66
77
  if (key === null) {
67
78
  context.report({ node, messageId: "cssNotFound", data: { path: cssFile } });
68
79
  return;
69
80
  }
70
- let canonicalOf;
71
- try {
72
- canonicalOf = canonicalizeTokens(key, tokens, rem);
73
- }
74
- catch {
75
- return;
81
+ let rewritten = checkedValues.get(value);
82
+ if (!rewritten) {
83
+ const tokens = uniqueTokens(value);
84
+ let canonicalOf;
85
+ try {
86
+ canonicalOf = canonicalizeTokens(key, tokens, rem);
87
+ }
88
+ catch {
89
+ return;
90
+ }
91
+ rewritten = rewriteClassValue(value, canonicalOf);
92
+ if (checkedValues.size >= MAX_CACHED_VALUES)
93
+ checkedValues.clear();
94
+ checkedValues.set(value, rewritten);
76
95
  }
77
- const { fixed, changes } = rewriteClassValue(value, canonicalOf);
96
+ const { fixed, changes } = rewritten;
78
97
  if (changes.length === 0)
79
98
  return;
80
99
  const summary = summarizeChanges(changes);
@@ -1,13 +1,15 @@
1
1
  import { __unstable__loadDesignSystem } from "@tailwindcss/node";
2
2
  import { runAsWorker } from "synckit";
3
+ import { optimizeDesignSystem } from "./optimize-system.js";
3
4
  const designSystems = new Map();
4
5
  runAsWorker(async (job) => {
5
6
  if (job.type === "load") {
6
- designSystems.set(job.key, await __unstable__loadDesignSystem(job.cssContent, { base: job.base }));
7
+ designSystems.set(job.key, optimizeDesignSystem(await __unstable__loadDesignSystem(job.cssContent, { base: job.base })));
7
8
  return { ok: true };
8
9
  }
9
10
  const designSystem = designSystems.get(job.key);
10
11
  if (!designSystem)
11
12
  throw new Error(`Tailwind design system not loaded for "${job.key}"`);
12
- return designSystem.canonicalizeCandidates(job.candidates, { rem: job.rem });
13
+ // Tailwind deduplicates canonical spellings, so a batch result is not positional.
14
+ return job.candidates.map(candidate => designSystem.canonicalizeCandidates([candidate], { rem: job.rem })[0] ?? candidate);
13
15
  });
package/package.json CHANGED
@@ -1,64 +1,65 @@
1
- {
2
- "name": "ai-deslop",
3
- "version": "0.4.3",
4
- "description": "Opinionated Oxlint rules that reject low-evidence, low-signal, AI-slop code patterns.",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "default": "./dist/index.js"
12
- }
13
- },
14
- "files": [
15
- "dist"
16
- ],
1
+ {
2
+ "name": "ai-deslop",
3
+ "version": "0.4.5",
4
+ "description": "Opinionated Oxlint rules that reject low-evidence, low-signal, AI-slop code patterns.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
17
  "scripts": {
18
- "build": "tsc -p tsconfig.build.json",
19
- "prepublishOnly": "npm run build",
20
- "check": "npm run build && oxlint src && tsc --noEmit",
21
- "lint": "oxlint src",
22
- "test": "vitest run",
23
- "typecheck": "tsc --noEmit"
24
- },
25
- "dependencies": {
26
- "@oxlint/plugins": "1.82.0",
27
- "@tailwindcss/node": "^4.3.3",
28
- "synckit": "^0.11.13"
29
- },
30
- "peerDependencies": {
31
- "tailwindcss": "^4.0.0"
32
- },
33
- "peerDependenciesMeta": {
34
- "tailwindcss": {
35
- "optional": true
36
- }
37
- },
38
- "devDependencies": {
39
- "@types/node": "26.2.0",
40
- "oxlint": "1.82.0",
41
- "typescript": "7.0.2",
42
- "vitest": "^5.0.1"
43
- },
44
- "license": "MIT",
45
- "repository": {
46
- "type": "git",
47
- "url": "git+https://github.com/maharshi365/deslop.git"
48
- },
49
- "homepage": "https://github.com/maharshi365/deslop#readme",
50
- "bugs": {
51
- "url": "https://github.com/maharshi365/deslop/issues"
52
- },
53
- "keywords": [
54
- "oxlint",
55
- "oxlint-plugin",
56
- "eslint",
57
- "ai-slop",
58
- "code-quality",
59
- "typescript"
60
- ],
61
- "publishConfig": {
62
- "access": "public"
63
- }
64
- }
18
+ "bench": "npm run build && node bench.mjs",
19
+ "build": "tsc -p tsconfig.build.json",
20
+ "prepublishOnly": "npm run build",
21
+ "check": "npm run build && oxlint src && tsc --noEmit",
22
+ "lint": "oxlint src",
23
+ "test": "vitest run",
24
+ "typecheck": "tsc --noEmit"
25
+ },
26
+ "dependencies": {
27
+ "@oxlint/plugins": "1.82.0",
28
+ "@tailwindcss/node": "^4.3.3",
29
+ "synckit": "^0.11.13"
30
+ },
31
+ "peerDependencies": {
32
+ "tailwindcss": "^4.0.0"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "tailwindcss": {
36
+ "optional": true
37
+ }
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "26.2.0",
41
+ "oxlint": "1.82.0",
42
+ "typescript": "7.0.2",
43
+ "vitest": "^5.0.1"
44
+ },
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/maharshi365/deslop.git"
49
+ },
50
+ "homepage": "https://github.com/maharshi365/deslop#readme",
51
+ "bugs": {
52
+ "url": "https://github.com/maharshi365/deslop/issues"
53
+ },
54
+ "keywords": [
55
+ "oxlint",
56
+ "oxlint-plugin",
57
+ "eslint",
58
+ "ai-slop",
59
+ "code-quality",
60
+ "typescript"
61
+ ],
62
+ "publishConfig": {
63
+ "access": "public"
64
+ }
65
+ }