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`] = ` exports[`compiler: v-for > codegen > skipped key 1`] = `
"const _Vue = Vue "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`] = ` exports[`compiler: v-for > codegen > skipped key 1`] = `
"const _Vue = Vue "const _Vue = Vue

View File

@ -218,6 +218,43 @@ describe('compiler: v-for', () => {
"state ['my items']", "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', () => { 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', () => { test('missing source', () => {
const onError = vi.fn() const onError = vi.fn()
parseWithForTransform('<span v-for="item in" />', { onError }) 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', () => { test('<template v-for> key placement', () => {
const onError = vi.fn() const onError = vi.fn()
parseWithForTransform( 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', () => { test('skipped key', () => {
const source = '<span v-for="( item,, index ) in items" />' const source = '<span v-for="( item,, index ) in items" />'
const { node: forNode } = parseWithForTransform(source) const { node: forNode } = parseWithForTransform(source)
@ -747,6 +850,21 @@ describe('compiler: v-for', () => {
expect(generate(root).code).toMatchSnapshot() 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', () => { test('value + key + index', () => {
const { const {
root, root,

View File

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