fix: Do not hoist imports whose names match class properties (fix #10093).#10094
fix: Do not hoist imports whose names match class properties (fix #10093).#10094sheremet-va merged 7 commits intovitest-dev:mainfrom
Conversation
I think it is just being cautious. In your PR this is processed incorrectly: hoistSimpleCodeWithoutMocks(
`
import { remove, add, update } from 'vue'
class A {
remove = 1
add = null
update = update // notice update is not transformed
}
remove(2);
add(4);
`,
) |
|
Thank you, I pushed a fix for that case and added it to the test. |
|
I expanded the test with more access types: import { remove, add, update, del, call } from 'vue'
class A {
// No hoisting - good
remove = 1
// No hoisting - good
add = null
// Hoisting - good
update = update
// No hoisting, fine as the function captures the scope
// del = () => __vi_import_0__.del()
del = () => del()
// No hoisting - Is this correct?
// call = __vi_import_0__.call(4)
call = call(4)
}Most cases look good, but I'm not sure if we want to hoist the invocation of Debugging the test, the CallExpression => 'call' does return true for a reference, and is added to the identifiers array, but the hoisting never occurs... |
|
Well, that seems to have made the CI pass. I'm trying to wrap my head around why the hoisting is needed at all. I think the reason for it is to ensure the name is preserved for NamedEvaluation in the spec; so that the name of the assignment inherited from the variable name is the same as the original code. I'm not confident on that determination though. But if that's true, I believe a NamedEvaluation isn't relevant for the property when its set to the result of a call expression, so I don't think hoisting If i'm wrong, though, the bug lies elsewhere in vitest, as esmWalker still calls onIdentifier for it, albeit with false for all 3 options. |
|
It seems like the current behavior was added to fix this issue - #303 |
|
Looking at the issue, I believe the fix introduced by #303 is preserved; the property 'add', which shares a name with an import, is not transformed as was happening prior to that fix. The test still covers this case. Interestingly, the hoisting seems almost incidental to the bug fix, which was that class property keys were being transformed as imports. Unfortunately, that still leaves me with no clear idea why the hoisting itself was required. |
I think hoisting is a side effect of the way the transform works - vitejs/vite#6261 I'm not sure it was intended to be hoisted. The if check was initially applied only to |
|
Ya, I think I agree this might be unintended. All the code surrounding the classDeclaration usage does seem to only want class declarations, even beyond the name of the variable. Reverting its computation to: const classDeclaration = (parent.type === 'ClassDeclaration' && node === parent.superClass)doesn't seem to have broken anything, and the bug in #303 still remains fixed. The condition itself makes much more sense for hoisting, as we want to pull up a superclass that the parent class is dependent on. The |
|
There seems to be a test for the hoisting behavior here: // #2221
test('should declare variable for imported super class', () => {
expect(
hoistSimpleCodeWithoutMocks(
`import { Foo } from './dependency';` + `class A extends Foo {}`,
),
).toMatchInlineSnapshot(`
"vi.mock('faker');
const __vi_import_0__ = await import("./dependency");
import {vi} from "vitest";
const Foo = __vi_import_0__.Foo;
class A extends Foo {}"
`)
// exported classes: should prepend the declaration at root level, before the
// first class that uses the binding
expect(
hoistSimpleCodeWithoutMocks(
`import { Foo } from './dependency';`
+ `export default class A extends Foo {}\n`
+ `export class B extends Foo {}`,
),
).toMatchInlineSnapshot(`
"vi.mock('faker');
const __vi_import_0__ = await import("./dependency");
import {vi} from "vitest";
const Foo = __vi_import_0__.Foo;
export default class A extends Foo {}
export class B extends Foo {}"
`)
})It's marked #2221, but I can't find that issue or PR in the vitest repository. |
This transform is based on Vite's implementation so most of the # reference vite's repo: vitejs/vite#2221 |
|
Okay, so the purpose isn't actually hoisting, but just getting the appropriate reference. That makes a lot more sense. I definitely think aliasing the properties was an unintended bug by #303, and we can probably remove it. So in this PR, esmWalker now correctly outputs identifiers used in the assignment portion of class properties, but not for the properties themselves. It also now no longer indicates that such references are class declarations, resulting in the references no longer getting aliased. I also think I was off base with my assumption that it was a named reference thing. Thinking about it more, I believe the name for the named reference is always set by the assignment target, not the reference identifier. So I don't think removing the aliasing is going to result in observable differences to tests, although I might be wrong there. |
|
Can you also port the fix to Vite repo? It's here: https://github.com/vitejs/vite/blob/05803be47e88a0a0f2b792f0d9b819770bd23dbf/packages/vite/src/node/ssr/ssrTransform.ts#L367 |
Certainly, I will get to this in the next day. I'm not sure what's causing tests to fail, only on macos, and only for a splattering of completely unrelated tests. For the rolldown tests, the vite-ui tests are failing. Its looking for a button, found it, but then waits too long to click it and it disappears? And for Build&Test, it seems to be missing some stdout content: Neither error reproduces locally for me, and neither is happening for the other OSes in the pipeline. These tests seemed to have failed earlier, but all of them passed in at least one commit. I want to blame test flakiness, especially because b08d4b8 and b937428 should be identical content. |
fix: Do not hoist imports whose names match class properties (fix #10093).
Description
Fixes vitest hoisting imports when a class property shares the same name.
Resolves #10093
Full reproduction
When vitest 4.1.3 encounters a class property that matches the name of an import, it attempts to hoist the import above the class property, often resulting in reference errors:
Since the property name is not a reference to the import, the hoisting is unnecessary, and hoisting itself causes a ReferenceError as it accesses the vite import of the module before the actual import is performed.
I am familiar with javascript parsing, but not too familiar with estree or vitest's internals. This PR is mostly a hunch, and I am not sure of the full ramifications of the change. However, it fixes the issue in my bug example repo.
There is a test at test/core/test/injector-mock.test.ts that seems to explicitly test for the faulty behavior. That is, it looks for the
addandremovevariables being hoisted above the class even though the class does not reference them. Fixing this bug caused this test to fail, as now there is no need to hoist the variables at all. I tweaked the test to reference the imports, and checked that the imports are still referenced correctly. However, I am not sure why this test existed in its original form, and I don't know if I'm missing some reason as to why this original behavior is actually the desired one.Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
pnpm-lock.yamlunless you introduce a new test example.Tests
pnpm test:ci.Changesets
feat:,fix:,perf:,docs:, orchore:.