skir 0.0.1 → 0.0.3
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/dist/casing.d.ts +1 -4
- package/dist/casing.d.ts.map +1 -1
- package/dist/casing.js +0 -29
- package/dist/casing.js.map +1 -1
- package/dist/casing.test.js +2 -13
- package/dist/casing.test.js.map +1 -1
- package/dist/command_line_parser.d.ts +3 -3
- package/dist/command_line_parser.d.ts.map +1 -1
- package/dist/command_line_parser.js +35 -38
- package/dist/command_line_parser.js.map +1 -1
- package/dist/command_line_parser.test.js +73 -78
- package/dist/command_line_parser.test.js.map +1 -1
- package/dist/compatibility_checker.d.ts +9 -3
- package/dist/compatibility_checker.d.ts.map +1 -1
- package/dist/compatibility_checker.js +17 -4
- package/dist/compatibility_checker.js.map +1 -1
- package/dist/compatibility_checker.test.js +55 -1
- package/dist/compatibility_checker.test.js.map +1 -1
- package/dist/compiler.js +34 -17
- package/dist/compiler.js.map +1 -1
- package/dist/config.d.ts +5 -35
- package/dist/config.d.ts.map +1 -1
- package/dist/definition_finder.d.ts +1 -1
- package/dist/definition_finder.d.ts.map +1 -1
- package/dist/doc_comment_parser.d.ts +3 -0
- package/dist/doc_comment_parser.d.ts.map +1 -0
- package/dist/doc_comment_parser.js +223 -0
- package/dist/doc_comment_parser.js.map +1 -0
- package/dist/doc_comment_parser.test.d.ts +2 -0
- package/dist/doc_comment_parser.test.d.ts.map +1 -0
- package/dist/doc_comment_parser.test.js +496 -0
- package/dist/doc_comment_parser.test.js.map +1 -0
- package/dist/error_renderer.d.ts +1 -1
- package/dist/error_renderer.d.ts.map +1 -1
- package/dist/error_renderer.js +84 -65
- package/dist/error_renderer.js.map +1 -1
- package/dist/formatter.d.ts +15 -2
- package/dist/formatter.d.ts.map +1 -1
- package/dist/formatter.js +191 -234
- package/dist/formatter.js.map +1 -1
- package/dist/formatter.test.js +322 -88
- package/dist/formatter.test.js.map +1 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -4
- package/dist/index.js.map +1 -1
- package/dist/language_server.js +1 -1
- package/dist/language_server.js.map +1 -1
- package/dist/literals.d.ts +1 -2
- package/dist/literals.d.ts.map +1 -1
- package/dist/literals.js +1 -12
- package/dist/literals.js.map +1 -1
- package/dist/literals.test.js +1 -4
- package/dist/literals.test.js.map +1 -1
- package/dist/module_set.d.ts +3 -7
- package/dist/module_set.d.ts.map +1 -1
- package/dist/module_set.js +205 -51
- package/dist/module_set.js.map +1 -1
- package/dist/module_set.test.js +595 -28
- package/dist/module_set.test.js.map +1 -1
- package/dist/parser.d.ts +3 -4
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +186 -92
- package/dist/parser.js.map +1 -1
- package/dist/parser.test.js +243 -15
- package/dist/parser.test.js.map +1 -1
- package/dist/project_initializer.d.ts +2 -0
- package/dist/project_initializer.d.ts.map +1 -0
- package/dist/project_initializer.js +30 -0
- package/dist/project_initializer.js.map +1 -0
- package/dist/snapshotter.d.ts +3 -0
- package/dist/snapshotter.d.ts.map +1 -1
- package/dist/snapshotter.js +43 -6
- package/dist/snapshotter.js.map +1 -1
- package/dist/tokenizer.d.ts +8 -2
- package/dist/tokenizer.d.ts.map +1 -1
- package/dist/tokenizer.js +26 -20
- package/dist/tokenizer.js.map +1 -1
- package/dist/tokenizer.test.js +285 -269
- package/dist/tokenizer.test.js.map +1 -1
- package/package.json +7 -5
- package/src/casing.ts +1 -36
- package/src/command_line_parser.ts +42 -48
- package/src/compatibility_checker.ts +29 -7
- package/src/compiler.ts +35 -18
- package/src/definition_finder.ts +1 -1
- package/src/doc_comment_parser.ts +251 -0
- package/src/error_renderer.ts +90 -66
- package/src/formatter.ts +249 -238
- package/src/index.ts +0 -6
- package/src/language_server.ts +8 -8
- package/src/literals.ts +5 -14
- package/src/module_set.ts +259 -79
- package/src/parser.ts +214 -98
- package/src/project_initializer.ts +39 -0
- package/src/snapshotter.ts +46 -5
- package/src/tokenizer.ts +47 -25
- package/dist/encoding.d.ts +0 -2
- package/dist/encoding.d.ts.map +0 -1
- package/dist/encoding.js +0 -38
- package/dist/encoding.js.map +0 -1
- package/dist/encoding.test.d.ts +0 -2
- package/dist/encoding.test.d.ts.map +0 -1
- package/dist/encoding.test.js +0 -23
- package/dist/encoding.test.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -14
- package/dist/index.test.js.map +0 -1
- package/dist/types.d.ts +0 -375
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/src/encoding.ts +0 -32
- package/src/types.ts +0 -518
package/src/casing.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ErrorSink, Token } from "skir-internal";
|
|
2
2
|
|
|
3
3
|
/** Registers an error if the given token does not match the expected casing. */
|
|
4
4
|
export function validate(
|
|
@@ -14,41 +14,6 @@ export function validate(
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export function convertCase(
|
|
18
|
-
text: string,
|
|
19
|
-
source: Casing,
|
|
20
|
-
target: Casing,
|
|
21
|
-
): string {
|
|
22
|
-
let words: string[];
|
|
23
|
-
switch (source) {
|
|
24
|
-
case "lowerCamel":
|
|
25
|
-
case "UpperCamel":
|
|
26
|
-
words = text.split(/(?=[A-Z])/).map((w) => w.toLowerCase());
|
|
27
|
-
break;
|
|
28
|
-
case "lower_underscore":
|
|
29
|
-
words = text.split("_");
|
|
30
|
-
break;
|
|
31
|
-
case "UPPER_UNDERSCORE":
|
|
32
|
-
words = text.split("_").map((w) => w.toLowerCase());
|
|
33
|
-
break;
|
|
34
|
-
}
|
|
35
|
-
switch (target) {
|
|
36
|
-
case "lowerCamel":
|
|
37
|
-
return words.map((w, i) => (i ? capitalize(w) : w)).join("");
|
|
38
|
-
case "lower_underscore":
|
|
39
|
-
return words.join("_");
|
|
40
|
-
case "UpperCamel":
|
|
41
|
-
return words.map(capitalize).join("");
|
|
42
|
-
case "UPPER_UNDERSCORE":
|
|
43
|
-
return words.map((w) => w.toUpperCase()).join("_");
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Returns a new string with the first letter of `name` capitalized. */
|
|
48
|
-
export function capitalize(name: string): string {
|
|
49
|
-
return name[0]!.toUpperCase() + name.slice(1);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
17
|
export function caseMatches(
|
|
53
18
|
name: string,
|
|
54
19
|
expected: "lower_underscore" | "UpperCamel" | "UPPER_UNDERSCORE",
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
export type ParsedArgs =
|
|
2
2
|
| {
|
|
3
3
|
kind: "gen";
|
|
4
|
+
subcommand?: "watch";
|
|
4
5
|
root?: string;
|
|
5
|
-
watch?: true;
|
|
6
6
|
}
|
|
7
7
|
| {
|
|
8
8
|
kind: "format";
|
|
9
|
+
subcommand?: "check";
|
|
9
10
|
root?: string;
|
|
10
|
-
check?: true;
|
|
11
11
|
}
|
|
12
12
|
| {
|
|
13
13
|
kind: "snapshot";
|
|
14
|
+
subcommand?: "check" | "view";
|
|
14
15
|
root?: string;
|
|
15
|
-
check?: true;
|
|
16
16
|
}
|
|
17
17
|
| {
|
|
18
18
|
kind: "init";
|
|
@@ -80,26 +80,30 @@ export function parseCommandLine(args: string[]): ParsedArgs {
|
|
|
80
80
|
const COMMAND_BASE = "npx skir";
|
|
81
81
|
|
|
82
82
|
const HELP_TEXT = `
|
|
83
|
-
Usage: ${COMMAND_BASE} <command> [options]
|
|
83
|
+
Usage: ${COMMAND_BASE} <command> [subcommand] [options]
|
|
84
84
|
|
|
85
85
|
Commands:
|
|
86
|
-
gen
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
gen [watch] Generate code from Skir source files to target languages
|
|
87
|
+
watch: Automatically regenerate when .skir files change
|
|
88
|
+
format [check] Format all .skir files in the specified directory
|
|
89
|
+
check: Fail if code is not properly formatted
|
|
90
|
+
snapshot [check|view] Manage .skir file snapshots for compatibility checking
|
|
91
|
+
check: Fail if there are breaking changes since last snapshot
|
|
92
|
+
view: Display the last snapshot
|
|
93
|
+
init Initialize a new Skir project with a minimal skir.yml file
|
|
94
|
+
help Display this help message
|
|
91
95
|
|
|
92
96
|
Options:
|
|
93
97
|
--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
98
|
|
|
97
99
|
Examples:
|
|
98
100
|
${COMMAND_BASE} gen
|
|
101
|
+
${COMMAND_BASE} gen watch
|
|
99
102
|
${COMMAND_BASE} format --root path/to/root/dir
|
|
100
|
-
${COMMAND_BASE} format -r path/to/root/dir
|
|
101
|
-
${COMMAND_BASE}
|
|
102
|
-
${COMMAND_BASE} snapshot
|
|
103
|
+
${COMMAND_BASE} format check -r path/to/root/dir
|
|
104
|
+
${COMMAND_BASE} snapshot
|
|
105
|
+
${COMMAND_BASE} snapshot check
|
|
106
|
+
${COMMAND_BASE} snapshot view --root path/to/root/dir
|
|
103
107
|
`;
|
|
104
108
|
|
|
105
109
|
export class CommandLineParseError extends Error {
|
|
@@ -111,8 +115,7 @@ export class CommandLineParseError extends Error {
|
|
|
111
115
|
|
|
112
116
|
type ParsedOptions = {
|
|
113
117
|
root?: string;
|
|
114
|
-
|
|
115
|
-
check?: boolean;
|
|
118
|
+
subcommand?: string;
|
|
116
119
|
unknown: string[];
|
|
117
120
|
};
|
|
118
121
|
|
|
@@ -136,24 +139,14 @@ function parseOptions(args: string[]): ParsedOptions {
|
|
|
136
139
|
}
|
|
137
140
|
options.root = args[i + 1];
|
|
138
141
|
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
142
|
} else if (arg.startsWith("-")) {
|
|
154
143
|
options.unknown.push(arg);
|
|
155
144
|
} else {
|
|
156
|
-
|
|
145
|
+
// Positional argument - treat as subcommand
|
|
146
|
+
if (options.subcommand !== undefined) {
|
|
147
|
+
throw new CommandLineParseError(`Unexpected argument: ${arg}`);
|
|
148
|
+
}
|
|
149
|
+
options.subcommand = arg;
|
|
157
150
|
}
|
|
158
151
|
}
|
|
159
152
|
|
|
@@ -163,63 +156,64 @@ function parseOptions(args: string[]): ParsedOptions {
|
|
|
163
156
|
function buildGenCommand(options: ParsedOptions): ParsedArgs {
|
|
164
157
|
validateNoUnknownOptions(options, "gen");
|
|
165
158
|
|
|
166
|
-
if (options.
|
|
159
|
+
if (options.subcommand !== undefined && options.subcommand !== "watch") {
|
|
167
160
|
throw new CommandLineParseError(
|
|
168
|
-
|
|
161
|
+
`Unknown subcommand for 'gen': ${options.subcommand}`,
|
|
169
162
|
);
|
|
170
163
|
}
|
|
171
164
|
|
|
172
165
|
return {
|
|
173
166
|
kind: "gen",
|
|
174
167
|
root: options.root,
|
|
175
|
-
|
|
168
|
+
subcommand: options.subcommand === "watch" ? "watch" : undefined,
|
|
176
169
|
};
|
|
177
170
|
}
|
|
178
171
|
|
|
179
172
|
function buildFormatCommand(options: ParsedOptions): ParsedArgs {
|
|
180
173
|
validateNoUnknownOptions(options, "format");
|
|
181
174
|
|
|
182
|
-
if (options.
|
|
175
|
+
if (options.subcommand !== undefined && options.subcommand !== "check") {
|
|
183
176
|
throw new CommandLineParseError(
|
|
184
|
-
|
|
177
|
+
`Unknown subcommand for 'format': ${options.subcommand}`,
|
|
185
178
|
);
|
|
186
179
|
}
|
|
187
180
|
|
|
188
181
|
return {
|
|
189
182
|
kind: "format",
|
|
190
183
|
root: options.root,
|
|
191
|
-
|
|
184
|
+
subcommand: options.subcommand === "check" ? "check" : undefined,
|
|
192
185
|
};
|
|
193
186
|
}
|
|
194
187
|
|
|
195
188
|
function buildSnapshotCommand(options: ParsedOptions): ParsedArgs {
|
|
196
189
|
validateNoUnknownOptions(options, "snapshot");
|
|
197
190
|
|
|
198
|
-
if (
|
|
191
|
+
if (
|
|
192
|
+
options.subcommand !== undefined &&
|
|
193
|
+
options.subcommand !== "check" &&
|
|
194
|
+
options.subcommand !== "view"
|
|
195
|
+
) {
|
|
199
196
|
throw new CommandLineParseError(
|
|
200
|
-
|
|
197
|
+
`Unknown subcommand for 'snapshot': ${options.subcommand}`,
|
|
201
198
|
);
|
|
202
199
|
}
|
|
203
200
|
|
|
204
201
|
return {
|
|
205
202
|
kind: "snapshot",
|
|
206
203
|
root: options.root,
|
|
207
|
-
|
|
204
|
+
subcommand:
|
|
205
|
+
options.subcommand === "check" || options.subcommand === "view"
|
|
206
|
+
? options.subcommand
|
|
207
|
+
: undefined,
|
|
208
208
|
};
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
function buildInitCommand(options: ParsedOptions): ParsedArgs {
|
|
212
212
|
validateNoUnknownOptions(options, "init");
|
|
213
213
|
|
|
214
|
-
if (options.
|
|
215
|
-
throw new CommandLineParseError(
|
|
216
|
-
"Option --watch is not valid for the 'init' command",
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (options.check) {
|
|
214
|
+
if (options.subcommand !== undefined) {
|
|
221
215
|
throw new CommandLineParseError(
|
|
222
|
-
|
|
216
|
+
`Unknown subcommand for 'init': ${options.subcommand}`,
|
|
223
217
|
);
|
|
224
218
|
}
|
|
225
219
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { ModuleSet } from "./module_set.js";
|
|
2
1
|
import type {
|
|
3
2
|
Field,
|
|
4
3
|
Method,
|
|
@@ -7,7 +6,8 @@ import type {
|
|
|
7
6
|
RecordLocation,
|
|
8
7
|
ResolvedType,
|
|
9
8
|
Token,
|
|
10
|
-
} from "
|
|
9
|
+
} from "skir-internal";
|
|
10
|
+
import { ModuleSet } from "./module_set.js";
|
|
11
11
|
|
|
12
12
|
export interface BeforeAfter<T> {
|
|
13
13
|
before: T;
|
|
@@ -50,7 +50,14 @@ export type BreakingChange =
|
|
|
50
50
|
reintroducedAs: Token;
|
|
51
51
|
}
|
|
52
52
|
| {
|
|
53
|
-
kind: "
|
|
53
|
+
kind: "missing-variant";
|
|
54
|
+
record: BeforeAfter<RecordLocation>;
|
|
55
|
+
enumEpression: BeforeAfter<Expression>;
|
|
56
|
+
variantName: Token;
|
|
57
|
+
number: number;
|
|
58
|
+
}
|
|
59
|
+
| {
|
|
60
|
+
kind: "variant-kind-change";
|
|
54
61
|
record: BeforeAfter<RecordLocation>;
|
|
55
62
|
enumEpression: BeforeAfter<Expression>;
|
|
56
63
|
variantName: BeforeAfter<Token>;
|
|
@@ -210,9 +217,21 @@ class BackwardCompatibilityChecker {
|
|
|
210
217
|
});
|
|
211
218
|
}
|
|
212
219
|
}
|
|
220
|
+
const removedNumbersAfter = new Set<number>(
|
|
221
|
+
record.after.record.removedNumbers,
|
|
222
|
+
);
|
|
213
223
|
for (const fieldBefore of record.before.record.fields) {
|
|
214
224
|
const fieldAfter = numberToFieldAfter.get(fieldBefore.number);
|
|
215
225
|
if (fieldAfter === undefined) {
|
|
226
|
+
if (!removedNumbersAfter.has(fieldBefore.number)) {
|
|
227
|
+
this.pushBreakingChange({
|
|
228
|
+
kind: "missing-variant",
|
|
229
|
+
record,
|
|
230
|
+
enumEpression: recordExpression,
|
|
231
|
+
variantName: fieldBefore.name,
|
|
232
|
+
number: fieldBefore.number,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
216
235
|
continue;
|
|
217
236
|
}
|
|
218
237
|
if (fieldBefore.type && fieldAfter.type) {
|
|
@@ -249,7 +268,7 @@ class BackwardCompatibilityChecker {
|
|
|
249
268
|
);
|
|
250
269
|
} else if (fieldBefore.type || fieldAfter.type) {
|
|
251
270
|
this.pushBreakingChange({
|
|
252
|
-
kind: "
|
|
271
|
+
kind: "variant-kind-change",
|
|
253
272
|
record,
|
|
254
273
|
enumEpression: recordExpression,
|
|
255
274
|
variantName: {
|
|
@@ -362,7 +381,7 @@ class BackwardCompatibilityChecker {
|
|
|
362
381
|
private readonly seenRecordKeys = new Set<string>();
|
|
363
382
|
}
|
|
364
383
|
|
|
365
|
-
function getTokenForBreakingChange(
|
|
384
|
+
export function getTokenForBreakingChange(
|
|
366
385
|
breakingChange: BreakingChange,
|
|
367
386
|
): Token | null {
|
|
368
387
|
switch (breakingChange.kind) {
|
|
@@ -371,7 +390,10 @@ function getTokenForBreakingChange(
|
|
|
371
390
|
}
|
|
372
391
|
case "missing-slots":
|
|
373
392
|
case "record-kind-change":
|
|
374
|
-
case "
|
|
393
|
+
case "variant-kind-change": {
|
|
394
|
+
return breakingChange.record.after.record.name;
|
|
395
|
+
}
|
|
396
|
+
case "missing-variant": {
|
|
375
397
|
return breakingChange.record.after.record.name;
|
|
376
398
|
}
|
|
377
399
|
case "removed-number-reintroduced": {
|
|
@@ -384,7 +406,7 @@ function getTokenForBreakingChange(
|
|
|
384
406
|
}
|
|
385
407
|
}
|
|
386
408
|
|
|
387
|
-
|
|
409
|
+
function getTokenForExpression(expression: Expression): Token {
|
|
388
410
|
switch (expression.kind) {
|
|
389
411
|
case "item": {
|
|
390
412
|
return getTokenForExpression(expression.arrayExpression);
|
package/src/compiler.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import * as fs from "fs/promises";
|
|
3
3
|
import { glob } from "glob";
|
|
4
4
|
import * as paths from "path";
|
|
5
|
+
import type { CodeGenerator } from "skir-internal";
|
|
5
6
|
import Watcher from "watcher";
|
|
6
7
|
import * as yaml from "yaml";
|
|
7
8
|
import { fromZodError } from "zod-validation-error";
|
|
@@ -17,9 +18,9 @@ import { formatModule } from "./formatter.js";
|
|
|
17
18
|
import { REAL_FILE_SYSTEM } from "./io.js";
|
|
18
19
|
import { collectModules } from "./module_collector.js";
|
|
19
20
|
import { ModuleSet } from "./module_set.js";
|
|
20
|
-
import {
|
|
21
|
+
import { initializeProject } from "./project_initializer.js";
|
|
22
|
+
import { takeSnapshot, viewSnapshot } from "./snapshotter.js";
|
|
21
23
|
import { tokenizeModule } from "./tokenizer.js";
|
|
22
|
-
import type { CodeGenerator } from "./types.js";
|
|
23
24
|
|
|
24
25
|
interface GeneratorBundle<Config = unknown> {
|
|
25
26
|
generator: CodeGenerator<Config>;
|
|
@@ -136,6 +137,9 @@ class WatchModeMainLoop {
|
|
|
136
137
|
renderErrors(errors);
|
|
137
138
|
return false;
|
|
138
139
|
} else {
|
|
140
|
+
if (moduleSet.recordMap.size <= 0) {
|
|
141
|
+
console.error(makeRed("No skir modules found in source directory"));
|
|
142
|
+
}
|
|
139
143
|
await this.doGenerate(moduleSet);
|
|
140
144
|
if (this.watchModeOn) {
|
|
141
145
|
const date = new Date().toLocaleTimeString("en-US");
|
|
@@ -291,12 +295,12 @@ async function format(root: string, mode: "fix" | "check"): Promise<void> {
|
|
|
291
295
|
if (unformattedCode === undefined) {
|
|
292
296
|
throw new Error(`Cannot read ${skirFile.fullpath()}`);
|
|
293
297
|
}
|
|
294
|
-
const tokens = tokenizeModule(unformattedCode, ""
|
|
298
|
+
const tokens = tokenizeModule(unformattedCode, "");
|
|
295
299
|
if (tokens.errors.length) {
|
|
296
300
|
renderErrors(tokens.errors);
|
|
297
301
|
process.exit(1);
|
|
298
302
|
}
|
|
299
|
-
const formattedCode = formatModule(tokens.result);
|
|
303
|
+
const formattedCode = formatModule(tokens.result).newSourceCode;
|
|
300
304
|
pathToFormatResult.set(skirFile.fullpath(), {
|
|
301
305
|
formattedCode: formattedCode,
|
|
302
306
|
alreadyFormatted: formattedCode === unformattedCode,
|
|
@@ -347,6 +351,17 @@ async function main(): Promise<void> {
|
|
|
347
351
|
process.exit(1);
|
|
348
352
|
}
|
|
349
353
|
|
|
354
|
+
switch (args.kind) {
|
|
355
|
+
case "init": {
|
|
356
|
+
initializeProject(root!);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
case "help":
|
|
360
|
+
case "error": {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
350
365
|
// Use an absolute path to make error messages more helpful.
|
|
351
366
|
const skirConfigPath = paths.resolve(paths.join(root!, "skir.yml"));
|
|
352
367
|
const skirConfigContents = REAL_FILE_SYSTEM.readTextFile(skirConfigPath);
|
|
@@ -375,7 +390,7 @@ async function main(): Promise<void> {
|
|
|
375
390
|
switch (args.kind) {
|
|
376
391
|
case "format": {
|
|
377
392
|
// Check or fix the formatting to the .skir files in the source directory.
|
|
378
|
-
await format(srcDir, args.check ? "check" : "fix");
|
|
393
|
+
await format(srcDir, args.subcommand === "check" ? "check" : "fix");
|
|
379
394
|
break;
|
|
380
395
|
}
|
|
381
396
|
case "gen": {
|
|
@@ -399,7 +414,7 @@ async function main(): Promise<void> {
|
|
|
399
414
|
process.exit(1);
|
|
400
415
|
}
|
|
401
416
|
}
|
|
402
|
-
const watch =
|
|
417
|
+
const watch = args.subcommand === "watch";
|
|
403
418
|
const watchModeMainLoop = new WatchModeMainLoop(
|
|
404
419
|
srcDir,
|
|
405
420
|
generatorBundles,
|
|
@@ -413,21 +428,23 @@ async function main(): Promise<void> {
|
|
|
413
428
|
}
|
|
414
429
|
break;
|
|
415
430
|
}
|
|
416
|
-
case "init": {
|
|
417
|
-
// TODO
|
|
418
|
-
break;
|
|
419
|
-
}
|
|
420
431
|
case "snapshot": {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
432
|
+
if (args.subcommand === "view") {
|
|
433
|
+
viewSnapshot({
|
|
434
|
+
rootDir: root!,
|
|
435
|
+
});
|
|
436
|
+
} else {
|
|
437
|
+
takeSnapshot({
|
|
438
|
+
rootDir: root!,
|
|
439
|
+
srcDir: srcDir,
|
|
440
|
+
check: args.subcommand === "check",
|
|
441
|
+
});
|
|
442
|
+
}
|
|
426
443
|
break;
|
|
427
444
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
445
|
+
default: {
|
|
446
|
+
const _: never = args;
|
|
447
|
+
throw new TypeError(_);
|
|
431
448
|
}
|
|
432
449
|
}
|
|
433
450
|
}
|
package/src/definition_finder.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Utilities to help implement the jump-to-definition functionality for skir
|
|
3
3
|
* files in IDEs.
|
|
4
4
|
*/
|
|
5
|
-
import type { Declaration, Module, ResolvedType, Token } from "
|
|
5
|
+
import type { Declaration, Module, ResolvedType, Token } from "skir-internal";
|
|
6
6
|
|
|
7
7
|
export interface DefinitionMatch {
|
|
8
8
|
modulePath: string;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { assert } from "node:console";
|
|
2
|
+
import type {
|
|
3
|
+
Doc,
|
|
4
|
+
DocPiece,
|
|
5
|
+
DocReference,
|
|
6
|
+
Result,
|
|
7
|
+
SkirError,
|
|
8
|
+
Token,
|
|
9
|
+
} from "skir-internal";
|
|
10
|
+
|
|
11
|
+
export function parseDocComments(docComments: readonly Token[]): Result<Doc> {
|
|
12
|
+
const parser = new DocCommentsParser(docComments);
|
|
13
|
+
return parser.parse();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class DocCommentsParser {
|
|
17
|
+
private readonly pieces: DocPiece[] = [];
|
|
18
|
+
private readonly errors: SkirError[] = [];
|
|
19
|
+
private currentText = "";
|
|
20
|
+
private docCommentIndex = -1;
|
|
21
|
+
private charIndex = -1;
|
|
22
|
+
private contentOffset = -1;
|
|
23
|
+
|
|
24
|
+
constructor(private readonly docComments: readonly Token[]) {}
|
|
25
|
+
|
|
26
|
+
parse(): Result<Doc> {
|
|
27
|
+
while (this.nextDocComment()) {
|
|
28
|
+
this.parseCurrentDocComment();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Add any remaining text
|
|
32
|
+
if (this.currentText.length > 0) {
|
|
33
|
+
this.pieces.push({ kind: "text", text: this.currentText });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const text = this.docComments
|
|
37
|
+
.map((c) => c.text.slice(c.text.startsWith("/// ") ? 4 : 3))
|
|
38
|
+
.join("\n");
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
result: {
|
|
42
|
+
text: text,
|
|
43
|
+
pieces: this.pieces,
|
|
44
|
+
},
|
|
45
|
+
errors: this.errors,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private parseCurrentDocComment(): void {
|
|
50
|
+
// Matches unescaped [ or ], OR escaped [[ or ]]
|
|
51
|
+
const specialCharRegex = /\[\[|\]\]|\[|\]/g;
|
|
52
|
+
|
|
53
|
+
while (this.charIndex < this.content.length) {
|
|
54
|
+
// Find next special character or escaped bracket
|
|
55
|
+
specialCharRegex.lastIndex = this.charIndex;
|
|
56
|
+
const match = specialCharRegex.exec(this.content);
|
|
57
|
+
|
|
58
|
+
if (!match) {
|
|
59
|
+
// No more special characters, add rest as text
|
|
60
|
+
this.currentText += this.content.slice(this.charIndex);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Add text before the special character
|
|
65
|
+
if (match.index > this.charIndex) {
|
|
66
|
+
this.currentText += this.content.slice(this.charIndex, match.index);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const matched = match[0];
|
|
70
|
+
this.charIndex = match.index;
|
|
71
|
+
|
|
72
|
+
if (matched === "[[") {
|
|
73
|
+
// Escaped left bracket
|
|
74
|
+
this.currentText += "[";
|
|
75
|
+
this.charIndex += 2;
|
|
76
|
+
} else if (matched === "]]") {
|
|
77
|
+
// Escaped right bracket
|
|
78
|
+
this.currentText += "]";
|
|
79
|
+
this.charIndex += 2;
|
|
80
|
+
} else if (matched === "[") {
|
|
81
|
+
// Start of a reference - save current text if any
|
|
82
|
+
if (this.currentText.length > 0) {
|
|
83
|
+
this.pieces.push({ kind: "text", text: this.currentText });
|
|
84
|
+
this.currentText = "";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Parse the reference
|
|
88
|
+
const reference = this.parseReference();
|
|
89
|
+
this.pieces.push(reference);
|
|
90
|
+
} else if (matched === "]") {
|
|
91
|
+
// Unmatched right bracket - treat as text
|
|
92
|
+
this.currentText += matched;
|
|
93
|
+
this.charIndex++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Add newline between comment lines (except after the last line)
|
|
98
|
+
if (this.docCommentIndex < this.docComments.length - 1) {
|
|
99
|
+
this.currentText += "\n";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private parseReference(): DocReference {
|
|
104
|
+
const { content, docComment } = this;
|
|
105
|
+
|
|
106
|
+
const leftBracketCharIndex = this.charIndex;
|
|
107
|
+
const startPosition = docComment.position + leftBracketCharIndex;
|
|
108
|
+
|
|
109
|
+
const rightBracketCharIndex = content.indexOf("]", leftBracketCharIndex);
|
|
110
|
+
|
|
111
|
+
// End position: right after the closing bracket or at end of the line if
|
|
112
|
+
// not found.
|
|
113
|
+
const endCharIndex =
|
|
114
|
+
rightBracketCharIndex < 0 ? content.length : rightBracketCharIndex + 1;
|
|
115
|
+
|
|
116
|
+
const referenceText = content.slice(leftBracketCharIndex, endCharIndex);
|
|
117
|
+
const referenceRange: Token = {
|
|
118
|
+
text: referenceText,
|
|
119
|
+
originalText: referenceText,
|
|
120
|
+
position: startPosition,
|
|
121
|
+
line: docComment.line,
|
|
122
|
+
colNumber: startPosition - docComment.line.position,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
let hasError = false;
|
|
126
|
+
if (rightBracketCharIndex < 0) {
|
|
127
|
+
hasError = true;
|
|
128
|
+
this.errors.push({
|
|
129
|
+
token: referenceRange,
|
|
130
|
+
message: "Unterminated reference",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Move past the left bracket
|
|
135
|
+
this.charIndex++;
|
|
136
|
+
|
|
137
|
+
const wordRegex = /[a-zA-Z][_a-zA-Z0-9]*/g;
|
|
138
|
+
|
|
139
|
+
const tokens: Token[] = [];
|
|
140
|
+
while (this.charIndex < endCharIndex) {
|
|
141
|
+
const char = content[this.charIndex]!;
|
|
142
|
+
const position = docComment.position + this.charIndex;
|
|
143
|
+
|
|
144
|
+
const makeToken = (text: string): Token => ({
|
|
145
|
+
text: text,
|
|
146
|
+
originalText: text,
|
|
147
|
+
position: position,
|
|
148
|
+
line: docComment.line,
|
|
149
|
+
colNumber: position - docComment.line.position,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (char === ".") {
|
|
153
|
+
// Dot token
|
|
154
|
+
tokens.push(makeToken("."));
|
|
155
|
+
this.charIndex++;
|
|
156
|
+
} else if (/^[a-zA-Z]/.test(char)) {
|
|
157
|
+
// Start of a word token - use regex to match the whole word
|
|
158
|
+
wordRegex.lastIndex = this.charIndex;
|
|
159
|
+
const match = wordRegex.exec(content);
|
|
160
|
+
const word = match![0];
|
|
161
|
+
tokens.push(makeToken(word));
|
|
162
|
+
this.charIndex += word.length;
|
|
163
|
+
} else if (char === "]") {
|
|
164
|
+
// Reached the end of the reference
|
|
165
|
+
tokens.push(makeToken("]"));
|
|
166
|
+
this.charIndex++;
|
|
167
|
+
} else {
|
|
168
|
+
// Invalid character in reference (including whitespace)
|
|
169
|
+
const column = this.docComment.colNumber + this.charIndex;
|
|
170
|
+
hasError = true;
|
|
171
|
+
this.errors.push({
|
|
172
|
+
token: referenceRange,
|
|
173
|
+
message: `Invalid character in reference at column ${column + 1}`,
|
|
174
|
+
});
|
|
175
|
+
// Exit loop
|
|
176
|
+
this.charIndex = endCharIndex;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const nameChain = hasError ? [] : this.parseNameChain(tokens);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
kind: "reference",
|
|
184
|
+
nameChain: nameChain,
|
|
185
|
+
absolute: tokens[0]?.text === ".",
|
|
186
|
+
referee: undefined,
|
|
187
|
+
docComment: this.docComment,
|
|
188
|
+
referenceRange: referenceRange,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private parseNameChain(tokens: readonly Token[]): Token[] {
|
|
193
|
+
const nameChain: Token[] = [];
|
|
194
|
+
let expect: "identifier" | "identifier or '.'" | "'.' or ']'" =
|
|
195
|
+
"identifier or '.'";
|
|
196
|
+
for (const token of tokens) {
|
|
197
|
+
let expected: boolean;
|
|
198
|
+
if (/^[a-zA-Z]/.test(token.text)) {
|
|
199
|
+
expected = expect === "identifier or '.'" || expect === "identifier";
|
|
200
|
+
expect = "'.' or ']'";
|
|
201
|
+
nameChain.push(token);
|
|
202
|
+
} else if (token.text === ".") {
|
|
203
|
+
expected = expect === "identifier or '.'" || expect === "'.' or ']'";
|
|
204
|
+
expect = "identifier";
|
|
205
|
+
} else {
|
|
206
|
+
assert(token.text === "]");
|
|
207
|
+
expected = expect === "'.' or ']'";
|
|
208
|
+
}
|
|
209
|
+
if (!expected) {
|
|
210
|
+
this.errors.push({
|
|
211
|
+
token: token,
|
|
212
|
+
expected: expect,
|
|
213
|
+
});
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
if (token.text === "]") {
|
|
217
|
+
return nameChain;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// An error has already been pushed to signify the unterminated reference.
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/// The current doc comment being parsed.
|
|
225
|
+
private get docComment(): Token {
|
|
226
|
+
return this.docComments[this.docCommentIndex]!;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// The text of the current doc comment being parsed.
|
|
230
|
+
private get content(): string {
|
|
231
|
+
return this.docComment.text;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private nextDocComment(): boolean {
|
|
235
|
+
if (this.docCommentIndex < this.docComments.length - 1) {
|
|
236
|
+
this.docCommentIndex++;
|
|
237
|
+
const { content } = this;
|
|
238
|
+
if (content.startsWith("/// ")) {
|
|
239
|
+
this.contentOffset = 4;
|
|
240
|
+
} else if (content.startsWith("///")) {
|
|
241
|
+
this.contentOffset = 3;
|
|
242
|
+
} else {
|
|
243
|
+
throw new Error("Expected doc comment to start with ///");
|
|
244
|
+
}
|
|
245
|
+
this.charIndex = this.contentOffset;
|
|
246
|
+
return true;
|
|
247
|
+
} else {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|