@defold-typescript/transpiler 0.40.0 → 0.42.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 +3 -1
- package/dist/index.js +184 -235
- package/dist/lifecycle-erasure.d.ts +7 -0
- package/dist/project-resources.d.ts +2 -1
- package/dist/text-script-property.d.ts +16 -0
- package/dist/type-application-erasure.d.ts +3 -0
- package/package.json +2 -2
- package/src/index.ts +7 -1
- package/src/lifecycle-erasure.ts +51 -22
- package/src/program-diagnostics.ts +2 -2
- package/src/project-resources.ts +20 -7
- package/src/session.ts +2 -2
- package/src/text-script-property.ts +100 -0
- package/src/transpile.ts +2 -2
- package/src/type-application-erasure.ts +34 -0
- package/dist/array-index-option-lowering.d.ts +0 -6
- package/src/array-index-option-lowering.ts +0 -212
|
@@ -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;
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export declare const SCAFFOLDED_DEFIGNORE_LINES: string[];
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function parseDefignore(text: string): string[];
|
|
3
|
+
export declare function isDefignoredPath(displayPath: string, lines?: readonly string[]): boolean;
|
|
@@ -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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defold-typescript/transpiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.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.
|
|
38
|
+
"@defold-typescript/types": "0.42.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
|
@@ -48,7 +48,11 @@ export {
|
|
|
48
48
|
stripIncludeBase,
|
|
49
49
|
} from "./output-paths";
|
|
50
50
|
export { getProgramDiagnostics } from "./program-diagnostics";
|
|
51
|
-
export {
|
|
51
|
+
export {
|
|
52
|
+
isDefignoredPath,
|
|
53
|
+
parseDefignore,
|
|
54
|
+
SCAFFOLDED_DEFIGNORE_LINES,
|
|
55
|
+
} from "./project-resources";
|
|
52
56
|
export { rewriteEmittedRequires } from "./require-rewrite";
|
|
53
57
|
export { buildSceneAddressDeclaration } from "./scene-address-declaration";
|
|
54
58
|
export type { SceneCollectionRoles, SceneCollectionRolesInput } from "./scene-collection-roles";
|
|
@@ -86,6 +90,8 @@ export type { SceneMessage } from "./scene-text-format";
|
|
|
86
90
|
export { parseSceneTextFormat, SceneTextFormatError } from "./scene-text-format";
|
|
87
91
|
export type { TranspileSession } from "./session";
|
|
88
92
|
export { createTranspileSession } from "./session";
|
|
93
|
+
export type { TextScriptPropertyFinding } from "./text-script-property";
|
|
94
|
+
export { findTextScriptProperties } from "./text-script-property";
|
|
89
95
|
export { TIMERS_REQUIRE_NAME } from "./timers-runtime";
|
|
90
96
|
export type {
|
|
91
97
|
TranspileDiagnostic,
|
package/src/lifecycle-erasure.ts
CHANGED
|
@@ -208,18 +208,19 @@ function emitInitMerge(
|
|
|
208
208
|
return [builderDecl, createAssignmentStatement(createIdentifier("init"), initFn, property)];
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
234
|
+
return members;
|
|
241
235
|
}
|
|
242
236
|
|
|
243
|
-
|
|
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
|
-
|
|
246
|
-
):
|
|
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,
|
|
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) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
2
|
import * as tstl from "typescript-to-lua";
|
|
3
|
-
import { arrayIndexOptionLoweringPlugin } from "./array-index-option-lowering";
|
|
4
3
|
import { lifecycleErasurePlugin } from "./lifecycle-erasure";
|
|
5
4
|
import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
|
|
6
5
|
import { messageGuardLoweringPlugin } from "./message-guard-lowering";
|
|
7
6
|
import { timersLoweringPlugin } from "./timers-lowering";
|
|
7
|
+
import { typeApplicationErasurePlugin } from "./type-application-erasure";
|
|
8
8
|
import { windowEventGuardLoweringPlugin } from "./window-event-guard-lowering";
|
|
9
9
|
|
|
10
10
|
// The same lua plugins the build path lists (transpile.ts / session.ts), so the
|
|
@@ -15,7 +15,7 @@ const LUA_PLUGINS: tstl.Plugin[] = [
|
|
|
15
15
|
windowEventGuardLoweringPlugin,
|
|
16
16
|
messageDispatchLoweringPlugin,
|
|
17
17
|
timersLoweringPlugin,
|
|
18
|
-
|
|
18
|
+
typeApplicationErasurePlugin,
|
|
19
19
|
];
|
|
20
20
|
|
|
21
21
|
const noopWriteFile: ts.WriteFileCallback = () => {};
|
package/src/project-resources.ts
CHANGED
|
@@ -9,12 +9,25 @@
|
|
|
9
9
|
// `.vscode`).
|
|
10
10
|
export const SCAFFOLDED_DEFIGNORE_LINES = ["/node_modules", "/.defold-types", "/.vscode"];
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
export function
|
|
12
|
+
// A project's own `.defignore` text as root-anchored lines, the shape
|
|
13
|
+
// `isDefignoredPath` matches against. A line without its leading `/` names the
|
|
14
|
+
// same root-relative path.
|
|
15
|
+
export function parseDefignore(text: string): string[] {
|
|
16
|
+
return text
|
|
17
|
+
.split("\n")
|
|
18
|
+
.map((line) => line.trim())
|
|
19
|
+
.filter((line) => line !== "")
|
|
20
|
+
.map((line) => (line.startsWith("/") ? line : `/${line}`));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Whether a project-relative display path lies under one of `lines`, by
|
|
24
|
+
// default the ones `init` scaffolds. Root-anchored, because a `.defignore` line
|
|
25
|
+
// is a root-relative path: `assets/node_modules/tiles.atlas` is a resource
|
|
26
|
+
// Defold loads.
|
|
27
|
+
export function isDefignoredPath(
|
|
28
|
+
displayPath: string,
|
|
29
|
+
lines: readonly string[] = SCAFFOLDED_DEFIGNORE_LINES,
|
|
30
|
+
): boolean {
|
|
16
31
|
const rooted = `/${displayPath.replace(/^\/+/, "")}`;
|
|
17
|
-
return
|
|
18
|
-
(line) => rooted === line || rooted.startsWith(`${line}/`),
|
|
19
|
-
);
|
|
32
|
+
return lines.some((line) => rooted === line || rooted.startsWith(`${line}/`));
|
|
20
33
|
}
|
package/src/session.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as ts from "typescript";
|
|
5
5
|
import * as tstl from "typescript-to-lua";
|
|
6
|
-
import { arrayIndexOptionLoweringPlugin } from "./array-index-option-lowering";
|
|
7
6
|
import { type CompanionEmitter, createCompanionEmitPlugin } from "./companion-emit";
|
|
8
7
|
import { editorScriptErasurePlugin } from "./editor-script-erasure";
|
|
9
8
|
import { lifecycleErasurePlugin } from "./lifecycle-erasure";
|
|
@@ -11,6 +10,7 @@ import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
|
|
|
11
10
|
import { messageGuardLoweringPlugin } from "./message-guard-lowering";
|
|
12
11
|
import { timersLoweringPlugin } from "./timers-lowering";
|
|
13
12
|
import { AMBIENT_FILES, collectOutputs, type TranspileProjectResult } from "./transpile";
|
|
13
|
+
import { typeApplicationErasurePlugin } from "./type-application-erasure";
|
|
14
14
|
import { windowEventGuardLoweringPlugin } from "./window-event-guard-lowering";
|
|
15
15
|
|
|
16
16
|
export interface TranspileSession {
|
|
@@ -44,7 +44,7 @@ function compilerOptions(companionEmit: CompanionEmitter): tstl.CompilerOptions
|
|
|
44
44
|
{ plugin: windowEventGuardLoweringPlugin },
|
|
45
45
|
{ plugin: messageDispatchLoweringPlugin },
|
|
46
46
|
{ plugin: timersLoweringPlugin },
|
|
47
|
-
{ plugin:
|
|
47
|
+
{ plugin: typeApplicationErasurePlugin },
|
|
48
48
|
{ plugin: companionEmit.plugin },
|
|
49
49
|
],
|
|
50
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
|
@@ -3,7 +3,6 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type * as ts from "typescript";
|
|
5
5
|
import * as tstl from "typescript-to-lua";
|
|
6
|
-
import { arrayIndexOptionLoweringPlugin } from "./array-index-option-lowering";
|
|
7
6
|
import { createCompanionEmitPlugin } from "./companion-emit";
|
|
8
7
|
import { editorScriptErasurePlugin } from "./editor-script-erasure";
|
|
9
8
|
import {
|
|
@@ -15,6 +14,7 @@ import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
|
|
|
15
14
|
import { messageGuardLoweringPlugin } from "./message-guard-lowering";
|
|
16
15
|
import { importsTimersModule, timersLoweringPlugin } from "./timers-lowering";
|
|
17
16
|
import { TIMERS_RUNTIME } from "./timers-runtime";
|
|
17
|
+
import { typeApplicationErasurePlugin } from "./type-application-erasure";
|
|
18
18
|
import { windowEventGuardLoweringPlugin } from "./window-event-guard-lowering";
|
|
19
19
|
|
|
20
20
|
export interface TranspileResult {
|
|
@@ -322,7 +322,7 @@ export function transpileProject(input: TranspileProjectInput): TranspileProject
|
|
|
322
322
|
{ plugin: windowEventGuardLoweringPlugin },
|
|
323
323
|
{ plugin: messageDispatchLoweringPlugin },
|
|
324
324
|
{ plugin: timersLoweringPlugin },
|
|
325
|
-
{ plugin:
|
|
325
|
+
{ plugin: typeApplicationErasurePlugin },
|
|
326
326
|
{ plugin: companionEmit.plugin },
|
|
327
327
|
],
|
|
328
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
|
+
};
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { type Plugin } from "typescript-to-lua";
|
|
2
|
-
export declare const ONE_BASED_INDEX_OPTION_APIS: ReadonlySet<string>;
|
|
3
|
-
export declare const INDEX_OPTION_VARIABLE_MESSAGE = "`options.index` is zero-based and is converted to Defold's 1-based index only in an object literal written at the call; pass the options inline.";
|
|
4
|
-
export declare const INDEX_OPTION_SPREAD_MESSAGE = "A spread carrying `index` cannot be converted to Defold's 1-based index; write `index` explicitly after the spread.";
|
|
5
|
-
export declare const INDEX_OPTION_UNDEFINED_MESSAGE = "`options.index` may be undefined here, so it cannot be converted to Defold's 1-based index; pass a number, or leave `index` out.";
|
|
6
|
-
export declare const arrayIndexOptionLoweringPlugin: Plugin;
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import * as ts from "typescript";
|
|
2
|
-
import {
|
|
3
|
-
createBinaryExpression,
|
|
4
|
-
createNumericLiteral,
|
|
5
|
-
type Expression,
|
|
6
|
-
getOriginalPos,
|
|
7
|
-
isCallExpression,
|
|
8
|
-
isNumericLiteral,
|
|
9
|
-
isStringLiteral,
|
|
10
|
-
isTableExpression,
|
|
11
|
-
type Plugin,
|
|
12
|
-
SyntaxKind,
|
|
13
|
-
setNodePosition,
|
|
14
|
-
type TableExpression,
|
|
15
|
-
} from "typescript-to-lua";
|
|
16
|
-
|
|
17
|
-
// The Defold functions whose `options.index` upstream documents as "index into
|
|
18
|
-
// array property (1 based)". TypeScript authors it zero-based; the lowering
|
|
19
|
-
// adds the one Lua expects.
|
|
20
|
-
export const ONE_BASED_INDEX_OPTION_APIS: ReadonlySet<string> = new Set([
|
|
21
|
-
"go.get",
|
|
22
|
-
"go.set",
|
|
23
|
-
"gui.get",
|
|
24
|
-
"gui.set",
|
|
25
|
-
]);
|
|
26
|
-
|
|
27
|
-
export const INDEX_OPTION_VARIABLE_MESSAGE =
|
|
28
|
-
"`options.index` is zero-based and is converted to Defold's 1-based index only in an object literal written at the call; pass the options inline.";
|
|
29
|
-
export const INDEX_OPTION_SPREAD_MESSAGE =
|
|
30
|
-
"A spread carrying `index` cannot be converted to Defold's 1-based index; write `index` explicitly after the spread.";
|
|
31
|
-
export const INDEX_OPTION_UNDEFINED_MESSAGE =
|
|
32
|
-
"`options.index` may be undefined here, so it cannot be converted to Defold's 1-based index; pass a number, or leave `index` out.";
|
|
33
|
-
|
|
34
|
-
const OPTIONS_PARAMETER = "options";
|
|
35
|
-
const INDEX_KEY = "index";
|
|
36
|
-
|
|
37
|
-
const loweredLiterals = new WeakSet<ts.Node>();
|
|
38
|
-
|
|
39
|
-
// Keyed like `tableKey` in url-address-slots.ts: only a symbol declared in an
|
|
40
|
-
// ambient `declare global` block carries the `global.` prefix, so a project's
|
|
41
|
-
// own `go` or `gui` never matches. Walking up from the signature declaration
|
|
42
|
-
// reaches `go.get<P>()` from the function type its curried inner call resolves to.
|
|
43
|
-
function oneBasedApi(declaration: ts.Declaration, checker: ts.TypeChecker): boolean {
|
|
44
|
-
let node: ts.Node | undefined = declaration;
|
|
45
|
-
while (node !== undefined && !ts.isFunctionDeclaration(node)) node = node.parent;
|
|
46
|
-
if (node?.name === undefined) return false;
|
|
47
|
-
const symbol = checker.getSymbolAtLocation(node.name);
|
|
48
|
-
if (symbol === undefined) return false;
|
|
49
|
-
const fqn = checker.getFullyQualifiedName(symbol);
|
|
50
|
-
return fqn.startsWith("global.") && ONE_BASED_INDEX_OPTION_APIS.has(fqn.slice("global.".length));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function optionsArgument(
|
|
54
|
-
call: ts.CallExpression,
|
|
55
|
-
checker: ts.TypeChecker,
|
|
56
|
-
): ts.Expression | undefined {
|
|
57
|
-
const declaration = checker.getResolvedSignature(call)?.declaration;
|
|
58
|
-
if (declaration === undefined || !ts.isFunctionLike(declaration)) return undefined;
|
|
59
|
-
if (!oneBasedApi(declaration, checker)) return undefined;
|
|
60
|
-
const index = declaration.parameters.findIndex(
|
|
61
|
-
(parameter) =>
|
|
62
|
-
parameter.dotDotDotToken === undefined &&
|
|
63
|
-
ts.isIdentifier(parameter.name) &&
|
|
64
|
-
parameter.name.text === OPTIONS_PARAMETER,
|
|
65
|
-
);
|
|
66
|
-
return index === -1 ? undefined : call.arguments[index];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function unwrap(expression: ts.Expression): ts.Expression {
|
|
70
|
-
let current = expression;
|
|
71
|
-
while (
|
|
72
|
-
ts.isParenthesizedExpression(current) ||
|
|
73
|
-
ts.isAsExpression(current) ||
|
|
74
|
-
ts.isSatisfiesExpression(current) ||
|
|
75
|
-
ts.isNonNullExpression(current) ||
|
|
76
|
-
ts.isTypeAssertionExpression(current)
|
|
77
|
-
) {
|
|
78
|
-
current = current.expression;
|
|
79
|
-
}
|
|
80
|
-
return current;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function someConstituent(type: ts.Type, predicate: (type: ts.Type) => boolean): boolean {
|
|
84
|
-
return type.isUnion() ? type.types.some(predicate) : predicate(type);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function carriesIndex(type: ts.Type): boolean {
|
|
88
|
-
return someConstituent(type, (t) => t.getProperty(INDEX_KEY) !== undefined);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function admitsUndefined(type: ts.Type): boolean {
|
|
92
|
-
return someConstituent(
|
|
93
|
-
type,
|
|
94
|
-
(t) => (t.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0,
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function propertyName(
|
|
99
|
-
property: ts.ObjectLiteralElementLike,
|
|
100
|
-
checker: ts.TypeChecker,
|
|
101
|
-
): string | undefined {
|
|
102
|
-
const name = property.name;
|
|
103
|
-
if (name === undefined) return undefined;
|
|
104
|
-
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
105
|
-
return name.text;
|
|
106
|
-
}
|
|
107
|
-
if (ts.isComputedPropertyName(name)) {
|
|
108
|
-
const type = checker.getTypeAtLocation(name.expression);
|
|
109
|
-
if (type.isStringLiteral()) return type.value;
|
|
110
|
-
}
|
|
111
|
-
return undefined;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function indexValueType(
|
|
115
|
-
property: ts.ObjectLiteralElementLike,
|
|
116
|
-
checker: ts.TypeChecker,
|
|
117
|
-
): ts.Type | undefined {
|
|
118
|
-
if (ts.isPropertyAssignment(property)) return checker.getTypeAtLocation(property.initializer);
|
|
119
|
-
if (ts.isShorthandPropertyAssignment(property)) return checker.getTypeAtLocation(property.name);
|
|
120
|
-
return undefined;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function rejection(node: ts.Node, messageText: string): ts.Diagnostic {
|
|
124
|
-
const file = node.getSourceFile();
|
|
125
|
-
const start = node.getStart(file);
|
|
126
|
-
return {
|
|
127
|
-
file,
|
|
128
|
-
start,
|
|
129
|
-
length: node.getEnd() - start,
|
|
130
|
-
category: ts.DiagnosticCategory.Error,
|
|
131
|
-
code: 0,
|
|
132
|
-
messageText,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// The first reason `options` cannot be lowered, or `undefined` when it can.
|
|
137
|
-
function rejectOptions(options: ts.Expression, checker: ts.TypeChecker): ts.Diagnostic | undefined {
|
|
138
|
-
const literal = unwrap(options);
|
|
139
|
-
if (!ts.isObjectLiteralExpression(literal)) {
|
|
140
|
-
return carriesIndex(checker.getTypeAtLocation(literal))
|
|
141
|
-
? rejection(options, INDEX_OPTION_VARIABLE_MESSAGE)
|
|
142
|
-
: undefined;
|
|
143
|
-
}
|
|
144
|
-
let uncoveredSpread: ts.SpreadAssignment | undefined;
|
|
145
|
-
let explicitIndex: ts.ObjectLiteralElementLike | undefined;
|
|
146
|
-
for (const property of literal.properties) {
|
|
147
|
-
if (ts.isSpreadAssignment(property)) {
|
|
148
|
-
if (carriesIndex(checker.getTypeAtLocation(property.expression))) uncoveredSpread = property;
|
|
149
|
-
} else if (propertyName(property, checker) === INDEX_KEY) {
|
|
150
|
-
uncoveredSpread = undefined;
|
|
151
|
-
explicitIndex = property;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
if (uncoveredSpread !== undefined) return rejection(uncoveredSpread, INDEX_OPTION_SPREAD_MESSAGE);
|
|
155
|
-
const valueType = explicitIndex && indexValueType(explicitIndex, checker);
|
|
156
|
-
if (explicitIndex !== undefined && valueType !== undefined && admitsUndefined(valueType)) {
|
|
157
|
-
return rejection(explicitIndex, INDEX_OPTION_UNDEFINED_MESSAGE);
|
|
158
|
-
}
|
|
159
|
-
return undefined;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function plusOne(value: Expression): Expression {
|
|
163
|
-
if (isNumericLiteral(value)) {
|
|
164
|
-
return setNodePosition(createNumericLiteral(value.value + 1), getOriginalPos(value));
|
|
165
|
-
}
|
|
166
|
-
return setNodePosition(
|
|
167
|
-
createBinaryExpression(value, createNumericLiteral(1), SyntaxKind.AdditionOperator),
|
|
168
|
-
getOriginalPos(value),
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// `rejectOptions` guarantees the last `index` field is the effective one: a
|
|
173
|
-
// spread carrying `index` after it is rejected. Every earlier one is shadowed
|
|
174
|
-
// and keeps its source value.
|
|
175
|
-
function offsetEffectiveIndexField(tables: readonly TableExpression[]): void {
|
|
176
|
-
for (const field of tables.flatMap((table) => table.fields).reverse()) {
|
|
177
|
-
if (field.key !== undefined && isStringLiteral(field.key) && field.key.value === INDEX_KEY) {
|
|
178
|
-
field.value = plusOne(field.value);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export const arrayIndexOptionLoweringPlugin: Plugin = {
|
|
185
|
-
visitors: {
|
|
186
|
-
[ts.SyntaxKind.CallExpression]: (node, context): Expression => {
|
|
187
|
-
const options = optionsArgument(node, context.checker);
|
|
188
|
-
if (options !== undefined) {
|
|
189
|
-
const diagnostic = rejectOptions(options, context.checker);
|
|
190
|
-
if (diagnostic !== undefined) {
|
|
191
|
-
context.diagnostics.push(diagnostic);
|
|
192
|
-
} else {
|
|
193
|
-
const literal = unwrap(options);
|
|
194
|
-
if (ts.isObjectLiteralExpression(literal)) loweredLiterals.add(literal);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return context.superTransformExpression(node);
|
|
198
|
-
},
|
|
199
|
-
// A spread lowers the literal to `__TS__ObjectAssign({}, base, {index = …})`,
|
|
200
|
-
// so the table carrying `index` can sit one call deep.
|
|
201
|
-
[ts.SyntaxKind.ObjectLiteralExpression]: (node, context): Expression => {
|
|
202
|
-
const result = context.superTransformExpression(node);
|
|
203
|
-
if (!loweredLiterals.has(node)) return result;
|
|
204
|
-
if (isTableExpression(result)) {
|
|
205
|
-
offsetEffectiveIndexField([result]);
|
|
206
|
-
} else if (isCallExpression(result)) {
|
|
207
|
-
offsetEffectiveIndexField(result.params.filter(isTableExpression));
|
|
208
|
-
}
|
|
209
|
-
return result;
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
};
|