Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/LuaLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ export enum LuaLibFeature {
FunctionApply = "FunctionApply",
FunctionBind = "FunctionBind",
FunctionCall = "FunctionCall",
Index = "Index",
InstanceOf = "InstanceOf",
Iterator = "Iterator",
Map = "Map",
NewIndex = "NewIndex",
ObjectAssign = "ObjectAssign",
ObjectEntries = "ObjectEntries",
ObjectKeys = "ObjectKeys",
Expand Down
158 changes: 101 additions & 57 deletions src/LuaTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,12 +419,14 @@ export class LuaTransformer {
instanceFields,
statement
));
} else if (instanceFields.length > 0) {
} else if (instanceFields.length > 0
|| statement.members.some(m => tsHelper.isGetAccessorOverride(m, statement, this.checker)))
{
// Generate a constructor if none was defined in a class with instance fields that need initialization
// className.prototype.____constructor = function(self, ...)
// baseClassName.prototype.____constructor(self, ...)
// ...
const constructorBody = this.transformClassInstanceFields(instanceFields);
const constructorBody = this.transformClassInstanceFields(statement, instanceFields);
const superCall = tstl.createExpressionStatement(
tstl.createCallExpression(
tstl.createTableIndexExpression(
Expand Down Expand Up @@ -520,13 +522,61 @@ export class LuaTransformer {
const assignClassPrototype = tstl.createAssignmentStatement(createClassPrototype(), classPrototypeTable);
result.push(assignClassPrototype);

// className.prototype.__index = className.prototype
const classPrototypeIndex = tstl.createTableIndexExpression(
createClassPrototype(),
tstl.createStringLiteral("__index")
);
const assignClassPrototypeIndex = tstl.createAssignmentStatement(classPrototypeIndex, createClassPrototype());
result.push(assignClassPrototypeIndex);
if (statement.members.some(ts.isGetAccessor)) {
// className.prototype.____getters = {}
const classPrototypeGetters = tstl.createTableIndexExpression(
createClassPrototype(),
tstl.createStringLiteral("____getters")
);
const assignClassPrototypeGetters = tstl.createAssignmentStatement(
classPrototypeGetters,
tstl.createTableExpression()
);
result.push(assignClassPrototypeGetters);

// className.prototype.__index = __TS_Index(className.prototype)
const assignClassPrototypeIndex = tstl.createAssignmentStatement(
classPrototypeIndex,
this.transformLuaLibFunction(LuaLibFeature.Index, undefined, createClassPrototype())
);
result.push(assignClassPrototypeIndex);

} else {
// className.prototype.__index = className.prototype
const assignClassPrototypeIndex = tstl.createAssignmentStatement(
classPrototypeIndex,
createClassPrototype()
);
result.push(assignClassPrototypeIndex);
}

if (tsHelper.hasSetAccessorInClassOrAncestor(statement, this.checker)) {
// className.prototype.____setters = {}
const classPrototypeSetters = tstl.createTableIndexExpression(
createClassPrototype(),
tstl.createStringLiteral("____setters")
);
const assignClassPrototypeSetters = tstl.createAssignmentStatement(
classPrototypeSetters,
tstl.createTableExpression()
);
result.push(assignClassPrototypeSetters);

// className.prototype.__newindex = __TS_NewIndex(className.prototype)
const classPrototypeNewIndex = tstl.createTableIndexExpression(
createClassPrototype(),
tstl.createStringLiteral("__newindex")
);
const assignClassPrototypeIndex = tstl.createAssignmentStatement(
classPrototypeNewIndex,
this.transformLuaLibFunction(LuaLibFeature.NewIndex, undefined, createClassPrototype())
);
result.push(assignClassPrototypeIndex);
}

// className.prototype.constructor = className
const classPrototypeConstructor = tstl.createTableIndexExpression(
Expand Down Expand Up @@ -620,7 +670,11 @@ export class LuaTransformer {
return result;
}

public transformClassInstanceFields(instanceFields: ts.PropertyDeclaration[]): tstl.Statement[] {
public transformClassInstanceFields(
classDeclarataion: ts.ClassLikeDeclaration,
instanceFields: ts.PropertyDeclaration[]
): tstl.Statement[]
{
const statements: tstl.Statement[] = [];

for (const f of instanceFields) {
Expand All @@ -638,6 +692,19 @@ export class LuaTransformer {
statements.push(assignClassField);
}

const getOverrides = classDeclarataion.members.filter(
m => tsHelper.isGetAccessorOverride(m, classDeclarataion, this.checker)
);
for (const getter of getOverrides) {
const resetGetter = tstl.createExpressionStatement(
tstl.createCallExpression(
tstl.createIdentifier("rawset"),
[this.createSelfIdentifier(), this.transformPropertyName(getter.name), tstl.createNilLiteral()]
)
);
statements.push(resetGetter);
}

return statements;
}

Expand All @@ -663,7 +730,7 @@ export class LuaTransformer {
return undefined;
}

const bodyStatements: tstl.Statement[] = this.transformClassInstanceFields(instanceFields);
const bodyStatements: tstl.Statement[] = this.transformClassInstanceFields(classDeclaration, instanceFields);

// Check for field declarations in constructor
const constructorFieldsDeclarations = statement.parameters.filter(p => p.modifiers !== undefined);
Expand Down Expand Up @@ -731,15 +798,20 @@ export class LuaTransformer {
[this.createSelfIdentifier()]
);

return tstl.createAssignmentStatement(
tstl.createTableIndexExpression(
tstl.createTableIndexExpression(
this.addExportToIdentifier(tstl.cloneIdentifier(className)),
tstl.createStringLiteral("prototype")
),
tstl.createStringLiteral("get__" + name.text)),
accessorFunction
const classPrototype = tstl.createTableIndexExpression(
this.addExportToIdentifier(tstl.cloneIdentifier(className)),
tstl.createStringLiteral("prototype")
);
const classGetters = tstl.createTableIndexExpression(
classPrototype,
tstl.createStringLiteral("____getters")
);
const getter = tstl.createTableIndexExpression(
classGetters,
tstl.createStringLiteral(name.text)
);
const assignGetter = tstl.createAssignmentStatement(getter, accessorFunction);
return assignGetter;
}

public transformSetAccessorDeclaration(
Expand All @@ -760,15 +832,20 @@ export class LuaTransformer {
restParam
);

return tstl.createAssignmentStatement(
tstl.createTableIndexExpression(
tstl.createTableIndexExpression(
this.addExportToIdentifier(tstl.cloneIdentifier(className)),
tstl.createStringLiteral("prototype")
),
tstl.createStringLiteral("set__" + name.text)),
accessorFunction
const classPrototype = tstl.createTableIndexExpression(
this.addExportToIdentifier(tstl.cloneIdentifier(className)),
tstl.createStringLiteral("prototype")
);
const classSetters = tstl.createTableIndexExpression(
classPrototype,
tstl.createStringLiteral("____setters")
);
const setter = tstl.createTableIndexExpression(
classSetters,
tstl.createStringLiteral(name.text)
);
const assignSetter = tstl.createAssignmentStatement(setter, accessorFunction);
return assignSetter;
}

public transformMethodDeclaration(
Expand Down Expand Up @@ -2141,16 +2218,6 @@ export class LuaTransformer {
}

public transformAssignment(lhs: ts.Expression, right: tstl.Expression): tstl.Statement {
if (ts.isPropertyAccessExpression(lhs)) {
const hasSetAccessor = tsHelper.hasSetAccessor(lhs, this.checker);
if (hasSetAccessor) {
return tstl.createExpressionStatement(this.transformSetAccessor(lhs, right), lhs.parent);
} else if (hasSetAccessor === undefined) {
// Undefined hasSetAccessor indicates a union with both set accessors and no accessors
// on the same field.
throw TSTLErrors.UnsupportedUnionAccessor(lhs);
}
}
return tstl.createAssignmentStatement(
this.transformExpression(lhs) as tstl.IdentifierOrTableIndexExpression,
right,
Expand Down Expand Up @@ -2218,10 +2285,7 @@ export class LuaTransformer {
);
}

if (
(ts.isPropertyAccessExpression(expression.left) && !tsHelper.hasSetAccessor(expression.left, this.checker))
|| ts.isElementAccessExpression(expression.left)
) {
if (ts.isPropertyAccessExpression(expression.left) || ts.isElementAccessExpression(expression.left)) {
// Left is property/element access: cache result while maintaining order of evaluation
// (function(o, i, v) o[i] = v; return v end)(${objExpression}, ${indexExpression}, ${right})
const objParameter = tstl.createIdentifier("o");
Expand Down Expand Up @@ -2986,14 +3050,6 @@ export class LuaTransformer {
public transformPropertyAccessExpression(node: ts.PropertyAccessExpression): tstl.Expression {
const property = node.name.text;

const hasGetAccessor = tsHelper.hasGetAccessor(node, this.checker);
if (hasGetAccessor) {
return this.transformGetAccessor(node);
} else if (hasGetAccessor === undefined) {
// Undefined hasGetAccessor indicates a union with both get accessors and no accessors on the same field.
throw TSTLErrors.UnsupportedUnionAccessor(node);
}

// Check for primitive types to override
const type = this.checker.getTypeAtLocation(node.expression);
if (tsHelper.isStringType(type)) {
Expand Down Expand Up @@ -3031,18 +3087,6 @@ export class LuaTransformer {
return tstl.createTableIndexExpression(callPath, tstl.createStringLiteral(property), node);
}

public transformGetAccessor(node: ts.PropertyAccessExpression): tstl.MethodCallExpression {
const name = tstl.createIdentifier(`get__${node.name.escapedText}`);
const expression = this.transformExpression(node.expression);
return tstl.createMethodCallExpression(expression, name, [], node);
}

public transformSetAccessor(node: ts.PropertyAccessExpression, value: tstl.Expression): tstl.MethodCallExpression {
const name = tstl.createIdentifier(`set__${node.name.escapedText}`);
const expression = this.transformExpression(node.expression);
return tstl.createMethodCallExpression(expression, name, [value], node);
}

// Transpile a Math._ property
public transformMathExpression(identifier: ts.Identifier): tstl.TableIndexExpression {
const translation = {
Expand Down
126 changes: 72 additions & 54 deletions src/TSHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,60 +261,6 @@ export class TSHelper {
return undefined;
}

public static hasExplicitGetAccessor(type: ts.Type, name: ts.__String): boolean {
if (type && type.symbol && type.symbol.members) {
const field = type.symbol.members.get(name);
return field && (field.flags & ts.SymbolFlags.GetAccessor) !== 0;
}
}

public static typeHasGetAccessor(type: ts.Type, name: ts.__String, checker: ts.TypeChecker): boolean | undefined {
if (type.isUnion()) {
if (type.types.some(t => TSHelper.typeHasGetAccessor(t, name, checker))) {
// undefined if only a subset of types implements the accessor
return type.types.every(t => TSHelper.typeHasGetAccessor(t, name, checker)) ? true : undefined;
}
return false;
}
return TSHelper.forTypeOrAnySupertype(type, checker, t => TSHelper.hasExplicitGetAccessor(t, name));
}

public static hasGetAccessor(node: ts.Node, checker: ts.TypeChecker): boolean | undefined {
if (ts.isPropertyAccessExpression(node)) {
const name = node.name.escapedText;
const type = checker.getTypeAtLocation(node.expression);
return TSHelper.typeHasGetAccessor(type, name, checker);
}
return false;
}

public static hasExplicitSetAccessor(type: ts.Type, name: ts.__String): boolean {
if (type && type.symbol && type.symbol.members) {
const field = type.symbol.members.get(name);
return field && (field.flags & ts.SymbolFlags.SetAccessor) !== 0;
}
}

public static typeHasSetAccessor(type: ts.Type, name: ts.__String, checker: ts.TypeChecker): boolean | undefined {
if (type.isUnion()) {
if (type.types.some(t => TSHelper.typeHasSetAccessor(t, name, checker))) {
// undefined if only a subset of types implements the accessor
return type.types.every(t => TSHelper.typeHasSetAccessor(t, name, checker)) ? true : undefined;
}
return false;
}
return TSHelper.forTypeOrAnySupertype(type, checker, t => TSHelper.hasExplicitSetAccessor(t, name));
}

public static hasSetAccessor(node: ts.Node, checker: ts.TypeChecker): boolean {
if (ts.isPropertyAccessExpression(node)) {
const name = node.name.escapedText;
const type = checker.getTypeAtLocation(node.expression);
return TSHelper.typeHasSetAccessor(type, name, checker);
}
return false;
}

public static isBinaryAssignmentToken(token: ts.SyntaxKind): [boolean, tstl.BinaryOperator] {
switch (token) {
case ts.SyntaxKind.BarEqualsToken:
Expand Down Expand Up @@ -398,6 +344,78 @@ export class TSHelper {
param => ts.isIdentifier(param.name) && param.name.originalKeywordKind === ts.SyntaxKind.ThisKeyword);
}

public static findInClassOrAncestor(
classDeclaration: ts.ClassLikeDeclarationBase,
callback: (classDeclaration: ts.ClassLikeDeclarationBase) => boolean,
checker: ts.TypeChecker
): ts.ClassLikeDeclarationBase
{
if (callback(classDeclaration)) {
return classDeclaration;
}

const extendsType = TSHelper.getExtendedType(classDeclaration, checker);
if (!extendsType) {
return undefined;
}

const symbol = extendsType.getSymbol();
const declaration = symbol.getDeclarations().find(ts.isClassLike);
if (!declaration) {
return undefined;
}

return TSHelper.findInClassOrAncestor(declaration, callback, checker);
}

public static hasSetAccessorInClassOrAncestor(
classDeclaration: ts.ClassLikeDeclarationBase,
checker: ts.TypeChecker
): boolean
{
return TSHelper.findInClassOrAncestor(
classDeclaration,
c => c.members.some(ts.isSetAccessor),
checker
) !== undefined;
}

public static getPropertyName(propertyName: ts.PropertyName): string | number | undefined {
if (ts.isIdentifier(propertyName) || ts.isStringLiteral(propertyName) || ts.isNumericLiteral(propertyName)) {
return propertyName.text;
} else {
return undefined; // TODO: how to handle computed property names?
}
}

public static isSamePropertyName(a: ts.PropertyName, b: ts.PropertyName): boolean {
const aName = TSHelper.getPropertyName(a);
const bName = TSHelper.getPropertyName(b);
return aName !== undefined && aName === bName;
}

public static isGetAccessorOverride(
element: ts.ClassElement,
classDeclaration: ts.ClassLikeDeclarationBase,
checker: ts.TypeChecker
): element is ts.GetAccessorDeclaration
{
if (!ts.isGetAccessor(element)) {
return false;
}

const hasInitializedField = (e: ts.ClassElement) =>
ts.isPropertyDeclaration(e)
&& e.initializer
&& TSHelper.isSamePropertyName(e.name, element.name);

return TSHelper.findInClassOrAncestor(
classDeclaration,
c => c.members.some(hasInitializedField),
checker
) !== undefined;
}

public static inferAssignedType(expression: ts.Expression, checker: ts.TypeChecker): ts.Type {
if (ts.isParenthesizedExpression(expression.parent)) {
// Ignore expressions wrapped in parenthesis
Expand Down
Loading