@gi-tcg/gts-transpiler 0.3.2 → 0.3.4

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.
@@ -1,7 +1,7 @@
1
1
  import type { AST } from "../../types.ts";
2
2
  import { walk } from "zimmerframe";
3
3
  import type { SourceInfo, TranspileResult } from "../index.ts";
4
- import { print } from "esrap";
4
+ import { print } from "espolar";
5
5
  import {
6
6
  initialTranspileState,
7
7
  type TranspileOption,
@@ -10,28 +10,23 @@ import {
10
10
  import { gtsToTypingsWalker, type TypingTranspileState } from "./walker.ts";
11
11
  import { applyReplacements } from "./replacements.ts";
12
12
  import type { Program } from "estree";
13
- import { convertToVolarMappings, type VolarMappingResult } from "./mappings.ts";
14
- import { collectLeafTokens, type LeafToken } from "./collect_tokens.ts";
15
- import { patchedPrinter } from "./printer.ts";
16
-
17
- interface TypingTranspileOption extends TranspileOption {
18
- leafTokens: LeafToken[];
19
- /**
20
- * "row:col" -> "replacement string"
21
- */
22
- extraMappings: Map<string, string>;
23
- }
13
+ import {
14
+ VERIFICATION_ONLY_MAPPING_DATA,
15
+ type VolarMappingResult,
16
+ } from "./mappings.ts";
17
+ import { getPrintOptions } from "./printer.ts";
18
+ import { getContentStartOffset } from "./content_start.ts";
24
19
 
25
- function gtsToTypings(
26
- ast: AST.Program,
27
- option: TypingTranspileOption,
28
- ): TranspileResult {
20
+ export function transformForVolar(
21
+ ast: Program,
22
+ option: TranspileOption,
23
+ sourceInfo: Required<SourceInfo>,
24
+ ): VolarMappingResult {
29
25
  const state: TypingTranspileState = {
30
26
  ...(initialTranspileState(option) as Pick<
31
27
  TypingTranspileState,
32
28
  keyof TranspileState
33
29
  >),
34
- leafTokens: option.leafTokens,
35
30
  idCounter: 0,
36
31
  typingPendingStatements: [],
37
32
  rootVmId: { type: "Identifier", name: "__gts_root_vm" },
@@ -44,40 +39,70 @@ function gtsToTypings(
44
39
  metaTypeIdStack: [],
45
40
  finalMetaTypeIdStack: [],
46
41
  attrsOfCurrentVm: [],
47
- extraMappings: option.extraMappings,
42
+
43
+ sourceNodes: new WeakSet(),
44
+ namedAttributeCalleeLParenRange: new WeakMap(),
45
+ literalFromIdentifier: new WeakSet(),
46
+ lastArgNodes: new WeakSet(),
47
+ lastImportDeclarationIfGen: null,
48
+ diagnosticsOnTopNodes: new WeakSet(),
49
+ extraMappings: [],
50
+ contentStartOffset: getContentStartOffset(sourceInfo.content),
48
51
  };
52
+ // mark sourceNodes before the transformation
53
+ walk(ast as AST.Node, state, {
54
+ _(node, { state, next }) {
55
+ if (node.range) {
56
+ state.sourceNodes.add(node);
57
+ }
58
+ next();
59
+ },
60
+ });
49
61
  const newAst = walk(ast as AST.Node, state, gtsToTypingsWalker);
50
- const { code, map } = print(newAst, patchedPrinter, {
51
- indent: " ",
62
+ // mark lastArgNodes
63
+ walk(newAst as AST.Node, state, {
64
+ CallExpression(node, { state }) {
65
+ const lastArg = node.arguments.at(-1);
66
+ if (lastArg) {
67
+ state.lastArgNodes.add(lastArg);
68
+ }
69
+ },
70
+ ImportDeclaration(node, { state }) {
71
+ if (!state.sourceNodes.has(node)) {
72
+ state.lastImportDeclarationIfGen = node;
73
+ }
74
+ },
52
75
  });
53
- return {
54
- code: applyReplacements(state, code),
55
- sourceMap: map,
56
- };
57
- }
58
-
59
- export function transformForVolar(
60
- ast: Program,
61
- option: TranspileOption,
62
- sourceInfo: Required<SourceInfo>,
63
- ): VolarMappingResult {
64
- const tokens = collectLeafTokens(ast);
65
- const extraMappings = new Map<string, string>();
66
- const { code, sourceMap } = gtsToTypings(ast, {
67
- ...option,
68
- leafTokens: tokens,
69
- extraMappings,
76
+ const printOptions = getPrintOptions(sourceInfo.content, state);
77
+ let { code, mappings } = print(newAst, printOptions);
78
+ code = applyReplacements(state, code, mappings);
79
+ for (const extraMapping of state.extraMappings) {
80
+ const genOffset = code.indexOf(extraMapping.generatedNeedle);
81
+ mappings.push({
82
+ sourceOffsets: [extraMapping.sourceOffset],
83
+ lengths: [extraMapping.length],
84
+ generatedOffsets: [genOffset],
85
+ generatedLengths: [extraMapping.generatedNeedle.length],
86
+ data: VERIFICATION_ONLY_MAPPING_DATA,
87
+ });
88
+ }
89
+ walk(newAst, null, {
90
+ ImportDeclaration(node) {
91
+ if (!node.range) {
92
+ return;
93
+ }
94
+ const endOffset = node.range[1];
95
+ const mappingEndsWithThisImport = mappings.find(
96
+ (m) => m.sourceOffsets[0] + m.lengths[0] === endOffset,
97
+ );
98
+ if (mappingEndsWithThisImport?.lengths[0]) {
99
+ mappingEndsWithThisImport.lengths[0] += 1; // include the newline after the import
100
+ }
101
+ },
70
102
  });
71
- const volarMappings = convertToVolarMappings(
72
- code,
73
- sourceInfo.content,
74
- sourceMap,
75
- tokens,
76
- extraMappings,
77
- );
78
103
  return {
79
104
  code,
80
- mappings: volarMappings,
105
+ mappings,
81
106
  };
82
107
  }
83
108
 
@@ -1,13 +1,11 @@
1
- import { decode } from "@jridgewell/sourcemap-codec";
2
1
  import type { CodeInformation, CodeMapping } from "@volar/language-core";
3
- import type { LeafToken } from "./collect_tokens.ts";
4
2
 
5
3
  export interface VolarMappingResult {
6
4
  code: string;
7
5
  mappings: CodeMapping[];
8
6
  }
9
7
 
10
- const DEFAULT_VOLAR_MAPPING_DATA: CodeInformation = {
8
+ export const DEFAULT_VOLAR_MAPPING_DATA: CodeInformation = {
11
9
  completion: true,
12
10
  format: true,
13
11
  navigation: true,
@@ -15,354 +13,9 @@ const DEFAULT_VOLAR_MAPPING_DATA: CodeInformation = {
15
13
  structure: true,
16
14
  verification: true,
17
15
  };
18
-
19
- interface SourceMap {
20
- mappings: string;
21
- }
22
-
23
- interface CodePosition {
24
- line: number;
25
- column: number;
26
- end_line: number;
27
- end_column: number;
28
- code: string;
29
- }
30
-
31
- type LineOffsets = number[];
32
-
33
- type CodeToGeneratedMap = Map<string, CodePosition[]>;
34
-
35
- /**
36
- * Convert byte offset to line/column
37
- * @param offset
38
- * @param line_offsets
39
- * @returns */
40
- export const offset_to_line_col = (
41
- offset: number,
42
- line_offsets: LineOffsets,
43
- ): { line: number; column: number } => {
44
- // Binary search
45
- let left = 0;
46
- let right = line_offsets.length - 1;
47
- let line = 1;
48
-
49
- while (left <= right) {
50
- const mid = Math.floor((left + right) / 2);
51
- if (
52
- offset >= line_offsets[mid] &&
53
- (mid === line_offsets.length - 1 || offset < line_offsets[mid + 1])
54
- ) {
55
- line = mid + 1;
56
- break;
57
- } else if (offset < line_offsets[mid]) {
58
- right = mid - 1;
59
- } else {
60
- left = mid + 1;
61
- }
62
- }
63
-
64
- const column = offset - line_offsets[line - 1];
65
- return { line, column };
16
+ export const VERIFICATION_ONLY_MAPPING_DATA: CodeInformation = {
17
+ verification: true,
66
18
  };
67
19
 
68
- /**
69
- * Build a source-to-generated position lookup map from an esrap source map
70
- * Applies post-processing adjustments during map building for efficiency
71
- * @param source_map - The source map object from esrap (v3 format)
72
- * @param line_offsets - Pre-computed line offsets array
73
- * @param generated_code - The final generated code (after post-processing)
74
- * @returns source-to-generated map
75
- */
76
- export function buildSrcToGenMap(
77
- source_map: SourceMap,
78
- line_offsets: LineOffsets,
79
- generated_code: string,
80
- ): CodeToGeneratedMap {
81
- const map: CodeToGeneratedMap = new Map();
82
-
83
- // Decode the VLQ-encoded mappings string
84
- const decoded = decode(source_map.mappings);
85
-
86
- /**
87
- * Convert line/column position to byte offset
88
- * @param {number} line - 1-based line number
89
- * @param {number} column - 0-based column number
90
- * @returns {number} Byte offset
91
- */
92
- const line_col_to_byte_offset = (line: number, column: number) => {
93
- return line_offsets[line - 1] + column;
94
- };
95
-
96
- // Apply post-processing adjustments to all segments first
97
- const adjusted_segments: {
98
- line: number;
99
- column: number;
100
- sourceLine: number;
101
- sourceColumn: number;
102
- }[][] = [];
103
-
104
- for (
105
- let generated_line = 0;
106
- generated_line < decoded.length;
107
- generated_line++
108
- ) {
109
- const line = decoded[generated_line];
110
- adjusted_segments[generated_line] = [];
111
-
112
- for (const segment of line) {
113
- if (segment.length >= 4) {
114
- let adjusted_line = generated_line + 1;
115
- let adjusted_column = segment[0];
116
- adjusted_segments[generated_line].push({
117
- line: adjusted_line,
118
- column: adjusted_column,
119
- sourceLine: segment[2]!,
120
- sourceColumn: segment[3]!,
121
- });
122
- }
123
- }
124
- }
125
-
126
- // Now build the map using adjusted positions
127
- for (let line_idx = 0; line_idx < adjusted_segments.length; line_idx++) {
128
- const line_segments = adjusted_segments[line_idx];
129
-
130
- for (let seg_idx = 0; seg_idx < line_segments.length; seg_idx++) {
131
- const segment = line_segments[seg_idx];
132
- const line = segment.line;
133
- const column = segment.column;
134
-
135
- // Determine end position using next segment
136
- let end_line = line;
137
- let end_column = column;
138
-
139
- // Look for next segment to determine end position
140
- if (seg_idx + 1 < line_segments.length) {
141
- // Next segment on same line
142
- const next_segment = line_segments[seg_idx + 1];
143
- end_line = next_segment.line;
144
- end_column = next_segment.column;
145
- } else if (
146
- line_idx + 1 < adjusted_segments.length &&
147
- adjusted_segments[line_idx + 1].length > 0
148
- ) {
149
- // Look at first segment of next line
150
- const next_segment = adjusted_segments[line_idx + 1][0];
151
- end_line = next_segment.line;
152
- end_column = next_segment.column;
153
- }
154
-
155
- // Extract code snippet
156
- const start_offset = line_col_to_byte_offset(line, column);
157
- const end_offset = line_col_to_byte_offset(end_line, end_column);
158
- const code_snippet = generated_code.slice(start_offset, end_offset);
159
-
160
- // Create key from source position (1-indexed line, 0-indexed column)
161
- segment.sourceLine += 1;
162
- const key = `${segment.sourceLine}:${segment.sourceColumn}`;
163
-
164
- // Store adjusted generated position with code snippet
165
- const gen_pos = {
166
- line,
167
- column,
168
- end_line,
169
- end_column,
170
- code: code_snippet,
171
- metadata: {},
172
- };
173
-
174
- if (!map.has(key)) {
175
- map.set(key, []);
176
- }
177
- map.get(key)!.push(gen_pos);
178
- }
179
- }
180
-
181
- return map;
182
- }
183
-
184
- /**
185
- * Look up generated position for a given source position
186
- * @param src_line - 1-based line number in source
187
- * @param src_column - 0-based column number in source
188
- * @param srcToGenMap - Lookup map
189
- * @returns Generated position
190
- */
191
- export function getGeneratedPosition(
192
- src_line: number,
193
- src_column: number,
194
- srcToGenMap: CodeToGeneratedMap,
195
- ): CodePosition[] {
196
- const key = `${src_line}:${src_column}`;
197
- const positions = srcToGenMap.get(key);
198
-
199
- // If multiple generated positions map to same source, return the first
200
- return positions || [];
201
- }
202
- // Helper to create a line-to-offset lookup table
203
- function createLineOffsets(content: string): number[] {
204
- const lines = content.split("\n");
205
- const offsets: number[] = [];
206
- let currentOffset = 0;
207
-
208
- for (const line of lines) {
209
- offsets.push(currentOffset);
210
- // +1 for the newline character (handle \r\n vs \n if necessary)
211
- currentOffset += line.length + 1;
212
- }
213
- return offsets;
214
- }
215
-
216
- /**
217
- * Convert line/column to byte offset
218
- * @param line
219
- * @param column
220
- * @param line_offsets
221
- * @returns
222
- */
223
- function locToOffset(
224
- line: number,
225
- column: number,
226
- line_offsets: number[],
227
- ): number {
228
- if (line < 1 || line > line_offsets.length) {
229
- // throw new Error(
230
- // `Location line or line offsets length is out of bounds, line: ${line}, line offsets length: ${line_offsets.length}`
231
- // );
232
- }
233
- return line_offsets[line - 1] + column;
234
- }
235
-
236
- export function convertToVolarMappings(
237
- generated: string,
238
- source: string,
239
- sourceMap: SourceMap,
240
- tokens: LeafToken[],
241
- extraMappings: Map<string, string>,
242
- ): CodeMapping[] {
243
- const sourceLineOffsets = createLineOffsets(source);
244
- const generatedLineOffsets = createLineOffsets(generated);
245
- const srcToGenMap = buildSrcToGenMap(
246
- sourceMap,
247
- generatedLineOffsets,
248
- generated,
249
- );
250
-
251
- const mappings: CodeMapping[] = [];
252
-
253
- for (const token of tokens) {
254
- let sourceStart = locToOffset(
255
- token.loc.start.line,
256
- token.loc.start.column,
257
- sourceLineOffsets,
258
- );
259
- sourceStart += token.sourceStartOffset ?? 0;
260
-
261
- const sourceEnd = locToOffset(
262
- token.loc.end.line,
263
- token.loc.end.column,
264
- sourceLineOffsets,
265
- );
266
-
267
- let sourceLength = token.sourceLength ?? sourceEnd - sourceStart;
268
-
269
- sourceLength += token.sourceLengthOffset ?? 0;
270
- const [genLineCol] = getGeneratedPosition(
271
- token.loc.start.line,
272
- token.loc.start.column,
273
- srcToGenMap,
274
- );
275
- if (!genLineCol) {
276
- // No mapping found for this token - skip it
277
- continue;
278
- }
279
- let genStart = locToOffset(
280
- genLineCol.line,
281
- genLineCol.column,
282
- generatedLineOffsets,
283
- );
284
- if (token.isDummy) {
285
- // A dummy token might be generated for a missing property / argument.
286
- // Notice that when facing this scenario, the parser tries to 'defer' and step through
287
- // all whitespaces and insert the invalid node just before the next token.
288
- // But in a mapping context, we need the caret next to the previous token (commonly the `.` dot)
289
- // to allow triggering completion correctly. So we adjust the sourceStart and sourceLength accordingly.
290
- // After adjustment, the mapping will include all whitespaces as the invalid node and maps to an empty string.
291
- while (sourceStart > 0 && /\s/.test(source[sourceStart - 1])) {
292
- sourceStart--;
293
- sourceLength++;
294
- }
295
- }
296
- const generatedLength = token.generatedLength ?? sourceLength;
297
-
298
- if (
299
- typeof token.generatedStartOffset === "number" &&
300
- token.generatedStartOffset > 0
301
- ) {
302
- // maps verification back to the start of source
303
- mappings.push({
304
- sourceOffsets: [sourceStart],
305
- generatedOffsets: [genStart],
306
- lengths: [0],
307
- generatedLengths: [generatedLength],
308
- data: {
309
- verification: true,
310
- },
311
- });
312
- genStart += token.generatedStartOffset;
313
- }
314
-
315
- mappings.push({
316
- sourceOffsets: [sourceStart],
317
- generatedOffsets: [genStart],
318
- lengths: [sourceLength],
319
- generatedLengths: [generatedLength],
320
- data: DEFAULT_VOLAR_MAPPING_DATA,
321
- });
322
- }
323
-
324
- // Handle extra mappings that purely generated and wants a diagnostic
325
- // that appears on the top of file.
326
- // We'd add these mappings at walker that they will have a `loc` to `1:0`
327
- // so find them by looking up a source position of `1:0` will work
328
- const locMapsToTop = getGeneratedPosition(1, 0, srcToGenMap);
329
- for (const loc of locMapsToTop) {
330
- const offset = locToOffset(loc.line, loc.column, generatedLineOffsets);
331
- mappings.push({
332
- sourceOffsets: [0],
333
- generatedOffsets: [offset],
334
- lengths: [0],
335
- generatedLengths: [0],
336
- data: {
337
- verification: true,
338
- },
339
- });
340
- }
341
-
342
- for (const [loc, codeSnippet] of extraMappings) {
343
- const generatedStart = generated.indexOf(codeSnippet);
344
- if (generatedStart === -1) {
345
- continue;
346
- }
347
- const [lineStr, columnStr] = loc.split(":");
348
- const line = Number(lineStr);
349
- const column = Number(columnStr);
350
- const sourceStart = locToOffset(line, column, sourceLineOffsets);
351
- const sourceLength = 1;
352
- const generatedLength = codeSnippet.length;
353
- mappings.push({
354
- sourceOffsets: [sourceStart],
355
- generatedOffsets: [generatedStart],
356
- lengths: [sourceLength],
357
- generatedLengths: [generatedLength],
358
- data: {
359
- verification: true,
360
- },
361
- });
362
- }
363
-
364
- // Sort mappings by source offset // Sort mappings by source offset
365
- mappings.sort((a, b) => a.sourceOffsets[0] - b.sourceOffsets[0]);
366
-
367
- return mappings;
368
- }
20
+ Object.freeze(DEFAULT_VOLAR_MAPPING_DATA);
21
+ Object.freeze(VERIFICATION_ONLY_MAPPING_DATA);