Repository: vuejs/eslint-plugin-vue
Pull request #275 (2017-12-02 09:44:25)

[Update] Make `vue/html-quotes` fixable
This PR makes `vue/html-quotes` rule fixable. If the quotes exist in the attribute value, the rule replaces those quotes by HTML entities.

This PR includes #274 to address invalid EOF.


Patch:
diff --git a/README.md b/README.md
index e9a3a7a6..4ba6f752 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,12 @@ Please try it and report any issues that you might have encountered.
 
 If you want to check previous releases [go here](https://github.com/vuejs/eslint-plugin-vue/releases).
 
+## :art: Playground on the Web
+
+You can try this plugin on the Web.
+
+- https://mysticatea.github.io/vue-eslint-demo/
+
 ## :grey_exclamation: Requirements
 
 - [ESLint](http://eslint.org/) `>=3.18.0`.
diff --git a/lib/rules/html-end-tags.js b/lib/rules/html-end-tags.js
index a4c05eec..922f34d8 100644
--- a/lib/rules/html-end-tags.js
+++ b/lib/rules/html-end-tags.js
@@ -22,22 +22,19 @@ const utils = require('../utils')
  * @returns {Object} AST event handlers.
  */
 function create (context) {
+  let hasInvalidEOF = false
+
   return utils.defineTemplateBodyVisitor(context, {
     VElement (node) {
+      if (hasInvalidEOF) {
+        return
+      }
+
       const name = node.name
       const isVoid = utils.isHtmlVoidElementName(name)
       const isSelfClosing = node.startTag.selfClosing
       const hasEndTag = node.endTag != null
 
-      if (isVoid && hasEndTag) {
-        context.report({
-          node: node.endTag,
-          loc: node.endTag.loc,
-          message: "'<{{name}}>' should not have end tag.",
-          data: { name },
-          fix: (fixer) => fixer.remove(node.endTag)
-        })
-      }
       if (!isVoid && !hasEndTag && !isSelfClosing) {
         context.report({
           node: node.startTag,
@@ -48,6 +45,10 @@ function create (context) {
         })
       }
     }
+  }, {
+    Program (node) {
+      hasInvalidEOF = utils.hasInvalidEOF(node)
+    }
   })
 }
 
diff --git a/lib/rules/html-quotes.js b/lib/rules/html-quotes.js
index 9326420b..b0156323 100644
--- a/lib/rules/html-quotes.js
+++ b/lib/rules/html-quotes.js
@@ -26,20 +26,38 @@ function create (context) {
   const double = context.options[0] !== 'single'
   const quoteChar = double ? '"' : "'"
   const quoteName = double ? 'double quotes' : 'single quotes'
+  const quotePattern = double ? /"/g : /'/g
+  const quoteEscaped = double ? '&quot;' : '&apos;'
+  let hasInvalidEOF
 
   return utils.defineTemplateBodyVisitor(context, {
     'VAttribute[value!=null]' (node) {
+      if (hasInvalidEOF) {
+        return
+      }
+
       const text = sourceCode.getText(node.value)
       const firstChar = text[0]
+
       if (firstChar !== quoteChar) {
         context.report({
           node: node.value,
           loc: node.value.loc,
           message: 'Expected to be enclosed by {{kind}}.',
-          data: { kind: quoteName }
+          data: { kind: quoteName },
+          fix (fixer) {
+            const contentText = (firstChar === "'" || firstChar === '"') ? text.slice(1, -1) : text
+            const replacement = quoteChar + contentText.replace(quotePattern, quoteEscaped) + quoteChar
+
+            return fixer.replaceText(node.value, replacement)
+          }
         })
       }
     }
+  }, {
+    Program (node) {
+      hasInvalidEOF = utils.hasInvalidEOF(node)
+    }
   })
 }
 
@@ -54,7 +72,7 @@ module.exports = {
       description: 'enforce quotes style of HTML attributes',
       category: 'recommended'
     },
-    fixable: false,
+    fixable: 'code',
     schema: [
       { enum: ['double', 'single'] }
     ]
diff --git a/lib/rules/html-self-closing.js b/lib/rules/html-self-closing.js
index f311a63f..23263e2b 100644
--- a/lib/rules/html-self-closing.js
+++ b/lib/rules/html-self-closing.js
@@ -88,9 +88,14 @@ function isEmpty (node, sourceCode) {
 function create (context) {
   const sourceCode = context.getSourceCode()
   const options = parseOptions(context.options[0])
+  let hasInvalidEOF = false
 
   return utils.defineTemplateBodyVisitor(context, {
     'VElement' (node) {
+      if (hasInvalidEOF) {
+        return
+      }
+
       const elementType = getElementType(node)
       const mode = options[elementType]
 
@@ -131,6 +136,10 @@ function create (context) {
         })
       }
     }
+  }, {
+    Program (node) {
+      hasInvalidEOF = utils.hasInvalidEOF(node)
+    }
   })
 }
 
diff --git a/lib/rules/valid-v-on.js b/lib/rules/valid-v-on.js
index b9c4f9ca..9986f561 100644
--- a/lib/rules/valid-v-on.js
+++ b/lib/rules/valid-v-on.js
@@ -18,7 +18,7 @@ const utils = require('../utils')
 const VALID_MODIFIERS = new Set([
   'stop', 'prevent', 'capture', 'self', 'ctrl', 'shift', 'alt', 'meta',
   'native', 'once', 'left', 'right', 'middle', 'passive', 'esc', 'tab',
-  'enter', 'space', 'up', 'left', 'right', 'down', 'delete'
+  'enter', 'space', 'up', 'left', 'right', 'down', 'delete', 'exact'
 ])
 const VERB_MODIFIERS = new Set([
   'stop', 'prevent'
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 635707b6..77c9a72b 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -601,5 +601,18 @@ module.exports = {
    */
   isSingleLine (node) {
     return node.loc.start.line === node.loc.end.line
+  },
+
+  /**
+   * Check whether the templateBody of the program has invalid EOF or not.
+   * @param {Program} node The program node to check.
+   * @returns {boolean} `true` if it has invalid EOF.
+   */
+  hasInvalidEOF (node) {
+    const body = node.templateBody
+    if (body == null || body.errors == null) {
+      return
+    }
+    return body.errors.some(error => typeof error.code === 'string' && error.code.startsWith('eof-'))
   }
 }


Test patch:
diff --git a/tests/lib/rules/html-end-tags.js b/tests/lib/rules/html-end-tags.js
index 16cb8fe2..66aad36d 100644
--- a/tests/lib/rules/html-end-tags.js
+++ b/tests/lib/rules/html-end-tags.js
@@ -54,27 +54,21 @@ tester.run('html-end-tags', rule, {
     {
       filename: 'test.vue',
       code: '<template><div><div/></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div a="b>test</div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><!--</div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><svg><![CDATA[test</svg></div></template>'
     }
   ],
   invalid: [
-    // {
-    //   filename: 'test.vue',
-    //   code: '<template><div><hr></hr></div></template>',
-    //   output: '<template><div><hr></template>',
-    //   errors: ["'<hr>' should not have end tag."]
-    // },
-    // {
-    //   filename: 'test.vue',
-    //   code: '<template><div><img></img></div></template>',
-    //   output: '<template><div><img></template>',
-    //   errors: ["'<img>' should not have end tag."]
-    // },
-    // {
-    //   filename: 'test.vue',
-    //   code: '<template><div><input></input></div></template>',
-    //   output: '<template><div><input></template>',
-    //   errors: ["'<input>' should not have end tag."]
-    // },
     {
       filename: 'test.vue',
       code: '<template><div><div></div></template>',
diff --git a/tests/lib/rules/html-quotes.js b/tests/lib/rules/html-quotes.js
index 937bd1d3..a7a65752 100644
--- a/tests/lib/rules/html-quotes.js
+++ b/tests/lib/rules/html-quotes.js
@@ -54,91 +54,116 @@ tester.run('html-quotes', rule, {
       filename: 'test.vue',
       code: "<template><div :class='foo'></div></template>",
       options: ['single']
+    },
+
+    // Invalid EOF
+    {
+      code: '<template><div class="foo></div></template>',
+      options: ['single']
+    },
+    {
+      code: '<template><div class=\'foo></div></template>',
+      options: ['double']
     }
   ],
   invalid: [
     {
       filename: 'test.vue',
       code: '<template><div class=foo></div></template>',
+      output: '<template><div class="foo"></div></template>',
       errors: ['Expected to be enclosed by double quotes.']
     },
     {
       filename: 'test.vue',
       code: "<template><div class='foo'></div></template>",
+      output: '<template><div class="foo"></div></template>',
       errors: ['Expected to be enclosed by double quotes.']
     },
     {
       filename: 'test.vue',
       code: '<template><div :class=foo></div></template>',
+      output: '<template><div :class="foo"></div></template>',
       errors: ['Expected to be enclosed by double quotes.']
     },
     {
       filename: 'test.vue',
       code: "<template><div :class='foo'></div></template>",
+      output: '<template><div :class="foo"></div></template>',
       errors: ['Expected to be enclosed by double quotes.']
     },
     {
       filename: 'test.vue',
       code: '<template><div :class=foo+"bar"></div></template>',
+      output: '<template><div :class="foo+&quot;bar&quot;"></div></template>',
       errors: ['Expected to be enclosed by double quotes.']
     },
     {
       filename: 'test.vue',
       code: '<template><div class=foo></div></template>',
+      output: '<template><div class="foo"></div></template>',
       options: ['double'],
       errors: ['Expected to be enclosed by double quotes.']
     },
     {
       filename: 'test.vue',
       code: "<template><div class='foo'></div></template>",
+      output: '<template><div class="foo"></div></template>',
       options: ['double'],
       errors: ['Expected to be enclosed by double quotes.']
     },
     {
       filename: 'test.vue',
       code: '<template><div :class=foo></div></template>',
+      output: '<template><div :class="foo"></div></template>',
       options: ['double'],
       errors: ['Expected to be enclosed by double quotes.']
     },
     {
       filename: 'test.vue',
       code: "<template><div :class='foo'></div></template>",
+      output: '<template><div :class="foo"></div></template>',
       options: ['double'],
       errors: ['Expected to be enclosed by double quotes.']
     },
     {
       filename: 'test.vue',
       code: '<template><div :class=foo+"bar"></div></template>',
+      output: '<template><div :class="foo+&quot;bar&quot;"></div></template>',
       options: ['double'],
       errors: ['Expected to be enclosed by double quotes.']
     },
     {
       filename: 'test.vue',
       code: '<template><div class=foo></div></template>',
+      output: '<template><div class=\'foo\'></div></template>',
       options: ['single'],
       errors: ['Expected to be enclosed by single quotes.']
     },
     {
       filename: 'test.vue',
       code: '<template><div class="foo"></div></template>',
+      output: '<template><div class=\'foo\'></div></template>',
       options: ['single'],
       errors: ['Expected to be enclosed by single quotes.']
     },
     {
       filename: 'test.vue',
       code: '<template><div :class=foo></div></template>',
+      output: '<template><div :class=\'foo\'></div></template>',
       options: ['single'],
       errors: ['Expected to be enclosed by single quotes.']
     },
     {
       filename: 'test.vue',
       code: '<template><div :class="foo"></div></template>',
+      output: '<template><div :class=\'foo\'></div></template>',
       options: ['single'],
       errors: ['Expected to be enclosed by single quotes.']
     },
     {
       filename: 'test.vue',
       code: "<template><div :class=foo+'bar'></div></template>",
+      output: "<template><div :class='foo+&apos;bar&apos;'></div></template>",
       options: ['single'],
       errors: ['Expected to be enclosed by single quotes.']
     }
diff --git a/tests/lib/rules/html-self-closing.js b/tests/lib/rules/html-self-closing.js
index 3623745c..250236ca 100644
--- a/tests/lib/rules/html-self-closing.js
+++ b/tests/lib/rules/html-self-closing.js
@@ -65,7 +65,11 @@ tester.run('html-self-closing', rule, {
       code: '<template><div><!-- comment --></div></template>',
       output: null,
       options: [{ html: { normal: 'always' }}]
-    }
+    },
+
+    // Invalid EOF
+    '<template><div a=">test</div></template>',
+    '<template><div><!--test</div></template>'
 
     // other cases are in `invalid` tests.
   ],