deepkit-framework icon indicating copy to clipboard operation
deepkit-framework copied to clipboard

[type-compiler] function __types should be hoisted?

Open PedroClerici opened this issue 4 months ago • 3 comments

I was using @deepkit/type and found a problem with function hoisting reflections.

import { ReflectionFunction } from "@deepkit/type";

ReflectionFunction.from(system).getParameters(); // returns: []

function system(foo: string) {
    return foo
}

So I made a fork and tried to fix the issue. It kinda worked:

import { ReflectionFunction } from "@deepkit/type";

ReflectionFunction.from(system).getParameters(); // returns: [ ReflectionParameter { parameter: { kind ...

function system(foo: string) {
    return foo
}

But now, this is a issue:

import { ReflectionFunction } from "@deepkit/type";

console.log(ReflectionFunction.from(system).getParameters()); // ReferenceError: Cannot access '__ΩFoo' before initialization

class Bar {}

type Foo = Bar

function system(foo: Foo) {
    return foo
}

So should we hoist everything to make it work? or leave it be?

PedroClerici avatar Aug 10 '25 08:08 PedroClerici

good question. if we start to hoist function types like this, we have also to hoist all __Ω expressions. the easiest way to do that is to remove NodeFlags.Const in createVariableStatement statements in compiler.ts so that it emits var which is hoisted per default automatically

marcj avatar Aug 10 '25 13:08 marcj

Thanks for the reply. I will give it a shot on my fork!

PedroClerici avatar Aug 10 '25 14:08 PedroClerici

Following your advice, I managed to make it work.

import { ReflectionFunction } from "@deepkit/type";

ReflectionFunction.from(system).getParameters();  // returns: [ ReflectionParameter { parameter: { kind ...

class Bar {}

type Foo = Bar

function system(foo: Foo) {
    return foo
}

But now, since function declarations are just hoisted to the top of the file instead of the top of the scope, this became a problem.

import { ReflectionFunction } from "@deepkit/type";

ReflectionFunction.from(system).getParameters();  // returns: []

class Bar {}

type Foo = Bar

function system(foo: Foo) {
    return foo
}

if (true) {
    ReflectionFunction.from(system).getParameters(); // returns: []

    function system() {
        console.log("Hello, world!")
    }
}

Most of linters (at the least the popular ones that I use) don't recoment you do function redefinitions insides scopes so I let you judge if this is a big deal or not. I also have a transformer proof of concept that hoists things to the top of the scope instead of the top of the file:

import * as ts from "typescript";

const helloWorldStatement = ts.factory.createExpressionStatement(
  ts.factory.createCallExpression(
    ts.factory.createPropertyAccessExpression(
      ts.factory.createIdentifier("console"),
      ts.factory.createIdentifier("log"),
    ),
    undefined,
    [ts.factory.createStringLiteral("Hello, world!")],
  ),
);

export const transformer = (context: ts.TransformationContext) => {
  const statementsToHoist: ts.Statement[][] = [];

  const visitor = (node: ts.Node): ts.VisitResult<ts.Node> => {
    if (ts.isBlock(node) || ts.isSourceFile(node)) {
      statementsToHoist.push([]);

      const updatedNode = ts.visitEachChild(node, visitor, context);

      const hoisted = statementsToHoist.pop();

      if (ts.isBlock(updatedNode)) {
        return context.factory.updateBlock(updatedNode, [
          ...hoisted!,
          ...updatedNode.statements,
        ]);
      }

      if (ts.isSourceFile(updatedNode)) {
        return context.factory.updateSourceFile(updatedNode, [
          ...hoisted!,
          ...updatedNode.statements,
        ]);
      }

      return updatedNode;
    }

    if (ts.isFunctionDeclaration(node)) {
      statementsToHoist[statementsToHoist.length - 1].push(helloWorldStatement);
    }

    return ts.visitEachChild(node, visitor, context);
  };

  return (sourceFile: ts.SourceFile) => {
    return ts.visitNode(sourceFile, visitor);
  };
};

PedroClerici avatar Aug 11 '25 13:08 PedroClerici