@typespec/protobuf 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/.c8rc.json +3 -0
  2. package/.eslintrc.cjs +7 -0
  3. package/.mocharc.yaml +4 -0
  4. package/.rush/temp/operation/build/state.json +3 -0
  5. package/.rush/temp/operation/test-official/state.json +3 -0
  6. package/.rush/temp/package-deps_build.json +81 -0
  7. package/.rush/temp/package-deps_test-official.json +81 -0
  8. package/.rush/temp/shrinkwrap-deps.json +177 -0
  9. package/CHANGELOG.json +4 -0
  10. package/LICENSE +21 -0
  11. package/README.md +30 -0
  12. package/coverage/cobertura-coverage.xml +2291 -0
  13. package/coverage/coverage-final.json +7 -0
  14. package/coverage/tmp/coverage-6408-1683144315012-0.json +1 -0
  15. package/dist/src/ast.d.ts +199 -0
  16. package/dist/src/ast.d.ts.map +1 -0
  17. package/dist/src/ast.js +60 -0
  18. package/dist/src/ast.js.map +1 -0
  19. package/dist/src/index.d.ts +62 -0
  20. package/dist/src/index.d.ts.map +1 -0
  21. package/dist/src/index.js +5 -0
  22. package/dist/src/index.js.map +1 -0
  23. package/dist/src/lib.d.ts +197 -0
  24. package/dist/src/lib.d.ts.map +1 -0
  25. package/dist/src/lib.js +134 -0
  26. package/dist/src/lib.js.map +1 -0
  27. package/dist/src/proto.d.ts +62 -0
  28. package/dist/src/proto.d.ts.map +1 -0
  29. package/dist/src/proto.js +162 -0
  30. package/dist/src/proto.js.map +1 -0
  31. package/dist/src/transform/index.d.ts +7 -0
  32. package/dist/src/transform/index.d.ts.map +1 -0
  33. package/dist/src/transform/index.js +744 -0
  34. package/dist/src/transform/index.js.map +1 -0
  35. package/dist/src/write.d.ts +12 -0
  36. package/dist/src/write.d.ts.map +1 -0
  37. package/dist/src/write.js +204 -0
  38. package/dist/src/write.js.map +1 -0
  39. package/dist/test/scenarios.test.d.ts +2 -0
  40. package/dist/test/scenarios.test.d.ts.map +1 -0
  41. package/dist/test/scenarios.test.js +165 -0
  42. package/dist/test/scenarios.test.js.map +1 -0
  43. package/lib/proto.tsp +303 -0
  44. package/package.json +49 -0
  45. package/protobuf.build.log +48 -0
  46. package/src/ast.ts +279 -0
  47. package/src/index.ts +7 -0
  48. package/src/lib.ts +161 -0
  49. package/src/proto.ts +217 -0
  50. package/src/transform/index.ts +980 -0
  51. package/src/write.ts +244 -0
  52. package/temp/tsconfig.tsbuildinfo +1 -0
  53. package/test/include/foo/bar.proto +7 -0
  54. package/test/scenarios/addressbook/input/addressbook.tsp +27 -0
  55. package/test/scenarios/addressbook/input/main.tsp +13 -0
  56. package/test/scenarios/addressbook/output/@typespec/protobuf/addressbook.proto +26 -0
  57. package/test/scenarios/addressbook/output/@typespec/protobuf/main.proto +14 -0
  58. package/test/scenarios/anonymous-model/diagnostics.txt +1 -0
  59. package/test/scenarios/anonymous-model/input/main.tsp +24 -0
  60. package/test/scenarios/anonymous-package/input/main.tsp +19 -0
  61. package/test/scenarios/anonymous-package/output/@typespec/protobuf/main.proto +15 -0
  62. package/test/scenarios/array/input/main.tsp +22 -0
  63. package/test/scenarios/array/output/@typespec/protobuf/com/azure/test.proto +18 -0
  64. package/test/scenarios/array-nested/diagnostics.txt +1 -0
  65. package/test/scenarios/array-nested/input/main.tsp +20 -0
  66. package/test/scenarios/cross package references/input/main.tsp +27 -0
  67. package/test/scenarios/cross package references/output/@typespec/protobuf/A.proto +10 -0
  68. package/test/scenarios/cross package references/output/@typespec/protobuf/B.proto +15 -0
  69. package/test/scenarios/derived-scalar/input/main.tsp +24 -0
  70. package/test/scenarios/derived-scalar/output/@typespec/protobuf/com/azure/Test.proto +18 -0
  71. package/test/scenarios/enum/input/main.tsp +33 -0
  72. package/test/scenarios/enum/output/@typespec/protobuf/main.proto +31 -0
  73. package/test/scenarios/enum-nonintegral/diagnostics.txt +4 -0
  74. package/test/scenarios/enum-nonintegral/input/main.tsp +31 -0
  75. package/test/scenarios/enum-nozero/diagnostics.txt +1 -0
  76. package/test/scenarios/enum-nozero/input/main.tsp +25 -0
  77. package/test/scenarios/extern/input/main.tsp +17 -0
  78. package/test/scenarios/extern/output/@typespec/protobuf/main.proto +19 -0
  79. package/test/scenarios/illegal field reservations/diagnostics.txt +2 -0
  80. package/test/scenarios/illegal field reservations/input/main.tsp +16 -0
  81. package/test/scenarios/inferred-message-names/input/main.tsp +16 -0
  82. package/test/scenarios/inferred-message-names/output/@typespec/protobuf/com/azure/test.proto +18 -0
  83. package/test/scenarios/intrinsics/input/main.tsp +17 -0
  84. package/test/scenarios/intrinsics/output/@typespec/protobuf/com/azure/Test.proto +16 -0
  85. package/test/scenarios/map/input/main.tsp +15 -0
  86. package/test/scenarios/map/output/@typespec/protobuf/main.proto +13 -0
  87. package/test/scenarios/model-no-package/diagnostics.txt +2 -0
  88. package/test/scenarios/model-no-package/input/main.tsp +19 -0
  89. package/test/scenarios/name-collision/input/main.tsp +24 -0
  90. package/test/scenarios/name-collision/output/@typespec/protobuf/main.proto +24 -0
  91. package/test/scenarios/options/input/main.tsp +25 -0
  92. package/test/scenarios/options/output/@typespec/protobuf/com/azure/Test.proto +20 -0
  93. package/test/scenarios/options-invalid/diagnostics.txt +1 -0
  94. package/test/scenarios/options-invalid/input/main.tsp +25 -0
  95. package/test/scenarios/reserved field collisions/diagnostics.txt +5 -0
  96. package/test/scenarios/reserved field collisions/input/main.tsp +19 -0
  97. package/test/scenarios/reserved fields/input/main.tsp +16 -0
  98. package/test/scenarios/reserved fields/output/@typespec/protobuf/main.proto +16 -0
  99. package/test/scenarios/simple/input/main.tsp +22 -0
  100. package/test/scenarios/simple/output/@typespec/protobuf/com/azure/Test.proto +18 -0
  101. package/test/scenarios/simple-error/diagnostics.txt +6 -0
  102. package/test/scenarios/simple-error/input/main.tsp +22 -0
  103. package/test/scenarios/simple-no-service/input/main.tsp +22 -0
  104. package/test/scenarios/simple-no-service/output/@typespec/protobuf/com/azure/Test.proto +18 -0
  105. package/test/scenarios/streams/input/main.tsp +30 -0
  106. package/test/scenarios/streams/output/@typespec/protobuf/main.proto +19 -0
  107. package/test/scenarios/type-validation/diagnostics.txt +1 -0
  108. package/test/scenarios/type-validation/input/main.tsp +11 -0
  109. package/test/scenarios/union/diagnostics.txt +1 -0
  110. package/test/scenarios/union/input/main.tsp +33 -0
  111. package/test/scenarios.test.ts +226 -0
  112. package/tsconfig.json +15 -0
@@ -0,0 +1,980 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT license.
3
+
4
+ import {
5
+ DiagnosticTarget,
6
+ Enum,
7
+ formatDiagnostic,
8
+ getEffectiveModelType,
9
+ getTypeName,
10
+ Interface,
11
+ IntrinsicType,
12
+ isDeclaredInNamespace,
13
+ Model,
14
+ ModelProperty,
15
+ Namespace,
16
+ Operation,
17
+ Program,
18
+ resolvePath,
19
+ Scalar,
20
+ StringLiteral,
21
+ SyntaxKind,
22
+ Type,
23
+ Union,
24
+ } from "@typespec/compiler";
25
+ import {
26
+ map,
27
+ matchType,
28
+ ProtoEnumDeclaration,
29
+ ProtoFieldDeclaration,
30
+ ProtoFile,
31
+ ProtoMap,
32
+ ProtoMessageBodyDeclaration,
33
+ ProtoMessageDeclaration,
34
+ ProtoMethodDeclaration,
35
+ ProtoRef,
36
+ ProtoScalar,
37
+ ProtoTopLevelDeclaration,
38
+ ProtoType,
39
+ ref,
40
+ scalar,
41
+ ScalarIntegralName,
42
+ StreamingMode,
43
+ unreachable,
44
+ } from "../ast.js";
45
+ import { ProtobufEmitterOptions, reportDiagnostic, state } from "../lib.js";
46
+ import { $field, isMap, Reservation } from "../proto.js";
47
+ import { writeProtoFile } from "../write.js";
48
+
49
+ // Cache for scalar -> ProtoScalar map
50
+ const _protoScalarsMap = new WeakMap<Program, Map<Type, ProtoScalar>>();
51
+ const _protoExternMap = new WeakMap<Program, Map<string, [string, ProtoRef]>>();
52
+
53
+ /**
54
+ * Create a worker function that converts the TypeSpec program to Protobuf and writes it to the file system.
55
+ */
56
+ export function createProtobufEmitter(
57
+ program: Program
58
+ ): (outDir: string, options: ProtobufEmitterOptions) => Promise<void> {
59
+ return async function doEmit(outDir, options) {
60
+ // Convert the program to a set of proto files.
61
+ const files = tspToProto(program);
62
+
63
+ if (!program.compilerOptions.noEmit && !options?.noEmit && !program.hasError()) {
64
+ for (const file of files) {
65
+ // If the file has a package, emit it to a path that is shaped like the package name. Otherwise emit to
66
+ // main.proto
67
+
68
+ // Collisions have already been detected.
69
+
70
+ const packageSlug = file.package?.split(".") ?? ["main"];
71
+ const filePath = resolvePath(outDir, ...packageSlug.slice(0, -1));
72
+
73
+ await program.host.mkdirp(filePath);
74
+ await program.host.writeFile(
75
+ resolvePath(filePath, packageSlug[packageSlug.length - 1] + ".proto"),
76
+ writeProtoFile(file)
77
+ );
78
+ }
79
+ }
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Create a set of proto files that represent the TypeSpec program.
85
+ *
86
+ * This is the meat of the emitter.
87
+ */
88
+ function tspToProto(program: Program): ProtoFile[] {
89
+ const packages = new Set<Namespace>(
90
+ program.stateMap(state.package).keys() as Iterable<Namespace>
91
+ );
92
+
93
+ const serviceInterfaces = [...(program.stateSet(state.service) as Set<Interface>)];
94
+
95
+ const declarationMap = new Map<Namespace, ProtoTopLevelDeclaration[]>(
96
+ [...packages].map((p) => [p, []])
97
+ );
98
+
99
+ const visitedTypes = new Set<Type>();
100
+
101
+ /**
102
+ * Visits a model type, converting it into a message definition and adding it if it has not already been visited.
103
+ * @param model - the model type to consider
104
+ */
105
+ function visitModel(model: Model, source: Type) {
106
+ const modelPackage = getPackageOfType(program, model);
107
+ const declarations = modelPackage && declarationMap.get(modelPackage);
108
+
109
+ if (!declarations) {
110
+ reportDiagnostic(program, {
111
+ target: source,
112
+ code: "model-not-in-package",
113
+ format: { name: model.name },
114
+ });
115
+ }
116
+
117
+ if (!visitedTypes.has(model)) {
118
+ visitedTypes.add(model);
119
+ declarations?.push(toMessage(model));
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Visits an enum type, converting it into a Protobuf enum definition and adding it if it has not already been visited.
125
+ */
126
+ function visitEnum(e: Enum) {
127
+ const modelPackage = getPackageOfType(program, e);
128
+ const declarations = modelPackage && declarationMap.get(modelPackage);
129
+ if (!visitedTypes.has(e)) {
130
+ visitedTypes.add(e);
131
+
132
+ const members = [...e.members.values()];
133
+
134
+ // We only support enums where every variant is explicitly assigned an integer value
135
+ if (
136
+ members.some(
137
+ ({ value: v }) => v === undefined || typeof v !== "number" || !Number.isInteger(v)
138
+ )
139
+ ) {
140
+ reportDiagnostic(program, {
141
+ target: e,
142
+ code: "unconvertible-enum",
143
+ });
144
+ }
145
+
146
+ // we also only support enums where the first value is zero.
147
+ if (members[0].value !== 0) {
148
+ reportDiagnostic(program, {
149
+ target: members[0],
150
+ code: "unconvertible-enum",
151
+ messageId: "no-zero-first",
152
+ });
153
+ }
154
+
155
+ declarations?.push(toEnum(e));
156
+ }
157
+ }
158
+
159
+ const importMap = new Map([...packages].map((ns) => [ns, new Set<string>()]));
160
+
161
+ function typeWantsImport(program: Program, t: Model | Operation, path: string) {
162
+ const packageNs = getPackageOfType(program, t);
163
+
164
+ if (packageNs) {
165
+ importMap.get(packageNs)?.add(path);
166
+ }
167
+ }
168
+
169
+ const mapImportSourceInformation = new WeakMap<
170
+ ProtoMap,
171
+ [Model | Operation, NamespaceTraversable]
172
+ >();
173
+
174
+ const effectiveModelCache = new Map<Model, Model | undefined>();
175
+
176
+ for (const packageNs of packages) {
177
+ addDeclarationsOfPackage(packageNs);
178
+ }
179
+
180
+ // Emit a file per package.
181
+ const files = [...packages].map((namespace) => {
182
+ const details = program.stateMap(state.package).get(namespace) as Model | undefined;
183
+ const packageOptionsRaw = details?.properties.get("options")?.type as Model | undefined;
184
+
185
+ const packageOptions = [...(packageOptionsRaw?.properties.entries() ?? [])]
186
+ .map(([k, { type }]) => {
187
+ // This condition is enforced by the definition of `dec package`
188
+ if (type.kind === "Boolean" || type.kind === "String" || type.kind === "Number") {
189
+ return [k, type.value] as [string, unknown];
190
+ } else throw new Error(`Unexpected option type ${type.kind}`);
191
+ })
192
+ .filter((v) => !!v) as [string, unknown][];
193
+
194
+ return {
195
+ package: (
196
+ (details?.properties.get("name") as ModelProperty | undefined)?.type as
197
+ | StringLiteral
198
+ | undefined
199
+ )?.value,
200
+
201
+ options: Object.fromEntries(packageOptions),
202
+
203
+ imports: [...(importMap.get(namespace) ?? [])],
204
+
205
+ declarations: declarationMap.get(namespace),
206
+ source: namespace,
207
+ } as ProtoFile;
208
+ });
209
+
210
+ checkForNamespaceCollisions(files);
211
+
212
+ return files;
213
+
214
+ /**
215
+ * Recursively searches a namespace for declarations that should be reified as Protobuf.
216
+ *
217
+ * @param namespace - the namespace to analyze
218
+ * @returns an array of declarations
219
+ */
220
+ function addDeclarationsOfPackage(namespace: Namespace) {
221
+ const models = [...namespace.models.values()];
222
+
223
+ // Eagerly visit all models in the namespace.
224
+ for (const model of models) {
225
+ // Don't eagerly visit externs
226
+ if (
227
+ // Don't eagerly visit externs
228
+ !program.stateMap(state.externRef).has(model) &&
229
+ // Only eagerly visit models where every field has a field index annotation.
230
+ ([...model.properties.values()].every((p) => program.stateMap(state.fieldIndex).has(p)) ||
231
+ // OR where the model has been explicitly marked as a message.
232
+ program.stateSet(state.message).has(model))
233
+ ) {
234
+ visitModel(model, model);
235
+ }
236
+ }
237
+
238
+ const interfacesInNamespace = new Set(
239
+ serviceInterfaces.filter((iface) => isDeclaredInNamespace(iface, namespace))
240
+ );
241
+
242
+ // Each interface will be reified as a `service` declaration.
243
+ const declarations = declarationMap.get(namespace)!;
244
+ for (const iface of interfacesInNamespace) {
245
+ declarations.push({
246
+ kind: "service",
247
+ name: iface.name,
248
+ // The service's methods are just projections of the interface operations.
249
+ operations: [...iface.operations.values()].map(toMethodFromOperation),
250
+ });
251
+ }
252
+ }
253
+
254
+ // #region inline helpers
255
+
256
+ /**
257
+ * @param operation - the operation to convert
258
+ * @returns a corresponding method declaration
259
+ */
260
+ function toMethodFromOperation(operation: Operation): ProtoMethodDeclaration {
261
+ const streamingMode = program.stateMap(state.stream).get(operation) ?? StreamingMode.None;
262
+
263
+ return {
264
+ kind: "method",
265
+ stream: streamingMode,
266
+ name: capitalize(operation.name),
267
+ input: addImportSourceForProtoIfNeeded(
268
+ program,
269
+ addInputParams(operation.parameters, operation),
270
+ operation,
271
+ operation.parameters
272
+ ),
273
+ returns: addImportSourceForProtoIfNeeded(
274
+ program,
275
+ addReturnType(operation.returnType, operation),
276
+ operation,
277
+ operation.returnType as NamespaceTraversable
278
+ ),
279
+ };
280
+ }
281
+
282
+ /**
283
+ * Checks a parameter Model satisfies the constraints for a Protobuf method input and adds it to the declarations,
284
+ * returning a ProtoRef to the generated named message.
285
+ *
286
+ * @param model - the model to add
287
+ * @returns a reference to the model's message
288
+ */
289
+ function addInputParams(paramsModel: Model, operation: Operation): ProtoRef {
290
+ const effectiveModel = computeEffectiveModel(
291
+ paramsModel,
292
+ capitalize(operation.name) + "Request"
293
+ );
294
+
295
+ /* c8 ignore start */
296
+
297
+ // Not sure if this can or can't actually happen at runtime, but we'll defensively handle it anyway.
298
+ if (!effectiveModel) {
299
+ reportDiagnostic(program, {
300
+ code: "unsupported-input-type",
301
+ messageId: "unconvertible",
302
+ target: paramsModel,
303
+ });
304
+
305
+ return unreachable("unsupported input type");
306
+ }
307
+ /* c8 ignore stop */
308
+
309
+ return checkExtern(effectiveModel, operation);
310
+ }
311
+
312
+ /**
313
+ * Returns an extern ref if the given type is an instance of `Extern`, otherwise returns a ref to the model's name.
314
+ */
315
+ function checkExtern(model: Model, relativeSource: Model | Operation): ProtoRef {
316
+ const extern = program.stateMap(state.externRef).get(model) as [string, string] | undefined;
317
+ if (extern) {
318
+ typeWantsImport(program, relativeSource, extern[0]);
319
+ return ref(extern[1]);
320
+ }
321
+
322
+ return ref(model.name);
323
+ }
324
+
325
+ /**
326
+ * Gets a cached intrinsic type. This will also attach desired imports to the relative reference source.
327
+ */
328
+ function getCachedExternType(
329
+ program: Program,
330
+ relativeSource: Operation | Model,
331
+ name: string
332
+ ): ProtoRef {
333
+ let cache = _protoExternMap.get(program);
334
+
335
+ if (!cache) {
336
+ cache = new Map();
337
+ _protoExternMap.set(program, cache);
338
+ }
339
+
340
+ const cachedRef = cache.get(name);
341
+
342
+ if (cachedRef) {
343
+ const [source, ref] = cachedRef;
344
+ typeWantsImport(program, relativeSource, source);
345
+ return ref;
346
+ }
347
+
348
+ const [emptyType, diagnostics] = program.resolveTypeReference(name);
349
+
350
+ if (!emptyType) {
351
+ throw new Error(
352
+ `Could not resolve the empty type: ${diagnostics.map(formatDiagnostic).join("\n")}`
353
+ );
354
+ }
355
+
356
+ const extern = program.stateMap(state.externRef).get(emptyType) as [string, string] | undefined;
357
+
358
+ if (!extern) {
359
+ throw new Error(`Unexpected: '${name}' was resolved but is not an extern type.`);
360
+ }
361
+
362
+ const [source, protoName] = extern;
363
+ typeWantsImport(program, relativeSource, source);
364
+ const result = ref(protoName);
365
+
366
+ cache.set(name, [source, result]);
367
+
368
+ return result;
369
+ }
370
+
371
+ /**
372
+ * Checks that a return type is a Model and converts it to a message, adding it to the declarations and returning
373
+ * a reference to its name.
374
+ *
375
+ * @param t - the model to add
376
+ * @param operationName - the name of the originating operation, used to compute a synthetic model name if required
377
+ * @returns a reference to the model's message
378
+ */
379
+ function addReturnType(t: Type, operation: Operation): ProtoRef {
380
+ switch (t.kind) {
381
+ case "Model":
382
+ return addReturnModel(t, operation);
383
+ case "Intrinsic":
384
+ return addIntrinsicType(t, operation);
385
+ /* eslint-ignore-next-line no-fallthrough */
386
+ default:
387
+ reportDiagnostic(program, {
388
+ code: "unsupported-return-type",
389
+ target: getOperationReturnSyntaxTarget(operation),
390
+ });
391
+
392
+ return unreachable("unsupported return type");
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Adds an intrinsic type. Intrinsics are assumed to map to Extern types, so this will add the appropriate import.
398
+ *
399
+ * @param t - the intrinsic type to add
400
+ * @param relativeSource - the relative source of the type
401
+ * @returns a reference to the type's message
402
+ */
403
+ function addIntrinsicType(t: IntrinsicType, relativeSource: Operation | Model): ProtoRef {
404
+ switch (t.name) {
405
+ case "unknown":
406
+ return getCachedExternType(program, relativeSource, "TypeSpec.Protobuf.WellKnown.Any");
407
+ case "void": {
408
+ return getCachedExternType(program, relativeSource, "TypeSpec.Protobuf.WellKnown.Empty");
409
+ }
410
+ }
411
+
412
+ reportDiagnostic(program, {
413
+ code: "unsupported-intrinsic",
414
+ format: { type: t.name },
415
+ target: t,
416
+ });
417
+
418
+ return unreachable("unsupported intrinsic type");
419
+ }
420
+
421
+ /**
422
+ * Converts a TypeSpec Model to a Protobuf Ref in return position, adding a corresponding message if necessary.
423
+ *
424
+ * @param m - the model to add to the Protofile.
425
+ * @returns a Protobuf reference to the model
426
+ */
427
+ function addReturnModel(m: Model, operation: Operation): ProtoRef {
428
+ const extern = program.stateMap(state.externRef).get(m) as [string, string] | undefined;
429
+ if (extern) {
430
+ typeWantsImport(program, operation, extern[0]);
431
+ return ref(extern[1]);
432
+ }
433
+
434
+ const effectiveModel = computeEffectiveModel(m, capitalize(operation.name) + "Response");
435
+ if (effectiveModel) {
436
+ return ref(effectiveModel.name);
437
+ }
438
+
439
+ reportDiagnostic(program, {
440
+ code: "unsupported-return-type",
441
+ target: getOperationReturnSyntaxTarget(operation),
442
+ });
443
+
444
+ return unreachable("unsupported return type");
445
+ }
446
+
447
+ /**
448
+ * Converts a TypeSpec type to a Protobuf type, adding a corresponding message if necessary.
449
+ *
450
+ * @param t - the type to add to the ProtoFile.
451
+ * @returns a Protobuf type corresponding to the given type
452
+ */
453
+ function addType(t: Type, relativeSource: Model | Operation): ProtoType {
454
+ // Exit early if this type is an extern.
455
+ const extern = program.stateMap(state.externRef).get(t) as [string, string] | undefined;
456
+ if (extern) {
457
+ typeWantsImport(program, relativeSource, extern[0]);
458
+ return ref(extern[1]);
459
+ }
460
+
461
+ if (isMap(program, t)) {
462
+ const mapType = mapToProto(t as Model, relativeSource);
463
+ mapImportSourceInformation.set(mapType, [relativeSource, t as NamespaceTraversable]);
464
+ return mapType;
465
+ }
466
+
467
+ // Arrays transform into repeated fields, so we'll silently replace `t` with the array's member if this is an array.
468
+ // The `repeated` keyword will be added when the field is composed.
469
+ if (isArray(t)) {
470
+ return arrayToProto(t as Model, relativeSource);
471
+ }
472
+
473
+ switch (t.kind) {
474
+ case "Model":
475
+ // If we came from another model and this model is anonymous, then we can't reference it by name.
476
+ if (t.name === "" && relativeSource.kind === "Model") {
477
+ reportDiagnostic(program, {
478
+ code: "anonymous-model",
479
+ target: t,
480
+ });
481
+ return unreachable("anonymous model");
482
+ }
483
+ visitModel(t, relativeSource);
484
+ return ref(t.name);
485
+ case "Enum":
486
+ visitEnum(t);
487
+ return ref(t.name);
488
+ case "Scalar":
489
+ return scalarToProto(t);
490
+ case "Intrinsic":
491
+ return addIntrinsicType(t, relativeSource);
492
+ default:
493
+ reportDiagnostic(program, {
494
+ code: "unsupported-field-type",
495
+ messageId: "unconvertible",
496
+ format: {
497
+ type: t.kind,
498
+ },
499
+ target: t,
500
+ });
501
+ return unreachable("unsupported field type");
502
+ }
503
+ }
504
+
505
+ function mapToProto(t: Model, relativeSource: Model | Operation): ProtoMap {
506
+ const [keyType, valueType] = t.templateMapper!.args;
507
+
508
+ // A map's value cannot be another map.
509
+ if (isMap(program, valueType)) {
510
+ reportDiagnostic(program, {
511
+ code: "unsupported-field-type",
512
+ messageId: "recursive-map",
513
+ target: valueType,
514
+ });
515
+ return unreachable("recursive map");
516
+ }
517
+
518
+ // This is a core compile error.
519
+ if (!keyType || !valueType) return unreachable("nonexistent map key or value type");
520
+
521
+ // Key constraint (integral | string) is enforced by the type constraint on the `Map<>` type.
522
+ const keyProto = addType(keyType, relativeSource);
523
+ const valueProto = addType(valueType, relativeSource) as ProtoRef | ProtoScalar;
524
+
525
+ return map(keyProto[1] as "string" | ScalarIntegralName, valueProto);
526
+ }
527
+
528
+ function arrayToProto(t: Model, relativeSource: Model | Operation): ProtoType {
529
+ const valueType = (t as Model).templateMapper!.args[0];
530
+
531
+ // Nested arrays are not supported.
532
+ if (isArray(valueType)) {
533
+ reportDiagnostic(program, {
534
+ code: "nested-array",
535
+ target: valueType,
536
+ });
537
+ return ref("<unreachable>");
538
+ }
539
+
540
+ return addType(valueType, relativeSource);
541
+ }
542
+
543
+ function getProtoScalarsMap(program: Program): Map<Type, ProtoScalar> {
544
+ // The type references are different object identities in different programs, so we need to cache the map per program.
545
+ // This really only affects tests in our current use case, but someone could be using the compiler API to compile
546
+ // multiple programs and it also affects that.
547
+ let scalarMap;
548
+ if (_protoScalarsMap.has(program)) {
549
+ scalarMap = _protoScalarsMap.get(program)!;
550
+ } else {
551
+ const entries = [
552
+ [program.resolveTypeReference("TypeSpec.bytes"), scalar("bytes")],
553
+ [program.resolveTypeReference("TypeSpec.boolean"), scalar("bool")],
554
+ [program.resolveTypeReference("TypeSpec.string"), scalar("string")],
555
+ [program.resolveTypeReference("TypeSpec.int32"), scalar("int32")],
556
+ [program.resolveTypeReference("TypeSpec.int64"), scalar("int64")],
557
+ [program.resolveTypeReference("TypeSpec.uint32"), scalar("uint32")],
558
+ [program.resolveTypeReference("TypeSpec.uint64"), scalar("uint64")],
559
+ [program.resolveTypeReference("TypeSpec.float32"), scalar("float")],
560
+ [program.resolveTypeReference("TypeSpec.float64"), scalar("double")],
561
+ [program.resolveTypeReference("TypeSpec.Protobuf.sfixed32"), scalar("sfixed32")],
562
+ [program.resolveTypeReference("TypeSpec.Protobuf.sfixed64"), scalar("sfixed64")],
563
+ [program.resolveTypeReference("TypeSpec.Protobuf.sint32"), scalar("sint32")],
564
+ [program.resolveTypeReference("TypeSpec.Protobuf.sint64"), scalar("sint64")],
565
+ [program.resolveTypeReference("TypeSpec.Protobuf.fixed32"), scalar("fixed32")],
566
+ [program.resolveTypeReference("TypeSpec.Protobuf.fixed64"), scalar("fixed64")],
567
+ ] as const;
568
+
569
+ for (const [[type, diagnostics]] of entries) {
570
+ if (!type) {
571
+ const diagnosticString = diagnostics.map(formatDiagnostic).join("\n");
572
+ throw new Error(
573
+ `Failed to construct TypeSpec -> Protobuf scalar map. Unexpected failure to resolve TypeSpec scalar: ${diagnosticString}`
574
+ );
575
+ }
576
+ }
577
+
578
+ scalarMap = new Map<Type, ProtoScalar>(entries.map(([[type], scalar]) => [type!, scalar]));
579
+
580
+ _protoScalarsMap.set(program, scalarMap);
581
+ }
582
+ // Lazy initialize this map of known proto scalars.
583
+
584
+ return scalarMap;
585
+ }
586
+
587
+ function scalarToProto(t: Scalar): ProtoType {
588
+ const fullName = getTypeName(t);
589
+
590
+ const protoType = getProtoScalarsMap(program).get(t);
591
+
592
+ if (!protoType) {
593
+ if (t.baseScalar) {
594
+ return scalarToProto(t.baseScalar);
595
+ } else {
596
+ reportDiagnostic(program, {
597
+ code: "unsupported-field-type",
598
+ messageId: "unknown-scalar",
599
+ format: {
600
+ name: fullName,
601
+ },
602
+ target: t,
603
+ });
604
+ return unreachable("unknown scalar");
605
+ }
606
+ }
607
+
608
+ return protoType;
609
+ }
610
+
611
+ function computeEffectiveModel(model: Model, anonymousModelName: string): Model | undefined {
612
+ if (effectiveModelCache.has(model)) return effectiveModelCache.get(model);
613
+
614
+ let effectiveModel = getEffectiveModelType(program, model);
615
+
616
+ if (effectiveModel.name === "") {
617
+ // Name the model automatically if it is anonymous
618
+ effectiveModel = program.checker.createAndFinishType({
619
+ ...model,
620
+ name: anonymousModelName,
621
+ });
622
+ }
623
+
624
+ if (!program.stateMap(state.externRef).has(effectiveModel)) {
625
+ visitModel(effectiveModel, model);
626
+ }
627
+
628
+ effectiveModelCache.set(model, effectiveModel);
629
+
630
+ return effectiveModel;
631
+ }
632
+ // #endregion
633
+
634
+ function checkForNamespaceCollisions(files: ProtoFile[]) {
635
+ const namespaces = new Set<string | undefined>();
636
+
637
+ for (const file of files) {
638
+ if (namespaces.has(file.package)) {
639
+ reportDiagnostic(program, {
640
+ code: "namespace-collision",
641
+ format: {
642
+ name: `"${file.package}"` ?? "<empty>",
643
+ },
644
+ target: file.source,
645
+ });
646
+ }
647
+
648
+ namespaces.add(file.package);
649
+ }
650
+ }
651
+
652
+ /**
653
+ * @param model - the Model to convert
654
+ * @returns a corresponding message declaration
655
+ */
656
+ function toMessage(model: Model): ProtoMessageDeclaration {
657
+ return {
658
+ kind: "message",
659
+ name: model.name,
660
+ reservations: program.stateMap(state.reserve).get(model),
661
+ declarations: [...model.properties.values()].map((f) => toMessageBodyDeclaration(f, model)),
662
+ };
663
+ }
664
+
665
+ /**
666
+ * @param property - the ModelProperty to convert
667
+ * @returns a corresponding declaration
668
+ */
669
+ function toMessageBodyDeclaration(
670
+ property: ModelProperty,
671
+ model: Model
672
+ ): ProtoMessageBodyDeclaration {
673
+ if (property.type.kind === "Union") {
674
+ // Unions are difficult to represent in protobuf, so for now we don't support them.
675
+ // See : https://github.com/microsoft/typespec/issues/1854
676
+ reportDiagnostic(program, {
677
+ code: "unsupported-field-type",
678
+ messageId: "union",
679
+ target: property,
680
+ });
681
+ return unreachable("union");
682
+ }
683
+
684
+ const fieldIndex = program.stateMap(state.fieldIndex).get(property) as number | undefined;
685
+ const fieldIndexNode = property.decorators.find((d) => d.decorator === $field)?.args[0].node;
686
+
687
+ if (fieldIndex === undefined) {
688
+ reportDiagnostic(program, {
689
+ code: "field-index",
690
+ messageId: "missing",
691
+ format: {
692
+ name: property.name,
693
+ },
694
+ target: property,
695
+ });
696
+ }
697
+
698
+ if (fieldIndex && !fieldIndexNode)
699
+ throw new Error("Failed to recover field decorator argument.");
700
+
701
+ const reservations = program.stateMap(state.reserve).get(model) as Reservation[] | undefined;
702
+
703
+ if (reservations) {
704
+ for (const reservation of reservations) {
705
+ if (typeof reservation === "string" && reservation === property.name) {
706
+ reportDiagnostic(program, {
707
+ code: "field-name",
708
+ messageId: "user-reserved",
709
+ format: {
710
+ name: property.name,
711
+ },
712
+ target: getPropertyNameSyntaxTarget(property),
713
+ });
714
+ } else if (
715
+ fieldIndex !== undefined &&
716
+ typeof reservation === "number" &&
717
+ reservation === fieldIndex
718
+ ) {
719
+ reportDiagnostic(program, {
720
+ code: "field-index",
721
+ messageId: "user-reserved",
722
+ format: {
723
+ index: fieldIndex.toString(),
724
+ },
725
+ // Fail over to using the model if the field index node is missing... this should never occur but it's the
726
+ // simplest way to satisfy the type system.
727
+ target: fieldIndexNode ?? model,
728
+ });
729
+ } else if (
730
+ fieldIndex !== undefined &&
731
+ Array.isArray(reservation) &&
732
+ fieldIndex >= reservation[0] &&
733
+ fieldIndex <= reservation[1]
734
+ ) {
735
+ reportDiagnostic(program, {
736
+ code: "field-index",
737
+ messageId: "user-reserved-range",
738
+ format: {
739
+ index: fieldIndex.toString(),
740
+ },
741
+ target: fieldIndexNode ?? model,
742
+ });
743
+ }
744
+ }
745
+ }
746
+
747
+ const field: ProtoFieldDeclaration = {
748
+ kind: "field",
749
+ name: property.name,
750
+ type: addImportSourceForProtoIfNeeded(
751
+ program,
752
+ addType(property.type, model),
753
+ model,
754
+ property.type as NamespaceTraversable
755
+ ),
756
+ index: program.stateMap(state.fieldIndex).get(property),
757
+ };
758
+
759
+ // Determine if the property type is an array
760
+ if (isArray(property.type)) field.repeated = true;
761
+
762
+ return field;
763
+ }
764
+
765
+ /**
766
+ * @param e - the Enum to convert
767
+ * @returns a corresponding protobuf enum declaration
768
+ *
769
+ * INVARIANT: the enum's members must be integer values
770
+ */
771
+ function toEnum(e: Enum): ProtoEnumDeclaration {
772
+ const needsAlias = new Set([...e.members.values()].map((v) => v.value)).size !== e.members.size;
773
+
774
+ return {
775
+ kind: "enum",
776
+ name: e.name,
777
+ allowAlias: needsAlias,
778
+ variants: [...e.members.values()].map(({ name, value }) => [name, value as number]),
779
+ };
780
+ }
781
+
782
+ type NamespaceTraversable =
783
+ | Enum
784
+ | Model
785
+ | Interface
786
+ | Union
787
+ | Operation
788
+ | Namespace
789
+ | IntrinsicType;
790
+
791
+ function getPackageOfType(program: Program, t: NamespaceTraversable): Namespace | null {
792
+ /* c8 ignore start */
793
+
794
+ // Most of this should be unreachable, but we'll guard it with diagnostics anyway in case of eventual synthetic types.
795
+
796
+ switch (t.kind) {
797
+ case "Intrinsic":
798
+ // Intrinsics are all handled explicitly.
799
+ return null;
800
+ case "Enum":
801
+ case "Model":
802
+ case "Union":
803
+ case "Interface":
804
+ if (!t.namespace) {
805
+ return null;
806
+ } else {
807
+ return getPackageOfType(program, t.namespace);
808
+ }
809
+ case "Operation": {
810
+ const logicalParent = t.interface ?? t.namespace;
811
+ if (!logicalParent) {
812
+ return null;
813
+ } else {
814
+ return getPackageOfType(program, logicalParent);
815
+ }
816
+ }
817
+ case "Namespace":
818
+ if (packages.has(t)) return t;
819
+
820
+ if (!t.namespace) {
821
+ return null;
822
+ } else {
823
+ return getPackageOfType(program, t.namespace);
824
+ }
825
+ }
826
+ /* c8 ignore stop */
827
+ }
828
+
829
+ function addImportSourceForProtoIfNeeded<T extends ProtoType>(
830
+ program: Program,
831
+ pt: T,
832
+ dependent: Model | Operation,
833
+ dependency: NamespaceTraversable
834
+ ): T {
835
+ {
836
+ // Early escape for intrinsics
837
+ if (dependency.kind === "Intrinsic") {
838
+ // Intrinsics and imports are handled explicitly by the emitter.
839
+ return pt;
840
+ }
841
+ }
842
+
843
+ {
844
+ // Early escape for externs
845
+ let effectiveModel: Model | undefined;
846
+ if (
847
+ program.stateMap(state.externRef).has(dependency) ||
848
+ (dependency.kind === "Model" &&
849
+ (effectiveModel = effectiveModelCache.get(dependency)) &&
850
+ program.stateMap(state.externRef).has(effectiveModel))
851
+ ) {
852
+ return pt;
853
+ }
854
+ }
855
+
856
+ if (isArray(dependency)) {
857
+ return addImportSourceForProtoIfNeeded(
858
+ program,
859
+ pt,
860
+ dependent,
861
+ (dependency as Model).templateMapper!.args[0] as NamespaceTraversable
862
+ );
863
+ }
864
+ try {
865
+ // If we had an error producing an "unreachable" type, we would actually reach it during validation below, so the
866
+ // try/catch allows us to pass the unreachable back up the chain.
867
+ return matchType(pt, {
868
+ map(k, v) {
869
+ const mapInfo = mapImportSourceInformation.get(pt as ProtoMap);
870
+ return mapInfo !== undefined
871
+ ? (map(
872
+ k,
873
+ addImportSourceForProtoIfNeeded(program, v, mapInfo[0], mapInfo[1]) as
874
+ | ProtoRef
875
+ | ProtoScalar
876
+ // Anything else is unreachable by construction.
877
+ ) as T)
878
+ : pt;
879
+ },
880
+ scalar() {
881
+ return pt;
882
+ },
883
+ ref(r) {
884
+ const [dependentPackage, dependencyPackage] = [
885
+ getPackageOfType(program, dependent),
886
+ getPackageOfType(program, dependency),
887
+ ];
888
+
889
+ if (
890
+ dependentPackage === null ||
891
+ dependencyPackage === null ||
892
+ dependentPackage === dependencyPackage
893
+ )
894
+ return pt;
895
+
896
+ const dependencyDetails = program.stateMap(state.package).get(dependencyPackage) as
897
+ | Model
898
+ | undefined;
899
+
900
+ const dependencyPackageName = (
901
+ dependencyDetails?.properties.get("name")?.type as StringLiteral | undefined
902
+ )?.value;
903
+
904
+ const dependencyPackagePrefix =
905
+ dependencyPackageName === undefined || dependencyPackageName === ""
906
+ ? ""
907
+ : dependencyPackageName + ".";
908
+
909
+ const dependencyFileName =
910
+ (dependencyPackageName?.split(".") ?? ["main"]).join("/") + ".proto";
911
+
912
+ importMap.get(dependentPackage)?.add(dependencyFileName);
913
+
914
+ return ref(dependencyPackagePrefix + r) as T;
915
+ },
916
+ });
917
+ } catch {
918
+ return pt;
919
+ }
920
+ }
921
+ }
922
+
923
+ function isArray(t: Type) {
924
+ return t.kind === "Model" && t.name === "Array" && t.namespace?.name === "TypeSpec";
925
+ }
926
+
927
+ /**
928
+ * Simple utility function to capitalize a string.
929
+ */
930
+ function capitalize<S extends string>(s: S) {
931
+ return (s.slice(0, 1).toUpperCase() + s.slice(1)) as Capitalize<S>;
932
+ }
933
+
934
+ /**
935
+ * Gets the syntactic return type target for an operation.
936
+ *
937
+ * Helps us squiggle the right things for operation return types.
938
+ *
939
+ * See https://github.com/microsoft/typespec/issues/1650. This issue tracks helpers for doing this without requiring
940
+ * emitters to implement this functionality.
941
+ */
942
+ function getOperationReturnSyntaxTarget(op: Operation): DiagnosticTarget {
943
+ const signature = op.node.signature;
944
+ switch (signature.kind) {
945
+ case SyntaxKind.OperationSignatureDeclaration:
946
+ return signature.returnType;
947
+ case SyntaxKind.OperationSignatureReference:
948
+ return op;
949
+ default:
950
+ const __exhaust: never = signature;
951
+ throw new Error(
952
+ `Internal Emitter Error: reached unreachable operation signature: ${op.node.signature.kind}`
953
+ );
954
+ }
955
+ }
956
+
957
+ /**
958
+ * Gets the syntactic position of a model property name.
959
+ *
960
+ * See https://github.com/microsoft/typespec/issues/1650. This issue tracks helpers for doing this without requiring
961
+ * emitters to implement this functionality.
962
+ */
963
+ function getPropertyNameSyntaxTarget(property: ModelProperty): DiagnosticTarget {
964
+ const node = property.node;
965
+
966
+ switch (node.kind) {
967
+ case SyntaxKind.ModelProperty:
968
+ return node.id;
969
+ case SyntaxKind.ModelSpreadProperty:
970
+ return node;
971
+ case SyntaxKind.ProjectionModelProperty:
972
+ case SyntaxKind.ProjectionModelSpreadProperty:
973
+ return property;
974
+ default:
975
+ const __exhaust: never = node;
976
+ throw new Error(
977
+ `Internal Emitter Error: reached unreachable model property node: ${property.node.kind}`
978
+ );
979
+ }
980
+ }