Skip to content

Commit b31a1aa

Browse files
gzzhanghaoyyx990803
authored andcommitted
feat(compiler): output source range for compiler errors (#7127)
ref #6338
1 parent d08b49f commit b31a1aa

File tree

21 files changed

+325
-127
lines changed

21 files changed

+325
-127
lines changed

flow/compiler.js

+27-6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ declare type CompilerOptions = {
1818
shouldDecodeTags?: boolean;
1919
shouldDecodeNewlines?: boolean;
2020
shouldDecodeNewlinesForHref?: boolean;
21+
outputSourceRange?: boolean;
2122

2223
// runtime user-configurable
2324
delimiters?: [string, string]; // template delimiters
@@ -27,13 +28,19 @@ declare type CompilerOptions = {
2728
scopeId?: string;
2829
};
2930

31+
declare type WarningMessage = {
32+
msg: string;
33+
start?: number;
34+
end?: number;
35+
};
36+
3037
declare type CompiledResult = {
3138
ast: ?ASTElement;
3239
render: string;
3340
staticRenderFns: Array<string>;
3441
stringRenderFns?: Array<string>;
35-
errors?: Array<string>;
36-
tips?: Array<string>;
42+
errors?: Array<string | WarningMessage>;
43+
tips?: Array<string | WarningMessage>;
3744
};
3845

3946
declare type ModuleOptions = {
@@ -53,11 +60,14 @@ declare type ModuleOptions = {
5360
declare type ASTModifiers = { [key: string]: boolean };
5461
declare type ASTIfCondition = { exp: ?string; block: ASTElement };
5562
declare type ASTIfConditions = Array<ASTIfCondition>;
63+
declare type ASTAttr = { name: string; value: any; start?: number; end?: number };
5664

5765
declare type ASTElementHandler = {
5866
value: string;
5967
params?: Array<any>;
6068
modifiers: ?ASTModifiers;
69+
start?: number;
70+
end?: number;
6171
};
6272

6373
declare type ASTElementHandlers = {
@@ -70,18 +80,24 @@ declare type ASTDirective = {
7080
value: string;
7181
arg: ?string;
7282
modifiers: ?ASTModifiers;
83+
start?: number;
84+
end?: number;
7385
};
7486

7587
declare type ASTNode = ASTElement | ASTText | ASTExpression;
7688

7789
declare type ASTElement = {
7890
type: 1;
7991
tag: string;
80-
attrsList: Array<{ name: string; value: any }>;
92+
attrsList: Array<ASTAttr>;
8193
attrsMap: { [key: string]: any };
94+
rawAttrsMap: { [key: string]: ASTAttr };
8295
parent: ASTElement | void;
8396
children: Array<ASTNode>;
8497

98+
start?: number;
99+
end?: number;
100+
85101
processed?: true;
86102

87103
static?: boolean;
@@ -91,8 +107,8 @@ declare type ASTElement = {
91107
hasBindings?: boolean;
92108

93109
text?: string;
94-
attrs?: Array<{ name: string; value: any }>;
95-
props?: Array<{ name: string; value: string }>;
110+
attrs?: Array<ASTAttr>;
111+
props?: Array<ASTAttr>;
96112
plain?: boolean;
97113
pre?: true;
98114
ns?: string;
@@ -160,6 +176,8 @@ declare type ASTExpression = {
160176
static?: boolean;
161177
// 2.4 ssr optimization
162178
ssrOptimizability?: number;
179+
start?: number;
180+
end?: number;
163181
};
164182

165183
declare type ASTText = {
@@ -169,6 +187,8 @@ declare type ASTText = {
169187
isComment?: boolean;
170188
// 2.4 ssr optimization
171189
ssrOptimizability?: number;
190+
start?: number;
191+
end?: number;
172192
};
173193

174194
// SFC-parser related declarations
@@ -179,7 +199,8 @@ declare type SFCDescriptor = {
179199
script: ?SFCBlock;
180200
styles: Array<SFCBlock>;
181201
customBlocks: Array<SFCBlock>;
182-
};
202+
errors: Array<string | WarningMessage>;
203+
}
183204

184205
declare type SFCBlock = {
185206
type: string;

src/compiler/codegen/index.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ function genOnce (el: ASTElement, state: CodegenState): string {
130130
}
131131
if (!key) {
132132
process.env.NODE_ENV !== 'production' && state.warn(
133-
`v-once can only be used inside v-for that is keyed. `
133+
`v-once can only be used inside v-for that is keyed. `,
134+
el.rawAttrsMap['v-once']
134135
)
135136
return genElement(el, state)
136137
}
@@ -202,6 +203,7 @@ export function genFor (
202203
`<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
203204
`v-for should have explicit keys. ` +
204205
`See https://vuejs.org/guide/list.html#key for more info.`,
206+
el.rawAttrsMap['v-for'],
205207
true /* tip */
206208
)
207209
}
@@ -333,7 +335,10 @@ function genInlineTemplate (el: ASTElement, state: CodegenState): ?string {
333335
if (process.env.NODE_ENV !== 'production' && (
334336
el.children.length !== 1 || ast.type !== 1
335337
)) {
336-
state.warn('Inline-template components must have exactly one child element.')
338+
state.warn(
339+
'Inline-template components must have exactly one child element.',
340+
{ start: el.start }
341+
)
337342
}
338343
if (ast.type === 1) {
339344
const inlineRenderFns = generate(ast, state.options)
@@ -503,7 +508,7 @@ function genComponent (
503508
})`
504509
}
505510

506-
function genProps (props: Array<{ name: string, value: any }>): string {
511+
function genProps (props: Array<ASTAttr>): string {
507512
let res = ''
508513
for (let i = 0; i < props.length; i++) {
509514
const prop = props[i]

src/compiler/create-compiler.js

+23-3
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,29 @@ export function createCompilerCreator (baseCompile: Function): Function {
1313
const finalOptions = Object.create(baseOptions)
1414
const errors = []
1515
const tips = []
16-
finalOptions.warn = (msg, tip) => {
16+
17+
let warn = (msg, range, tip) => {
1718
(tip ? tips : errors).push(msg)
1819
}
1920

2021
if (options) {
22+
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
23+
// $flow-disable-line
24+
const leadingSpaceLength = template.match(/^\s*/)[0].length
25+
26+
warn = (msg, range, tip) => {
27+
const data: WarningMessage = { msg }
28+
if (range) {
29+
if (range.start != null) {
30+
data.start = range.start + leadingSpaceLength
31+
}
32+
if (range.end != null) {
33+
data.end = range.end + leadingSpaceLength
34+
}
35+
}
36+
(tip ? tips : errors).push(data)
37+
}
38+
}
2139
// merge custom modules
2240
if (options.modules) {
2341
finalOptions.modules =
@@ -38,9 +56,11 @@ export function createCompilerCreator (baseCompile: Function): Function {
3856
}
3957
}
4058

41-
const compiled = baseCompile(template, finalOptions)
59+
finalOptions.warn = warn
60+
61+
const compiled = baseCompile(template.trim(), finalOptions)
4262
if (process.env.NODE_ENV !== 'production') {
43-
errors.push.apply(errors, detectErrors(compiled.ast))
63+
detectErrors(compiled.ast, warn)
4464
}
4565
compiled.errors = errors
4666
compiled.tips = tips

src/compiler/error-detector.js

+31-26
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import { dirRE, onRE } from './parser/index'
44

5+
type Range = { start?: number, end?: number };
6+
57
// these keywords should not appear inside expressions, but operators like
68
// typeof, instanceof and in are allowed
79
const prohibitedKeywordRE = new RegExp('\\b' + (
@@ -19,89 +21,92 @@ const unaryOperatorsRE = new RegExp('\\b' + (
1921
const stripStringRE = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`/g
2022

2123
// detect problematic expressions in a template
22-
export function detectErrors (ast: ?ASTNode): Array<string> {
23-
const errors: Array<string> = []
24+
export function detectErrors (ast: ?ASTNode, warn: Function) {
2425
if (ast) {
25-
checkNode(ast, errors)
26+
checkNode(ast, warn)
2627
}
27-
return errors
2828
}
2929

30-
function checkNode (node: ASTNode, errors: Array<string>) {
30+
function checkNode (node: ASTNode, warn: Function) {
3131
if (node.type === 1) {
3232
for (const name in node.attrsMap) {
3333
if (dirRE.test(name)) {
3434
const value = node.attrsMap[name]
3535
if (value) {
36+
const range = node.rawAttrsMap[name]
3637
if (name === 'v-for') {
37-
checkFor(node, `v-for="${value}"`, errors)
38+
checkFor(node, `v-for="${value}"`, warn, range)
3839
} else if (onRE.test(name)) {
39-
checkEvent(value, `${name}="${value}"`, errors)
40+
checkEvent(value, `${name}="${value}"`, warn, range)
4041
} else {
41-
checkExpression(value, `${name}="${value}"`, errors)
42+
checkExpression(value, `${name}="${value}"`, warn, range)
4243
}
4344
}
4445
}
4546
}
4647
if (node.children) {
4748
for (let i = 0; i < node.children.length; i++) {
48-
checkNode(node.children[i], errors)
49+
checkNode(node.children[i], warn)
4950
}
5051
}
5152
} else if (node.type === 2) {
52-
checkExpression(node.expression, node.text, errors)
53+
checkExpression(node.expression, node.text, warn, node)
5354
}
5455
}
5556

56-
function checkEvent (exp: string, text: string, errors: Array<string>) {
57+
function checkEvent (exp: string, text: string, warn: Function, range?: Range) {
5758
const stipped = exp.replace(stripStringRE, '')
5859
const keywordMatch: any = stipped.match(unaryOperatorsRE)
5960
if (keywordMatch && stipped.charAt(keywordMatch.index - 1) !== '$') {
60-
errors.push(
61+
warn(
6162
`avoid using JavaScript unary operator as property name: ` +
62-
`"${keywordMatch[0]}" in expression ${text.trim()}`
63+
`"${keywordMatch[0]}" in expression ${text.trim()}`,
64+
range
6365
)
6466
}
65-
checkExpression(exp, text, errors)
67+
checkExpression(exp, text, warn, range)
6668
}
6769

68-
function checkFor (node: ASTElement, text: string, errors: Array<string>) {
69-
checkExpression(node.for || '', text, errors)
70-
checkIdentifier(node.alias, 'v-for alias', text, errors)
71-
checkIdentifier(node.iterator1, 'v-for iterator', text, errors)
72-
checkIdentifier(node.iterator2, 'v-for iterator', text, errors)
70+
function checkFor (node: ASTElement, text: string, warn: Function, range?: Range) {
71+
checkExpression(node.for || '', text, warn, range)
72+
checkIdentifier(node.alias, 'v-for alias', text, warn, range)
73+
checkIdentifier(node.iterator1, 'v-for iterator', text, warn, range)
74+
checkIdentifier(node.iterator2, 'v-for iterator', text, warn, range)
7375
}
7476

7577
function checkIdentifier (
7678
ident: ?string,
7779
type: string,
7880
text: string,
79-
errors: Array<string>
81+
warn: Function,
82+
range?: Range
8083
) {
8184
if (typeof ident === 'string') {
8285
try {
8386
new Function(`var ${ident}=_`)
8487
} catch (e) {
85-
errors.push(`invalid ${type} "${ident}" in expression: ${text.trim()}`)
88+
warn(`invalid ${type} "${ident}" in expression: ${text.trim()}`, range)
8689
}
8790
}
8891
}
8992

90-
function checkExpression (exp: string, text: string, errors: Array<string>) {
93+
function checkExpression (exp: string, text: string, warn: Function, range?: Range) {
9194
try {
9295
new Function(`return ${exp}`)
9396
} catch (e) {
9497
const keywordMatch = exp.replace(stripStringRE, '').match(prohibitedKeywordRE)
9598
if (keywordMatch) {
96-
errors.push(
99+
warn(
97100
`avoid using JavaScript keyword as property name: ` +
98-
`"${keywordMatch[0]}"\n Raw expression: ${text.trim()}`
101+
`"${keywordMatch[0]}"\n Raw expression: ${text.trim()}`,
102+
range
99103
)
100104
} else {
101-
errors.push(
105+
warn(
102106
`invalid expression: ${e.message} in\n\n` +
103107
` ${exp}\n\n` +
104-
` Raw expression: ${text.trim()}\n`
108+
` Raw expression: ${text.trim()}\n`,
109+
range
105110
)
106111
}
107112
}

0 commit comments

Comments
 (0)