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.
Files changed (141) hide show
  1. package/README.md +447 -0
  2. package/dist/casing.d.ts +8 -0
  3. package/dist/casing.d.ts.map +1 -0
  4. package/dist/casing.js +49 -0
  5. package/dist/casing.js.map +1 -0
  6. package/dist/casing.test.d.ts +2 -0
  7. package/dist/casing.test.d.ts.map +1 -0
  8. package/dist/casing.test.js +134 -0
  9. package/dist/casing.test.js.map +1 -0
  10. package/dist/command_line_parser.d.ts +33 -0
  11. package/dist/command_line_parser.d.ts.map +1 -0
  12. package/dist/command_line_parser.js +171 -0
  13. package/dist/command_line_parser.js.map +1 -0
  14. package/dist/command_line_parser.test.d.ts +2 -0
  15. package/dist/command_line_parser.test.d.ts.map +1 -0
  16. package/dist/command_line_parser.test.js +302 -0
  17. package/dist/command_line_parser.test.js.map +1 -0
  18. package/dist/compatibility_checker.d.ts +68 -0
  19. package/dist/compatibility_checker.d.ts.map +1 -0
  20. package/dist/compatibility_checker.js +328 -0
  21. package/dist/compatibility_checker.js.map +1 -0
  22. package/dist/compatibility_checker.test.d.ts +2 -0
  23. package/dist/compatibility_checker.test.d.ts.map +1 -0
  24. package/dist/compatibility_checker.test.js +528 -0
  25. package/dist/compatibility_checker.test.js.map +1 -0
  26. package/dist/compiler.d.ts +3 -0
  27. package/dist/compiler.d.ts.map +1 -0
  28. package/dist/compiler.js +358 -0
  29. package/dist/compiler.js.map +1 -0
  30. package/dist/config.d.ts +47 -0
  31. package/dist/config.d.ts.map +1 -0
  32. package/dist/config.js +23 -0
  33. package/dist/config.js.map +1 -0
  34. package/dist/definition_finder.d.ts +12 -0
  35. package/dist/definition_finder.d.ts.map +1 -0
  36. package/dist/definition_finder.js +180 -0
  37. package/dist/definition_finder.js.map +1 -0
  38. package/dist/definition_finder.test.d.ts +2 -0
  39. package/dist/definition_finder.test.d.ts.map +1 -0
  40. package/dist/definition_finder.test.js +164 -0
  41. package/dist/definition_finder.test.js.map +1 -0
  42. package/dist/encoding.d.ts +2 -0
  43. package/dist/encoding.d.ts.map +1 -0
  44. package/dist/encoding.js +38 -0
  45. package/dist/encoding.js.map +1 -0
  46. package/dist/encoding.test.d.ts +2 -0
  47. package/dist/encoding.test.d.ts.map +1 -0
  48. package/dist/encoding.test.js +23 -0
  49. package/dist/encoding.test.js.map +1 -0
  50. package/dist/error_renderer.d.ts +10 -0
  51. package/dist/error_renderer.d.ts.map +1 -0
  52. package/dist/error_renderer.js +247 -0
  53. package/dist/error_renderer.js.map +1 -0
  54. package/dist/formatter.d.ts +3 -0
  55. package/dist/formatter.d.ts.map +1 -0
  56. package/dist/formatter.js +263 -0
  57. package/dist/formatter.js.map +1 -0
  58. package/dist/formatter.test.d.ts +2 -0
  59. package/dist/formatter.test.d.ts.map +1 -0
  60. package/dist/formatter.test.js +156 -0
  61. package/dist/formatter.test.js.map +1 -0
  62. package/dist/index.d.ts +6 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +5 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/index.test.d.ts +2 -0
  67. package/dist/index.test.d.ts.map +1 -0
  68. package/dist/index.test.js +14 -0
  69. package/dist/index.test.js.map +1 -0
  70. package/dist/io.d.ts +13 -0
  71. package/dist/io.d.ts.map +1 -0
  72. package/dist/io.js +22 -0
  73. package/dist/io.js.map +1 -0
  74. package/dist/language_server.d.ts +15 -0
  75. package/dist/language_server.d.ts.map +1 -0
  76. package/dist/language_server.js +248 -0
  77. package/dist/language_server.js.map +1 -0
  78. package/dist/literals.d.ts +13 -0
  79. package/dist/literals.d.ts.map +1 -0
  80. package/dist/literals.js +100 -0
  81. package/dist/literals.js.map +1 -0
  82. package/dist/literals.test.d.ts +2 -0
  83. package/dist/literals.test.d.ts.map +1 -0
  84. package/dist/literals.test.js +149 -0
  85. package/dist/literals.test.js.map +1 -0
  86. package/dist/module_collector.d.ts +3 -0
  87. package/dist/module_collector.d.ts.map +1 -0
  88. package/dist/module_collector.js +22 -0
  89. package/dist/module_collector.js.map +1 -0
  90. package/dist/module_set.d.ts +44 -0
  91. package/dist/module_set.d.ts.map +1 -0
  92. package/dist/module_set.js +1025 -0
  93. package/dist/module_set.js.map +1 -0
  94. package/dist/module_set.test.d.ts +2 -0
  95. package/dist/module_set.test.d.ts.map +1 -0
  96. package/dist/module_set.test.js +1330 -0
  97. package/dist/module_set.test.js.map +1 -0
  98. package/dist/parser.d.ts +6 -0
  99. package/dist/parser.d.ts.map +1 -0
  100. package/dist/parser.js +971 -0
  101. package/dist/parser.js.map +1 -0
  102. package/dist/parser.test.d.ts +2 -0
  103. package/dist/parser.test.d.ts.map +1 -0
  104. package/dist/parser.test.js +1366 -0
  105. package/dist/parser.test.js.map +1 -0
  106. package/dist/snapshotter.d.ts +6 -0
  107. package/dist/snapshotter.d.ts.map +1 -0
  108. package/dist/snapshotter.js +107 -0
  109. package/dist/snapshotter.js.map +1 -0
  110. package/dist/tokenizer.d.ts +4 -0
  111. package/dist/tokenizer.d.ts.map +1 -0
  112. package/dist/tokenizer.js +192 -0
  113. package/dist/tokenizer.js.map +1 -0
  114. package/dist/tokenizer.test.d.ts +2 -0
  115. package/dist/tokenizer.test.d.ts.map +1 -0
  116. package/dist/tokenizer.test.js +425 -0
  117. package/dist/tokenizer.test.js.map +1 -0
  118. package/dist/types.d.ts +375 -0
  119. package/dist/types.d.ts.map +1 -0
  120. package/dist/types.js +2 -0
  121. package/dist/types.js.map +1 -0
  122. package/package.json +63 -0
  123. package/src/casing.ts +64 -0
  124. package/src/command_line_parser.ts +249 -0
  125. package/src/compatibility_checker.ts +470 -0
  126. package/src/compiler.ts +435 -0
  127. package/src/config.ts +28 -0
  128. package/src/definition_finder.ts +221 -0
  129. package/src/encoding.ts +32 -0
  130. package/src/error_renderer.ts +278 -0
  131. package/src/formatter.ts +274 -0
  132. package/src/index.ts +6 -0
  133. package/src/io.ts +33 -0
  134. package/src/language_server.ts +301 -0
  135. package/src/literals.ts +120 -0
  136. package/src/module_collector.ts +22 -0
  137. package/src/module_set.ts +1175 -0
  138. package/src/parser.ts +1122 -0
  139. package/src/snapshotter.ts +136 -0
  140. package/src/tokenizer.ts +216 -0
  141. package/src/types.ts +518 -0
@@ -0,0 +1,136 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { checkBackwardCompatibility } from "./compatibility_checker.js";
4
+ import {
5
+ formatError,
6
+ makeRed,
7
+ renderBreakingChanges,
8
+ renderErrors,
9
+ } from "./error_renderer.js";
10
+ import { collectModules } from "./module_collector.js";
11
+ import { ModuleSet } from "./module_set.js";
12
+
13
+ export async function takeSnapshot(args: {
14
+ rootDir: string;
15
+ srcDir: string;
16
+ check: boolean;
17
+ }): Promise<void> {
18
+ const newModuleSet = await collectModules(args.srcDir);
19
+ if (newModuleSet.errors.length) {
20
+ renderErrors(newModuleSet.errors);
21
+ process.exit(1);
22
+ }
23
+ const snapshotPath = join(args.rootDir, "skir-snapshot.json");
24
+ const oldModuleSet = await readLastSnapshot(join(args.rootDir, snapshotPath));
25
+ if (!(oldModuleSet instanceof ModuleSet)) {
26
+ console.error(makeRed(`Corrupted snapshot file: ${snapshotPath}`));
27
+ console.error(`Error: ${oldModuleSet.error.toString()}`);
28
+ console.log(
29
+ "If the snapshot file cannot be restored to a valid state, delete it and run again. " +
30
+ "Breaking changes from recent commits will not be detected, but a valid snapshot will be created for future comparisons.",
31
+ );
32
+ process.exit(1);
33
+ }
34
+ const breakingChanges = checkBackwardCompatibility({
35
+ before: oldModuleSet,
36
+ after: newModuleSet,
37
+ });
38
+ if (breakingChanges.length) {
39
+ renderBreakingChanges(breakingChanges, {
40
+ before: oldModuleSet,
41
+ after: newModuleSet,
42
+ });
43
+ process.exit(1);
44
+ }
45
+ const newSnapshot = makeSnapshot(newModuleSet, new Date());
46
+ if (sameModules(newSnapshot, makeSnapshot(oldModuleSet, new Date()))) {
47
+ console.log("No changes detected since last snapshot.");
48
+ return;
49
+ }
50
+ if (args.check) {
51
+ console.error(
52
+ makeRed(
53
+ `Modules have changed since the last snapshot. ` +
54
+ `Run the command without --check to take a new snapshot.`,
55
+ ),
56
+ );
57
+ process.exit(1);
58
+ }
59
+ await writeFile(snapshotPath, JSON.stringify(newSnapshot, null, 2), "utf-8");
60
+ console.log("Snapshot taken. No breaking changes detected.");
61
+ }
62
+
63
+ async function readLastSnapshot(snapshotPath: string): Promise<
64
+ | ModuleSet
65
+ | {
66
+ kind: "corrupted";
67
+ error: any;
68
+ }
69
+ > {
70
+ let snapshot: Snapshot;
71
+ try {
72
+ const textContent = await readFile(snapshotPath, "utf-8");
73
+ snapshot = JSON.parse(textContent);
74
+ } catch (error) {
75
+ if (error instanceof SyntaxError) {
76
+ return {
77
+ kind: "corrupted",
78
+ error: error,
79
+ };
80
+ }
81
+ const isNotFoundError =
82
+ error instanceof Error && "code" in error && error.code === "ENOENT";
83
+ if (isNotFoundError) {
84
+ return ModuleSet.fromMap(new Map<string, string>());
85
+ } else {
86
+ // Rethrow I/O error
87
+ throw error;
88
+ }
89
+ }
90
+ const pathToSourceCode = new Map<string, string>();
91
+ try {
92
+ for (const [path, sourceCode] of Object.entries(snapshot.modules)) {
93
+ // + "" to ensure string type
94
+ pathToSourceCode.set(path + "", sourceCode + "");
95
+ }
96
+ } catch (error) {
97
+ return {
98
+ kind: "corrupted",
99
+ error: error,
100
+ };
101
+ }
102
+ const moduleSet = ModuleSet.fromMap(pathToSourceCode);
103
+ if (moduleSet.errors.length) {
104
+ const firstError = formatError(moduleSet.errors[0]!);
105
+ return {
106
+ kind: "corrupted",
107
+ error: new Error(`errors in modules; first error: ${firstError}`),
108
+ };
109
+ }
110
+ return moduleSet;
111
+ }
112
+
113
+ function makeSnapshot(moduleSet: ModuleSet, now: Date): Snapshot {
114
+ const modules: { [path: string]: string } = {};
115
+ for (const module of moduleSet.resolvedModules) {
116
+ modules[module.path] = module.sourceCode;
117
+ }
118
+ return {
119
+ lastChange: now.toISOString(),
120
+ modules: modules,
121
+ };
122
+ }
123
+
124
+ function sameModules(a: Snapshot, b: Snapshot): boolean {
125
+ return (
126
+ Object.keys(a.modules).length === Object.keys(b.modules).length ||
127
+ Object.entries(a.modules).every(([path, sourceCode]) => {
128
+ return sourceCode === b.modules[path];
129
+ })
130
+ );
131
+ }
132
+
133
+ interface Snapshot {
134
+ lastChange: string;
135
+ modules: { [path: string]: string };
136
+ }
@@ -0,0 +1,216 @@
1
+ import { unquoteAndUnescape } from "./literals.js";
2
+ import type { CodeLine, ErrorSink, Result, SkirError, Token } from "./types.js";
3
+
4
+ /** Tokenizes the given module. */
5
+ export function tokenizeModule(
6
+ code: string,
7
+ modulePath: string,
8
+ keepComments?: "keep-comments",
9
+ ): Result<Token[]> {
10
+ const tokens: Token[] = [];
11
+ const errors: SkirError[] = [];
12
+
13
+ const lines = new Lines(code, modulePath);
14
+
15
+ // Multiline comment: \/\*([^*]|\*[^\/])*(\*\/)?
16
+ // Single-line comment: \/\/[^\n\r]*
17
+ // Number: -?(0|[1-9][0-9]*)(\.[0-9]+)?\b
18
+ // Word: \b\w+\b
19
+ // Whitespaces: [ \n\r\t]+
20
+ // Symbol: \.\.|\{\||\|\}|[{}[\]()*.:=;|?,]
21
+ // Double-quote string literal: "(\\(\n|\r|\n\r|.)|[^\\\"\n\r])*"?
22
+ // Single-quote string literal: '(\\(\n|\r|\n\r|.)|[^\\\'\n\r])*'?
23
+ //
24
+ // To iterate on this regex, use https://regex101.com/.
25
+ const re =
26
+ /(\/\*([^*]|\*[^/])*(\*\/)?)|(\/\/[^\n\r]*)|(-?(0|[1-9][0-9]*)(\.[0-9]+)?\b)|\b(\w+)\b|([ \n\r\t]+)|(\.\.|\{\||\|\}|[{}[\]()*.:=;|?,])|("(\\(\n|\r|\n\r|.)|[^\\"\n\r])*"?)|('(\\(\n|\r|\n\r|.)|[^\\'\n\r])*'?)|($)/g;
27
+ // 1 2 3 4 5 6 7 8 9 10 11 13 14 16 17
28
+
29
+ let group: RegExpExecArray | null;
30
+ let expectedPosition = 0;
31
+ while ((group = re.exec(code)) !== null) {
32
+ const position = re.lastIndex - group[0].length;
33
+
34
+ // Check that all the regex matches are consecutive.
35
+ // If there is a gap between two matches, it means that we have an invalid
36
+ // sequence of characters.
37
+ if (position !== expectedPosition) {
38
+ const line = lines.advancePosition(expectedPosition);
39
+ const colNumber = expectedPosition - line.position;
40
+ errors.push({
41
+ token: {
42
+ text: code.substring(expectedPosition, position),
43
+ position: expectedPosition,
44
+ line: line,
45
+ colNumber: colNumber,
46
+ },
47
+ message: "Invalid sequence of characters",
48
+ });
49
+ }
50
+ expectedPosition = re.lastIndex;
51
+
52
+ const line = lines.advancePosition(position);
53
+ const colNumber = position - line.position;
54
+ const token = {
55
+ text: group[0],
56
+ position: position,
57
+ line: line,
58
+ colNumber: colNumber,
59
+ };
60
+
61
+ // Skip multiline comments.
62
+ if (group[1] !== undefined && !keepComments) {
63
+ // Make sure the multiline comment is terminated.
64
+ if (!group[1].endsWith("*/")) {
65
+ errors.push({
66
+ token: token,
67
+ message: "Unterminated multi-line comment",
68
+ });
69
+ }
70
+ continue;
71
+ }
72
+
73
+ // Skip single-line comments.
74
+ if (group[4] !== undefined && !keepComments) {
75
+ continue;
76
+ }
77
+
78
+ if (group[8] !== undefined) {
79
+ if (!validateWord(token, errors)) {
80
+ continue;
81
+ }
82
+ }
83
+
84
+ // Skip whitespaces.
85
+ if (group[9] !== undefined) continue;
86
+
87
+ // Validate string literals.
88
+ const stringLiteral = group[11] || group[14];
89
+ if (stringLiteral !== undefined) {
90
+ if (!stringLiteral.endsWith(stringLiteral[0]!)) {
91
+ errors.push({
92
+ token: token,
93
+ message: "Unterminated string literal",
94
+ });
95
+ continue;
96
+ }
97
+ const stringLiteralRe =
98
+ /^["'](\\(\r\n|u[0-9A-Fa-f]{4}|["'\\/bfnrt\n\r])|[^\\])*["']$/;
99
+ if (!stringLiteralRe.test(stringLiteral)) {
100
+ errors.push({
101
+ token: token,
102
+ message: "String literal contains invalid escape sequence",
103
+ });
104
+ continue;
105
+ }
106
+ // Check that the string doesn't contain lone surrogates.
107
+ // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/isWellFormed
108
+ const unquoted = unquoteAndUnescape(stringLiteral);
109
+ try {
110
+ encodeURIComponent(unquoted);
111
+ } catch {
112
+ errors.push({
113
+ token: token,
114
+ message: "String literal contains lone surrogates",
115
+ });
116
+ continue;
117
+ }
118
+ }
119
+
120
+ tokens.push(token);
121
+
122
+ if (group[17] !== undefined) {
123
+ // The end of the string has been reached.
124
+ // The last token we added is an empty string.
125
+ break;
126
+ }
127
+ }
128
+
129
+ return { result: tokens, errors: errors };
130
+ }
131
+
132
+ function validateWord(token: Token, errors: ErrorSink): boolean {
133
+ if (/^[0-9]/.test(token.text)) {
134
+ errors.push({
135
+ token: token,
136
+ message: "Invalid number",
137
+ });
138
+ return false;
139
+ }
140
+
141
+ if (token.text.startsWith("_")) {
142
+ errors.push({
143
+ token: token,
144
+ message: "Identifier cannot start with _",
145
+ });
146
+ return false;
147
+ }
148
+ if (token.text.endsWith("_")) {
149
+ errors.push({
150
+ token: token,
151
+ message: "Identifier cannot end with _",
152
+ });
153
+ return false;
154
+ }
155
+ if (token.text.includes("__")) {
156
+ errors.push({
157
+ token: token,
158
+ message: "Identifier cannot contain __ sequence",
159
+ });
160
+ return false;
161
+ }
162
+ if (/_[0-9]/.test(token.text)) {
163
+ // The problem is we could end up with duplicate field names when converting
164
+ // to camel case. For example: "field0" and "field_0" would both be
165
+ // converted to "field0".
166
+ errors.push({
167
+ token: token,
168
+ message: "Digit cannot follow _",
169
+ });
170
+ return false;
171
+ }
172
+
173
+ return true;
174
+ }
175
+
176
+ class Lines {
177
+ /** Splits the given code into multiple lines. */
178
+ constructor(code: string, modulePath: string) {
179
+ const lines = this.lines;
180
+ let lineStart = 0;
181
+ let group: RegExpExecArray | null;
182
+ const re = /\n|\r|\n\r|$/g;
183
+ while ((group = re.exec(code)) !== null) {
184
+ const match = group[0];
185
+ const lineEnd = re.lastIndex - match.length;
186
+ const lineString = code.substring(lineStart, lineEnd);
187
+ lines.push({
188
+ lineNumber: lines.length,
189
+ line: lineString,
190
+ position: lineStart,
191
+ modulePath: modulePath,
192
+ });
193
+ // Record the position after the EOL or EOF.
194
+ lineStart = re.lastIndex;
195
+ if (match === "") {
196
+ break;
197
+ }
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Returns the line at the given position, where the position is the char
203
+ * index within `code`.
204
+ * Requires the position to be >= the position given the last time the method
205
+ * was called.
206
+ */
207
+ advancePosition(position: number): CodeLine {
208
+ const lines = this.lines;
209
+ while (lines.length >= 2 && position >= this.lines[1]!.position) {
210
+ lines.shift();
211
+ }
212
+ return lines[0]!;
213
+ }
214
+
215
+ private readonly lines: CodeLine[] = [];
216
+ }