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.
- package/README.md +447 -0
- package/dist/casing.d.ts +8 -0
- package/dist/casing.d.ts.map +1 -0
- package/dist/casing.js +49 -0
- package/dist/casing.js.map +1 -0
- package/dist/casing.test.d.ts +2 -0
- package/dist/casing.test.d.ts.map +1 -0
- package/dist/casing.test.js +134 -0
- package/dist/casing.test.js.map +1 -0
- package/dist/command_line_parser.d.ts +33 -0
- package/dist/command_line_parser.d.ts.map +1 -0
- package/dist/command_line_parser.js +171 -0
- package/dist/command_line_parser.js.map +1 -0
- package/dist/command_line_parser.test.d.ts +2 -0
- package/dist/command_line_parser.test.d.ts.map +1 -0
- package/dist/command_line_parser.test.js +302 -0
- package/dist/command_line_parser.test.js.map +1 -0
- package/dist/compatibility_checker.d.ts +68 -0
- package/dist/compatibility_checker.d.ts.map +1 -0
- package/dist/compatibility_checker.js +328 -0
- package/dist/compatibility_checker.js.map +1 -0
- package/dist/compatibility_checker.test.d.ts +2 -0
- package/dist/compatibility_checker.test.d.ts.map +1 -0
- package/dist/compatibility_checker.test.js +528 -0
- package/dist/compatibility_checker.test.js.map +1 -0
- package/dist/compiler.d.ts +3 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +358 -0
- package/dist/compiler.js.map +1 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +23 -0
- package/dist/config.js.map +1 -0
- package/dist/definition_finder.d.ts +12 -0
- package/dist/definition_finder.d.ts.map +1 -0
- package/dist/definition_finder.js +180 -0
- package/dist/definition_finder.js.map +1 -0
- package/dist/definition_finder.test.d.ts +2 -0
- package/dist/definition_finder.test.d.ts.map +1 -0
- package/dist/definition_finder.test.js +164 -0
- package/dist/definition_finder.test.js.map +1 -0
- package/dist/encoding.d.ts +2 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +38 -0
- package/dist/encoding.js.map +1 -0
- package/dist/encoding.test.d.ts +2 -0
- package/dist/encoding.test.d.ts.map +1 -0
- package/dist/encoding.test.js +23 -0
- package/dist/encoding.test.js.map +1 -0
- package/dist/error_renderer.d.ts +10 -0
- package/dist/error_renderer.d.ts.map +1 -0
- package/dist/error_renderer.js +247 -0
- package/dist/error_renderer.js.map +1 -0
- package/dist/formatter.d.ts +3 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +263 -0
- package/dist/formatter.js.map +1 -0
- package/dist/formatter.test.d.ts +2 -0
- package/dist/formatter.test.d.ts.map +1 -0
- package/dist/formatter.test.js +156 -0
- package/dist/formatter.test.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +14 -0
- package/dist/index.test.js.map +1 -0
- package/dist/io.d.ts +13 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/io.js +22 -0
- package/dist/io.js.map +1 -0
- package/dist/language_server.d.ts +15 -0
- package/dist/language_server.d.ts.map +1 -0
- package/dist/language_server.js +248 -0
- package/dist/language_server.js.map +1 -0
- package/dist/literals.d.ts +13 -0
- package/dist/literals.d.ts.map +1 -0
- package/dist/literals.js +100 -0
- package/dist/literals.js.map +1 -0
- package/dist/literals.test.d.ts +2 -0
- package/dist/literals.test.d.ts.map +1 -0
- package/dist/literals.test.js +149 -0
- package/dist/literals.test.js.map +1 -0
- package/dist/module_collector.d.ts +3 -0
- package/dist/module_collector.d.ts.map +1 -0
- package/dist/module_collector.js +22 -0
- package/dist/module_collector.js.map +1 -0
- package/dist/module_set.d.ts +44 -0
- package/dist/module_set.d.ts.map +1 -0
- package/dist/module_set.js +1025 -0
- package/dist/module_set.js.map +1 -0
- package/dist/module_set.test.d.ts +2 -0
- package/dist/module_set.test.d.ts.map +1 -0
- package/dist/module_set.test.js +1330 -0
- package/dist/module_set.test.js.map +1 -0
- package/dist/parser.d.ts +6 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +971 -0
- package/dist/parser.js.map +1 -0
- package/dist/parser.test.d.ts +2 -0
- package/dist/parser.test.d.ts.map +1 -0
- package/dist/parser.test.js +1366 -0
- package/dist/parser.test.js.map +1 -0
- package/dist/snapshotter.d.ts +6 -0
- package/dist/snapshotter.d.ts.map +1 -0
- package/dist/snapshotter.js +107 -0
- package/dist/snapshotter.js.map +1 -0
- package/dist/tokenizer.d.ts +4 -0
- package/dist/tokenizer.d.ts.map +1 -0
- package/dist/tokenizer.js +192 -0
- package/dist/tokenizer.js.map +1 -0
- package/dist/tokenizer.test.d.ts +2 -0
- package/dist/tokenizer.test.d.ts.map +1 -0
- package/dist/tokenizer.test.js +425 -0
- package/dist/tokenizer.test.js.map +1 -0
- package/dist/types.d.ts +375 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
- package/src/casing.ts +64 -0
- package/src/command_line_parser.ts +249 -0
- package/src/compatibility_checker.ts +470 -0
- package/src/compiler.ts +435 -0
- package/src/config.ts +28 -0
- package/src/definition_finder.ts +221 -0
- package/src/encoding.ts +32 -0
- package/src/error_renderer.ts +278 -0
- package/src/formatter.ts +274 -0
- package/src/index.ts +6 -0
- package/src/io.ts +33 -0
- package/src/language_server.ts +301 -0
- package/src/literals.ts +120 -0
- package/src/module_collector.ts +22 -0
- package/src/module_set.ts +1175 -0
- package/src/parser.ts +1122 -0
- package/src/snapshotter.ts +136 -0
- package/src/tokenizer.ts +216 -0
- 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
|
+
}
|