storm-lua-minify 0.1.0

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/ast2lua.ts ADDED
@@ -0,0 +1,799 @@
1
+ // based on "luamin": Copyright Mathias Bynens <https://mathiasbynens.be/>
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ /* eslint-disable @typescript-eslint/no-unnecessary-condition */
5
+ import Parser, { Comment } from "luaparse";
6
+ import { SourceNode } from "source-map";
7
+
8
+ export type Chunk = Parser.Chunk & {
9
+ globals?: (Parser.Base<"Identifer"> & {
10
+ name: string;
11
+ isLocal: boolean;
12
+ })[];
13
+ comments?: Comment[];
14
+ };
15
+
16
+ const PRECEDENCE: Record<string, number> = {
17
+ or: 1,
18
+ and: 2,
19
+ "<": 3,
20
+ ">": 3,
21
+ "<=": 3,
22
+ ">=": 3,
23
+ "~=": 3,
24
+ "==": 3,
25
+ "..": 5,
26
+ "+": 6,
27
+ "-": 6, // binary -
28
+ "*": 7,
29
+ "/": 7,
30
+ "%": 7,
31
+ unarynot: 8,
32
+ "unary#": 8,
33
+ "unary-": 8, // unary -
34
+ "^": 10,
35
+ };
36
+
37
+ const IDENTIFIER_PARTS = [
38
+ "0",
39
+ "1",
40
+ "2",
41
+ "3",
42
+ "4",
43
+ "5",
44
+ "6",
45
+ "7",
46
+ "8",
47
+ "9",
48
+ "a",
49
+ "b",
50
+ "c",
51
+ "d",
52
+ "e",
53
+ "f",
54
+ "g",
55
+ "h",
56
+ "i",
57
+ "j",
58
+ "k",
59
+ "l",
60
+ "m",
61
+ "n",
62
+ "o",
63
+ "p",
64
+ "q",
65
+ "r",
66
+ "s",
67
+ "t",
68
+ "u",
69
+ "v",
70
+ "w",
71
+ "x",
72
+ "y",
73
+ "z",
74
+ "A",
75
+ "B",
76
+ "C",
77
+ "D",
78
+ "E",
79
+ "F",
80
+ "G",
81
+ "H",
82
+ "I",
83
+ "J",
84
+ "K",
85
+ "L",
86
+ "M",
87
+ "N",
88
+ "O",
89
+ "P",
90
+ "Q",
91
+ "R",
92
+ "S",
93
+ "T",
94
+ "U",
95
+ "V",
96
+ "W",
97
+ "X",
98
+ "Y",
99
+ "Z",
100
+ "_",
101
+ ];
102
+
103
+ const identifierMap = new Map<string, string>();
104
+ const identifiersInUse = new Set<string>();
105
+
106
+ function wrapArray<T>(obj: T | T[]): T[] {
107
+ if (Array.isArray(obj)) {
108
+ return obj;
109
+ }
110
+ return [obj];
111
+ }
112
+
113
+ function generateZeroes(length: number) {
114
+ let zero = "0";
115
+ let result = "";
116
+ if (length < 1) {
117
+ return result;
118
+ }
119
+ if (length == 1) {
120
+ return zero;
121
+ }
122
+ while (length) {
123
+ if (length & 1) {
124
+ result += zero;
125
+ }
126
+ // eslint-disable-next-line no-cond-assign
127
+ if ((length >>= 1)) {
128
+ zero += zero;
129
+ }
130
+ }
131
+ return result;
132
+ }
133
+
134
+ function isKeyword(id: string) {
135
+ switch (id.length) {
136
+ case 2:
137
+ return "do" == id || "if" == id || "in" == id || "or" == id;
138
+ case 3:
139
+ return (
140
+ "and" == id || "end" == id || "for" == id || "nil" == id || "not" == id
141
+ );
142
+ case 4:
143
+ return "else" == id || "goto" == id || "then" == id || "true" == id;
144
+ case 5:
145
+ return (
146
+ "break" == id ||
147
+ "false" == id ||
148
+ "local" == id ||
149
+ "until" == id ||
150
+ "while" == id
151
+ );
152
+ case 6:
153
+ return "elseif" == id || "repeat" == id || "return" == id;
154
+ case 8:
155
+ return "function" == id;
156
+ }
157
+ return false;
158
+ }
159
+
160
+ function isNeedSeparator(a: string, b: string) {
161
+ const lastCharA = a.slice(-1);
162
+ const firstCharB = b.charAt(0);
163
+
164
+ const regexAlphaUnderscore = /[a-zA-Z_]/;
165
+ const regexAlphaNumUnderscore = /[a-zA-Z0-9_]/;
166
+ const regexDigits = /[0-9]/;
167
+
168
+ if (lastCharA == "" || firstCharB == "") {
169
+ return false;
170
+ }
171
+ if (regexAlphaUnderscore.test(lastCharA)) {
172
+ if (regexAlphaNumUnderscore.test(firstCharB)) {
173
+ // e.g. `while` + `1`
174
+ // e.g. `local a` + `local b`
175
+ return true;
176
+ } else {
177
+ // e.g. `not` + `(2>3 or 3<2)`
178
+ // e.g. `x` + `^`
179
+ return false;
180
+ }
181
+ }
182
+ if (regexDigits.test(lastCharA)) {
183
+ if (
184
+ firstCharB == "(" ||
185
+ !(firstCharB == "." || regexAlphaUnderscore.test(firstCharB))
186
+ ) {
187
+ // e.g. `1` + `+`
188
+ // e.g. `1` + `==`
189
+ return false;
190
+ } else {
191
+ // e.g. `1` + `..`
192
+ // e.g. `1` + `and`
193
+ return true;
194
+ }
195
+ }
196
+ if (lastCharA == firstCharB && lastCharA == "-") {
197
+ // e.g. `1-` + `-2`
198
+ return true;
199
+ }
200
+ const secondLastCharA = a.slice(-2, -1);
201
+ if (
202
+ lastCharA == "." &&
203
+ secondLastCharA != "." &&
204
+ regexAlphaNumUnderscore.test(firstCharB)
205
+ ) {
206
+ // e.g. `1.` + `print`
207
+ return true;
208
+ }
209
+ return false;
210
+ }
211
+
212
+ interface ExpressionOptoions {
213
+ precedence?: number;
214
+ preserveIdentifiers?: boolean;
215
+ direction?: "left" | "right" | undefined;
216
+ parent?: string | undefined;
217
+ }
218
+
219
+ function addWithSeparator(
220
+ val: SourceNode,
221
+ adding: (string | SourceNode)[] | SourceNode | string,
222
+ separator = " "
223
+ ) {
224
+ if (
225
+ isNeedSeparator(
226
+ val.toString(),
227
+ wrapArray(adding)
228
+ .map((p) => p.toString())
229
+ .join()
230
+ )
231
+ ) {
232
+ val.add(separator);
233
+ }
234
+ val.add(adding);
235
+ return val;
236
+ }
237
+
238
+ function prependWithSeparator(
239
+ val: SourceNode,
240
+ prepending: (string | SourceNode)[] | SourceNode | string,
241
+ separator = " "
242
+ ) {
243
+ if (
244
+ isNeedSeparator(
245
+ wrapArray(prepending)
246
+ .map((p) => p.toString())
247
+ .join(),
248
+ val.toString()
249
+ )
250
+ ) {
251
+ val.prepend(separator);
252
+ }
253
+ val.prepend(prepending);
254
+ return val;
255
+ }
256
+
257
+ function insertSeparator(
258
+ a: string | SourceNode,
259
+ b: string | SourceNode,
260
+ separator = " "
261
+ ) {
262
+ return isNeedSeparator(a.toString(), b.toString()) ? separator : undefined;
263
+ }
264
+
265
+ export class Minifier {
266
+ private fileName: string;
267
+ private ast: Chunk;
268
+ private requireHelper: (fileName: string) => SourceNode | undefined;
269
+
270
+ constructor(
271
+ fileName: string,
272
+ ast: Chunk,
273
+ requireHelper: (fileName: string) => SourceNode | undefined
274
+ ) {
275
+ this.fileName = fileName;
276
+ this.ast = ast;
277
+ this.requireHelper = requireHelper;
278
+ ast.globals?.map((v) => identifiersInUse.add(v.name));
279
+ }
280
+
281
+ parse() {
282
+ const body = this.formatStatementList(this.ast.body);
283
+ /*if (this.ast.comments) {
284
+ const comments = this.ast.comments as Comment[];
285
+ comments.reverse().filter(v => v.raw.includes("--#") || v.raw.includes("[[#")).forEach(comment => {
286
+ body.prepend(this.sourceNodeHelper(comment, comment.raw));
287
+ })
288
+ return body;
289
+ } else*/ {
290
+ return body;
291
+ }
292
+ }
293
+
294
+ private sourceNodeHelper(
295
+ node: Parser.Node | undefined,
296
+ chuncks: (SourceNode | string)[] | SourceNode | string,
297
+ name?: string
298
+ ) {
299
+ const line = node?.loc?.start.line;
300
+ const column = node?.loc?.start.column;
301
+ return new SourceNode(
302
+ line == undefined ? null : line,
303
+ column == undefined ? null : column,
304
+ this.fileName, // 本当に自分のファイル名でよいかは要検討
305
+ chuncks,
306
+ name
307
+ );
308
+ }
309
+
310
+ private formatStatementList(body: Parser.Statement[] | Parser.Statement) {
311
+ const result = this.sourceNodeHelper(undefined, []);
312
+ wrapArray(body).forEach((statement) => {
313
+ addWithSeparator(result, this.formatStatement(statement), "\n");
314
+ });
315
+ return result;
316
+ }
317
+
318
+ private formatStatement(statement: Parser.Statement): SourceNode {
319
+ if (statement.type == "AssignmentStatement") {
320
+ // left-hand side
321
+ const variables = statement.variables
322
+ .map((variable) => [this.formatExpression(variable), ","])
323
+ .flat();
324
+ const inits = statement.init
325
+ .map((init) => [this.formatExpression(init), ","])
326
+ .flat();
327
+
328
+ const result = this.sourceNodeHelper(
329
+ statement,
330
+ this.sourceNodeHelper(undefined, variables.slice(0, -1))
331
+ );
332
+ addWithSeparator(result, "=");
333
+ addWithSeparator(
334
+ result,
335
+ this.sourceNodeHelper(undefined, inits.slice(0, -1))
336
+ );
337
+ return result;
338
+ } else if (statement.type == "LocalStatement") {
339
+ const variables = statement.variables
340
+ .map((variable) => [this.formatExpression(variable), ","])
341
+ .flat();
342
+ const result = this.sourceNodeHelper(statement, [
343
+ "local ",
344
+ this.sourceNodeHelper(undefined, variables.slice(0, -1)),
345
+ ]);
346
+
347
+ if (statement.init.length) {
348
+ const inits = statement.init
349
+ .map((init) => [this.formatExpression(init), ","])
350
+ .flat();
351
+
352
+ addWithSeparator(result, "=");
353
+ addWithSeparator(
354
+ result,
355
+ this.sourceNodeHelper(undefined, inits.slice(0, -1))
356
+ );
357
+ }
358
+ return result;
359
+ } else if (statement.type == "CallStatement") {
360
+ return this.formatExpression(statement.expression); // NOTE: もう一度囲んでもいい
361
+ } else if (statement.type == "IfStatement") {
362
+ const result = this.sourceNodeHelper(statement, []);
363
+ statement.clauses.forEach((clause) => {
364
+ const clauseMap = this.sourceNodeHelper(clause, []);
365
+ if (clause.type == "IfClause") {
366
+ addWithSeparator(clauseMap, "if");
367
+ addWithSeparator(clauseMap, this.formatExpression(clause.condition));
368
+ addWithSeparator(clauseMap, "then");
369
+ } else if (clause.type == "ElseifClause") {
370
+ addWithSeparator(clauseMap, "elseif");
371
+ addWithSeparator(clauseMap, this.formatExpression(clause.condition));
372
+ addWithSeparator(clauseMap, "then");
373
+ } else {
374
+ addWithSeparator(clauseMap, "else");
375
+ }
376
+ addWithSeparator(clauseMap, this.formatStatementList(clause.body));
377
+ addWithSeparator(result, clauseMap);
378
+ });
379
+ addWithSeparator(result, "end");
380
+ return result;
381
+ } else if (statement.type == "WhileStatement") {
382
+ const result = this.sourceNodeHelper(statement, "while");
383
+ addWithSeparator(result, this.formatExpression(statement.condition));
384
+ addWithSeparator(result, "do");
385
+ addWithSeparator(result, this.formatStatementList(statement.body));
386
+ addWithSeparator(result, "end");
387
+ return result;
388
+ } else if (statement.type == "DoStatement") {
389
+ const result = this.sourceNodeHelper(statement, "do");
390
+ addWithSeparator(result, this.formatStatementList(statement.body));
391
+ addWithSeparator(result, "end");
392
+ return result;
393
+ } else if (statement.type == "ReturnStatement") {
394
+ const result = this.sourceNodeHelper(statement, "return");
395
+ if (statement.arguments.length) {
396
+ const returns = statement.arguments
397
+ .map((argument) => [this.formatExpression(argument), ","])
398
+ .flat();
399
+ addWithSeparator(result, returns.slice(0, -1));
400
+ }
401
+ return result;
402
+ } else if (statement.type == "BreakStatement") {
403
+ return this.sourceNodeHelper(statement, "break");
404
+ } else if (statement.type == "RepeatStatement") {
405
+ const result = this.sourceNodeHelper(statement, "repeat");
406
+ addWithSeparator(result, this.formatStatementList(statement.body));
407
+ addWithSeparator(result, "until");
408
+ addWithSeparator(result, this.formatExpression(statement.condition));
409
+ return result;
410
+ } else if (statement.type == "FunctionDeclaration") {
411
+ const result = this.sourceNodeHelper(
412
+ statement,
413
+ (statement.isLocal ? "local " : "") + "function "
414
+ );
415
+ if (statement.identifier) {
416
+ addWithSeparator(result, this.formatExpression(statement.identifier));
417
+ }
418
+ addWithSeparator(result, "(");
419
+
420
+ if (statement.parameters.length) {
421
+ const parameters = statement.parameters
422
+ .map((parameter) => {
423
+ return [
424
+ parameter.type == "Identifier"
425
+ ? this.generateIdentifier(parameter)
426
+ : parameter.value,
427
+ ",",
428
+ ];
429
+ })
430
+ .flat();
431
+ addWithSeparator(result, parameters.slice(0, -1));
432
+ }
433
+
434
+ addWithSeparator(result, ")");
435
+ addWithSeparator(result, this.formatStatementList(statement.body));
436
+ addWithSeparator(result, "end");
437
+ return result;
438
+ } else if (statement.type == "ForGenericStatement") {
439
+ // see also `ForNumericStatement`
440
+ const result = this.sourceNodeHelper(statement, "for");
441
+ const variables = statement.variables
442
+ .map((variable) => [this.generateIdentifier(variable), ","])
443
+ .flat();
444
+ const iterators = statement.iterators
445
+ .map((iterator) => [this.formatExpression(iterator), ","])
446
+ .flat();
447
+ addWithSeparator(result, variables.slice(0, -1));
448
+ addWithSeparator(result, "in");
449
+ addWithSeparator(result, iterators.slice(0, -1));
450
+ addWithSeparator(result, "do");
451
+ addWithSeparator(result, this.formatStatementList(statement.body));
452
+ addWithSeparator(result, "end");
453
+ return result;
454
+ } else if (statement.type == "ForNumericStatement") {
455
+ // The variables in a `ForNumericStatement` are always local
456
+ const result = this.sourceNodeHelper(statement, "for");
457
+ addWithSeparator(result, this.generateIdentifier(statement.variable));
458
+ addWithSeparator(result, "=");
459
+ addWithSeparator(result, this.formatExpression(statement.start));
460
+ addWithSeparator(result, ",");
461
+ addWithSeparator(result, this.formatExpression(statement.end));
462
+
463
+ if (statement.step) {
464
+ addWithSeparator(result, ",");
465
+ addWithSeparator(result, this.formatExpression(statement.step));
466
+ }
467
+
468
+ addWithSeparator(result, "do");
469
+ addWithSeparator(result, this.formatStatementList(statement.body));
470
+ addWithSeparator(result, "end");
471
+ return result;
472
+ } else if (statement.type == "LabelStatement") {
473
+ // The identifier names in a `LabelStatement` can safely be renamed
474
+ return this.sourceNodeHelper(statement, [
475
+ "::",
476
+ this.generateIdentifier(statement.label),
477
+ "::",
478
+ ]);
479
+ } else if (statement.type == "GotoStatement") {
480
+ // The identifier names in a `GotoStatement` can safely be renamed
481
+ return this.sourceNodeHelper(statement, [
482
+ "goto ",
483
+ this.generateIdentifier(statement.label),
484
+ ]);
485
+ } else {
486
+ throw TypeError(
487
+ "Unknown statement type: `" + JSON.stringify(statement) + "`"
488
+ );
489
+ }
490
+ }
491
+
492
+ /*function joinStatements(a: string | SourceNode, b: string | SourceNode, separator = " ") {
493
+ return isNeedSeparator(a.toString(), b.toString()) ? a.toString() + separator + b.toString() : a.toString() + b.toString();
494
+ }*/
495
+
496
+ private formatExpression(
497
+ expression: Parser.Expression,
498
+ argOptions?: ExpressionOptoions
499
+ ): SourceNode {
500
+ if (expression.type == "Identifier") {
501
+ return this.sourceNodeHelper(
502
+ expression,
503
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error
504
+ //@ts-ignore
505
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
506
+ expression.isLocal
507
+ ? this.generateIdentifier(expression, true)
508
+ : expression.name,
509
+ expression.name
510
+ );
511
+ } else if (
512
+ expression.type == "StringLiteral" ||
513
+ expression.type == "NumericLiteral" ||
514
+ expression.type == "BooleanLiteral" ||
515
+ expression.type == "NilLiteral" ||
516
+ expression.type == "VarargLiteral"
517
+ ) {
518
+ return this.sourceNodeHelper(expression, expression.raw);
519
+ } else if (
520
+ expression.type == "LogicalExpression" ||
521
+ expression.type == "BinaryExpression"
522
+ ) {
523
+ const operator = expression.operator;
524
+ const currentPrecedence = PRECEDENCE[operator];
525
+ let associativity: "left" | "right" = "left";
526
+ const options = {
527
+ precedence: 0,
528
+ preserveIdentifiers: false,
529
+ ...argOptions,
530
+ };
531
+
532
+ const leftHand = this.formatExpression(expression.left, {
533
+ precedence: currentPrecedence,
534
+ direction: "left",
535
+ parent: operator,
536
+ });
537
+ const rightHand = this.formatExpression(expression.right, {
538
+ precedence: currentPrecedence,
539
+ direction: "right",
540
+ parent: operator,
541
+ });
542
+ if (operator == "^" || operator == "..") {
543
+ associativity = "right";
544
+ } else if (
545
+ currentPrecedence < options.precedence ||
546
+ (currentPrecedence == options.precedence &&
547
+ associativity != options.direction &&
548
+ options.parent != "+" &&
549
+ !(options.parent == "*" && (operator == "/" || operator == "*")))
550
+ ) {
551
+ return this.sourceNodeHelper(
552
+ expression,
553
+ [
554
+ "(",
555
+ leftHand,
556
+ insertSeparator(leftHand, operator),
557
+ operator,
558
+ insertSeparator(operator, rightHand),
559
+ rightHand,
560
+ ")",
561
+ ].filter((p): p is Exclude<typeof p, undefined> => p !== undefined)
562
+ );
563
+ }
564
+ return this.sourceNodeHelper(
565
+ expression,
566
+ [
567
+ leftHand,
568
+ insertSeparator(leftHand, operator),
569
+ operator,
570
+ insertSeparator(operator, rightHand),
571
+ rightHand,
572
+ ].filter((p): p is Exclude<typeof p, undefined> => p !== undefined)
573
+ );
574
+ } else if (expression.type == "UnaryExpression") {
575
+ const operator = expression.operator;
576
+ const currentPrecedence = PRECEDENCE["unary" + operator];
577
+ const options = {
578
+ precedence: 0,
579
+ ...argOptions,
580
+ };
581
+
582
+ const p2 = this.formatExpression(expression.argument, {
583
+ precedence: currentPrecedence,
584
+ });
585
+ const result = this.sourceNodeHelper(
586
+ expression,
587
+ [operator, insertSeparator(operator, p2), p2].filter(
588
+ (p): p is Exclude<typeof p, undefined> => p !== undefined
589
+ )
590
+ );
591
+
592
+ if (
593
+ currentPrecedence < options.precedence &&
594
+ // In principle, we should parenthesize the RHS of an
595
+ // expression like `3^-2`, because `^` has higher precedence
596
+ // than unary `-` according to the manual. But that is
597
+ // misleading on the RHS of `^`, since the parser will
598
+ // always try to find a unary operator regardless of
599
+ // precedence.
600
+ !(options.parent == "^" && options.direction == "right")
601
+ ) {
602
+ result.prepend("(");
603
+ result.add(")");
604
+ }
605
+ return result;
606
+ } else if (expression.type == "CallExpression") {
607
+ // requireの展開モードは2種類: SLモード-その場に読み込み, FLモード-require相当の関数で呼び出し
608
+ if (
609
+ this.formatBase(expression.base).toString() == "require" &&
610
+ expression?.arguments[0].type === "StringLiteral"
611
+ ) {
612
+ const res = this.requireHelper(
613
+ expression.arguments[0].raw.replaceAll('"', "")
614
+ );
615
+ if (res != undefined) {
616
+ return res;
617
+ }
618
+ }
619
+ const args = expression.arguments
620
+ .map((arg) => [this.formatExpression(arg), ","])
621
+ .flat();
622
+ return this.sourceNodeHelper(expression, [
623
+ this.formatBase(expression.base),
624
+ "(",
625
+ this.sourceNodeHelper(undefined, args.slice(0, -1)),
626
+ ")",
627
+ ]);
628
+ } else if (expression.type == "TableCallExpression") {
629
+ return this.sourceNodeHelper(expression, [
630
+ this.formatExpression(expression.base),
631
+ this.formatExpression(expression.arguments),
632
+ ]);
633
+ } else if (expression.type == "StringCallExpression") {
634
+ return this.sourceNodeHelper(expression, [
635
+ this.formatExpression(expression.base),
636
+ this.formatExpression(expression.argument),
637
+ ]);
638
+ } else if (expression.type == "IndexExpression") {
639
+ return this.sourceNodeHelper(expression, [
640
+ this.formatBase(expression.base),
641
+ "[",
642
+ this.formatExpression(expression.index),
643
+ "]",
644
+ ]);
645
+ } else if (expression.type == "MemberExpression") {
646
+ return this.sourceNodeHelper(expression, [
647
+ this.formatBase(expression.base),
648
+ expression.indexer,
649
+ this.formatExpression(expression.identifier, {
650
+ preserveIdentifiers: true,
651
+ }),
652
+ ]);
653
+ } else if (expression.type == "FunctionDeclaration") {
654
+ const result = this.sourceNodeHelper(expression, ["function", "("]);
655
+
656
+ if (expression.parameters.length) {
657
+ const parameters = expression.parameters
658
+ .map((parameter) => {
659
+ return [
660
+ this.sourceNodeHelper(
661
+ parameter,
662
+ parameter.type === "Identifier"
663
+ ? this.generateIdentifier(parameter)
664
+ : parameter.value
665
+ ),
666
+ ",",
667
+ ];
668
+ })
669
+ .flat();
670
+ addWithSeparator(result, parameters.slice(0, -1));
671
+ }
672
+ result.add(")");
673
+ const body = this.formatStatementList(expression.body);
674
+ addWithSeparator(result, body);
675
+ addWithSeparator(result, "end");
676
+ return result;
677
+ } else if (expression.type == "TableConstructorExpression") {
678
+ const result = this.sourceNodeHelper(expression, "{");
679
+ const fields = expression.fields
680
+ .map((field, ix, ar) => {
681
+ // Stormworks "propert" Trailing Comma: https://nona-takahara.github.io/blog/entry11.html
682
+ const comma =
683
+ ix !== ar.length - 1 ||
684
+ this.formatExpression(field.value).toString().includes("property")
685
+ ? ","
686
+ : undefined;
687
+
688
+ if (field.type == "TableKey") {
689
+ return this.sourceNodeHelper(
690
+ field,
691
+ [
692
+ this.sourceNodeHelper(undefined, [
693
+ "[",
694
+ this.formatExpression(field.key),
695
+ "]",
696
+ ]),
697
+ "=",
698
+ this.formatExpression(field.value),
699
+ comma,
700
+ ].filter(
701
+ (p): p is Exclude<typeof p, undefined> => p !== undefined
702
+ )
703
+ );
704
+ } else if (field.type == "TableValue") {
705
+ return [this.formatExpression(field.value), comma].filter(
706
+ (p): p is Exclude<typeof p, undefined> => p !== undefined
707
+ );
708
+ } else {
709
+ // at this point, `field.type == 'TableKeyString'`
710
+ // TODO: keep track of nested scopes (#18)
711
+ return this.sourceNodeHelper(
712
+ field,
713
+ [
714
+ this.formatExpression(field.key, { preserveIdentifiers: true }),
715
+ "=",
716
+ this.formatExpression(field.value),
717
+ comma,
718
+ ].filter(
719
+ (p): p is Exclude<typeof p, undefined> => p !== undefined
720
+ )
721
+ );
722
+ }
723
+ })
724
+ .flat();
725
+ addWithSeparator(result, fields);
726
+ addWithSeparator(result, "}");
727
+ return result;
728
+ } else {
729
+ throw TypeError(
730
+ "Unknown expression type: `" + JSON.stringify(expression) + "`"
731
+ );
732
+ }
733
+ }
734
+
735
+ private formatBase(base: Parser.Expression): SourceNode {
736
+ const type = base.type;
737
+ const needsParens =
738
+ type == "CallExpression" ||
739
+ type == "BinaryExpression" ||
740
+ type == "FunctionDeclaration" ||
741
+ type == "TableConstructorExpression" ||
742
+ type == "LogicalExpression" ||
743
+ type == "StringLiteral";
744
+ const result = this.sourceNodeHelper(base, this.formatExpression(base));
745
+ if (needsParens) {
746
+ prependWithSeparator(result, "(");
747
+ addWithSeparator(result, ")");
748
+ }
749
+ return result;
750
+ }
751
+
752
+ private currentIdentifier = "";
753
+
754
+ private generateIdentifier(
755
+ nameItem: Parser.Identifier,
756
+ nested = false
757
+ ): SourceNode {
758
+ if (nameItem.name === "self") {
759
+ return this.sourceNodeHelper(nameItem, "self", "self");
760
+ }
761
+
762
+ const defined = identifierMap.get(nameItem.name);
763
+ if (defined) {
764
+ return this.sourceNodeHelper(nameItem, defined, nameItem.name); // 第3引数は要調査
765
+ }
766
+
767
+ const length = this.currentIdentifier.length;
768
+ let position = length - 1;
769
+ let character: string;
770
+ let index;
771
+ while (position >= 0) {
772
+ character = this.currentIdentifier.charAt(position);
773
+ index = IDENTIFIER_PARTS.indexOf(character);
774
+ if (index != IDENTIFIER_PARTS.length - 1) {
775
+ this.currentIdentifier =
776
+ this.currentIdentifier.substring(0, position) +
777
+ IDENTIFIER_PARTS[index + 1] +
778
+ generateZeroes(length - (position + 1));
779
+ if (
780
+ isKeyword(this.currentIdentifier) ||
781
+ identifiersInUse.has(this.currentIdentifier)
782
+ ) {
783
+ return this.generateIdentifier(nameItem, nested);
784
+ }
785
+ identifierMap.set(nameItem.name, this.currentIdentifier);
786
+ return this.generateIdentifier(nameItem, nested);
787
+ }
788
+ --position;
789
+ }
790
+ this.currentIdentifier = "a" + generateZeroes(length);
791
+ if (identifiersInUse.has(this.currentIdentifier)) {
792
+ return this.generateIdentifier(nameItem, nested);
793
+ }
794
+ identifierMap.set(nameItem.name, this.currentIdentifier);
795
+ return this.generateIdentifier(nameItem, nested);
796
+
797
+ // return this.sourceNodeHelper(nameItem, nameItem.name, nested ? nameItem.name : undefined);
798
+ }
799
+ }