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/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(moduleTokens: ModuleTokens): FormattedModule {
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(t.text);
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(token: string): string {
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
@@ -1298,7 +1298,7 @@ abstract class ModuleParserBase implements ModuleParser {
1298
1298
  };
1299
1299
  }
1300
1300
 
1301
- return parseModule(tokens.result);
1301
+ return parseModule(tokens.result, "strict");
1302
1302
  }
1303
1303
  }
1304
1304
 
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(moduleTokens: ModuleTokens): Result<MutableModule> {
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 < 0) {
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 -1;
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 -1;
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 < 0) {
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;
@@ -1,6 +1,7 @@
1
1
  import { readFile, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { checkBackwardCompatibility } from "./compatibility_checker.js";
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 = checkBackwardCompatibility({
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
- modules: { [path: string]: string };
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
  }