skir 0.0.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.
Files changed (141) hide show
  1. package/README.md +447 -0
  2. package/dist/casing.d.ts +8 -0
  3. package/dist/casing.d.ts.map +1 -0
  4. package/dist/casing.js +49 -0
  5. package/dist/casing.js.map +1 -0
  6. package/dist/casing.test.d.ts +2 -0
  7. package/dist/casing.test.d.ts.map +1 -0
  8. package/dist/casing.test.js +134 -0
  9. package/dist/casing.test.js.map +1 -0
  10. package/dist/command_line_parser.d.ts +33 -0
  11. package/dist/command_line_parser.d.ts.map +1 -0
  12. package/dist/command_line_parser.js +171 -0
  13. package/dist/command_line_parser.js.map +1 -0
  14. package/dist/command_line_parser.test.d.ts +2 -0
  15. package/dist/command_line_parser.test.d.ts.map +1 -0
  16. package/dist/command_line_parser.test.js +302 -0
  17. package/dist/command_line_parser.test.js.map +1 -0
  18. package/dist/compatibility_checker.d.ts +68 -0
  19. package/dist/compatibility_checker.d.ts.map +1 -0
  20. package/dist/compatibility_checker.js +328 -0
  21. package/dist/compatibility_checker.js.map +1 -0
  22. package/dist/compatibility_checker.test.d.ts +2 -0
  23. package/dist/compatibility_checker.test.d.ts.map +1 -0
  24. package/dist/compatibility_checker.test.js +528 -0
  25. package/dist/compatibility_checker.test.js.map +1 -0
  26. package/dist/compiler.d.ts +3 -0
  27. package/dist/compiler.d.ts.map +1 -0
  28. package/dist/compiler.js +358 -0
  29. package/dist/compiler.js.map +1 -0
  30. package/dist/config.d.ts +47 -0
  31. package/dist/config.d.ts.map +1 -0
  32. package/dist/config.js +23 -0
  33. package/dist/config.js.map +1 -0
  34. package/dist/definition_finder.d.ts +12 -0
  35. package/dist/definition_finder.d.ts.map +1 -0
  36. package/dist/definition_finder.js +180 -0
  37. package/dist/definition_finder.js.map +1 -0
  38. package/dist/definition_finder.test.d.ts +2 -0
  39. package/dist/definition_finder.test.d.ts.map +1 -0
  40. package/dist/definition_finder.test.js +164 -0
  41. package/dist/definition_finder.test.js.map +1 -0
  42. package/dist/encoding.d.ts +2 -0
  43. package/dist/encoding.d.ts.map +1 -0
  44. package/dist/encoding.js +38 -0
  45. package/dist/encoding.js.map +1 -0
  46. package/dist/encoding.test.d.ts +2 -0
  47. package/dist/encoding.test.d.ts.map +1 -0
  48. package/dist/encoding.test.js +23 -0
  49. package/dist/encoding.test.js.map +1 -0
  50. package/dist/error_renderer.d.ts +10 -0
  51. package/dist/error_renderer.d.ts.map +1 -0
  52. package/dist/error_renderer.js +247 -0
  53. package/dist/error_renderer.js.map +1 -0
  54. package/dist/formatter.d.ts +3 -0
  55. package/dist/formatter.d.ts.map +1 -0
  56. package/dist/formatter.js +263 -0
  57. package/dist/formatter.js.map +1 -0
  58. package/dist/formatter.test.d.ts +2 -0
  59. package/dist/formatter.test.d.ts.map +1 -0
  60. package/dist/formatter.test.js +156 -0
  61. package/dist/formatter.test.js.map +1 -0
  62. package/dist/index.d.ts +6 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +5 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/index.test.d.ts +2 -0
  67. package/dist/index.test.d.ts.map +1 -0
  68. package/dist/index.test.js +14 -0
  69. package/dist/index.test.js.map +1 -0
  70. package/dist/io.d.ts +13 -0
  71. package/dist/io.d.ts.map +1 -0
  72. package/dist/io.js +22 -0
  73. package/dist/io.js.map +1 -0
  74. package/dist/language_server.d.ts +15 -0
  75. package/dist/language_server.d.ts.map +1 -0
  76. package/dist/language_server.js +248 -0
  77. package/dist/language_server.js.map +1 -0
  78. package/dist/literals.d.ts +13 -0
  79. package/dist/literals.d.ts.map +1 -0
  80. package/dist/literals.js +100 -0
  81. package/dist/literals.js.map +1 -0
  82. package/dist/literals.test.d.ts +2 -0
  83. package/dist/literals.test.d.ts.map +1 -0
  84. package/dist/literals.test.js +149 -0
  85. package/dist/literals.test.js.map +1 -0
  86. package/dist/module_collector.d.ts +3 -0
  87. package/dist/module_collector.d.ts.map +1 -0
  88. package/dist/module_collector.js +22 -0
  89. package/dist/module_collector.js.map +1 -0
  90. package/dist/module_set.d.ts +44 -0
  91. package/dist/module_set.d.ts.map +1 -0
  92. package/dist/module_set.js +1025 -0
  93. package/dist/module_set.js.map +1 -0
  94. package/dist/module_set.test.d.ts +2 -0
  95. package/dist/module_set.test.d.ts.map +1 -0
  96. package/dist/module_set.test.js +1330 -0
  97. package/dist/module_set.test.js.map +1 -0
  98. package/dist/parser.d.ts +6 -0
  99. package/dist/parser.d.ts.map +1 -0
  100. package/dist/parser.js +971 -0
  101. package/dist/parser.js.map +1 -0
  102. package/dist/parser.test.d.ts +2 -0
  103. package/dist/parser.test.d.ts.map +1 -0
  104. package/dist/parser.test.js +1366 -0
  105. package/dist/parser.test.js.map +1 -0
  106. package/dist/snapshotter.d.ts +6 -0
  107. package/dist/snapshotter.d.ts.map +1 -0
  108. package/dist/snapshotter.js +107 -0
  109. package/dist/snapshotter.js.map +1 -0
  110. package/dist/tokenizer.d.ts +4 -0
  111. package/dist/tokenizer.d.ts.map +1 -0
  112. package/dist/tokenizer.js +192 -0
  113. package/dist/tokenizer.js.map +1 -0
  114. package/dist/tokenizer.test.d.ts +2 -0
  115. package/dist/tokenizer.test.d.ts.map +1 -0
  116. package/dist/tokenizer.test.js +425 -0
  117. package/dist/tokenizer.test.js.map +1 -0
  118. package/dist/types.d.ts +375 -0
  119. package/dist/types.d.ts.map +1 -0
  120. package/dist/types.js +2 -0
  121. package/dist/types.js.map +1 -0
  122. package/package.json +63 -0
  123. package/src/casing.ts +64 -0
  124. package/src/command_line_parser.ts +249 -0
  125. package/src/compatibility_checker.ts +470 -0
  126. package/src/compiler.ts +435 -0
  127. package/src/config.ts +28 -0
  128. package/src/definition_finder.ts +221 -0
  129. package/src/encoding.ts +32 -0
  130. package/src/error_renderer.ts +278 -0
  131. package/src/formatter.ts +274 -0
  132. package/src/index.ts +6 -0
  133. package/src/io.ts +33 -0
  134. package/src/language_server.ts +301 -0
  135. package/src/literals.ts +120 -0
  136. package/src/module_collector.ts +22 -0
  137. package/src/module_set.ts +1175 -0
  138. package/src/parser.ts +1122 -0
  139. package/src/snapshotter.ts +136 -0
  140. package/src/tokenizer.ts +216 -0
  141. package/src/types.ts +518 -0
@@ -0,0 +1,249 @@
1
+ export type ParsedArgs =
2
+ | {
3
+ kind: "gen";
4
+ root?: string;
5
+ watch?: true;
6
+ }
7
+ | {
8
+ kind: "format";
9
+ root?: string;
10
+ check?: true;
11
+ }
12
+ | {
13
+ kind: "snapshot";
14
+ root?: string;
15
+ check?: true;
16
+ }
17
+ | {
18
+ kind: "init";
19
+ root?: string;
20
+ }
21
+ | {
22
+ kind: "help";
23
+ root?: undefined;
24
+ }
25
+ | {
26
+ kind: "error";
27
+ root?: undefined;
28
+ };
29
+
30
+ /**
31
+ * Parse command-line arguments and return a structured representation.
32
+ *
33
+ * @param args - Array of command-line arguments (typically process.argv.slice(2))
34
+ * @returns ParsedCommandLine object representing the command and its options
35
+ */
36
+ export function parseCommandLine(args: string[]): ParsedArgs {
37
+ if (args.length === 0) {
38
+ printHelp();
39
+ return { kind: "help" };
40
+ }
41
+
42
+ const command = args[0];
43
+
44
+ if (command === "help" || command === "--help" || command === "-h") {
45
+ printHelp();
46
+ return { kind: "help" };
47
+ }
48
+
49
+ const validCommands = ["gen", "format", "snapshot", "init"];
50
+ if (!command || !validCommands.includes(command)) {
51
+ printError(`Unknown command: ${command}`);
52
+ printHelp();
53
+ return { kind: "error" };
54
+ }
55
+
56
+ try {
57
+ const options = parseOptions(args.slice(1));
58
+
59
+ switch (command) {
60
+ case "gen":
61
+ return buildGenCommand(options);
62
+ case "format":
63
+ return buildFormatCommand(options);
64
+ case "snapshot":
65
+ return buildSnapshotCommand(options);
66
+ case "init":
67
+ return buildInitCommand(options);
68
+ default:
69
+ throw new CommandLineParseError(`Unexpected command: ${command}`);
70
+ }
71
+ } catch (error) {
72
+ if (error instanceof CommandLineParseError) {
73
+ printError(error.message);
74
+ return { kind: "error" };
75
+ }
76
+ throw error;
77
+ }
78
+ }
79
+
80
+ const COMMAND_BASE = "npx skir";
81
+
82
+ const HELP_TEXT = `
83
+ Usage: ${COMMAND_BASE} <command> [options]
84
+
85
+ Commands:
86
+ gen Generate code from Skir source files to target languages
87
+ format Format all .skir files in the specified directory
88
+ snapshot Verify compatibility by comparing current .skir files against the last snapshot
89
+ init Initialize a new Skir project with a minimal skir.yml file
90
+ help Display this help message
91
+
92
+ Options:
93
+ --root, -r <path> Path to the directory containing the skir.yml configuration file
94
+ --watch, -w Enable watch mode to automatically regenerate code when .skir files change (gen only)
95
+ --check, -c Check mode: fail if code is not properly formatted (format) or if there are breaking changes (snapshot)
96
+
97
+ Examples:
98
+ ${COMMAND_BASE} gen
99
+ ${COMMAND_BASE} format --root path/to/root/dir
100
+ ${COMMAND_BASE} format -r path/to/root/dir
101
+ ${COMMAND_BASE} gen -r path/to/root/dir --watch
102
+ ${COMMAND_BASE} snapshot --root path/to/root/dir
103
+ `;
104
+
105
+ export class CommandLineParseError extends Error {
106
+ constructor(message: string) {
107
+ super(message);
108
+ this.name = "CommandLineParseError";
109
+ }
110
+ }
111
+
112
+ type ParsedOptions = {
113
+ root?: string;
114
+ watch?: boolean;
115
+ check?: boolean;
116
+ unknown: string[];
117
+ };
118
+
119
+ function parseOptions(args: string[]): ParsedOptions {
120
+ const options: ParsedOptions = {
121
+ unknown: [],
122
+ };
123
+
124
+ for (let i = 0; i < args.length; i++) {
125
+ const arg = args[i];
126
+ if (!arg) continue;
127
+
128
+ if (arg === "--root" || arg === "-r") {
129
+ if (i + 1 >= args.length) {
130
+ throw new CommandLineParseError(`Option ${arg} requires a value`);
131
+ }
132
+ if (options.root !== undefined) {
133
+ throw new CommandLineParseError(
134
+ `Option ${arg} specified multiple times`,
135
+ );
136
+ }
137
+ options.root = args[i + 1];
138
+ i++; // Skip the next argument as it's the value
139
+ } else if (arg === "--watch" || arg === "-w") {
140
+ if (options.watch) {
141
+ throw new CommandLineParseError(
142
+ `Option ${arg} specified multiple times`,
143
+ );
144
+ }
145
+ options.watch = true;
146
+ } else if (arg === "--check" || arg === "-c") {
147
+ if (options.check) {
148
+ throw new CommandLineParseError(
149
+ `Option ${arg} specified multiple times`,
150
+ );
151
+ }
152
+ options.check = true;
153
+ } else if (arg.startsWith("-")) {
154
+ options.unknown.push(arg);
155
+ } else {
156
+ throw new CommandLineParseError(`Unexpected argument: ${arg}`);
157
+ }
158
+ }
159
+
160
+ return options;
161
+ }
162
+
163
+ function buildGenCommand(options: ParsedOptions): ParsedArgs {
164
+ validateNoUnknownOptions(options, "gen");
165
+
166
+ if (options.check) {
167
+ throw new CommandLineParseError(
168
+ "Option --check is not valid for the 'gen' command",
169
+ );
170
+ }
171
+
172
+ return {
173
+ kind: "gen",
174
+ root: options.root,
175
+ watch: options.watch ? true : undefined,
176
+ };
177
+ }
178
+
179
+ function buildFormatCommand(options: ParsedOptions): ParsedArgs {
180
+ validateNoUnknownOptions(options, "format");
181
+
182
+ if (options.watch) {
183
+ throw new CommandLineParseError(
184
+ "Option --watch is not valid for the 'format' command",
185
+ );
186
+ }
187
+
188
+ return {
189
+ kind: "format",
190
+ root: options.root,
191
+ check: options.check ? true : undefined,
192
+ };
193
+ }
194
+
195
+ function buildSnapshotCommand(options: ParsedOptions): ParsedArgs {
196
+ validateNoUnknownOptions(options, "snapshot");
197
+
198
+ if (options.watch) {
199
+ throw new CommandLineParseError(
200
+ "Option --watch is not valid for the 'snapshot' command",
201
+ );
202
+ }
203
+
204
+ return {
205
+ kind: "snapshot",
206
+ root: options.root,
207
+ check: options.check ? true : undefined,
208
+ };
209
+ }
210
+
211
+ function buildInitCommand(options: ParsedOptions): ParsedArgs {
212
+ validateNoUnknownOptions(options, "init");
213
+
214
+ if (options.watch) {
215
+ throw new CommandLineParseError(
216
+ "Option --watch is not valid for the 'init' command",
217
+ );
218
+ }
219
+
220
+ if (options.check) {
221
+ throw new CommandLineParseError(
222
+ "Option --check is not valid for the 'init' command",
223
+ );
224
+ }
225
+
226
+ return {
227
+ kind: "init",
228
+ root: options.root,
229
+ };
230
+ }
231
+
232
+ function validateNoUnknownOptions(
233
+ options: ParsedOptions,
234
+ command: string,
235
+ ): void {
236
+ if (options.unknown.length > 0) {
237
+ throw new CommandLineParseError(
238
+ `Unknown option${options.unknown.length > 1 ? "s" : ""} for '${command}': ${options.unknown.join(", ")}`,
239
+ );
240
+ }
241
+ }
242
+
243
+ function printHelp(): void {
244
+ console.log(HELP_TEXT);
245
+ }
246
+
247
+ function printError(message: string): void {
248
+ console.error(`Error: ${message}\n`);
249
+ }
@@ -0,0 +1,470 @@
1
+ import { ModuleSet } from "./module_set.js";
2
+ import type {
3
+ Field,
4
+ Method,
5
+ Primitive,
6
+ Record,
7
+ RecordLocation,
8
+ ResolvedType,
9
+ Token,
10
+ } from "./types.js";
11
+
12
+ export interface BeforeAfter<T> {
13
+ before: T;
14
+ after: T;
15
+ }
16
+
17
+ export type BreakingChange =
18
+ | {
19
+ kind: "illegal-type-change";
20
+ expression: BeforeAfter<Expression>;
21
+ type: BeforeAfter<ResolvedType>;
22
+ }
23
+ | {
24
+ kind: "missing-slots";
25
+ record: BeforeAfter<RecordLocation>;
26
+ recordExpression: BeforeAfter<Expression>;
27
+ missingRangeStart: number;
28
+ missingRangeEnd: number;
29
+ }
30
+ | {
31
+ kind: "missing-record";
32
+ record: RecordLocation;
33
+ recordNumber: number;
34
+ }
35
+ | {
36
+ kind: "missing-method";
37
+ method: Method;
38
+ }
39
+ | {
40
+ kind: "record-kind-change";
41
+ record: BeforeAfter<RecordLocation>;
42
+ recordExpression: BeforeAfter<Expression>;
43
+ recordType: BeforeAfter<"struct" | "enum">;
44
+ }
45
+ | {
46
+ kind: "removed-number-reintroduced";
47
+ record: BeforeAfter<RecordLocation>;
48
+ recordExpression: BeforeAfter<Expression>;
49
+ removedNumber: number;
50
+ reintroducedAs: Token;
51
+ }
52
+ | {
53
+ kind: "enum-variant-kind-change";
54
+ record: BeforeAfter<RecordLocation>;
55
+ enumEpression: BeforeAfter<Expression>;
56
+ variantName: BeforeAfter<Token>;
57
+ number: number;
58
+ };
59
+
60
+ export type Expression =
61
+ | {
62
+ kind: "request-type";
63
+ methodName: Token;
64
+ }
65
+ | {
66
+ kind: "response-type";
67
+ methodName: Token;
68
+ }
69
+ | {
70
+ kind: "record";
71
+ recordName: Token;
72
+ }
73
+ | {
74
+ kind: "item";
75
+ arrayExpression: Expression;
76
+ }
77
+ | {
78
+ kind: "optional-value";
79
+ optionalExpression: Expression;
80
+ }
81
+ | {
82
+ kind: "property";
83
+ structExpression: Expression;
84
+ fieldName: Token;
85
+ }
86
+ | {
87
+ kind: "as-variant";
88
+ enumExpression: Expression;
89
+ variantName: Token;
90
+ };
91
+
92
+ export function checkBackwardCompatibility(
93
+ moduleSet: BeforeAfter<ModuleSet>,
94
+ ): readonly BreakingChange[] {
95
+ return new BackwardCompatibilityChecker(moduleSet).check();
96
+ }
97
+
98
+ class BackwardCompatibilityChecker {
99
+ constructor(private readonly moduleSet: BeforeAfter<ModuleSet>) {}
100
+
101
+ check(): readonly BreakingChange[] {
102
+ for (const moduleBefore of this.moduleSet.before.resolvedModules) {
103
+ for (const methodBefore of moduleBefore.methods) {
104
+ if (methodBefore.hasExplicitNumber) {
105
+ const { number } = methodBefore;
106
+ const methodAfter = this.moduleSet.after.findMethodByNumber(number);
107
+ if (methodAfter === undefined) {
108
+ this.breakingChanges.push({
109
+ kind: "missing-method",
110
+ method: methodBefore,
111
+ });
112
+ } else {
113
+ this.checkMethod({
114
+ before: methodBefore,
115
+ after: methodAfter,
116
+ });
117
+ }
118
+ }
119
+ }
120
+ for (const recordBefore of moduleBefore.records) {
121
+ const { recordNumber } = recordBefore.record;
122
+ if (recordNumber !== null) {
123
+ const recordAfter =
124
+ this.moduleSet.after.findRecordByNumber(recordNumber);
125
+ if (recordAfter === undefined) {
126
+ this.breakingChanges.push({
127
+ kind: "missing-record",
128
+ record: recordBefore,
129
+ recordNumber: recordNumber,
130
+ });
131
+ } else {
132
+ const record: BeforeAfter<RecordLocation> = {
133
+ before: recordBefore,
134
+ after: recordAfter,
135
+ };
136
+ this.checkRecord(
137
+ record,
138
+ map(record, (r) => ({
139
+ kind: "record",
140
+ recordName: r.record.name,
141
+ })),
142
+ );
143
+ }
144
+ }
145
+ }
146
+ }
147
+ return this.breakingChanges;
148
+ }
149
+
150
+ private checkMethod(method: BeforeAfter<Method>): void {
151
+ this.checkType(
152
+ map(method, (m) => m.requestType!),
153
+ map(method, (m) => ({ kind: "request-type", methodName: m.name })),
154
+ );
155
+ this.checkType(
156
+ map(method, (m) => m.responseType!),
157
+ map(method, (m) => ({ kind: "response-type", methodName: m.name })),
158
+ );
159
+ }
160
+
161
+ private checkRecord(
162
+ record: BeforeAfter<RecordLocation>,
163
+ recordExpression: BeforeAfter<Expression>,
164
+ ): void {
165
+ {
166
+ // Avoid infinite recursion when checking recursive records.
167
+ const recordKeys =
168
+ record.before.record.key + ":" + record.after.record.key;
169
+ if (this.seenRecordKeys.has(recordKeys)) {
170
+ return;
171
+ }
172
+ this.seenRecordKeys.add(recordKeys);
173
+ }
174
+ const recordType = map(record, (r) => r.record.recordType);
175
+ if (recordType.after !== recordType.before) {
176
+ this.pushBreakingChange({
177
+ kind: "record-kind-change",
178
+ record,
179
+ recordExpression,
180
+ recordType,
181
+ });
182
+ return;
183
+ }
184
+ const isStruct = recordType.before === "struct";
185
+ const { numSlotsInclRemovedNumbers } = record.before.record;
186
+ if (
187
+ record.after.record.numSlotsInclRemovedNumbers <
188
+ numSlotsInclRemovedNumbers
189
+ ) {
190
+ this.pushBreakingChange({
191
+ kind: "missing-slots",
192
+ record,
193
+ recordExpression,
194
+ missingRangeStart: record.after.record.numSlotsInclRemovedNumbers,
195
+ missingRangeEnd: numSlotsInclRemovedNumbers,
196
+ });
197
+ return;
198
+ }
199
+ const numberToFieldAfter = indexFields(record.after.record);
200
+ // Check that no removed number was reintroduced.
201
+ for (const removedNumber of record.before.record.removedNumbers) {
202
+ const fieldAfter = numberToFieldAfter.get(removedNumber);
203
+ if (fieldAfter) {
204
+ this.pushBreakingChange({
205
+ kind: "removed-number-reintroduced",
206
+ record,
207
+ recordExpression,
208
+ removedNumber: removedNumber,
209
+ reintroducedAs: fieldAfter.name,
210
+ });
211
+ }
212
+ }
213
+ for (const fieldBefore of record.before.record.fields) {
214
+ const fieldAfter = numberToFieldAfter.get(fieldBefore.number);
215
+ if (fieldAfter === undefined) {
216
+ continue;
217
+ }
218
+ if (fieldBefore.type && fieldAfter.type) {
219
+ this.checkType(
220
+ {
221
+ before: fieldBefore.type,
222
+ after: fieldAfter.type,
223
+ },
224
+ isStruct
225
+ ? {
226
+ before: {
227
+ kind: "property",
228
+ structExpression: recordExpression.before,
229
+ fieldName: fieldBefore.name,
230
+ },
231
+ after: {
232
+ kind: "property",
233
+ structExpression: recordExpression.after,
234
+ fieldName: fieldAfter.name,
235
+ },
236
+ }
237
+ : {
238
+ before: {
239
+ kind: "as-variant",
240
+ enumExpression: recordExpression.before,
241
+ variantName: fieldBefore.name,
242
+ },
243
+ after: {
244
+ kind: "as-variant",
245
+ enumExpression: recordExpression.after,
246
+ variantName: fieldAfter.name,
247
+ },
248
+ },
249
+ );
250
+ } else if (fieldBefore.type || fieldAfter.type) {
251
+ this.pushBreakingChange({
252
+ kind: "enum-variant-kind-change",
253
+ record,
254
+ enumEpression: recordExpression,
255
+ variantName: {
256
+ before: fieldBefore.name,
257
+ after: fieldAfter.name,
258
+ },
259
+ number: fieldBefore.number,
260
+ });
261
+ }
262
+ }
263
+ }
264
+
265
+ private checkType(
266
+ type: BeforeAfter<ResolvedType>,
267
+ expression: BeforeAfter<Expression>,
268
+ ): null {
269
+ const illegalTypeChange: BreakingChange = {
270
+ kind: "illegal-type-change",
271
+ expression: expression,
272
+ type: type,
273
+ };
274
+ switch (type.before.kind) {
275
+ case "array": {
276
+ if (type.after.kind === "array") {
277
+ return this.checkType(
278
+ {
279
+ before: type.before.item,
280
+ after: type.after.item,
281
+ },
282
+ map(expression, (e) => ({
283
+ kind: "item",
284
+ arrayExpression: e,
285
+ })),
286
+ );
287
+ } else {
288
+ this.pushBreakingChange(illegalTypeChange);
289
+ return null;
290
+ }
291
+ }
292
+ case "optional": {
293
+ if (type.after.kind === "optional") {
294
+ return this.checkType(
295
+ {
296
+ before: type.before.other,
297
+ after: type.after.other,
298
+ },
299
+ map(expression, (e) => ({
300
+ kind: "optional-value",
301
+ optionalExpression: e,
302
+ })),
303
+ );
304
+ } else {
305
+ this.pushBreakingChange(illegalTypeChange);
306
+ return null;
307
+ }
308
+ }
309
+ case "record": {
310
+ if (type.after.kind === "record") {
311
+ const record: BeforeAfter<RecordLocation> = {
312
+ before: this.moduleSet.before.recordMap.get(type.before.key)!,
313
+ after: this.moduleSet.after.recordMap.get(type.after.key)!,
314
+ };
315
+ this.checkRecord(record, expression);
316
+ return null;
317
+ } else {
318
+ this.pushBreakingChange(illegalTypeChange);
319
+ return null;
320
+ }
321
+ }
322
+ case "primitive": {
323
+ if (
324
+ type.after.kind !== "primitive" ||
325
+ !primitiveTypesAreCompatible({
326
+ before: type.before.primitive,
327
+ after: type.after.primitive,
328
+ })
329
+ ) {
330
+ this.pushBreakingChange(illegalTypeChange);
331
+ }
332
+ return null;
333
+ }
334
+ }
335
+ }
336
+
337
+ private pushBreakingChange(breakingChange: BreakingChange): void {
338
+ const token = getTokenForBreakingChange(breakingChange);
339
+ if (token === null) {
340
+ return;
341
+ }
342
+ let tokenBreakingChangeKinds = this.tokenToBreakingChangeKinds.get(token);
343
+ if (tokenBreakingChangeKinds === undefined) {
344
+ tokenBreakingChangeKinds = new Set();
345
+ this.tokenToBreakingChangeKinds.set(token, tokenBreakingChangeKinds);
346
+ }
347
+ if (tokenBreakingChangeKinds.has(breakingChange.kind)) {
348
+ return;
349
+ }
350
+ tokenBreakingChangeKinds.add(breakingChange.kind);
351
+ this.breakingChanges.push(breakingChange);
352
+ }
353
+
354
+ private readonly breakingChanges: BreakingChange[] = [];
355
+ // This map helps avoid reporting multiple variants of the same breaking
356
+ // change on the same token multiple times.
357
+ private readonly tokenToBreakingChangeKinds = new Map<
358
+ Token,
359
+ Set<BreakingChange["kind"]>
360
+ >();
361
+ // Helps avoid infinite recursion when checking recursive records.
362
+ private readonly seenRecordKeys = new Set<string>();
363
+ }
364
+
365
+ function getTokenForBreakingChange(
366
+ breakingChange: BreakingChange,
367
+ ): Token | null {
368
+ switch (breakingChange.kind) {
369
+ case "illegal-type-change": {
370
+ return getTokenForExpression(breakingChange.expression.after);
371
+ }
372
+ case "missing-slots":
373
+ case "record-kind-change":
374
+ case "enum-variant-kind-change": {
375
+ return breakingChange.record.after.record.name;
376
+ }
377
+ case "removed-number-reintroduced": {
378
+ return breakingChange.reintroducedAs;
379
+ }
380
+ case "missing-record":
381
+ case "missing-method": {
382
+ return null;
383
+ }
384
+ }
385
+ }
386
+
387
+ export function getTokenForExpression(expression: Expression): Token {
388
+ switch (expression.kind) {
389
+ case "item": {
390
+ return getTokenForExpression(expression.arrayExpression);
391
+ }
392
+ case "optional-value": {
393
+ return getTokenForExpression(expression.optionalExpression);
394
+ }
395
+ case "property": {
396
+ return expression.fieldName;
397
+ }
398
+ case "as-variant": {
399
+ return expression.variantName;
400
+ }
401
+ case "record": {
402
+ return expression.recordName;
403
+ }
404
+ case "request-type": {
405
+ return expression.methodName;
406
+ }
407
+ case "response-type": {
408
+ return expression.methodName;
409
+ }
410
+ }
411
+ }
412
+
413
+ function primitiveTypesAreCompatible(type: BeforeAfter<Primitive>): boolean {
414
+ switch (type.before) {
415
+ case "bool":
416
+ return (
417
+ type.after === "bool" ||
418
+ type.after === "int32" ||
419
+ type.after === "int64" ||
420
+ type.after === "uint64" ||
421
+ type.after === "float32" ||
422
+ type.after === "float64"
423
+ );
424
+ case "int32":
425
+ return (
426
+ type.after === "int32" ||
427
+ type.after === "int64" ||
428
+ type.after === "uint64" ||
429
+ type.after === "float32" ||
430
+ type.after === "float64"
431
+ );
432
+ case "int64":
433
+ return (
434
+ type.after === "int64" ||
435
+ type.after === "float32" ||
436
+ type.after === "float64"
437
+ );
438
+ case "uint64":
439
+ return (
440
+ type.after === "uint64" ||
441
+ type.after === "float32" ||
442
+ type.after === "float64"
443
+ );
444
+ case "float32":
445
+ case "float64":
446
+ return type.after === "float32" || type.after === "float64";
447
+ case "timestamp":
448
+ case "string":
449
+ case "bytes":
450
+ return type.after === type.before;
451
+ }
452
+ }
453
+
454
+ function indexFields(record: Record): Map<number, Field> {
455
+ const result = new Map<number, Field>();
456
+ for (const field of record.fields) {
457
+ result.set(field.number, field);
458
+ }
459
+ return result;
460
+ }
461
+
462
+ function map<T, U>(
463
+ beforeAfter: BeforeAfter<T>,
464
+ fn: (value: T) => U,
465
+ ): BeforeAfter<U> {
466
+ return {
467
+ before: fn(beforeAfter.before),
468
+ after: fn(beforeAfter.after),
469
+ };
470
+ }