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.
@@ -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 key = options.schemas?.matchTypes === false ? `${kind}:${fingerprint}` : fingerprint;
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 !== "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);
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: inspectSchema,
543
+ VariableDeclarator(node) {
544
+ inspectSchema(node);
545
+ inspectValibot(node);
546
+ inspectArkType(node);
547
+ inspectDrizzle(node);
548
+ },
300
549
  };
301
550
  },
302
551
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-deslop",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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",