@defold-typescript/transpiler 0.39.0 → 0.41.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/dist/index.d.ts CHANGED
@@ -32,6 +32,8 @@ export type { SceneMessage } from "./scene-text-format";
32
32
  export { parseSceneTextFormat, SceneTextFormatError } from "./scene-text-format";
33
33
  export type { TranspileSession } from "./session";
34
34
  export { createTranspileSession } from "./session";
35
+ export type { TextScriptPropertyFinding } from "./text-script-property";
36
+ export { findTextScriptProperties } from "./text-script-property";
35
37
  export { TIMERS_REQUIRE_NAME } from "./timers-runtime";
36
38
  export type { TranspileDiagnostic, TranspileProjectInput, TranspileProjectResult, TranspileResult, } from "./transpile";
37
39
  export { transpile, transpileProject } from "./transpile";
package/dist/index.js CHANGED
@@ -270,11 +270,11 @@ function emitInitMerge(fn, context, property) {
270
270
  const initFn = createFunctionExpression2(createBlock2([stateDecl, guard]), [createIdentifier2("self")], undefined, NodeFlags2.Declaration, fn);
271
271
  return [builderDecl, createAssignmentStatement(createIdentifier2("init"), initFn, property)];
272
272
  }
273
- function emitPropertyRegistrations(property, context) {
273
+ function propertyMembers(property) {
274
274
  if (!ts2.isPropertyAssignment(property) || !ts2.isObjectLiteralExpression(property.initializer)) {
275
275
  return [];
276
276
  }
277
- const registrations = [];
277
+ const members = [];
278
278
  for (const member of property.initializer.properties) {
279
279
  if (!ts2.isPropertyAssignment(member)) {
280
280
  continue;
@@ -283,21 +283,34 @@ function emitPropertyRegistrations(property, context) {
283
283
  if (!ts2.isIdentifier(key) && !ts2.isStringLiteral(key)) {
284
284
  continue;
285
285
  }
286
- registrations.push(createExpressionStatement(createCallExpression2(createTableIndexExpression(createIdentifier2("go"), createStringLiteral2("property")), [createStringLiteral2(key.text), context.transformExpression(member.initializer)])));
286
+ members.push({ name: key.text, initializer: member.initializer });
287
287
  }
288
- return registrations;
288
+ return members;
289
289
  }
290
- function eraseFactoryCall(expression, context) {
290
+ function factoryHooks(expression, checker) {
291
291
  if (!ts2.isCallExpression(expression)) {
292
292
  return;
293
293
  }
294
- if (!resolvesToFactoryExport(expression.expression, context.checker)) {
294
+ if (!resolvesToFactoryExport(expression.expression, checker)) {
295
295
  return;
296
296
  }
297
297
  const hooks = expression.arguments[0];
298
298
  if (hooks === undefined || !ts2.isObjectLiteralExpression(hooks)) {
299
299
  return;
300
300
  }
301
+ return hooks;
302
+ }
303
+ function isPropertiesHook(property) {
304
+ return hookName(property) === "properties";
305
+ }
306
+ function emitPropertyRegistrations(property, context) {
307
+ return propertyMembers(property).map(({ name, initializer }) => createExpressionStatement(createCallExpression2(createTableIndexExpression(createIdentifier2("go"), createStringLiteral2("property")), [createStringLiteral2(name), context.transformExpression(initializer)])));
308
+ }
309
+ function eraseFactoryCall(expression, context) {
310
+ const hooks = factoryHooks(expression, context.checker);
311
+ if (hooks === undefined) {
312
+ return;
313
+ }
301
314
  const registrations = [];
302
315
  const statements = [];
303
316
  for (const property of hooks.properties) {
@@ -2058,7 +2071,7 @@ function computeOutputRel(rel, config, kind) {
2058
2071
  return baseRel.replace(/\.ts$/, SCRIPT_SUFFIX_BY_KIND[kind]);
2059
2072
  }
2060
2073
  // src/program-diagnostics.ts
2061
- import * as ts8 from "typescript";
2074
+ import * as ts9 from "typescript";
2062
2075
  import * as tstl from "typescript-to-lua";
2063
2076
 
2064
2077
  // src/message-guard-lowering.ts
@@ -2126,11 +2139,41 @@ var timersLoweringPlugin = {
2126
2139
  }
2127
2140
  };
2128
2141
 
2129
- // src/window-event-guard-lowering.ts
2142
+ // src/type-application-erasure.ts
2130
2143
  import * as ts7 from "typescript";
2144
+ var TYPE_APPLICATION_APIS = new Set(["go.get", "go.set"]);
2145
+ function isTypeApplication(call, checker) {
2146
+ if (call.arguments.length !== 0)
2147
+ return false;
2148
+ const declaration = checker.getResolvedSignature(call)?.declaration;
2149
+ if (declaration === undefined || !ts7.isFunctionDeclaration(declaration))
2150
+ return false;
2151
+ if (declaration.parameters.length !== 0 || declaration.typeParameters === undefined)
2152
+ return false;
2153
+ if (declaration.name === undefined)
2154
+ return false;
2155
+ const symbol = checker.getSymbolAtLocation(declaration.name);
2156
+ if (symbol === undefined)
2157
+ return false;
2158
+ const fqn = checker.getFullyQualifiedName(symbol);
2159
+ return fqn.startsWith("global.") && TYPE_APPLICATION_APIS.has(fqn.slice("global.".length));
2160
+ }
2161
+ var typeApplicationErasurePlugin = {
2162
+ visitors: {
2163
+ [ts7.SyntaxKind.CallExpression]: (node, context) => {
2164
+ if (isTypeApplication(node, context.checker)) {
2165
+ return context.transformExpression(node.expression);
2166
+ }
2167
+ return context.superTransformExpression(node);
2168
+ }
2169
+ }
2170
+ };
2171
+
2172
+ // src/window-event-guard-lowering.ts
2173
+ import * as ts8 from "typescript";
2131
2174
  import {
2132
2175
  createBinaryExpression as createBinaryExpression4,
2133
- SyntaxKind as SyntaxKind10
2176
+ SyntaxKind as SyntaxKind11
2134
2177
  } from "typescript-to-lua";
2135
2178
  var GUARD_MODULE2 = "@defold-typescript/types";
2136
2179
  var GUARD_NAME2 = "isWindowEvent";
@@ -2139,7 +2182,7 @@ function resolvesToGuardExport2(callee, checker) {
2139
2182
  if (symbol === undefined) {
2140
2183
  return false;
2141
2184
  }
2142
- if (symbol.flags & ts7.SymbolFlags.Alias) {
2185
+ if (symbol.flags & ts8.SymbolFlags.Alias) {
2143
2186
  symbol = checker.getAliasedSymbol(symbol);
2144
2187
  }
2145
2188
  if (symbol.getName() !== GUARD_NAME2) {
@@ -2153,12 +2196,12 @@ function resolvesToGuardExport2(callee, checker) {
2153
2196
  }
2154
2197
  var windowEventGuardLoweringPlugin = {
2155
2198
  visitors: {
2156
- [ts7.SyntaxKind.CallExpression]: (node, context) => {
2199
+ [ts8.SyntaxKind.CallExpression]: (node, context) => {
2157
2200
  const [eventArg, , expectedArg] = node.arguments;
2158
2201
  if (node.arguments.length === 3 && eventArg !== undefined && expectedArg !== undefined && resolvesToGuardExport2(node.expression, context.checker)) {
2159
2202
  const event = context.transformExpression(eventArg);
2160
2203
  const expected = context.transformExpression(expectedArg);
2161
- return createBinaryExpression4(event, expected, SyntaxKind10.EqualityOperator, node);
2204
+ return createBinaryExpression4(event, expected, SyntaxKind11.EqualityOperator, node);
2162
2205
  }
2163
2206
  return context.superTransformExpression(node);
2164
2207
  }
@@ -2171,14 +2214,15 @@ var LUA_PLUGINS = [
2171
2214
  messageGuardLoweringPlugin,
2172
2215
  windowEventGuardLoweringPlugin,
2173
2216
  messageDispatchLoweringPlugin,
2174
- timersLoweringPlugin
2217
+ timersLoweringPlugin,
2218
+ typeApplicationErasurePlugin
2175
2219
  ];
2176
2220
  var noopWriteFile = () => {};
2177
2221
  function isAmbient(fileName) {
2178
2222
  return /[\\/]node_modules[\\/]/.test(fileName) || /(^|[\\/])lib\.[^\\/]*\.d\.ts$/.test(fileName);
2179
2223
  }
2180
2224
  function getProgramDiagnostics(program, sourceFile) {
2181
- const { diagnostics } = tstl.getProgramTranspileResult(ts8.sys, noopWriteFile, {
2225
+ const { diagnostics } = tstl.getProgramTranspileResult(ts9.sys, noopWriteFile, {
2182
2226
  program,
2183
2227
  plugins: LUA_PLUGINS,
2184
2228
  ...sourceFile ? { sourceFiles: [sourceFile] } : {}
@@ -2749,11 +2793,11 @@ function relativeAddressesFrom(index, context) {
2749
2793
  import { readFileSync as readFileSync2 } from "node:fs";
2750
2794
  import { createRequire as createRequire2 } from "node:module";
2751
2795
  import * as path3 from "node:path";
2752
- import * as ts12 from "typescript";
2796
+ import * as ts13 from "typescript";
2753
2797
  import * as tstl3 from "typescript-to-lua";
2754
2798
 
2755
2799
  // src/companion-emit.ts
2756
- import * as ts9 from "typescript";
2800
+ import * as ts10 from "typescript";
2757
2801
  import {
2758
2802
  createAssignmentStatement as createAssignmentStatement2,
2759
2803
  createCallExpression as createCallExpression4,
@@ -2787,8 +2831,8 @@ function exportsIdentifier() {
2787
2831
  }
2788
2832
  function isRuntimeScriptSource(sourceFile, checker) {
2789
2833
  return sourceFile.statements.some((statement) => {
2790
- const expression = ts9.isExpressionStatement(statement) ? statement.expression : ts9.isExportAssignment(statement) && !statement.isExportEquals ? statement.expression : undefined;
2791
- return expression !== undefined && ts9.isCallExpression(expression) && resolvesToFactoryExport(expression.expression, checker);
2834
+ const expression = ts10.isExpressionStatement(statement) ? statement.expression : ts10.isExportAssignment(statement) && !statement.isExportEquals ? statement.expression : undefined;
2835
+ return expression !== undefined && ts10.isCallExpression(expression) && resolvesToFactoryExport(expression.expression, checker);
2792
2836
  });
2793
2837
  }
2794
2838
  function planFor(sourceFile, checker) {
@@ -2940,11 +2984,11 @@ function freeNames(statements) {
2940
2984
  function resolvedRequirePaths(sourceFile, checker) {
2941
2985
  const paths = new Map;
2942
2986
  for (const statement of sourceFile.statements) {
2943
- const specifier = ts9.isImportDeclaration(statement) || ts9.isExportDeclaration(statement) ? statement.moduleSpecifier : undefined;
2944
- if (specifier === undefined || !ts9.isStringLiteral(specifier)) {
2987
+ const specifier = ts10.isImportDeclaration(statement) || ts10.isExportDeclaration(statement) ? statement.moduleSpecifier : undefined;
2988
+ if (specifier === undefined || !ts10.isStringLiteral(specifier)) {
2945
2989
  continue;
2946
2990
  }
2947
- const target = checker.getSymbolAtLocation(specifier)?.declarations?.find((declaration) => ts9.isSourceFile(declaration));
2991
+ const target = checker.getSymbolAtLocation(specifier)?.declarations?.find((declaration) => ts10.isSourceFile(declaration));
2948
2992
  if (target === undefined || target.isDeclarationFile) {
2949
2993
  continue;
2950
2994
  }
@@ -3108,11 +3152,11 @@ function createCompanionEmitPlugin() {
3108
3152
  return statements;
3109
3153
  }
3110
3154
  const visitors = {
3111
- [ts9.SyntaxKind.VariableStatement]: capture,
3112
- [ts9.SyntaxKind.FunctionDeclaration]: capture,
3113
- [ts9.SyntaxKind.ClassDeclaration]: capture,
3114
- [ts9.SyntaxKind.EnumDeclaration]: capture,
3115
- [ts9.SyntaxKind.ExportDeclaration]: capture
3155
+ [ts10.SyntaxKind.VariableStatement]: capture,
3156
+ [ts10.SyntaxKind.FunctionDeclaration]: capture,
3157
+ [ts10.SyntaxKind.ClassDeclaration]: capture,
3158
+ [ts10.SyntaxKind.EnumDeclaration]: capture,
3159
+ [ts10.SyntaxKind.ExportDeclaration]: capture
3116
3160
  };
3117
3161
  let configuredNoResolvePaths = [];
3118
3162
  const plugin = {
@@ -3181,7 +3225,7 @@ function emitCompanion(program, emitHost, file, sourceFile, plan, owned, compani
3181
3225
  }
3182
3226
 
3183
3227
  // src/editor-script-erasure.ts
3184
- import * as ts10 from "typescript";
3228
+ import * as ts11 from "typescript";
3185
3229
  import {
3186
3230
  createReturnStatement as createReturnStatement3
3187
3231
  } from "typescript-to-lua";
@@ -3201,7 +3245,7 @@ function resolveEditorFactoryExport(callee, checker) {
3201
3245
  if (symbol === undefined) {
3202
3246
  return;
3203
3247
  }
3204
- if (symbol.flags & ts10.SymbolFlags.Alias) {
3248
+ if (symbol.flags & ts11.SymbolFlags.Alias) {
3205
3249
  symbol = checker.getAliasedSymbol(symbol);
3206
3250
  }
3207
3251
  const name = symbol.getName();
@@ -3215,7 +3259,7 @@ function resolveEditorFactoryExport(callee, checker) {
3215
3259
  return declaration.getSourceFile().fileName.includes(EDITOR_FACTORY_MODULE) ? name : undefined;
3216
3260
  }
3217
3261
  function lowerEditorFactoryCall(expression, context) {
3218
- if (!ts10.isCallExpression(expression)) {
3262
+ if (!ts11.isCallExpression(expression)) {
3219
3263
  return;
3220
3264
  }
3221
3265
  if (resolveEditorFactoryExport(expression.expression, context.checker) !== EDITOR_SCRIPT_FACTORY_NAME) {
@@ -3228,7 +3272,7 @@ function lowerEditorFactoryCall(expression, context) {
3228
3272
  return [createReturnStatement3([context.transformExpression(module)])];
3229
3273
  }
3230
3274
  function isEditorFactoryOnlyImport(node) {
3231
- if (!ts10.isStringLiteral(node.moduleSpecifier) || !EDITOR_FACTORY_SPECIFIERS.has(node.moduleSpecifier.text)) {
3275
+ if (!ts11.isStringLiteral(node.moduleSpecifier) || !EDITOR_FACTORY_SPECIFIERS.has(node.moduleSpecifier.text)) {
3232
3276
  return false;
3233
3277
  }
3234
3278
  const clause = node.importClause;
@@ -3239,7 +3283,7 @@ function isEditorFactoryOnlyImport(node) {
3239
3283
  return false;
3240
3284
  }
3241
3285
  const bindings = clause.namedBindings;
3242
- if (bindings === undefined || !ts10.isNamedImports(bindings)) {
3286
+ if (bindings === undefined || !ts11.isNamedImports(bindings)) {
3243
3287
  return false;
3244
3288
  }
3245
3289
  const runtime = bindings.elements.filter((element) => !element.isTypeOnly);
@@ -3250,7 +3294,7 @@ function isEditorFactoryOnlyImport(node) {
3250
3294
  }
3251
3295
  var editorScriptErasurePlugin = {
3252
3296
  visitors: {
3253
- [ts10.SyntaxKind.ExportAssignment]: (node, context) => {
3297
+ [ts11.SyntaxKind.ExportAssignment]: (node, context) => {
3254
3298
  if (!node.isExportEquals) {
3255
3299
  const lowered = lowerEditorFactoryCall(node.expression, context);
3256
3300
  if (lowered !== undefined) {
@@ -3259,14 +3303,14 @@ var editorScriptErasurePlugin = {
3259
3303
  }
3260
3304
  return context.superTransformStatements(node);
3261
3305
  },
3262
- [ts10.SyntaxKind.CallExpression]: (node, context) => {
3306
+ [ts11.SyntaxKind.CallExpression]: (node, context) => {
3263
3307
  const command = node.arguments[0];
3264
3308
  if (command !== undefined && resolveEditorFactoryExport(node.expression, context.checker) === EDITOR_COMMAND_FACTORY_NAME) {
3265
3309
  return context.transformExpression(command);
3266
3310
  }
3267
3311
  return context.superTransformExpression(node);
3268
3312
  },
3269
- [ts10.SyntaxKind.ImportDeclaration]: (node, context) => isEditorFactoryOnlyImport(node) ? [] : context.superTransformStatements(node)
3313
+ [ts11.SyntaxKind.ImportDeclaration]: (node, context) => isEditorFactoryOnlyImport(node) ? [] : context.superTransformStatements(node)
3270
3314
  }
3271
3315
  };
3272
3316
 
@@ -3277,17 +3321,17 @@ import * as path2 from "node:path";
3277
3321
  import * as tstl2 from "typescript-to-lua";
3278
3322
 
3279
3323
  // src/go-property-direct-call.ts
3280
- import * as ts11 from "typescript";
3324
+ import * as ts12 from "typescript";
3281
3325
  var GO_PROPERTY_DIRECT_CALL_MESSAGE = "`go.property` called directly is deprecated; declare the property in `defineScript({ properties })` so it types onto `self` (the transpiler emits the registration for you).";
3282
3326
  function findDirectGoPropertyCalls(sourceFile) {
3283
3327
  const calls = [];
3284
3328
  const visit = (node) => {
3285
- if (ts11.isCallExpression(node) && ts11.isPropertyAccessExpression(node.expression) && node.expression.name.text === "property" && ts11.isIdentifier(node.expression.expression) && node.expression.expression.text === "go") {
3329
+ if (ts12.isCallExpression(node) && ts12.isPropertyAccessExpression(node.expression) && node.expression.name.text === "property" && ts12.isIdentifier(node.expression.expression) && node.expression.expression.text === "go") {
3286
3330
  calls.push(node);
3287
3331
  }
3288
- ts11.forEachChild(node, visit);
3332
+ ts12.forEachChild(node, visit);
3289
3333
  };
3290
- ts11.forEachChild(sourceFile, visit);
3334
+ ts12.forEachChild(sourceFile, visit);
3291
3335
  return calls;
3292
3336
  }
3293
3337
 
@@ -3467,6 +3511,7 @@ function transpileProject(input) {
3467
3511
  { plugin: windowEventGuardLoweringPlugin },
3468
3512
  { plugin: messageDispatchLoweringPlugin },
3469
3513
  { plugin: timersLoweringPlugin },
3514
+ { plugin: typeApplicationErasurePlugin },
3470
3515
  { plugin: companionEmit.plugin }
3471
3516
  ]
3472
3517
  });
@@ -3497,6 +3542,7 @@ function compilerOptions(companionEmit) {
3497
3542
  { plugin: windowEventGuardLoweringPlugin },
3498
3543
  { plugin: messageDispatchLoweringPlugin },
3499
3544
  { plugin: timersLoweringPlugin },
3545
+ { plugin: typeApplicationErasurePlugin },
3500
3546
  { plugin: companionEmit.plugin }
3501
3547
  ]
3502
3548
  };
@@ -3539,7 +3585,7 @@ function createTranspileSession() {
3539
3585
  if (cached) {
3540
3586
  return cached;
3541
3587
  }
3542
- const created = ts12.createSourceFile(fileName, content, ts12.ScriptTarget.Latest, false);
3588
+ const created = ts13.createSourceFile(fileName, content, ts13.ScriptTarget.Latest, false);
3543
3589
  sourceFileCache.set(normalized, created);
3544
3590
  return created;
3545
3591
  }
@@ -3558,7 +3604,7 @@ function createTranspileSession() {
3558
3604
  return cached;
3559
3605
  }
3560
3606
  const fileContent = readFileSync2(filePath, "utf8");
3561
- const created = ts12.createSourceFile(filePath, fileContent, ts12.ScriptTarget.Latest, false);
3607
+ const created = ts13.createSourceFile(filePath, fileContent, ts13.ScriptTarget.Latest, false);
3562
3608
  libCache.set(fileName, created);
3563
3609
  return created;
3564
3610
  }
@@ -3571,10 +3617,10 @@ function createTranspileSession() {
3571
3617
  return AMBIENT_FILES[normalized];
3572
3618
  }
3573
3619
  const host = {
3574
- fileExists: (fileName) => mergedContent(normalizeSlashes(fileName)) !== undefined || ts12.sys.fileExists(fileName),
3620
+ fileExists: (fileName) => mergedContent(normalizeSlashes(fileName)) !== undefined || ts13.sys.fileExists(fileName),
3575
3621
  getCanonicalFileName: (fileName) => fileName,
3576
3622
  getCurrentDirectory: () => "",
3577
- getDefaultLibFileName: ts12.getDefaultLibFileName,
3623
+ getDefaultLibFileName: ts13.getDefaultLibFileName,
3578
3624
  readFile: () => "",
3579
3625
  getNewLine: () => `
3580
3626
  `,
@@ -3596,15 +3642,15 @@ function createTranspileSession() {
3596
3642
  const userKeys = new Set(userFiles.keys());
3597
3643
  const rootNames = [...Object.keys(AMBIENT_FILES).map(normalizeSlashes), ...userKeys];
3598
3644
  const companionEmit = createCompanionEmitPlugin();
3599
- program = ts12.createProgram(rootNames, compilerOptions(companionEmit), host, program);
3600
- const preEmitDiagnostics = ts12.getPreEmitDiagnostics(program);
3645
+ program = ts13.createProgram(rootNames, compilerOptions(companionEmit), host, program);
3646
+ const preEmitDiagnostics = ts13.getPreEmitDiagnostics(program);
3601
3647
  const collector = createOutputCollector();
3602
3648
  const { diagnostics: transpileDiagnostics } = new tstl3.Transpiler().emit({
3603
3649
  program,
3604
3650
  writeFile: collector.writeFile
3605
3651
  });
3606
3652
  const diagnostics = [
3607
- ...ts12.sortAndDeduplicateDiagnostics([...preEmitDiagnostics, ...transpileDiagnostics])
3653
+ ...ts13.sortAndDeduplicateDiagnostics([...preEmitDiagnostics, ...transpileDiagnostics])
3608
3654
  ];
3609
3655
  return collectOutputs(collector.files, diagnostics, userKeys, companionEmit.companions);
3610
3656
  }
@@ -3613,8 +3659,71 @@ function createTranspileSession() {
3613
3659
  getProgram: () => program
3614
3660
  };
3615
3661
  }
3662
+ // src/text-script-property.ts
3663
+ import * as ts14 from "typescript";
3664
+ function findTextScriptProperties(program, fileNames) {
3665
+ const checker = program.getTypeChecker();
3666
+ const findings = [];
3667
+ for (const file of fileNames) {
3668
+ const sourceFile = program.getSourceFile(file);
3669
+ if (sourceFile === undefined) {
3670
+ continue;
3671
+ }
3672
+ const located = [];
3673
+ const visit = (node) => {
3674
+ if (ts14.isCallExpression(node)) {
3675
+ const hooks = factoryHooks(node, checker);
3676
+ for (const property of hooks?.properties ?? []) {
3677
+ if (!isPropertiesHook(property)) {
3678
+ continue;
3679
+ }
3680
+ for (const { name, initializer } of propertyMembers(property)) {
3681
+ if (mayHoldString(checker.getTypeAtLocation(initializer), checker)) {
3682
+ located.push({ name, value: initializer });
3683
+ }
3684
+ }
3685
+ }
3686
+ }
3687
+ ts14.forEachChild(node, visit);
3688
+ };
3689
+ visit(sourceFile);
3690
+ for (const call of findDirectGoPropertyCalls(sourceFile)) {
3691
+ const [nameArgument, value] = call.arguments;
3692
+ if (nameArgument === undefined || value === undefined) {
3693
+ continue;
3694
+ }
3695
+ if (mayHoldString(checker.getTypeAtLocation(value), checker)) {
3696
+ const name = ts14.isStringLiteralLike(nameArgument) ? nameArgument.text : nameArgument.getText(sourceFile);
3697
+ located.push({ name, value });
3698
+ }
3699
+ }
3700
+ located.sort((left, right) => left.value.getStart(sourceFile) - right.value.getStart(sourceFile));
3701
+ for (const { name, value } of located) {
3702
+ const position = sourceFile.getLineAndCharacterOfPosition(value.getStart(sourceFile));
3703
+ findings.push({ name, file, line: position.line + 1, column: position.character + 1 });
3704
+ }
3705
+ }
3706
+ return findings;
3707
+ }
3708
+ function mayHoldString(type, checker, visited = new Set) {
3709
+ if (visited.has(type)) {
3710
+ return false;
3711
+ }
3712
+ visited.add(type);
3713
+ if (type.flags & ts14.TypeFlags.StringLike) {
3714
+ return true;
3715
+ }
3716
+ if (type.isUnionOrIntersection()) {
3717
+ return type.types.some((member) => mayHoldString(member, checker, visited));
3718
+ }
3719
+ if (type.flags & ts14.TypeFlags.Instantiable) {
3720
+ const constraint = checker.getBaseConstraintOfType(type);
3721
+ return constraint !== undefined && constraint !== type ? mayHoldString(constraint, checker, visited) : false;
3722
+ }
3723
+ return false;
3724
+ }
3616
3725
  // src/url-address-slots.ts
3617
- import * as ts13 from "typescript";
3726
+ import * as ts15 from "typescript";
3618
3727
  var ADDRESS_CLASSES = {
3619
3728
  "game-object": true,
3620
3729
  component: true,
@@ -3633,7 +3742,7 @@ function isFragmentCaret(slot, position) {
3633
3742
  return slot.fragmentStart !== -1 && position >= slot.fragmentStart;
3634
3743
  }
3635
3744
  function tableKey(checker, callee) {
3636
- const symbol = checker.getSymbolAtLocation(ts13.isIdentifier(callee) ? callee : callee.name);
3745
+ const symbol = checker.getSymbolAtLocation(ts15.isIdentifier(callee) ? callee : callee.name);
3637
3746
  if (!symbol)
3638
3747
  return;
3639
3748
  const fqn = checker.getFullyQualifiedName(symbol);
@@ -3641,43 +3750,43 @@ function tableKey(checker, callee) {
3641
3750
  }
3642
3751
  function parameterName(checker, call, index) {
3643
3752
  const declaration = checker.getResolvedSignature(call)?.declaration;
3644
- if (!declaration || !ts13.isFunctionLike(declaration))
3753
+ if (!declaration || !ts15.isFunctionLike(declaration))
3645
3754
  return;
3646
3755
  const parameter = declaration.parameters[index];
3647
- if (!parameter || parameter.dotDotDotToken || !ts13.isIdentifier(parameter.name))
3756
+ if (!parameter || parameter.dotDotDotToken || !ts15.isIdentifier(parameter.name))
3648
3757
  return;
3649
3758
  return parameter.name.text;
3650
3759
  }
3651
3760
  var EQUALITY_OPERATORS = new Set([
3652
- ts13.SyntaxKind.EqualsEqualsEqualsToken,
3653
- ts13.SyntaxKind.ExclamationEqualsEqualsToken,
3654
- ts13.SyntaxKind.EqualsEqualsToken,
3655
- ts13.SyntaxKind.ExclamationEqualsToken
3761
+ ts15.SyntaxKind.EqualsEqualsEqualsToken,
3762
+ ts15.SyntaxKind.ExclamationEqualsEqualsToken,
3763
+ ts15.SyntaxKind.EqualsEqualsToken,
3764
+ ts15.SyntaxKind.ExclamationEqualsToken
3656
3765
  ]);
3657
3766
  function comparisonSatisfied(checker, entry, call) {
3658
3767
  if (entry.comparedParameter === undefined)
3659
3768
  return true;
3660
3769
  let node = call;
3661
- while (ts13.isParenthesizedExpression(node.parent))
3770
+ while (ts15.isParenthesizedExpression(node.parent))
3662
3771
  node = node.parent;
3663
3772
  const comparison = node.parent;
3664
- if (!ts13.isBinaryExpression(comparison))
3773
+ if (!ts15.isBinaryExpression(comparison))
3665
3774
  return false;
3666
3775
  if (!EQUALITY_OPERATORS.has(comparison.operatorToken.kind))
3667
3776
  return false;
3668
3777
  const other = comparison.left === node ? comparison.right : comparison.left;
3669
3778
  const symbol = checker.getSymbolAtLocation(other);
3670
- return (symbol?.declarations ?? []).some((declaration) => ts13.isParameter(declaration) && ts13.isIdentifier(declaration.name) && declaration.name.text === entry.comparedParameter);
3779
+ return (symbol?.declarations ?? []).some((declaration) => ts15.isParameter(declaration) && ts15.isIdentifier(declaration.name) && declaration.name.text === entry.comparedParameter);
3671
3780
  }
3672
3781
  function classifiedEntryOfArgument(checker, table, argument) {
3673
3782
  const call = argument.parent;
3674
- if (!ts13.isCallExpression(call))
3783
+ if (!ts15.isCallExpression(call))
3675
3784
  return;
3676
3785
  const argumentIndex = call.arguments.indexOf(argument);
3677
3786
  if (argumentIndex === -1)
3678
3787
  return;
3679
3788
  const callee = call.expression;
3680
- if (!ts13.isPropertyAccessExpression(callee) && !ts13.isIdentifier(callee))
3789
+ if (!ts15.isPropertyAccessExpression(callee) && !ts15.isIdentifier(callee))
3681
3790
  return;
3682
3791
  const fqn = tableKey(checker, callee);
3683
3792
  if (fqn === undefined)
@@ -3694,10 +3803,10 @@ function classifiedEntryOfArgument(checker, table, argument) {
3694
3803
  }
3695
3804
  function slotThroughHashedCall(checker, table, literal) {
3696
3805
  const call = literal.parent;
3697
- if (!ts13.isCallExpression(call))
3806
+ if (!ts15.isCallExpression(call))
3698
3807
  return;
3699
3808
  const callee = call.expression;
3700
- if (!ts13.isPropertyAccessExpression(callee) && !ts13.isIdentifier(callee))
3809
+ if (!ts15.isPropertyAccessExpression(callee) && !ts15.isIdentifier(callee))
3701
3810
  return;
3702
3811
  if (tableKey(checker, callee) !== "hash")
3703
3812
  return;
@@ -3715,24 +3824,24 @@ function addressTextOf(checker, entry, call) {
3715
3824
  if (entry.addressParameter === undefined)
3716
3825
  return;
3717
3826
  const declaration = checker.getResolvedSignature(call)?.declaration;
3718
- if (!declaration || !ts13.isFunctionLike(declaration))
3827
+ if (!declaration || !ts15.isFunctionLike(declaration))
3719
3828
  return;
3720
- const index = declaration.parameters.findIndex((parameter) => !parameter.dotDotDotToken && ts13.isIdentifier(parameter.name) && parameter.name.text === entry.addressParameter);
3829
+ const index = declaration.parameters.findIndex((parameter) => !parameter.dotDotDotToken && ts15.isIdentifier(parameter.name) && parameter.name.text === entry.addressParameter);
3721
3830
  if (index === -1)
3722
3831
  return;
3723
3832
  const argument = call.arguments[index];
3724
- return argument && ts13.isStringLiteralLike(argument) ? argument.text : undefined;
3833
+ return argument && ts15.isStringLiteralLike(argument) ? argument.text : undefined;
3725
3834
  }
3726
3835
  function innermostLiteralAt(sourceFile, position) {
3727
3836
  let found;
3728
3837
  const visit = (node) => {
3729
3838
  if (position < node.getStart(sourceFile) || position > node.getEnd())
3730
3839
  return;
3731
- if (ts13.isStringLiteralLike(node))
3840
+ if (ts15.isStringLiteralLike(node))
3732
3841
  found = node;
3733
- ts13.forEachChild(node, visit);
3842
+ ts15.forEachChild(node, visit);
3734
3843
  };
3735
- ts13.forEachChild(sourceFile, visit);
3844
+ ts15.forEachChild(sourceFile, visit);
3736
3845
  return found;
3737
3846
  }
3738
3847
  function resolveClassifiedSlotAtPosition(input) {
@@ -3766,23 +3875,23 @@ function resolveClassifiedSlotAtPosition(input) {
3766
3875
  };
3767
3876
  }
3768
3877
  // src/url-cross-world-address.ts
3769
- import * as ts15 from "typescript";
3878
+ import * as ts17 from "typescript";
3770
3879
 
3771
3880
  // src/url-address-literals.ts
3772
- import * as ts14 from "typescript";
3881
+ import * as ts16 from "typescript";
3773
3882
  function isAmbient2(fileName) {
3774
3883
  return /[\\/]node_modules[\\/]/.test(fileName) || /(^|[\\/])lib\.[^\\/]*\.d\.ts$/.test(fileName);
3775
3884
  }
3776
3885
  function referenceTargetSymbol(type) {
3777
- if ((type.flags & ts14.TypeFlags.Object) === 0)
3886
+ if ((type.flags & ts16.TypeFlags.Object) === 0)
3778
3887
  return;
3779
- if ((type.objectFlags & ts14.ObjectFlags.Reference) === 0)
3888
+ if ((type.objectFlags & ts16.ObjectFlags.Reference) === 0)
3780
3889
  return;
3781
3890
  return type.target.symbol;
3782
3891
  }
3783
3892
  function ambientHashAliasTargets(checker, anchor) {
3784
3893
  const targets = new Set;
3785
- for (const symbol of checker.getSymbolsInScope(anchor, ts14.SymbolFlags.Type)) {
3894
+ for (const symbol of checker.getSymbolsInScope(anchor, ts16.SymbolFlags.Type)) {
3786
3895
  if (symbol.name !== "Hash")
3787
3896
  continue;
3788
3897
  const target = referenceTargetSymbol(checker.getDeclaredTypeOfSymbol(symbol));
@@ -3798,7 +3907,7 @@ function canonicalHashSymbols(checker, program) {
3798
3907
  if (!anchor)
3799
3908
  return canonical;
3800
3909
  const aliased = ambientHashAliasTargets(checker, anchor);
3801
- for (const symbol of checker.getSymbolsInScope(anchor, ts14.SymbolFlags.Function)) {
3910
+ for (const symbol of checker.getSymbolsInScope(anchor, ts16.SymbolFlags.Function)) {
3802
3911
  if (symbol.name !== "hash")
3803
3912
  continue;
3804
3913
  const declaration = symbol.declarations?.[0];
@@ -3820,7 +3929,7 @@ function hashedSourceOfType(checker, type, canonical) {
3820
3929
  return source?.isStringLiteral() ? source.value : undefined;
3821
3930
  }
3822
3931
  function staticAddressTextOf(checker, node, canonical) {
3823
- return ts14.isStringLiteralLike(node) ? node.text : hashedSourceOfType(checker, checker.getTypeAtLocation(node), canonical);
3932
+ return ts16.isStringLiteralLike(node) ? node.text : hashedSourceOfType(checker, checker.getTypeAtLocation(node), canonical);
3824
3933
  }
3825
3934
 
3826
3935
  // src/url-cross-world-address.ts
@@ -3844,7 +3953,7 @@ function checkCrossWorldAddresses(input) {
3844
3953
  if (worlds.length === 0)
3845
3954
  continue;
3846
3955
  const visit = (node) => {
3847
- if (ts15.isExpression(node)) {
3956
+ if (ts17.isExpression(node)) {
3848
3957
  const entry = addressEntryOfArgument(checker, table, node);
3849
3958
  if (entry !== undefined && slotRejectsForeignSocket(entry)) {
3850
3959
  const address = staticAddressTextOf(checker, node, canonical);
@@ -3861,14 +3970,14 @@ function checkCrossWorldAddresses(input) {
3861
3970
  }
3862
3971
  }
3863
3972
  }
3864
- ts15.forEachChild(node, visit);
3973
+ ts17.forEachChild(node, visit);
3865
3974
  };
3866
- ts15.forEachChild(sourceFile, visit);
3975
+ ts17.forEachChild(sourceFile, visit);
3867
3976
  }
3868
3977
  return findings;
3869
3978
  }
3870
3979
  // src/url-fragment-reachability.ts
3871
- import * as ts16 from "typescript";
3980
+ import * as ts18 from "typescript";
3872
3981
  function pathOfKey(key) {
3873
3982
  const socket = socketOfAddress(key);
3874
3983
  return socket === undefined ? key : key.slice(socket.length + 1);
@@ -3913,7 +4022,7 @@ function checkUrlFragmentReachability(input) {
3913
4022
  if (isAmbient2(sourceFile.fileName))
3914
4023
  continue;
3915
4024
  const visit = (node) => {
3916
- if (ts16.isExpression(node) && isAddressClass(addressClassOfArgument(checker, table, node))) {
4025
+ if (ts18.isExpression(node) && isAddressClass(addressClassOfArgument(checker, table, node))) {
3917
4026
  const text = staticAddressTextOf(checker, node, canonical);
3918
4027
  if (text !== undefined) {
3919
4028
  const hash = text.indexOf("#");
@@ -3932,9 +4041,9 @@ function checkUrlFragmentReachability(input) {
3932
4041
  }
3933
4042
  }
3934
4043
  }
3935
- ts16.forEachChild(node, visit);
4044
+ ts18.forEachChild(node, visit);
3936
4045
  };
3937
- ts16.forEachChild(sourceFile, visit);
4046
+ ts18.forEachChild(sourceFile, visit);
3938
4047
  }
3939
4048
  return { kind: "checked", findings };
3940
4049
  }
@@ -3979,6 +4088,7 @@ export {
3979
4088
  displayPathOf,
3980
4089
  findEmittedRequires,
3981
4090
  findMainEntryFactoryImports,
4091
+ findTextScriptProperties,
3982
4092
  getProgramDiagnostics,
3983
4093
  guiScriptResourceOf,
3984
4094
  guiScriptResourcesOf,
@@ -3,5 +3,12 @@ import { type Plugin } from "typescript-to-lua";
3
3
  export declare const FACTORY_MODULE = "@defold-typescript/types";
4
4
  export declare const FACTORY_NAMES: Set<string>;
5
5
  export declare function resolvesToFactoryExport(callee: ts.Expression, checker: ts.TypeChecker): boolean;
6
+ export interface PropertyMember {
7
+ readonly name: string;
8
+ readonly initializer: ts.Expression;
9
+ }
10
+ export declare function propertyMembers(property: ts.ObjectLiteralElementLike): PropertyMember[];
11
+ export declare function factoryHooks(expression: ts.Expression, checker: ts.TypeChecker): ts.ObjectLiteralExpression | undefined;
12
+ export declare function isPropertiesHook(property: ts.ObjectLiteralElementLike): boolean;
6
13
  export declare function isFactoryOnlyImport(node: ts.ImportDeclaration): boolean;
7
14
  export declare const lifecycleErasurePlugin: Plugin;
@@ -0,0 +1,16 @@
1
+ import * as ts from "typescript";
2
+ export interface TextScriptPropertyFinding {
3
+ readonly name: string;
4
+ readonly file: string;
5
+ /** 1-based line of the default's initializer or the direct call's value. */
6
+ readonly line: number;
7
+ /** 1-based column of the default's initializer or the direct call's value. */
8
+ readonly column: number;
9
+ }
10
+ export declare function findTextScriptProperties(program: ts.Program, fileNames: readonly string[]): TextScriptPropertyFinding[];
11
+ /**
12
+ * Whether a value of `type` may be a string at runtime: a string-like type, a
13
+ * union or intersection with such a member, or a type parameter or other
14
+ * instantiable type whose constraint may hold one.
15
+ */
16
+ export declare function mayHoldString(type: ts.Type, checker: ts.TypeChecker, visited?: Set<ts.Type>): boolean;
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from "typescript-to-lua";
2
+ export declare const TYPE_APPLICATION_APIS: ReadonlySet<string>;
3
+ export declare const typeApplicationErasurePlugin: Plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/transpiler",
3
- "version": "0.39.0",
3
+ "version": "0.41.0",
4
4
  "description": "TypeScript-to-Lua build pipeline tuned for Defold's runtime.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,7 +35,7 @@
35
35
  "test": "bun test"
36
36
  },
37
37
  "dependencies": {
38
- "@defold-typescript/types": "0.39.0",
38
+ "@defold-typescript/types": "0.41.0",
39
39
  "@typescript-to-lua/language-extensions": "1.19.0",
40
40
  "typescript-to-lua": "^1.36.0"
41
41
  }
package/src/index.ts CHANGED
@@ -86,6 +86,8 @@ export type { SceneMessage } from "./scene-text-format";
86
86
  export { parseSceneTextFormat, SceneTextFormatError } from "./scene-text-format";
87
87
  export type { TranspileSession } from "./session";
88
88
  export { createTranspileSession } from "./session";
89
+ export type { TextScriptPropertyFinding } from "./text-script-property";
90
+ export { findTextScriptProperties } from "./text-script-property";
89
91
  export { TIMERS_REQUIRE_NAME } from "./timers-runtime";
90
92
  export type {
91
93
  TranspileDiagnostic,
@@ -208,18 +208,19 @@ function emitInitMerge(
208
208
  return [builderDecl, createAssignmentStatement(createIdentifier("init"), initFn, property)];
209
209
  }
210
210
 
211
- // The value-keyed `properties` field maps each `key: default` to a chunk-scope
212
- // `go.property("key", default)` the editor reads. Emitted before the hook
213
- // assignments and after the source's `local` consts, so a default referencing a
214
- // module const resolves.
215
- function emitPropertyRegistrations(
216
- property: ts.ObjectLiteralElementLike,
217
- context: TransformationContext,
218
- ): Statement[] {
211
+ export interface PropertyMember {
212
+ readonly name: string;
213
+ readonly initializer: ts.Expression;
214
+ }
215
+
216
+ // The `key: default` members of a factory's `properties` field that register as
217
+ // script properties: a property assignment keyed by an identifier or a string
218
+ // literal. Shared by the emitter and every analysis of what it registers.
219
+ export function propertyMembers(property: ts.ObjectLiteralElementLike): PropertyMember[] {
219
220
  if (!ts.isPropertyAssignment(property) || !ts.isObjectLiteralExpression(property.initializer)) {
220
221
  return [];
221
222
  }
222
- const registrations: Statement[] = [];
223
+ const members: PropertyMember[] = [];
223
224
  for (const member of property.initializer.properties) {
224
225
  if (!ts.isPropertyAssignment(member)) {
225
226
  continue;
@@ -228,32 +229,60 @@ function emitPropertyRegistrations(
228
229
  if (!ts.isIdentifier(key) && !ts.isStringLiteral(key)) {
229
230
  continue;
230
231
  }
231
- registrations.push(
232
- createExpressionStatement(
233
- createCallExpression(
234
- createTableIndexExpression(createIdentifier("go"), createStringLiteral("property")),
235
- [createStringLiteral(key.text), context.transformExpression(member.initializer)],
236
- ),
237
- ),
238
- );
232
+ members.push({ name: key.text, initializer: member.initializer });
239
233
  }
240
- return registrations;
234
+ return members;
241
235
  }
242
236
 
243
- function eraseFactoryCall(
237
+ // The hooks object literal of a call to a lifecycle factory, or `undefined` when
238
+ // the expression is not one the erasure rewrites.
239
+ export function factoryHooks(
244
240
  expression: ts.Expression,
245
- context: TransformationContext,
246
- ): Statement[] | undefined {
241
+ checker: ts.TypeChecker,
242
+ ): ts.ObjectLiteralExpression | undefined {
247
243
  if (!ts.isCallExpression(expression)) {
248
244
  return undefined;
249
245
  }
250
- if (!resolvesToFactoryExport(expression.expression, context.checker)) {
246
+ if (!resolvesToFactoryExport(expression.expression, checker)) {
251
247
  return undefined;
252
248
  }
253
249
  const hooks = expression.arguments[0];
254
250
  if (hooks === undefined || !ts.isObjectLiteralExpression(hooks)) {
255
251
  return undefined;
256
252
  }
253
+ return hooks;
254
+ }
255
+
256
+ export function isPropertiesHook(property: ts.ObjectLiteralElementLike): boolean {
257
+ return hookName(property) === "properties";
258
+ }
259
+
260
+ // The value-keyed `properties` field maps each `key: default` to a chunk-scope
261
+ // `go.property("key", default)` the editor reads. Emitted before the hook
262
+ // assignments and after the source's `local` consts, so a default referencing a
263
+ // module const resolves.
264
+ function emitPropertyRegistrations(
265
+ property: ts.ObjectLiteralElementLike,
266
+ context: TransformationContext,
267
+ ): Statement[] {
268
+ return propertyMembers(property).map(({ name, initializer }) =>
269
+ createExpressionStatement(
270
+ createCallExpression(
271
+ createTableIndexExpression(createIdentifier("go"), createStringLiteral("property")),
272
+ [createStringLiteral(name), context.transformExpression(initializer)],
273
+ ),
274
+ ),
275
+ );
276
+ }
277
+
278
+ function eraseFactoryCall(
279
+ expression: ts.Expression,
280
+ context: TransformationContext,
281
+ ): Statement[] | undefined {
282
+ const hooks = factoryHooks(expression, context.checker);
283
+ if (hooks === undefined) {
284
+ return undefined;
285
+ }
257
286
  const registrations: Statement[] = [];
258
287
  const statements: Statement[] = [];
259
288
  for (const property of hooks.properties) {
@@ -4,6 +4,7 @@ import { lifecycleErasurePlugin } from "./lifecycle-erasure";
4
4
  import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
5
5
  import { messageGuardLoweringPlugin } from "./message-guard-lowering";
6
6
  import { timersLoweringPlugin } from "./timers-lowering";
7
+ import { typeApplicationErasurePlugin } from "./type-application-erasure";
7
8
  import { windowEventGuardLoweringPlugin } from "./window-event-guard-lowering";
8
9
 
9
10
  // The same lua plugins the build path lists (transpile.ts / session.ts), so the
@@ -14,6 +15,7 @@ const LUA_PLUGINS: tstl.Plugin[] = [
14
15
  windowEventGuardLoweringPlugin,
15
16
  messageDispatchLoweringPlugin,
16
17
  timersLoweringPlugin,
18
+ typeApplicationErasurePlugin,
17
19
  ];
18
20
 
19
21
  const noopWriteFile: ts.WriteFileCallback = () => {};
package/src/session.ts CHANGED
@@ -10,6 +10,7 @@ import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
10
10
  import { messageGuardLoweringPlugin } from "./message-guard-lowering";
11
11
  import { timersLoweringPlugin } from "./timers-lowering";
12
12
  import { AMBIENT_FILES, collectOutputs, type TranspileProjectResult } from "./transpile";
13
+ import { typeApplicationErasurePlugin } from "./type-application-erasure";
13
14
  import { windowEventGuardLoweringPlugin } from "./window-event-guard-lowering";
14
15
 
15
16
  export interface TranspileSession {
@@ -43,6 +44,7 @@ function compilerOptions(companionEmit: CompanionEmitter): tstl.CompilerOptions
43
44
  { plugin: windowEventGuardLoweringPlugin },
44
45
  { plugin: messageDispatchLoweringPlugin },
45
46
  { plugin: timersLoweringPlugin },
47
+ { plugin: typeApplicationErasurePlugin },
46
48
  { plugin: companionEmit.plugin },
47
49
  ],
48
50
  };
@@ -0,0 +1,100 @@
1
+ import * as ts from "typescript";
2
+ import { findDirectGoPropertyCalls } from "./go-property-direct-call";
3
+ import { factoryHooks, isPropertiesHook, propertyMembers } from "./lifecycle-erasure";
4
+
5
+ export interface TextScriptPropertyFinding {
6
+ readonly name: string;
7
+ readonly file: string;
8
+ /** 1-based line of the default's initializer or the direct call's value. */
9
+ readonly line: number;
10
+ /** 1-based column of the default's initializer or the direct call's value. */
11
+ readonly column: number;
12
+ }
13
+
14
+ // The script properties whose default may hold a string, declared either in a
15
+ // `properties` field or by a direct `go.property` call. Defold registers a
16
+ // string `go.property` only from 1.13.2, so a build for an older target must
17
+ // refuse them; typed rather than literal-driven, so a const holding a string
18
+ // counts.
19
+ export function findTextScriptProperties(
20
+ program: ts.Program,
21
+ fileNames: readonly string[],
22
+ ): TextScriptPropertyFinding[] {
23
+ const checker = program.getTypeChecker();
24
+ const findings: TextScriptPropertyFinding[] = [];
25
+
26
+ for (const file of fileNames) {
27
+ const sourceFile = program.getSourceFile(file);
28
+ if (sourceFile === undefined) {
29
+ continue;
30
+ }
31
+ const located: { name: string; value: ts.Node }[] = [];
32
+ const visit = (node: ts.Node): void => {
33
+ if (ts.isCallExpression(node)) {
34
+ const hooks = factoryHooks(node, checker);
35
+ for (const property of hooks?.properties ?? []) {
36
+ if (!isPropertiesHook(property)) {
37
+ continue;
38
+ }
39
+ for (const { name, initializer } of propertyMembers(property)) {
40
+ if (mayHoldString(checker.getTypeAtLocation(initializer), checker)) {
41
+ located.push({ name, value: initializer });
42
+ }
43
+ }
44
+ }
45
+ }
46
+ ts.forEachChild(node, visit);
47
+ };
48
+ visit(sourceFile);
49
+ for (const call of findDirectGoPropertyCalls(sourceFile)) {
50
+ const [nameArgument, value] = call.arguments;
51
+ if (nameArgument === undefined || value === undefined) {
52
+ continue;
53
+ }
54
+ if (mayHoldString(checker.getTypeAtLocation(value), checker)) {
55
+ const name = ts.isStringLiteralLike(nameArgument)
56
+ ? nameArgument.text
57
+ : nameArgument.getText(sourceFile);
58
+ located.push({ name, value });
59
+ }
60
+ }
61
+ located.sort(
62
+ (left, right) => left.value.getStart(sourceFile) - right.value.getStart(sourceFile),
63
+ );
64
+ for (const { name, value } of located) {
65
+ const position = sourceFile.getLineAndCharacterOfPosition(value.getStart(sourceFile));
66
+ findings.push({ name, file, line: position.line + 1, column: position.character + 1 });
67
+ }
68
+ }
69
+
70
+ return findings;
71
+ }
72
+
73
+ /**
74
+ * Whether a value of `type` may be a string at runtime: a string-like type, a
75
+ * union or intersection with such a member, or a type parameter or other
76
+ * instantiable type whose constraint may hold one.
77
+ */
78
+ export function mayHoldString(
79
+ type: ts.Type,
80
+ checker: ts.TypeChecker,
81
+ visited: Set<ts.Type> = new Set(),
82
+ ): boolean {
83
+ if (visited.has(type)) {
84
+ return false;
85
+ }
86
+ visited.add(type);
87
+ if (type.flags & ts.TypeFlags.StringLike) {
88
+ return true;
89
+ }
90
+ if (type.isUnionOrIntersection()) {
91
+ return type.types.some((member) => mayHoldString(member, checker, visited));
92
+ }
93
+ if (type.flags & ts.TypeFlags.Instantiable) {
94
+ const constraint = checker.getBaseConstraintOfType(type);
95
+ return constraint !== undefined && constraint !== type
96
+ ? mayHoldString(constraint, checker, visited)
97
+ : false;
98
+ }
99
+ return false;
100
+ }
package/src/transpile.ts CHANGED
@@ -14,6 +14,7 @@ import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
14
14
  import { messageGuardLoweringPlugin } from "./message-guard-lowering";
15
15
  import { importsTimersModule, timersLoweringPlugin } from "./timers-lowering";
16
16
  import { TIMERS_RUNTIME } from "./timers-runtime";
17
+ import { typeApplicationErasurePlugin } from "./type-application-erasure";
17
18
  import { windowEventGuardLoweringPlugin } from "./window-event-guard-lowering";
18
19
 
19
20
  export interface TranspileResult {
@@ -321,6 +322,7 @@ export function transpileProject(input: TranspileProjectInput): TranspileProject
321
322
  { plugin: windowEventGuardLoweringPlugin },
322
323
  { plugin: messageDispatchLoweringPlugin },
323
324
  { plugin: timersLoweringPlugin },
325
+ { plugin: typeApplicationErasurePlugin },
324
326
  { plugin: companionEmit.plugin },
325
327
  ],
326
328
  });
@@ -0,0 +1,34 @@
1
+ import * as ts from "typescript";
2
+ import type { Expression, Plugin } from "typescript-to-lua";
3
+
4
+ // The Defold functions declared with a curried type-application overload,
5
+ // `go.get<P>()(url, property)`: the empty call only fixes `P` for the checker.
6
+ export const TYPE_APPLICATION_APIS: ReadonlySet<string> = new Set(["go.get", "go.set"]);
7
+
8
+ // Keyed by the declaration's fully qualified name: only a symbol declared in
9
+ // an ambient `declare global` block carries the `global.` prefix, so a
10
+ // project's own `go` never matches.
11
+ function isTypeApplication(call: ts.CallExpression, checker: ts.TypeChecker): boolean {
12
+ if (call.arguments.length !== 0) return false;
13
+ const declaration = checker.getResolvedSignature(call)?.declaration;
14
+ if (declaration === undefined || !ts.isFunctionDeclaration(declaration)) return false;
15
+ if (declaration.parameters.length !== 0 || declaration.typeParameters === undefined) return false;
16
+ if (declaration.name === undefined) return false;
17
+ const symbol = checker.getSymbolAtLocation(declaration.name);
18
+ if (symbol === undefined) return false;
19
+ const fqn = checker.getFullyQualifiedName(symbol);
20
+ return fqn.startsWith("global.") && TYPE_APPLICATION_APIS.has(fqn.slice("global.".length));
21
+ }
22
+
23
+ // `go.get<P>()` becomes `go.get`, so the curried call reaches the engine as
24
+ // `go.get(url, property)` and a stored `go.get<P>()` is the function itself.
25
+ export const typeApplicationErasurePlugin: Plugin = {
26
+ visitors: {
27
+ [ts.SyntaxKind.CallExpression]: (node, context): Expression => {
28
+ if (isTypeApplication(node, context.checker)) {
29
+ return context.transformExpression(node.expression);
30
+ }
31
+ return context.superTransformExpression(node);
32
+ },
33
+ },
34
+ };