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.
- package/README.md +447 -0
- package/dist/casing.d.ts +8 -0
- package/dist/casing.d.ts.map +1 -0
- package/dist/casing.js +49 -0
- package/dist/casing.js.map +1 -0
- package/dist/casing.test.d.ts +2 -0
- package/dist/casing.test.d.ts.map +1 -0
- package/dist/casing.test.js +134 -0
- package/dist/casing.test.js.map +1 -0
- package/dist/command_line_parser.d.ts +33 -0
- package/dist/command_line_parser.d.ts.map +1 -0
- package/dist/command_line_parser.js +171 -0
- package/dist/command_line_parser.js.map +1 -0
- package/dist/command_line_parser.test.d.ts +2 -0
- package/dist/command_line_parser.test.d.ts.map +1 -0
- package/dist/command_line_parser.test.js +302 -0
- package/dist/command_line_parser.test.js.map +1 -0
- package/dist/compatibility_checker.d.ts +68 -0
- package/dist/compatibility_checker.d.ts.map +1 -0
- package/dist/compatibility_checker.js +328 -0
- package/dist/compatibility_checker.js.map +1 -0
- package/dist/compatibility_checker.test.d.ts +2 -0
- package/dist/compatibility_checker.test.d.ts.map +1 -0
- package/dist/compatibility_checker.test.js +528 -0
- package/dist/compatibility_checker.test.js.map +1 -0
- package/dist/compiler.d.ts +3 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +358 -0
- package/dist/compiler.js.map +1 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +23 -0
- package/dist/config.js.map +1 -0
- package/dist/definition_finder.d.ts +12 -0
- package/dist/definition_finder.d.ts.map +1 -0
- package/dist/definition_finder.js +180 -0
- package/dist/definition_finder.js.map +1 -0
- package/dist/definition_finder.test.d.ts +2 -0
- package/dist/definition_finder.test.d.ts.map +1 -0
- package/dist/definition_finder.test.js +164 -0
- package/dist/definition_finder.test.js.map +1 -0
- package/dist/encoding.d.ts +2 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +38 -0
- package/dist/encoding.js.map +1 -0
- package/dist/encoding.test.d.ts +2 -0
- package/dist/encoding.test.d.ts.map +1 -0
- package/dist/encoding.test.js +23 -0
- package/dist/encoding.test.js.map +1 -0
- package/dist/error_renderer.d.ts +10 -0
- package/dist/error_renderer.d.ts.map +1 -0
- package/dist/error_renderer.js +247 -0
- package/dist/error_renderer.js.map +1 -0
- package/dist/formatter.d.ts +3 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +263 -0
- package/dist/formatter.js.map +1 -0
- package/dist/formatter.test.d.ts +2 -0
- package/dist/formatter.test.d.ts.map +1 -0
- package/dist/formatter.test.js +156 -0
- package/dist/formatter.test.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +14 -0
- package/dist/index.test.js.map +1 -0
- package/dist/io.d.ts +13 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/io.js +22 -0
- package/dist/io.js.map +1 -0
- package/dist/language_server.d.ts +15 -0
- package/dist/language_server.d.ts.map +1 -0
- package/dist/language_server.js +248 -0
- package/dist/language_server.js.map +1 -0
- package/dist/literals.d.ts +13 -0
- package/dist/literals.d.ts.map +1 -0
- package/dist/literals.js +100 -0
- package/dist/literals.js.map +1 -0
- package/dist/literals.test.d.ts +2 -0
- package/dist/literals.test.d.ts.map +1 -0
- package/dist/literals.test.js +149 -0
- package/dist/literals.test.js.map +1 -0
- package/dist/module_collector.d.ts +3 -0
- package/dist/module_collector.d.ts.map +1 -0
- package/dist/module_collector.js +22 -0
- package/dist/module_collector.js.map +1 -0
- package/dist/module_set.d.ts +44 -0
- package/dist/module_set.d.ts.map +1 -0
- package/dist/module_set.js +1025 -0
- package/dist/module_set.js.map +1 -0
- package/dist/module_set.test.d.ts +2 -0
- package/dist/module_set.test.d.ts.map +1 -0
- package/dist/module_set.test.js +1330 -0
- package/dist/module_set.test.js.map +1 -0
- package/dist/parser.d.ts +6 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +971 -0
- package/dist/parser.js.map +1 -0
- package/dist/parser.test.d.ts +2 -0
- package/dist/parser.test.d.ts.map +1 -0
- package/dist/parser.test.js +1366 -0
- package/dist/parser.test.js.map +1 -0
- package/dist/snapshotter.d.ts +6 -0
- package/dist/snapshotter.d.ts.map +1 -0
- package/dist/snapshotter.js +107 -0
- package/dist/snapshotter.js.map +1 -0
- package/dist/tokenizer.d.ts +4 -0
- package/dist/tokenizer.d.ts.map +1 -0
- package/dist/tokenizer.js +192 -0
- package/dist/tokenizer.js.map +1 -0
- package/dist/tokenizer.test.d.ts +2 -0
- package/dist/tokenizer.test.d.ts.map +1 -0
- package/dist/tokenizer.test.js +425 -0
- package/dist/tokenizer.test.js.map +1 -0
- package/dist/types.d.ts +375 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
- package/src/casing.ts +64 -0
- package/src/command_line_parser.ts +249 -0
- package/src/compatibility_checker.ts +470 -0
- package/src/compiler.ts +435 -0
- package/src/config.ts +28 -0
- package/src/definition_finder.ts +221 -0
- package/src/encoding.ts +32 -0
- package/src/error_renderer.ts +278 -0
- package/src/formatter.ts +274 -0
- package/src/index.ts +6 -0
- package/src/io.ts +33 -0
- package/src/language_server.ts +301 -0
- package/src/literals.ts +120 -0
- package/src/module_collector.ts +22 -0
- package/src/module_set.ts +1175 -0
- package/src/parser.ts +1122 -0
- package/src/snapshotter.ts +136 -0
- package/src/tokenizer.ts +216 -0
- package/src/types.ts +518 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { caseMatches } from "./casing.js";
|
|
2
|
+
import {
|
|
3
|
+
BeforeAfter,
|
|
4
|
+
BreakingChange,
|
|
5
|
+
Expression,
|
|
6
|
+
getTokenForExpression,
|
|
7
|
+
} from "./compatibility_checker.js";
|
|
8
|
+
import { ModuleSet } from "./module_set.js";
|
|
9
|
+
import { RecordLocation, ResolvedType, SkirError, Token } from "./types.js";
|
|
10
|
+
|
|
11
|
+
export function renderErrors(errors: readonly SkirError[]): void {
|
|
12
|
+
const MAX_ERRORS = 10;
|
|
13
|
+
for (let i = 0; i < errors.length && i < MAX_ERRORS; ++i) {
|
|
14
|
+
const error = errors[i];
|
|
15
|
+
console.error(formatError(error!));
|
|
16
|
+
}
|
|
17
|
+
// Count the number of distinct modules with errors.
|
|
18
|
+
if (errors.length) {
|
|
19
|
+
const modules = new Set<string>();
|
|
20
|
+
for (const error of errors) {
|
|
21
|
+
modules.add(error.token.line.modulePath);
|
|
22
|
+
}
|
|
23
|
+
const numErrors = `${errors.length} error${errors.length <= 1 ? "" : "s"}`;
|
|
24
|
+
const numFiles = `${modules.size} file${modules.size <= 1 ? "" : "s"}`;
|
|
25
|
+
console.error(`Found ${numErrors} in ${numFiles}\n`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatError(error: SkirError): string {
|
|
30
|
+
const { token } = error;
|
|
31
|
+
const { line, colNumber } = token;
|
|
32
|
+
const lineNumberStr = (line.lineNumber + 1).toString();
|
|
33
|
+
let result = formatLocation(token);
|
|
34
|
+
result += " - ";
|
|
35
|
+
if (error.expected !== undefined) {
|
|
36
|
+
result += makeRed("expected");
|
|
37
|
+
result += `: ${error.expected}`;
|
|
38
|
+
} else {
|
|
39
|
+
result += makeRed("error");
|
|
40
|
+
result += `: ${error.message}`;
|
|
41
|
+
}
|
|
42
|
+
result += "\n\n";
|
|
43
|
+
result += makeBlackOnWhite(lineNumberStr);
|
|
44
|
+
result += " ";
|
|
45
|
+
result += line.line;
|
|
46
|
+
result += "\n";
|
|
47
|
+
result += makeBlackOnWhite(" ".repeat(lineNumberStr.length));
|
|
48
|
+
result += " ".repeat(colNumber + 1);
|
|
49
|
+
result += makeRed("~".repeat(Math.max(token.text.length, 1)));
|
|
50
|
+
result += "\n";
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function renderBreakingChanges(
|
|
55
|
+
breakingChanges: readonly BreakingChange[],
|
|
56
|
+
moduleSet: BeforeAfter<ModuleSet>,
|
|
57
|
+
): void {
|
|
58
|
+
const MAX = 10;
|
|
59
|
+
for (let i = 0; i < breakingChanges.length && i < MAX; ++i) {
|
|
60
|
+
const breakingChange = breakingChanges[i]!;
|
|
61
|
+
console.error(formatBreakingChange(breakingChange, moduleSet));
|
|
62
|
+
}
|
|
63
|
+
// Count the number of distinct modules with errors.
|
|
64
|
+
if (breakingChanges.length) {
|
|
65
|
+
console.error(`Found ${breakingChanges.length} errors\n`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatBreakingChange(
|
|
70
|
+
breakingChange: BreakingChange,
|
|
71
|
+
moduleSet: BeforeAfter<ModuleSet>,
|
|
72
|
+
): string {
|
|
73
|
+
switch (breakingChange.kind) {
|
|
74
|
+
case "illegal-type-change": {
|
|
75
|
+
const { expression, type } = breakingChange;
|
|
76
|
+
const location = formatLocation(getTokenForExpression(expression.after));
|
|
77
|
+
const errorHeader = makeRed("Illegal type change");
|
|
78
|
+
return [
|
|
79
|
+
`${location} - ${errorHeader}`,
|
|
80
|
+
" [Last snapshot]",
|
|
81
|
+
` Expression: ${formatExpression(expression.before)}`,
|
|
82
|
+
` Type: ${formatType(type.before, moduleSet.before)}`,
|
|
83
|
+
" [Now]",
|
|
84
|
+
` Expression: ${formatExpression(expression.after)}`,
|
|
85
|
+
` Type: ${formatType(type.after, moduleSet.after)}`,
|
|
86
|
+
].join("\n");
|
|
87
|
+
}
|
|
88
|
+
case "missing-slots": {
|
|
89
|
+
const { missingRangeEnd, missingRangeStart, recordExpression, record } =
|
|
90
|
+
breakingChange;
|
|
91
|
+
const location = formatLocation(record.after.record.name);
|
|
92
|
+
const errorHeader = makeRed("Missing slots in record");
|
|
93
|
+
return [
|
|
94
|
+
`${location} - ${errorHeader}`,
|
|
95
|
+
" [Last snapshot]",
|
|
96
|
+
` Expression: ${formatExpression(recordExpression.before)}`,
|
|
97
|
+
` Record: ${record.before.record.name.text}`,
|
|
98
|
+
` Slots: ${missingRangeEnd}`,
|
|
99
|
+
" [Now]",
|
|
100
|
+
` Expression: ${formatExpression(recordExpression.after)}`,
|
|
101
|
+
` Record: ${record.after.record.name.text}`,
|
|
102
|
+
` Slots: ${missingRangeStart}`,
|
|
103
|
+
" Fix: mark the field numbers as removed",
|
|
104
|
+
].join("\n");
|
|
105
|
+
}
|
|
106
|
+
case "missing-record": {
|
|
107
|
+
const { record, recordNumber } = breakingChange;
|
|
108
|
+
const recordDefinition = [
|
|
109
|
+
record.record.recordType,
|
|
110
|
+
" ",
|
|
111
|
+
getQualifiedName(record),
|
|
112
|
+
`(${recordNumber})`,
|
|
113
|
+
].join("");
|
|
114
|
+
return [
|
|
115
|
+
makeRed("Missing record"),
|
|
116
|
+
" [Last snapshot]",
|
|
117
|
+
` Record: ${recordDefinition}`,
|
|
118
|
+
].join("\n");
|
|
119
|
+
}
|
|
120
|
+
case "missing-method": {
|
|
121
|
+
const { method } = breakingChange;
|
|
122
|
+
const methodDefinition = [
|
|
123
|
+
"method ",
|
|
124
|
+
method.name.text,
|
|
125
|
+
"(",
|
|
126
|
+
formatType(method.requestType!, moduleSet.before),
|
|
127
|
+
"): ",
|
|
128
|
+
formatType(method.responseType!, moduleSet.before),
|
|
129
|
+
` = ${method.number}`,
|
|
130
|
+
].join("");
|
|
131
|
+
return [
|
|
132
|
+
makeRed("Missing method"),
|
|
133
|
+
" [Last snapshot]",
|
|
134
|
+
` Method: ${methodDefinition}`,
|
|
135
|
+
].join("\n");
|
|
136
|
+
}
|
|
137
|
+
case "enum-variant-kind-change": {
|
|
138
|
+
const { record, variantName, number } = breakingChange;
|
|
139
|
+
const location = formatLocation(record.after.record.name);
|
|
140
|
+
const errorHeader = makeRed("Illegal variant kind change");
|
|
141
|
+
const enumName = map(record, getQualifiedName);
|
|
142
|
+
const variantKind = map(variantName, (vn) => {
|
|
143
|
+
caseMatches(vn.text, "lower_underscore") ? "wrapper" : "constant";
|
|
144
|
+
});
|
|
145
|
+
return [
|
|
146
|
+
`${location} - ${errorHeader}`,
|
|
147
|
+
" [Last snapshot]",
|
|
148
|
+
` Enum: ${enumName.before}`,
|
|
149
|
+
` Variant: ${variantName.before.text} = ${number}`,
|
|
150
|
+
` Kind: ${variantKind.before}`,
|
|
151
|
+
" [Now]",
|
|
152
|
+
` Enum: ${enumName.after}`,
|
|
153
|
+
` Variant: ${variantName.after.text} = ${number}`,
|
|
154
|
+
` Kind: ${variantKind.after}`,
|
|
155
|
+
].join("\n");
|
|
156
|
+
}
|
|
157
|
+
case "record-kind-change": {
|
|
158
|
+
const { record, recordExpression, recordType } = breakingChange;
|
|
159
|
+
const location = formatLocation(record.after.record.name);
|
|
160
|
+
const errorHeader = makeRed("Record kind change");
|
|
161
|
+
return [
|
|
162
|
+
`${location} - ${errorHeader}`,
|
|
163
|
+
" [Last snapshot]",
|
|
164
|
+
` Expression: ${formatExpression(recordExpression.before)}`,
|
|
165
|
+
` Record: ${record.before.record.name.text}`,
|
|
166
|
+
` Kind: ${recordType.before}`,
|
|
167
|
+
" [Now]",
|
|
168
|
+
` Expression: ${formatExpression(recordExpression.after)}`,
|
|
169
|
+
` Record: ${record.after.record.name.text}`,
|
|
170
|
+
` Kind: ${recordType.after}`,
|
|
171
|
+
].join("\n");
|
|
172
|
+
}
|
|
173
|
+
case "removed-number-reintroduced": {
|
|
174
|
+
const { record, recordExpression, reintroducedAs, removedNumber } =
|
|
175
|
+
breakingChange;
|
|
176
|
+
const location = formatLocation(record.after.record.name);
|
|
177
|
+
const errorHeader = makeRed("Removed number reintroduced");
|
|
178
|
+
return [
|
|
179
|
+
`${location} - ${errorHeader}`,
|
|
180
|
+
" [Last snapshot]",
|
|
181
|
+
` Expression: ${formatExpression(recordExpression.before)}`,
|
|
182
|
+
` Record: ${record.before.record.name.text}`,
|
|
183
|
+
` Removed: ${removedNumber}`,
|
|
184
|
+
" [Now]",
|
|
185
|
+
` Expression: ${formatExpression(recordExpression.after)}`,
|
|
186
|
+
` Record: ${record.after.record.name.text}`,
|
|
187
|
+
record.after.record.recordType === "struct"
|
|
188
|
+
? ` Field: ${reintroducedAs.text}`
|
|
189
|
+
: ` Variant: ${reintroducedAs.text}`,
|
|
190
|
+
].join("\n");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function formatLocation(token: Token): string {
|
|
196
|
+
const { line, colNumber } = token;
|
|
197
|
+
return [
|
|
198
|
+
makeCyan(line.modulePath),
|
|
199
|
+
makeYellow((line.lineNumber + 1).toString()),
|
|
200
|
+
makeYellow((colNumber + 1).toString()),
|
|
201
|
+
].join(":");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function formatExpression(expression: Expression): string {
|
|
205
|
+
switch (expression.kind) {
|
|
206
|
+
case "request-type":
|
|
207
|
+
return `(${expression.methodName.text}::request)`;
|
|
208
|
+
case "response-type":
|
|
209
|
+
return `(${expression.methodName.text}::response)`;
|
|
210
|
+
case "record":
|
|
211
|
+
return `${expression.recordName.text}`;
|
|
212
|
+
case "item":
|
|
213
|
+
return formatExpression(expression.arrayExpression) + "[*]";
|
|
214
|
+
case "optional-value":
|
|
215
|
+
return formatExpression(expression.optionalExpression) + "!";
|
|
216
|
+
case "property": {
|
|
217
|
+
const structExpression = formatExpression(expression.structExpression);
|
|
218
|
+
return `${structExpression}.${expression.fieldName.text}`;
|
|
219
|
+
}
|
|
220
|
+
case "as-variant": {
|
|
221
|
+
const enumExpression = formatExpression(expression.enumExpression);
|
|
222
|
+
return `${enumExpression}.as_${expression.variantName.text}`;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function formatType(resolvedType: ResolvedType, moduleSet: ModuleSet): string {
|
|
228
|
+
switch (resolvedType.kind) {
|
|
229
|
+
case "array":
|
|
230
|
+
return `[${formatType(resolvedType.item, moduleSet)}]`;
|
|
231
|
+
case "optional":
|
|
232
|
+
return `${formatType(resolvedType.other, moduleSet)}?`;
|
|
233
|
+
case "primitive":
|
|
234
|
+
return resolvedType.primitive;
|
|
235
|
+
case "record": {
|
|
236
|
+
const record = moduleSet.recordMap.get(resolvedType.key)!;
|
|
237
|
+
return getQualifiedName(record);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getQualifiedName(recordLocation: RecordLocation): string {
|
|
243
|
+
return recordLocation.recordAncestors.map((r) => r.name.text).join(".");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function map<T, U>(
|
|
247
|
+
beforeAfter: BeforeAfter<T>,
|
|
248
|
+
fn: (value: T) => U,
|
|
249
|
+
): BeforeAfter<U> {
|
|
250
|
+
return {
|
|
251
|
+
before: fn(beforeAfter.before),
|
|
252
|
+
after: fn(beforeAfter.after),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function makeRed(text: string): string {
|
|
257
|
+
return `\x1b[31m${text}\x1b[0m`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function makeGreen(text: string): string {
|
|
261
|
+
return `\x1b[32m${text}\x1b[0m`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function makeGray(text: string): string {
|
|
265
|
+
return `\x1b[90m${text}\x1b[0m`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function makeCyan(text: string): string {
|
|
269
|
+
return `\x1b[36m${text}\x1b[0m`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function makeYellow(text: string): string {
|
|
273
|
+
return `\x1b[33m${text}\x1b[0m`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function makeBlackOnWhite(text: string): string {
|
|
277
|
+
return `\x1b[47m${text}\x1b[0m`;
|
|
278
|
+
}
|
package/src/formatter.ts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import type { Token } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export function formatModule(tokens: readonly Token[]): string {
|
|
4
|
+
const sink = new CodeSink();
|
|
5
|
+
let inValue = false;
|
|
6
|
+
let indentDepth = 0;
|
|
7
|
+
const iterator = new TokenIterator(tokens);
|
|
8
|
+
|
|
9
|
+
const copyInlineComments = (): void => {
|
|
10
|
+
while (
|
|
11
|
+
iterator.hasNext() &&
|
|
12
|
+
isComment(iterator.peek().text) &&
|
|
13
|
+
iterator.peek().line.lineNumber === iterator.current.line.lineNumber
|
|
14
|
+
) {
|
|
15
|
+
// Preserve comments before line break.
|
|
16
|
+
sink.write(" " + iterator.next().text);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const breakLine = (): void => {
|
|
21
|
+
copyInlineComments();
|
|
22
|
+
const { lastLineOnlyHasWhitespaces } = sink;
|
|
23
|
+
if (iterator.hasNext()) {
|
|
24
|
+
const current = iterator.current;
|
|
25
|
+
const next = iterator.peek();
|
|
26
|
+
if (next.line.lineNumber >= current.line.lineNumber + 2) {
|
|
27
|
+
// Preserve double line breaks.
|
|
28
|
+
sink.write("\n");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!lastLineOnlyHasWhitespaces) {
|
|
32
|
+
sink.write("\n" + " ".repeat(indentDepth));
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const breakLineAndIndent = (): void => {
|
|
37
|
+
++indentDepth;
|
|
38
|
+
breakLine();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const unindent = (): void => {
|
|
42
|
+
--indentDepth;
|
|
43
|
+
sink.removeWhitespaceSuffix(" ");
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
while (iterator.hasNext()) {
|
|
47
|
+
const token = iterator.next();
|
|
48
|
+
switch (token.text) {
|
|
49
|
+
case "as":
|
|
50
|
+
case "const":
|
|
51
|
+
case "enum":
|
|
52
|
+
case "import":
|
|
53
|
+
case "method":
|
|
54
|
+
case "struct":
|
|
55
|
+
case "*":
|
|
56
|
+
case ":": {
|
|
57
|
+
sink.write(token.text + " ");
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case "from": {
|
|
61
|
+
sink.write(" from ");
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case "removed": {
|
|
65
|
+
if (iterator.hasNext() && iterator.peek().text === ";") {
|
|
66
|
+
sink.write("removed");
|
|
67
|
+
} else {
|
|
68
|
+
sink.write("removed ");
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case "{": {
|
|
73
|
+
if (iterator.hasNext() && iterator.peek().text === "}") {
|
|
74
|
+
sink.write(inValue ? "{}" : " {}");
|
|
75
|
+
iterator.next();
|
|
76
|
+
if (!inValue) {
|
|
77
|
+
breakLine();
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
sink.write(inValue ? "{" : " {");
|
|
81
|
+
breakLineAndIndent();
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case "{|": {
|
|
86
|
+
if (iterator.hasNext() && iterator.peek().text === "|}") {
|
|
87
|
+
sink.write(inValue ? "{||}" : " {||}");
|
|
88
|
+
iterator.next();
|
|
89
|
+
if (!inValue) {
|
|
90
|
+
breakLine();
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
sink.write(inValue ? "{|" : " {|");
|
|
94
|
+
breakLineAndIndent();
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case "}": {
|
|
99
|
+
if (inValue) {
|
|
100
|
+
sink.maybeWriteTrailingComma();
|
|
101
|
+
breakLine();
|
|
102
|
+
}
|
|
103
|
+
unindent();
|
|
104
|
+
sink.write("}");
|
|
105
|
+
if (!inValue) {
|
|
106
|
+
breakLine();
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case "|}": {
|
|
111
|
+
if (inValue) {
|
|
112
|
+
sink.maybeWriteTrailingComma();
|
|
113
|
+
breakLine();
|
|
114
|
+
}
|
|
115
|
+
unindent();
|
|
116
|
+
sink.write("|}");
|
|
117
|
+
if (!inValue) {
|
|
118
|
+
breakLine();
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case "[": {
|
|
123
|
+
if (iterator.hasNext() && iterator.peek().text === "]") {
|
|
124
|
+
sink.write("[]");
|
|
125
|
+
iterator.next();
|
|
126
|
+
} else {
|
|
127
|
+
sink.write("[");
|
|
128
|
+
if (inValue) {
|
|
129
|
+
breakLineAndIndent();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case "]": {
|
|
135
|
+
if (inValue) {
|
|
136
|
+
sink.maybeWriteTrailingComma();
|
|
137
|
+
breakLine();
|
|
138
|
+
unindent();
|
|
139
|
+
}
|
|
140
|
+
sink.write("]");
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case ";": {
|
|
144
|
+
sink.write(";");
|
|
145
|
+
inValue = false;
|
|
146
|
+
breakLine();
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case "=": {
|
|
150
|
+
inValue = true;
|
|
151
|
+
sink.write(" = ");
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case ",": {
|
|
155
|
+
if (inValue) {
|
|
156
|
+
sink.write(",");
|
|
157
|
+
breakLine();
|
|
158
|
+
} else {
|
|
159
|
+
sink.write(", ");
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
default: {
|
|
164
|
+
if (isComment(token.text)) {
|
|
165
|
+
sink.writeComment(token.text);
|
|
166
|
+
breakLine();
|
|
167
|
+
} else if (token.text.startsWith("'")) {
|
|
168
|
+
const unescapedDoubleQuoteRegex = /(?:^|[^\\])(?:\\\\)*"/;
|
|
169
|
+
if (unescapedDoubleQuoteRegex.test(token.text)) {
|
|
170
|
+
sink.write(token.text);
|
|
171
|
+
} else {
|
|
172
|
+
// Switch to double quotes.
|
|
173
|
+
const unquoted = token.text.slice(1, -1);
|
|
174
|
+
sink.write(`"${unquoted}"`);
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
sink.write(token.text);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const result = sink.code;
|
|
184
|
+
if (indentDepth !== 0) {
|
|
185
|
+
throw new Error(`result=${result}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
class TokenIterator {
|
|
192
|
+
private nextIndex = 0;
|
|
193
|
+
|
|
194
|
+
constructor(private readonly tokens: readonly Token[]) {}
|
|
195
|
+
|
|
196
|
+
next(): Token {
|
|
197
|
+
if (this.nextIndex < this.tokens.length) {
|
|
198
|
+
return this.tokens[this.nextIndex++]!;
|
|
199
|
+
}
|
|
200
|
+
throw new Error();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
peek(): Token {
|
|
204
|
+
if (this.nextIndex < this.tokens.length) {
|
|
205
|
+
return this.tokens[this.nextIndex]!;
|
|
206
|
+
}
|
|
207
|
+
throw new Error();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
hasNext(): boolean {
|
|
211
|
+
return this.nextIndex < this.tokens.length;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
get current(): Token {
|
|
215
|
+
const index = this.nextIndex - 1;
|
|
216
|
+
if (index < this.tokens.length) {
|
|
217
|
+
return this.tokens[index]!;
|
|
218
|
+
}
|
|
219
|
+
throw new Error();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isComment(token: string): boolean {
|
|
224
|
+
return token.startsWith("//") || token.startsWith("/*");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
class CodeSink {
|
|
228
|
+
private _code: string = "";
|
|
229
|
+
// Position after the last non-whitespace character which is not part of a
|
|
230
|
+
// comment.
|
|
231
|
+
private endPosition = 0;
|
|
232
|
+
|
|
233
|
+
/** Writes a token possibly preceded or followed by whitespaces. */
|
|
234
|
+
write(text: string): void {
|
|
235
|
+
const trimmed = text.trim();
|
|
236
|
+
if (trimmed && !isComment(trimmed)) {
|
|
237
|
+
this.endPosition = trimmed.endsWith(",")
|
|
238
|
+
? 0
|
|
239
|
+
: this.code.length + text.trimEnd().length;
|
|
240
|
+
}
|
|
241
|
+
this._code += text;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
writeComment(text: string): void {
|
|
245
|
+
if (this.lastLineOnlyHasWhitespaces) {
|
|
246
|
+
this._code += text;
|
|
247
|
+
} else {
|
|
248
|
+
this._code = this.code.trimEnd() + " " + text;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
maybeWriteTrailingComma(): void {
|
|
253
|
+
if (this.endPosition === 0) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const { code } = this;
|
|
257
|
+
this._code =
|
|
258
|
+
code.slice(0, this.endPosition) + "," + code.slice(this.endPosition);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
removeWhitespaceSuffix(suffix: string): void {
|
|
262
|
+
if (this.code.endsWith(suffix)) {
|
|
263
|
+
this._code = this.code.slice(0, -suffix.length);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
get lastLineOnlyHasWhitespaces(): boolean {
|
|
268
|
+
return /^$|\n\s*$/.test(this.code);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
get code(): string {
|
|
272
|
+
return this._code;
|
|
273
|
+
}
|
|
274
|
+
}
|
package/src/index.ts
ADDED
package/src/io.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
|
|
3
|
+
export interface FileReader {
|
|
4
|
+
readTextFile(path: string): string | undefined;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface FileWriter {
|
|
8
|
+
writeTextFile(path: string, contents: string): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class RealFileSystem implements FileReader, FileWriter {
|
|
12
|
+
readTextFile(path: string): string | undefined {
|
|
13
|
+
try {
|
|
14
|
+
return fs.readFileSync(path, "utf-8");
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (
|
|
17
|
+
error &&
|
|
18
|
+
typeof error === "object" &&
|
|
19
|
+
"code" in error &&
|
|
20
|
+
error.code === "ENOENT"
|
|
21
|
+
) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
writeTextFile(path: string, contents: string): void {
|
|
29
|
+
fs.writeFileSync(path, contents, "utf-8");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const REAL_FILE_SYSTEM = new RealFileSystem();
|