ai-deslop 0.2.2 → 0.3.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 CHANGED
@@ -13,30 +13,45 @@ npm install --save-dev ai-deslop
13
13
 
14
14
  Ships compiled JavaScript — works with any oxlint JS-plugin runtime (Bun, Deno, or Node.js).
15
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
+
16
31
  ## Rules
17
32
 
18
33
  | Rule | Status | Description |
19
34
  | --- | --- | --- |
20
- | [`no-array-filter-map`](docs/deslop.md) | Recommended | Rejects adjacent eager array filter/map passes; allows lazy iterator pipelines. |
21
- | [`no-reduce-accumulator-copy`](docs/deslop.md) | Recommended | Rejects non-spread accumulator copies inside reducers. Enable native `oxc/no-accumulating-spread` alongside it. |
22
- | [`no-chained-type-assertions`](docs/deslop.md) | Recommended | Rejects nested `as` / angle-bracket assertions that fabricate evidence. |
23
- | [`no-conditional-empty-object-spread`](docs/deslop.md) | Recommended | Reports object spreads using a conditional `{}` branch to omit fields. |
24
- | [`no-known-value-widening`](docs/deslop.md) | Recommended | Rejects known expressions flowing into explicit `unknown`, `object`, or open dictionaries. |
25
- | [`no-module-mocking`](docs/deslop.md) | Recommended | Rejects Vitest and Jest module mocks. |
26
- | [`no-object-parameters`](docs/deslop.md) | Recommended | Rejects `object` on function inputs. |
27
- | [`no-reflect-apply`](docs/deslop.md) | Recommended | Rejects global `Reflect.apply`. |
28
- | [`no-reflect-get`](docs/deslop.md) | Recommended | Rejects global `Reflect.get`. |
29
- | [`no-runtime-typeof`](docs/deslop.md) | Recommended | Requires boundary parsing instead of ad hoc `typeof` narrowing. |
30
- | [`no-shape-in-symbol-names`](docs/deslop.md) | Recommended | Rejects `shape` in locally owned symbol names. |
31
- | [`no-unknown-parameters`](docs/deslop.md) | Recommended | Rejects `unknown` on function inputs. |
32
- | [`no-unknown-returns`](docs/deslop.md) | Recommended | Rejects return contracts resolving to `unknown`. |
33
- | [`no-unknown-type-aliases`](docs/deslop.md) | Recommended | Rejects aliases whose resolved type is `unknown`. |
34
- | [`no-unsafe-dictionary-type`](docs/deslop.md) | Recommended | Rejects dictionary values based on unsafe escape hatches. |
35
- | [`no-widen-then-assert`](docs/deslop.md) | Recommended | Rejects widening known evidence before narrowing assertions. |
36
- | [`require-safety-comment-for-type-assertion`](docs/deslop.md) | Recommended | Requires an invariant justification for non-const assertions. |
37
- | [`no-call-only-assertions`](docs/deslop.md) | Recommended | Flags tests whose assertions only check mock calls. |
38
- | [`no-pass-through-type-alias`](docs/deslop.md) | Recommended | Disallows aliases that only rename another type. |
39
- | [`no-duplicate-types`](docs/deslop.md#no-duplicate-types) | Opt-in | Reports exact duplicate object contracts. No fixer. |
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. |
54
+ | [`no-duplicate-types`](docs/no-duplicate-types.md) | Opt-in | Reports exact duplicate object contracts. No fixer. |
40
55
  | [`canonical-class-names`](docs/tailwind.md) | Opt-in | Fixes non-canonical Tailwind CSS v4 class spellings. |
41
56
 
42
57
  | Standard rule | Status | Description |
@@ -48,7 +63,7 @@ Ships compiled JavaScript — works with any oxlint JS-plugin runtime (Bun, Deno
48
63
 
49
64
  | Guide | Contents |
50
65
  | --- | --- |
51
- | [Deslop rules](docs/deslop.md) | Recommended and opt-in deslop configuration, complete TypeScript/JSON examples, and `no-duplicate-types` options. |
66
+ | [No duplicate types](docs/no-duplicate-types.md) | Exact comparison behavior, worker scope, complete TypeScript/JSON examples, and every supported option. |
52
67
  | [Tailwind canonical class names](docs/tailwind.md) | Tailwind v4 setup, complete TypeScript/JSON examples, and every supported option. |
53
68
 
54
69
  ## Credits
@@ -1,4 +1,10 @@
1
1
  import { defineRule } from "@oxlint/plugins";
2
+ const ignoredZodMethods = new Set([
3
+ "min", "max", "length", "nonempty", "email", "url", "uuid", "guid", "cuid", "cuid2", "ulid", "xid", "ksuid",
4
+ "nanoid", "regex", "includes", "startsWith", "endsWith", "datetime", "date", "time", "duration", "ip", "cidr",
5
+ "emoji", "base64", "base64url", "jwt", "int", "safe", "finite", "multipleOf", "step", "positive", "negative",
6
+ "nonpositive", "nonnegative", "gt", "gte", "lt", "lte", "describe", "brand", "readonly", "default",
7
+ ]);
2
8
  function propertyKey(node) {
3
9
  const value = node;
4
10
  if (value.type === "Identifier" && typeof value.name === "string")
@@ -100,44 +106,197 @@ function canonicalDeclaration(node) {
100
106
  for (const [index, parameter] of (node.typeParameters?.params ?? []).entries()) {
101
107
  parameters.set(parameter.name.name, `$${index}`);
102
108
  }
103
- if (node.type === "TSTypeAliasDeclaration")
104
- return canonicalType(node.typeAnnotation, parameters);
109
+ if (node.type === "TSTypeAliasDeclaration") {
110
+ if (node.typeAnnotation.type !== "TSTypeLiteral")
111
+ return null;
112
+ return canonicalMembers(node.typeAnnotation.members, parameters);
113
+ }
105
114
  if (node.extends.length > 0)
106
115
  return null;
107
116
  return canonicalMembers(node.body.body, parameters);
108
117
  }
118
+ function memberCall(node) {
119
+ if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.property.type !== "Identifier")
120
+ return null;
121
+ if (node.arguments.some((argument) => argument.type === "SpreadElement"))
122
+ return null;
123
+ return { name: node.callee.property.name, object: node.callee.object, arguments_: node.arguments };
124
+ }
125
+ function schemaLiteral(node) {
126
+ if (node.type !== "Literal")
127
+ return null;
128
+ if (typeof node.value === "string")
129
+ return JSON.stringify(node.value);
130
+ if (typeof node.value === "number" || typeof node.value === "boolean")
131
+ return String(node.value);
132
+ if (typeof node.value === "bigint")
133
+ return `${node.value}n`;
134
+ return null;
135
+ }
136
+ function canonicalSchemaProperty(node, zodNamespaces) {
137
+ let current = node;
138
+ let optional = false;
139
+ let nullable = false;
140
+ while (true) {
141
+ const call = memberCall(current);
142
+ if (call === null || !(["optional", "nullish"].includes(call.name) && call.arguments_.length === 0 || ignoredZodMethods.has(call.name)))
143
+ break;
144
+ if (call.name === "optional")
145
+ optional = true;
146
+ if (call.name === "nullish") {
147
+ optional = true;
148
+ nullable = true;
149
+ }
150
+ current = call.object;
151
+ }
152
+ const type = canonicalSchema(current, zodNamespaces);
153
+ return type === null ? null : { type: nullable ? `union(${["TSNullKeyword", type].sort().join(",")})` : type, optional };
154
+ }
155
+ function canonicalSchemaObject(node, zodNamespaces) {
156
+ if (node.type !== "ObjectExpression")
157
+ return null;
158
+ const members = [];
159
+ for (const property of node.properties) {
160
+ if (property.type !== "Property" || property.computed || property.kind !== "init")
161
+ return null;
162
+ const key = propertyKey(property.key);
163
+ if (key === null)
164
+ return null;
165
+ const value = canonicalSchemaProperty(property.value, zodNamespaces);
166
+ if (value === null)
167
+ return null;
168
+ members.push(`prop(${key},${value.optional ? "?" : "!"},rw,${value.type})`);
169
+ }
170
+ members.sort();
171
+ return { fingerprint: `{${members.join(",")}}`, properties: members.length };
172
+ }
173
+ function canonicalSchema(node, zodNamespaces) {
174
+ const call = memberCall(node);
175
+ if (call === null)
176
+ return null;
177
+ if (call.object.type === "Identifier" && zodNamespaces.has(call.object.name)) {
178
+ switch (call.name) {
179
+ case "string": return call.arguments_.length === 0 ? "TSStringKeyword" : null;
180
+ case "number": return call.arguments_.length === 0 ? "TSNumberKeyword" : null;
181
+ case "boolean": return call.arguments_.length === 0 ? "TSBooleanKeyword" : null;
182
+ case "bigint": return call.arguments_.length === 0 ? "TSBigIntKeyword" : null;
183
+ case "unknown": return call.arguments_.length === 0 ? "TSUnknownKeyword" : null;
184
+ case "any": return call.arguments_.length === 0 ? "TSAnyKeyword" : null;
185
+ case "never": return call.arguments_.length === 0 ? "TSNeverKeyword" : null;
186
+ case "null": return call.arguments_.length === 0 ? "TSNullKeyword" : null;
187
+ case "undefined": return call.arguments_.length === 0 ? "TSUndefinedKeyword" : null;
188
+ case "void": return call.arguments_.length === 0 ? "TSVoidKeyword" : null;
189
+ case "literal": {
190
+ if (call.arguments_.length !== 1)
191
+ return null;
192
+ const value = schemaLiteral(call.arguments_[0]);
193
+ return value === null ? null : `literal(${value})`;
194
+ }
195
+ case "enum": {
196
+ if (call.arguments_.length !== 1 || call.arguments_[0].type !== "ArrayExpression")
197
+ return null;
198
+ const values = call.arguments_[0].elements.map((element) => element === null || element.type === "SpreadElement" ? null : schemaLiteral(element));
199
+ if (values.some((value) => value === null))
200
+ return null;
201
+ return `union(${[...new Set(values)].sort().join(",")})`;
202
+ }
203
+ case "object": {
204
+ if (call.arguments_.length !== 1)
205
+ return null;
206
+ return canonicalSchemaObject(call.arguments_[0], zodNamespaces)?.fingerprint ?? null;
207
+ }
208
+ case "array": {
209
+ if (call.arguments_.length !== 1)
210
+ return null;
211
+ const element = canonicalSchema(call.arguments_[0], zodNamespaces);
212
+ return element === null ? null : `array(${element})`;
213
+ }
214
+ case "union": {
215
+ if (call.arguments_.length !== 1 || call.arguments_[0].type !== "ArrayExpression")
216
+ return null;
217
+ const types = call.arguments_[0].elements.map((element) => element === null || element.type === "SpreadElement" ? null : canonicalSchema(element, zodNamespaces));
218
+ if (types.some((type) => type === null))
219
+ return null;
220
+ return `union(${[...new Set(types)].sort().join(",")})`;
221
+ }
222
+ default: return null;
223
+ }
224
+ }
225
+ const inner = canonicalSchema(call.object, zodNamespaces);
226
+ if (inner === null)
227
+ return null;
228
+ switch (call.name) {
229
+ case "optional": return `union(${["TSUndefinedKeyword", inner].sort().join(",")})`;
230
+ case "nullable": return `union(${["TSNullKeyword", inner].sort().join(",")})`;
231
+ case "nullish": return `union(${["TSNullKeyword", "TSUndefinedKeyword", inner].sort().join(",")})`;
232
+ case "array": return call.arguments_.length === 0 ? `array(${inner})` : null;
233
+ default: return ignoredZodMethods.has(call.name) ? inner : null;
234
+ }
235
+ }
109
236
  /** Report exact structural duplicates without TypeScript assignability or fuzzy matching. */
110
237
  export const noDuplicateTypesRule = defineRule({
111
238
  meta: {
112
239
  type: "suggestion",
113
240
  docs: { description: "Report structurally identical TypeScript declarations" },
114
241
  messages: { duplicate: "{{message}} First seen as {{firstName}} in {{firstFile}}." },
115
- schema: [{ type: "object", properties: { message: { type: "string" }, minProperties: { type: "integer", minimum: 0 } }, additionalProperties: false }],
242
+ schema: [{ type: "object", properties: { message: { type: "string" }, minProperties: { type: "integer", minimum: 0 }, schemas: { type: "object", properties: { libraries: { type: "array", items: { enum: ["zod"] } }, matchTypes: { type: "boolean" } }, additionalProperties: false } }, additionalProperties: false }],
116
243
  },
117
244
  createOnce(context) {
118
245
  const seen = new Map();
119
246
  let options = {};
120
- const report = (node, fingerprint) => {
121
- const first = seen.get(fingerprint);
247
+ const report = (node, name, fingerprint, kind) => {
248
+ const key = options.schemas?.matchTypes === false ? `${kind}:${fingerprint}` : fingerprint;
249
+ const first = seen.get(key);
122
250
  if (first === undefined) {
123
- seen.set(fingerprint, { name: node.id.name, filename: context.filename });
251
+ seen.set(key, { name, filename: context.filename });
124
252
  return;
125
253
  }
126
254
  context.report({ node, messageId: "duplicate", data: { message: options.message ?? "This type is structurally identical to an existing declaration.", firstName: first.name, firstFile: first.filename } });
127
255
  };
128
256
  const inspect = (node) => {
257
+ let properties;
258
+ if (node.type === "TSTypeAliasDeclaration") {
259
+ if (node.typeAnnotation.type !== "TSTypeLiteral")
260
+ return;
261
+ properties = node.typeAnnotation.members.length;
262
+ }
263
+ else {
264
+ properties = node.body.body.length;
265
+ }
129
266
  const fingerprint = canonicalDeclaration(node);
130
267
  if (fingerprint === null)
131
268
  return;
132
- const properties = node.type === "TSTypeAliasDeclaration" && node.typeAnnotation.type === "TSTypeLiteral" ? node.typeAnnotation.members.length : node.type === "TSInterfaceDeclaration" ? node.body.body.length : Infinity;
133
269
  if (properties < (options.minProperties ?? 2))
134
270
  return;
135
- report(node, fingerprint);
271
+ report(node, node.id.name, fingerprint, "type");
272
+ };
273
+ const zodNamespaces = new Set();
274
+ const inspectSchema = (node) => {
275
+ if (options.schemas?.libraries?.includes("zod") !== true || node.id.type !== "Identifier" || node.init === null)
276
+ return;
277
+ const call = memberCall(node.init);
278
+ if (call === null || call.name !== "object" || call.arguments_.length !== 1 || call.object.type !== "Identifier" || !zodNamespaces.has(call.object.name))
279
+ return;
280
+ const schema = canonicalSchemaObject(call.arguments_[0], zodNamespaces);
281
+ if (schema === null || schema.properties < (options.minProperties ?? 2))
282
+ return;
283
+ report(node, node.id.name, schema.fingerprint, "schema");
136
284
  };
137
285
  return {
138
286
  Program() { options = context.options[0] ?? {}; },
287
+ ImportDeclaration(node) {
288
+ if (node.source.value !== "zod")
289
+ return;
290
+ for (const specifier of node.specifiers) {
291
+ if (specifier.type === "ImportNamespaceSpecifier" || specifier.type === "ImportDefaultSpecifier")
292
+ zodNamespaces.add(specifier.local.name);
293
+ if (specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier" && specifier.imported.name === "z")
294
+ zodNamespaces.add(specifier.local.name);
295
+ }
296
+ },
139
297
  TSTypeAliasDeclaration: inspect,
140
298
  TSInterfaceDeclaration: inspect,
299
+ VariableDeclarator: inspectSchema,
141
300
  };
142
301
  },
143
302
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-deslop",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
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",