diff --git a/docs/rules/README.md b/docs/rules/README.md
index b23b0535c..30c0f93d1 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -159,6 +159,7 @@ For example:
| [vue/no-deprecated-slot-attribute](./no-deprecated-slot-attribute.md) | disallow deprecated `slot` attribute (in Vue.js 2.6.0+) | :wrench: |
| [vue/no-deprecated-slot-scope-attribute](./no-deprecated-slot-scope-attribute.md) | disallow deprecated `slot-scope` attribute (in Vue.js 2.6.0+) | :wrench: |
| [vue/no-empty-pattern](./no-empty-pattern.md) | disallow empty destructuring patterns | |
+| [vue/no-irregular-whitespace](./no-irregular-whitespace.md) | disallow irregular whitespace | |
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |
| [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | |
| [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | |
diff --git a/docs/rules/no-irregular-whitespace.md b/docs/rules/no-irregular-whitespace.md
new file mode 100644
index 000000000..536e0d4e8
--- /dev/null
+++ b/docs/rules/no-irregular-whitespace.md
@@ -0,0 +1,167 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-irregular-whitespace
+description: disallow irregular whitespace
+---
+# vue/no-irregular-whitespace
+> disallow irregular whitespace
+
+`vue/no-irregular-whitespace` rule is aimed at catching invalid whitespace that is not a normal tab and space. Some of these characters may cause issues in modern browsers and others will be a debugging issue to spot.
+`vue/no-irregular-whitespace` rule is the similar rule as core [no-irregular-whitespace] rule but it applies to the source code in .vue.
+
+
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+
+
+## :wrench: Options
+
+```js
+{
+ "vue/no-irregular-whitespace": ["error", {
+ "skipStrings": true,
+ "skipComments": false,
+ "skipRegExps": false,
+ "skipTemplates": false,
+ "skipHTMLAttributeValues": false,
+ "skipHTMLTextContents": false
+ }]
+}
+```
+
+- `skipStrings`: if `true`, allows any whitespace characters in string literals. default `true`
+- `skipComments`: if `true`, allows any whitespace characters in comments. default `false`
+- `skipRegExps`: if `true`, allows any whitespace characters in regular expression literals. default `false`
+- `skipTemplates`: if `true`, allows any whitespace characters in template literals. default `false`
+- `skipHTMLAttributeValues`: if `true`, allows any whitespace characters in HTML attribute values. default `false`
+- `skipHTMLTextContents`: if `true`, allows any whitespace characters in HTML text contents. default `false`
+
+### `"skipStrings": true` (default)
+
+
+
+```vue
+
+```
+
+
+
+### `"skipStrings": false`
+
+
+
+```vue
+
+```
+
+
+
+### `"skipComments": true`
+
+
+
+```vue
+
+
+
+
+
+```
+
+
+
+### `"skipRegExps": true`
+
+
+
+```vue
+
+```
+
+
+
+### `"skipTemplates": true`
+
+
+
+```vue
+
+```
+
+
+
+### `"skipHTMLAttributeValues": true`
+
+
+
+```vue
+
+
+
+
+
+```
+
+
+
+### `"skipHTMLTextContents": true`
+
+
+
+```vue
+
+
+
+
+
+```
+
+
+
+## :books: Further reading
+
+- [no-irregular-whitespace]
+
+[no-irregular-whitespace]: https://eslint.org/docs/rules/no-irregular-whitespace
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-irregular-whitespace.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-irregular-whitespace.js)
diff --git a/lib/index.js b/lib/index.js
index e31140ade..d1b9bd9ef 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -45,6 +45,7 @@ module.exports = {
'no-dupe-keys': require('./rules/no-dupe-keys'),
'no-duplicate-attributes': require('./rules/no-duplicate-attributes'),
'no-empty-pattern': require('./rules/no-empty-pattern'),
+ 'no-irregular-whitespace': require('./rules/no-irregular-whitespace'),
'no-multi-spaces': require('./rules/no-multi-spaces'),
'no-parsing-error': require('./rules/no-parsing-error'),
'no-reserved-component-names': require('./rules/no-reserved-component-names'),
diff --git a/lib/rules/no-irregular-whitespace.js b/lib/rules/no-irregular-whitespace.js
new file mode 100644
index 000000000..0be04d21a
--- /dev/null
+++ b/lib/rules/no-irregular-whitespace.js
@@ -0,0 +1,234 @@
+/**
+ * @author Yosuke Ota
+ * @fileoverview Rule to disalow whitespace that is not a tab or space, whitespace inside strings and comments are allowed
+ */
+
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Constants
+// ------------------------------------------------------------------------------
+
+const ALL_IRREGULARS = /[\f\v\u0085\ufeff\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000\u2028\u2029]/u
+const IRREGULAR_WHITESPACE = /[\f\v\u0085\ufeff\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000]+/mgu
+const IRREGULAR_LINE_TERMINATORS = /[\u2028\u2029]/mgu
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ type: 'problem',
+
+ docs: {
+ description: 'disallow irregular whitespace',
+ category: undefined,
+ url: 'https://eslint.vuejs.org/rules/no-irregular-whitespace.html'
+ },
+
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ skipComments: {
+ type: 'boolean',
+ default: false
+ },
+ skipStrings: {
+ type: 'boolean',
+ default: true
+ },
+ skipTemplates: {
+ type: 'boolean',
+ default: false
+ },
+ skipRegExps: {
+ type: 'boolean',
+ default: false
+ },
+ skipHTMLAttributeValues: {
+ type: 'boolean',
+ default: false
+ },
+ skipHTMLTextContents: {
+ type: 'boolean',
+ default: false
+ }
+ },
+ additionalProperties: false
+ }
+ ],
+ messages: {
+ disallow: 'Irregular whitespace not allowed.'
+ }
+ },
+
+ create (context) {
+ // Module store of error indexes that we have found
+ let errorIndexes = []
+
+ // Lookup the `skipComments` option, which defaults to `false`.
+ const options = context.options[0] || {}
+ const skipComments = !!options.skipComments
+ const skipStrings = options.skipStrings !== false
+ const skipRegExps = !!options.skipRegExps
+ const skipTemplates = !!options.skipTemplates
+ const skipHTMLAttributeValues = !!options.skipHTMLAttributeValues
+ const skipHTMLTextContents = !!options.skipHTMLTextContents
+
+ const sourceCode = context.getSourceCode()
+
+ /**
+ * Removes errors that occur inside a string node
+ * @param {ASTNode} node to check for matching errors.
+ * @returns {void}
+ * @private
+ */
+ function removeWhitespaceError (node) {
+ const [startIndex, endIndex] = node.range
+
+ errorIndexes = errorIndexes
+ .filter(errorIndex => errorIndex < startIndex || endIndex <= errorIndex)
+ }
+
+ /**
+ * Checks literal nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors
+ * @param {ASTNode} node to check for matching errors.
+ * @returns {void}
+ * @private
+ */
+ function removeInvalidNodeErrorsInLiteral (node) {
+ const shouldCheckStrings = skipStrings && (typeof node.value === 'string')
+ const shouldCheckRegExps = skipRegExps && Boolean(node.regex)
+
+ if (shouldCheckStrings || shouldCheckRegExps) {
+ // If we have irregular characters remove them from the errors list
+ if (ALL_IRREGULARS.test(node.raw)) {
+ removeWhitespaceError(node)
+ }
+ }
+ }
+
+ /**
+ * Checks template string literal nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors
+ * @param {ASTNode} node to check for matching errors.
+ * @returns {void}
+ * @private
+ */
+ function removeInvalidNodeErrorsInTemplateLiteral (node) {
+ if (ALL_IRREGULARS.test(node.value.raw)) {
+ removeWhitespaceError(node)
+ }
+ }
+
+ /**
+ * Checks HTML attribute value nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors
+ * @param {ASTNode} node to check for matching errors.
+ * @returns {void}
+ * @private
+ */
+ function removeInvalidNodeErrorsInHTMLAttributeValue (node) {
+ if (ALL_IRREGULARS.test(sourceCode.getText(node))) {
+ removeWhitespaceError(node)
+ }
+ }
+
+ /**
+ * Checks HTML text content nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors
+ * @param {ASTNode} node to check for matching errors.
+ * @returns {void}
+ * @private
+ */
+ function removeInvalidNodeErrorsInHTMLTextContent (node) {
+ if (ALL_IRREGULARS.test(sourceCode.getText(node))) {
+ removeWhitespaceError(node)
+ }
+ }
+
+ /**
+ * Checks comment nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors
+ * @param {ASTNode} node to check for matching errors.
+ * @returns {void}
+ * @private
+ */
+ function removeInvalidNodeErrorsInComment (node) {
+ if (ALL_IRREGULARS.test(node.value)) {
+ removeWhitespaceError(node)
+ }
+ }
+
+ /**
+ * Checks the program source for irregular whitespaces and irregular line terminators
+ * @returns {void}
+ * @private
+ */
+ function checkForIrregularWhitespace () {
+ const source = sourceCode.getText()
+ let match
+ while ((match = IRREGULAR_WHITESPACE.exec(source)) !== null) {
+ errorIndexes.push(match.index)
+ }
+ while ((match = IRREGULAR_LINE_TERMINATORS.exec(source)) !== null) {
+ errorIndexes.push(match.index)
+ }
+ }
+
+ checkForIrregularWhitespace()
+
+ if (!errorIndexes.length) {
+ return {}
+ }
+ const bodyVisitor = utils.defineTemplateBodyVisitor(context,
+ {
+ ...(skipHTMLAttributeValues ? { 'VAttribute[directive=false] > VLiteral': removeInvalidNodeErrorsInHTMLAttributeValue } : {}),
+ ...(skipHTMLTextContents ? { VText: removeInvalidNodeErrorsInHTMLTextContent } : {}),
+
+ // inline scripts
+ Literal: removeInvalidNodeErrorsInLiteral,
+ ...(skipTemplates ? { TemplateElement: removeInvalidNodeErrorsInTemplateLiteral } : {})
+ }
+ )
+ return {
+ ...bodyVisitor,
+ Literal: removeInvalidNodeErrorsInLiteral,
+ ...(skipTemplates ? { TemplateElement: removeInvalidNodeErrorsInTemplateLiteral } : {}),
+ 'Program:exit' (node) {
+ if (bodyVisitor['Program:exit']) {
+ bodyVisitor['Program:exit'](node)
+ }
+ const templateBody = node.templateBody
+ if (skipComments) {
+ // First strip errors occurring in comment nodes.
+ sourceCode.getAllComments().forEach(removeInvalidNodeErrorsInComment)
+ if (templateBody) {
+ templateBody.comments.forEach(removeInvalidNodeErrorsInComment)
+ }
+ }
+
+ // Removes errors that occur outside script and template
+ const [scriptStart, scriptEnd] = node.range
+ const [templateStart, templateEnd] = templateBody ? templateBody.range : [0, 0]
+ errorIndexes = errorIndexes
+ .filter(errorIndex =>
+ (scriptStart <= errorIndex && errorIndex < scriptEnd) ||
+ (templateStart <= errorIndex && errorIndex < templateEnd)
+ )
+
+ // If we have any errors remaining report on them
+ errorIndexes.forEach(errorIndex => {
+ context.report({
+ loc: sourceCode.getLocFromIndex(errorIndex),
+ messageId: 'disallow'
+ })
+ })
+ }
+ }
+ }
+}
diff --git a/tests/lib/rules/no-irregular-whitespace.js b/tests/lib/rules/no-irregular-whitespace.js
new file mode 100644
index 000000000..3814c3f23
--- /dev/null
+++ b/tests/lib/rules/no-irregular-whitespace.js
@@ -0,0 +1,271 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-irregular-whitespace')
+
+const tester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: { ecmaVersion: 2018 }
+})
+
+const IRREGULAR_WHITESPACES = '\f\v\u0085\ufeff\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000'.split('')
+const IRREGULAR_LINE_TERMINATORS = '\u2028\u2029'.split('')
+const ALL_IRREGULAR_WHITESPACES = [].concat(IRREGULAR_WHITESPACES, IRREGULAR_LINE_TERMINATORS)
+const ALL_IRREGULAR_WHITESPACE_CODES = ALL_IRREGULAR_WHITESPACES.map(s => ('000' + s.charCodeAt(0).toString(16)).slice(-4))
+
+tester.run('no-irregular-whitespace', rule, {
+ valid: [
+ 'var a = \t\r\n b',
+ ' \t\r\n s \t\r\n
',
+ // escapes
+ ...ALL_IRREGULAR_WHITESPACE_CODES.map(s => `/\\u${s}/+'\\u${s}'`),
+ // html escapes
+ ...ALL_IRREGULAR_WHITESPACE_CODES
+ .map(s => `${s}s${s}
`),
+ // strings
+ ...IRREGULAR_WHITESPACES.map(s => `'${s}'`),
+ ...IRREGULAR_LINE_TERMINATORS.map(s => `'\\${s}'`), // multiline string
+ ...IRREGULAR_WHITESPACES.map(s => `{{ '${s}' }}`),
+ // comments
+ ...IRREGULAR_WHITESPACES.map(s => ({ code: `//${s}`, options: [{ skipComments: true }] })),
+ ...ALL_IRREGULAR_WHITESPACES.map(s => ({ code: `/*${s}*/`, options: [{ skipComments: true }] })),
+ ...IRREGULAR_WHITESPACES.map(s => ({ code: `{{ i//${s}\n }}
`, options: [{ skipComments: true }] })),
+ ...ALL_IRREGULAR_WHITESPACES.map(s => ({ code: `{{ i/*${s}*/ }}
`, options: [{ skipComments: true }] })),
+ // regexps
+ ...IRREGULAR_WHITESPACES.map(s => ({ code: `/${s}/`, options: [{ skipRegExps: true }] })),
+ ...IRREGULAR_WHITESPACES.map(s => ({ code: `{{ /${s}/ }}
`, options: [{ skipRegExps: true }] })),
+ // templates
+ ...ALL_IRREGULAR_WHITESPACES.map(s => ({ code: `\`${s}\``, options: [{ skipTemplates: true }] })),
+ ...ALL_IRREGULAR_WHITESPACES.map(s => ({ code: `{{ \`${s}\` }}
`, options: [{ skipTemplates: true }] })),
+ // attribute values
+ ...ALL_IRREGULAR_WHITESPACES.map(s => ({ code: ``, options: [{ skipHTMLAttributeValues: true }] })),
+ // text contents
+ ...ALL_IRREGULAR_WHITESPACES.map(s => ({ code: `${s}
`, options: [{ skipHTMLTextContents: true }] })),
+ // outside
+ `\u3000\u3000\u3000\u3000\u3000\u3000`
+ ],
+ invalid: [
+ {
+ code: `var any \u000B = 'thing';`,
+ errors: [
+ {
+ message: 'Irregular whitespace not allowed.',
+ line: 1,
+ column: 9
+ }
+ ]
+ },
+ {
+ code: `
+
+ \u3000
+
+ \u3000
+
+ \u3000
+
+ `,
+ errors: [
+ {
+ message: 'Irregular whitespace not allowed.',
+ line: 3,
+ column: 9
+ },
+ {
+ message: 'Irregular whitespace not allowed.',
+ line: 5,
+ column: 11
+ },
+ {
+ message: 'Irregular whitespace not allowed.',
+ line: 6,
+ column: 17
+ },
+ {
+ message: 'Irregular whitespace not allowed.',
+ line: 7,
+ column: 17
+ },
+ {
+ message: 'Irregular whitespace not allowed.',
+ line: 7,
+ column: 23
+ },
+ {
+ message: 'Irregular whitespace not allowed.',
+ line: 8,
+ column: 11
+ },
+ {
+ message: 'Irregular whitespace not allowed.',
+ line: 9,
+ column: 9
+ },
+ {
+ message: 'Irregular whitespace not allowed.',
+ line: 11,
+ column: 9
+ },
+ {
+ message: 'Irregular whitespace not allowed.',
+ line: 12,
+ column: 9
+ },
+ {
+ message: 'Irregular whitespace not allowed.',
+ line: 15,
+ column: 15
+ }
+ ]
+ },
+ // strings
+ ...IRREGULAR_WHITESPACES.map(s => ({
+ code: `'${s}'`,
+ options: [{ skipStrings: false }],
+ errors: [{
+ message: 'Irregular whitespace not allowed.',
+ line: 1,
+ column: 2
+ }]
+ })),
+ ...IRREGULAR_LINE_TERMINATORS.map(s => ({
+ code: `'\\${s}'`,
+ options: [{ skipStrings: false }],
+ errors: [{
+ message: 'Irregular whitespace not allowed.',
+ line: 1,
+ column: 3
+ }]
+ })),
+ ...IRREGULAR_WHITESPACES.map(s => ({
+ code: `{{ '${s}' }}`,
+ options: [{ skipStrings: false }],
+ errors: [{
+ message: 'Irregular whitespace not allowed.',
+ line: 1,
+ column: 15
+ }]
+ })),
+ // comments
+ ...IRREGULAR_WHITESPACES.map(s => ({
+ code: `//${s}`,
+ errors: [{
+ message: 'Irregular whitespace not allowed.',
+ line: 1,
+ column: 3
+ }]
+ })),
+ ...ALL_IRREGULAR_WHITESPACES.map(s => ({
+ code: `/*${s}*/`,
+ errors: [{
+ message: 'Irregular whitespace not allowed.',
+ line: 1,
+ column: 3
+ }]
+ })),
+ ...IRREGULAR_WHITESPACES.map(s => ({
+ code: `{{ i//${s}\n }}
`,
+ errors: [{
+ message: 'Irregular whitespace not allowed.',
+ line: 1,
+ column: 22
+ }]
+ })),
+ ...ALL_IRREGULAR_WHITESPACES.map(s => ({
+ code: `{{ i/*${s}*/ }}
`,
+ errors: [{
+ message: 'Irregular whitespace not allowed.',
+ line: 1,
+ column: 22
+ }]
+ })),
+ // regexps
+ ...IRREGULAR_WHITESPACES.map(s => ({
+ code: `/${s}/`,
+ errors: [{
+ message: 'Irregular whitespace not allowed.',
+ line: 1,
+ column: 2
+ }]
+ })),
+ ...IRREGULAR_WHITESPACES.map(s => ({
+ code: `{{ /${s}/ }}
`,
+ errors: [{
+ message: 'Irregular whitespace not allowed.',
+ line: 1,
+ column: 20
+ }]
+ })),
+ // templates
+ ...ALL_IRREGULAR_WHITESPACES.map(s => ({
+ code: `\`${s}\``,
+ errors: [{
+ message: 'Irregular whitespace not allowed.',
+ line: 1,
+ column: 2
+ }]
+ })),
+ ...ALL_IRREGULAR_WHITESPACES.map(s => ({
+ code: `{{ \`${s}\` }}
`,
+ errors: [{
+ message: 'Irregular whitespace not allowed.',
+ line: 1,
+ column: 20
+ }]
+ })),
+ // attribute values
+ ...ALL_IRREGULAR_WHITESPACES.map(s => ({
+ code: ``,
+ errors: [{
+ message: 'Irregular whitespace not allowed.',
+ line: 1,
+ column: 22
+ }]
+ })),
+ // text contents
+ ...ALL_IRREGULAR_WHITESPACES.map(s => ({
+ code: `${s}
`,
+ errors: [{
+ message: 'Irregular whitespace not allowed.',
+ line: 1,
+ column: 16
+ }]
+ })),
+ // options
+ {
+ code: `
+
+
+
+ `,
+ options: [{ skipComments: true, skipStrings: true, skipTemplates: true, skipRegExps: true, skipHTMLAttributeValues: true, skipHTMLTextContents: true }],
+ errors: [{
+ message: 'Irregular whitespace not allowed.',
+ line: 6,
+ column: 17
+ }]
+ }
+ ]
+})