skir 1.0.5 → 1.0.7
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/compatibility_checker.d.ts +1 -1
- package/dist/compatibility_checker.d.ts.map +1 -1
- package/dist/compatibility_checker.js +3 -3
- package/dist/compatibility_checker.js.map +1 -1
- package/dist/compatibility_checker.test.js +2 -2
- package/dist/compatibility_checker.test.js.map +1 -1
- package/dist/compiler.js +9 -0
- package/dist/compiler.js.map +1 -1
- package/dist/formatter.d.ts +3 -1
- package/dist/formatter.d.ts.map +1 -1
- package/dist/formatter.js +11 -3
- package/dist/formatter.js.map +1 -1
- package/dist/formatter.test.js +23 -3
- package/dist/formatter.test.js.map +1 -1
- package/dist/module_set.js +1 -1
- package/dist/module_set.js.map +1 -1
- package/dist/parser.d.ts +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +15 -10
- package/dist/parser.js.map +1 -1
- package/dist/parser.test.js +68 -2
- package/dist/parser.test.js.map +1 -1
- package/dist/snapshotter.d.ts.map +1 -1
- package/dist/snapshotter.js +74 -2
- package/dist/snapshotter.js.map +1 -1
- package/package.json +1 -1
- package/src/compatibility_checker.ts +3 -3
- package/src/compiler.ts +9 -0
- package/src/formatter.ts +24 -3
- package/src/module_set.ts +1 -1
- package/src/parser.ts +17 -9
- package/src/snapshotter.ts +99 -3
package/src/formatter.ts
CHANGED
|
@@ -14,11 +14,17 @@ export interface TextEdit {
|
|
|
14
14
|
readonly newText: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** A function which returns a random number between 0 and 1. */
|
|
18
|
+
export type RandomGenerator = () => number;
|
|
19
|
+
|
|
17
20
|
/**
|
|
18
21
|
* Formats the given module and returns the new source code.
|
|
19
22
|
* Preserves token ordering.
|
|
20
23
|
*/
|
|
21
|
-
export function formatModule(
|
|
24
|
+
export function formatModule(
|
|
25
|
+
moduleTokens: ModuleTokens,
|
|
26
|
+
randomGenerator: RandomGenerator = Math.random,
|
|
27
|
+
): FormattedModule {
|
|
22
28
|
const tokens = moduleTokens.tokensWithComments;
|
|
23
29
|
|
|
24
30
|
const context: Context = {
|
|
@@ -28,9 +34,14 @@ export function formatModule(moduleTokens: ModuleTokens): FormattedModule {
|
|
|
28
34
|
|
|
29
35
|
let newSourceCode = "";
|
|
30
36
|
const textEdits: TextEdit[] = [];
|
|
37
|
+
let lastNonCommentToken = "";
|
|
31
38
|
|
|
32
39
|
const appendToken: (t: Token) => void = (t: Token) => {
|
|
33
|
-
const newToken = normalizeToken(
|
|
40
|
+
const newToken = normalizeToken(
|
|
41
|
+
t.text,
|
|
42
|
+
lastNonCommentToken,
|
|
43
|
+
randomGenerator,
|
|
44
|
+
);
|
|
34
45
|
if (newToken !== t.text) {
|
|
35
46
|
textEdits.push({
|
|
36
47
|
oldStart: t.position,
|
|
@@ -38,6 +49,9 @@ export function formatModule(moduleTokens: ModuleTokens): FormattedModule {
|
|
|
38
49
|
newText: newToken,
|
|
39
50
|
});
|
|
40
51
|
}
|
|
52
|
+
if (!isComment(t)) {
|
|
53
|
+
lastNonCommentToken = t.text;
|
|
54
|
+
}
|
|
41
55
|
newSourceCode += newToken;
|
|
42
56
|
};
|
|
43
57
|
appendToken(tokens[0]!);
|
|
@@ -248,7 +262,11 @@ function isComment(token: Token): boolean {
|
|
|
248
262
|
return token.text.startsWith("//") || token.text.startsWith("/*");
|
|
249
263
|
}
|
|
250
264
|
|
|
251
|
-
function normalizeToken(
|
|
265
|
+
function normalizeToken(
|
|
266
|
+
token: string,
|
|
267
|
+
lastNonCommentToken: string,
|
|
268
|
+
randomGenerator: RandomGenerator,
|
|
269
|
+
): string {
|
|
252
270
|
if (token.startsWith("//")) {
|
|
253
271
|
// Make sure there is a space between the double slash and the comment text.
|
|
254
272
|
if (
|
|
@@ -279,6 +297,9 @@ function normalizeToken(token: string): string {
|
|
|
279
297
|
// A double-quoted string
|
|
280
298
|
// Remove escape characters before double quotes.
|
|
281
299
|
return token.replace(/\\(?=(?:\\\\)*')/g, "");
|
|
300
|
+
} else if (token === "?" && ["=", "("].includes(lastNonCommentToken)) {
|
|
301
|
+
const randomNumber = Math.floor(randomGenerator() * 1_000_000);
|
|
302
|
+
return String(randomNumber);
|
|
282
303
|
} else {
|
|
283
304
|
return token;
|
|
284
305
|
}
|
package/src/module_set.ts
CHANGED
package/src/parser.ts
CHANGED
|
@@ -33,10 +33,13 @@ import { mergeDocs } from "./doc_comment_parser.js";
|
|
|
33
33
|
import { ModuleTokens } from "./tokenizer.js";
|
|
34
34
|
|
|
35
35
|
/** Runs syntactic analysis on a module. */
|
|
36
|
-
export function parseModule(
|
|
36
|
+
export function parseModule(
|
|
37
|
+
moduleTokens: ModuleTokens,
|
|
38
|
+
mode: "strict" | "lenient",
|
|
39
|
+
): Result<MutableModule> {
|
|
37
40
|
const { modulePath, sourceCode } = moduleTokens;
|
|
38
41
|
const errors: SkirError[] = [];
|
|
39
|
-
const it = new TokenIterator(moduleTokens, errors);
|
|
42
|
+
const it = new TokenIterator(moduleTokens, mode, errors);
|
|
40
43
|
const declarations = parseDeclarations(it, "module");
|
|
41
44
|
it.expectThenNext([""]);
|
|
42
45
|
// Create a mappinng from names to declarations, and check for duplicates.
|
|
@@ -404,8 +407,8 @@ function parseRecord(
|
|
|
404
407
|
let stableId: number | null = null;
|
|
405
408
|
if (it.current === "(") {
|
|
406
409
|
it.next();
|
|
407
|
-
stableId = parseUint32(it);
|
|
408
|
-
if (stableId
|
|
410
|
+
stableId = parseUint32(it, "?");
|
|
411
|
+
if (stableId === -2) {
|
|
409
412
|
return null;
|
|
410
413
|
}
|
|
411
414
|
if (it.expectThenNext([")"]).case < 0) {
|
|
@@ -671,10 +674,14 @@ function parseRecordRef(
|
|
|
671
674
|
return { kind: "record", nameParts: nameParts, absolute: absolute };
|
|
672
675
|
}
|
|
673
676
|
|
|
674
|
-
function parseUint32(it: TokenIterator): number {
|
|
677
|
+
function parseUint32(it: TokenIterator, maybeQuestionMark?: "?"): number {
|
|
678
|
+
if (maybeQuestionMark && it.mode === "lenient" && it.current === "?") {
|
|
679
|
+
it.next();
|
|
680
|
+
return -1;
|
|
681
|
+
}
|
|
675
682
|
const match = it.expectThenNext([TOKEN_IS_POSITIVE_INT]);
|
|
676
683
|
if (match.case < 0) {
|
|
677
|
-
return -
|
|
684
|
+
return -2;
|
|
678
685
|
}
|
|
679
686
|
const { text } = match.token;
|
|
680
687
|
const valueAsBigInt = BigInt(text);
|
|
@@ -685,7 +692,7 @@ function parseUint32(it: TokenIterator): number {
|
|
|
685
692
|
token: match.token,
|
|
686
693
|
message: "Value out of uint32 range",
|
|
687
694
|
});
|
|
688
|
-
return -
|
|
695
|
+
return -2;
|
|
689
696
|
}
|
|
690
697
|
}
|
|
691
698
|
|
|
@@ -865,8 +872,8 @@ function parseMethod(it: TokenIterator, doc: Doc): MutableMethod | null {
|
|
|
865
872
|
if (it.expectThenNext(["="]).case < 0) {
|
|
866
873
|
return null;
|
|
867
874
|
}
|
|
868
|
-
const number = parseUint32(it);
|
|
869
|
-
if (number
|
|
875
|
+
const number = parseUint32(it, "?");
|
|
876
|
+
if (number === -2) {
|
|
870
877
|
return null;
|
|
871
878
|
}
|
|
872
879
|
it.expectThenNext([";"]);
|
|
@@ -1121,6 +1128,7 @@ class TokenIterator {
|
|
|
1121
1128
|
|
|
1122
1129
|
constructor(
|
|
1123
1130
|
readonly moduleTokens: ModuleTokens,
|
|
1131
|
+
readonly mode: "strict" | "lenient",
|
|
1124
1132
|
readonly errors: ErrorSink,
|
|
1125
1133
|
) {
|
|
1126
1134
|
this.tokens = moduleTokens.tokens;
|
package/src/snapshotter.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { RecordLocation, ResolvedType } from "skir-internal";
|
|
4
|
+
import { checkCompatibility } from "./compatibility_checker.js";
|
|
4
5
|
import {
|
|
5
6
|
formatError,
|
|
6
7
|
makeGreen,
|
|
@@ -37,7 +38,7 @@ export async function takeSnapshot(args: {
|
|
|
37
38
|
);
|
|
38
39
|
return false;
|
|
39
40
|
}
|
|
40
|
-
const breakingChanges =
|
|
41
|
+
const breakingChanges = checkCompatibility({
|
|
41
42
|
before: oldModuleSet,
|
|
42
43
|
after: newModuleSet,
|
|
43
44
|
});
|
|
@@ -65,6 +66,33 @@ export async function takeSnapshot(args: {
|
|
|
65
66
|
}
|
|
66
67
|
await writeFile(snapshotPath, JSON.stringify(newSnapshot, null, 2), "utf-8");
|
|
67
68
|
console.log("Snapshot taken. No breaking changes detected.");
|
|
69
|
+
|
|
70
|
+
const trackedRecordCount = newSnapshot.trackedRecordIds.length;
|
|
71
|
+
const untrackedRecordCount = newSnapshot.untrackedRecordIds.length;
|
|
72
|
+
const formatCount = (n: number, what: string): string => {
|
|
73
|
+
return `${n} ${what}${n === 1 ? "" : "s"}`;
|
|
74
|
+
};
|
|
75
|
+
console.log(
|
|
76
|
+
[
|
|
77
|
+
formatCount(trackedRecordCount, "tracked record"),
|
|
78
|
+
", ",
|
|
79
|
+
formatCount(untrackedRecordCount, "untracked record"),
|
|
80
|
+
" found in new snapshot.",
|
|
81
|
+
].join(""),
|
|
82
|
+
);
|
|
83
|
+
console.log("See them in " + rewritePathForRendering(snapshotPath));
|
|
84
|
+
|
|
85
|
+
if (trackedRecordCount === 0) {
|
|
86
|
+
console.log(makeRed("Warning: no tracked records found."));
|
|
87
|
+
console.log(
|
|
88
|
+
"Breaking changes cannot be detected without tracking records.",
|
|
89
|
+
);
|
|
90
|
+
console.log(
|
|
91
|
+
"To track a record and its dependencies, give it a stable identifier, e.g.:",
|
|
92
|
+
);
|
|
93
|
+
console.log(" struct MyStruct(56789) { ... }");
|
|
94
|
+
}
|
|
95
|
+
|
|
68
96
|
return true;
|
|
69
97
|
}
|
|
70
98
|
|
|
@@ -173,9 +201,12 @@ function makeSnapshot(moduleSet: ModuleSet, now: Date): Snapshot {
|
|
|
173
201
|
for (const module of moduleSet.resolvedModules) {
|
|
174
202
|
modules[module.path] = module.sourceCode;
|
|
175
203
|
}
|
|
204
|
+
const trackedRecordIds = collectTrackedRecords(moduleSet);
|
|
176
205
|
return {
|
|
177
206
|
readMe: "DO NOT EDIT. To update, run: npx skir snapshot",
|
|
178
207
|
lastChange: now.toISOString(),
|
|
208
|
+
trackedRecordIds: Array.from(trackedRecordIds.trackedRecordIds).sort(),
|
|
209
|
+
untrackedRecordIds: Array.from(trackedRecordIds.untrackedRecordIds).sort(),
|
|
179
210
|
modules,
|
|
180
211
|
};
|
|
181
212
|
}
|
|
@@ -192,5 +223,70 @@ function sameModules(a: Snapshot, b: Snapshot): boolean {
|
|
|
192
223
|
interface Snapshot {
|
|
193
224
|
readMe: string;
|
|
194
225
|
lastChange: string;
|
|
195
|
-
|
|
226
|
+
trackedRecordIds: readonly string[];
|
|
227
|
+
untrackedRecordIds: readonly string[];
|
|
228
|
+
modules: Readonly<{ [path: string]: string }>;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
interface TrackedRecords {
|
|
232
|
+
trackedRecordIds: ReadonlySet<string>;
|
|
233
|
+
untrackedRecordIds: ReadonlySet<string>;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function collectTrackedRecords(moduleSet: ModuleSet): TrackedRecords {
|
|
237
|
+
const seenRecordIds = new Set<string>();
|
|
238
|
+
const trackedRecordIds = new Set<string>();
|
|
239
|
+
|
|
240
|
+
const getRecordId = (record: RecordLocation): string => {
|
|
241
|
+
const qualifiedName = record.recordAncestors
|
|
242
|
+
.map((token) => token.name.text)
|
|
243
|
+
.join(".");
|
|
244
|
+
return `${record.modulePath}:${qualifiedName}`;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const getRecordForType = (type: ResolvedType): RecordLocation | null => {
|
|
248
|
+
switch (type.kind) {
|
|
249
|
+
case "array":
|
|
250
|
+
return getRecordForType(type.item);
|
|
251
|
+
case "optional":
|
|
252
|
+
return getRecordForType(type.other);
|
|
253
|
+
case "primitive":
|
|
254
|
+
return null;
|
|
255
|
+
case "record":
|
|
256
|
+
return moduleSet.recordMap.get(type.key) ?? null;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const processRecord = (record: RecordLocation): void => {
|
|
261
|
+
const recordId = getRecordId(record);
|
|
262
|
+
if (seenRecordIds.has(recordId)) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
seenRecordIds.add(recordId);
|
|
266
|
+
if (record.record.recordNumber === null) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
trackedRecordIds.add(recordId);
|
|
270
|
+
// Recursively process the field/variant types
|
|
271
|
+
for (const field of record.record.fields) {
|
|
272
|
+
const fieldType = field.type;
|
|
273
|
+
if (fieldType) {
|
|
274
|
+
const fieldRecord = getRecordForType(fieldType);
|
|
275
|
+
if (fieldRecord) {
|
|
276
|
+
processRecord(fieldRecord);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
for (const record of moduleSet.recordMap.values()) {
|
|
283
|
+
processRecord(record);
|
|
284
|
+
}
|
|
285
|
+
const untrackedRecordIds = new Set(
|
|
286
|
+
[...seenRecordIds].filter((id) => !trackedRecordIds.has(id)),
|
|
287
|
+
);
|
|
288
|
+
return {
|
|
289
|
+
trackedRecordIds: trackedRecordIds,
|
|
290
|
+
untrackedRecordIds: untrackedRecordIds,
|
|
291
|
+
};
|
|
196
292
|
}
|