This commit is contained in:
Tycho 2025-06-19 14:14:53 +08:00 committed by GitHub
commit f8b4f289fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 162 additions and 5 deletions

View File

@ -45,6 +45,20 @@ return function render(_ctx, _cache) {
}"
`;
exports[`compiler: v-for > codegen > no whitespace around (in|of) with basic v-for 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList([items], (item) => {
return (_openBlock(), _createElementBlock("span"))
}), 256 /* UNKEYED_FRAGMENT */))
}
}"
`;
exports[`compiler: v-for > codegen > skipped key 1`] = `
"const _Vue = Vue

View File

@ -45,6 +45,20 @@ return function render(_ctx, _cache) {
}"
`;
exports[`compiler: v-for > codegen > no whitespace around (in|of) with basic v-for 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList([items], (item) => {
return (_openBlock(), _createElementBlock("span"))
}), 256 /* UNKEYED_FRAGMENT */))
}
}"
`;
exports[`compiler: v-for > codegen > skipped key 1`] = `
"const _Vue = Vue

View File

@ -218,6 +218,43 @@ describe('compiler: v-for', () => {
"state ['my items']",
)
})
test('no whitespace around (in|of) with simple expression', () => {
const { node: forNode } = parseWithForTransform(
'<span v-for="(item)in[items]" />',
)
expect(forNode.keyAlias).toBeUndefined()
expect(forNode.objectIndexAlias).toBeUndefined()
expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('item')
expect((forNode.source as SimpleExpressionNode).content).toBe('[items]')
})
test('no whitespace around (in|of) with object de-structured value', () => {
const { node: forNode } = parseWithForTransform(
'<span v-for="{ id, value }in[item]" />',
)
expect(forNode.keyAlias).toBeUndefined()
expect(forNode.objectIndexAlias).toBeUndefined()
expect((forNode.valueAlias as SimpleExpressionNode).content).toBe(
'{ id, value }',
)
expect((forNode.source as SimpleExpressionNode).content).toBe('[item]')
})
test('no whitespace around (in|of) with array de-structured value', () => {
const { node: forNode } = parseWithForTransform(
'<span v-for="[ id ]in[item]" />',
)
expect(forNode.keyAlias).toBeUndefined()
expect(forNode.objectIndexAlias).toBeUndefined()
expect((forNode.valueAlias as SimpleExpressionNode).content).toBe(
'[ id ]',
)
expect((forNode.source as SimpleExpressionNode).content).toBe('[item]')
})
})
describe('errors', () => {
@ -257,6 +294,18 @@ describe('compiler: v-for', () => {
)
})
test('invalid expression containing (in|of)', () => {
const onError = vi.fn()
parseWithForTransform('<span v-for="fooinbar" />', { onError })
expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({
code: ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION,
}),
)
})
test('missing source', () => {
const onError = vi.fn()
parseWithForTransform('<span v-for="item in" />', { onError })
@ -293,6 +342,18 @@ describe('compiler: v-for', () => {
)
})
test('missing source and value', () => {
const onError = vi.fn()
parseWithForTransform('<span v-for=" in " />', { onError })
expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({
code: ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION,
}),
)
})
test('<template v-for> key placement', () => {
const onError = vi.fn()
parseWithForTransform(
@ -437,6 +498,48 @@ describe('compiler: v-for', () => {
)
})
test('no whitespace around (in|of) with bracketed value, key, index', () => {
const source = '<span v-for="( item, key, index )in[items]" />'
const { node: forNode } = parseWithForTransform(source)
const itemOffset = source.indexOf('item')
const value = forNode.valueAlias as SimpleExpressionNode
expect(value.content).toBe('item')
expect(value.loc.start.offset).toBe(itemOffset)
expect(value.loc.start.line).toBe(1)
expect(value.loc.start.column).toBe(itemOffset + 1)
expect(value.loc.end.line).toBe(1)
expect(value.loc.end.column).toBe(itemOffset + 1 + `item`.length)
const keyOffset = source.indexOf('key')
const key = forNode.keyAlias as SimpleExpressionNode
expect(key.content).toBe('key')
expect(key.loc.start.offset).toBe(keyOffset)
expect(key.loc.start.line).toBe(1)
expect(key.loc.start.column).toBe(keyOffset + 1)
expect(key.loc.end.line).toBe(1)
expect(key.loc.end.column).toBe(keyOffset + 1 + `key`.length)
const indexOffset = source.indexOf('index')
const index = forNode.objectIndexAlias as SimpleExpressionNode
expect(index.content).toBe('index')
expect(index.loc.start.offset).toBe(indexOffset)
expect(index.loc.start.line).toBe(1)
expect(index.loc.start.column).toBe(indexOffset + 1)
expect(index.loc.end.line).toBe(1)
expect(index.loc.end.column).toBe(indexOffset + 1 + `index`.length)
const itemsOffset = source.indexOf('[items]')
expect((forNode.source as SimpleExpressionNode).content).toBe('[items]')
expect(forNode.source.loc.start.offset).toBe(itemsOffset)
expect(forNode.source.loc.start.line).toBe(1)
expect(forNode.source.loc.start.column).toBe(itemsOffset + 1)
expect(forNode.source.loc.end.line).toBe(1)
expect(forNode.source.loc.end.column).toBe(
itemsOffset + 1 + `[items]`.length,
)
})
test('skipped key', () => {
const source = '<span v-for="( item,, index ) in items" />'
const { node: forNode } = parseWithForTransform(source)
@ -747,6 +850,21 @@ describe('compiler: v-for', () => {
expect(generate(root).code).toMatchSnapshot()
})
test('no whitespace around (in|of) with basic v-for', () => {
const {
root,
node: { codegenNode },
} = parseWithForTransform('<span v-for="(item)in[items]" />')
expect(assertSharedCodegen(codegenNode)).toMatchObject({
source: { content: `[items]` },
params: [{ content: `item` }],
innerVNodeCall: {
tag: `"span"`,
},
})
expect(generate(root).code).toMatchSnapshot()
})
test('value + key + index', () => {
const {
root,

View File

@ -491,15 +491,25 @@ const tokenizer = new Tokenizer(stack, {
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
function matchForAlias(exp: string) {
const inMatch = exp.match(forAliasRE)
if (!inMatch) return
const LHS = inMatch[1].trim()
const RHS = inMatch[2].trim()
if (LHS && RHS) return { LHS, RHS }
}
function parseForExpression(
input: SimpleExpressionNode,
): ForParseResult | undefined {
const loc = input.loc
const exp = input.content
const inMatch = exp.match(forAliasRE)
const inMatch = matchForAlias(exp)
if (!inMatch) return
const [, LHS, RHS] = inMatch
const { LHS, RHS } = inMatch
const createAliasExpression = (
content: string,
@ -518,14 +528,14 @@ function parseForExpression(
}
const result: ForParseResult = {
source: createAliasExpression(RHS.trim(), exp.indexOf(RHS, LHS.length)),
source: createAliasExpression(RHS, exp.indexOf(RHS, LHS.length)),
value: undefined,
key: undefined,
index: undefined,
finalized: false,
}
let valueContent = LHS.trim().replace(stripParensRE, '').trim()
let valueContent = LHS.replace(stripParensRE, '').trim()
const trimmedOffset = LHS.indexOf(valueContent)
const iteratorMatch = valueContent.match(forIteratorRE)

View File

@ -563,4 +563,5 @@ export function getMemoedVNodeCall(
}
}
export const forAliasRE: RegExp = /([\s\S]*?)\s+(?:in|of)\s+(\S[\s\S]*)/
export const forAliasRE: RegExp =
/([\s\S]*?[\s\)\}\]]+)(?:in|of)([\s\[]+[\s\S]*)/