@gi-tcg/gts-transpiler 0.3.2 → 0.3.3

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,5 +1,12 @@
1
1
  import type { ExpressionStatement } from "estree";
2
2
  import type { TypingTranspileState } from "./walker.ts";
3
+ import dedent from "dedent";
4
+ import type { CodeMapping } from "@volar/language-core";
5
+
6
+ interface MatchInfo {
7
+ sourceEnd: number;
8
+ lengthOffset: number;
9
+ }
3
10
 
4
11
  type ReplacementPayload =
5
12
  | {
@@ -23,7 +30,7 @@ type ReplacementPayload =
23
30
  defType: string;
24
31
  collectedAttrs: string[];
25
32
  finalMetaType: string;
26
- errorLoc?: string;
33
+ errorRange?: [number, number];
27
34
  }
28
35
  | {
29
36
  type: "enterAttr";
@@ -75,55 +82,54 @@ export const createReplacementHolder = (
75
82
  export function applyReplacements(
76
83
  state: TypingTranspileState,
77
84
  code: string,
85
+ mappings: CodeMapping[],
78
86
  ): string {
79
87
  const replacementRegex = new RegExp(
80
- "\\b" + state.replacementTag.name + "`(.*?)`",
88
+ "\\b" + state.replacementTag.name + "`(.*?)(?<!\\\\)`",
81
89
  "gm",
82
90
  );
83
91
  const { NamedDefinitionLit, MetaLit } = state;
84
92
  const NamedDefinition = JSON.stringify(NamedDefinitionLit.value);
85
93
  const Meta = JSON.stringify(MetaLit.value);
86
- const oneLine = (
87
- strings: TemplateStringsArray,
88
- ...values: Array<string | number | boolean>
89
- ): string =>
90
- strings
91
- .reduce(
92
- (result, chunk, index) =>
93
- result + chunk + (index < values.length ? values[index] : ""),
94
- "",
95
- )
96
- .replace(/\n[ \t]*/g, " ")
97
- .trim();
98
- // All replacement should be written in one line to avoid messing up source map
99
- return code.replace(replacementRegex, (_, rawPayload) => {
100
- const payload: ReplacementPayload = JSON.parse(rawPayload);
101
- if (payload.type === "preface") {
102
- return oneLine`
94
+ const matchInfos: MatchInfo[] = [];
95
+
96
+ const result = code.replace(
97
+ replacementRegex,
98
+ (match, rawPayload: string, offset: number) => {
99
+ const payload: ReplacementPayload = JSON.parse(
100
+ rawPayload.replace(/\\`/g, "`"),
101
+ );
102
+ let replacement: string;
103
+ if (payload.type === "preface") {
104
+ replacement = dedent`
103
105
  namespace ${state.utilNsId.name} {
104
106
  export type UniqueKeyProbSegment = "__gts_unique_prob_seg__";
105
107
  export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
106
108
  }
107
109
  `;
108
- } else if (payload.type === "enterVMFromRoot") {
109
- return oneLine`
110
+ } else if (payload.type === "enterVMFromRoot") {
111
+ replacement = dedent`
110
112
  type ${payload.defType} = (typeof ${payload.vm})[${NamedDefinition}];
111
113
  type ${payload.metaType} = ${payload.defType}[${Meta}];
112
114
  `;
113
- } else if (payload.type === "enterVMFromAttr") {
114
- return oneLine`
115
+ } else if (payload.type === "enterVMFromAttr") {
116
+ replacement = dedent`
115
117
  type ${payload.defType} = ${payload.returnType} extends { namedDefinition: infer Def } ? Def : { ${Meta}: unknown };
116
118
  type ${payload.metaType} = ${payload.defType}[${Meta}];
117
119
  `;
118
- } else if (payload.type === "exitVM") {
119
- const lhs = `${payload.finalMetaType}_lhs`;
120
- const requiredAttrsNs = `${payload.finalMetaType}_rans`;
121
- const collectedAttrsExpr = `${payload.collectedAttrs.join(" | ")}`;
122
- const needleString = `"${requiredAttrsNs}_NeedleString" as any as "required attributes are missing"`;
123
- if (payload.errorLoc) {
124
- state.extraMappings.set(payload.errorLoc, needleString);
125
- }
126
- return oneLine`
120
+ } else if (payload.type === "exitVM") {
121
+ const lhs = `${payload.finalMetaType}_lhs`;
122
+ const requiredAttrsNs = `${payload.finalMetaType}_rans`;
123
+ const collectedAttrsExpr = `${payload.collectedAttrs.join(" | ")}`;
124
+ const needleString = `"${requiredAttrsNs}_NeedleString" as any as "required attributes are missing"`;
125
+ if (payload.errorRange) {
126
+ state.extraMappings.push({
127
+ sourceOffset: payload.errorRange[0],
128
+ length: payload.errorRange[1] - payload.errorRange[0],
129
+ generatedNeedle: needleString,
130
+ });
131
+ }
132
+ replacement = dedent`
127
133
  type ${payload.finalMetaType} = ${payload.metaType};
128
134
  let ${lhs}!: { ${Meta}: ${payload.metaType} } & Omit<${payload.defType}, ${Meta}>;
129
135
  type ${lhs} = typeof ${lhs};
@@ -133,16 +139,17 @@ export function applyReplacements(
133
139
  };
134
140
  ((_: ${requiredAttrsNs}.Expected extends ${requiredAttrsNs}.Collected ? string : ${requiredAttrsNs}.Expected) => 0)(${needleString});
135
141
  `;
136
- } else if (payload.type === "enterAttr") {
137
- const uniqueKeyLhs = `${payload.lhs}_uniqueKey_lhs`;
138
- const uniqueKey = `${payload.lhs}_uniqueKey`;
139
- const uniqueKeyForThis = `${payload.lhs}_uniqueKeyFor_${payload.lhs}`;
140
- const uniqueKeyHelperIntf = `${payload.defType}_uniqueKeyProbeHelper`;
141
- const omittedKeys = `${payload.lhs}_omittedKeys`;
142
- return oneLine`
142
+ } else if (payload.type === "enterAttr") {
143
+ const uniqueKeyLhs = `${payload.lhs}_uniqueKey_lhs`;
144
+ const uniqueKey = `${payload.lhs}_uniqueKey`;
145
+ const uniqueKeyForThis = `${payload.lhs}_uniqueKeyFor_${payload.lhs}`;
146
+ const uniqueKeyHelperIntf = `${payload.defType}_uniqueKeyProbeHelper`;
147
+ const omittedKeys = `${payload.lhs}_omittedKeys`;
148
+ replacement = dedent`
143
149
  type ${uniqueKeyLhs} = {
144
150
  ${Meta}: ${payload.metaType};
145
151
  uniqueKey: ${payload.defType} extends { [${payload.attrName}]: { uniqueKey: infer UniqueKey } } ? UniqueKey : () => 0;
152
+ };
146
153
 
147
154
  let ${uniqueKeyLhs}!: ${uniqueKeyLhs};
148
155
  let ${uniqueKey} = ${uniqueKeyLhs}.uniqueKey();
@@ -164,9 +171,9 @@ export function applyReplacements(
164
171
  );
165
172
  let ${payload.lhs}!: { ${Meta}: ${payload.metaType} } & Omit<${payload.defType}, ${omittedKeys}>;
166
173
  `;
167
- } else if (payload.type === "createBindingTyping") {
168
- const typingIdLhs = `${payload.typingId}_lhs`;
169
- return oneLine`
174
+ } else if (payload.type === "createBindingTyping") {
175
+ const typingIdLhs = `${payload.typingId}_lhs`;
176
+ replacement = dedent`
170
177
  type ${typingIdLhs} = {
171
178
  ${Meta}: ${payload.finalMetaType};
172
179
  as: ${payload.defType} extends { [${payload.attrName}]: { as: infer As } } ? As : unknown;
@@ -175,13 +182,34 @@ export function applyReplacements(
175
182
  let ${payload.typingId} = ${typingIdLhs}.as();
176
183
  type ${payload.typingId} = typeof ${payload.typingId};
177
184
  `;
178
- } else if (payload.type === "exitAttr") {
179
- return oneLine`
185
+ } else if (payload.type === "exitAttr") {
186
+ replacement = dedent`
180
187
  type ${payload.returnType} = typeof ${payload.returnType};
181
188
  type ${payload.newMetaType} = ${payload.returnType} extends { rewriteMeta: infer NewMeta extends {} } ? NewMeta : ${payload.oldMetaType}
182
189
  `;
183
- } else {
184
- return "";
190
+ } else {
191
+ replacement = "";
192
+ }
193
+ matchInfos.push({
194
+ sourceEnd: offset + match.length,
195
+ lengthOffset: replacement.length - match.length,
196
+ });
197
+ return replacement;
198
+ },
199
+ );
200
+
201
+ for (const mapping of mappings) {
202
+ for (let i = 0; i < mapping.generatedOffsets.length; i++) {
203
+ const orig = mapping.generatedOffsets[i];
204
+ let shift = 0;
205
+ for (const info of matchInfos) {
206
+ if (orig >= info.sourceEnd) {
207
+ shift += info.lengthOffset;
208
+ }
209
+ }
210
+ mapping.generatedOffsets[i] = orig + shift;
185
211
  }
186
- });
212
+ }
213
+
214
+ return result;
187
215
  }
@@ -1,21 +1,19 @@
1
1
  import type { Visitors } from "zimmerframe";
2
2
  import type {
3
- ArrayExpression,
4
3
  ArrowFunctionExpression,
5
- BlockStatement,
6
4
  Comment,
7
- Declaration,
8
5
  EmptyStatement,
9
6
  ExportNamedDeclaration,
10
7
  Expression,
11
8
  ExpressionStatement,
12
9
  GTSDefineStatement,
13
10
  Identifier,
11
+ ImportDeclaration,
14
12
  Literal,
15
13
  MemberExpression,
16
14
  Node,
17
15
  Program,
18
- SourceLocation,
16
+ SimpleLiteral,
19
17
  Statement,
20
18
  VariableDeclaration,
21
19
  VariableDeclarator,
@@ -25,8 +23,8 @@ import {
25
23
  type ExternalizedBinding,
26
24
  type TranspileState,
27
25
  } from "../gts.ts";
28
- import type { LeafToken } from "./collect_tokens.ts";
29
26
  import { createReplacementHolder } from "./replacements.ts";
27
+ import type { SourceRange } from "espolar";
30
28
 
31
29
  interface ExternalizedTypedBinding extends ExternalizedBinding {
32
30
  typingId: Identifier;
@@ -34,7 +32,6 @@ interface ExternalizedTypedBinding extends ExternalizedBinding {
34
32
  }
35
33
 
36
34
  export interface TypingTranspileState extends TranspileState {
37
- leafTokens: LeafToken[];
38
35
  externalizedBindings: ExternalizedTypedBinding[];
39
36
  idCounter: number;
40
37
  rootVmId: Identifier;
@@ -55,7 +52,29 @@ export interface TypingTranspileState extends TranspileState {
55
52
  /** Pending statements to be inserted to the top-level */
56
53
  typingPendingStatements: Statement[];
57
54
  replacementTag: Identifier;
58
- extraMappings: Map<string, string>;
55
+ /** untouched source nodes */
56
+ sourceNodes: WeakSet<Node>;
57
+ /**
58
+ * The callee of typing source of GTS' attribute names. Map the character
59
+ * after the name (typically whitespace) as the lParen of CallExpression
60
+ */
61
+ namedAttributeCalleeLParenRange: WeakMap<Node, SourceRange>;
62
+ /**
63
+ * String literal nodes that are derived from identifiers.
64
+ * - sourceStart += 1
65
+ * - sourceLength -= 2
66
+ */
67
+ literalFromIdentifier: WeakSet<Literal>;
68
+ /** Nodes that are last arguments of a CallExpression */
69
+ lastArgNodes: WeakSet<Node>;
70
+ /** Nodes that have a diagnostic mappings to the top of the file */
71
+ diagnosticsOnTopNodes: WeakSet<Node>;
72
+ /** Extra mappings, the generated range will be found by the needle after replacement */
73
+ extraMappings: {
74
+ sourceOffset: number;
75
+ length: number;
76
+ generatedNeedle: string;
77
+ }[];
59
78
  }
60
79
 
61
80
  const EMPTY: EmptyStatement = { type: "EmptyStatement" };
@@ -128,7 +147,7 @@ const enterVMFromAttr = (
128
147
  state.attrsOfCurrentVm.push([]);
129
148
  };
130
149
 
131
- const exitVM = (state: TypingTranspileState, errorLoc?: string) => {
150
+ const exitVM = (state: TypingTranspileState, errorRange?: [number, number]) => {
132
151
  const currentDefTypeId = state.vmDefTypeIdStack.pop()!;
133
152
  const currentMetaId = state.metaTypeIdStack.pop()!;
134
153
  const finalMetaId = state.finalMetaTypeIdStack.pop()!;
@@ -140,7 +159,7 @@ const exitVM = (state: TypingTranspileState, errorLoc?: string) => {
140
159
  defType: currentDefTypeId.name,
141
160
  finalMetaType: finalMetaId.name,
142
161
  collectedAttrs: collectedAttrNames,
143
- errorLoc,
162
+ errorRange,
144
163
  }),
145
164
  );
146
165
  };
@@ -153,7 +172,7 @@ const enterAttr = (
153
172
  const metaTypeId = state.metaTypeIdStack.at(-1);
154
173
  if (!defTypeId || !metaTypeId) {
155
174
  // TODO error handling?
156
- return { lhsId: { type: "Identifier", name: "__invalid_attr_obj" } };
175
+ return { lhsId: { type: "Identifier", name: "__gts_invalid_attr_obj" } };
157
176
  }
158
177
  state.attrsOfCurrentVm.at(-1)!.push(attrName);
159
178
  const lhsId: Identifier = {
@@ -264,10 +283,10 @@ export const gtsToTypingsWalker: Visitors<Node, TypingTranspileState> = {
264
283
  body.unshift(varDecl);
265
284
  }
266
285
  }
286
+ const importDecls: ImportDeclaration[] = [];
267
287
  if (state.hasQueryExpressions) {
268
- body.unshift({
288
+ importDecls.push({
269
289
  type: "ImportDeclaration",
270
- diagnosticsOnTop: true,
271
290
  specifiers: [
272
291
  {
273
292
  type: "ImportDefaultSpecifier",
@@ -281,22 +300,28 @@ export const gtsToTypingsWalker: Visitors<Node, TypingTranspileState> = {
281
300
  attributes: [],
282
301
  });
283
302
  }
284
- body.unshift(
285
- {
286
- type: "ImportDeclaration",
287
- diagnosticsOnTop: true,
288
- specifiers: [
289
- {
290
- type: "ImportDefaultSpecifier",
291
- local: state.rootVmId,
292
- },
293
- ],
294
- source: {
295
- type: "Literal",
296
- value: `${state.providerImportSource}/vm`,
303
+ importDecls.push({
304
+ type: "ImportDeclaration",
305
+ specifiers: [
306
+ {
307
+ type: "ImportDefaultSpecifier",
308
+ local: state.rootVmId,
297
309
  },
298
- attributes: [],
310
+ ],
311
+ source: {
312
+ type: "Literal",
313
+ value: `${state.providerImportSource}/vm`,
299
314
  },
315
+ attributes: [],
316
+ });
317
+ for (const importDecl of importDecls) {
318
+ state.diagnosticsOnTopNodes.add(importDecl.source);
319
+ for (const specifier of importDecl.specifiers) {
320
+ state.diagnosticsOnTopNodes.add(specifier);
321
+ }
322
+ }
323
+ body.unshift(
324
+ ...importDecls,
300
325
  createReplacementHolder(state, {
301
326
  type: "preface",
302
327
  }),
@@ -317,30 +342,20 @@ export const gtsToTypingsWalker: Visitors<Node, TypingTranspileState> = {
317
342
  const attrName = JSON.stringify(
318
343
  name.type === "Literal" ? String(name.value) : name.name,
319
344
  );
320
- const attributeNameToken = state.leafTokens.find((t) => t.loc === name.loc);
321
- if (attributeNameToken) {
322
- attributeNameToken.sourceLengthOffset = 1;
323
- }
324
345
  const { lhsId } = enterAttr(state, attrName);
325
- state.extraMappings.set(
326
- `${name.loc?.start.line}:${name.loc?.start.column}`,
327
- `${lhsId.name}${name.type === "Literal" ? `[` : `.`}`,
328
- );
329
346
  const positionals = body.positionalAttributes.attributes.map(
330
347
  (attr): Expression => {
331
348
  if (attr.type === "Identifier" && /^[a-z_]/.test(attr.name)) {
332
- const token = state.leafTokens.find((t) => t.loc === attr.loc);
333
- if (token) {
334
- token.generatedStartOffset = 1;
335
- token.generatedLength = attr.name.length + 2; // quotation mark
336
- }
337
- return {
349
+ const lit: SimpleLiteral = {
338
350
  type: "Literal",
339
351
  value: attr.name,
340
352
  loc: attr.loc,
353
+ range: attr.range,
341
354
  };
355
+ state.literalFromIdentifier.add(lit);
356
+ return lit;
342
357
  } else {
343
- return visit(attr) as Expression;
358
+ return { ...(visit(attr) as Expression) };
344
359
  }
345
360
  },
346
361
  );
@@ -348,6 +363,27 @@ export const gtsToTypingsWalker: Visitors<Node, TypingTranspileState> = {
348
363
  type: "Identifier",
349
364
  name: `__gts_attrRet_${state.idCounter++}`,
350
365
  };
366
+ const callee: MemberExpression = {
367
+ type: "MemberExpression",
368
+ object: lhsId,
369
+ property: name,
370
+ computed: name.type === "Literal",
371
+ optional: false,
372
+ };
373
+ if (name.range) {
374
+ // If the `obj.name` is not callable, the error squiggle begins from `obj` not `name`
375
+ // Add a diagnostic-only mapping from the `name` to a call starting from `obj.`
376
+ // This should be unique since each attribute decl generates a new obj name
377
+ state.extraMappings.push({
378
+ sourceOffset: name.range[0],
379
+ length: name.range[1] - name.range[0],
380
+ generatedNeedle: `${lhsId.name}${name.type === "Literal" ? `[` : `.`}`,
381
+ });
382
+ state.namedAttributeCalleeLParenRange.set(callee, {
383
+ start: name.range[1],
384
+ end: name.range[1] + 1,
385
+ });
386
+ }
351
387
  state.typingPendingStatements.push({
352
388
  type: "VariableDeclaration",
353
389
  kind: "const",
@@ -358,13 +394,7 @@ export const gtsToTypingsWalker: Visitors<Node, TypingTranspileState> = {
358
394
  init: {
359
395
  type: "CallExpression",
360
396
  optional: false,
361
- callee: {
362
- type: "MemberExpression",
363
- object: lhsId,
364
- property: name,
365
- computed: name.type === "Literal",
366
- optional: false,
367
- },
397
+ callee,
368
398
  arguments: positionals,
369
399
  },
370
400
  },
@@ -373,16 +403,13 @@ export const gtsToTypingsWalker: Visitors<Node, TypingTranspileState> = {
373
403
  if (body.namedAttributes) {
374
404
  enterVMFromAttr(state, returnValue);
375
405
  visit(body.namedAttributes);
376
- exitVM(
377
- state,
378
- `${body.namedAttributes.loc?.start.line}:${body.namedAttributes.loc?.start.column}`,
379
- );
406
+ exitVM(state, body.namedAttributes.range);
380
407
  }
381
408
  if (bindingName) {
382
409
  const export_ = node.bindingAccessModifier !== "private";
383
410
  const typingId: Identifier = {
384
411
  type: "Identifier",
385
- name: `gts_binding_type_${state.externalizedBindings.length}`,
412
+ name: `__gts_binding_type_${state.externalizedBindings.length}`,
386
413
  };
387
414
  genBindingTyping(state, {
388
415
  attrName,
@@ -415,11 +442,13 @@ export const gtsToTypingsWalker: Visitors<Node, TypingTranspileState> = {
415
442
  const attrName = JSON.stringify(state.ActionLit.value);
416
443
  const { lhsId } = enterAttr(state, attrName);
417
444
  const actionNotExistsReplacementStr = `${lhsId.name}[${attrName}]`;
418
- const actionNotExistsErrorLoc = `${node.directAction.loc?.start.line}:${node.directAction.loc?.start.column}`;
419
- state.extraMappings.set(
420
- actionNotExistsErrorLoc,
421
- actionNotExistsReplacementStr,
422
- );
445
+ if (node.directAction.range) {
446
+ state.extraMappings.push({
447
+ sourceOffset: node.directAction.range[0],
448
+ length: node.directAction.range[1] - node.directAction.range[0],
449
+ generatedNeedle: actionNotExistsReplacementStr,
450
+ });
451
+ }
423
452
  const fn: ArrowFunctionExpression = {
424
453
  type: "ArrowFunctionExpression",
425
454
  params: state.shortcutFunctionParameters,
@@ -459,14 +488,4 @@ export const gtsToTypingsWalker: Visitors<Node, TypingTranspileState> = {
459
488
  return EMPTY;
460
489
  },
461
490
  ...(commonGtsVisitor as Visitors<Node, TypingTranspileState>),
462
- GTSShortcutArgumentExpression(node, { state, visit }): MemberExpression {
463
- const lhs = { ...state.fnArgId };
464
- return {
465
- type: "MemberExpression",
466
- object: lhs,
467
- computed: false,
468
- optional: false,
469
- property: visit(node.property) as Identifier,
470
- };
471
- },
472
491
  };
package/src/types.ts CHANGED
@@ -6,7 +6,6 @@ import type * as AST from "estree";
6
6
  type ForInit = boolean | "await";
7
7
 
8
8
  declare module "estree" {
9
-
10
9
  interface Identifier {
11
10
  isDummy?: boolean;
12
11
  }
@@ -24,17 +23,12 @@ declare module "estree" {
24
23
  GTSShortcutFunctionExpression: GTSShortcutFunctionExpression;
25
24
  GTSQueryExpression: GTSQueryExpression;
26
25
  }
27
-
28
- interface ImportDeclaration {
29
- /** Emit inner diagnostics to the top-of-file */
30
- diagnosticsOnTop?: boolean;
31
- }
32
-
26
+
33
27
  interface SimpleCallExpression {
34
- lParenLoc?: SourceLocation;
28
+ lParenRange?: [number, number];
35
29
  }
36
30
  interface NewExpression {
37
- lParenLoc?: SourceLocation;
31
+ lParenRange?: [number, number];
38
32
  }
39
33
 
40
34
  interface GTSDefineStatement extends BaseStatement {