Skip to content

ReferenceError / crash when a class property inside a mocked module matches the name of an import (TypeScript + useDefineForClassFields) #10093

@SunsetFi

Description

@SunsetFi

Describe the bug

When a mock defines a class with a property key that matches that of an import, vitest incorrectly hoists the imported reference above that mock, while leaving the instantiation below it.

The bug is triggered with the following conditions:

  • Using typescript 6.0.2
  • With "useDefineForClassFields" turned on in the tsconfig
  • Mocking a class with a class property
  • In a file that imports a function whose name matches the class property

The useDefineForClassFields setting is a requirement for this bug. Without this setting, typescript writes the property set inside the constructor, where presumably vitest knows it is a property name and not a reference. With this setting on, typescript emits code more-or-less as written.

I tried to reproduce this with pure javascript, but after a few tries I wasn't able to. It seems to be typescript specific.

Reproduction

Full reproduction: https://github.com/SunsetFi/vitest-class-method-name-collision/tree/main
Failing test in CI: https://github.com/SunsetFi/vitest-class-method-name-collision/actions/runs/24091793731/job/70279852955

Sample test:

const { myMethodMock } = vi.hoisted(() => ({
  myMethodMock: vi.fn(),
}));

vi.mock("./MyClass", () => ({
  MyClass: class {
    // This line triggers the crash
    myMethod = myMethodMock;
    // This workaround fixes it:
    // ["myMethod"] = myMethodMock;
  },
}));

// This incorrectly gets hoisted above the MyClass mock,
// presumably because vitest sees the property name and thinks its accessing
// this function.
import { myMethod } from "./my-method";

describe("myMethod", () => {
  it("should call MyClass.myMethod", () => {
    myMethodMock.mockReturnValue("Mocked Hello, World!");
    const result = myMethod();
    expect(myMethodMock).toHaveBeenCalled();
    expect(result).toBe("Mocked Hello, World!");
  });
});

This produces a ReferenceError, Cannot access <import> before initialization. However, the line and character number are nonsense as the error occurs in generated code.

ReferenceError: Cannot access '__vi_import_0__' before initialization
 ❯ src/my-method.spec.ts:4:18
      2|
      3| const { myMethodMock } = vi.hoisted(() => ({
      4|   myMethodMock: vi.fn(),
       |                  ^
      5| }));

The bug seems agnostic to how the class itself is defined. All of these trigger the bug:

vi.mock("./MyClass", () => ({
  MyClass: class {
    myMethod = myMethodMock;
  },
}));
vi.mock("./MyClass", () => ({
  MyClass: class MyClass {
    myMethod = myMethodMock;
  },
}));
vi.mock("./MyClass", () => {
  class MyClass {
    myMethod = myMethodMock;
  }
  return { MyClass };
});

Method declarations, however, do not trigger it. The following works without issue:

vi.mock("./MyClass", () => ({
  MyClass: class {
    myMethod() {
      return myMethodMock();
    }
  },
}));

System Info

System:
    OS: Linux 6.19 Arch Linux
    CPU: (16) x64 AMD Ryzen 7 7840HS w/ Radeon 780M Graphics
    Memory: 15.06 GB / 30.65 GB
    Container: Yes
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 24.14.1 - /home/sunset/.vite-plus/js_runtime/node/24.14.1/bin/node
    npm: 11.11.0 - /home/sunset/.vite-plus/js_runtime/node/24.14.1/bin/npm
    pnpm: 10.17.1 - /home/sunset/.local/share/pnpm/pnpm
    Deno: 2.7.11 - /usr/bin/deno
  Browsers:
    Firefox: 149.0
    Firefox Developer Edition: 149.0
  npmPackages:
    vitest: ^4.1.3 => 4.1.3

Used Package Manager

pnpm

Validations

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions