ai-deslop 0.3.0 → 0.4.1
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/dist/rules/no-duplicate-types.js +260 -11
- package/package.json +1 -1
|
@@ -122,6 +122,17 @@ function memberCall(node) {
|
|
|
122
122
|
return null;
|
|
123
123
|
return { name: node.callee.property.name, object: node.callee.object, arguments_: node.arguments };
|
|
124
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
|
+
}
|
|
125
136
|
function schemaLiteral(node) {
|
|
126
137
|
if (node.type !== "Literal")
|
|
127
138
|
return null;
|
|
@@ -233,24 +244,194 @@ function canonicalSchema(node, zodNamespaces) {
|
|
|
233
244
|
default: return ignoredZodMethods.has(call.name) ? inner : null;
|
|
234
245
|
}
|
|
235
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
|
+
}
|
|
236
414
|
/** Report exact structural duplicates without TypeScript assignability or fuzzy matching. */
|
|
237
415
|
export const noDuplicateTypesRule = defineRule({
|
|
238
416
|
meta: {
|
|
239
417
|
type: "suggestion",
|
|
240
418
|
docs: { description: "Report structurally identical TypeScript declarations" },
|
|
241
419
|
messages: { duplicate: "{{message}} First seen as {{firstName}} in {{firstFile}}." },
|
|
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 }],
|
|
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 }],
|
|
243
421
|
},
|
|
244
422
|
createOnce(context) {
|
|
245
423
|
const seen = new Map();
|
|
246
424
|
let options = {};
|
|
247
425
|
const report = (node, name, fingerprint, kind) => {
|
|
248
|
-
const
|
|
426
|
+
const isolates = kind === "schema" ? options.schemas?.matchTypes === false : kind.startsWith("drizzle-") && options.drizzle?.matchTypes === false;
|
|
427
|
+
const key = isolates ? `${kind}:${fingerprint}` : fingerprint;
|
|
249
428
|
const first = seen.get(key);
|
|
250
429
|
if (first === undefined) {
|
|
251
|
-
seen.set(key, { name, filename: context.filename });
|
|
430
|
+
seen.set(key, { node, name, filename: context.filename });
|
|
252
431
|
return;
|
|
253
432
|
}
|
|
433
|
+
if (first.node === node)
|
|
434
|
+
return;
|
|
254
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 } });
|
|
255
436
|
};
|
|
256
437
|
const inspect = (node) => {
|
|
@@ -271,6 +452,12 @@ export const noDuplicateTypesRule = defineRule({
|
|
|
271
452
|
report(node, node.id.name, fingerprint, "type");
|
|
272
453
|
};
|
|
273
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();
|
|
274
461
|
const inspectSchema = (node) => {
|
|
275
462
|
if (options.schemas?.libraries?.includes("zod") !== true || node.id.type !== "Identifier" || node.init === null)
|
|
276
463
|
return;
|
|
@@ -282,21 +469,83 @@ export const noDuplicateTypesRule = defineRule({
|
|
|
282
469
|
return;
|
|
283
470
|
report(node, node.id.name, schema.fingerprint, "schema");
|
|
284
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
|
+
}
|
|
500
|
+
};
|
|
285
501
|
return {
|
|
286
502
|
Program() { options = context.options[0] ?? {}; },
|
|
287
503
|
ImportDeclaration(node) {
|
|
288
|
-
if (node.source.value
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
+
}
|
|
295
539
|
}
|
|
296
540
|
},
|
|
297
541
|
TSTypeAliasDeclaration: inspect,
|
|
298
542
|
TSInterfaceDeclaration: inspect,
|
|
299
|
-
VariableDeclarator
|
|
543
|
+
VariableDeclarator(node) {
|
|
544
|
+
inspectSchema(node);
|
|
545
|
+
inspectValibot(node);
|
|
546
|
+
inspectArkType(node);
|
|
547
|
+
inspectDrizzle(node);
|
|
548
|
+
},
|
|
300
549
|
};
|
|
301
550
|
},
|
|
302
551
|
});
|