Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it possible to insert a synthesized function call that doesn't include a this argument? #1427

Open
MCJack123 opened this issue Apr 5, 2023 · 1 comment
Labels

Comments

@MCJack123
Copy link

I'm writing a small plugin for myself that will automatically insert runtime type checks for any function which I add a @typecheck annotation. It works by inserting calls to my own expect function at the beginning of any function definition. I've been able to successfully get it to insert the calls; however, it always emits an extra nil argument first for this, even when I declare expect to have this: void. I tried some tricks to make the TypeScript compiler see the declaration properly, but these haven't ended up working.

Here's my current code (apologies for the hackery):

Code
import * as ts from "typescript";
import * as tstl from "typescript-to-lua";

type Writeable<T> = { -readonly [P in keyof T]: T[P] };

function unsynthesize<T extends ts.Node>(node: T): T {
    let n = node as Writeable<T>;
    n.pos = 0;
    n.end = 0;
    return n;
}

function fillTypeList(args: ts.Expression[], type: ts.TypeNode, context: tstl.TransformationContext): boolean {
    if (ts.isUnionTypeNode(type)) {
        for (let t of type.types) if (!fillTypeList(args, t, context)) return false;
    } else if (ts.isFunctionOrConstructorTypeNode(type)) {
        args.push(unsynthesize(ts.factory.createStringLiteral("function")));
    } else if (ts.isArrayTypeNode(type)) {
        args.push(unsynthesize(ts.factory.createStringLiteral("table")));
    } else if (ts.isLiteralTypeNode(type)) {
        if (type.literal.kind === ts.SyntaxKind.NullKeyword) args.push(unsynthesize(ts.factory.createStringLiteral("nil")));
        else if (type.literal.kind === ts.SyntaxKind.FalseKeyword || type.literal.kind === ts.SyntaxKind.TrueKeyword) args.push(unsynthesize(ts.factory.createStringLiteral("boolean")));
        else return false;
    } else if (ts.isParenthesizedTypeNode(type)) {
        return fillTypeList(args, type.type, context);
    } else if (ts.isOptionalTypeNode(type)) {
        if (!fillTypeList(args, type.type, context)) return false;
        args.push(unsynthesize(ts.factory.createStringLiteral("nil")));
    } else if (type.kind === ts.SyntaxKind.NullKeyword) {
        args.push(unsynthesize(ts.factory.createStringLiteral("nil")));
    } else if (type.kind === ts.SyntaxKind.BooleanKeyword) {
        args.push(unsynthesize(ts.factory.createStringLiteral("boolean")));
    } else if (type.kind === ts.SyntaxKind.NumberKeyword) {
        args.push(unsynthesize(ts.factory.createStringLiteral("number")));
    } else if (type.kind === ts.SyntaxKind.StringKeyword) {
        args.push(unsynthesize(ts.factory.createStringLiteral("string")));
    } else if (type.kind === ts.SyntaxKind.FunctionKeyword) {
        args.push(unsynthesize(ts.factory.createStringLiteral("function")));
    } else if (type.kind === ts.SyntaxKind.ObjectKeyword) {
        args.push(unsynthesize(ts.factory.createStringLiteral("table")));
    } else if (ts.isTypeReferenceNode(type)) {
        args.push(unsynthesize(ts.factory.createStringLiteral(type.typeName.getText())));
    } else {
        context.diagnostics.push({
            category: ts.DiagnosticCategory.Warning,
            code: 0,
            file: type.getSourceFile(),
            start: type.pos,
            length: type.end - type.pos,
            messageText: "Could not construct type name for parameter; no type check will be emitted."
        })
        return false;
    }
    return true;
}

function addTypeChecks(m: ts.MethodDeclaration | ts.FunctionDeclaration, context: tstl.TransformationContext) {
    if (m["jsDoc"]) {
        let jsDoc = m["jsDoc"] as ts.JSDoc[];
        if (jsDoc[0].tags?.find(v => v.tagName.escapedText === "typecheck")) {
            let add: ts.Statement[] = [];
            for (let a in m.parameters) {
                let arg = m.parameters[a];
                if (arg.type && !ts.isThisTypeNode(arg.type)) {
                    let args: ts.Expression[] = [
                        unsynthesize(ts.factory.createNumericLiteral(parseInt(a) + 1)),
                        unsynthesize(ts.factory.createIdentifier(arg.name.getText()))
                    ];
                    if (fillTypeList(args, arg.type, context)) {
                        let id = unsynthesize(ts.factory.createIdentifier("expect")) as Writeable<ts.Identifier>;
                        let call = unsynthesize(ts.factory.createCallExpression(
                            id,
                            [
                                unsynthesize(ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)),
                                unsynthesize(ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)),
                                unsynthesize(ts.factory.createRestTypeNode(unsynthesize(ts.factory.createArrayTypeNode(unsynthesize(ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword))))))
                            ], args)) as Writeable<ts.CallExpression>;
                        for (let n of call.arguments) (n as Writeable<ts.Expression>).parent = call;
                        id.parent = call;
                        call.flags &= ~ts.NodeFlags.Synthesized;
                        let stat = unsynthesize(ts.factory.createExpressionStatement(call)) as Writeable<ts.Statement>;
                        call.parent = stat;
                        stat.parent = m;
                        add.push(stat);
                    }
                }
            }
            m.body?.statements["unshift"](...add);
        }
    }
}

class TypeCheckPlugin implements tstl.Plugin {
    public visitors = {
        [ts.SyntaxKind.FunctionDeclaration]: (node: ts.FunctionDeclaration, context: tstl.TransformationContext): tstl.Statement[] => {
            addTypeChecks(node, context);
            return context.superTransformStatements(node);
        },
        [ts.SyntaxKind.ClassDeclaration]: (node: ts.ClassDeclaration, context: tstl.TransformationContext): tstl.Statement[] => {
            for (let m of node.members) {
                if (ts.isMethodDeclaration(m)) {
                    addTypeChecks(m, context);
                }
            }
            return context.superTransformStatements(node);
        }
    }
}

const plugin = new TypeCheckPlugin();
export default plugin;

I'd appreciate any help in making this work - I'm hoping someone knows enough about the TypeScript compiler to be able to work this out. If it's not possible, it would be nice to have some sort of override to force synthesized calls to not have a this argument.

@Perryvw
Copy link
Member

Perryvw commented Apr 16, 2023

There are two main ways TSTL detects functions without this:

  • The function has a first parameter with parameter name ts.createThisKeyword() with type void. You could try adding this as first parameter to your synthesized function.
  • The function has a jsdoc comment with /** @noSelf */ (see https://typescripttolua.github.io/docs/the-self-parameter). You could try adding this jsdoc comment to your synthesized function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants