@defold-typescript/transpiler 0.40.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 +2 -0
- package/dist/index.js +177 -233
- package/dist/lifecycle-erasure.d.ts +7 -0
- 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 +2 -0
- package/src/lifecycle-erasure.ts +51 -22
- package/src/program-diagnostics.ts +2 -2
- 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
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
|
|
273
|
+
function propertyMembers(property) {
|
|
274
274
|
if (!ts2.isPropertyAssignment(property) || !ts2.isObjectLiteralExpression(property.initializer)) {
|
|
275
275
|
return [];
|
|
276
276
|
}
|
|
277
|
-
const
|
|
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
|
-
|
|
286
|
+
members.push({ name: key.text, initializer: member.initializer });
|
|
287
287
|
}
|
|
288
|
-
return
|
|
288
|
+
return members;
|
|
289
289
|
}
|
|
290
|
-
function
|
|
290
|
+
function factoryHooks(expression, checker) {
|
|
291
291
|
if (!ts2.isCallExpression(expression)) {
|
|
292
292
|
return;
|
|
293
293
|
}
|
|
294
|
-
if (!resolvesToFactoryExport(expression.expression,
|
|
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) {
|
|
@@ -2061,176 +2074,13 @@ function computeOutputRel(rel, config, kind) {
|
|
|
2061
2074
|
import * as ts9 from "typescript";
|
|
2062
2075
|
import * as tstl from "typescript-to-lua";
|
|
2063
2076
|
|
|
2064
|
-
// src/
|
|
2077
|
+
// src/message-guard-lowering.ts
|
|
2065
2078
|
import * as ts5 from "typescript";
|
|
2066
2079
|
import {
|
|
2067
2080
|
createBinaryExpression as createBinaryExpression3,
|
|
2068
|
-
createNumericLiteral,
|
|
2069
|
-
getOriginalPos,
|
|
2070
|
-
isCallExpression as isCallExpression3,
|
|
2071
|
-
isNumericLiteral as isNumericLiteral3,
|
|
2072
|
-
isStringLiteral as isStringLiteral5,
|
|
2073
|
-
isTableExpression,
|
|
2074
|
-
SyntaxKind as SyntaxKind7,
|
|
2075
|
-
setNodePosition
|
|
2076
|
-
} from "typescript-to-lua";
|
|
2077
|
-
var ONE_BASED_INDEX_OPTION_APIS = new Set([
|
|
2078
|
-
"go.get",
|
|
2079
|
-
"go.set",
|
|
2080
|
-
"gui.get",
|
|
2081
|
-
"gui.set"
|
|
2082
|
-
]);
|
|
2083
|
-
var 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.";
|
|
2084
|
-
var INDEX_OPTION_SPREAD_MESSAGE = "A spread carrying `index` cannot be converted to Defold's 1-based index; write `index` explicitly after the spread.";
|
|
2085
|
-
var 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.";
|
|
2086
|
-
var OPTIONS_PARAMETER = "options";
|
|
2087
|
-
var INDEX_KEY = "index";
|
|
2088
|
-
var loweredLiterals = new WeakSet;
|
|
2089
|
-
function oneBasedApi(declaration, checker) {
|
|
2090
|
-
let node = declaration;
|
|
2091
|
-
while (node !== undefined && !ts5.isFunctionDeclaration(node))
|
|
2092
|
-
node = node.parent;
|
|
2093
|
-
if (node?.name === undefined)
|
|
2094
|
-
return false;
|
|
2095
|
-
const symbol = checker.getSymbolAtLocation(node.name);
|
|
2096
|
-
if (symbol === undefined)
|
|
2097
|
-
return false;
|
|
2098
|
-
const fqn = checker.getFullyQualifiedName(symbol);
|
|
2099
|
-
return fqn.startsWith("global.") && ONE_BASED_INDEX_OPTION_APIS.has(fqn.slice("global.".length));
|
|
2100
|
-
}
|
|
2101
|
-
function optionsArgument(call, checker) {
|
|
2102
|
-
const declaration = checker.getResolvedSignature(call)?.declaration;
|
|
2103
|
-
if (declaration === undefined || !ts5.isFunctionLike(declaration))
|
|
2104
|
-
return;
|
|
2105
|
-
if (!oneBasedApi(declaration, checker))
|
|
2106
|
-
return;
|
|
2107
|
-
const index = declaration.parameters.findIndex((parameter) => parameter.dotDotDotToken === undefined && ts5.isIdentifier(parameter.name) && parameter.name.text === OPTIONS_PARAMETER);
|
|
2108
|
-
return index === -1 ? undefined : call.arguments[index];
|
|
2109
|
-
}
|
|
2110
|
-
function unwrap(expression) {
|
|
2111
|
-
let current = expression;
|
|
2112
|
-
while (ts5.isParenthesizedExpression(current) || ts5.isAsExpression(current) || ts5.isSatisfiesExpression(current) || ts5.isNonNullExpression(current) || ts5.isTypeAssertionExpression(current)) {
|
|
2113
|
-
current = current.expression;
|
|
2114
|
-
}
|
|
2115
|
-
return current;
|
|
2116
|
-
}
|
|
2117
|
-
function someConstituent(type, predicate) {
|
|
2118
|
-
return type.isUnion() ? type.types.some(predicate) : predicate(type);
|
|
2119
|
-
}
|
|
2120
|
-
function carriesIndex(type) {
|
|
2121
|
-
return someConstituent(type, (t) => t.getProperty(INDEX_KEY) !== undefined);
|
|
2122
|
-
}
|
|
2123
|
-
function admitsUndefined(type) {
|
|
2124
|
-
return someConstituent(type, (t) => (t.flags & (ts5.TypeFlags.Undefined | ts5.TypeFlags.Void)) !== 0);
|
|
2125
|
-
}
|
|
2126
|
-
function propertyName(property, checker) {
|
|
2127
|
-
const name = property.name;
|
|
2128
|
-
if (name === undefined)
|
|
2129
|
-
return;
|
|
2130
|
-
if (ts5.isIdentifier(name) || ts5.isStringLiteral(name) || ts5.isNumericLiteral(name)) {
|
|
2131
|
-
return name.text;
|
|
2132
|
-
}
|
|
2133
|
-
if (ts5.isComputedPropertyName(name)) {
|
|
2134
|
-
const type = checker.getTypeAtLocation(name.expression);
|
|
2135
|
-
if (type.isStringLiteral())
|
|
2136
|
-
return type.value;
|
|
2137
|
-
}
|
|
2138
|
-
return;
|
|
2139
|
-
}
|
|
2140
|
-
function indexValueType(property, checker) {
|
|
2141
|
-
if (ts5.isPropertyAssignment(property))
|
|
2142
|
-
return checker.getTypeAtLocation(property.initializer);
|
|
2143
|
-
if (ts5.isShorthandPropertyAssignment(property))
|
|
2144
|
-
return checker.getTypeAtLocation(property.name);
|
|
2145
|
-
return;
|
|
2146
|
-
}
|
|
2147
|
-
function rejection(node, messageText) {
|
|
2148
|
-
const file = node.getSourceFile();
|
|
2149
|
-
const start = node.getStart(file);
|
|
2150
|
-
return {
|
|
2151
|
-
file,
|
|
2152
|
-
start,
|
|
2153
|
-
length: node.getEnd() - start,
|
|
2154
|
-
category: ts5.DiagnosticCategory.Error,
|
|
2155
|
-
code: 0,
|
|
2156
|
-
messageText
|
|
2157
|
-
};
|
|
2158
|
-
}
|
|
2159
|
-
function rejectOptions(options, checker) {
|
|
2160
|
-
const literal = unwrap(options);
|
|
2161
|
-
if (!ts5.isObjectLiteralExpression(literal)) {
|
|
2162
|
-
return carriesIndex(checker.getTypeAtLocation(literal)) ? rejection(options, INDEX_OPTION_VARIABLE_MESSAGE) : undefined;
|
|
2163
|
-
}
|
|
2164
|
-
let uncoveredSpread;
|
|
2165
|
-
let explicitIndex;
|
|
2166
|
-
for (const property of literal.properties) {
|
|
2167
|
-
if (ts5.isSpreadAssignment(property)) {
|
|
2168
|
-
if (carriesIndex(checker.getTypeAtLocation(property.expression)))
|
|
2169
|
-
uncoveredSpread = property;
|
|
2170
|
-
} else if (propertyName(property, checker) === INDEX_KEY) {
|
|
2171
|
-
uncoveredSpread = undefined;
|
|
2172
|
-
explicitIndex = property;
|
|
2173
|
-
}
|
|
2174
|
-
}
|
|
2175
|
-
if (uncoveredSpread !== undefined)
|
|
2176
|
-
return rejection(uncoveredSpread, INDEX_OPTION_SPREAD_MESSAGE);
|
|
2177
|
-
const valueType = explicitIndex && indexValueType(explicitIndex, checker);
|
|
2178
|
-
if (explicitIndex !== undefined && valueType !== undefined && admitsUndefined(valueType)) {
|
|
2179
|
-
return rejection(explicitIndex, INDEX_OPTION_UNDEFINED_MESSAGE);
|
|
2180
|
-
}
|
|
2181
|
-
return;
|
|
2182
|
-
}
|
|
2183
|
-
function plusOne(value) {
|
|
2184
|
-
if (isNumericLiteral3(value)) {
|
|
2185
|
-
return setNodePosition(createNumericLiteral(value.value + 1), getOriginalPos(value));
|
|
2186
|
-
}
|
|
2187
|
-
return setNodePosition(createBinaryExpression3(value, createNumericLiteral(1), SyntaxKind7.AdditionOperator), getOriginalPos(value));
|
|
2188
|
-
}
|
|
2189
|
-
function offsetEffectiveIndexField(tables) {
|
|
2190
|
-
for (const field of tables.flatMap((table) => table.fields).reverse()) {
|
|
2191
|
-
if (field.key !== undefined && isStringLiteral5(field.key) && field.key.value === INDEX_KEY) {
|
|
2192
|
-
field.value = plusOne(field.value);
|
|
2193
|
-
return;
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
}
|
|
2197
|
-
var arrayIndexOptionLoweringPlugin = {
|
|
2198
|
-
visitors: {
|
|
2199
|
-
[ts5.SyntaxKind.CallExpression]: (node, context) => {
|
|
2200
|
-
const options = optionsArgument(node, context.checker);
|
|
2201
|
-
if (options !== undefined) {
|
|
2202
|
-
const diagnostic = rejectOptions(options, context.checker);
|
|
2203
|
-
if (diagnostic !== undefined) {
|
|
2204
|
-
context.diagnostics.push(diagnostic);
|
|
2205
|
-
} else {
|
|
2206
|
-
const literal = unwrap(options);
|
|
2207
|
-
if (ts5.isObjectLiteralExpression(literal))
|
|
2208
|
-
loweredLiterals.add(literal);
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
return context.superTransformExpression(node);
|
|
2212
|
-
},
|
|
2213
|
-
[ts5.SyntaxKind.ObjectLiteralExpression]: (node, context) => {
|
|
2214
|
-
const result = context.superTransformExpression(node);
|
|
2215
|
-
if (!loweredLiterals.has(node))
|
|
2216
|
-
return result;
|
|
2217
|
-
if (isTableExpression(result)) {
|
|
2218
|
-
offsetEffectiveIndexField([result]);
|
|
2219
|
-
} else if (isCallExpression3(result)) {
|
|
2220
|
-
offsetEffectiveIndexField(result.params.filter(isTableExpression));
|
|
2221
|
-
}
|
|
2222
|
-
return result;
|
|
2223
|
-
}
|
|
2224
|
-
}
|
|
2225
|
-
};
|
|
2226
|
-
|
|
2227
|
-
// src/message-guard-lowering.ts
|
|
2228
|
-
import * as ts6 from "typescript";
|
|
2229
|
-
import {
|
|
2230
|
-
createBinaryExpression as createBinaryExpression4,
|
|
2231
2081
|
createCallExpression as createCallExpression3,
|
|
2232
2082
|
createIdentifier as createIdentifier3,
|
|
2233
|
-
SyntaxKind as
|
|
2083
|
+
SyntaxKind as SyntaxKind7
|
|
2234
2084
|
} from "typescript-to-lua";
|
|
2235
2085
|
var GUARD_MODULE = "@defold-typescript/types";
|
|
2236
2086
|
var GUARD_NAME = "isMessage";
|
|
@@ -2239,7 +2089,7 @@ function resolvesToGuardExport(callee, checker) {
|
|
|
2239
2089
|
if (symbol === undefined) {
|
|
2240
2090
|
return false;
|
|
2241
2091
|
}
|
|
2242
|
-
if (symbol.flags &
|
|
2092
|
+
if (symbol.flags & ts5.SymbolFlags.Alias) {
|
|
2243
2093
|
symbol = checker.getAliasedSymbol(symbol);
|
|
2244
2094
|
}
|
|
2245
2095
|
if (symbol.getName() !== GUARD_NAME) {
|
|
@@ -2253,12 +2103,12 @@ function resolvesToGuardExport(callee, checker) {
|
|
|
2253
2103
|
}
|
|
2254
2104
|
var messageGuardLoweringPlugin = {
|
|
2255
2105
|
visitors: {
|
|
2256
|
-
[
|
|
2106
|
+
[ts5.SyntaxKind.CallExpression]: (node, context) => {
|
|
2257
2107
|
const [messageIdArg, , expectedArg] = node.arguments;
|
|
2258
2108
|
if (node.arguments.length === 3 && messageIdArg !== undefined && expectedArg !== undefined && resolvesToGuardExport(node.expression, context.checker)) {
|
|
2259
2109
|
const messageId = context.transformExpression(messageIdArg);
|
|
2260
2110
|
const expected = context.transformExpression(expectedArg);
|
|
2261
|
-
return
|
|
2111
|
+
return createBinaryExpression3(messageId, createCallExpression3(createIdentifier3("hash"), [expected]), SyntaxKind7.EqualityOperator, node);
|
|
2262
2112
|
}
|
|
2263
2113
|
return context.superTransformExpression(node);
|
|
2264
2114
|
}
|
|
@@ -2266,34 +2116,64 @@ var messageGuardLoweringPlugin = {
|
|
|
2266
2116
|
};
|
|
2267
2117
|
|
|
2268
2118
|
// src/timers-lowering.ts
|
|
2269
|
-
import * as
|
|
2119
|
+
import * as ts6 from "typescript";
|
|
2270
2120
|
import { LuaLibFeature } from "typescript-to-lua";
|
|
2271
2121
|
function isTimersSpecifier(node) {
|
|
2272
|
-
return
|
|
2122
|
+
return ts6.isStringLiteral(node) && node.text === TIMERS_MODULE_SPECIFIER;
|
|
2273
2123
|
}
|
|
2274
2124
|
function importsTimersModule(sourceFile) {
|
|
2275
|
-
return sourceFile.statements.some((statement) =>
|
|
2125
|
+
return sourceFile.statements.some((statement) => ts6.isImportDeclaration(statement) && isTimersSpecifier(statement.moduleSpecifier));
|
|
2276
2126
|
}
|
|
2277
2127
|
var TIMERS_REQUIRE_PATH = `@NoResolution:${TIMERS_REQUIRE_NAME}`;
|
|
2278
2128
|
var timersLoweringPlugin = {
|
|
2279
2129
|
visitors: {
|
|
2280
|
-
[
|
|
2130
|
+
[ts6.SyntaxKind.ImportDeclaration]: (node, context) => {
|
|
2281
2131
|
if (!isTimersSpecifier(node.moduleSpecifier)) {
|
|
2282
2132
|
return context.superTransformStatements(node);
|
|
2283
2133
|
}
|
|
2284
2134
|
context.usedLuaLibFeatures.add(LuaLibFeature.Promise);
|
|
2285
2135
|
context.usedLuaLibFeatures.add(LuaLibFeature.New);
|
|
2286
|
-
const rewritten =
|
|
2136
|
+
const rewritten = ts6.factory.updateImportDeclaration(node, node.modifiers, node.importClause, ts6.factory.createStringLiteral(TIMERS_REQUIRE_PATH), node.attributes);
|
|
2287
2137
|
return context.superTransformStatements(rewritten);
|
|
2288
2138
|
}
|
|
2289
2139
|
}
|
|
2290
2140
|
};
|
|
2291
2141
|
|
|
2142
|
+
// src/type-application-erasure.ts
|
|
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
|
+
|
|
2292
2172
|
// src/window-event-guard-lowering.ts
|
|
2293
2173
|
import * as ts8 from "typescript";
|
|
2294
2174
|
import {
|
|
2295
|
-
createBinaryExpression as
|
|
2296
|
-
SyntaxKind as
|
|
2175
|
+
createBinaryExpression as createBinaryExpression4,
|
|
2176
|
+
SyntaxKind as SyntaxKind11
|
|
2297
2177
|
} from "typescript-to-lua";
|
|
2298
2178
|
var GUARD_MODULE2 = "@defold-typescript/types";
|
|
2299
2179
|
var GUARD_NAME2 = "isWindowEvent";
|
|
@@ -2321,7 +2201,7 @@ var windowEventGuardLoweringPlugin = {
|
|
|
2321
2201
|
if (node.arguments.length === 3 && eventArg !== undefined && expectedArg !== undefined && resolvesToGuardExport2(node.expression, context.checker)) {
|
|
2322
2202
|
const event = context.transformExpression(eventArg);
|
|
2323
2203
|
const expected = context.transformExpression(expectedArg);
|
|
2324
|
-
return
|
|
2204
|
+
return createBinaryExpression4(event, expected, SyntaxKind11.EqualityOperator, node);
|
|
2325
2205
|
}
|
|
2326
2206
|
return context.superTransformExpression(node);
|
|
2327
2207
|
}
|
|
@@ -2335,7 +2215,7 @@ var LUA_PLUGINS = [
|
|
|
2335
2215
|
windowEventGuardLoweringPlugin,
|
|
2336
2216
|
messageDispatchLoweringPlugin,
|
|
2337
2217
|
timersLoweringPlugin,
|
|
2338
|
-
|
|
2218
|
+
typeApplicationErasurePlugin
|
|
2339
2219
|
];
|
|
2340
2220
|
var noopWriteFile = () => {};
|
|
2341
2221
|
function isAmbient(fileName) {
|
|
@@ -2930,18 +2810,18 @@ import {
|
|
|
2930
2810
|
createTableIndexExpression as createTableIndexExpression2,
|
|
2931
2811
|
createVariableDeclarationStatement as createVariableDeclarationStatement3,
|
|
2932
2812
|
isBlock as isBlock3,
|
|
2933
|
-
isCallExpression as
|
|
2813
|
+
isCallExpression as isCallExpression4,
|
|
2934
2814
|
isDoStatement,
|
|
2935
2815
|
isForInStatement as isForInStatement2,
|
|
2936
2816
|
isForStatement,
|
|
2937
2817
|
isFunctionDefinition,
|
|
2938
2818
|
isFunctionExpression as isFunctionExpression4,
|
|
2939
|
-
isIdentifier as
|
|
2819
|
+
isIdentifier as isIdentifier4,
|
|
2940
2820
|
isMethodCallExpression,
|
|
2941
2821
|
isRepeatStatement,
|
|
2942
2822
|
isReturnStatement as isReturnStatement2,
|
|
2943
|
-
isStringLiteral as
|
|
2944
|
-
isTableExpression
|
|
2823
|
+
isStringLiteral as isStringLiteral6,
|
|
2824
|
+
isTableExpression,
|
|
2945
2825
|
isVariableDeclarationStatement,
|
|
2946
2826
|
LuaPrinter
|
|
2947
2827
|
} from "typescript-to-lua";
|
|
@@ -3013,7 +2893,7 @@ function freeNames(statements) {
|
|
|
3013
2893
|
if (node === undefined) {
|
|
3014
2894
|
return;
|
|
3015
2895
|
}
|
|
3016
|
-
if (
|
|
2896
|
+
if (isIdentifier4(node)) {
|
|
3017
2897
|
if (!isBound(node.text)) {
|
|
3018
2898
|
free.add(node.text);
|
|
3019
2899
|
}
|
|
@@ -3119,14 +2999,14 @@ function resolvedRequirePaths(sourceFile, checker) {
|
|
|
3119
2999
|
function resolveMovedRequires(statements, paths) {
|
|
3120
3000
|
for (const statement of statements) {
|
|
3121
3001
|
forEachLuaNode(statement, (node) => {
|
|
3122
|
-
if (!
|
|
3002
|
+
if (!isCallExpression4(node) || !isIdentifier4(node.expression)) {
|
|
3123
3003
|
return;
|
|
3124
3004
|
}
|
|
3125
3005
|
if (node.expression.text !== "require" || node.params.length !== 1) {
|
|
3126
3006
|
return;
|
|
3127
3007
|
}
|
|
3128
3008
|
const [argument] = node.params;
|
|
3129
|
-
if (argument === undefined || !
|
|
3009
|
+
if (argument === undefined || !isStringLiteral6(argument)) {
|
|
3130
3010
|
return;
|
|
3131
3011
|
}
|
|
3132
3012
|
const resolved = paths.get(argument.value);
|
|
@@ -3142,14 +3022,14 @@ function withResolvedRequire(statement, paths) {
|
|
|
3142
3022
|
}
|
|
3143
3023
|
const name = statement.left[0];
|
|
3144
3024
|
const [value] = statement.right ?? [];
|
|
3145
|
-
if (name === undefined || value === undefined || !
|
|
3025
|
+
if (name === undefined || value === undefined || !isCallExpression4(value)) {
|
|
3146
3026
|
return statement;
|
|
3147
3027
|
}
|
|
3148
|
-
if (!
|
|
3028
|
+
if (!isIdentifier4(value.expression) || value.expression.text !== "require") {
|
|
3149
3029
|
return statement;
|
|
3150
3030
|
}
|
|
3151
3031
|
const [argument] = value.params;
|
|
3152
|
-
if (argument === undefined || !
|
|
3032
|
+
if (argument === undefined || !isStringLiteral6(argument)) {
|
|
3153
3033
|
return statement;
|
|
3154
3034
|
}
|
|
3155
3035
|
const resolved = paths.get(argument.value);
|
|
@@ -3185,7 +3065,7 @@ function copiedPrelude(halves, importBindings) {
|
|
|
3185
3065
|
function namesIdentifier(statement, names) {
|
|
3186
3066
|
let found = false;
|
|
3187
3067
|
forEachLuaNode(statement, (node) => {
|
|
3188
|
-
if (
|
|
3068
|
+
if (isIdentifier4(node) && names.has(node.text)) {
|
|
3189
3069
|
found = true;
|
|
3190
3070
|
}
|
|
3191
3071
|
});
|
|
@@ -3196,14 +3076,14 @@ function isExportsTableInit(statement) {
|
|
|
3196
3076
|
return false;
|
|
3197
3077
|
}
|
|
3198
3078
|
const [single] = statement.right ?? [];
|
|
3199
|
-
return statement.left[0]?.text === EXPORTS_NAME && single !== undefined &&
|
|
3079
|
+
return statement.left[0]?.text === EXPORTS_NAME && single !== undefined && isTableExpression(single);
|
|
3200
3080
|
}
|
|
3201
3081
|
function isExportsReturn(statement) {
|
|
3202
3082
|
if (!isReturnStatement2(statement) || statement.expressions.length !== 1) {
|
|
3203
3083
|
return false;
|
|
3204
3084
|
}
|
|
3205
3085
|
const [single] = statement.expressions;
|
|
3206
|
-
return single !== undefined &&
|
|
3086
|
+
return single !== undefined && isIdentifier4(single) && single.text === EXPORTS_NAME;
|
|
3207
3087
|
}
|
|
3208
3088
|
function isHoistedNameDeclaration(statement) {
|
|
3209
3089
|
return isVariableDeclarationStatement(statement) && statement.right === undefined;
|
|
@@ -3631,7 +3511,7 @@ function transpileProject(input) {
|
|
|
3631
3511
|
{ plugin: windowEventGuardLoweringPlugin },
|
|
3632
3512
|
{ plugin: messageDispatchLoweringPlugin },
|
|
3633
3513
|
{ plugin: timersLoweringPlugin },
|
|
3634
|
-
{ plugin:
|
|
3514
|
+
{ plugin: typeApplicationErasurePlugin },
|
|
3635
3515
|
{ plugin: companionEmit.plugin }
|
|
3636
3516
|
]
|
|
3637
3517
|
});
|
|
@@ -3662,7 +3542,7 @@ function compilerOptions(companionEmit) {
|
|
|
3662
3542
|
{ plugin: windowEventGuardLoweringPlugin },
|
|
3663
3543
|
{ plugin: messageDispatchLoweringPlugin },
|
|
3664
3544
|
{ plugin: timersLoweringPlugin },
|
|
3665
|
-
{ plugin:
|
|
3545
|
+
{ plugin: typeApplicationErasurePlugin },
|
|
3666
3546
|
{ plugin: companionEmit.plugin }
|
|
3667
3547
|
]
|
|
3668
3548
|
};
|
|
@@ -3779,8 +3659,71 @@ function createTranspileSession() {
|
|
|
3779
3659
|
getProgram: () => program
|
|
3780
3660
|
};
|
|
3781
3661
|
}
|
|
3782
|
-
// src/
|
|
3662
|
+
// src/text-script-property.ts
|
|
3783
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
|
+
}
|
|
3725
|
+
// src/url-address-slots.ts
|
|
3726
|
+
import * as ts15 from "typescript";
|
|
3784
3727
|
var ADDRESS_CLASSES = {
|
|
3785
3728
|
"game-object": true,
|
|
3786
3729
|
component: true,
|
|
@@ -3799,7 +3742,7 @@ function isFragmentCaret(slot, position) {
|
|
|
3799
3742
|
return slot.fragmentStart !== -1 && position >= slot.fragmentStart;
|
|
3800
3743
|
}
|
|
3801
3744
|
function tableKey(checker, callee) {
|
|
3802
|
-
const symbol = checker.getSymbolAtLocation(
|
|
3745
|
+
const symbol = checker.getSymbolAtLocation(ts15.isIdentifier(callee) ? callee : callee.name);
|
|
3803
3746
|
if (!symbol)
|
|
3804
3747
|
return;
|
|
3805
3748
|
const fqn = checker.getFullyQualifiedName(symbol);
|
|
@@ -3807,43 +3750,43 @@ function tableKey(checker, callee) {
|
|
|
3807
3750
|
}
|
|
3808
3751
|
function parameterName(checker, call, index) {
|
|
3809
3752
|
const declaration = checker.getResolvedSignature(call)?.declaration;
|
|
3810
|
-
if (!declaration || !
|
|
3753
|
+
if (!declaration || !ts15.isFunctionLike(declaration))
|
|
3811
3754
|
return;
|
|
3812
3755
|
const parameter = declaration.parameters[index];
|
|
3813
|
-
if (!parameter || parameter.dotDotDotToken || !
|
|
3756
|
+
if (!parameter || parameter.dotDotDotToken || !ts15.isIdentifier(parameter.name))
|
|
3814
3757
|
return;
|
|
3815
3758
|
return parameter.name.text;
|
|
3816
3759
|
}
|
|
3817
3760
|
var EQUALITY_OPERATORS = new Set([
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3761
|
+
ts15.SyntaxKind.EqualsEqualsEqualsToken,
|
|
3762
|
+
ts15.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
3763
|
+
ts15.SyntaxKind.EqualsEqualsToken,
|
|
3764
|
+
ts15.SyntaxKind.ExclamationEqualsToken
|
|
3822
3765
|
]);
|
|
3823
3766
|
function comparisonSatisfied(checker, entry, call) {
|
|
3824
3767
|
if (entry.comparedParameter === undefined)
|
|
3825
3768
|
return true;
|
|
3826
3769
|
let node = call;
|
|
3827
|
-
while (
|
|
3770
|
+
while (ts15.isParenthesizedExpression(node.parent))
|
|
3828
3771
|
node = node.parent;
|
|
3829
3772
|
const comparison = node.parent;
|
|
3830
|
-
if (!
|
|
3773
|
+
if (!ts15.isBinaryExpression(comparison))
|
|
3831
3774
|
return false;
|
|
3832
3775
|
if (!EQUALITY_OPERATORS.has(comparison.operatorToken.kind))
|
|
3833
3776
|
return false;
|
|
3834
3777
|
const other = comparison.left === node ? comparison.right : comparison.left;
|
|
3835
3778
|
const symbol = checker.getSymbolAtLocation(other);
|
|
3836
|
-
return (symbol?.declarations ?? []).some((declaration) =>
|
|
3779
|
+
return (symbol?.declarations ?? []).some((declaration) => ts15.isParameter(declaration) && ts15.isIdentifier(declaration.name) && declaration.name.text === entry.comparedParameter);
|
|
3837
3780
|
}
|
|
3838
3781
|
function classifiedEntryOfArgument(checker, table, argument) {
|
|
3839
3782
|
const call = argument.parent;
|
|
3840
|
-
if (!
|
|
3783
|
+
if (!ts15.isCallExpression(call))
|
|
3841
3784
|
return;
|
|
3842
3785
|
const argumentIndex = call.arguments.indexOf(argument);
|
|
3843
3786
|
if (argumentIndex === -1)
|
|
3844
3787
|
return;
|
|
3845
3788
|
const callee = call.expression;
|
|
3846
|
-
if (!
|
|
3789
|
+
if (!ts15.isPropertyAccessExpression(callee) && !ts15.isIdentifier(callee))
|
|
3847
3790
|
return;
|
|
3848
3791
|
const fqn = tableKey(checker, callee);
|
|
3849
3792
|
if (fqn === undefined)
|
|
@@ -3860,10 +3803,10 @@ function classifiedEntryOfArgument(checker, table, argument) {
|
|
|
3860
3803
|
}
|
|
3861
3804
|
function slotThroughHashedCall(checker, table, literal) {
|
|
3862
3805
|
const call = literal.parent;
|
|
3863
|
-
if (!
|
|
3806
|
+
if (!ts15.isCallExpression(call))
|
|
3864
3807
|
return;
|
|
3865
3808
|
const callee = call.expression;
|
|
3866
|
-
if (!
|
|
3809
|
+
if (!ts15.isPropertyAccessExpression(callee) && !ts15.isIdentifier(callee))
|
|
3867
3810
|
return;
|
|
3868
3811
|
if (tableKey(checker, callee) !== "hash")
|
|
3869
3812
|
return;
|
|
@@ -3881,24 +3824,24 @@ function addressTextOf(checker, entry, call) {
|
|
|
3881
3824
|
if (entry.addressParameter === undefined)
|
|
3882
3825
|
return;
|
|
3883
3826
|
const declaration = checker.getResolvedSignature(call)?.declaration;
|
|
3884
|
-
if (!declaration || !
|
|
3827
|
+
if (!declaration || !ts15.isFunctionLike(declaration))
|
|
3885
3828
|
return;
|
|
3886
|
-
const index = declaration.parameters.findIndex((parameter) => !parameter.dotDotDotToken &&
|
|
3829
|
+
const index = declaration.parameters.findIndex((parameter) => !parameter.dotDotDotToken && ts15.isIdentifier(parameter.name) && parameter.name.text === entry.addressParameter);
|
|
3887
3830
|
if (index === -1)
|
|
3888
3831
|
return;
|
|
3889
3832
|
const argument = call.arguments[index];
|
|
3890
|
-
return argument &&
|
|
3833
|
+
return argument && ts15.isStringLiteralLike(argument) ? argument.text : undefined;
|
|
3891
3834
|
}
|
|
3892
3835
|
function innermostLiteralAt(sourceFile, position) {
|
|
3893
3836
|
let found;
|
|
3894
3837
|
const visit = (node) => {
|
|
3895
3838
|
if (position < node.getStart(sourceFile) || position > node.getEnd())
|
|
3896
3839
|
return;
|
|
3897
|
-
if (
|
|
3840
|
+
if (ts15.isStringLiteralLike(node))
|
|
3898
3841
|
found = node;
|
|
3899
|
-
|
|
3842
|
+
ts15.forEachChild(node, visit);
|
|
3900
3843
|
};
|
|
3901
|
-
|
|
3844
|
+
ts15.forEachChild(sourceFile, visit);
|
|
3902
3845
|
return found;
|
|
3903
3846
|
}
|
|
3904
3847
|
function resolveClassifiedSlotAtPosition(input) {
|
|
@@ -3932,23 +3875,23 @@ function resolveClassifiedSlotAtPosition(input) {
|
|
|
3932
3875
|
};
|
|
3933
3876
|
}
|
|
3934
3877
|
// src/url-cross-world-address.ts
|
|
3935
|
-
import * as
|
|
3878
|
+
import * as ts17 from "typescript";
|
|
3936
3879
|
|
|
3937
3880
|
// src/url-address-literals.ts
|
|
3938
|
-
import * as
|
|
3881
|
+
import * as ts16 from "typescript";
|
|
3939
3882
|
function isAmbient2(fileName) {
|
|
3940
3883
|
return /[\\/]node_modules[\\/]/.test(fileName) || /(^|[\\/])lib\.[^\\/]*\.d\.ts$/.test(fileName);
|
|
3941
3884
|
}
|
|
3942
3885
|
function referenceTargetSymbol(type) {
|
|
3943
|
-
if ((type.flags &
|
|
3886
|
+
if ((type.flags & ts16.TypeFlags.Object) === 0)
|
|
3944
3887
|
return;
|
|
3945
|
-
if ((type.objectFlags &
|
|
3888
|
+
if ((type.objectFlags & ts16.ObjectFlags.Reference) === 0)
|
|
3946
3889
|
return;
|
|
3947
3890
|
return type.target.symbol;
|
|
3948
3891
|
}
|
|
3949
3892
|
function ambientHashAliasTargets(checker, anchor) {
|
|
3950
3893
|
const targets = new Set;
|
|
3951
|
-
for (const symbol of checker.getSymbolsInScope(anchor,
|
|
3894
|
+
for (const symbol of checker.getSymbolsInScope(anchor, ts16.SymbolFlags.Type)) {
|
|
3952
3895
|
if (symbol.name !== "Hash")
|
|
3953
3896
|
continue;
|
|
3954
3897
|
const target = referenceTargetSymbol(checker.getDeclaredTypeOfSymbol(symbol));
|
|
@@ -3964,7 +3907,7 @@ function canonicalHashSymbols(checker, program) {
|
|
|
3964
3907
|
if (!anchor)
|
|
3965
3908
|
return canonical;
|
|
3966
3909
|
const aliased = ambientHashAliasTargets(checker, anchor);
|
|
3967
|
-
for (const symbol of checker.getSymbolsInScope(anchor,
|
|
3910
|
+
for (const symbol of checker.getSymbolsInScope(anchor, ts16.SymbolFlags.Function)) {
|
|
3968
3911
|
if (symbol.name !== "hash")
|
|
3969
3912
|
continue;
|
|
3970
3913
|
const declaration = symbol.declarations?.[0];
|
|
@@ -3986,7 +3929,7 @@ function hashedSourceOfType(checker, type, canonical) {
|
|
|
3986
3929
|
return source?.isStringLiteral() ? source.value : undefined;
|
|
3987
3930
|
}
|
|
3988
3931
|
function staticAddressTextOf(checker, node, canonical) {
|
|
3989
|
-
return
|
|
3932
|
+
return ts16.isStringLiteralLike(node) ? node.text : hashedSourceOfType(checker, checker.getTypeAtLocation(node), canonical);
|
|
3990
3933
|
}
|
|
3991
3934
|
|
|
3992
3935
|
// src/url-cross-world-address.ts
|
|
@@ -4010,7 +3953,7 @@ function checkCrossWorldAddresses(input) {
|
|
|
4010
3953
|
if (worlds.length === 0)
|
|
4011
3954
|
continue;
|
|
4012
3955
|
const visit = (node) => {
|
|
4013
|
-
if (
|
|
3956
|
+
if (ts17.isExpression(node)) {
|
|
4014
3957
|
const entry = addressEntryOfArgument(checker, table, node);
|
|
4015
3958
|
if (entry !== undefined && slotRejectsForeignSocket(entry)) {
|
|
4016
3959
|
const address = staticAddressTextOf(checker, node, canonical);
|
|
@@ -4027,14 +3970,14 @@ function checkCrossWorldAddresses(input) {
|
|
|
4027
3970
|
}
|
|
4028
3971
|
}
|
|
4029
3972
|
}
|
|
4030
|
-
|
|
3973
|
+
ts17.forEachChild(node, visit);
|
|
4031
3974
|
};
|
|
4032
|
-
|
|
3975
|
+
ts17.forEachChild(sourceFile, visit);
|
|
4033
3976
|
}
|
|
4034
3977
|
return findings;
|
|
4035
3978
|
}
|
|
4036
3979
|
// src/url-fragment-reachability.ts
|
|
4037
|
-
import * as
|
|
3980
|
+
import * as ts18 from "typescript";
|
|
4038
3981
|
function pathOfKey(key) {
|
|
4039
3982
|
const socket = socketOfAddress(key);
|
|
4040
3983
|
return socket === undefined ? key : key.slice(socket.length + 1);
|
|
@@ -4079,7 +4022,7 @@ function checkUrlFragmentReachability(input) {
|
|
|
4079
4022
|
if (isAmbient2(sourceFile.fileName))
|
|
4080
4023
|
continue;
|
|
4081
4024
|
const visit = (node) => {
|
|
4082
|
-
if (
|
|
4025
|
+
if (ts18.isExpression(node) && isAddressClass(addressClassOfArgument(checker, table, node))) {
|
|
4083
4026
|
const text = staticAddressTextOf(checker, node, canonical);
|
|
4084
4027
|
if (text !== undefined) {
|
|
4085
4028
|
const hash = text.indexOf("#");
|
|
@@ -4098,9 +4041,9 @@ function checkUrlFragmentReachability(input) {
|
|
|
4098
4041
|
}
|
|
4099
4042
|
}
|
|
4100
4043
|
}
|
|
4101
|
-
|
|
4044
|
+
ts18.forEachChild(node, visit);
|
|
4102
4045
|
};
|
|
4103
|
-
|
|
4046
|
+
ts18.forEachChild(sourceFile, visit);
|
|
4104
4047
|
}
|
|
4105
4048
|
return { kind: "checked", findings };
|
|
4106
4049
|
}
|
|
@@ -4145,6 +4088,7 @@ export {
|
|
|
4145
4088
|
displayPathOf,
|
|
4146
4089
|
findEmittedRequires,
|
|
4147
4090
|
findMainEntryFactoryImports,
|
|
4091
|
+
findTextScriptProperties,
|
|
4148
4092
|
getProgramDiagnostics,
|
|
4149
4093
|
guiScriptResourceOf,
|
|
4150
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defold-typescript/transpiler",
|
|
3
|
-
"version": "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.
|
|
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,
|
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/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
|
-
};
|