Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(compiler): output source range for compiler errors (#6338) #7127

Merged
merged 10 commits into from
Dec 22, 2018
35 changes: 30 additions & 5 deletions flow/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ declare type CompilerOptions = {
shouldDecodeTags?: boolean;
shouldDecodeNewlines?: boolean;
shouldDecodeNewlinesForHref?: boolean;
outputSourceRange?: boolean;

// for ssr optimization compiler
scopeId?: string;
Expand All @@ -27,13 +28,19 @@ declare type CompilerOptions = {
comments?: boolean
};

declare type WarningMessage = {
msg: string;
start?: number;
end?: number;
};

declare type CompiledResult = {
ast: ?ASTElement;
render: string;
staticRenderFns: Array<string>;
stringRenderFns?: Array<string>;
errors?: Array<string>;
tips?: Array<string>;
errors?: Array<string | WarningMessage>;
tips?: Array<string | WarningMessage>;
};

declare type ModuleOptions = {
Expand All @@ -50,10 +57,13 @@ declare type ModuleOptions = {
declare type ASTModifiers = { [key: string]: boolean };
declare type ASTIfCondition = { exp: ?string; block: ASTElement };
declare type ASTIfConditions = Array<ASTIfCondition>;
declare type ASTAttr = { name: string; value: string; start?: number; end?: number };

declare type ASTElementHandler = {
value: string;
modifiers: ?ASTModifiers;
start?: number;
end?: number;
};

declare type ASTElementHandlers = {
Expand All @@ -66,18 +76,24 @@ declare type ASTDirective = {
value: string;
arg: ?string;
modifiers: ?ASTModifiers;
start?: number;
end?: number;
};

declare type ASTNode = ASTElement | ASTText | ASTExpression;

declare type ASTElement = {
type: 1;
tag: string;
attrsList: Array<{ name: string; value: string }>;
attrsList: Array<ASTAttr>;
rawAttrsList?: Array<ASTAttr>;
attrsMap: { [key: string]: string | null };
parent: ASTElement | void;
children: Array<ASTNode>;

start?: number;
end?: number;

processed?: true;

static?: boolean;
Expand All @@ -87,8 +103,8 @@ declare type ASTElement = {
hasBindings?: boolean;

text?: string;
attrs?: Array<{ name: string; value: string }>;
props?: Array<{ name: string; value: string }>;
attrs?: Array<ASTAttr>;
props?: Array<ASTAttr>;
plain?: boolean;
pre?: true;
ns?: string;
Expand Down Expand Up @@ -155,6 +171,8 @@ declare type ASTExpression = {
static?: boolean;
// 2.4 ssr optimization
ssrOptimizability?: number;
start?: number;
end?: number;
};

declare type ASTText = {
Expand All @@ -164,6 +182,8 @@ declare type ASTText = {
isComment?: boolean;
// 2.4 ssr optimization
ssrOptimizability?: number;
start?: number;
end?: number;
};

// SFC-parser related declarations
Expand All @@ -174,6 +194,7 @@ declare type SFCDescriptor = {
script: ?SFCBlock;
styles: Array<SFCBlock>;
customBlocks: Array<SFCCustomBlock>;
errors: Array<string | WarningMessage>;
}

declare type SFCCustomBlock = {
Expand All @@ -183,6 +204,8 @@ declare type SFCCustomBlock = {
end?: number;
src?: string;
attrs: {[attribute:string]: string};
blockStart?: number;
blockEnd?: number;
};

declare type SFCBlock = {
Expand All @@ -194,4 +217,6 @@ declare type SFCBlock = {
src?: string;
scoped?: boolean;
module?: string | boolean;
blockStart?: number;
blockEnd?: number;
};
13 changes: 9 additions & 4 deletions src/compiler/codegen/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { genHandlers } from './events'
import baseDirectives from '../directives/index'
import { camelize, no, extend } from 'shared/util'
import { baseWarn, pluckModuleFunction } from '../helpers'
import { baseWarn, getRawAttr, pluckModuleFunction } from '../helpers'

type TransformFunction = (el: ASTElement, code: string) => string;
type DataGenFunction = (el: ASTElement) => string;
Expand Down Expand Up @@ -115,7 +115,8 @@ function genOnce (el: ASTElement, state: CodegenState): string {
}
if (!key) {
process.env.NODE_ENV !== 'production' && state.warn(
`v-once can only be used inside v-for that is keyed. `
`v-once can only be used inside v-for that is keyed. `,
getRawAttr(el, 'v-once')
)
return genElement(el, state)
}
Expand Down Expand Up @@ -187,6 +188,7 @@ export function genFor (
`<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
`v-for should have explicit keys. ` +
`See https://vuejs.org/guide/list.html#key for more info.`,
getRawAttr(el, 'v-for'),
true /* tip */
)
}
Expand Down Expand Up @@ -318,7 +320,10 @@ function genInlineTemplate (el: ASTElement, state: CodegenState): ?string {
if (process.env.NODE_ENV !== 'production' && (
el.children.length !== 1 || ast.type !== 1
)) {
state.warn('Inline-template components must have exactly one child element.')
state.warn(
'Inline-template components must have exactly one child element.',
{ start: el.start }
)
}
if (ast.type === 1) {
const inlineRenderFns = generate(ast, state.options)
Expand Down Expand Up @@ -485,7 +490,7 @@ function genComponent (
})`
}

function genProps (props: Array<{ name: string, value: string }>): string {
function genProps (props: Array<ASTAttr>): string {
let res = ''
for (let i = 0; i < props.length; i++) {
const prop = props[i]
Expand Down
26 changes: 23 additions & 3 deletions src/compiler/create-compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,29 @@ export function createCompilerCreator (baseCompile: Function): Function {
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
finalOptions.warn = (msg, tip) => {

let warn = (msg, range, tip) => {
(tip ? tips : errors).push(msg)
}

if (options) {
if (options.outputSourceRange) {
// $flow-disable-line
const leadingSpaceLength = template.match(/^\s*/)[0].length

warn = (msg, range, tip) => {
const data: WarningMessage = { msg }
if (range) {
if (range.start != null) {
data.start = range.start + leadingSpaceLength
}
if (range.end != null) {
data.end = range.end + leadingSpaceLength
}
}
(tip ? tips : errors).push(data)
}
}
// merge custom modules
if (options.modules) {
finalOptions.modules =
Expand All @@ -38,9 +56,11 @@ export function createCompilerCreator (baseCompile: Function): Function {
}
}

const compiled = baseCompile(template, finalOptions)
finalOptions.warn = warn

const compiled = baseCompile(template.trim(), finalOptions)
if (process.env.NODE_ENV !== 'production') {
errors.push.apply(errors, detectErrors(compiled.ast))
detectErrors(compiled.ast, warn)
}
compiled.errors = errors
compiled.tips = tips
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/directives/on.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/* @flow */

import { warn } from 'core/util/index'
import { getRawAttr } from '../helpers'

export default function on (el: ASTElement, dir: ASTDirective) {
if (process.env.NODE_ENV !== 'production' && dir.modifiers) {
warn(`v-on without argument does not support modifiers.`)
warn(`v-on without argument does not support modifiers.`, getRawAttr(el, 'v-on'))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be wrong, but warn's second parameter seems to accept Vue vm instance, but getRawAttr returns AstAttr

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I didn't notice that warn is different from the one in other files.

}
el.wrapListeners = (code: string) => `_g(${code},${dir.value})`
}
58 changes: 32 additions & 26 deletions src/compiler/error-detector.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
/* @flow */

import { getRawAttr } from './helpers'
import { dirRE, onRE } from './parser/index'

type Range = { start?: number, end?: number };

// these keywords should not appear inside expressions, but operators like
// typeof, instanceof and in are allowed
const prohibitedKeywordRE = new RegExp('\\b' + (
Expand All @@ -19,89 +22,92 @@ const unaryOperatorsRE = new RegExp('\\b' + (
const stripStringRE = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`/g

// detect problematic expressions in a template
export function detectErrors (ast: ?ASTNode): Array<string> {
const errors: Array<string> = []
export function detectErrors (ast: ?ASTNode, warn: Function) {
if (ast) {
checkNode(ast, errors)
checkNode(ast, warn)
}
return errors
}

function checkNode (node: ASTNode, errors: Array<string>) {
function checkNode (node: ASTNode, warn: Function) {
if (node.type === 1) {
for (const name in node.attrsMap) {
if (dirRE.test(name)) {
const value = node.attrsMap[name]
if (value) {
const range = getRawAttr(node, name)
if (name === 'v-for') {
checkFor(node, `v-for="${value}"`, errors)
checkFor(node, `v-for="${value}"`, warn, range)
} else if (onRE.test(name)) {
checkEvent(value, `${name}="${value}"`, errors)
checkEvent(value, `${name}="${value}"`, warn, range)
} else {
checkExpression(value, `${name}="${value}"`, errors)
checkExpression(value, `${name}="${value}"`, warn, range)
}
}
}
}
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
checkNode(node.children[i], errors)
checkNode(node.children[i], warn)
}
}
} else if (node.type === 2) {
checkExpression(node.expression, node.text, errors)
checkExpression(node.expression, node.text, warn, node)
}
}

function checkEvent (exp: string, text: string, errors: Array<string>) {
function checkEvent (exp: string, text: string, warn: Function, range?: Range) {
const stipped = exp.replace(stripStringRE, '')
const keywordMatch: any = stipped.match(unaryOperatorsRE)
if (keywordMatch && stipped.charAt(keywordMatch.index - 1) !== '$') {
errors.push(
warn(
`avoid using JavaScript unary operator as property name: ` +
`"${keywordMatch[0]}" in expression ${text.trim()}`
`"${keywordMatch[0]}" in expression ${text.trim()}`,
range
)
}
checkExpression(exp, text, errors)
checkExpression(exp, text, warn, range)
}

function checkFor (node: ASTElement, text: string, errors: Array<string>) {
checkExpression(node.for || '', text, errors)
checkIdentifier(node.alias, 'v-for alias', text, errors)
checkIdentifier(node.iterator1, 'v-for iterator', text, errors)
checkIdentifier(node.iterator2, 'v-for iterator', text, errors)
function checkFor (node: ASTElement, text: string, warn: Function, range?: Range) {
checkExpression(node.for || '', text, warn, range)
checkIdentifier(node.alias, 'v-for alias', text, warn, range)
checkIdentifier(node.iterator1, 'v-for iterator', text, warn, range)
checkIdentifier(node.iterator2, 'v-for iterator', text, warn, range)
}

function checkIdentifier (
ident: ?string,
type: string,
text: string,
errors: Array<string>
warn: Function,
range?: Range
) {
if (typeof ident === 'string') {
try {
new Function(`var ${ident}=_`)
} catch (e) {
errors.push(`invalid ${type} "${ident}" in expression: ${text.trim()}`)
warn(`invalid ${type} "${ident}" in expression: ${text.trim()}`, range)
}
}
}

function checkExpression (exp: string, text: string, errors: Array<string>) {
function checkExpression (exp: string, text: string, warn: Function, range?: Range) {
try {
new Function(`return ${exp}`)
} catch (e) {
const keywordMatch = exp.replace(stripStringRE, '').match(prohibitedKeywordRE)
if (keywordMatch) {
errors.push(
warn(
`avoid using JavaScript keyword as property name: ` +
`"${keywordMatch[0]}"\n Raw expression: ${text.trim()}`
`"${keywordMatch[0]}"\n Raw expression: ${text.trim()}`,
range
)
} else {
errors.push(
warn(
`invalid expression: ${e.message} in\n\n` +
` ${exp}\n\n` +
` Raw expression: ${text.trim()}\n`
` Raw expression: ${text.trim()}\n`,
range
)
}
}
Expand Down
Loading