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
55 changes: 53 additions & 2 deletions src/transformation/utils/function-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,37 @@ function getExplicitThisParameter(signatureDeclaration: ts.SignatureDeclaration)
}
}

const callContextTypes = new WeakMap<ts.CallLikeExpression, ContextType>();

export function getCallContextType(context: TransformationContext, callExpression: ts.CallLikeExpression): ContextType {
const known = callContextTypes.get(callExpression);
if (known !== undefined) return known;

const signature = context.checker.getResolvedSignature(callExpression);
const signatureDeclaration = signature?.getDeclaration();

let contextType = ContextType.None;

if (signatureDeclaration) {
contextType = computeDeclarationContextType(context, signatureDeclaration);
} else {
// No signature declaration could be resolved, so instead try to see if the declaration is in a
// noSelfInFile file
const declarations = findRootDeclarations(context, callExpression);
contextType = declarations.some(d => getFileAnnotations(d.getSourceFile()).has(AnnotationKind.NoSelfInFile))
? ContextType.Void
: context.options.noImplicitSelf
? ContextType.Void
: ContextType.NonVoid;
}

callContextTypes.set(callExpression, contextType);
return contextType;
}

const signatureDeclarationContextTypes = new WeakMap<ts.SignatureDeclaration, ContextType>();

export function getDeclarationContextType(
function getSignatureContextType(
context: TransformationContext,
signatureDeclaration: ts.SignatureDeclaration
): ContextType {
Expand All @@ -48,6 +76,29 @@ export function getDeclarationContextType(
return contextType;
}

function findRootDeclarations(context: TransformationContext, callExpression: ts.CallLikeExpression): ts.Declaration[] {
const calledExpression = ts.isTaggedTemplateExpression(callExpression)
? callExpression.tag
: ts.isJsxSelfClosingElement(callExpression)
? callExpression.tagName
: ts.isJsxOpeningElement(callExpression)
? callExpression.tagName
: callExpression.expression;
const calledSymbol = context.checker.getSymbolAtLocation(calledExpression);
if (calledSymbol === undefined) return [];

return (
calledSymbol.getDeclarations()?.flatMap(d => {
if (ts.isImportSpecifier(d)) {
const aliasSymbol = context.checker.getAliasedSymbol(calledSymbol);
return aliasSymbol.getDeclarations() ?? [];
} else {
return [d];
}
}) ?? []
);
}

function computeDeclarationContextType(context: TransformationContext, signatureDeclaration: ts.SignatureDeclaration) {
const thisParameter = getExplicitThisParameter(signatureDeclaration);
if (thisParameter) {
Expand Down Expand Up @@ -172,6 +223,6 @@ function computeFunctionContextType(context: TransformationContext, type: ts.Typ
}

return reduceContextTypes(
signatures.flatMap(s => getSignatureDeclarations(context, s)).map(s => getDeclarationContextType(context, s))
signatures.flatMap(s => getSignatureDeclarations(context, s)).map(s => getSignatureContextType(context, s))
);
}
32 changes: 11 additions & 21 deletions src/transformation/visitors/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as lua from "../../LuaAST";
import { transformBuiltinCallExpression } from "../builtins";
import { FunctionVisitor, TransformationContext } from "../context";
import { validateAssignment } from "../utils/assignment-validation";
import { ContextType, getDeclarationContextType } from "../utils/function-context";
import { ContextType, getCallContextType } from "../utils/function-context";
import { wrapInTable } from "../utils/lua-ast";
import { isValidLuaIdentifier } from "../utils/safe-names";
import { isExpressionWithEvaluationEffect } from "../utils/typescript";
Expand Down Expand Up @@ -117,16 +117,15 @@ function transformElementAccessCall(
export function transformContextualCallExpression(
context: TransformationContext,
node: ts.CallExpression | ts.TaggedTemplateExpression,
args: ts.Expression[] | ts.NodeArray<ts.Expression>,
signature?: ts.Signature
args: ts.Expression[] | ts.NodeArray<ts.Expression>
): lua.Expression {
if (ts.isOptionalChain(node)) {
return transformOptionalChain(context, node);
}
const left = ts.isCallExpression(node) ? getCalledExpression(node) : node.tag;

let { precedingStatements: argPrecedingStatements, result: transformedArguments } =
transformInPrecedingStatementScope(context, () => transformArguments(context, args, signature));
transformInPrecedingStatementScope(context, () => transformArguments(context, args));

if (
ts.isPropertyAccessExpression(left) &&
Expand Down Expand Up @@ -189,10 +188,9 @@ function transformPropertyCall(
return lua.createCallExpression(context.transformExpression(node.expression), parameters, node);
}

const signatureDeclaration = signature?.getDeclaration();
if (!signatureDeclaration || getDeclarationContextType(context, signatureDeclaration) !== ContextType.Void) {
if (getCallContextType(context, node) !== ContextType.Void) {
// table:name()
return transformContextualCallExpression(context, node, node.arguments, signature);
return transformContextualCallExpression(context, node, node.arguments);
} else {
// table.name()
const [callPath, parameters] = transformCallAndArguments(context, node.expression, node.arguments, signature);
Expand All @@ -202,14 +200,12 @@ function transformPropertyCall(
}

function transformElementCall(context: TransformationContext, node: ts.CallExpression): lua.Expression {
const signature = context.checker.getResolvedSignature(node);
const signatureDeclaration = signature?.getDeclaration();
if (!signatureDeclaration || getDeclarationContextType(context, signatureDeclaration) !== ContextType.Void) {
if (getCallContextType(context, node) !== ContextType.Void) {
// A contextual parameter must be given to this call expression
return transformContextualCallExpression(context, node, node.arguments, signature);
return transformContextualCallExpression(context, node, node.arguments);
} else {
// No context
const [expression, parameters] = transformCallAndArguments(context, node.expression, node.arguments, signature);
const [expression, parameters] = transformCallAndArguments(context, node.expression, node.arguments);
return lua.createCallExpression(expression, parameters, node);
}
}
Expand Down Expand Up @@ -267,7 +263,9 @@ export const transformCallExpression: FunctionVisitor<ts.CallExpression> = (node

let callPath: lua.Expression;
let parameters: lua.Expression[];
const isContextualCall = isContextualCallExpression(context, signature);

const isContextualCall = getCallContextType(context, node) !== ContextType.Void;

if (!isContextualCall) {
[callPath, parameters] = transformCallAndArguments(context, calledExpression, node.arguments, signature);
} else {
Expand All @@ -290,14 +288,6 @@ export const transformCallExpression: FunctionVisitor<ts.CallExpression> = (node
return wrapResultInTable ? wrapInTable(callExpression) : callExpression;
};

function isContextualCallExpression(context: TransformationContext, signature: ts.Signature | undefined): boolean {
const declaration = signature?.getDeclaration();
if (!declaration) {
return !context.options.noImplicitSelf;
}
return getDeclarationContextType(context, declaration) !== ContextType.Void;
}

export function getCalledExpression(node: ts.CallExpression): ts.Expression {
return ts.skipOuterExpressions(node.expression);
}
11 changes: 4 additions & 7 deletions src/transformation/visitors/template.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as ts from "typescript";
import * as lua from "../../LuaAST";
import { FunctionVisitor } from "../context";
import { ContextType, getDeclarationContextType } from "../utils/function-context";
import { ContextType, getCallContextType } from "../utils/function-context";
import { wrapInToStringForConcat } from "../utils/lua-ast";
import { isStringType } from "../utils/typescript";
import { transformArguments, transformContextualCallExpression } from "./call";
Expand Down Expand Up @@ -88,17 +88,14 @@ export const transformTaggedTemplateExpression: FunctionVisitor<ts.TaggedTemplat
expressions.unshift(stringObject);

// Evaluate if there is a self parameter to be used.
const signature = context.checker.getResolvedSignature(expression);
const signatureDeclaration = signature?.getDeclaration();
const useSelfParameter =
signatureDeclaration && getDeclarationContextType(context, signatureDeclaration) !== ContextType.Void;
const useSelfParameter = getCallContextType(context, expression) !== ContextType.Void;

if (useSelfParameter) {
return transformContextualCallExpression(context, expression, expressions, signature);
return transformContextualCallExpression(context, expression, expressions);
}

// Argument evaluation.
const callArguments = transformArguments(context, expressions, signature);
const callArguments = transformArguments(context, expressions);

const leftHandSideExpression = context.transformExpression(expression.tag);
return lua.createCallExpression(leftHandSideExpression, callArguments);
Expand Down
55 changes: 55 additions & 0 deletions test/unit/functions/noSelfAnnotation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,58 @@ test("explicit this parameter respected over @noSelf", () => {
export const result = foo(1);
`.expectToMatchJsResult();
});

test("respect noSelfInFile over noImplicitSelf", () => {
const result = util.testModule`
/** @noSelfInFile **/
const func: Function = () => 1;
export const result = func(1);
`
.expectToMatchJsResult()
.getLuaResult();

expect(result.transpiledFiles).not.toHaveLength(0);

const mainFile = result.transpiledFiles.find(f => f.outPath === "main.lua");
expect(mainFile).toBeDefined();

// avoid ts error "not defined", even though toBeDefined is being checked above
if (!mainFile) return;

expect(mainFile.lua).toBeDefined();
expect(mainFile.lua).toContain("func(1)");
});

test("respect noSelfInFile over noImplicitSelf (func declared in other file)", () => {
const result = util.testModule`
import { func, result } from "./functions";

export const result1 = result;
export const result2 = func(1);
`
.addExtraFile(
"functions.ts",
`
/** @noSelfInFile **/
export const func: Function = () => 1;
export const result = func(2);
`
)
.expectToMatchJsResult()
.getLuaResult();

expect(result.transpiledFiles).not.toHaveLength(0);

const mainFile = result.transpiledFiles.find(f => f.outPath.includes("main.lua"));
expect(mainFile).toBeDefined();
const functionFile = result.transpiledFiles.find(f => f.outPath.includes("functions.lua"));
expect(functionFile).toBeDefined();

// avoid ts error "not defined", even though toBeDefined is being checked above
if (!mainFile || !functionFile) return;

expect(mainFile.lua).toBeDefined();
expect(mainFile.lua).toContain("func(1)");
expect(mainFile.lua).toBeDefined();
expect(functionFile.lua).toContain("func(2)");
});