Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
1a674c9f14 fix(transpiler): legacy TS decorator parameter no longer shadows imported binding
When a constructor parameter had the same name as an imported variable
used in a decorator expression (e.g. `@Inject(X) X: string`), the
transpiler incorrectly dropped the import because the parameter binding
shadowed the import during symbol resolution. This happened because
parameter decorator expressions were visited inside the function_args
scope, which already contained the parameter binding from the parse pass.

The fix visits decorator expressions in the enclosing (parent) scope,
matching the semantics of the generated code where decorator calls are
hoisted outside the function.

Closes #17036

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-19 09:37:40 +00:00
2 changed files with 136 additions and 0 deletions

View File

@@ -136,7 +136,19 @@ pub fn Visit(
for (args) |*arg| {
if (arg.ts_decorators.len > 0) {
// Decorator expressions for parameters are evaluated in the
// enclosing scope, not in the function argument scope. This
// is important because the generated code for legacy TS
// decorators hoists decorator expressions outside the
// function. Without this, a parameter binding with the same
// name as an import (e.g. `@Inject(X) X: string`) would
// shadow the import inside the decorator expression.
const current = p.current_scope;
if (current.parent) |parent| {
p.current_scope = parent;
}
arg.ts_decorators = p.visitTSDecorators(arg.ts_decorators);
p.current_scope = current;
}
p.visitBinding(arg.binding, duplicate_args_check_ptr);

View File

@@ -0,0 +1,124 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// https://github.com/oven-sh/bun/issues/17036
// Legacy TypeScript decorator parameter with the same name as an imported
// variable should not shadow the import in the decorator expression.
test("legacy TS decorator parameter does not shadow import", async () => {
using dir = tempDir("issue-17036", {
"tsconfig.json": JSON.stringify({
compilerOptions: {
experimentalDecorators: true,
},
}),
"token.ts": `export const X = "hello";`,
"inject.ts": `export function Inject(token: any): ParameterDecorator {
return (target, propertyKey, parameterIndex) => {};
}`,
// Parameter name X shadows the imported X, but the decorator expression
// @Inject(X) should still refer to the imported X.
"index.ts": `import { Inject } from "./inject.ts";
import { X } from "./token.ts";
class ExampleClass {
constructor(
@Inject(X) X: string
) {}
}
console.log("OK");`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "index.ts"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("OK\n");
expect(stderr).not.toContain("ReferenceError");
expect(exitCode).toBe(0);
});
test("legacy TS decorator parameter - multiple params with shadowing", async () => {
using dir = tempDir("issue-17036-multi", {
"tsconfig.json": JSON.stringify({
compilerOptions: {
experimentalDecorators: true,
},
}),
"tokens.ts": `export const A = "tokenA";
export const B = "tokenB";`,
"inject.ts": `export function Inject(token: any): ParameterDecorator {
return (target, propertyKey, parameterIndex) => {};
}`,
"index.ts": `import { Inject } from "./inject.ts";
import { A, B } from "./tokens.ts";
class ExampleClass {
constructor(
@Inject(A) A: string,
@Inject(B) B: string
) {}
}
console.log("OK");`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "index.ts"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("OK\n");
expect(stderr).not.toContain("ReferenceError");
expect(exitCode).toBe(0);
});
test("legacy TS decorator parameter - non-shadowing still works", async () => {
using dir = tempDir("issue-17036-noshadow", {
"tsconfig.json": JSON.stringify({
compilerOptions: {
experimentalDecorators: true,
},
}),
"token.ts": `export const X = "hello";`,
"inject.ts": `export function Inject(token: any): ParameterDecorator {
return (target, propertyKey, parameterIndex) => {};
}`,
// Parameter name differs from import name - should still work
"index.ts": `import { Inject } from "./inject.ts";
import { X } from "./token.ts";
class ExampleClass {
constructor(
@Inject(X) myParam: string
) {}
}
console.log("OK");`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "index.ts"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("OK\n");
expect(stderr).not.toContain("ReferenceError");
expect(exitCode).toBe(0);
});