@zmdb/compiler 1.0.0-beta.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/LICENSE +674 -0
- package/README.md +40 -0
- package/dist/codegen/index.d.ts +50 -0
- package/dist/codegen/index.d.ts.map +1 -0
- package/dist/codegen/index.js +364 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/codegen/scan.d.ts +105 -0
- package/dist/codegen/scan.d.ts.map +1 -0
- package/dist/codegen/scan.js +450 -0
- package/dist/codegen/scan.js.map +1 -0
- package/dist/codegen/witness.d.ts +76 -0
- package/dist/codegen/witness.d.ts.map +1 -0
- package/dist/codegen/witness.js +690 -0
- package/dist/codegen/witness.js.map +1 -0
- package/dist/config/contract.d.ts +41 -0
- package/dist/config/contract.d.ts.map +1 -0
- package/dist/config/contract.js +5 -0
- package/dist/config/contract.js.map +1 -0
- package/dist/config/index.d.ts +53 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +277 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/index.zmdb.generated.d.ts +12 -0
- package/dist/config/index.zmdb.generated.js +28 -0
- package/dist/config/index.zmdb.witness.d.ts +3 -0
- package/dist/config/index.zmdb.witness.d.ts.map +1 -0
- package/dist/config/index.zmdb.witness.js +20 -0
- package/dist/config/index.zmdb.witness.js.map +1 -0
- package/dist/configured-plugin.d.ts +11 -0
- package/dist/configured-plugin.d.ts.map +1 -0
- package/dist/configured-plugin.js +27 -0
- package/dist/configured-plugin.js.map +1 -0
- package/dist/emit/index.d.ts +103 -0
- package/dist/emit/index.d.ts.map +1 -0
- package/dist/emit/index.js +1356 -0
- package/dist/emit/index.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +254 -0
- package/dist/index.js.map +1 -0
- package/dist/lint/ast.d.ts +17 -0
- package/dist/lint/ast.d.ts.map +1 -0
- package/dist/lint/ast.js +77 -0
- package/dist/lint/ast.js.map +1 -0
- package/dist/lint/host-types.d.ts +9 -0
- package/dist/lint/host-types.d.ts.map +1 -0
- package/dist/lint/host-types.js +2 -0
- package/dist/lint/host-types.js.map +1 -0
- package/dist/lint/index.d.ts +23 -0
- package/dist/lint/index.d.ts.map +1 -0
- package/dist/lint/index.js +53 -0
- package/dist/lint/index.js.map +1 -0
- package/dist/lint/rules/no-distributed-nullable-tags.d.ts +3 -0
- package/dist/lint/rules/no-distributed-nullable-tags.d.ts.map +1 -0
- package/dist/lint/rules/no-distributed-nullable-tags.js +71 -0
- package/dist/lint/rules/no-distributed-nullable-tags.js.map +1 -0
- package/dist/lint/rules/no-empty-patch.d.ts +3 -0
- package/dist/lint/rules/no-empty-patch.d.ts.map +1 -0
- package/dist/lint/rules/no-empty-patch.js +26 -0
- package/dist/lint/rules/no-empty-patch.js.map +1 -0
- package/dist/lint/rules/no-interpolated-sql.d.ts +3 -0
- package/dist/lint/rules/no-interpolated-sql.d.ts.map +1 -0
- package/dist/lint/rules/no-interpolated-sql.js +35 -0
- package/dist/lint/rules/no-interpolated-sql.js.map +1 -0
- package/dist/lint/rules/no-unbounded-find.d.ts +3 -0
- package/dist/lint/rules/no-unbounded-find.d.ts.map +1 -0
- package/dist/lint/rules/no-unbounded-find.js +26 -0
- package/dist/lint/rules/no-unbounded-find.js.map +1 -0
- package/dist/lint/rules/no-unknown-json-column.d.ts +3 -0
- package/dist/lint/rules/no-unknown-json-column.d.ts.map +1 -0
- package/dist/lint/rules/no-unknown-json-column.js +35 -0
- package/dist/lint/rules/no-unknown-json-column.js.map +1 -0
- package/dist/lint/rules/require-sql-on-number.d.ts +3 -0
- package/dist/lint/rules/require-sql-on-number.d.ts.map +1 -0
- package/dist/lint/rules/require-sql-on-number.js +34 -0
- package/dist/lint/rules/require-sql-on-number.js.map +1 -0
- package/dist/lint/types.d.ts +12 -0
- package/dist/lint/types.d.ts.map +1 -0
- package/dist/lint/types.js +2 -0
- package/dist/lint/types.js.map +1 -0
- package/dist/metro/metro.d.ts +22 -0
- package/dist/metro/metro.d.ts.map +1 -0
- package/dist/metro/metro.js +242 -0
- package/dist/metro/metro.js.map +1 -0
- package/dist/protobuf/decode.d.ts +20 -0
- package/dist/protobuf/decode.d.ts.map +1 -0
- package/dist/protobuf/decode.js +289 -0
- package/dist/protobuf/decode.js.map +1 -0
- package/dist/protobuf/descriptor.d.ts +16 -0
- package/dist/protobuf/descriptor.d.ts.map +1 -0
- package/dist/protobuf/descriptor.js +303 -0
- package/dist/protobuf/descriptor.js.map +1 -0
- package/dist/protobuf/encode.d.ts +20 -0
- package/dist/protobuf/encode.d.ts.map +1 -0
- package/dist/protobuf/encode.js +203 -0
- package/dist/protobuf/encode.js.map +1 -0
- package/dist/protobuf/grpc-ir.d.ts +16 -0
- package/dist/protobuf/grpc-ir.d.ts.map +1 -0
- package/dist/protobuf/grpc-ir.js +2 -0
- package/dist/protobuf/grpc-ir.js.map +1 -0
- package/dist/protobuf/plan.d.ts +42 -0
- package/dist/protobuf/plan.d.ts.map +1 -0
- package/dist/protobuf/plan.js +158 -0
- package/dist/protobuf/plan.js.map +1 -0
- package/dist/reflect/callsites.d.ts +40 -0
- package/dist/reflect/callsites.d.ts.map +1 -0
- package/dist/reflect/callsites.js +153 -0
- package/dist/reflect/callsites.js.map +1 -0
- package/dist/reflect/index.d.ts +106 -0
- package/dist/reflect/index.d.ts.map +1 -0
- package/dist/reflect/index.js +1421 -0
- package/dist/reflect/index.js.map +1 -0
- package/dist/reflect/session.d.ts +118 -0
- package/dist/reflect/session.d.ts.map +1 -0
- package/dist/reflect/session.js +228 -0
- package/dist/reflect/session.js.map +1 -0
- package/dist/testing/index.d.ts +69 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +210 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/transform/index.d.ts +74 -0
- package/dist/transform/index.d.ts.map +1 -0
- package/dist/transform/index.js +572 -0
- package/dist/transform/index.js.map +1 -0
- package/dist/unplugin/index.d.ts +53 -0
- package/dist/unplugin/index.d.ts.map +1 -0
- package/dist/unplugin/index.js +130 -0
- package/dist/unplugin/index.js.map +1 -0
- package/dist/unplugin/inline-bench.d.ts +29 -0
- package/dist/unplugin/inline-bench.d.ts.map +1 -0
- package/dist/unplugin/inline-bench.js +82 -0
- package/dist/unplugin/inline-bench.js.map +1 -0
- package/package.json +110 -0
- package/src/codegen/index.ts +451 -0
- package/src/codegen/scan.ts +574 -0
- package/src/codegen/witness.ts +819 -0
- package/src/config/contract.ts +48 -0
- package/src/config/index.ts +384 -0
- package/src/config/index.zmdb.generated.d.ts +12 -0
- package/src/config/index.zmdb.generated.js +28 -0
- package/src/config/index.zmdb.witness.ts +22 -0
- package/src/configured-plugin.ts +35 -0
- package/src/emit/index.ts +1496 -0
- package/src/errors.ts +6 -0
- package/src/index.ts +336 -0
- package/src/lint/ast.ts +96 -0
- package/src/lint/host-types.ts +9 -0
- package/src/lint/index.ts +80 -0
- package/src/lint/rules/no-distributed-nullable-tags.ts +76 -0
- package/src/lint/rules/no-empty-patch.ts +26 -0
- package/src/lint/rules/no-interpolated-sql.ts +45 -0
- package/src/lint/rules/no-unbounded-find.ts +26 -0
- package/src/lint/rules/no-unknown-json-column.ts +36 -0
- package/src/lint/rules/require-sql-on-number.ts +34 -0
- package/src/lint/types.ts +11 -0
- package/src/metro/metro.ts +313 -0
- package/src/protobuf/decode.ts +375 -0
- package/src/protobuf/descriptor.ts +353 -0
- package/src/protobuf/encode.ts +277 -0
- package/src/protobuf/grpc-ir.ts +17 -0
- package/src/protobuf/plan.ts +244 -0
- package/src/reflect/callsites.ts +198 -0
- package/src/reflect/index.ts +1723 -0
- package/src/reflect/session.ts +276 -0
- package/src/testing/index.ts +298 -0
- package/src/transform/index.ts +690 -0
- package/src/unplugin/index.ts +182 -0
- package/src/unplugin/inline-bench.ts +92 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { type ObjectIR, type ProtoScalar, type ScalarIR, type TypeIR } from '@zmdb/schema/ir';
|
|
2
|
+
|
|
3
|
+
type NumericMethod =
|
|
4
|
+
| 'uint32'
|
|
5
|
+
| 'int32'
|
|
6
|
+
| 'sint32'
|
|
7
|
+
| 'uint64'
|
|
8
|
+
| 'int64'
|
|
9
|
+
| 'sint64'
|
|
10
|
+
| 'fixed32'
|
|
11
|
+
| 'sfixed32'
|
|
12
|
+
| 'fixed64'
|
|
13
|
+
| 'sfixed64'
|
|
14
|
+
| 'float'
|
|
15
|
+
| 'double';
|
|
16
|
+
|
|
17
|
+
export type ScalarZero = 'number' | 'bigint' | 'boolean' | 'string';
|
|
18
|
+
|
|
19
|
+
export interface ScalarPlan<Zero extends string = ScalarZero> {
|
|
20
|
+
readonly kind: 'scalar';
|
|
21
|
+
readonly method: NumericMethod | 'bool' | 'string';
|
|
22
|
+
readonly wire: 0 | 1 | 2 | 5;
|
|
23
|
+
readonly zero: Zero;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface EnumPlan {
|
|
27
|
+
readonly kind: 'enum';
|
|
28
|
+
readonly helper: string;
|
|
29
|
+
readonly wire: 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface MessagePlan {
|
|
33
|
+
readonly kind: 'message';
|
|
34
|
+
readonly helper: string;
|
|
35
|
+
readonly wire: 2;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TimestampPlan {
|
|
39
|
+
readonly kind: 'timestamp';
|
|
40
|
+
readonly helper: string;
|
|
41
|
+
readonly wire: 2;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type AtomPlan<Zero extends string = ScalarZero> = ScalarPlan<Zero> | EnumPlan | MessagePlan | TimestampPlan;
|
|
45
|
+
|
|
46
|
+
export interface FieldPlan<Atom> {
|
|
47
|
+
readonly atom: Atom;
|
|
48
|
+
readonly repeated: boolean;
|
|
49
|
+
readonly nullable: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
53
|
+
const THIRTY_TWO_BIT = new Set<ProtoScalar>(['int32', 'uint32', 'sint32', 'fixed32', 'sfixed32']);
|
|
54
|
+
const SIXTY_FOUR_BIT = new Set<ProtoScalar>(['int64', 'uint64', 'sint64', 'fixed64', 'sfixed64']);
|
|
55
|
+
const FLOATING = new Set<ProtoScalar>(['float', 'double']);
|
|
56
|
+
|
|
57
|
+
type Refuse = (path: string, reason: string, source?: string) => undefined;
|
|
58
|
+
|
|
59
|
+
export type FieldAtomIR = Extract<TypeIR, { kind: 'scalar' | 'object' | 'ref' }>;
|
|
60
|
+
|
|
61
|
+
export function collectMessages(
|
|
62
|
+
node: TypeIR,
|
|
63
|
+
suggested: string,
|
|
64
|
+
register: (node: ObjectIR, suggested: string) => boolean,
|
|
65
|
+
): void {
|
|
66
|
+
switch (node.kind) {
|
|
67
|
+
case 'object':
|
|
68
|
+
if (!register(node, suggested)) return;
|
|
69
|
+
for (const property of node.properties) {
|
|
70
|
+
collectMessages(property.type, `${suggested}${safeName(property.name)}`, register);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
case 'array':
|
|
74
|
+
collectMessages(node.element, suggested, register);
|
|
75
|
+
return;
|
|
76
|
+
case 'tuple':
|
|
77
|
+
for (const [index, element] of node.elements.entries()) {
|
|
78
|
+
collectMessages(element, `${suggested}${index + 1}`, register);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
case 'union':
|
|
82
|
+
for (const member of node.members) collectMessages(member, suggested, register);
|
|
83
|
+
return;
|
|
84
|
+
default:
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function planField<Atom>(
|
|
90
|
+
node: TypeIR,
|
|
91
|
+
path: string,
|
|
92
|
+
atomPlan: (node: FieldAtomIR, path: string) => Atom | undefined,
|
|
93
|
+
enumeration: (path: string, values: readonly string[]) => Atom,
|
|
94
|
+
refuse: Refuse,
|
|
95
|
+
): FieldPlan<Atom> | undefined {
|
|
96
|
+
if (node.kind === 'array') {
|
|
97
|
+
if (node.element.kind === 'array') {
|
|
98
|
+
return refuse(
|
|
99
|
+
path,
|
|
100
|
+
'a nested array would require `repeated repeated`, which proto3 cannot spell without an explicit wrapper message',
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const element = planField(node.element, `${path}[]`, atomPlan, enumeration, refuse);
|
|
104
|
+
if (element === undefined) return undefined;
|
|
105
|
+
if (element.repeated || element.nullable) {
|
|
106
|
+
return refuse(path, 'a repeated protobuf element cannot itself be repeated or nullable');
|
|
107
|
+
}
|
|
108
|
+
return { atom: element.atom, repeated: true, nullable: false };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (node.kind === 'union') {
|
|
112
|
+
const values = node.members.filter(member => member.kind !== 'null' && member.kind !== 'undefined');
|
|
113
|
+
const nullable = node.members.some(member => member.kind === 'null');
|
|
114
|
+
const literals = values.filter(member => member.kind === 'literal' && typeof member.value === 'string');
|
|
115
|
+
if (literals.length === values.length && literals.length > 0) {
|
|
116
|
+
const atom = enumeration(
|
|
117
|
+
path,
|
|
118
|
+
literals.map(member => (member.kind === 'literal' && typeof member.value === 'string' ? member.value : '')),
|
|
119
|
+
);
|
|
120
|
+
return { atom, repeated: false, nullable };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const [only] = values;
|
|
124
|
+
if (values.length === 1 && only !== undefined) {
|
|
125
|
+
const resolved = planField(only, path, atomPlan, enumeration, refuse);
|
|
126
|
+
return resolved === undefined ? undefined : { ...resolved, nullable: resolved.nullable || nullable };
|
|
127
|
+
}
|
|
128
|
+
if (values.some(member => member.kind === 'object')) {
|
|
129
|
+
return refuse(
|
|
130
|
+
path,
|
|
131
|
+
'a union of message types would require `oneof`, but union arms have no ProtoField<N> tag slot',
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return refuse(path, 'this TypeScript union has no single protobuf field spelling');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
switch (node.kind) {
|
|
138
|
+
case 'scalar':
|
|
139
|
+
case 'object':
|
|
140
|
+
case 'ref': {
|
|
141
|
+
const atom = atomPlan(node, path);
|
|
142
|
+
return atom === undefined ? undefined : { atom, repeated: false, nullable: false };
|
|
143
|
+
}
|
|
144
|
+
case 'literal':
|
|
145
|
+
return typeof node.value === 'string'
|
|
146
|
+
? { atom: enumeration(path, [node.value]), repeated: false, nullable: false }
|
|
147
|
+
: refuse(path, 'a numeric or boolean literal has no protobuf wire constraint');
|
|
148
|
+
case 'tuple':
|
|
149
|
+
return refuse(path, 'a tuple has no protobuf field spelling; declare a numbered wrapper message');
|
|
150
|
+
case 'unknown':
|
|
151
|
+
return refuse(path, '`unknown` has no protobuf wire type');
|
|
152
|
+
case 'null':
|
|
153
|
+
case 'undefined':
|
|
154
|
+
return refuse(path, 'a protobuf field cannot contain only null or undefined');
|
|
155
|
+
case 'unsupported':
|
|
156
|
+
return refuse(path, node.reason, node.source);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function planScalar(
|
|
161
|
+
node: ScalarIR,
|
|
162
|
+
path: string,
|
|
163
|
+
timestamp: () => string,
|
|
164
|
+
refuse: Refuse,
|
|
165
|
+
): ScalarPlan | TimestampPlan | undefined {
|
|
166
|
+
const proto = node.proto;
|
|
167
|
+
switch (node.scalar) {
|
|
168
|
+
case 'number':
|
|
169
|
+
case 'integer':
|
|
170
|
+
if (proto === undefined) return scalar('double', 1, 'number');
|
|
171
|
+
if (THIRTY_TWO_BIT.has(proto) || FLOATING.has(proto)) return numeric(proto);
|
|
172
|
+
if (SIXTY_FOUR_BIT.has(proto)) {
|
|
173
|
+
return refuse(
|
|
174
|
+
path,
|
|
175
|
+
`Proto<'${proto}'> needs bigint because a TypeScript number cannot preserve every 64-bit integer`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return refuse(path, `Proto<'${proto}'> is not a numeric protobuf scalar`);
|
|
179
|
+
case 'bigint':
|
|
180
|
+
if (proto === undefined) {
|
|
181
|
+
return refuse(path, 'an untagged bigint has no inferable protobuf width or signedness; add Proto<K>');
|
|
182
|
+
}
|
|
183
|
+
return SIXTY_FOUR_BIT.has(proto)
|
|
184
|
+
? numeric(proto)
|
|
185
|
+
: refuse(path, `a bigint protobuf field needs an explicit 64-bit scalar, not Proto<'${proto}'>`);
|
|
186
|
+
case 'boolean':
|
|
187
|
+
if (proto === undefined || proto === 'bool') return scalar('bool', 0, 'boolean');
|
|
188
|
+
return refuse(path, `a boolean protobuf field cannot use Proto<'${proto}'>`);
|
|
189
|
+
case 'string':
|
|
190
|
+
if (proto === undefined || proto === 'string') return scalar('string', 2, 'string');
|
|
191
|
+
if (proto === 'bytes') {
|
|
192
|
+
return refuse(
|
|
193
|
+
path,
|
|
194
|
+
"Proto<'bytes'> needs Uint8Array, and the current reflection refuses typed-array data types",
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return refuse(path, `a string protobuf field cannot use Proto<'${proto}'>`);
|
|
198
|
+
case 'date':
|
|
199
|
+
if (proto !== undefined) {
|
|
200
|
+
return refuse(path, `Date has the fixed google.protobuf.Timestamp mapping, not Proto<'${proto}'>`);
|
|
201
|
+
}
|
|
202
|
+
return { kind: 'timestamp', helper: timestamp(), wire: 2 };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function numeric(method: ProtoScalar): ScalarPlan {
|
|
207
|
+
switch (method) {
|
|
208
|
+
case 'int32':
|
|
209
|
+
case 'uint32':
|
|
210
|
+
case 'sint32':
|
|
211
|
+
return scalar(method, 0, 'number');
|
|
212
|
+
case 'int64':
|
|
213
|
+
case 'uint64':
|
|
214
|
+
case 'sint64':
|
|
215
|
+
return scalar(method, 0, 'bigint');
|
|
216
|
+
case 'fixed32':
|
|
217
|
+
case 'sfixed32':
|
|
218
|
+
case 'float':
|
|
219
|
+
return scalar(method, 5, 'number');
|
|
220
|
+
case 'fixed64':
|
|
221
|
+
case 'sfixed64':
|
|
222
|
+
return scalar(method, 1, 'bigint');
|
|
223
|
+
case 'double':
|
|
224
|
+
return scalar(method, 1, 'number');
|
|
225
|
+
case 'bool':
|
|
226
|
+
case 'string':
|
|
227
|
+
case 'bytes':
|
|
228
|
+
throw new Error(`non-numeric protobuf scalar ${method}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function scalar(method: ScalarPlan['method'], wire: ScalarPlan['wire'], zero: ScalarPlan['zero']): ScalarPlan {
|
|
233
|
+
return { kind: 'scalar', method, wire, zero };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function isPacked<Zero extends string>(atom: AtomPlan<Zero>): atom is ScalarPlan<Zero> | EnumPlan {
|
|
237
|
+
return atom.kind === 'enum' || (atom.kind === 'scalar' && atom.method !== 'string');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function safeName(raw: string): string {
|
|
241
|
+
const words = raw.split(/[^A-Za-z0-9_$]+/).filter(word => word.length > 0);
|
|
242
|
+
const joined = words.map(word => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`).join('');
|
|
243
|
+
return joined.length === 0 ? 'Message' : joined;
|
|
244
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// Finding the call sites the transformer replaces.
|
|
2
|
+
//
|
|
3
|
+
// The generic `findCallSites` helper remains text-based because reflection fixtures use
|
|
4
|
+
// local probe functions. Production transformation uses `findOwnedCallSites` instead.
|
|
5
|
+
// The five protobuf/gRPC calls resolve strictly to exports of `@zmdb/protobuf`, so a
|
|
6
|
+
// local shadow, foreign same-named export, or old forwarding package is not a target.
|
|
7
|
+
// Existing validator/schema calls retain their historical text-based forwarding support.
|
|
8
|
+
|
|
9
|
+
import type { CallExpression, Node, SourceFile, TypeNode } from 'typescript/unstable/ast';
|
|
10
|
+
import {
|
|
11
|
+
isCallExpression,
|
|
12
|
+
isIdentifier,
|
|
13
|
+
isImportDeclaration,
|
|
14
|
+
isPropertyAccessExpression,
|
|
15
|
+
isStringLiteral,
|
|
16
|
+
} from 'typescript/unstable/ast/is';
|
|
17
|
+
import { SymbolFlags, type Checker, type Symbol as TsSymbol } from 'typescript/unstable/sync';
|
|
18
|
+
|
|
19
|
+
export const CALL_OWNERS: Readonly<Record<string, readonly string[]>> = Object.freeze({
|
|
20
|
+
is: ['@zmdb/validator', 'zmdb', 'zmdb/validator'],
|
|
21
|
+
isShallow: ['@zmdb/validator', 'zmdb/validator'],
|
|
22
|
+
assert: ['@zmdb/validator', 'zmdb', 'zmdb/validator'],
|
|
23
|
+
assertShallow: ['@zmdb/validator', 'zmdb/validator'],
|
|
24
|
+
equals: ['@zmdb/validator', 'zmdb/validator'],
|
|
25
|
+
assertEquals: ['@zmdb/validator', 'zmdb/validator'],
|
|
26
|
+
validate: ['@zmdb/validator', 'zmdb', 'zmdb/validator'],
|
|
27
|
+
validateShallow: ['@zmdb/validator', 'zmdb/validator'],
|
|
28
|
+
random: ['@zmdb/validator', 'zmdb/validator'],
|
|
29
|
+
toJsonSchema: ['@zmdb/schema/openapi', 'zmdb/schema'],
|
|
30
|
+
schemaOf: ['@zmdb/schema', 'zmdb', 'zmdb/schema'],
|
|
31
|
+
toolFor: ['@zmdb/ai'],
|
|
32
|
+
grpcDescriptor: ['@zmdb/protobuf'],
|
|
33
|
+
loadGrpcService: ['@zmdb/protobuf'],
|
|
34
|
+
protoDescriptor: ['@zmdb/protobuf'],
|
|
35
|
+
protoDecode: ['@zmdb/protobuf'],
|
|
36
|
+
protoEncode: ['@zmdb/protobuf'],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const OWNED_CALLEES: ReadonlySet<string> = new Set(Object.keys(CALL_OWNERS));
|
|
40
|
+
export const STRICT_OWNER_CALLEES: ReadonlySet<string> = new Set([
|
|
41
|
+
'grpcDescriptor',
|
|
42
|
+
'loadGrpcService',
|
|
43
|
+
'protoDescriptor',
|
|
44
|
+
'protoDecode',
|
|
45
|
+
'protoEncode',
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
export interface CallSite {
|
|
49
|
+
/** The canonical exported name, even when the local import is aliased. */
|
|
50
|
+
readonly callee: string;
|
|
51
|
+
/** The local named-import binding, or the namespace binding for `ns.callee`. */
|
|
52
|
+
readonly binding: string;
|
|
53
|
+
/** The owner module whose export resolved to this call. */
|
|
54
|
+
readonly specifier?: string;
|
|
55
|
+
/** The single type argument. A call with none is not a call site. */
|
|
56
|
+
readonly typeArgument: TypeNode;
|
|
57
|
+
readonly node: CallExpression;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `is(...)` → `is`; `v.is(...)` → `is`; anything else → `undefined`. */
|
|
61
|
+
function calleeName(call: CallExpression): { readonly callee: string; readonly binding: string } | undefined {
|
|
62
|
+
const target = call.expression;
|
|
63
|
+
if (isIdentifier(target)) return { callee: target.text, binding: target.text };
|
|
64
|
+
if (isPropertyAccessExpression(target) && isIdentifier(target.name) && isIdentifier(target.expression)) {
|
|
65
|
+
return { callee: target.name.text, binding: target.expression.text };
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Every call to one of `callees` that carries a type argument, in source order.
|
|
72
|
+
*
|
|
73
|
+
* Source order matters downstream: the transformer rewrites by text offset, and
|
|
74
|
+
* rewriting back-to-front is what keeps earlier offsets valid.
|
|
75
|
+
*/
|
|
76
|
+
export function findCallSites(sourceFile: SourceFile, callees: ReadonlySet<string>): readonly CallSite[] {
|
|
77
|
+
const found: CallSite[] = [];
|
|
78
|
+
|
|
79
|
+
// `forEachChild` is a method on the node, not a free function: it is not among the
|
|
80
|
+
// exports of `typescript/unstable/ast`, which only offers the *visitor* (rewriting)
|
|
81
|
+
// half of the pair.
|
|
82
|
+
const visit = (node: Node): undefined => {
|
|
83
|
+
if (isCallExpression(node)) {
|
|
84
|
+
const typeArgument = node.typeArguments?.[0];
|
|
85
|
+
const named = calleeName(node);
|
|
86
|
+
if (typeArgument && named !== undefined && callees.has(named.callee)) {
|
|
87
|
+
found.push({ ...named, typeArgument, node });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
node.forEachChild(visit);
|
|
91
|
+
return undefined;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
sourceFile.forEachChild(visit);
|
|
95
|
+
return found;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolved(checker: Checker, symbol: TsSymbol | undefined): TsSymbol | undefined {
|
|
99
|
+
if (symbol === undefined) return undefined;
|
|
100
|
+
return (symbol.flags & SymbolFlags.Alias) === 0 ? symbol : checker.getAliasedSymbol(symbol);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function importedExports(
|
|
104
|
+
sourceFile: SourceFile,
|
|
105
|
+
checker: Checker,
|
|
106
|
+
callees: ReadonlySet<string>,
|
|
107
|
+
owners: Readonly<Record<string, readonly string[]>>,
|
|
108
|
+
): ReadonlyMap<number, { readonly callee: string; readonly specifier: string }> {
|
|
109
|
+
const bySymbol = new Map<number, { readonly callee: string; readonly specifier: string }>();
|
|
110
|
+
|
|
111
|
+
for (const statement of sourceFile.statements) {
|
|
112
|
+
if (!isImportDeclaration(statement) || !isStringLiteral(statement.moduleSpecifier)) continue;
|
|
113
|
+
const specifier = statement.moduleSpecifier.text;
|
|
114
|
+
const candidates = [...callees].filter(callee => owners[callee]?.includes(specifier) === true);
|
|
115
|
+
if (candidates.length === 0) continue;
|
|
116
|
+
|
|
117
|
+
const module = checker.getSymbolAtLocation(statement.moduleSpecifier);
|
|
118
|
+
if (module === undefined) continue;
|
|
119
|
+
const exports = new Map(checker.getExportsOfModule(module).map(symbol => [symbol.name, symbol]));
|
|
120
|
+
for (const callee of candidates) {
|
|
121
|
+
const symbol = resolved(checker, exports.get(callee));
|
|
122
|
+
if (symbol !== undefined) bySymbol.set(symbol.id, { callee, specifier });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return bySymbol;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function callTarget(call: CallExpression): { readonly location: Node; readonly binding: string } | undefined {
|
|
130
|
+
const target = call.expression;
|
|
131
|
+
if (isIdentifier(target)) return { location: target, binding: target.text };
|
|
132
|
+
if (isPropertyAccessExpression(target) && isIdentifier(target.name) && isIdentifier(target.expression)) {
|
|
133
|
+
return { location: target.name, binding: target.expression.text };
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface OwnedCallOptions {
|
|
139
|
+
/**
|
|
140
|
+
* Test-only escape for synthetic global declarations. Production callers leave
|
|
141
|
+
* this false; strict-owner calls then require a matching `CALL_OWNERS` import.
|
|
142
|
+
*/
|
|
143
|
+
readonly allowUnbound?: boolean;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Calls whose resolved symbol is an allowed package export, plus legacy
|
|
148
|
+
* text-matched validator/schema calls that intentionally retain forwarding support.
|
|
149
|
+
*
|
|
150
|
+
* The canonical export table is built only from owner modules the file actually
|
|
151
|
+
* imports. Strict-owner calls therefore cannot match a foreign same-named function
|
|
152
|
+
* by structural type or text.
|
|
153
|
+
*/
|
|
154
|
+
export function findOwnedCallSites(
|
|
155
|
+
sourceFile: SourceFile,
|
|
156
|
+
checker: Checker,
|
|
157
|
+
callees: ReadonlySet<string>,
|
|
158
|
+
owners: Readonly<Record<string, readonly string[]>>,
|
|
159
|
+
options: OwnedCallOptions = {},
|
|
160
|
+
): readonly CallSite[] {
|
|
161
|
+
if (options.allowUnbound === true) return findCallSites(sourceFile, callees);
|
|
162
|
+
|
|
163
|
+
const canonical = importedExports(sourceFile, checker, callees, owners);
|
|
164
|
+
const found: CallSite[] = [];
|
|
165
|
+
|
|
166
|
+
const visit = (node: Node): undefined => {
|
|
167
|
+
if (isCallExpression(node)) {
|
|
168
|
+
const typeArgument = node.typeArguments?.[0];
|
|
169
|
+
const target = callTarget(node);
|
|
170
|
+
if (typeArgument !== undefined && target !== undefined) {
|
|
171
|
+
const symbol = resolved(checker, checker.getSymbolAtLocation(target.location));
|
|
172
|
+
const owned = symbol === undefined ? undefined : canonical.get(symbol.id);
|
|
173
|
+
if (owned !== undefined) {
|
|
174
|
+
found.push({
|
|
175
|
+
callee: owned.callee,
|
|
176
|
+
binding: target.binding,
|
|
177
|
+
specifier: owned.specifier,
|
|
178
|
+
typeArgument,
|
|
179
|
+
node,
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
const named = calleeName(node);
|
|
183
|
+
if (named !== undefined && callees.has(named.callee) && !STRICT_OWNER_CALLEES.has(named.callee)) {
|
|
184
|
+
// Validator/schema calls retain their established forwarding support.
|
|
185
|
+
// The new protobuf boundary is deliberately stricter because those old
|
|
186
|
+
// owners are removed rather than kept as compatibility aliases.
|
|
187
|
+
found.push({ ...named, typeArgument, node });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
node.forEachChild(visit);
|
|
193
|
+
return undefined;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
sourceFile.forEachChild(visit);
|
|
197
|
+
return found;
|
|
198
|
+
}
|