ai-deslop 0.4.1 → 0.4.2

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.
@@ -0,0 +1,9 @@
1
+ import type { ESTree } from "@oxlint/plugins";
2
+ export interface DrizzleBuilder {
3
+ name: string;
4
+ type: string;
5
+ }
6
+ export declare function canonicalDrizzleTable(node: ESTree.Expression, tableFactories: Set<string>, schemaObjects: Set<string>, builders: Map<string, DrizzleBuilder>, model: "select" | "insert"): {
7
+ fingerprint: string;
8
+ properties: number;
9
+ } | null;
@@ -0,0 +1,66 @@
1
+ import { canonicalType, memberCall, propertyKey } from "./shared.js";
2
+ function canonicalIntegerType(node, builder) {
3
+ if (builder.name !== "integer" || node.arguments.length === 1)
4
+ return builder.type;
5
+ if (node.arguments.length !== 2 || node.arguments[1].type !== "ObjectExpression")
6
+ return null;
7
+ let mode = null;
8
+ for (const property of node.arguments[1].properties) {
9
+ if (property.type !== "Property" || property.computed || property.kind !== "init" || propertyKey(property.key) !== "mode" || property.value.type !== "Literal" || typeof property.value.value !== "string")
10
+ return null;
11
+ mode = property.value.value;
12
+ }
13
+ return mode === null ? builder.type : mode === "boolean" ? "TSBooleanKeyword" : null;
14
+ }
15
+ function canonicalColumn(node, builders) {
16
+ let current = node, notNull = false, hasDefault = false, typeOverride = null;
17
+ while (true) {
18
+ const call = memberCall(current);
19
+ if (call === null)
20
+ break;
21
+ if (call.name === "notNull" && call.arguments_.length === 0)
22
+ notNull = true;
23
+ else if (call.name === "primaryKey")
24
+ notNull = true;
25
+ else if (call.name === "$type" && call.arguments_.length === 0 && call.typeArguments !== null && call.typeArguments.params.length === 1 && typeOverride === null) {
26
+ typeOverride = canonicalType(call.typeArguments.params[0], new Map());
27
+ if (typeOverride === null)
28
+ return null;
29
+ }
30
+ else if (["default", "defaultNow", "defaultRandom", "$default", "$defaultFn"].includes(call.name))
31
+ hasDefault = true;
32
+ else if (!["primaryKey", "unique", "references", "onUpdate", "$onUpdate", "$onUpdateFn"].includes(call.name))
33
+ return null;
34
+ current = call.object;
35
+ }
36
+ if (current.type !== "CallExpression" || current.callee.type !== "Identifier")
37
+ return null;
38
+ const builder = builders.get(current.callee.name);
39
+ if (builder === undefined)
40
+ return null;
41
+ const type = typeOverride ?? canonicalIntegerType(current, builder);
42
+ if (type === null)
43
+ return null;
44
+ return { type, notNull, hasDefault: hasDefault || builder.name === "serial" };
45
+ }
46
+ export function canonicalDrizzleTable(node, tableFactories, schemaObjects, builders, model) {
47
+ if (node.type !== "CallExpression" || node.arguments.length < 2 || node.arguments[1].type === "SpreadElement")
48
+ return null;
49
+ const directTable = node.callee.type === "Identifier" && tableFactories.has(node.callee.name);
50
+ const schemaTable = node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.object.type === "Identifier" && schemaObjects.has(node.callee.object.name) && node.callee.property.type === "Identifier" && node.callee.property.name === "table";
51
+ if (!directTable && !schemaTable || node.arguments[1].type !== "ObjectExpression")
52
+ return null;
53
+ const members = [];
54
+ for (const property of node.arguments[1].properties) {
55
+ if (property.type !== "Property" || property.computed || property.kind !== "init")
56
+ return null;
57
+ const key = propertyKey(property.key), column = canonicalColumn(property.value, builders);
58
+ if (key === null || column === null)
59
+ return null;
60
+ const optional = model === "insert" && (!column.notNull || column.hasDefault);
61
+ const type = column.notNull ? column.type : `union(${["TSNullKeyword", column.type].sort().join(",")})`;
62
+ members.push(`prop(${key},${optional ? "?" : "!"},rw,${type})`);
63
+ }
64
+ members.sort();
65
+ return { fingerprint: `{${members.join(",")}}`, properties: members.length };
66
+ }
@@ -0,0 +1,9 @@
1
+ import type { ESTree } from "@oxlint/plugins";
2
+ type CanonicalObject = {
3
+ fingerprint: string;
4
+ properties: number;
5
+ };
6
+ export declare function canonicalZodObject(node: ESTree.Expression, namespaces: Set<string>): CanonicalObject | null;
7
+ export declare function canonicalValibotObject(node: ESTree.Expression, namespaces: Set<string>, functions: Map<string, string>): CanonicalObject | null;
8
+ export declare function canonicalArkObject(node: ESTree.Expression): CanonicalObject | null;
9
+ export {};
@@ -0,0 +1,152 @@
1
+ import { factoryCall, memberCall, propertyKey, schemaLiteral } from "./shared.js";
2
+ const ignoredZodMethods = new Set(["min", "max", "length", "nonempty", "email", "url", "uuid", "guid", "cuid", "cuid2", "ulid", "xid", "ksuid", "nanoid", "regex", "includes", "startsWith", "endsWith", "datetime", "date", "time", "duration", "ip", "cidr", "emoji", "base64", "base64url", "jwt", "int", "safe", "finite", "multipleOf", "step", "positive", "negative", "nonpositive", "nonnegative", "gt", "gte", "lt", "lte", "describe", "brand", "readonly", "default"]);
3
+ const keywordTypes = { string: "TSStringKeyword", number: "TSNumberKeyword", boolean: "TSBooleanKeyword", bigint: "TSBigIntKeyword", unknown: "TSUnknownKeyword", any: "TSAnyKeyword", never: "TSNeverKeyword", null: "TSNullKeyword", undefined: "TSUndefinedKeyword", void: "TSVoidKeyword" };
4
+ const union = (types) => `union(${[...new Set(types)].sort().join(",")})`;
5
+ function objectFingerprint(node, canonicalProperty) {
6
+ if (node.type !== "ObjectExpression")
7
+ return null;
8
+ const members = [];
9
+ for (const property of node.properties) {
10
+ if (property.type !== "Property" || property.computed || property.kind !== "init")
11
+ return null;
12
+ const key = propertyKey(property.key), value = canonicalProperty(property.value);
13
+ if (key === null || value === null)
14
+ return null;
15
+ members.push(`prop(${key},${value.optional ? "?" : "!"},rw,${value.type})`);
16
+ }
17
+ members.sort();
18
+ return { fingerprint: `{${members.join(",")}}`, properties: members.length };
19
+ }
20
+ function canonicalZodProperty(node, namespaces) {
21
+ let current = node, optional = false, nullable = false;
22
+ while (true) {
23
+ const call = memberCall(current);
24
+ if (call === null || !((["optional", "nullish"].includes(call.name) && call.arguments_.length === 0) || ignoredZodMethods.has(call.name)))
25
+ break;
26
+ if (call.name === "optional")
27
+ optional = true;
28
+ if (call.name === "nullish") {
29
+ optional = true;
30
+ nullable = true;
31
+ }
32
+ current = call.object;
33
+ }
34
+ const type = canonicalZod(current, namespaces);
35
+ return type === null ? null : { type: nullable ? union(["TSNullKeyword", type]) : type, optional };
36
+ }
37
+ export function canonicalZodObject(node, namespaces) {
38
+ return objectFingerprint(node, (value) => canonicalZodProperty(value, namespaces));
39
+ }
40
+ function canonicalZod(node, namespaces) {
41
+ const call = memberCall(node);
42
+ if (call === null)
43
+ return null;
44
+ if (call.object.type === "Identifier" && namespaces.has(call.object.name)) {
45
+ if (call.name in keywordTypes)
46
+ return call.arguments_.length === 0 ? keywordTypes[call.name] : null;
47
+ if (call.name === "literal") {
48
+ const value = call.arguments_.length === 1 ? schemaLiteral(call.arguments_[0]) : null;
49
+ return value === null ? null : `literal(${value})`;
50
+ }
51
+ if (call.name === "enum" || call.name === "union") {
52
+ if (call.arguments_.length !== 1 || call.arguments_[0].type !== "ArrayExpression")
53
+ return null;
54
+ const types = call.arguments_[0].elements.map((element) => element === null || element.type === "SpreadElement" ? null : call.name === "enum" ? schemaLiteral(element) : canonicalZod(element, namespaces));
55
+ return types.some((type) => type === null) ? null : union(types);
56
+ }
57
+ if (call.name === "object")
58
+ return call.arguments_.length === 1 ? canonicalZodObject(call.arguments_[0], namespaces)?.fingerprint ?? null : null;
59
+ if (call.name === "array") {
60
+ const element = call.arguments_.length === 1 ? canonicalZod(call.arguments_[0], namespaces) : null;
61
+ return element === null ? null : `array(${element})`;
62
+ }
63
+ return null;
64
+ }
65
+ const inner = canonicalZod(call.object, namespaces);
66
+ if (inner === null)
67
+ return null;
68
+ if (call.name === "optional")
69
+ return union(["TSUndefinedKeyword", inner]);
70
+ if (call.name === "nullable")
71
+ return union(["TSNullKeyword", inner]);
72
+ if (call.name === "nullish")
73
+ return union(["TSNullKeyword", "TSUndefinedKeyword", inner]);
74
+ if (call.name === "array")
75
+ return call.arguments_.length === 0 ? `array(${inner})` : null;
76
+ return ignoredZodMethods.has(call.name) ? inner : null;
77
+ }
78
+ function canonicalValibotProperty(node, namespaces, functions) {
79
+ const call = factoryCall(node, namespaces, functions);
80
+ if (call !== null && ["optional", "nullable", "nullish"].includes(call.name)) {
81
+ if (call.arguments_.length !== 1)
82
+ return null;
83
+ const inner = canonicalValibot(call.arguments_[0], namespaces, functions);
84
+ if (inner === null)
85
+ return null;
86
+ return { type: call.name === "nullable" || call.name === "nullish" ? union(["TSNullKeyword", inner]) : inner, optional: call.name === "optional" || call.name === "nullish" };
87
+ }
88
+ const type = canonicalValibot(node, namespaces, functions);
89
+ return type === null ? null : { type, optional: false };
90
+ }
91
+ export function canonicalValibotObject(node, namespaces, functions) {
92
+ return objectFingerprint(node, (value) => canonicalValibotProperty(value, namespaces, functions));
93
+ }
94
+ function canonicalValibot(node, namespaces, functions) {
95
+ const call = factoryCall(node, namespaces, functions);
96
+ if (call === null)
97
+ return null;
98
+ if (call.name in keywordTypes)
99
+ return call.arguments_.length === 0 ? keywordTypes[call.name] : null;
100
+ if (call.name === "literal") {
101
+ const value = call.arguments_.length === 1 ? schemaLiteral(call.arguments_[0]) : null;
102
+ return value === null ? null : `literal(${value})`;
103
+ }
104
+ if (call.name === "picklist" || call.name === "union") {
105
+ if (call.arguments_.length !== 1 || call.arguments_[0].type !== "ArrayExpression")
106
+ return null;
107
+ const types = call.arguments_[0].elements.map((element) => element === null || element.type === "SpreadElement" ? null : call.name === "picklist" ? schemaLiteral(element) : canonicalValibot(element, namespaces, functions));
108
+ return types.some((type) => type === null) ? null : union(types);
109
+ }
110
+ if (call.name === "array") {
111
+ const element = call.arguments_.length === 1 ? canonicalValibot(call.arguments_[0], namespaces, functions) : null;
112
+ return element === null ? null : `array(${element})`;
113
+ }
114
+ return call.name === "object" && call.arguments_.length === 1 ? canonicalValibotObject(call.arguments_[0], namespaces, functions)?.fingerprint ?? null : null;
115
+ }
116
+ function canonicalArkString(value) {
117
+ let current = value.trim();
118
+ while (current.startsWith("(") && current.endsWith(")"))
119
+ current = current.slice(1, -1).trim();
120
+ if (current.endsWith("[]")) {
121
+ const element = canonicalArkString(current.slice(0, -2));
122
+ return element === null ? null : `array(${element})`;
123
+ }
124
+ if (current.includes("|")) {
125
+ const types = current.split("|").map(canonicalArkString);
126
+ return types.some((type) => type === null) ? null : union(types);
127
+ }
128
+ if (/^'(?:[^'\\]|\\.)*'$/.test(current) || /^"(?:[^"\\]|\\.)*"$/.test(current))
129
+ return `literal(${JSON.stringify(current.slice(1, -1))})`;
130
+ if (/^-?\d+(?:\.\d+)?$/.test(current) || current === "true" || current === "false")
131
+ return `literal(${current})`;
132
+ return keywordTypes[current] ?? null;
133
+ }
134
+ export function canonicalArkObject(node) {
135
+ if (node.type !== "ObjectExpression")
136
+ return null;
137
+ const members = [];
138
+ for (const property of node.properties) {
139
+ if (property.type !== "Property" || property.computed || property.kind !== "init")
140
+ return null;
141
+ const keyValue = property.key, rawKey = typeof keyValue.value === "string" ? keyValue.value : propertyKey(property.key);
142
+ if (rawKey === null)
143
+ return null;
144
+ const optional = rawKey.endsWith("?"), key = optional ? rawKey.slice(0, -1) : rawKey, literal = property.value;
145
+ const type = typeof literal.value === "string" ? canonicalArkString(literal.value) : canonicalArkObject(property.value)?.fingerprint;
146
+ if (type === null || type === undefined)
147
+ return null;
148
+ members.push(`prop(${key},${optional ? "?" : "!"},rw,${type})`);
149
+ }
150
+ members.sort();
151
+ return { fingerprint: `{${members.join(",")}}`, properties: members.length };
152
+ }
@@ -0,0 +1,18 @@
1
+ import type { ESTree } from "@oxlint/plugins";
2
+ export type TypeDeclaration = ESTree.TSTypeAliasDeclaration | ESTree.TSInterfaceDeclaration;
3
+ export type ReportNode = TypeDeclaration | ESTree.VariableDeclarator;
4
+ export declare function propertyKey(node: unknown): string | null;
5
+ export declare function canonicalTypeName(node: ESTree.TSTypeName): string | null;
6
+ export declare function canonicalType(node: ESTree.TSType, parameters: Map<string, string>): string | null;
7
+ export declare function canonicalDeclaration(node: TypeDeclaration): string | null;
8
+ export declare function memberCall(node: ESTree.Expression): {
9
+ name: string;
10
+ object: ESTree.Expression;
11
+ arguments_: ESTree.Expression[];
12
+ typeArguments: ESTree.TSTypeParameterInstantiation | null;
13
+ } | null;
14
+ export declare function factoryCall(node: ESTree.Expression, namespaces: Set<string>, functions: Map<string, string>): {
15
+ name: string;
16
+ arguments_: ESTree.Expression[];
17
+ } | null;
18
+ export declare function schemaLiteral(node: ESTree.Expression): string | null;
@@ -0,0 +1,130 @@
1
+ export function propertyKey(node) {
2
+ const value = node;
3
+ if (value.type === "Identifier" && typeof value.name === "string")
4
+ return value.name;
5
+ if (typeof value.value === "string")
6
+ return JSON.stringify(value.value);
7
+ if (typeof value.value === "number")
8
+ return String(value.value);
9
+ return null;
10
+ }
11
+ function literalValue(node) {
12
+ const literal = node.literal;
13
+ if (typeof literal.value === "string")
14
+ return JSON.stringify(literal.value);
15
+ if (typeof literal.value === "number" || typeof literal.value === "boolean")
16
+ return String(literal.value);
17
+ if (typeof literal.value === "bigint")
18
+ return `${literal.value}n`;
19
+ if (literal.type === "UnaryExpression" && literal.operator === "-") {
20
+ if (typeof literal.argument?.value === "number")
21
+ return `-${literal.argument.value}`;
22
+ if (typeof literal.argument?.value === "bigint")
23
+ return `-${literal.argument.value}n`;
24
+ }
25
+ return null;
26
+ }
27
+ export function canonicalTypeName(node) {
28
+ if (node.type === "Identifier")
29
+ return node.name;
30
+ if (node.type !== "TSQualifiedName")
31
+ return null;
32
+ const left = canonicalTypeName(node.left);
33
+ return left === null ? null : `${left}.${node.right.name}`;
34
+ }
35
+ function canonicalMembers(members, parameters) {
36
+ const result = [];
37
+ for (const member of members) {
38
+ if (member.type !== "TSPropertySignature" || member.computed || member.typeAnnotation === null)
39
+ return null;
40
+ const key = propertyKey(member.key);
41
+ const type = canonicalType(member.typeAnnotation.typeAnnotation, parameters);
42
+ if (key === null || type === null)
43
+ return null;
44
+ result.push(`prop(${key},${member.optional ? "?" : "!"},${member.readonly ? "ro" : "rw"},${type})`);
45
+ }
46
+ result.sort();
47
+ return `{${result.join(",")}}`;
48
+ }
49
+ export function canonicalType(node, parameters) {
50
+ switch (node.type) {
51
+ case "TSAnyKeyword":
52
+ case "TSBigIntKeyword":
53
+ case "TSBooleanKeyword":
54
+ case "TSNeverKeyword":
55
+ case "TSNullKeyword":
56
+ case "TSNumberKeyword":
57
+ case "TSObjectKeyword":
58
+ case "TSStringKeyword":
59
+ case "TSSymbolKeyword":
60
+ case "TSUndefinedKeyword":
61
+ case "TSUnknownKeyword":
62
+ case "TSVoidKeyword": return node.type;
63
+ case "TSLiteralType": {
64
+ const value = literalValue(node);
65
+ return value === null ? null : `literal(${value})`;
66
+ }
67
+ case "TSArrayType": {
68
+ const element = canonicalType(node.elementType, parameters);
69
+ return element === null ? null : `array(${element})`;
70
+ }
71
+ case "TSParenthesizedType": return canonicalType(node.typeAnnotation, parameters);
72
+ case "TSTypeOperator": {
73
+ const type = canonicalType(node.typeAnnotation, parameters);
74
+ return type === null ? null : `${node.operator}(${type})`;
75
+ }
76
+ case "TSUnionType":
77
+ case "TSIntersectionType": {
78
+ const types = node.types.map((type) => canonicalType(type, parameters));
79
+ if (types.some((type) => type === null))
80
+ return null;
81
+ return `${node.type === "TSUnionType" ? "union" : "intersection"}(${[...new Set(types)].sort().join(",")})`;
82
+ }
83
+ case "TSTypeLiteral": return canonicalMembers(node.members, parameters);
84
+ case "TSTypeReference": {
85
+ const name = canonicalTypeName(node.typeName);
86
+ if (name === null)
87
+ return null;
88
+ const arguments_ = node.typeArguments?.params.map((type) => canonicalType(type, parameters)) ?? [];
89
+ if (arguments_.some((type) => type === null))
90
+ return null;
91
+ return `ref(${parameters.get(name) ?? name}<${arguments_.join(",")}>)`;
92
+ }
93
+ default: return null;
94
+ }
95
+ }
96
+ export function canonicalDeclaration(node) {
97
+ const parameters = new Map();
98
+ for (const [index, parameter] of (node.typeParameters?.params ?? []).entries())
99
+ parameters.set(parameter.name.name, `$${index}`);
100
+ if (node.type === "TSTypeAliasDeclaration")
101
+ return node.typeAnnotation.type === "TSTypeLiteral" ? canonicalMembers(node.typeAnnotation.members, parameters) : null;
102
+ return node.extends.length === 0 ? canonicalMembers(node.body.body, parameters) : null;
103
+ }
104
+ export function memberCall(node) {
105
+ if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.property.type !== "Identifier" || node.arguments.some((argument) => argument.type === "SpreadElement"))
106
+ return null;
107
+ return { name: node.callee.property.name, object: node.callee.object, arguments_: node.arguments, typeArguments: node.typeArguments ?? null };
108
+ }
109
+ export function factoryCall(node, namespaces, functions) {
110
+ if (node.type !== "CallExpression" || node.arguments.some((argument) => argument.type === "SpreadElement"))
111
+ return null;
112
+ if (node.callee.type === "Identifier") {
113
+ const name = functions.get(node.callee.name);
114
+ return name === undefined ? null : { name, arguments_: node.arguments };
115
+ }
116
+ if (node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.object.type !== "Identifier" || node.callee.property.type !== "Identifier" || !namespaces.has(node.callee.object.name))
117
+ return null;
118
+ return { name: node.callee.property.name, arguments_: node.arguments };
119
+ }
120
+ export function schemaLiteral(node) {
121
+ if (node.type !== "Literal")
122
+ return null;
123
+ if (typeof node.value === "string")
124
+ return JSON.stringify(node.value);
125
+ if (typeof node.value === "number" || typeof node.value === "boolean")
126
+ return String(node.value);
127
+ if (typeof node.value === "bigint")
128
+ return `${node.value}n`;
129
+ return null;
130
+ }
@@ -1,416 +1,12 @@
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
- ]);
8
- function propertyKey(node) {
9
- const value = node;
10
- if (value.type === "Identifier" && typeof value.name === "string")
11
- return value.name;
12
- if (typeof value.value === "string")
13
- return JSON.stringify(value.value);
14
- if (typeof value.value === "number")
15
- return String(value.value);
16
- return null;
17
- }
18
- function literalValue(node) {
19
- const literal = node.literal;
20
- if (typeof literal.value === "string")
21
- return JSON.stringify(literal.value);
22
- if (typeof literal.value === "number" || typeof literal.value === "boolean")
23
- return String(literal.value);
24
- if (typeof literal.value === "bigint")
25
- return `${literal.value}n`;
26
- if (literal.type === "UnaryExpression" && literal.operator === "-") {
27
- if (typeof literal.argument?.value === "number")
28
- return `-${literal.argument.value}`;
29
- if (typeof literal.argument?.value === "bigint")
30
- return `-${literal.argument.value}n`;
31
- }
32
- return null;
33
- }
34
- function canonicalTypeName(node) {
35
- if (node.type === "Identifier")
36
- return node.name;
37
- if (node.type !== "TSQualifiedName")
38
- return null;
39
- const left = canonicalTypeName(node.left);
40
- return left === null ? null : `${left}.${node.right.name}`;
41
- }
42
- function canonicalMembers(members, parameters) {
43
- const result = [];
44
- for (const member of members) {
45
- if (member.type !== "TSPropertySignature" || member.computed || member.typeAnnotation === null)
46
- return null;
47
- const key = propertyKey(member.key);
48
- const type = canonicalType(member.typeAnnotation.typeAnnotation, parameters);
49
- if (key === null || type === null)
50
- return null;
51
- result.push(`prop(${key},${member.optional ? "?" : "!"},${member.readonly ? "ro" : "rw"},${type})`);
52
- }
53
- result.sort();
54
- return `{${result.join(",")}}`;
55
- }
56
- function canonicalType(node, parameters) {
57
- switch (node.type) {
58
- case "TSAnyKeyword":
59
- case "TSBigIntKeyword":
60
- case "TSBooleanKeyword":
61
- case "TSNeverKeyword":
62
- case "TSNullKeyword":
63
- case "TSNumberKeyword":
64
- case "TSObjectKeyword":
65
- case "TSStringKeyword":
66
- case "TSSymbolKeyword":
67
- case "TSUndefinedKeyword":
68
- case "TSUnknownKeyword":
69
- case "TSVoidKeyword":
70
- return node.type;
71
- case "TSLiteralType": {
72
- const value = literalValue(node);
73
- return value === null ? null : `literal(${value})`;
74
- }
75
- case "TSArrayType": {
76
- const element = canonicalType(node.elementType, parameters);
77
- return element === null ? null : `array(${element})`;
78
- }
79
- case "TSParenthesizedType": return canonicalType(node.typeAnnotation, parameters);
80
- case "TSTypeOperator": {
81
- const type = canonicalType(node.typeAnnotation, parameters);
82
- return type === null ? null : `${node.operator}(${type})`;
83
- }
84
- case "TSUnionType":
85
- case "TSIntersectionType": {
86
- const types = node.types.map((type) => canonicalType(type, parameters));
87
- if (types.some((type) => type === null))
88
- return null;
89
- return `${node.type === "TSUnionType" ? "union" : "intersection"}(${[...new Set(types)].sort().join(",")})`;
90
- }
91
- case "TSTypeLiteral": return canonicalMembers(node.members, parameters);
92
- case "TSTypeReference": {
93
- const name = canonicalTypeName(node.typeName);
94
- if (name === null)
95
- return null;
96
- const arguments_ = node.typeArguments?.params.map((type) => canonicalType(type, parameters)) ?? [];
97
- if (arguments_.some((type) => type === null))
98
- return null;
99
- return `ref(${parameters.get(name) ?? name}<${arguments_.join(",")}>)`;
100
- }
101
- default: return null;
102
- }
103
- }
104
- function canonicalDeclaration(node) {
105
- const parameters = new Map();
106
- for (const [index, parameter] of (node.typeParameters?.params ?? []).entries()) {
107
- parameters.set(parameter.name.name, `$${index}`);
108
- }
109
- if (node.type === "TSTypeAliasDeclaration") {
110
- if (node.typeAnnotation.type !== "TSTypeLiteral")
111
- return null;
112
- return canonicalMembers(node.typeAnnotation.members, parameters);
113
- }
114
- if (node.extends.length > 0)
115
- return null;
116
- return canonicalMembers(node.body.body, parameters);
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
- }
2
+ import { canonicalDrizzleTable } from "./no-duplicate-types/drizzle.js";
3
+ import { canonicalArkObject, canonicalValibotObject, canonicalZodObject } from "./no-duplicate-types/schemas.js";
4
+ import { canonicalDeclaration, canonicalTypeName, factoryCall, memberCall } from "./no-duplicate-types/shared.js";
5
+ const drizzleColumnTypes = {
6
+ text: "TSStringKeyword", varchar: "TSStringKeyword", uuid: "TSStringKeyword", char: "TSStringKeyword",
7
+ integer: "TSNumberKeyword", int: "TSNumberKeyword", serial: "TSNumberKeyword", smallint: "TSNumberKeyword",
8
+ real: "TSNumberKeyword", float: "TSNumberKeyword", doublePrecision: "TSNumberKeyword", boolean: "TSBooleanKeyword",
9
+ };
414
10
  /** Report exact structural duplicates without TypeScript assignability or fuzzy matching. */
415
11
  export const noDuplicateTypesRule = defineRule({
416
12
  meta: {
@@ -421,131 +17,116 @@ export const noDuplicateTypesRule = defineRule({
421
17
  },
422
18
  createOnce(context) {
423
19
  const seen = new Map();
20
+ const zodNamespaces = new Set(), valibotNamespaces = new Set(), arkTypeFunctions = new Set();
21
+ const valibotFunctions = new Map(), drizzleTables = new Set(), drizzleSchemaFactories = new Set(), drizzleSchemaObjects = new Set();
22
+ const drizzleBuilders = new Map(), drizzleModels = new Map();
23
+ const drizzleAliases = [];
24
+ let hasArkTypeImport = false;
424
25
  let options = {};
26
+ const minProperties = () => options.minProperties ?? 2;
27
+ const enabled = (library) => options.schemas?.libraries?.includes(library) === true;
425
28
  const report = (node, name, fingerprint, kind) => {
426
29
  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);
30
+ const key = isolates ? `${kind}:${fingerprint}` : fingerprint, first = seen.get(key);
429
31
  if (first === undefined) {
430
32
  seen.set(key, { node, name, filename: context.filename });
431
33
  return;
432
34
  }
433
- if (first.node === node)
434
- return;
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 } });
35
+ if (first.node !== node)
36
+ context.report({ node, messageId: "duplicate", data: { message: options.message ?? "This type is structurally identical to an existing declaration.", firstName: first.name, firstFile: first.filename } });
436
37
  };
437
- const inspect = (node) => {
438
- let properties;
439
- if (node.type === "TSTypeAliasDeclaration") {
440
- if (node.typeAnnotation.type !== "TSTypeLiteral")
441
- return;
442
- properties = node.typeAnnotation.members.length;
443
- }
444
- else {
445
- properties = node.body.body.length;
446
- }
38
+ const inspectType = (node) => {
39
+ const properties = node.type === "TSTypeAliasDeclaration" ? node.typeAnnotation.type === "TSTypeLiteral" ? node.typeAnnotation.members.length : 0 : node.body.body.length;
447
40
  const fingerprint = canonicalDeclaration(node);
448
- if (fingerprint === null)
449
- return;
450
- if (properties < (options.minProperties ?? 2))
451
- return;
452
- report(node, node.id.name, fingerprint, "type");
41
+ if (fingerprint !== null && properties >= minProperties())
42
+ report(node, node.id.name, fingerprint, "type");
453
43
  };
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
44
  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)
45
+ if (node.id.type !== "Identifier" || node.init === null)
477
46
  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");
47
+ const zod = memberCall(node.init);
48
+ const valibot = factoryCall(node.init, valibotNamespaces, valibotFunctions);
49
+ const arkCallee = node.init.type === "CallExpression" ? node.init.callee : null;
50
+ const schema = enabled("zod") && zod?.name === "object" && zod.arguments_.length === 1 && zod.object.type === "Identifier" && zodNamespaces.has(zod.object.name) ? canonicalZodObject(zod.arguments_[0], zodNamespaces)
51
+ : enabled("valibot") && valibot?.name === "object" && valibot.arguments_.length === 1 ? canonicalValibotObject(valibot.arguments_[0], valibotNamespaces, valibotFunctions)
52
+ : enabled("arktype") && node.init.type === "CallExpression" && typeof arkCallee?.name === "string" && (arkTypeFunctions.has(arkCallee.name) || hasArkTypeImport && arkCallee.name === "type") && node.init.arguments.length === 1 && node.init.arguments[0].type !== "SpreadElement" ? canonicalArkObject(node.init.arguments[0])
53
+ : null;
54
+ if (schema !== null && schema.properties >= minProperties())
55
+ report(node, node.id.name, schema.fingerprint, "schema");
491
56
  };
492
57
  const inspectDrizzle = (node) => {
493
58
  if (options.drizzle === undefined || node.id.type !== "Identifier" || node.init === null)
494
59
  return;
60
+ const models = new Map();
495
61
  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}`);
62
+ const table = canonicalDrizzleTable(node.init, drizzleTables, drizzleSchemaObjects, drizzleBuilders, model);
63
+ if (table === null || table.properties < minProperties())
64
+ continue;
65
+ models.set(model, table.fingerprint);
66
+ report(node, node.id.name, table.fingerprint, `drizzle-${model}`);
499
67
  }
68
+ if (models.size > 0)
69
+ drizzleModels.set(node.id.name, models);
500
70
  };
501
- return {
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
- }
71
+ const trackImport = (node) => {
72
+ if (node.source.value === "zod")
73
+ for (const specifier of node.specifiers)
74
+ if (specifier.type === "ImportNamespaceSpecifier" || specifier.type === "ImportDefaultSpecifier" || specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier" && specifier.imported.name === "z")
75
+ zodNamespaces.add(specifier.local.name);
76
+ if (node.source.value === "valibot")
77
+ for (const specifier of node.specifiers) {
78
+ if (specifier.type === "ImportNamespaceSpecifier")
79
+ valibotNamespaces.add(specifier.local.name);
80
+ if (specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier")
81
+ valibotFunctions.set(specifier.local.name, specifier.imported.name);
519
82
  }
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
- }
83
+ if (node.source.value === "arktype") {
84
+ hasArkTypeImport = true;
85
+ for (const specifier of node.specifiers) {
86
+ const imported = specifier.type === "ImportSpecifier" ? specifier.imported : null;
87
+ if (imported?.name === "type")
88
+ arkTypeFunctions.add(specifier.local.name);
527
89
  }
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
- },
541
- TSTypeAliasDeclaration: inspect,
542
- TSInterfaceDeclaration: inspect,
90
+ }
91
+ const source = /^drizzle-orm\/(pg|mysql|sqlite)-core$/.exec(String(node.source.value));
92
+ if (source === null)
93
+ return;
94
+ for (const specifier of node.specifiers) {
95
+ if (specifier.type !== "ImportSpecifier" || specifier.imported.type !== "Identifier")
96
+ continue;
97
+ const name = specifier.imported.name;
98
+ if (["pgTable", "mysqlTable", "sqliteTable"].includes(name))
99
+ drizzleTables.add(specifier.local.name);
100
+ if (name === "pgSchema")
101
+ drizzleSchemaFactories.add(specifier.local.name);
102
+ const type = source[1] === "sqlite" && name === "blob" ? "ref(Uint8Array<>)" : drizzleColumnTypes[name];
103
+ if (type !== undefined)
104
+ drizzleBuilders.set(specifier.local.name, { name, type });
105
+ }
106
+ };
107
+ return {
108
+ Program() { options = context.options[0] ?? {}; },
109
+ ImportDeclaration: trackImport,
110
+ TSTypeAliasDeclaration(node) { inspectType(node); drizzleAliases.push(node); },
111
+ TSInterfaceDeclaration: inspectType,
543
112
  VariableDeclarator(node) {
544
113
  inspectSchema(node);
545
- inspectValibot(node);
546
- inspectArkType(node);
114
+ if (node.id.type === "Identifier" && node.init?.type === "CallExpression" && node.init.callee.type === "Identifier" && drizzleSchemaFactories.has(node.init.callee.name))
115
+ drizzleSchemaObjects.add(node.id.name);
547
116
  inspectDrizzle(node);
548
117
  },
118
+ "Program:exit"() {
119
+ for (const node of drizzleAliases) {
120
+ if (options.drizzle === undefined || node.typeAnnotation.type !== "TSTypeQuery" || node.typeAnnotation.typeArguments !== null)
121
+ continue;
122
+ const name = canonicalTypeName(node.typeAnnotation.exprName), match = name === null ? null : /^(.*)\.\$infer(Select|Insert)$/.exec(name);
123
+ if (match === null)
124
+ continue;
125
+ const model = match[2] === "Select" ? "select" : "insert", fingerprint = drizzleModels.get(match[1])?.get(model);
126
+ if (fingerprint !== undefined)
127
+ report(node, node.id.name, fingerprint, `drizzle-${model}`);
128
+ }
129
+ },
549
130
  };
550
131
  },
551
132
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-deslop",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
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",