skir 1.2.7 → 1.2.9

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
@@ -1,4 +1,13 @@
1
- import type { Module, Range, SkirError, Token } from "skir-internal";
1
+ import type {
2
+ Declaration,
3
+ Doc,
4
+ Module,
5
+ Range,
6
+ Record,
7
+ SkirError,
8
+ Token,
9
+ Value,
10
+ } from "skir-internal";
2
11
  import { formatImportBlock } from "./import_block_formatter.js";
3
12
  import { parseModule } from "./parser.js";
4
13
  import { ModuleTokens, tokenizeModule } from "./tokenizer.js";
@@ -30,13 +39,19 @@ export type RandomGenerator = () => number;
30
39
 
31
40
  /**
32
41
  * Formats the given module and returns the new source code.
33
- * Preserves token ordering.
42
+ * If a resolved module is provided, the formatter will try to convert enum
43
+ * variants spelled with the legacy UPPERCASE format to the preferred lowercase
44
+ * format.
34
45
  */
35
- export function formatModule(
36
- sourceCode: string,
37
- modulePath: string,
38
- randomGenerator: RandomGenerator = Math.random,
39
- ): FormattedModule {
46
+ export function formatModule(args: {
47
+ sourceCode: string;
48
+ modulePath: string;
49
+ resolvedModule?: Module;
50
+ randomGenerator?: RandomGenerator;
51
+ }): FormattedModule {
52
+ const { modulePath, resolvedModule, randomGenerator = Math.random } = args;
53
+ let { sourceCode } = args;
54
+
40
55
  const makeErroredResult = (
41
56
  errors: readonly SkirError[],
42
57
  ): FormattedModule => ({
@@ -45,6 +60,15 @@ export function formatModule(
45
60
  errors: errors,
46
61
  });
47
62
 
63
+ if (resolvedModule) {
64
+ if (resolvedModule.sourceCode !== sourceCode) {
65
+ throw new Error(
66
+ "Resolved module's source code does not match the provided source code.",
67
+ );
68
+ }
69
+ sourceCode = convertLegacyVariantNames(resolvedModule).newSourceCode;
70
+ }
71
+
48
72
  const moduleTokens = tokenizeModule(sourceCode, modulePath);
49
73
  if (moduleTokens.errors.length > 0) {
50
74
  return makeErroredResult(moduleTokens.errors);
@@ -447,3 +471,106 @@ function normalizeToken(
447
471
  return token;
448
472
  }
449
473
  }
474
+
475
+ /**
476
+ * Converts the constant variants spelled with the legacy UPPERCASE format to
477
+ * the preferred lowercase format.
478
+ * The enum must not be declared in an external dependency.
479
+ */
480
+ function convertLegacyVariantNames(resolvedModule: Module): {
481
+ newSourceCode: string;
482
+ } {
483
+ const legacyTokens: Token[] = [];
484
+ const isEligibleEnum = (record: Record): boolean =>
485
+ record.recordType === "enum" &&
486
+ !record.name.line.modulePath.startsWith("@");
487
+ const collectInDoc = (doc: Doc): void => {
488
+ for (const piece of doc.pieces) {
489
+ if (piece.kind !== "reference") continue;
490
+ const { referee } = piece;
491
+ if (
492
+ referee?.kind === "field" &&
493
+ !referee.field.unresolvedType &&
494
+ isEligibleEnum(referee.record) &&
495
+ /^[A-Z]/.test(referee.field.name.text)
496
+ ) {
497
+ const { token } = piece.nameParts.at(-1)!;
498
+ legacyTokens.push(token);
499
+ }
500
+ }
501
+ };
502
+ const collectInValue = (value: Value): void => {
503
+ switch (value.kind) {
504
+ case "array": {
505
+ return value.items.forEach(collectInValue);
506
+ }
507
+ case "object": {
508
+ return Object.values(value.entries).forEach((val) => {
509
+ collectInValue(val.value);
510
+ });
511
+ }
512
+ case "literal": {
513
+ if (
514
+ value.type &&
515
+ value.type.kind === "enum" &&
516
+ isEligibleEnum(value.type.enum) &&
517
+ /^['"][A-Z]/.test(value.token.text)
518
+ ) {
519
+ legacyTokens.push(value.token);
520
+ }
521
+ }
522
+ }
523
+ };
524
+ const collect = (declaration: Declaration, inEligibleEnum: boolean): void => {
525
+ // First, collect tokens from doc references.
526
+ switch (declaration.kind) {
527
+ case "constant":
528
+ case "field":
529
+ case "method":
530
+ case "record": {
531
+ collectInDoc(declaration.doc);
532
+ }
533
+ }
534
+ switch (declaration.kind) {
535
+ case "constant": {
536
+ return collectInValue(declaration.value);
537
+ }
538
+ case "field": {
539
+ if (
540
+ inEligibleEnum &&
541
+ !declaration.unresolvedType &&
542
+ /^[A-Z]/.test(declaration.name.text)
543
+ ) {
544
+ legacyTokens.push(declaration.name);
545
+ }
546
+ break;
547
+ }
548
+ case "record": {
549
+ const isEligible = isEligibleEnum(declaration);
550
+ declaration.declarations.forEach((decl) => collect(decl, isEligible));
551
+ }
552
+ }
553
+ };
554
+ for (const declaration of resolvedModule.declarations) {
555
+ const inEligibleEnum = false;
556
+ collect(declaration, inEligibleEnum);
557
+ }
558
+
559
+ const oldSourceCode = resolvedModule.sourceCode;
560
+ if (legacyTokens.length === 0) {
561
+ return { newSourceCode: oldSourceCode };
562
+ }
563
+ legacyTokens.sort((a, b) => a.position - b.position);
564
+
565
+ const fragments: string[] = [];
566
+ let lastPosition = 0;
567
+ for (const legacyToken of legacyTokens) {
568
+ fragments.push(oldSourceCode.slice(lastPosition, legacyToken.position));
569
+ fragments.push(legacyToken.text.toLowerCase());
570
+ lastPosition = legacyToken.position + legacyToken.text.length;
571
+ }
572
+ fragments.push(oldSourceCode.slice(lastPosition));
573
+ return {
574
+ newSourceCode: fragments.join(""),
575
+ };
576
+ }
@@ -168,7 +168,11 @@ export async function getModuleFromGithubUrl(
168
168
  }
169
169
  }
170
170
 
171
- const moduleSet = ModuleSet.compile(modulePathToContent);
171
+ const moduleSet = ModuleSet.compile(
172
+ modulePathToContent,
173
+ "no-cache",
174
+ "strict",
175
+ );
172
176
  if (moduleSet.errors.length > 0) {
173
177
  return {
174
178
  kind: "error",
@@ -11,7 +11,8 @@ import { ModuleSet } from "./module_set.js";
11
11
  export async function collectModules(
12
12
  srcDir: string,
13
13
  dependencies: ModuleSet,
14
- cache?: ModuleSet,
14
+ cache: ModuleSet | undefined,
15
+ parseMode: "strict" | "lenient",
15
16
  ): Promise<ModuleSet> {
16
17
  const modulePathToContent = new Map<string, string>();
17
18
  for (const [modulePath, module] of dependencies.modules) {
@@ -21,16 +22,19 @@ export async function collectModules(
21
22
  for (const { modulePath, content } of editableModules) {
22
23
  modulePathToContent.set(modulePath, content);
23
24
  }
24
- return ModuleSet.compile(modulePathToContent, cache ?? dependencies);
25
+ return ModuleSet.compile(
26
+ modulePathToContent,
27
+ cache ?? dependencies,
28
+ parseMode,
29
+ );
25
30
  }
26
31
 
27
- export interface EditableModule {
28
- readonly fullPath: string;
32
+ interface EditableModule {
29
33
  readonly modulePath: string;
30
34
  readonly content: string;
31
35
  }
32
36
 
33
- export async function collectEditableModules(
37
+ async function collectEditableModules(
34
38
  srcDir: string,
35
39
  ): Promise<ReadonlyArray<EditableModule>> {
36
40
  const skirFiles = await glob(Paths.join(srcDir, "**/*.skir"), {
@@ -57,7 +61,6 @@ export async function collectEditableModules(
57
61
  throw new ExitError("Cannot read " + rewritePathForRendering(fullPath));
58
62
  }
59
63
  return {
60
- fullPath: fullPath,
61
64
  modulePath: relativePath,
62
65
  content: content,
63
66
  };
package/src/module_set.ts CHANGED
@@ -49,8 +49,8 @@ import { ModuleTokens, tokenizeModule } from "./tokenizer.js";
49
49
  export class ModuleSet {
50
50
  static compile(
51
51
  modulePathToContent: ReadonlyMap<string, string>,
52
- cache?: ModuleSet,
53
- parseMode: "strict" | "lenient" = "strict",
52
+ cache: ModuleSet | "no-cache",
53
+ parseMode: "strict" | "lenient",
54
54
  ): ModuleSet {
55
55
  return new ModuleSet(modulePathToContent, cache, parseMode);
56
56
  }
@@ -59,7 +59,7 @@ export class ModuleSet {
59
59
  currentModulePath: string,
60
60
  currentPosition: number,
61
61
  modulePathToContent: ReadonlyMap<string, string>,
62
- cache?: ModuleSet,
62
+ cache: ModuleSet | "no-cache",
63
63
  ): Result<Module> {
64
64
  if (!modulePathToContent.has(currentModulePath)) {
65
65
  throw new Error(`Not found: ${currentModulePath}`);
@@ -71,18 +71,23 @@ export class ModuleSet {
71
71
  return moduleSet.modules.get(currentModulePath)!;
72
72
  }
73
73
 
74
+ static empty(): ModuleSet {
75
+ return ModuleSet.compile(new Map(), "no-cache", "strict");
76
+ }
77
+
74
78
  constructor(
75
79
  private readonly modulePathToContent: ReadonlyMap<string, string>,
76
- cache: ModuleSet | undefined,
80
+ cache: ModuleSet | "no-cache",
77
81
  private readonly parseMode: "strict" | "lenient",
78
82
  private readonly completionMode?: {
79
83
  readonly modulePath: string;
80
84
  readonly position: number;
81
85
  },
82
86
  ) {
83
- this.cache = cache
84
- ? new Cache(modulePathToContent, cache.moduleBundles, cache.registry)
85
- : undefined;
87
+ this.cache =
88
+ cache !== "no-cache"
89
+ ? new Cache(modulePathToContent, cache.moduleBundles, cache.registry)
90
+ : undefined;
86
91
  // In completion mode, no need to recompile modules which are not dependencies of
87
92
  // the current module.
88
93
  const modulePaths = completionMode
@@ -153,15 +158,18 @@ export class ModuleSet {
153
158
 
154
159
  let module: MutableModule;
155
160
  const errors: SkirError[] = [];
161
+ const warnings: SkirError[] = [];
156
162
  {
157
163
  const parseResult = parseModule(moduleTokens.result, this.parseMode);
158
164
  errors.push(...parseResult.errors);
165
+ warnings.push(...(parseResult.warnings ?? []));
159
166
  module = parseResult.result;
160
167
  }
161
168
 
162
169
  const moduleBundle = new ModuleBundle(moduleTokens, {
163
170
  result: module,
164
171
  errors: errors,
172
+ warnings: warnings,
165
173
  });
166
174
 
167
175
  // Process all imports.
@@ -901,7 +909,8 @@ export class ModuleSet {
901
909
  finalize(): FinalizationResult {
902
910
  type MutableModuleResult = {
903
911
  result: Module;
904
- errors: SkirError[];
912
+ readonly errors: SkirError[];
913
+ readonly warnings: SkirError[];
905
914
  };
906
915
  const modules = new Map<string, MutableModuleResult>();
907
916
 
@@ -911,6 +920,7 @@ export class ModuleSet {
911
920
  modules.set(modulePath, {
912
921
  result: module.result,
913
922
  errors: [...moduleErrors],
923
+ warnings: [...(module.warnings ?? [])],
914
924
  });
915
925
  }
916
926
 
@@ -961,12 +971,15 @@ export class ModuleSet {
961
971
 
962
972
  // Aggregate errors across all modules.
963
973
  const errors: SkirError[] = [];
974
+ const warnings: SkirError[] = [];
964
975
  for (const moduleBundle of modules.values()) {
965
976
  errors.push(...moduleBundle.errors);
977
+ warnings.push(...moduleBundle.warnings);
966
978
  }
967
979
  return {
968
980
  modules: modules,
969
981
  errors: errors,
982
+ warnings: warnings,
970
983
  };
971
984
  }
972
985
 
@@ -997,6 +1010,10 @@ export class ModuleSet {
997
1010
  return this.finalizationResult.errors;
998
1011
  }
999
1012
 
1013
+ get warnings(): readonly SkirError[] {
1014
+ return this.finalizationResult.warnings;
1015
+ }
1016
+
1000
1017
  get modules(): ReadonlyMap<string, Result<Module>> {
1001
1018
  return this.finalizationResult.modules;
1002
1019
  }
@@ -1459,6 +1476,8 @@ interface FinalizationResult {
1459
1476
  readonly modules: ReadonlyMap<string, Result<Module>>;
1460
1477
  /** Errors aggregated across all modules. */
1461
1478
  readonly errors: readonly SkirError[];
1479
+ /** Warnings aggregated across all modules. */
1480
+ readonly warnings: readonly SkirError[];
1462
1481
  }
1463
1482
 
1464
1483
  function ensureAllImportsAreUsed(
package/src/parser.ts CHANGED
@@ -46,7 +46,8 @@ export function parseModule(
46
46
  ): Result<MutableModule> {
47
47
  const { modulePath, sourceCode } = moduleTokens;
48
48
  const errors: SkirError[] = [];
49
- const it = new TokenIterator(moduleTokens, mode, errors);
49
+ const warnings: SkirError[] = [];
50
+ const it = new TokenIterator(moduleTokens, mode, errors, warnings);
50
51
  const maybeBrokenDeclarations = parseDeclarations(it, "module");
51
52
  const brokenConstants: BrokenConstant[] = maybeBrokenDeclarations.filter(
52
53
  (d) => d.kind === "broken-constant",
@@ -182,6 +183,7 @@ export function parseModule(
182
183
  brokenConstants: brokenConstants,
183
184
  },
184
185
  errors: errors,
186
+ warnings: warnings,
185
187
  };
186
188
  }
187
189
 
@@ -429,6 +431,29 @@ class RecordBuilder {
429
431
  });
430
432
  break;
431
433
  }
434
+ } else {
435
+ // An enum. Make sure that constant variants are either all spelled in
436
+ // lowercase or all spelled in uppercase (legacy format).
437
+ const firstLegacyConstant = Object.values(this.nameToDeclaration).find(
438
+ (d): d is MutableField =>
439
+ d.kind === "field" &&
440
+ !d.unresolvedType &&
441
+ Casing.caseMatches(d.name.text, "UPPER_UNDERSCORE"),
442
+ );
443
+ if (
444
+ firstLegacyConstant &&
445
+ Object.values(this.nameToDeclaration).some(
446
+ (d) =>
447
+ d.kind === "field" &&
448
+ !d.unresolvedType &&
449
+ Casing.caseMatches(d.name.text, "lower_underscore"),
450
+ )
451
+ ) {
452
+ this.errors.push({
453
+ token: firstLegacyConstant.name,
454
+ message: "Cannot mix legacy format and lowercase format",
455
+ });
456
+ }
432
457
  }
433
458
 
434
459
  const declarations = Object.values(this.nameToDeclaration);
@@ -610,12 +635,28 @@ function parseField(
610
635
  break;
611
636
  }
612
637
  case 2: {
613
- const expectedCasing = type ? "lower_underscore" : "UPPER_UNDERSCORE";
614
- Casing.validate(name, expectedCasing, it.errors);
615
- if (recordType === "enum" && name.text === "UNKNOWN") {
638
+ if (!type && Casing.caseMatches(name.text, "UPPER_UNDERSCORE")) {
639
+ // Legacy format for enum constant variants: UPPER_UNDERSCORE.
640
+ // Raise a warning unless the variant is defined in an external
641
+ // dependency.
642
+ if (!name.line.modulePath.startsWith("@")) {
643
+ it.warnings.push({
644
+ token: name,
645
+ message:
646
+ "Constant variants should be spelled in lowercase; uppercase format is legacy",
647
+ suggestReformat: true,
648
+ });
649
+ }
650
+ } else {
651
+ Casing.validate(name, "lower_underscore", it.errors);
652
+ }
653
+ if (
654
+ recordType === "enum" &&
655
+ ["UNKNOWN", "unknown"].includes(name.text)
656
+ ) {
616
657
  it.errors.push({
617
658
  token: name,
618
- message: `Cannot name field of enum: UNKNOWN`,
659
+ message: `Cannot name field of enum: unknown`,
619
660
  });
620
661
  }
621
662
  return makeField();
@@ -1330,6 +1371,7 @@ class TokenIterator {
1330
1371
  readonly moduleTokens: ModuleTokens,
1331
1372
  readonly mode: "strict" | "lenient",
1332
1373
  readonly errors: ErrorSink,
1374
+ readonly warnings: ErrorSink,
1333
1375
  ) {
1334
1376
  this.tokens = moduleTokens.tokens;
1335
1377
  }
@@ -20,9 +20,14 @@ export async function takeSnapshot(args: {
20
20
  dependencies: ModuleSet;
21
21
  subcommand: "ci" | "dry-run" | undefined;
22
22
  }): Promise<boolean> {
23
- const newModuleSet = await collectModules(args.srcDir, args.dependencies);
23
+ const newModuleSet = await collectModules(
24
+ args.srcDir,
25
+ args.dependencies,
26
+ undefined,
27
+ "strict",
28
+ );
24
29
  if (newModuleSet.errors.length) {
25
- renderErrors(newModuleSet.errors);
30
+ renderErrors(newModuleSet.errors, "error");
26
31
  return false;
27
32
  }
28
33
  const snapshotPath = join(args.rootDir, "skir-snapshot.json");
@@ -113,7 +118,7 @@ async function readLastSnapshot(
113
118
  const isNotFoundError =
114
119
  error instanceof Error && "code" in error && error.code === "ENOENT";
115
120
  if (isNotFoundError) {
116
- return ModuleSet.compile(new Map<string, string>());
121
+ return ModuleSet.empty();
117
122
  } else {
118
123
  // Rethrow I/O error
119
124
  throw error;
@@ -150,9 +155,9 @@ export function snapshotFileContentToModuleSet(
150
155
  error: error,
151
156
  };
152
157
  }
153
- const moduleSet = ModuleSet.compile(pathToSourceCode);
158
+ const moduleSet = ModuleSet.compile(pathToSourceCode, "no-cache", "strict");
154
159
  if (moduleSet.errors.length) {
155
- const firstError = formatError(moduleSet.errors[0]!);
160
+ const firstError = formatError(moduleSet.errors[0]!, "error");
156
161
  return {
157
162
  kind: "corrupted",
158
163
  error: new Error(`errors in modules; first error: ${firstError}`),