Преглед изворни кода

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

ref #6338
Jason пре 7 година
родитељ
комит
b31a1aa887

+ 27 - 6
flow/compiler.js

@@ -18,6 +18,7 @@ declare type CompilerOptions = {
   shouldDecodeTags?: boolean;
   shouldDecodeNewlines?:  boolean;
   shouldDecodeNewlinesForHref?: boolean;
+  outputSourceRange?: boolean;
 
   // runtime user-configurable
   delimiters?: [string, string]; // template delimiters
@@ -27,13 +28,19 @@ declare type CompilerOptions = {
   scopeId?: string;
 };
 
+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 = {
@@ -53,11 +60,14 @@ 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: any; start?: number; end?: number };
 
 declare type ASTElementHandler = {
   value: string;
   params?: Array<any>;
   modifiers: ?ASTModifiers;
+  start?: number;
+  end?: number;
 };
 
 declare type ASTElementHandlers = {
@@ -70,6 +80,8 @@ declare type ASTDirective = {
   value: string;
   arg: ?string;
   modifiers: ?ASTModifiers;
+  start?: number;
+  end?: number;
 };
 
 declare type ASTNode = ASTElement | ASTText | ASTExpression;
@@ -77,11 +89,15 @@ declare type ASTNode = ASTElement | ASTText | ASTExpression;
 declare type ASTElement = {
   type: 1;
   tag: string;
-  attrsList: Array<{ name: string; value: any }>;
+  attrsList: Array<ASTAttr>;
   attrsMap: { [key: string]: any };
+  rawAttrsMap: { [key: string]: ASTAttr };
   parent: ASTElement | void;
   children: Array<ASTNode>;
 
+  start?: number;
+  end?: number;
+
   processed?: true;
 
   static?: boolean;
@@ -91,8 +107,8 @@ declare type ASTElement = {
   hasBindings?: boolean;
 
   text?: string;
-  attrs?: Array<{ name: string; value: any }>;
-  props?: Array<{ name: string; value: string }>;
+  attrs?: Array<ASTAttr>;
+  props?: Array<ASTAttr>;
   plain?: boolean;
   pre?: true;
   ns?: string;
@@ -160,6 +176,8 @@ declare type ASTExpression = {
   static?: boolean;
   // 2.4 ssr optimization
   ssrOptimizability?: number;
+  start?: number;
+  end?: number;
 };
 
 declare type ASTText = {
@@ -169,6 +187,8 @@ declare type ASTText = {
   isComment?: boolean;
   // 2.4 ssr optimization
   ssrOptimizability?: number;
+  start?: number;
+  end?: number;
 };
 
 // SFC-parser related declarations
@@ -179,7 +199,8 @@ declare type SFCDescriptor = {
   script: ?SFCBlock;
   styles: Array<SFCBlock>;
   customBlocks: Array<SFCBlock>;
-};
+  errors: Array<string | WarningMessage>;
+}
 
 declare type SFCBlock = {
   type: string;

+ 8 - 3
src/compiler/codegen/index.js

@@ -130,7 +130,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. `,
+        el.rawAttrsMap['v-once']
       )
       return genElement(el, state)
     }
@@ -202,6 +203,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.`,
+      el.rawAttrsMap['v-for'],
       true /* tip */
     )
   }
@@ -333,7 +335,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)
@@ -503,7 +508,7 @@ function genComponent (
   })`
 }
 
-function genProps (props: Array<{ name: string, value: any }>): string {
+function genProps (props: Array<ASTAttr>): string {
   let res = ''
   for (let i = 0; i < props.length; i++) {
     const prop = props[i]

+ 23 - 3
src/compiler/create-compiler.js

@@ -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 (process.env.NODE_ENV !== 'production' && 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 =
@@ -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

+ 31 - 26
src/compiler/error-detector.js

@@ -2,6 +2,8 @@
 
 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' + (
@@ -19,89 +21,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 = node.rawAttrsMap[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
       )
     }
   }

+ 43 - 14
src/compiler/helpers.js

@@ -3,9 +3,13 @@
 import { emptyObject } from 'shared/util'
 import { parseFilters } from './parser/filter-parser'
 
-export function baseWarn (msg: string) {
+type Range = { start?: number, end?: number };
+
+/* eslint-disable no-unused-vars */
+export function baseWarn (msg: string, range?: Range) {
   console.error(`[Vue compiler]: ${msg}`)
 }
+/* eslint-enable no-unused-vars */
 
 export function pluckModuleFunction<F: Function> (
   modules: ?Array<Object>,
@@ -16,20 +20,20 @@ export function pluckModuleFunction<F: Function> (
     : []
 }
 
-export function addProp (el: ASTElement, name: string, value: string) {
-  (el.props || (el.props = [])).push({ name, value })
+export function addProp (el: ASTElement, name: string, value: string, range?: Range) {
+  (el.props || (el.props = [])).push(rangeSetItem({ name, value }, range))
   el.plain = false
 }
 
-export function addAttr (el: ASTElement, name: string, value: any) {
-  (el.attrs || (el.attrs = [])).push({ name, value })
+export function addAttr (el: ASTElement, name: string, value: any, range?: Range) {
+  (el.attrs || (el.attrs = [])).push(rangeSetItem({ name, value }, range))
   el.plain = false
 }
 
 // add a raw attr (use this in preTransforms)
-export function addRawAttr (el: ASTElement, name: string, value: any) {
+export function addRawAttr (el: ASTElement, name: string, value: any, range?: Range) {
   el.attrsMap[name] = value
-  el.attrsList.push({ name, value })
+  el.attrsList.push(rangeSetItem({ name, value }, range))
 }
 
 export function addDirective (
@@ -38,9 +42,10 @@ export function addDirective (
   rawName: string,
   value: string,
   arg: ?string,
-  modifiers: ?ASTModifiers
+  modifiers: ?ASTModifiers,
+  range?: Range
 ) {
-  (el.directives || (el.directives = [])).push({ name, rawName, value, arg, modifiers })
+  (el.directives || (el.directives = [])).push(rangeSetItem({ name, rawName, value, arg, modifiers }, range))
   el.plain = false
 }
 
@@ -50,7 +55,8 @@ export function addHandler (
   value: string,
   modifiers: ?ASTModifiers,
   important?: boolean,
-  warn?: Function
+  warn?: ?Function,
+  range?: Range
 ) {
   modifiers = modifiers || emptyObject
   // warn prevent and passive modifier
@@ -61,7 +67,8 @@ export function addHandler (
   ) {
     warn(
       'passive and prevent can\'t be used together. ' +
-      'Passive handler can\'t prevent default event.'
+      'Passive handler can\'t prevent default event.',
+      range
     )
   }
 
@@ -100,9 +107,7 @@ export function addHandler (
     events = el.events || (el.events = {})
   }
 
-  const newHandler: any = {
-    value: value.trim()
-  }
+  const newHandler: any = rangeSetItem({ value: value.trim() }, range)
   if (modifiers !== emptyObject) {
     newHandler.modifiers = modifiers
   }
@@ -120,6 +125,15 @@ export function addHandler (
   el.plain = false
 }
 
+export function getRawBindingAttr (
+  el: ASTElement,
+  name: string
+) {
+  return el.rawAttrsMap[':' + name] ||
+    el.rawAttrsMap['v-bind:' + name] ||
+    el.rawAttrsMap[name]
+}
+
 export function getBindingAttr (
   el: ASTElement,
   name: string,
@@ -162,3 +176,18 @@ export function getAndRemoveAttr (
   }
   return val
 }
+
+function rangeSetItem (
+  item: any,
+  range?: { start?: number, end?: number }
+) {
+  if (range) {
+    if (range.start != null) {
+      item.start = range.start
+    }
+    if (range.end != null) {
+      item.end = range.end
+    }
+  }
+  return item
+}

+ 1 - 1
src/compiler/optimizer.js

@@ -30,7 +30,7 @@ export function optimize (root: ?ASTElement, options: CompilerOptions) {
 
 function genStaticKeys (keys: string): Function {
   return makeMap(
-    'type,tag,attrsList,attrsMap,plain,parent,children,attrs' +
+    'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap' +
     (keys ? ',' + keys : '')
   )
 }

+ 16 - 7
src/compiler/parser/html-parser.js

@@ -69,7 +69,7 @@ export function parseHTML (html, options) {
 
           if (commentEnd >= 0) {
             if (options.shouldKeepComment) {
-              options.comment(html.substring(4, commentEnd))
+              options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
             }
             advance(commentEnd + 3)
             continue
@@ -129,16 +129,18 @@ export function parseHTML (html, options) {
           rest = html.slice(textEnd)
         }
         text = html.substring(0, textEnd)
-        advance(textEnd)
       }
 
       if (textEnd < 0) {
         text = html
-        html = ''
+      }
+
+      if (text) {
+        advance(text.length)
       }
 
       if (options.chars && text) {
-        options.chars(text)
+        options.chars(text, index - text.length, index)
       }
     } else {
       let endTagLength = 0
@@ -167,7 +169,7 @@ export function parseHTML (html, options) {
     if (html === last) {
       options.chars && options.chars(html)
       if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {
-        options.warn(`Mal-formatted tag at end of template: "${html}"`)
+        options.warn(`Mal-formatted tag at end of template: "${html}"`, { start: index + html.length })
       }
       break
     }
@@ -192,7 +194,9 @@ export function parseHTML (html, options) {
       advance(start[0].length)
       let end, attr
       while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
+        attr.start = index
         advance(attr[0].length)
+        attr.end = index
         match.attrs.push(attr)
       }
       if (end) {
@@ -231,10 +235,14 @@ export function parseHTML (html, options) {
         name: args[1],
         value: decodeAttr(value, shouldDecodeNewlines)
       }
+      if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
+        attrs[i].start = args.start + args[0].match(/^\s*/).length
+        attrs[i].end = args.end
+      }
     }
 
     if (!unary) {
-      stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
+      stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
       lastTag = tagName
     }
 
@@ -269,7 +277,8 @@ export function parseHTML (html, options) {
           options.warn
         ) {
           options.warn(
-            `tag <${stack[i].tag}> has no matching end tag.`
+            `tag <${stack[i].tag}> has no matching end tag.`,
+            { start: stack[i].start }
           )
         }
         if (options.end) {

+ 100 - 44
src/compiler/parser/index.js

@@ -16,6 +16,7 @@ import {
   addDirective,
   getBindingAttr,
   getAndRemoveAttr,
+  getRawBindingAttr,
   pluckModuleFunction
 } from '../helpers'
 
@@ -41,11 +42,9 @@ let platformIsPreTag
 let platformMustUseProp
 let platformGetTagNamespace
 
-type Attr = { name: string; value: string };
-
 export function createASTElement (
   tag: string,
-  attrs: Array<Attr>,
+  attrs: Array<ASTAttr>,
   parent: ASTElement | void
 ): ASTElement {
   return {
@@ -53,6 +52,7 @@ export function createASTElement (
     tag,
     attrsList: attrs,
     attrsMap: makeAttrsMap(attrs),
+    rawAttrsMap: {},
     parent,
     children: []
   }
@@ -85,10 +85,10 @@ export function parse (
   let inPre = false
   let warned = false
 
-  function warnOnce (msg) {
+  function warnOnce (msg, range) {
     if (!warned) {
       warned = true
-      warn(msg)
+      warn(msg, range)
     }
   }
 
@@ -114,7 +114,8 @@ export function parse (
     shouldDecodeNewlines: options.shouldDecodeNewlines,
     shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
     shouldKeepComment: options.comments,
-    start (tag, attrs, unary) {
+    outputSourceRange: options.outputSourceRange,
+    start (tag, attrs, unary, start) {
       // check namespace.
       // inherit parent ns if there is one
       const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
@@ -130,12 +131,21 @@ export function parse (
         element.ns = ns
       }
 
+      if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
+        element.start = start
+        element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
+          cumulated[attr.name] = attr
+          return cumulated
+        }, {})
+      }
+
       if (isForbiddenTag(element) && !isServerRendering()) {
         element.forbidden = true
         process.env.NODE_ENV !== 'production' && warn(
           'Templates should only be responsible for mapping the state to the ' +
           'UI. Avoid placing tags with side-effects in your templates, such as ' +
-          `<${tag}>` + ', as they will not be parsed.'
+          `<${tag}>` + ', as they will not be parsed.',
+          { start: element.start }
         )
       }
 
@@ -169,13 +179,15 @@ export function parse (
           if (el.tag === 'slot' || el.tag === 'template') {
             warnOnce(
               `Cannot use <${el.tag}> as component root element because it may ` +
-              'contain multiple nodes.'
+              'contain multiple nodes.',
+              { start: el.start }
             )
           }
           if (el.attrsMap.hasOwnProperty('v-for')) {
             warnOnce(
               'Cannot use v-for on stateful component root element because ' +
-              'it renders multiple elements.'
+              'it renders multiple elements.',
+              el.rawAttrsMap['v-for']
             )
           }
         }
@@ -197,7 +209,8 @@ export function parse (
           warnOnce(
             `Component template should contain exactly one root element. ` +
             `If you are using v-if on multiple elements, ` +
-            `use v-else-if to chain them instead.`
+            `use v-else-if to chain them instead.`,
+            { start: element.start }
           )
         }
       }
@@ -221,7 +234,7 @@ export function parse (
       }
     },
 
-    end () {
+    end (tag, start, end) {
       // remove trailing whitespace
       const element = stack[stack.length - 1]
       const lastNode = element.children[element.children.length - 1]
@@ -231,19 +244,24 @@ export function parse (
       // pop stack
       stack.length -= 1
       currentParent = stack[stack.length - 1]
+      if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
+        element.end = end
+      }
       closeElement(element)
     },
 
-    chars (text: string) {
+    chars (text: string, start: number, end: number) {
       if (!currentParent) {
         if (process.env.NODE_ENV !== 'production') {
           if (text === template) {
             warnOnce(
-              'Component template requires a root element, rather than just text.'
+              'Component template requires a root element, rather than just text.',
+              { start }
             )
           } else if ((text = text.trim())) {
             warnOnce(
-              `text "${text}" outside root element will be ignored.`
+              `text "${text}" outside root element will be ignored.`,
+              { start }
             )
           }
         }
@@ -264,27 +282,40 @@ export function parse (
         : preserveWhitespace && children.length ? ' ' : ''
       if (text) {
         let res
+        let child: ?ASTNode
         if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
-          children.push({
+          child = {
             type: 2,
             expression: res.expression,
             tokens: res.tokens,
             text
-          })
+          }
         } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
-          children.push({
+          child = {
             type: 3,
             text
-          })
+          }
+        }
+        if (child) {
+          if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
+            child.start = start
+            child.end = end
+          }
+          children.push(child)
         }
       }
     },
-    comment (text: string) {
-      currentParent.children.push({
+    comment (text: string, start, end) {
+      const child: ASTText = {
         type: 3,
         text,
         isComment: true
-      })
+      }
+      if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
+        child.start = start
+        child.end = end
+      }
+      currentParent.children.push(child)
     }
   })
   return root
@@ -297,13 +328,18 @@ function processPre (el) {
 }
 
 function processRawAttrs (el) {
-  const l = el.attrsList.length
-  if (l) {
-    const attrs = el.attrs = new Array(l)
-    for (let i = 0; i < l; i++) {
+  const list = el.attrsList
+  const len = list.length
+  if (len) {
+    const attrs: Array<ASTAttr> = el.attrs = new Array(len)
+    for (let i = 0; i < len; i++) {
       attrs[i] = {
-        name: el.attrsList[i].name,
-        value: JSON.stringify(el.attrsList[i].value)
+        name: list[i].name,
+        value: JSON.stringify(list[i].value)
+      }
+      if (list[i].start != null) {
+        attrs[i].start = list[i].start
+        attrs[i].end = list[i].end
       }
     }
   } else if (!el.pre) {
@@ -333,7 +369,10 @@ function processKey (el) {
   if (exp) {
     if (process.env.NODE_ENV !== 'production') {
       if (el.tag === 'template') {
-        warn(`<template> cannot be keyed. Place the key on real elements instead.`)
+        warn(
+          `<template> cannot be keyed. Place the key on real elements instead.`,
+          getRawBindingAttr(el, 'key')
+        )
       }
       if (el.for) {
         const iterator = el.iterator2 || el.iterator1
@@ -342,6 +381,7 @@ function processKey (el) {
           warn(
             `Do not use v-for index as key on <transition-group> children, ` +
             `this is the same as not using keys.`,
+            getRawBindingAttr(el, 'key'),
             true /* tip */
           )
         }
@@ -367,7 +407,8 @@ export function processFor (el: ASTElement) {
       extend(el, res)
     } else if (process.env.NODE_ENV !== 'production') {
       warn(
-        `Invalid v-for expression: ${exp}`
+        `Invalid v-for expression: ${exp}`,
+        el.rawAttrsMap['v-for']
       )
     }
   }
@@ -428,7 +469,8 @@ function processIfConditions (el, parent) {
   } else if (process.env.NODE_ENV !== 'production') {
     warn(
       `v-${el.elseif ? ('else-if="' + el.elseif + '"') : 'else'} ` +
-      `used on element <${el.tag}> without corresponding v-if.`
+      `used on element <${el.tag}> without corresponding v-if.`,
+      el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else']
     )
   }
 }
@@ -442,7 +484,8 @@ function findPrevElement (children: Array<any>): ASTElement | void {
       if (process.env.NODE_ENV !== 'production' && children[i].text !== ' ') {
         warn(
           `text "${children[i].text.trim()}" between v-if and v-else(-if) ` +
-          `will be ignored.`
+          `will be ignored.`,
+          children[i]
         )
       }
       children.pop()
@@ -471,7 +514,8 @@ function processSlot (el) {
       warn(
         `\`key\` does not work on <slot> because slots are abstract outlets ` +
         `and can possibly expand into multiple elements. ` +
-        `Use the key on a wrapping element instead.`
+        `Use the key on a wrapping element instead.`,
+        getRawBindingAttr(el, 'key')
       )
     }
   } else {
@@ -485,6 +529,7 @@ function processSlot (el) {
           `replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +
           `can also be used on plain elements in addition to <template> to ` +
           `denote scoped slots.`,
+          el.rawAttrsMap['scope'],
           true
         )
       }
@@ -496,6 +541,7 @@ function processSlot (el) {
           `Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +
           `(v-for takes higher priority). Use a wrapper <template> for the ` +
           `scoped slot to make it clearer.`,
+          el.rawAttrsMap['slot-scope'],
           true
         )
       }
@@ -507,7 +553,7 @@ function processSlot (el) {
       // preserve slot as an attribute for native shadow DOM compat
       // only for non-scoped slots.
       if (el.tag !== 'template' && !el.slotScope) {
-        addAttr(el, 'slot', slotTarget)
+        addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
       }
     }
   }
@@ -563,13 +609,21 @@ function processAttrs (el) {
             addHandler(
               el,
               `update:${camelize(name)}`,
-              syncGen
+              syncGen,
+              null,
+              false,
+              warn,
+              list[i]
             )
             if (hyphenate(name) !== camelize(name)) {
               addHandler(
                 el,
                 `update:${hyphenate(name)}`,
-                syncGen
+                syncGen,
+                null,
+                false,
+                warn,
+                list[i]
               )
             }
           }
@@ -577,13 +631,13 @@ function processAttrs (el) {
         if (isProp || (
           !el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)
         )) {
-          addProp(el, name, value)
+          addProp(el, name, value, list[i])
         } else {
-          addAttr(el, name, value)
+          addAttr(el, name, value, list[i])
         }
       } else if (onRE.test(name)) { // v-on
         name = name.replace(onRE, '')
-        addHandler(el, name, value, modifiers, false, warn)
+        addHandler(el, name, value, modifiers, false, warn, list[i])
       } else { // normal directives
         name = name.replace(dirRE, '')
         // parse arg
@@ -592,7 +646,7 @@ function processAttrs (el) {
         if (arg) {
           name = name.slice(0, -(arg.length + 1))
         }
-        addDirective(el, name, rawName, value, arg, modifiers)
+        addDirective(el, name, rawName, value, arg, modifiers, list[i])
         if (process.env.NODE_ENV !== 'production' && name === 'model') {
           checkForAliasModel(el, value)
         }
@@ -606,17 +660,18 @@ function processAttrs (el) {
             `${name}="${value}": ` +
             'Interpolation inside attributes has been removed. ' +
             'Use v-bind or the colon shorthand instead. For example, ' +
-            'instead of <div id="{{ val }}">, use <div :id="val">.'
+            'instead of <div id="{{ val }}">, use <div :id="val">.',
+            list[i]
           )
         }
       }
-      addAttr(el, name, JSON.stringify(value))
+      addAttr(el, name, JSON.stringify(value), list[i])
       // #6887 firefox doesn't update muted state if set via attribute
       // even immediately after element creation
       if (!el.component &&
           name === 'muted' &&
           platformMustUseProp(el.tag, el.attrsMap.type, name)) {
-        addProp(el, name, 'true')
+        addProp(el, name, 'true', list[i])
       }
     }
   }
@@ -649,7 +704,7 @@ function makeAttrsMap (attrs: Array<Object>): Object {
       process.env.NODE_ENV !== 'production' &&
       map[attrs[i].name] && !isIE && !isEdge
     ) {
-      warn('duplicate attribute: ' + attrs[i].name)
+      warn('duplicate attribute: ' + attrs[i].name, attrs[i])
     }
     map[attrs[i].name] = attrs[i].value
   }
@@ -696,7 +751,8 @@ function checkForAliasModel (el, value) {
         `You are binding v-model directly to a v-for iteration alias. ` +
         `This will not be able to modify the v-for source array because ` +
         `writing to the alias is like modifying a function local variable. ` +
-        `Consider using an array of objects and use v-model on an object property instead.`
+        `Consider using an array of objects and use v-model on an object property instead.`,
+        el.rawAttrsMap['v-model']
       )
     }
     _el = _el.parent

+ 1 - 1
src/platforms/web/compiler/directives/html.js

@@ -4,6 +4,6 @@ import { addProp } from 'compiler/helpers'
 
 export default function html (el: ASTElement, dir: ASTDirective) {
   if (dir.value) {
-    addProp(el, 'innerHTML', `_s(${dir.value})`)
+    addProp(el, 'innerHTML', `_s(${dir.value})`, dir)
   }
 }

+ 6 - 3
src/platforms/web/compiler/directives/model.js

@@ -28,7 +28,8 @@ export default function model (
     if (tag === 'input' && type === 'file') {
       warn(
         `<${el.tag} v-model="${value}" type="file">:\n` +
-        `File inputs are read only. Use a v-on:change listener instead.`
+        `File inputs are read only. Use a v-on:change listener instead.`,
+        el.rawAttrsMap['v-model']
       )
     }
   }
@@ -54,7 +55,8 @@ export default function model (
       `<${el.tag} v-model="${value}">: ` +
       `v-model is not supported on this element type. ` +
       'If you are working with contenteditable, it\'s recommended to ' +
-      'wrap a library dedicated for that purpose inside a custom component.'
+      'wrap a library dedicated for that purpose inside a custom component.',
+      el.rawAttrsMap['v-model']
     )
   }
 
@@ -138,7 +140,8 @@ function genDefaultModel (
       const binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value'
       warn(
         `${binding}="${value}" conflicts with v-model on the same element ` +
-        'because the latter already expands to a value binding internally'
+        'because the latter already expands to a value binding internally',
+        el.rawAttrsMap[binding]
       )
     }
   }

+ 1 - 1
src/platforms/web/compiler/directives/text.js

@@ -4,6 +4,6 @@ import { addProp } from 'compiler/helpers'
 
 export default function text (el: ASTElement, dir: ASTDirective) {
   if (dir.value) {
-    addProp(el, 'textContent', `_s(${dir.value})`)
+    addProp(el, 'textContent', `_s(${dir.value})`, dir)
   }
 }

+ 2 - 1
src/platforms/web/compiler/modules/class.js

@@ -17,7 +17,8 @@ function transformNode (el: ASTElement, options: CompilerOptions) {
         `class="${staticClass}": ` +
         'Interpolation inside attributes has been removed. ' +
         'Use v-bind or the colon shorthand instead. For example, ' +
-        'instead of <div class="{{ val }}">, use <div :class="val">.'
+        'instead of <div class="{{ val }}">, use <div :class="val">.',
+        el.rawAttrsMap['class']
       )
     }
   }

+ 2 - 1
src/platforms/web/compiler/modules/style.js

@@ -20,7 +20,8 @@ function transformNode (el: ASTElement, options: CompilerOptions) {
           `style="${staticStyle}": ` +
           'Interpolation inside attributes has been removed. ' +
           'Use v-bind or the colon shorthand instead. For example, ' +
-          'instead of <div style="{{ val }}">, use <div :style="val">.'
+          'instead of <div style="{{ val }}">, use <div :style="val">.',
+          el.rawAttrsMap['style']
         )
       }
     }

+ 2 - 1
src/platforms/weex/compiler/modules/class.js

@@ -20,7 +20,8 @@ function transformNode (el: ASTElement, options: CompilerOptions) {
     warn(
       `class="${staticClass}": ` +
       'Interpolation inside attributes has been deprecated. ' +
-      'Use v-bind or the colon shorthand instead.'
+      'Use v-bind or the colon shorthand instead.',
+      el.rawAttrsMap['class']
     )
   }
   if (!dynamic && classResult) {

+ 5 - 0
src/platforms/weex/compiler/modules/props.js

@@ -22,6 +22,11 @@ function transformNode (el: ASTElement) {
           el.attrsMap[realName] = el.attrsMap[attr.name]
           delete el.attrsMap[attr.name]
         }
+        if (el.rawAttrsMap && el.rawAttrsMap[attr.name]) {
+          el.rawAttrsMap[realName] = el.rawAttrsMap[attr.name]
+          // $flow-disable-line
+          delete el.rawAttrsMap[attr.name]
+        }
         attr.name = realName
       }
     })

+ 2 - 1
src/platforms/weex/compiler/modules/style.js

@@ -23,7 +23,8 @@ function transformNode (el: ASTElement, options: CompilerOptions) {
     warn(
       `style="${String(staticStyle)}": ` +
       'Interpolation inside attributes has been deprecated. ' +
-      'Use v-bind or the colon shorthand instead.'
+      'Use v-bind or the colon shorthand instead.',
+      el.rawAttrsMap['style']
     )
   }
   if (!dynamic && styleResult) {

+ 3 - 5
src/server/optimizing-compiler/modules.js

@@ -19,8 +19,6 @@ import {
 import type { StringSegment } from './codegen'
 import type { CodegenState } from 'compiler/codegen/index'
 
-type Attr = { name: string; value: string };
-
 const plainStringRE = /^"(?:[^"\\]|\\.)*"$|^'(?:[^'\\]|\\.)*'$/
 
 // let the model AST transform translate v-model into appropriate
@@ -42,14 +40,14 @@ export function applyModelTransform (el: ASTElement, state: CodegenState) {
 }
 
 export function genAttrSegments (
-  attrs: Array<Attr>
+  attrs: Array<ASTAttr>
 ): Array<StringSegment> {
   return attrs.map(({ name, value }) => genAttrSegment(name, value))
 }
 
 export function genDOMPropSegments (
-  props: Array<Attr>,
-  attrs: ?Array<Attr>
+  props: Array<ASTAttr>,
+  attrs: ?Array<ASTAttr>
 ): Array<StringSegment> {
   const segments = []
   props.forEach(({ name, value }) => {

+ 1 - 0
src/server/optimizing-compiler/optimizer.js

@@ -84,6 +84,7 @@ function optimizeSiblings (el) {
         tag: 'template',
         attrsList: [],
         attrsMap: {},
+        rawAttrsMap: {},
         children: currentOptimizableGroup,
         ssrOptimizability: optimizability.FULL
       })

+ 24 - 9
src/sfc/parser.js

@@ -8,11 +8,6 @@ const splitRE = /\r?\n/g
 const replaceRE = /./g
 const isSpecialTag = makeMap('script,style,template', true)
 
-type Attribute = {
-  name: string,
-  value: string
-};
-
 /**
  * Parse a single-file component (*.vue) file into an SFC Descriptor Object.
  */
@@ -24,14 +19,32 @@ export function parseComponent (
     template: null,
     script: null,
     styles: [],
-    customBlocks: []
+    customBlocks: [],
+    errors: []
   }
   let depth = 0
   let currentBlock: ?SFCBlock = null
 
+  let warn = msg => {
+    sfc.errors.push(msg)
+  }
+
+  if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
+    warn = (msg, range) => {
+      const data: WarningMessage = { msg }
+      if (range.start != null) {
+        data.start = range.start
+      }
+      if (range.end != null) {
+        data.end = range.end
+      }
+      sfc.errors.push(data)
+    }
+  }
+
   function start (
     tag: string,
-    attrs: Array<Attribute>,
+    attrs: Array<ASTAttr>,
     unary: boolean,
     start: number,
     end: number
@@ -62,7 +75,7 @@ export function parseComponent (
     }
   }
 
-  function checkAttrs (block: SFCBlock, attrs: Array<Attribute>) {
+  function checkAttrs (block: SFCBlock, attrs: Array<ASTAttr>) {
     for (let i = 0; i < attrs.length; i++) {
       const attr = attrs[i]
       if (attr.name === 'lang') {
@@ -111,8 +124,10 @@ export function parseComponent (
   }
 
   parseHTML(content, {
+    warn,
     start,
-    end
+    end,
+    outputSourceRange: options.outputSourceRange
   })
 
   return sfc

+ 21 - 0
test/unit/modules/compiler/compiler-options.spec.js

@@ -127,4 +127,25 @@ describe('compile options', () => {
     expect(compiled.errors[0]).toContain('Raw expression: v-if="a----"')
     expect(compiled.errors[1]).toContain('Raw expression: {{ b++++ }}')
   })
+
+  it('should collect errors with source range', () => {
+    let compiled = compile('hello', { outputSourceRange: true })
+    expect(compiled.errors.length).toBe(1)
+    expect(compiled.errors[0].start).toBe(0)
+    expect(compiled.errors[0].end).toBeUndefined()
+
+    compiled = compile('<div v-if="a----">{{ b++++ }}</div>', { outputSourceRange: true })
+    expect(compiled.errors.length).toBe(2)
+    expect(compiled.errors[0].start).toBe(5)
+    expect(compiled.errors[0].end).toBe(17)
+    expect(compiled.errors[1].start).toBe(18)
+    expect(compiled.errors[1].end).toBe(29)
+  })
+
+  it('should collect source range for binding keys', () => {
+    const compiled = compile('<div><slot v-bind:key="key" /></div>', { outputSourceRange: true })
+    expect(compiled.errors.length).toBe(1)
+    expect(compiled.errors[0].start).toBe(11)
+    expect(compiled.errors[0].end).toBe(27)
+  })
 })

+ 6 - 0
test/unit/modules/sfc/sfc-parser.spec.js

@@ -202,4 +202,10 @@ describe('Single File Component parser', () => {
     const res = parseComponent(`<template>hi</`)
     expect(res.template.content).toBe('hi')
   })
+
+  it('should collect errors with source range', () => {
+    const res = parseComponent(`<template>hi</`, { outputSourceRange: true })
+    expect(res.errors.length).toBe(1)
+    expect(res.errors[0].start).toBe(0)
+  })
 })