From a4ad23fdf184f3ded69a76d8d49a81f5471c9a72 Mon Sep 17 00:00:00 2001 From: ota Date: Sun, 22 Sep 2019 11:13:08 +0900 Subject: [PATCH] Add vue/no-irregular-whitespace rule --- docs/rules/README.md | 1 + docs/rules/no-irregular-whitespace.md | 167 +++++++++++++ lib/index.js | 1 + lib/rules/no-irregular-whitespace.js | 234 ++++++++++++++++++ tests/lib/rules/no-irregular-whitespace.js | 271 +++++++++++++++++++++ 5 files changed, 674 insertions(+) create mode 100644 docs/rules/no-irregular-whitespace.md create mode 100644 lib/rules/no-irregular-whitespace.js create mode 100644 tests/lib/rules/no-irregular-whitespace.js diff --git a/docs/rules/README.md b/docs/rules/README.md index 97117319f..7e4ea230c 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -154,6 +154,7 @@ For example: | [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: | | [vue/no-deprecated-scope-attribute](./no-deprecated-scope-attribute.md) | disallow deprecated `scope` attribute (in Vue.js 2.5.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-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | | | [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: | | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | | diff --git a/docs/rules/no-irregular-whitespace.md b/docs/rules/no-irregular-whitespace.md new file mode 100644 index 000000000..365360ce5 --- /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, + "skipComskipHTMLTextContentsments": 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 6c727f241..54a077a51 100644 --- a/lib/index.js +++ b/lib/index.js @@ -40,6 +40,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-keys': require('./rules/no-reserved-keys'), 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', + '', + // escapes + ...ALL_IRREGULAR_WHITESPACE_CODES.map(s => `/\\u${s}/+'\\u${s}'`), + // html escapes + ...ALL_IRREGULAR_WHITESPACE_CODES + .map(s => ``), + // strings + ...IRREGULAR_WHITESPACES.map(s => `'${s}'`), + ...IRREGULAR_LINE_TERMINATORS.map(s => `'\\${s}'`), // multiline string + ...IRREGULAR_WHITESPACES.map(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: ``, options: [{ skipComments: true }] })), + ...ALL_IRREGULAR_WHITESPACES.map(s => ({ code: ``, options: [{ skipComments: true }] })), + // regexps + ...IRREGULAR_WHITESPACES.map(s => ({ code: `/${s}/`, options: [{ skipRegExps: true }] })), + ...IRREGULAR_WHITESPACES.map(s => ({ code: ``, options: [{ skipRegExps: true }] })), + // templates + ...ALL_IRREGULAR_WHITESPACES.map(s => ({ code: `\`${s}\``, options: [{ skipTemplates: true }] })), + ...ALL_IRREGULAR_WHITESPACES.map(s => ({ code: ``, options: [{ skipTemplates: true }] })), + // attribute values + ...ALL_IRREGULAR_WHITESPACES.map(s => ({ code: ``, options: [{ skipHTMLAttributeValues: true }] })), + // text contents + ...ALL_IRREGULAR_WHITESPACES.map(s => ({ code: ``, 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: ` + + `, + 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: ``, + 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: ``, + errors: [{ + message: 'Irregular whitespace not allowed.', + line: 1, + column: 22 + }] + })), + ...ALL_IRREGULAR_WHITESPACES.map(s => ({ + code: ``, + 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: ``, + 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: ``, + 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: ``, + errors: [{ + message: 'Irregular whitespace not allowed.', + line: 1, + column: 16 + }] + })), + // options + { + code: ` +