ai-deslop 0.2.3 → 0.4.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 +36 -21
- package/dist/rules/no-duplicate-types.js +402 -5
- package/package.json +1 -1
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
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
| [`no-duplicate-types`](docs/
|
|
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
|
-
| [
|
|
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")
|
|
@@ -109,23 +115,323 @@ function canonicalDeclaration(node) {
|
|
|
109
115
|
return null;
|
|
110
116
|
return canonicalMembers(node.body.body, parameters);
|
|
111
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 factoryCall(node, namespaces, functions) {
|
|
126
|
+
if (node.type !== "CallExpression" || node.arguments.some((argument) => argument.type === "SpreadElement"))
|
|
127
|
+
return null;
|
|
128
|
+
if (node.callee.type === "Identifier") {
|
|
129
|
+
const name = functions.get(node.callee.name);
|
|
130
|
+
return name === undefined ? null : { name, arguments_: node.arguments };
|
|
131
|
+
}
|
|
132
|
+
if (node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.object.type !== "Identifier" || node.callee.property.type !== "Identifier" || !namespaces.has(node.callee.object.name))
|
|
133
|
+
return null;
|
|
134
|
+
return { name: node.callee.property.name, arguments_: node.arguments };
|
|
135
|
+
}
|
|
136
|
+
function schemaLiteral(node) {
|
|
137
|
+
if (node.type !== "Literal")
|
|
138
|
+
return null;
|
|
139
|
+
if (typeof node.value === "string")
|
|
140
|
+
return JSON.stringify(node.value);
|
|
141
|
+
if (typeof node.value === "number" || typeof node.value === "boolean")
|
|
142
|
+
return String(node.value);
|
|
143
|
+
if (typeof node.value === "bigint")
|
|
144
|
+
return `${node.value}n`;
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
function canonicalSchemaProperty(node, zodNamespaces) {
|
|
148
|
+
let current = node;
|
|
149
|
+
let optional = false;
|
|
150
|
+
let nullable = false;
|
|
151
|
+
while (true) {
|
|
152
|
+
const call = memberCall(current);
|
|
153
|
+
if (call === null || !(["optional", "nullish"].includes(call.name) && call.arguments_.length === 0 || ignoredZodMethods.has(call.name)))
|
|
154
|
+
break;
|
|
155
|
+
if (call.name === "optional")
|
|
156
|
+
optional = true;
|
|
157
|
+
if (call.name === "nullish") {
|
|
158
|
+
optional = true;
|
|
159
|
+
nullable = true;
|
|
160
|
+
}
|
|
161
|
+
current = call.object;
|
|
162
|
+
}
|
|
163
|
+
const type = canonicalSchema(current, zodNamespaces);
|
|
164
|
+
return type === null ? null : { type: nullable ? `union(${["TSNullKeyword", type].sort().join(",")})` : type, optional };
|
|
165
|
+
}
|
|
166
|
+
function canonicalSchemaObject(node, zodNamespaces) {
|
|
167
|
+
if (node.type !== "ObjectExpression")
|
|
168
|
+
return null;
|
|
169
|
+
const members = [];
|
|
170
|
+
for (const property of node.properties) {
|
|
171
|
+
if (property.type !== "Property" || property.computed || property.kind !== "init")
|
|
172
|
+
return null;
|
|
173
|
+
const key = propertyKey(property.key);
|
|
174
|
+
if (key === null)
|
|
175
|
+
return null;
|
|
176
|
+
const value = canonicalSchemaProperty(property.value, zodNamespaces);
|
|
177
|
+
if (value === null)
|
|
178
|
+
return null;
|
|
179
|
+
members.push(`prop(${key},${value.optional ? "?" : "!"},rw,${value.type})`);
|
|
180
|
+
}
|
|
181
|
+
members.sort();
|
|
182
|
+
return { fingerprint: `{${members.join(",")}}`, properties: members.length };
|
|
183
|
+
}
|
|
184
|
+
function canonicalSchema(node, zodNamespaces) {
|
|
185
|
+
const call = memberCall(node);
|
|
186
|
+
if (call === null)
|
|
187
|
+
return null;
|
|
188
|
+
if (call.object.type === "Identifier" && zodNamespaces.has(call.object.name)) {
|
|
189
|
+
switch (call.name) {
|
|
190
|
+
case "string": return call.arguments_.length === 0 ? "TSStringKeyword" : null;
|
|
191
|
+
case "number": return call.arguments_.length === 0 ? "TSNumberKeyword" : null;
|
|
192
|
+
case "boolean": return call.arguments_.length === 0 ? "TSBooleanKeyword" : null;
|
|
193
|
+
case "bigint": return call.arguments_.length === 0 ? "TSBigIntKeyword" : null;
|
|
194
|
+
case "unknown": return call.arguments_.length === 0 ? "TSUnknownKeyword" : null;
|
|
195
|
+
case "any": return call.arguments_.length === 0 ? "TSAnyKeyword" : null;
|
|
196
|
+
case "never": return call.arguments_.length === 0 ? "TSNeverKeyword" : null;
|
|
197
|
+
case "null": return call.arguments_.length === 0 ? "TSNullKeyword" : null;
|
|
198
|
+
case "undefined": return call.arguments_.length === 0 ? "TSUndefinedKeyword" : null;
|
|
199
|
+
case "void": return call.arguments_.length === 0 ? "TSVoidKeyword" : null;
|
|
200
|
+
case "literal": {
|
|
201
|
+
if (call.arguments_.length !== 1)
|
|
202
|
+
return null;
|
|
203
|
+
const value = schemaLiteral(call.arguments_[0]);
|
|
204
|
+
return value === null ? null : `literal(${value})`;
|
|
205
|
+
}
|
|
206
|
+
case "enum": {
|
|
207
|
+
if (call.arguments_.length !== 1 || call.arguments_[0].type !== "ArrayExpression")
|
|
208
|
+
return null;
|
|
209
|
+
const values = call.arguments_[0].elements.map((element) => element === null || element.type === "SpreadElement" ? null : schemaLiteral(element));
|
|
210
|
+
if (values.some((value) => value === null))
|
|
211
|
+
return null;
|
|
212
|
+
return `union(${[...new Set(values)].sort().join(",")})`;
|
|
213
|
+
}
|
|
214
|
+
case "object": {
|
|
215
|
+
if (call.arguments_.length !== 1)
|
|
216
|
+
return null;
|
|
217
|
+
return canonicalSchemaObject(call.arguments_[0], zodNamespaces)?.fingerprint ?? null;
|
|
218
|
+
}
|
|
219
|
+
case "array": {
|
|
220
|
+
if (call.arguments_.length !== 1)
|
|
221
|
+
return null;
|
|
222
|
+
const element = canonicalSchema(call.arguments_[0], zodNamespaces);
|
|
223
|
+
return element === null ? null : `array(${element})`;
|
|
224
|
+
}
|
|
225
|
+
case "union": {
|
|
226
|
+
if (call.arguments_.length !== 1 || call.arguments_[0].type !== "ArrayExpression")
|
|
227
|
+
return null;
|
|
228
|
+
const types = call.arguments_[0].elements.map((element) => element === null || element.type === "SpreadElement" ? null : canonicalSchema(element, zodNamespaces));
|
|
229
|
+
if (types.some((type) => type === null))
|
|
230
|
+
return null;
|
|
231
|
+
return `union(${[...new Set(types)].sort().join(",")})`;
|
|
232
|
+
}
|
|
233
|
+
default: return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const inner = canonicalSchema(call.object, zodNamespaces);
|
|
237
|
+
if (inner === null)
|
|
238
|
+
return null;
|
|
239
|
+
switch (call.name) {
|
|
240
|
+
case "optional": return `union(${["TSUndefinedKeyword", inner].sort().join(",")})`;
|
|
241
|
+
case "nullable": return `union(${["TSNullKeyword", inner].sort().join(",")})`;
|
|
242
|
+
case "nullish": return `union(${["TSNullKeyword", "TSUndefinedKeyword", inner].sort().join(",")})`;
|
|
243
|
+
case "array": return call.arguments_.length === 0 ? `array(${inner})` : null;
|
|
244
|
+
default: return ignoredZodMethods.has(call.name) ? inner : null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function canonicalValibotProperty(node, namespaces, functions) {
|
|
248
|
+
const call = factoryCall(node, namespaces, functions);
|
|
249
|
+
if (call !== null && ["optional", "nullable", "nullish"].includes(call.name)) {
|
|
250
|
+
if (call.arguments_.length !== 1)
|
|
251
|
+
return null;
|
|
252
|
+
const inner = canonicalValibot(call.arguments_[0], namespaces, functions);
|
|
253
|
+
if (inner === null)
|
|
254
|
+
return null;
|
|
255
|
+
const nullable = call.name === "nullable" || call.name === "nullish";
|
|
256
|
+
return { type: nullable ? `union(${["TSNullKeyword", inner].sort().join(",")})` : inner, optional: call.name === "optional" || call.name === "nullish" };
|
|
257
|
+
}
|
|
258
|
+
const type = canonicalValibot(node, namespaces, functions);
|
|
259
|
+
return type === null ? null : { type, optional: false };
|
|
260
|
+
}
|
|
261
|
+
function canonicalValibotObject(node, namespaces, functions) {
|
|
262
|
+
if (node.type !== "ObjectExpression")
|
|
263
|
+
return null;
|
|
264
|
+
const members = [];
|
|
265
|
+
for (const property of node.properties) {
|
|
266
|
+
if (property.type !== "Property" || property.computed || property.kind !== "init")
|
|
267
|
+
return null;
|
|
268
|
+
const key = propertyKey(property.key);
|
|
269
|
+
if (key === null)
|
|
270
|
+
return null;
|
|
271
|
+
const value = canonicalValibotProperty(property.value, namespaces, functions);
|
|
272
|
+
if (value === null)
|
|
273
|
+
return null;
|
|
274
|
+
members.push(`prop(${key},${value.optional ? "?" : "!"},rw,${value.type})`);
|
|
275
|
+
}
|
|
276
|
+
members.sort();
|
|
277
|
+
return { fingerprint: `{${members.join(",")}}`, properties: members.length };
|
|
278
|
+
}
|
|
279
|
+
function canonicalValibot(node, namespaces, functions) {
|
|
280
|
+
const call = factoryCall(node, namespaces, functions);
|
|
281
|
+
if (call === null)
|
|
282
|
+
return null;
|
|
283
|
+
switch (call.name) {
|
|
284
|
+
case "string": return call.arguments_.length === 0 ? "TSStringKeyword" : null;
|
|
285
|
+
case "number": return call.arguments_.length === 0 ? "TSNumberKeyword" : null;
|
|
286
|
+
case "boolean": return call.arguments_.length === 0 ? "TSBooleanKeyword" : null;
|
|
287
|
+
case "bigint": return call.arguments_.length === 0 ? "TSBigIntKeyword" : null;
|
|
288
|
+
case "unknown": return call.arguments_.length === 0 ? "TSUnknownKeyword" : null;
|
|
289
|
+
case "any": return call.arguments_.length === 0 ? "TSAnyKeyword" : null;
|
|
290
|
+
case "never": return call.arguments_.length === 0 ? "TSNeverKeyword" : null;
|
|
291
|
+
case "null": return call.arguments_.length === 0 ? "TSNullKeyword" : null;
|
|
292
|
+
case "undefined": return call.arguments_.length === 0 ? "TSUndefinedKeyword" : null;
|
|
293
|
+
case "void": return call.arguments_.length === 0 ? "TSVoidKeyword" : null;
|
|
294
|
+
case "literal": {
|
|
295
|
+
if (call.arguments_.length !== 1)
|
|
296
|
+
return null;
|
|
297
|
+
const value = schemaLiteral(call.arguments_[0]);
|
|
298
|
+
return value === null ? null : `literal(${value})`;
|
|
299
|
+
}
|
|
300
|
+
case "picklist":
|
|
301
|
+
case "union": {
|
|
302
|
+
if (call.arguments_.length !== 1 || call.arguments_[0].type !== "ArrayExpression")
|
|
303
|
+
return null;
|
|
304
|
+
const types = call.arguments_[0].elements.map((element) => element === null || element.type === "SpreadElement" ? null : call.name === "picklist" ? schemaLiteral(element) : canonicalValibot(element, namespaces, functions));
|
|
305
|
+
if (types.some((type) => type === null))
|
|
306
|
+
return null;
|
|
307
|
+
return `union(${[...new Set(types)].sort().join(",")})`;
|
|
308
|
+
}
|
|
309
|
+
case "array": {
|
|
310
|
+
if (call.arguments_.length !== 1)
|
|
311
|
+
return null;
|
|
312
|
+
const element = canonicalValibot(call.arguments_[0], namespaces, functions);
|
|
313
|
+
return element === null ? null : `array(${element})`;
|
|
314
|
+
}
|
|
315
|
+
case "object": {
|
|
316
|
+
if (call.arguments_.length !== 1)
|
|
317
|
+
return null;
|
|
318
|
+
return canonicalValibotObject(call.arguments_[0], namespaces, functions)?.fingerprint ?? null;
|
|
319
|
+
}
|
|
320
|
+
default: return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function canonicalArkString(value) {
|
|
324
|
+
let current = value.trim();
|
|
325
|
+
while (current.startsWith("(") && current.endsWith(")"))
|
|
326
|
+
current = current.slice(1, -1).trim();
|
|
327
|
+
if (current.endsWith("[]")) {
|
|
328
|
+
const element = canonicalArkString(current.slice(0, -2));
|
|
329
|
+
return element === null ? null : `array(${element})`;
|
|
330
|
+
}
|
|
331
|
+
if (current.includes("|")) {
|
|
332
|
+
const types = current.split("|").map((part) => canonicalArkString(part));
|
|
333
|
+
if (types.some((type) => type === null))
|
|
334
|
+
return null;
|
|
335
|
+
return `union(${[...new Set(types)].sort().join(",")})`;
|
|
336
|
+
}
|
|
337
|
+
if (/^'(?:[^'\\]|\\.)*'$/.test(current) || /^"(?:[^"\\]|\\.)*"$/.test(current))
|
|
338
|
+
return `literal(${JSON.stringify(current.slice(1, -1))})`;
|
|
339
|
+
if (/^-?\d+(?:\.\d+)?$/.test(current) || current === "true" || current === "false")
|
|
340
|
+
return `literal(${current})`;
|
|
341
|
+
const keywords = { string: "TSStringKeyword", number: "TSNumberKeyword", boolean: "TSBooleanKeyword", bigint: "TSBigIntKeyword", unknown: "TSUnknownKeyword", any: "TSAnyKeyword", never: "TSNeverKeyword", null: "TSNullKeyword", undefined: "TSUndefinedKeyword", void: "TSVoidKeyword" };
|
|
342
|
+
return keywords[current] ?? null;
|
|
343
|
+
}
|
|
344
|
+
function canonicalArkObject(node) {
|
|
345
|
+
if (node.type !== "ObjectExpression")
|
|
346
|
+
return null;
|
|
347
|
+
const members = [];
|
|
348
|
+
for (const property of node.properties) {
|
|
349
|
+
if (property.type !== "Property" || property.computed || property.kind !== "init")
|
|
350
|
+
return null;
|
|
351
|
+
const keyValue = property.key;
|
|
352
|
+
const rawKey = typeof keyValue.value === "string" ? keyValue.value : propertyKey(property.key);
|
|
353
|
+
if (rawKey === null)
|
|
354
|
+
return null;
|
|
355
|
+
const optional = rawKey.endsWith("?");
|
|
356
|
+
const key = optional ? rawKey.slice(0, -1) : rawKey;
|
|
357
|
+
const literal = property.value;
|
|
358
|
+
const type = typeof literal.value === "string" ? canonicalArkString(literal.value) : canonicalArkObject(property.value)?.fingerprint;
|
|
359
|
+
if (type === null || type === undefined)
|
|
360
|
+
return null;
|
|
361
|
+
members.push(`prop(${key},${optional ? "?" : "!"},rw,${type})`);
|
|
362
|
+
}
|
|
363
|
+
members.sort();
|
|
364
|
+
return { fingerprint: `{${members.join(",")}}`, properties: members.length };
|
|
365
|
+
}
|
|
366
|
+
function canonicalDrizzleColumn(node, builders) {
|
|
367
|
+
let current = node;
|
|
368
|
+
let notNull = false;
|
|
369
|
+
let hasDefault = false;
|
|
370
|
+
while (true) {
|
|
371
|
+
const call = memberCall(current);
|
|
372
|
+
if (call === null)
|
|
373
|
+
break;
|
|
374
|
+
if ((call.name === "notNull" || call.name === "primaryKey") && call.arguments_.length === 0)
|
|
375
|
+
notNull = true;
|
|
376
|
+
else if (["default", "defaultNow", "defaultRandom", "$default", "$defaultFn"].includes(call.name))
|
|
377
|
+
hasDefault = true;
|
|
378
|
+
else if (!["primaryKey", "unique", "references", "onUpdate", "$onUpdate", "$onUpdateFn"].includes(call.name))
|
|
379
|
+
return null;
|
|
380
|
+
current = call.object;
|
|
381
|
+
}
|
|
382
|
+
if (current.type !== "CallExpression" || current.callee.type !== "Identifier")
|
|
383
|
+
return null;
|
|
384
|
+
const type = builders.get(current.callee.name);
|
|
385
|
+
if (type === undefined)
|
|
386
|
+
return null;
|
|
387
|
+
if (current.callee.name === "serial")
|
|
388
|
+
hasDefault = true;
|
|
389
|
+
return { type, notNull, hasDefault };
|
|
390
|
+
}
|
|
391
|
+
function canonicalDrizzleTable(node, tableFactories, builders, model) {
|
|
392
|
+
if (node.type !== "CallExpression" || node.callee.type !== "Identifier" || !tableFactories.has(node.callee.name) || node.arguments.length < 2 || node.arguments[1].type === "SpreadElement")
|
|
393
|
+
return null;
|
|
394
|
+
const columns = node.arguments[1];
|
|
395
|
+
if (columns.type !== "ObjectExpression")
|
|
396
|
+
return null;
|
|
397
|
+
const members = [];
|
|
398
|
+
for (const property of columns.properties) {
|
|
399
|
+
if (property.type !== "Property" || property.computed || property.kind !== "init")
|
|
400
|
+
return null;
|
|
401
|
+
const key = propertyKey(property.key);
|
|
402
|
+
if (key === null)
|
|
403
|
+
return null;
|
|
404
|
+
const column = canonicalDrizzleColumn(property.value, builders);
|
|
405
|
+
if (column === null)
|
|
406
|
+
return null;
|
|
407
|
+
const optional = model === "insert" && (!column.notNull || column.hasDefault);
|
|
408
|
+
const type = column.notNull ? column.type : `union(${["TSNullKeyword", column.type].sort().join(",")})`;
|
|
409
|
+
members.push(`prop(${key},${optional ? "?" : "!"},rw,${type})`);
|
|
410
|
+
}
|
|
411
|
+
members.sort();
|
|
412
|
+
return { fingerprint: `{${members.join(",")}}`, properties: members.length };
|
|
413
|
+
}
|
|
112
414
|
/** Report exact structural duplicates without TypeScript assignability or fuzzy matching. */
|
|
113
415
|
export const noDuplicateTypesRule = defineRule({
|
|
114
416
|
meta: {
|
|
115
417
|
type: "suggestion",
|
|
116
418
|
docs: { description: "Report structurally identical TypeScript declarations" },
|
|
117
419
|
messages: { duplicate: "{{message}} First seen as {{firstName}} in {{firstFile}}." },
|
|
118
|
-
schema: [{ type: "object", properties: { message: { type: "string" }, minProperties: { type: "integer", minimum: 0 } }, additionalProperties: false }],
|
|
420
|
+
schema: [{ type: "object", properties: { message: { type: "string" }, minProperties: { type: "integer", minimum: 0 }, schemas: { type: "object", properties: { libraries: { type: "array", items: { enum: ["zod", "valibot", "arktype"] } }, matchTypes: { type: "boolean" } }, additionalProperties: false }, drizzle: { type: "object", properties: { models: { type: "array", items: { enum: ["select", "insert"] } }, matchTypes: { type: "boolean" } }, additionalProperties: false } }, additionalProperties: false }],
|
|
119
421
|
},
|
|
120
422
|
createOnce(context) {
|
|
121
423
|
const seen = new Map();
|
|
122
424
|
let options = {};
|
|
123
|
-
const report = (node, fingerprint) => {
|
|
124
|
-
const
|
|
425
|
+
const report = (node, name, fingerprint, kind) => {
|
|
426
|
+
const isolates = kind === "schema" ? options.schemas?.matchTypes === false : kind.startsWith("drizzle-") && options.drizzle?.matchTypes === false;
|
|
427
|
+
const key = isolates ? `${kind}:${fingerprint}` : fingerprint;
|
|
428
|
+
const first = seen.get(key);
|
|
125
429
|
if (first === undefined) {
|
|
126
|
-
seen.set(
|
|
430
|
+
seen.set(key, { node, name, filename: context.filename });
|
|
127
431
|
return;
|
|
128
432
|
}
|
|
433
|
+
if (first.node === node)
|
|
434
|
+
return;
|
|
129
435
|
context.report({ node, messageId: "duplicate", data: { message: options.message ?? "This type is structurally identical to an existing declaration.", firstName: first.name, firstFile: first.filename } });
|
|
130
436
|
};
|
|
131
437
|
const inspect = (node) => {
|
|
@@ -143,12 +449,103 @@ export const noDuplicateTypesRule = defineRule({
|
|
|
143
449
|
return;
|
|
144
450
|
if (properties < (options.minProperties ?? 2))
|
|
145
451
|
return;
|
|
146
|
-
report(node, fingerprint);
|
|
452
|
+
report(node, node.id.name, fingerprint, "type");
|
|
453
|
+
};
|
|
454
|
+
const zodNamespaces = new Set();
|
|
455
|
+
const valibotNamespaces = new Set();
|
|
456
|
+
const valibotFunctions = new Map();
|
|
457
|
+
const arkTypeFunctions = new Set();
|
|
458
|
+
let hasArkTypeImport = false;
|
|
459
|
+
const drizzleTables = new Set();
|
|
460
|
+
const drizzleBuilders = new Map();
|
|
461
|
+
const inspectSchema = (node) => {
|
|
462
|
+
if (options.schemas?.libraries?.includes("zod") !== true || node.id.type !== "Identifier" || node.init === null)
|
|
463
|
+
return;
|
|
464
|
+
const call = memberCall(node.init);
|
|
465
|
+
if (call === null || call.name !== "object" || call.arguments_.length !== 1 || call.object.type !== "Identifier" || !zodNamespaces.has(call.object.name))
|
|
466
|
+
return;
|
|
467
|
+
const schema = canonicalSchemaObject(call.arguments_[0], zodNamespaces);
|
|
468
|
+
if (schema === null || schema.properties < (options.minProperties ?? 2))
|
|
469
|
+
return;
|
|
470
|
+
report(node, node.id.name, schema.fingerprint, "schema");
|
|
471
|
+
};
|
|
472
|
+
const inspectValibot = (node) => {
|
|
473
|
+
if (options.schemas?.libraries?.includes("valibot") !== true || node.id.type !== "Identifier" || node.init === null)
|
|
474
|
+
return;
|
|
475
|
+
const call = factoryCall(node.init, valibotNamespaces, valibotFunctions);
|
|
476
|
+
if (call === null || call.name !== "object" || call.arguments_.length !== 1)
|
|
477
|
+
return;
|
|
478
|
+
const schema = canonicalValibotObject(call.arguments_[0], valibotNamespaces, valibotFunctions);
|
|
479
|
+
if (schema === null || schema.properties < (options.minProperties ?? 2))
|
|
480
|
+
return;
|
|
481
|
+
report(node, node.id.name, schema.fingerprint, "schema");
|
|
482
|
+
};
|
|
483
|
+
const inspectArkType = (node) => {
|
|
484
|
+
const callee = node.init?.type === "CallExpression" ? node.init.callee : null;
|
|
485
|
+
if (options.schemas?.libraries?.includes("arktype") !== true || node.id.type !== "Identifier" || node.init === null || node.init.type !== "CallExpression" || typeof callee?.name !== "string" || !(arkTypeFunctions.has(callee.name) || hasArkTypeImport && callee.name === "type") || node.init.arguments.length !== 1 || node.init.arguments[0].type === "SpreadElement")
|
|
486
|
+
return;
|
|
487
|
+
const schema = canonicalArkObject(node.init.arguments[0]);
|
|
488
|
+
if (schema === null || schema.properties < (options.minProperties ?? 2))
|
|
489
|
+
return;
|
|
490
|
+
report(node, node.id.name, schema.fingerprint, "schema");
|
|
491
|
+
};
|
|
492
|
+
const inspectDrizzle = (node) => {
|
|
493
|
+
if (options.drizzle === undefined || node.id.type !== "Identifier" || node.init === null)
|
|
494
|
+
return;
|
|
495
|
+
for (const model of options.drizzle.models ?? ["select", "insert"]) {
|
|
496
|
+
const table = canonicalDrizzleTable(node.init, drizzleTables, drizzleBuilders, model);
|
|
497
|
+
if (table !== null && table.properties >= (options.minProperties ?? 2))
|
|
498
|
+
report(node, node.id.name, table.fingerprint, `drizzle-${model}`);
|
|
499
|
+
}
|
|
147
500
|
};
|
|
148
501
|
return {
|
|
149
502
|
Program() { options = context.options[0] ?? {}; },
|
|
503
|
+
ImportDeclaration(node) {
|
|
504
|
+
if (node.source.value === "zod") {
|
|
505
|
+
for (const specifier of node.specifiers) {
|
|
506
|
+
if (specifier.type === "ImportNamespaceSpecifier" || specifier.type === "ImportDefaultSpecifier")
|
|
507
|
+
zodNamespaces.add(specifier.local.name);
|
|
508
|
+
if (specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier" && specifier.imported.name === "z")
|
|
509
|
+
zodNamespaces.add(specifier.local.name);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (node.source.value === "valibot") {
|
|
513
|
+
for (const specifier of node.specifiers) {
|
|
514
|
+
if (specifier.type === "ImportNamespaceSpecifier")
|
|
515
|
+
valibotNamespaces.add(specifier.local.name);
|
|
516
|
+
if (specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier")
|
|
517
|
+
valibotFunctions.set(specifier.local.name, specifier.imported.name);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (node.source.value === "arktype") {
|
|
521
|
+
hasArkTypeImport = true;
|
|
522
|
+
for (const specifier of node.specifiers) {
|
|
523
|
+
const imported = specifier.type === "ImportSpecifier" ? specifier.imported : null;
|
|
524
|
+
if (typeof imported?.name === "string" && imported.name === "type")
|
|
525
|
+
arkTypeFunctions.add(specifier.local.name);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (/^drizzle-orm\/(pg|mysql|sqlite)-core$/.test(String(node.source.value))) {
|
|
529
|
+
for (const specifier of node.specifiers) {
|
|
530
|
+
if (specifier.type !== "ImportSpecifier" || specifier.imported.type !== "Identifier")
|
|
531
|
+
continue;
|
|
532
|
+
if (["pgTable", "mysqlTable", "sqliteTable"].includes(specifier.imported.name))
|
|
533
|
+
drizzleTables.add(specifier.local.name);
|
|
534
|
+
const types = { text: "TSStringKeyword", varchar: "TSStringKeyword", uuid: "TSStringKeyword", char: "TSStringKeyword", integer: "TSNumberKeyword", int: "TSNumberKeyword", serial: "TSNumberKeyword", smallint: "TSNumberKeyword", boolean: "TSBooleanKeyword" };
|
|
535
|
+
const type = types[specifier.imported.name];
|
|
536
|
+
if (type !== undefined)
|
|
537
|
+
drizzleBuilders.set(specifier.local.name, type);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
},
|
|
150
541
|
TSTypeAliasDeclaration: inspect,
|
|
151
542
|
TSInterfaceDeclaration: inspect,
|
|
543
|
+
VariableDeclarator(node) {
|
|
544
|
+
inspectSchema(node);
|
|
545
|
+
inspectValibot(node);
|
|
546
|
+
inspectArkType(node);
|
|
547
|
+
inspectDrizzle(node);
|
|
548
|
+
},
|
|
152
549
|
};
|
|
153
550
|
},
|
|
154
551
|
});
|