From 79eb0919a8643f5dd626b458bb5f1d2088ec1845 Mon Sep 17 00:00:00 2001 From: ota Date: Thu, 13 Dec 2018 14:20:41 +0900 Subject: [PATCH 1/3] Add rules of HTML comment styles --- docs/rules/README.md | 3 + docs/rules/html-comment-content-newline.md | 229 +++++++ docs/rules/html-comment-content-spacing.md | 115 ++++ docs/rules/html-comment-indent.md | 133 ++++ lib/index.js | 3 + lib/rules/html-comment-content-newline.js | 181 +++++ lib/rules/html-comment-content-spacing.js | 155 +++++ lib/rules/html-comment-indent.js | 242 +++++++ lib/utils/html-comments.js | 196 ++++++ .../comment-tokens.json | 43 ++ .../source.vue | 4 + .../html-comments/blank1/comment-tokens.json | 43 ++ .../utils/html-comments/blank1/source.vue | 4 + .../html-comments/blank2/comment-tokens.json | 43 ++ .../utils/html-comments/blank2/source.vue | 5 + .../comment-tokens.json | 60 ++ .../decoration-empty-value/source.vue | 4 + .../decoration1/comment-tokens.json | 94 +++ .../html-comments/decoration1/source.vue | 4 + .../decoration2/comment-tokens.json | 94 +++ .../html-comments/decoration2/source.vue | 4 + .../decoration3/comment-tokens.json | 94 +++ .../html-comments/decoration3/source.vue | 6 + .../html-comments/empty/comment-tokens.json | 43 ++ .../utils/html-comments/empty/source.vue | 4 + .../comment-tokens.json | 1 + .../ie-conditional-comments1/source.vue | 6 + .../comment-tokens.json | 1 + .../ie-conditional-comments2/source.vue | 6 + .../comment-tokens.json | 1 + .../incorrectly-closed-comment/source.vue | 4 + .../non-decoration/comment-tokens.json | 60 ++ .../html-comments/non-decoration/source.vue | 4 + .../script-comment/comment-tokens.json | 1 + .../html-comments/script-comment/source.vue | 4 + .../html-comments/test1/comment-tokens.json | 60 ++ .../utils/html-comments/test1/source.vue | 4 + .../html-comments/test2/comment-tokens.json | 60 ++ .../utils/html-comments/test2/source.vue | 4 + .../html-comments/test3/comment-tokens.json | 60 ++ .../utils/html-comments/test3/source.vue | 4 + .../html-comments/test4/comment-tokens.json | 60 ++ .../utils/html-comments/test4/source.vue | 7 + .../lib/rules/html-comment-content-newline.js | 394 +++++++++++ .../lib/rules/html-comment-content-spacing.js | 326 +++++++++ tests/lib/rules/html-comment-indent.js | 648 ++++++++++++++++++ tests/lib/utils/html-comments.js | 69 ++ 47 files changed, 3590 insertions(+) create mode 100644 docs/rules/html-comment-content-newline.md create mode 100644 docs/rules/html-comment-content-spacing.md create mode 100644 docs/rules/html-comment-indent.md create mode 100644 lib/rules/html-comment-content-newline.js create mode 100644 lib/rules/html-comment-content-spacing.js create mode 100644 lib/rules/html-comment-indent.js create mode 100644 lib/utils/html-comments.js create mode 100644 tests/fixtures/utils/html-comments/abrupt-closing-of-empty-comment/comment-tokens.json create mode 100644 tests/fixtures/utils/html-comments/abrupt-closing-of-empty-comment/source.vue create mode 100644 tests/fixtures/utils/html-comments/blank1/comment-tokens.json create mode 100644 tests/fixtures/utils/html-comments/blank1/source.vue create mode 100644 tests/fixtures/utils/html-comments/blank2/comment-tokens.json create mode 100644 tests/fixtures/utils/html-comments/blank2/source.vue create mode 100644 tests/fixtures/utils/html-comments/decoration-empty-value/comment-tokens.json create mode 100644 tests/fixtures/utils/html-comments/decoration-empty-value/source.vue create mode 100644 tests/fixtures/utils/html-comments/decoration1/comment-tokens.json create mode 100644 tests/fixtures/utils/html-comments/decoration1/source.vue create mode 100644 tests/fixtures/utils/html-comments/decoration2/comment-tokens.json create mode 100644 tests/fixtures/utils/html-comments/decoration2/source.vue create mode 100644 tests/fixtures/utils/html-comments/decoration3/comment-tokens.json create mode 100644 tests/fixtures/utils/html-comments/decoration3/source.vue create mode 100644 tests/fixtures/utils/html-comments/empty/comment-tokens.json create mode 100644 tests/fixtures/utils/html-comments/empty/source.vue create mode 100644 tests/fixtures/utils/html-comments/ie-conditional-comments1/comment-tokens.json create mode 100644 tests/fixtures/utils/html-comments/ie-conditional-comments1/source.vue create mode 100644 tests/fixtures/utils/html-comments/ie-conditional-comments2/comment-tokens.json create mode 100644 tests/fixtures/utils/html-comments/ie-conditional-comments2/source.vue create mode 100644 tests/fixtures/utils/html-comments/incorrectly-closed-comment/comment-tokens.json create mode 100644 tests/fixtures/utils/html-comments/incorrectly-closed-comment/source.vue create mode 100644 tests/fixtures/utils/html-comments/non-decoration/comment-tokens.json create mode 100644 tests/fixtures/utils/html-comments/non-decoration/source.vue create mode 100644 tests/fixtures/utils/html-comments/script-comment/comment-tokens.json create mode 100644 tests/fixtures/utils/html-comments/script-comment/source.vue create mode 100644 tests/fixtures/utils/html-comments/test1/comment-tokens.json create mode 100644 tests/fixtures/utils/html-comments/test1/source.vue create mode 100644 tests/fixtures/utils/html-comments/test2/comment-tokens.json create mode 100644 tests/fixtures/utils/html-comments/test2/source.vue create mode 100644 tests/fixtures/utils/html-comments/test3/comment-tokens.json create mode 100644 tests/fixtures/utils/html-comments/test3/source.vue create mode 100644 tests/fixtures/utils/html-comments/test4/comment-tokens.json create mode 100644 tests/fixtures/utils/html-comments/test4/source.vue create mode 100644 tests/lib/rules/html-comment-content-newline.js create mode 100644 tests/lib/rules/html-comment-content-spacing.js create mode 100644 tests/lib/rules/html-comment-indent.js create mode 100644 tests/lib/utils/html-comments.js diff --git a/docs/rules/README.md b/docs/rules/README.md index 6ff3780de..83eda5692 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -274,6 +274,9 @@ For example: | [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: | | [vue/dot-location](./dot-location.md) | enforce consistent newlines before and after dots | :wrench: | | [vue/eqeqeq](./eqeqeq.md) | require the use of `===` and `!==` | :wrench: | +| [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: | +| [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: | +| [vue/html-comment-indent](./html-comment-indent.md) | enforce consistent indentation in HTML comments | :wrench: | | [vue/key-spacing](./key-spacing.md) | enforce consistent spacing between keys and values in object literal properties | :wrench: | | [vue/keyword-spacing](./keyword-spacing.md) | enforce consistent spacing before and after keywords | :wrench: | | [vue/match-component-file-name](./match-component-file-name.md) | require component name property to match its file name | | diff --git a/docs/rules/html-comment-content-newline.md b/docs/rules/html-comment-content-newline.md new file mode 100644 index 000000000..523e32336 --- /dev/null +++ b/docs/rules/html-comment-content-newline.md @@ -0,0 +1,229 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/html-comment-content-newline +description: enforce unified line brake in HTML comments +--- +# vue/html-comment-content-newline +> enforce unified line brake in HTML comments + +- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule. + +## :book: Rule Details + +This rule will enforce consistency of line break after the `` of comment. It also provides several exceptions for various documentation styles. + + + +```vue + +``` + + + +## :wrench: Options + +```json +{ + "vue/html-comment-content-newline": ["error", + { + "singleline": "always" | "never" | "ignore", + "multiline": "always" | "never" | "ignore", + }, + { + "exceptions": [] + } + ] +} +``` + +- The first option is either an object with `"singleline"` and `"multiline"` keys. + - `singleline` ... the configuration for single-line comments. + - `"never"` (default) ... disallow line breaks after the ``. + - `"always"` ... require one line break after the ``. + - `multiline` ... the configuration for multiline comments. + - `"never"` ... disallow line breaks after the ``. + - `"always"` (default) ... require one line break after the ``. + + You can also set the same value for both `singleline` and `multiline` by specifies a string. + +- This rule can also take a 2nd option, an object with the following key: `"exceptions"`. + - The `"exceptions"` value is an array of string patterns which are considered exceptions to the rule. + + ``` + "vue/html-comment-content-newline": ["error", { ... }, { "exceptions": ["*"] }] + ``` + +### `"always"` + + + +```vue + +``` + + + +### `"never"` + + + +```vue + +``` + + + +### `{"singleline": "always", "multiline": "ignore"}` + + + +```vue + +``` + + + + +### `{"singleline": "ignore", "multiline": "always"}` + + + +```vue + +``` + + + +### `"always", { "exceptions": ["*"] }` + + + +```vue + +``` + + + +## :couple: Related rules + +- [vue/html-comment-indent](./html-comment-indent.md) +- [vue/html-comment-content-spacing](./html-comment-content-spacing.md) +- [spaced-comment](https://eslint.org/docs/rules/spaced-comment) + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/html-comment-content-newline.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/html-comment-content-newline.js) diff --git a/docs/rules/html-comment-content-spacing.md b/docs/rules/html-comment-content-spacing.md new file mode 100644 index 000000000..6bcc5575d --- /dev/null +++ b/docs/rules/html-comment-content-spacing.md @@ -0,0 +1,115 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/html-comment-content-spacing +description: enforce unified spacing in HTML comments +--- +# vue/html-comment-content-spacing +> enforce unified spacing in HTML comments + +- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule. + +## :book: Rule Details + +This rule will enforce consistency of spacing after the `` of comment. It also provides several exceptions for various documentation styles. + +Whitespace after the `` makes it easier to read text in comments. + + + +```vue + +``` + + + +## :wrench: Options + +```json +{ + "vue/html-comment-content-spacing": ["error", + "always" | "never", + { + "exceptions": [] + } + ] +} +``` + +- The first is a string which be either `"always"` or `"never"`. The default is `"always"`. + - `"always"` ... there must be at least one whitespace at after the ``. + - `"never"` ... there should be no whitespace at after the ``. + + +- This rule can also take a 2nd option, an object with the following key: `"exceptions"`. + - The `"exceptions"` value is an array of string patterns which are considered exceptions to the rule. + Please note that exceptions are ignored if the first argument is `"never"`. + + ``` + "vue/html-comment-content-spacing": ["error", "always", { "exceptions": ["*"] }] + ``` + +### `"always"` + + + +```vue + +``` + + + +### `"never"` + + + +```vue + +``` + + + +### `"always", { "exceptions": ["*"] }` + + + +```vue + +``` + + + +## :couple: Related rules + +- [spaced-comment](https://eslint.org/docs/rules/spaced-comment) +- [vue/html-comment-content-newline](./html-comment-content-newline.md) + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/html-comment-content-spacing.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/html-comment-content-spacing.js) diff --git a/docs/rules/html-comment-indent.md b/docs/rules/html-comment-indent.md new file mode 100644 index 000000000..6da16a098 --- /dev/null +++ b/docs/rules/html-comment-indent.md @@ -0,0 +1,133 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/html-comment-indent +description: enforce consistent indentation in HTML comments +--- +# vue/html-comment-indent +> enforce consistent indentation in HTML comments + +- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule. + +## :book: Rule Details + +This rule enforces a consistent indentation style in HTML comment (``). The default style is 2 spaces. + + + +```vue + +``` + + + +## :wrench: Options + +```json +{ + "vue/html-comment-indent": ["error", type] +} +``` + +- `type` (`number | "tab"`) ... The type of indentation. Default is `2`. If this is a number, it's the number of spaces for one indent. If this is `"tab"`, it uses one tab for one indent. + +### `2` + + + +```vue + +``` + + + +### `4` + + + +```vue + +``` + + + +### `0` + + + +```vue + +``` + + + +### `"tab"` + + + +```vue + +``` + + + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/html-comment-indent.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/html-comment-indent.js) diff --git a/lib/index.js b/lib/index.js index 63a16b25c..6adbbecd4 100644 --- a/lib/index.js +++ b/lib/index.js @@ -23,6 +23,9 @@ module.exports = { 'eqeqeq': require('./rules/eqeqeq'), 'html-closing-bracket-newline': require('./rules/html-closing-bracket-newline'), 'html-closing-bracket-spacing': require('./rules/html-closing-bracket-spacing'), + 'html-comment-content-newline': require('./rules/html-comment-content-newline'), + 'html-comment-content-spacing': require('./rules/html-comment-content-spacing'), + 'html-comment-indent': require('./rules/html-comment-indent'), 'html-end-tags': require('./rules/html-end-tags'), 'html-indent': require('./rules/html-indent'), 'html-quotes': require('./rules/html-quotes'), diff --git a/lib/rules/html-comment-content-newline.js b/lib/rules/html-comment-content-newline.js new file mode 100644 index 000000000..68c9b8286 --- /dev/null +++ b/lib/rules/html-comment-content-newline.js @@ -0,0 +1,181 @@ +/** + * @author Yosuke ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ----------------------------------------------------------------------------- +// Requirements +// ----------------------------------------------------------------------------- + +const htmlComments = require('../utils/html-comments') + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +function parseOption (param) { + if (param && typeof param === 'string') { + return { + singleline: param, + multiline: param + } + } + return Object.assign({ + singleline: 'never', + multiline: 'always' + }, param) +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'suggestion', + + docs: { + description: 'enforce unified line brake in HTML comments', + category: undefined, + url: 'https://eslint.vuejs.org/rules/html-comment-content-newline.html' + }, + fixable: 'whitespace', + schema: [ + { + anyOf: [ + { + enum: ['always', 'never'] + }, + { + type: 'object', + properties: { + 'singleline': { enum: ['always', 'never', 'ignore'] }, + 'multiline': { enum: ['always', 'never', 'ignore'] } + }, + additionalProperties: false + } + ] + }, + { + type: 'object', + properties: { + exceptions: { + type: 'array', + items: { + type: 'string' + } + } + }, + additionalProperties: false + } + ], + messages: { + expectedAfterHTMLCommentOpen: "Expected line break after ''.", + expectedAfterExceptionBlock: 'Expected line break after exception block.', + expectedBeforeExceptionBlock: 'Expected line break before exception block.', + unexpectedAfterHTMLCommentOpen: "Unexpected line breaks after ''." + } + }, + + create (context) { + const option = parseOption(context.options[0]) + return htmlComments.defineVisitor(context, context.options[1], (commentTokens) => { + if (!commentTokens.value) { + return + } + const startLine = commentTokens.openDecoration + ? commentTokens.openDecoration.loc.end.line + : commentTokens.value.loc.start.line + const endLine = commentTokens.closeDecoration + ? commentTokens.closeDecoration.loc.start.line + : commentTokens.value.loc.end.line + const newlineType = startLine === endLine + ? option.singleline + : option.multiline + if (newlineType === 'ignore') { + return + } + checkCommentOpen(commentTokens, newlineType !== 'never') + checkCommentClose(commentTokens, newlineType !== 'never') + }) + + /** + * Reports the newline before the contents of a given comment if it's invalid. + * @param {string} commentTokens - comment tokens. + * @param {boolean} requireNewline - `true` if line breaks are required. + * @returns {void} + */ + function checkCommentOpen (commentTokens, requireNewline) { + const beforeToken = commentTokens.openDecoration || commentTokens.open + + if (requireNewline) { + if (beforeToken.loc.end.line < commentTokens.value.loc.start.line) { + // Is valid + return + } + context.report({ + loc: { + start: beforeToken.loc.end, + end: commentTokens.value.loc.start + }, + messageId: commentTokens.openDecoration ? 'expectedAfterExceptionBlock' : 'expectedAfterHTMLCommentOpen', + fix: commentTokens.openDecoration ? undefined : (fixer) => fixer.insertTextAfter(beforeToken, '\n') + }) + } else { + if (beforeToken.loc.end.line === commentTokens.value.loc.start.line) { + // Is valid + return + } + context.report({ + loc: { + start: beforeToken.loc.end, + end: commentTokens.value.loc.start + }, + messageId: 'unexpectedAfterHTMLCommentOpen', + fix: (fixer) => fixer.replaceTextRange([beforeToken.range[1], commentTokens.value.range[0]], ' ') + }) + } + } + + /** + * Reports the space after the contents of a given comment if it's invalid. + * @param {string} commentTokens - comment tokens. + * @param {boolean} requireNewline - `true` if line breaks are required. + * @returns {void} + */ + function checkCommentClose (commentTokens, requireNewline) { + const afterToken = commentTokens.closeDecoration || commentTokens.close + + if (requireNewline) { + if (commentTokens.value.loc.end.line < afterToken.loc.start.line) { + // Is valid + return + } + context.report({ + loc: { + start: commentTokens.value.loc.end, + end: afterToken.loc.start + }, + messageId: commentTokens.closeDecoration ? 'expectedBeforeExceptionBlock' : 'expectedBeforeHTMLCommentOpen', + fix: commentTokens.closeDecoration ? undefined : (fixer) => fixer.insertTextBefore(afterToken, '\n') + }) + } else { + if (commentTokens.value.loc.end.line === afterToken.loc.start.line) { + // Is valid + return + } + context.report({ + loc: { + start: commentTokens.value.loc.end, + end: afterToken.loc.start + }, + messageId: 'unexpectedBeforeHTMLCommentOpen', + fix: (fixer) => fixer.replaceTextRange([commentTokens.value.range[1], afterToken.range[0]], ' ') + }) + } + } + } +} diff --git a/lib/rules/html-comment-content-spacing.js b/lib/rules/html-comment-content-spacing.js new file mode 100644 index 000000000..956b8fcc8 --- /dev/null +++ b/lib/rules/html-comment-content-spacing.js @@ -0,0 +1,155 @@ +/** + * @author Yosuke ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ----------------------------------------------------------------------------- +// Requirements +// ----------------------------------------------------------------------------- + +const htmlComments = require('../utils/html-comments') + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'suggestion', + + docs: { + description: 'enforce unified spacing in HTML comments', + category: undefined, + url: 'https://eslint.vuejs.org/rules/html-comment-content-spacing.html' + }, + fixable: 'whitespace', + schema: [ + { + enum: ['always', 'never'] + }, + { + type: 'object', + properties: { + exceptions: { + type: 'array', + items: { + type: 'string' + } + } + }, + additionalProperties: false + } + ], + messages: { + expectedAfterHTMLCommentOpen: "Expected space after ''.", + expectedAfterExceptionBlock: 'Expected space after exception block.', + expectedBeforeExceptionBlock: 'Expected space before exception block.', + unexpectedAfterHTMLCommentOpen: "Unexpected space after ''." + } + }, + + create (context) { + // Unless the first option is never, require a space + const requireSpace = context.options[0] !== 'never' + return htmlComments.defineVisitor(context, context.options[1], (commentTokens) => { + if (!commentTokens.value) { + return + } + checkCommentOpen(commentTokens) + checkCommentClose(commentTokens) + }, { includeDirectives: true }) + + /** + * Reports the space before the contents of a given comment if it's invalid. + * @param {string} commentTokens - comment tokens. + * @returns {void} + */ + function checkCommentOpen (commentTokens) { + const beforeToken = commentTokens.openDecoration || commentTokens.open + if (beforeToken.loc.end.line !== commentTokens.value.loc.start.line) { + // Ignore newline + return + } + + if (requireSpace) { + if (beforeToken.range[1] < commentTokens.value.range[0]) { + // Is valid + return + } + context.report({ + loc: { + start: beforeToken.loc.end, + end: commentTokens.value.loc.start + }, + messageId: commentTokens.openDecoration ? 'expectedAfterExceptionBlock' : 'expectedAfterHTMLCommentOpen', + fix: commentTokens.openDecoration ? undefined : (fixer) => fixer.insertTextAfter(beforeToken, ' ') + }) + } else { + if (commentTokens.openDecoration) { + // Ignore expection block + return + } + if (beforeToken.range[1] === commentTokens.value.range[0]) { + // Is valid + return + } + context.report({ + loc: { + start: beforeToken.loc.end, + end: commentTokens.value.loc.start + }, + messageId: 'unexpectedAfterHTMLCommentOpen', + fix: (fixer) => fixer.removeRange([beforeToken.range[1], commentTokens.value.range[0]]) + }) + } + } + + /** + * Reports the space after the contents of a given comment if it's invalid. + * @param {string} commentTokens - comment tokens. + * @returns {void} + */ + function checkCommentClose (commentTokens) { + const afterToken = commentTokens.closeDecoration || commentTokens.close + if (commentTokens.value.loc.end.line !== afterToken.loc.start.line) { + // Ignore newline + return + } + + if (requireSpace) { + if (commentTokens.value.range[1] < afterToken.range[0]) { + // Is valid + return + } + context.report({ + loc: { + start: commentTokens.value.loc.end, + end: afterToken.loc.start + }, + messageId: commentTokens.closeDecoration ? 'expectedBeforeExceptionBlock' : 'expectedBeforeHTMLCommentOpen', + fix: commentTokens.closeDecoration ? undefined : (fixer) => fixer.insertTextBefore(afterToken, ' ') + }) + } else { + if (commentTokens.closeDecoration) { + // Ignore expection block + return + } + if (commentTokens.value.range[1] === afterToken.range[0]) { + // Is valid + return + } + context.report({ + loc: { + start: commentTokens.value.loc.end, + end: afterToken.loc.start + }, + messageId: 'unexpectedBeforeHTMLCommentOpen', + fix: (fixer) => fixer.removeRange([commentTokens.value.range[1], afterToken.range[0]]) + }) + } + } + } +} diff --git a/lib/rules/html-comment-indent.js b/lib/rules/html-comment-indent.js new file mode 100644 index 000000000..7a6cae38f --- /dev/null +++ b/lib/rules/html-comment-indent.js @@ -0,0 +1,242 @@ +/** + * @author Yosuke ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ----------------------------------------------------------------------------- +// Requirements +// ----------------------------------------------------------------------------- + +const htmlComments = require('../utils/html-comments') + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** + * Normalize options. + * @param {number|"tab"|undefined} type The type of indentation. + * @returns {Object} Normalized options. + */ +function parseOptions (type) { + const ret = { + indentChar: ' ', + indentSize: 2 + } + + if (Number.isSafeInteger(type)) { + ret.indentSize = type + } else if (type === 'tab') { + ret.indentChar = '\t' + ret.indentSize = 1 + } + ret.indentText = ret.indentChar.repeat(ret.indentSize) + + return ret +} + +function toDisplay (s, unitChar) { + if (s.length === 0 && unitChar) { + return `0 ${toUnit(unitChar)}s` + } + const char = s[0] + if (char === ' ' || char === '\t') { + if (s.split('').every(c => c === char)) { + return `${s.length} ${toUnit(char)}${s.length === 1 ? '' : 's'}` + } + } + + return JSON.stringify(s) +} + +function toUnit (char) { + if (char === '\t') { + return 'tab' + } + if (char === ' ') { + return 'space' + } + return JSON.stringify(char) +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'suggestion', + + docs: { + description: 'enforce consistent indentation in HTML comments', + category: undefined, + url: 'https://eslint.vuejs.org/rules/html-comment-indent.html' + }, + fixable: 'whitespace', + schema: [ + { + anyOf: [ + { type: 'integer', minimum: 0 }, + { enum: ['tab'] } + ] + } + ], + messages: { + unexpectedBaseIndentation: 'Expected base point indentation of {{expected}}, but found {{actual}}.', + missingBaseIndentation: 'Expected base point indentation of {{expected}}, but not found.', + unexpectedIndentationCharacter: 'Expected {{expected}} character, but found {{actual}} character.', + unexpectedIndentation: 'Expected indentation of {{expected}} but found {{actual}}.', + unexpectedRelativeIndentation: 'Expected relative indentation of {{expected}} but found {{actual}}.' + } + }, + + create (context) { + const options = parseOptions(context.options[0]) + const sourceCode = context.getSourceCode() + return htmlComments.defineVisitor(context, null, (commentTokens) => { + const baseIndentText = getLineIndentText(commentTokens.open.loc.start.line) + let endLine + if (commentTokens.value) { + const startLine = commentTokens.value.loc.start.line + endLine = commentTokens.value.loc.end.line + + const checkStartLine = commentTokens.open.loc.end.line === startLine ? startLine + 1 : startLine + + for (let line = checkStartLine; line <= endLine; line++) { + validateIndentForLine(line, baseIndentText, 1) + } + } else { + endLine = commentTokens.open.loc.end.line + } + + if (endLine < commentTokens.close.loc.start.line) { + // `-->` + validateIndentForLine(commentTokens.close.loc.start.line, baseIndentText, 0) + } + }, { includeDirectives: true }) + + /** + * Checks whether the given line is a blank line. + * @param {number} line The number of line. Begins with 1. + * @returns {boolean} `true` if the given line is a blank line + */ + function isEmptyLine (line) { + const lineText = sourceCode.getLines()[line - 1] + return !lineText.trim() + } + + /** + * Get the actual indentation of the given line. + * @param {number} line The number of line. Begins with 1. + * @returns {string} The actual indentation text + */ + function getLineIndentText (line) { + const lineText = sourceCode.getLines()[line - 1] + const charIndex = lineText.search(/\S/) + // already checked + // if (charIndex < 0) { + // return lineText + // } + return lineText.slice(0, charIndex) + } + + /** + * Define the function which fixes the problem. + * @param {number} line The number of line. + * @param {string} actualIndentText The actual indentation text. + * @param {string} expectedIndentText The expected indentation text. + * @returns {function} The defined function. + */ + function defineFix (line, actualIndentText, expectedIndentText) { + return fixer => { + const start = sourceCode.getIndexFromLoc({ + line, + column: 0 + }) + return fixer.replaceTextRange( + [start, start + actualIndentText.length], + expectedIndentText + ) + } + } + + /** + * Validate the indentation of a line. + * @param {number} line The number of line. Begins with 1. + * @param {string} baseIndentText The expected base indentation text. + * @param {number} offset The number of the indentation offset. + */ + function validateIndentForLine (line, baseIndentText, offset) { + if (isEmptyLine(line)) { + return + } + const actualIndentText = getLineIndentText(line) + + const expectedOffsetIndentText = options.indentText.repeat(offset) + const expectedIndentText = baseIndentText + expectedOffsetIndentText + + // validate base indent + if ( + baseIndentText && + (actualIndentText.length < baseIndentText.length || + !actualIndentText.startsWith(baseIndentText)) + ) { + context.report({ + loc: { + start: { line, column: 0 }, + end: { line, column: actualIndentText.length } + }, + messageId: actualIndentText + ? 'unexpectedBaseIndentation' + : 'missingBaseIndentation', + data: { + expected: toDisplay(baseIndentText), + actual: toDisplay(actualIndentText.slice(0, baseIndentText.length)) + }, + fix: defineFix(line, actualIndentText, expectedIndentText) + }) + return + } + + const actualOffsetIndentText = actualIndentText.slice(baseIndentText.length) + + // validate indent charctor + for (let i = 0; i < actualOffsetIndentText.length; ++i) { + if (actualOffsetIndentText[i] !== options.indentChar) { + context.report({ + loc: { + start: { line, column: baseIndentText.length + i }, + end: { line, column: baseIndentText.length + i + 1 } + }, + messageId: 'unexpectedIndentationCharacter', + data: { + expected: toUnit(options.indentChar), + actual: toUnit(actualOffsetIndentText[i]) + }, + fix: defineFix(line, actualIndentText, expectedIndentText) + }) + return + } + } + + // validate indent length + if (actualOffsetIndentText.length !== expectedOffsetIndentText.length) { + context.report({ + loc: { + start: { line, column: baseIndentText.length }, + end: { line, column: actualIndentText.length } + }, + messageId: baseIndentText + ? 'unexpectedRelativeIndentation' + : 'unexpectedIndentation', + data: { + expected: toDisplay(expectedOffsetIndentText, options.indentChar), + actual: toDisplay(actualOffsetIndentText, options.indentChar) + }, + fix: defineFix(line, actualIndentText, expectedIndentText) + }) + } + } + } +} diff --git a/lib/utils/html-comments.js b/lib/utils/html-comments.js new file mode 100644 index 000000000..81f145dbb --- /dev/null +++ b/lib/utils/html-comments.js @@ -0,0 +1,196 @@ +const utils = require('./') + +const COMMENT_DIRECTIVE = /^\s*eslint-(?:en|dis)able/ +const IE_CONDITIONAL_IF = /^\[if\s+/ +const IE_CONDITIONAL_ENDIF = /\[endif\]$/ + +function isCommentDirective (comment) { + return COMMENT_DIRECTIVE.test(comment.value) +} + +function isIEConditionalComment (comment) { + return IE_CONDITIONAL_IF.test(comment.value) || IE_CONDITIONAL_ENDIF.test(comment.value) +} + +/** + * Define HTML comment tokenizer + * + * @param {SourceCode} sourceCode The source code instance. + * @param {object} config The config. + * @returns {object} HTML comment tokenizer. + */ +function defineTokenizer (sourceCode, config) { + config = config || {} + + const exceptions = config.exceptions || [] + + /** + * Get a open decoration string from comment contents. + * @param {string} contents comment contents + * @returns {string} decoration string + */ + function getOpenDecoration (contents) { + let decoration = '' + for (const exception of exceptions) { + const length = exception.length + let index = 0 + while (contents.startsWith(exception, index)) { + index += length + } + const exceptionLength = index + if (decoration.length < exceptionLength) { + decoration = contents.slice(0, exceptionLength) + } + } + return decoration + } + + /** + * Get a close decoration string from comment contents. + * @param {string} contents comment contents + * @returns {string} decoration string + */ + function getCloseDecoration (contents) { + let decoration = '' + for (const exception of exceptions) { + const length = exception.length + let index = contents.length + while (contents.endsWith(exception, index)) { + index -= length + } + const exceptionLength = contents.length - index + if (decoration.length < exceptionLength) { + decoration = contents.slice(index) + } + } + return decoration + } + + /** + * Tokenize HTMLComment. + * @param {ASTNode} node a comment token + * @returns {object} the result of HTMLComment tokens. + */ + return function tokenizeHTMLComment (node) { + if (node.type !== 'HTMLComment') { + // Is not HTMLComment + return null + } + + const htmlCommentText = sourceCode.getText(node) + + if (!htmlCommentText.startsWith('')) { + // Is not normal HTML Comment + // e.g. Error Code: "abrupt-closing-of-empty-comment", "incorrectly-closed-comment" + return null + } + + let valueText = htmlCommentText.slice(4, -3) + const openDecorationText = getOpenDecoration(valueText) + valueText = valueText.slice(openDecorationText.length) + const firstCharIndex = valueText.search(/\S/) + const beforeSpace = firstCharIndex >= 0 ? valueText.slice(0, firstCharIndex) : valueText + valueText = valueText.slice(beforeSpace.length) + + const closeDecorationText = getCloseDecoration(valueText) + if (closeDecorationText) { + valueText = valueText.slice(0, -closeDecorationText.length) + } + const lastCharIndex = valueText.search(/\S\s*$/) + const afterSpace = lastCharIndex >= 0 ? valueText.slice(lastCharIndex + 1) : valueText + if (afterSpace) { + valueText = valueText.slice(0, -afterSpace.length) + } + + let tokenIndex = node.range[0] + const createToken = (type, value) => { + const range = [tokenIndex, tokenIndex + value.length] + tokenIndex = range[1] + let loc + return { + type, + value, + range, + get loc () { + if (loc) { + return loc + } + return (loc = { + start: sourceCode.getLocFromIndex(range[0]), + end: sourceCode.getLocFromIndex(range[1]) + }) + } + } + } + const open = createToken('HTMLCommentOpen', '') + + return { + /** HTML comment open (``) */ + closeDecoration, + /** HTML comment close (`-->`) */ + close + } + } +} + +/** + * Define HTML comment visitor + * + * @param {RuleContext} context The rule context. + * @param {object} config The config. + * @param {function} visitHTMLComment The HTML comment visitor. + * @returns {object} HTML comment visitor. + */ +function defineVisitor (context, config, visitHTMLComment, visitorOption) { + visitorOption = visitorOption || {} + return { + Program (node) { + if (utils.hasInvalidEOF(node)) { + return + } + if (!node.templateBody) { + return + } + const tokenizer = defineTokenizer(context.getSourceCode(), config) + + for (const comment of node.templateBody.comments) { + if (comment.type !== 'HTMLComment') { + continue + } + if (!visitorOption.includeDirectives && + isCommentDirective(comment)) { + // ignore directives + continue + } + if (isIEConditionalComment(comment)) { + // ignore IE conditional + continue + } + + const tokens = tokenizer(comment) + if (tokens) { + visitHTMLComment(tokens) + } + } + } + } +} + +module.exports = { + defineTokenizer, + defineVisitor, + isCommentDirective, + isIEConditionalComment +} diff --git a/tests/fixtures/utils/html-comments/abrupt-closing-of-empty-comment/comment-tokens.json b/tests/fixtures/utils/html-comments/abrupt-closing-of-empty-comment/comment-tokens.json new file mode 100644 index 000000000..adf085577 --- /dev/null +++ b/tests/fixtures/utils/html-comments/abrupt-closing-of-empty-comment/comment-tokens.json @@ -0,0 +1,43 @@ +[ + { + "open": { + "type": "HTMLCommentOpen", + "value": "", + "range": [ + 72, + 75 + ], + "loc": { + "start": { + "line": 3, + "column": 6 + }, + "end": { + "line": 4, + "column": 1 + } + } + } + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/html-comments/abrupt-closing-of-empty-comment/source.vue b/tests/fixtures/utils/html-comments/abrupt-closing-of-empty-comment/source.vue new file mode 100644 index 000000000..9276de6b5 --- /dev/null +++ b/tests/fixtures/utils/html-comments/abrupt-closing-of-empty-comment/source.vue @@ -0,0 +1,4 @@ + + diff --git a/tests/fixtures/utils/html-comments/blank1/comment-tokens.json b/tests/fixtures/utils/html-comments/blank1/comment-tokens.json new file mode 100644 index 000000000..016703d3b --- /dev/null +++ b/tests/fixtures/utils/html-comments/blank1/comment-tokens.json @@ -0,0 +1,43 @@ +[ + { + "open": { + "type": "HTMLCommentOpen", + "value": "", + "range": [ + 50, + 53 + ], + "loc": { + "start": { + "line": 3, + "column": 9 + }, + "end": { + "line": 3, + "column": 12 + } + } + } + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/html-comments/blank1/source.vue b/tests/fixtures/utils/html-comments/blank1/source.vue new file mode 100644 index 000000000..71043a9cb --- /dev/null +++ b/tests/fixtures/utils/html-comments/blank1/source.vue @@ -0,0 +1,4 @@ + + diff --git a/tests/fixtures/utils/html-comments/blank2/comment-tokens.json b/tests/fixtures/utils/html-comments/blank2/comment-tokens.json new file mode 100644 index 000000000..2cffdf190 --- /dev/null +++ b/tests/fixtures/utils/html-comments/blank2/comment-tokens.json @@ -0,0 +1,43 @@ +[ + { + "open": { + "type": "HTMLCommentOpen", + "value": "", + "range": [ + 50, + 53 + ], + "loc": { + "start": { + "line": 4, + "column": 2 + }, + "end": { + "line": 4, + "column": 5 + } + } + } + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/html-comments/blank2/source.vue b/tests/fixtures/utils/html-comments/blank2/source.vue new file mode 100644 index 000000000..887603525 --- /dev/null +++ b/tests/fixtures/utils/html-comments/blank2/source.vue @@ -0,0 +1,5 @@ + + diff --git a/tests/fixtures/utils/html-comments/decoration-empty-value/comment-tokens.json b/tests/fixtures/utils/html-comments/decoration-empty-value/comment-tokens.json new file mode 100644 index 000000000..fff156f17 --- /dev/null +++ b/tests/fixtures/utils/html-comments/decoration-empty-value/comment-tokens.json @@ -0,0 +1,60 @@ +[ + { + "open": { + "type": "HTMLCommentOpen", + "value": "", + "range": [ + 68, + 71 + ], + "loc": { + "start": { + "line": 3, + "column": 11 + }, + "end": { + "line": 3, + "column": 14 + } + } + } + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/html-comments/decoration-empty-value/source.vue b/tests/fixtures/utils/html-comments/decoration-empty-value/source.vue new file mode 100644 index 000000000..14bcb9519 --- /dev/null +++ b/tests/fixtures/utils/html-comments/decoration-empty-value/source.vue @@ -0,0 +1,4 @@ + + diff --git a/tests/fixtures/utils/html-comments/decoration1/comment-tokens.json b/tests/fixtures/utils/html-comments/decoration1/comment-tokens.json new file mode 100644 index 000000000..37dda22ae --- /dev/null +++ b/tests/fixtures/utils/html-comments/decoration1/comment-tokens.json @@ -0,0 +1,94 @@ +[ + { + "open": { + "type": "HTMLCommentOpen", + "value": "", + "range": [ + 69, + 72 + ], + "loc": { + "start": { + "line": 3, + "column": 23 + }, + "end": { + "line": 3, + "column": 26 + } + } + } + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/html-comments/decoration1/source.vue b/tests/fixtures/utils/html-comments/decoration1/source.vue new file mode 100644 index 000000000..c5a6c442c --- /dev/null +++ b/tests/fixtures/utils/html-comments/decoration1/source.vue @@ -0,0 +1,4 @@ + + diff --git a/tests/fixtures/utils/html-comments/decoration2/comment-tokens.json b/tests/fixtures/utils/html-comments/decoration2/comment-tokens.json new file mode 100644 index 000000000..0ad4e3634 --- /dev/null +++ b/tests/fixtures/utils/html-comments/decoration2/comment-tokens.json @@ -0,0 +1,94 @@ +[ + { + "open": { + "type": "HTMLCommentOpen", + "value": "", + "range": [ + 71, + 74 + ], + "loc": { + "start": { + "line": 3, + "column": 25 + }, + "end": { + "line": 3, + "column": 28 + } + } + } + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/html-comments/decoration2/source.vue b/tests/fixtures/utils/html-comments/decoration2/source.vue new file mode 100644 index 000000000..f40c67956 --- /dev/null +++ b/tests/fixtures/utils/html-comments/decoration2/source.vue @@ -0,0 +1,4 @@ + + diff --git a/tests/fixtures/utils/html-comments/decoration3/comment-tokens.json b/tests/fixtures/utils/html-comments/decoration3/comment-tokens.json new file mode 100644 index 000000000..ab7f1c333 --- /dev/null +++ b/tests/fixtures/utils/html-comments/decoration3/comment-tokens.json @@ -0,0 +1,94 @@ +[ + { + "open": { + "type": "HTMLCommentOpen", + "value": "", + "range": [ + 77, + 80 + ], + "loc": { + "start": { + "line": 5, + "column": 7 + }, + "end": { + "line": 5, + "column": 10 + } + } + } + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/html-comments/decoration3/source.vue b/tests/fixtures/utils/html-comments/decoration3/source.vue new file mode 100644 index 000000000..2fe37af4a --- /dev/null +++ b/tests/fixtures/utils/html-comments/decoration3/source.vue @@ -0,0 +1,6 @@ + + diff --git a/tests/fixtures/utils/html-comments/empty/comment-tokens.json b/tests/fixtures/utils/html-comments/empty/comment-tokens.json new file mode 100644 index 000000000..81d8f4723 --- /dev/null +++ b/tests/fixtures/utils/html-comments/empty/comment-tokens.json @@ -0,0 +1,43 @@ +[ + { + "open": { + "type": "HTMLCommentOpen", + "value": "", + "range": [ + 46, + 49 + ], + "loc": { + "start": { + "line": 3, + "column": 6 + }, + "end": { + "line": 3, + "column": 9 + } + } + } + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/html-comments/empty/source.vue b/tests/fixtures/utils/html-comments/empty/source.vue new file mode 100644 index 000000000..33d7fd240 --- /dev/null +++ b/tests/fixtures/utils/html-comments/empty/source.vue @@ -0,0 +1,4 @@ + + diff --git a/tests/fixtures/utils/html-comments/ie-conditional-comments1/comment-tokens.json b/tests/fixtures/utils/html-comments/ie-conditional-comments1/comment-tokens.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/tests/fixtures/utils/html-comments/ie-conditional-comments1/comment-tokens.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/fixtures/utils/html-comments/ie-conditional-comments1/source.vue b/tests/fixtures/utils/html-comments/ie-conditional-comments1/source.vue new file mode 100644 index 000000000..f8a6a1559 --- /dev/null +++ b/tests/fixtures/utils/html-comments/ie-conditional-comments1/source.vue @@ -0,0 +1,6 @@ + + diff --git a/tests/fixtures/utils/html-comments/ie-conditional-comments2/comment-tokens.json b/tests/fixtures/utils/html-comments/ie-conditional-comments2/comment-tokens.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/tests/fixtures/utils/html-comments/ie-conditional-comments2/comment-tokens.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/fixtures/utils/html-comments/ie-conditional-comments2/source.vue b/tests/fixtures/utils/html-comments/ie-conditional-comments2/source.vue new file mode 100644 index 000000000..679eba727 --- /dev/null +++ b/tests/fixtures/utils/html-comments/ie-conditional-comments2/source.vue @@ -0,0 +1,6 @@ + + diff --git a/tests/fixtures/utils/html-comments/incorrectly-closed-comment/comment-tokens.json b/tests/fixtures/utils/html-comments/incorrectly-closed-comment/comment-tokens.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/tests/fixtures/utils/html-comments/incorrectly-closed-comment/comment-tokens.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/fixtures/utils/html-comments/incorrectly-closed-comment/source.vue b/tests/fixtures/utils/html-comments/incorrectly-closed-comment/source.vue new file mode 100644 index 000000000..edfecdca6 --- /dev/null +++ b/tests/fixtures/utils/html-comments/incorrectly-closed-comment/source.vue @@ -0,0 +1,4 @@ + + diff --git a/tests/fixtures/utils/html-comments/non-decoration/comment-tokens.json b/tests/fixtures/utils/html-comments/non-decoration/comment-tokens.json new file mode 100644 index 000000000..4a42c85cc --- /dev/null +++ b/tests/fixtures/utils/html-comments/non-decoration/comment-tokens.json @@ -0,0 +1,60 @@ +[ + { + "open": { + "type": "HTMLCommentOpen", + "value": "", + "range": [ + 70, + 73 + ], + "loc": { + "start": { + "line": 3, + "column": 21 + }, + "end": { + "line": 3, + "column": 24 + } + } + } + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/html-comments/non-decoration/source.vue b/tests/fixtures/utils/html-comments/non-decoration/source.vue new file mode 100644 index 000000000..464b27799 --- /dev/null +++ b/tests/fixtures/utils/html-comments/non-decoration/source.vue @@ -0,0 +1,4 @@ + + diff --git a/tests/fixtures/utils/html-comments/script-comment/comment-tokens.json b/tests/fixtures/utils/html-comments/script-comment/comment-tokens.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/tests/fixtures/utils/html-comments/script-comment/comment-tokens.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/fixtures/utils/html-comments/script-comment/source.vue b/tests/fixtures/utils/html-comments/script-comment/source.vue new file mode 100644 index 000000000..41aedcefe --- /dev/null +++ b/tests/fixtures/utils/html-comments/script-comment/source.vue @@ -0,0 +1,4 @@ + + diff --git a/tests/fixtures/utils/html-comments/test1/comment-tokens.json b/tests/fixtures/utils/html-comments/test1/comment-tokens.json new file mode 100644 index 000000000..5c53e9ca4 --- /dev/null +++ b/tests/fixtures/utils/html-comments/test1/comment-tokens.json @@ -0,0 +1,60 @@ +[ + { + "open": { + "type": "HTMLCommentOpen", + "value": "", + "range": [ + 53, + 56 + ], + "loc": { + "start": { + "line": 3, + "column": 13 + }, + "end": { + "line": 3, + "column": 16 + } + } + } + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/html-comments/test1/source.vue b/tests/fixtures/utils/html-comments/test1/source.vue new file mode 100644 index 000000000..a270969a3 --- /dev/null +++ b/tests/fixtures/utils/html-comments/test1/source.vue @@ -0,0 +1,4 @@ + + diff --git a/tests/fixtures/utils/html-comments/test2/comment-tokens.json b/tests/fixtures/utils/html-comments/test2/comment-tokens.json new file mode 100644 index 000000000..c414ab176 --- /dev/null +++ b/tests/fixtures/utils/html-comments/test2/comment-tokens.json @@ -0,0 +1,60 @@ +[ + { + "open": { + "type": "HTMLCommentOpen", + "value": "", + "range": [ + 55, + 58 + ], + "loc": { + "start": { + "line": 3, + "column": 15 + }, + "end": { + "line": 3, + "column": 18 + } + } + } + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/html-comments/test2/source.vue b/tests/fixtures/utils/html-comments/test2/source.vue new file mode 100644 index 000000000..aed10db97 --- /dev/null +++ b/tests/fixtures/utils/html-comments/test2/source.vue @@ -0,0 +1,4 @@ + + diff --git a/tests/fixtures/utils/html-comments/test3/comment-tokens.json b/tests/fixtures/utils/html-comments/test3/comment-tokens.json new file mode 100644 index 000000000..40db0b89d --- /dev/null +++ b/tests/fixtures/utils/html-comments/test3/comment-tokens.json @@ -0,0 +1,60 @@ +[ + { + "open": { + "type": "HTMLCommentOpen", + "value": "", + "range": [ + 61, + 64 + ], + "loc": { + "start": { + "line": 3, + "column": 21 + }, + "end": { + "line": 3, + "column": 24 + } + } + } + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/html-comments/test3/source.vue b/tests/fixtures/utils/html-comments/test3/source.vue new file mode 100644 index 000000000..bdb9c48cd --- /dev/null +++ b/tests/fixtures/utils/html-comments/test3/source.vue @@ -0,0 +1,4 @@ + + diff --git a/tests/fixtures/utils/html-comments/test4/comment-tokens.json b/tests/fixtures/utils/html-comments/test4/comment-tokens.json new file mode 100644 index 000000000..3513260bd --- /dev/null +++ b/tests/fixtures/utils/html-comments/test4/comment-tokens.json @@ -0,0 +1,60 @@ +[ + { + "open": { + "type": "HTMLCommentOpen", + "value": "", + "range": [ + 73, + 76 + ], + "loc": { + "start": { + "line": 6, + "column": 2 + }, + "end": { + "line": 6, + "column": 5 + } + } + } + } +] \ No newline at end of file diff --git a/tests/fixtures/utils/html-comments/test4/source.vue b/tests/fixtures/utils/html-comments/test4/source.vue new file mode 100644 index 000000000..bca201562 --- /dev/null +++ b/tests/fixtures/utils/html-comments/test4/source.vue @@ -0,0 +1,7 @@ + + diff --git a/tests/lib/rules/html-comment-content-newline.js b/tests/lib/rules/html-comment-content-newline.js new file mode 100644 index 000000000..568b1f65b --- /dev/null +++ b/tests/lib/rules/html-comment-content-newline.js @@ -0,0 +1,394 @@ +/** + * @author Yosuke ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const rule = require('../../../lib/rules/html-comment-content-newline') + +const RuleTester = require('eslint').RuleTester + +const tester = new RuleTester({ + parser: 'vue-eslint-parser', + parserOptions: { + ecmaVersion: 2015 + } +}) +tester.run('html-comment-content-newline', rule, { + + valid: [ + { + code: ` + + ` + }, + { + code: ` + + `, + options: ['always'] + }, + { + code: ` + + `, + options: ['never'] + }, + { + code: ` + + `, + options: ['always'] + }, + { + code: ` + + `, + options: ['never'] + }, + { + code: ` + + `, + options: [{ singleline: 'always', multiline: 'never' }] + }, + { + code: ` + + `, + options: ['never'] + }, + // ignore + { + code: ` + + `, + options: [{ singleline: 'ignore', multiline: 'ignore' }] + }, + + // exceptions + { + code: ` + + `, + options: ['always', { exceptions: ['+'] }] + }, + { + code: ` + + `, + options: ['always', { exceptions: ['+-+'] }] + }, + { + code: ` + + `, + options: ['always', { exceptions: ['+'] }] + }, + { + code: ` + + `, + options: ['always', { exceptions: ['+', '*', '++xx'] }] + }, + { + code: ` + + `, + options: ['never', { exceptions: ['+'] }] + }, + + // directive + { + code: ` + + `, + options: ['always'] + }, + { + code: ` + + `, + options: ['never'] + }, + // invalid html + { + code: ` + + ` + }, + { + code: ` + + ` + } + ], + + invalid: [ + { + code: ` + + `, + output: ` + + `, + errors: [ + { message: 'Unexpected line breaks after \'\'.', line: 4, column: 20, endLine: 5, endColumn: 11 }, + { message: 'Expected line break after \'\'.', line: 7, column: 20, endColumn: 21 } + ] + }, + { + code: ` + + `, + options: ['always'], + output: ` + + `, + errors: [ + { message: 'Expected line break after \'\'.', line: 3, column: 22, endColumn: 22 }, + { message: 'Expected line break after \'\'.', line: 4, column: 24, endColumn: 26 } + ] + }, + { + code: ` + + `, + options: ['never'], + output: ` + + `, + errors: [ + { message: 'Unexpected line breaks after \'\'.', line: 4, column: 8, endLine: 5, endColumn: 1 } + ] + }, + { + code: ` + + `, + options: ['always'], + output: ` + + `, + errors: [ + { message: 'Expected line break after \'\'.', line: 3, column: 30, endColumn: 38 } + ] + }, + // exceptions + { + code: ` + + `, + options: ['always', { exceptions: ['+'] }], + output: null, + errors: [ + 'Expected line break after exception block.', + 'Expected line break before exception block.' + ] + }, + { + code: ` + + `, + options: ['always', { exceptions: ['*'] }], + output: null, + errors: [ + 'Expected line break after exception block.', + 'Expected line break before exception block.' + ] + }, + { + code: ` + + `, + options: ['always', { exceptions: ['#+#-'] }], + output: ` + + `, + errors: [ + 'Expected line break after exception block.', + 'Expected line break before \'-->\'.' + ] + }, + { + code: ` + + `, + options: ['always', { exceptions: ['*', '++'] }], + output: null, + errors: [ + 'Expected line break after exception block.', + { message: 'Expected line break before exception block.', line: 3, column: 27 } + ] + }, + { + code: ` + + `, + options: ['always', { exceptions: ['*', '++'] }], + output: null, + errors: [ + 'Expected line break after exception block.', + { message: 'Expected line break before exception block.', line: 3, column: 28 } + ] + } + ] + +}) diff --git a/tests/lib/rules/html-comment-content-spacing.js b/tests/lib/rules/html-comment-content-spacing.js new file mode 100644 index 000000000..03bf0fbbd --- /dev/null +++ b/tests/lib/rules/html-comment-content-spacing.js @@ -0,0 +1,326 @@ +/** + * @author Yosuke ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const rule = require('../../../lib/rules/html-comment-content-spacing') + +const RuleTester = require('eslint').RuleTester + +const tester = new RuleTester({ + parser: 'vue-eslint-parser', + parserOptions: { + ecmaVersion: 2015 + } +}) +tester.run('html-comment-content-spacing', rule, { + + valid: [ + { + code: ` + + ` + }, + { + code: ` + + `, + options: ['always'] + }, + { + code: ` + + `, + options: ['always'] + }, + { + code: ` + + `, + options: ['always'] + }, + { + code: ` + + `, + options: ['never'] + }, + { + code: ` + + `, + options: ['never'] + }, + { + code: ` + + `, + options: ['never'] + }, + { + code: ` + + `, + options: ['always'] + }, + { + code: ` + + `, + options: ['never'] + }, + { + code: ` + + `, + options: ['always'] + }, + { + code: ` + + `, + options: ['never'] + }, + + // exceptions + { + code: ` + + `, + options: ['always', { exceptions: ['+'] }] + }, + { + code: ` + + `, + options: ['always', { exceptions: ['+-+'] }] + }, + { + code: ` + + `, + options: ['always', { exceptions: ['+'] }] + }, + { + code: ` + + `, + options: ['always', { exceptions: ['+', '*', '++xx'] }] + }, + { + code: ` + + `, + options: ['never', { exceptions: ['+'] }] + }, + + // invalid html + { + code: ` + + ` + }, + { + code: ` + + ` + } + ], + + invalid: [ + { + code: ` + + `, + options: ['always'], + output: ` + + `, + errors: [ + { message: 'Expected space after \'\'.', line: 3, column: 22, endColumn: 22 } + ] + }, + { + code: ` + + `, + options: ['never'], + output: ` + + `, + errors: [ + { message: 'Unexpected space after \'\'.', line: 3, column: 23, endColumn: 24 } + ] + }, + { + code: ` + + `, + options: ['never'], + output: ` + + `, + errors: [ + { message: 'Unexpected space after \'\'.', line: 3, column: 30, endColumn: 38 } + ] + }, + // exceptions + { + code: ` + + `, + options: ['always', { exceptions: ['+'] }], + output: null, + errors: [ + 'Expected space after exception block.', + 'Expected space before exception block.' + ] + }, + { + code: ` + + `, + options: ['always', { exceptions: ['*'] }], + output: null, + errors: [ + 'Expected space after exception block.', + 'Expected space before exception block.' + ] + }, + { + code: ` + + `, + options: ['always', { exceptions: ['#+#-'] }], + output: ` + + `, + errors: [ + 'Expected space after exception block.', + 'Expected space before \'-->\'.' + ] + }, + { + code: ` + + `, + options: ['always', { exceptions: ['*', '++'] }], + output: null, + errors: [ + 'Expected space after exception block.', + { message: 'Expected space before exception block.', line: 3, column: 27 } + ] + }, + { + code: ` + + `, + options: ['always', { exceptions: ['*', '++'] }], + output: null, + errors: [ + 'Expected space after exception block.', + { message: 'Expected space before exception block.', line: 3, column: 28 } + ] + } + ] + +}) diff --git a/tests/lib/rules/html-comment-indent.js b/tests/lib/rules/html-comment-indent.js new file mode 100644 index 000000000..e6224ef09 --- /dev/null +++ b/tests/lib/rules/html-comment-indent.js @@ -0,0 +1,648 @@ +/** + * @author Yosuke ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const rule = require('../../../lib/rules/html-comment-indent') + +const RuleTester = require('eslint').RuleTester + +const tester = new RuleTester({ + parser: 'vue-eslint-parser', + parserOptions: { + ecmaVersion: 2015 + } +}) +tester.run('html-comment-indent', rule, { + valid: [ + { + code: ` + + ` + }, + { + code: ` + + `, + options: ['tab'] + }, + { + code: ` + + `, + options: [4] + }, + { + code: ` + + `, + options: [0] + }, + { + code: ` + + ` + }, + + // IE conditional comments + { + code: ` + + ` + }, + { + code: ` + + ` + } + ], + + invalid: [ + { + code: ` + + `, + output: ` + + `, + errors: [{ + message: 'Expected relative indentation of 2 spaces but found 0 spaces.', + line: 4, + column: 11, + endLine: 4, + endColumn: 11 + }, + { + message: 'Expected base point indentation of 10 spaces, but found 7 spaces.', + line: 7, + column: 1, + endLine: 7, + endColumn: 8 + }, + { + message: 'Expected relative indentation of 2 spaces but found 5 spaces.', + line: 8, + column: 11, + endLine: 8, + endColumn: 16 + }, + { + message: 'Expected relative indentation of 0 spaces but found 1 space.', + line: 9, + column: 11, + endLine: 9, + endColumn: 12 + }, + { + message: 'Expected space character, but found tab character.', + line: 11, + column: 14, + endLine: 11, + endColumn: 15 + }, + { + message: 'Expected space character, but found tab character.', + line: 12, + column: 13, + endLine: 12, + endColumn: 14 + }, + { + message: 'Expected base point indentation of 12 spaces, but found 11 spaces.', + line: 13, + column: 1, + endLine: 13, + endColumn: 12 + }] + }, + { + code: ` + + `, + options: ['tab'], + output: ` + + `, + errors: [{ + message: 'Expected relative indentation of 1 tab but found 0 tabs.', + line: 4, + column: 11, + endLine: 4, + endColumn: 11 + }, + { + message: 'Expected base point indentation of 10 spaces, but found " \\t".', + line: 7, + column: 1, + endLine: 7, + endColumn: 10 + }, + { + message: 'Expected relative indentation of 1 tab but found 2 tabs.', + line: 8, + column: 11, + endLine: 8, + endColumn: 13 + }, + { + message: 'Expected relative indentation of 0 tabs but found 1 tab.', + line: 9, + column: 11, + endLine: 9, + endColumn: 12 + }, + { + message: 'Expected tab character, but found space character.', + line: 11, + column: 14, + endLine: 11, + endColumn: 15 + }, + { + message: 'Expected tab character, but found space character.', + line: 12, + column: 13, + endLine: 12, + endColumn: 14 + }, + { + message: 'Expected base point indentation of 12 spaces, but found 11 spaces.', + line: 13, + column: 1, + endLine: 13, + endColumn: 12 + }] + }, + { + code: ` + + `, + options: [4], + output: ` + + `, + errors: [{ + message: 'Expected relative indentation of 4 spaces but found 0 spaces.', + line: 4, + column: 11, + endLine: 4, + endColumn: 11 + }, + { + message: 'Expected base point indentation of 10 spaces, but found 5 spaces.', + line: 7, + column: 1, + endLine: 7, + endColumn: 6 + }, + { + message: 'Expected relative indentation of 4 spaces but found 8 spaces.', + line: 8, + column: 11, + endLine: 8, + endColumn: 19 + }, + { + message: 'Expected relative indentation of 0 spaces but found 2 spaces.', + line: 9, + column: 11, + endLine: 9, + endColumn: 13 + }, + { + message: 'Expected space character, but found tab character.', + line: 11, + column: 15, + endLine: 11, + endColumn: 16 + }, + { + message: 'Expected space character, but found tab character.', + line: 12, + column: 13, + endLine: 12, + endColumn: 14 + }, + { + message: 'Expected base point indentation of 12 spaces, but found 10 spaces.', + line: 13, + column: 1, + endLine: 13, + endColumn: 11 + }] + }, + { + code: ` + + `, + options: [0], + output: ` + + `, + errors: [{ + message: 'Expected relative indentation of 0 spaces but found 2 spaces.', + line: 4, + column: 11, + endLine: 4, + endColumn: 13 + }, + { + message: 'Expected base point indentation of 10 spaces, but found 8 spaces.', + line: 7, + column: 1, + endLine: 7, + endColumn: 9 + }, + { + message: 'Expected space character, but found tab character.', + line: 8, + column: 11, + endLine: 8, + endColumn: 12 + }, + { + message: 'Expected relative indentation of 0 spaces but found 2 spaces.', + line: 9, + column: 11, + endLine: 9, + endColumn: 13 + }, + { + message: 'Expected space character, but found tab character.', + line: 11, + column: 13, + endLine: 11, + endColumn: 14 + }, + { + message: 'Expected base point indentation of 12 spaces, but found " \\t".', + line: 12, + column: 1, + endLine: 12, + endColumn: 12 + }, + { + message: 'Expected base point indentation of 12 spaces, but found 10 spaces.', + line: 13, + column: 1, + endLine: 13, + endColumn: 11 + }] + }, + { + code: ` + + `, + output: ` + + `, + errors: [{ + message: 'Expected relative indentation of 2 spaces but found 0 spaces.', + line: 5 + }, + { + message: 'Expected relative indentation of 2 spaces but found 4 spaces.', + line: 7 + }, + { + message: 'Expected relative indentation of 0 spaces but found 2 spaces.', + line: 12 + }, + { + message: 'Expected base point indentation of 10 spaces, but found 8 spaces.', + line: 14 + }] + }, + { + code: ` + + `, + output: ` + + `, + errors: [{ + message: 'Expected relative indentation of 2 spaces but found 0 spaces.', + line: 4 + }, + { + message: 'Expected relative indentation of 2 spaces but found 4 spaces.', + line: 6 + }] + }, + { + code: ` + +`, + output: ` + +`, + errors: [{ + message: 'Expected indentation of 2 spaces but found 0 spaces.', + line: 4 + }, + { + message: 'Expected indentation of 2 spaces but found 0 spaces.', + line: 5 + }, + { + message: 'Expected indentation of 0 spaces but found 2 spaces.', + line: 7 + }] + }, + { + code: ` + +`, + output: ` + +`, + errors: [{ + message: 'Expected base point indentation of 2 spaces, but not found.', + line: 4 + }, + { + message: 'Expected base point indentation of 2 spaces, but not found.', + line: 5 + }, + { + message: 'Expected base point indentation of 2 spaces, but not found.', + line: 7 + }] + }, + { + code: ` + + `, + output: ` + + `, + errors: [{ + message: 'Expected relative indentation of 2 spaces but found 1 space.', + line: 4 + }, + { + message: 'Expected relative indentation of 0 spaces but found 1 space.', + line: 5 + }] + }, + { + code: ` + + `, + output: ` + + `, + errors: [{ + message: 'Expected base point indentation of " \\t \\t \\t ", but found 7 spaces.', + line: 4 + }, + { + message: 'Expected base point indentation of " \\t \\t \\t ", but found 7 spaces.', + line: 5 + }] + } + ] + +}) diff --git a/tests/lib/utils/html-comments.js b/tests/lib/utils/html-comments.js new file mode 100644 index 000000000..93b9eb794 --- /dev/null +++ b/tests/lib/utils/html-comments.js @@ -0,0 +1,69 @@ +'use strict' + +const fs = require('fs') +const path = require('path') +const chai = require('chai') + +const Linter = require('eslint').Linter +const assert = chai.assert + +const htmlComments = require('../../../lib/utils/html-comments') + +const FIXTURE_ROOT = path.resolve(__dirname, '../../fixtures/utils/html-comments') + +/** + * Load test patterns from fixtures. + * + * @returns {object} The loaded patterns. + */ +function loadPatterns () { + return fs + .readdirSync(FIXTURE_ROOT) + .map(name => { + const code0 = fs.readFileSync(path.join(FIXTURE_ROOT, name, 'source.vue'), 'utf8') + const code = code0.replace(/^/, `${name}`) + const option = JSON.parse(/^/.exec(code0)[1]) + return { code, name, option } + }) +} + +function tokenize (code, option) { + const linter = new Linter() + const result = [] + + linter.defineRule('vue/html-comments-test', content => + htmlComments.defineVisitor(content, option, commentTokens => { + result.push(commentTokens) + }) + ) + linter.verify( + code, + { + parser: 'vue-eslint-parser', + parserOptions: { ecmaVersion: 2018 }, + rules: { 'vue/html-comments-test': 'error' } + }, + undefined, + true + ) + + return result +} + +describe('defineVisitor()', () => { + for (const { name, code, option } of loadPatterns()) { + describe(`'test/fixtures/utils/html-comments/${name}/source.vue'`, () => { + it('should be parsed to valid tokens.', () => { + const tokens = tokenize(code, option) + + const actual = JSON.stringify(tokens, null, 4) + + // update fixture + // fs.writeFileSync(path.join(FIXTURE_ROOT, name, 'comment-tokens.json'), actual, 'utf8') + + const expected = fs.readFileSync(path.join(FIXTURE_ROOT, name, 'comment-tokens.json'), 'utf8') + assert.strictEqual(actual, expected) + }) + }) + } +}) From 75080038ff5ea69bbc74c3099e3b7690cafadade Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sun, 10 May 2020 09:09:01 +0900 Subject: [PATCH 2/3] update --- docs/rules/html-comment-content-newline.md | 2 +- docs/rules/html-comment-content-spacing.md | 4 +- lib/rules/html-comment-content-newline.js | 66 +++++++------ lib/rules/html-comment-content-spacing.js | 62 ++++++------ lib/rules/html-comment-indent.js | 24 +++-- lib/utils/html-comments.js | 99 +++++++++++++++---- .../lib/rules/html-comment-content-newline.js | 2 +- .../lib/rules/html-comment-content-spacing.js | 2 +- tests/lib/rules/html-comment-indent.js | 2 +- tests/lib/utils/html-comments.js | 1 + 10 files changed, 167 insertions(+), 97 deletions(-) diff --git a/docs/rules/html-comment-content-newline.md b/docs/rules/html-comment-content-newline.md index 523e32336..4a94ff7e3 100644 --- a/docs/rules/html-comment-content-newline.md +++ b/docs/rules/html-comment-content-newline.md @@ -66,7 +66,7 @@ This rule will enforce consistency of line break after the `` makes it easier to read text in ``` - The first is a string which be either `"always"` or `"never"`. The default is `"always"`. - - `"always"` ... there must be at least one whitespace at after the ``. + - `"always"` (default) ... there must be at least one whitespace at after the ``. - `"never"` ... there should be no whitespace at after the ``. @@ -54,7 +54,7 @@ Whitespace after the `` makes it easier to read text in - The `"exceptions"` value is an array of string patterns which are considered exceptions to the rule. Please note that exceptions are ignored if the first argument is `"never"`. - ``` + ```json "vue/html-comment-content-spacing": ["error", "always", { "exceptions": ["*"] }] ``` diff --git a/lib/rules/html-comment-content-newline.js b/lib/rules/html-comment-content-newline.js index 68c9b8286..d6f50608a 100644 --- a/lib/rules/html-comment-content-newline.js +++ b/lib/rules/html-comment-content-newline.js @@ -10,6 +10,10 @@ const htmlComments = require('../utils/html-comments') +/** + * @typedef { import('../utils/html-comments').HTMLComment } HTMLComment + */ + // ------------------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------------------ @@ -37,7 +41,7 @@ module.exports = { docs: { description: 'enforce unified line brake in HTML comments', - category: undefined, + categories: undefined, url: 'https://eslint.vuejs.org/rules/html-comment-content-newline.html' }, fixable: 'whitespace', @@ -82,98 +86,98 @@ module.exports = { create (context) { const option = parseOption(context.options[0]) - return htmlComments.defineVisitor(context, context.options[1], (commentTokens) => { - if (!commentTokens.value) { + return htmlComments.defineVisitor(context, context.options[1], (comment) => { + if (!comment.value) { return } - const startLine = commentTokens.openDecoration - ? commentTokens.openDecoration.loc.end.line - : commentTokens.value.loc.start.line - const endLine = commentTokens.closeDecoration - ? commentTokens.closeDecoration.loc.start.line - : commentTokens.value.loc.end.line + const startLine = comment.openDecoration + ? comment.openDecoration.loc.end.line + : comment.value.loc.start.line + const endLine = comment.closeDecoration + ? comment.closeDecoration.loc.start.line + : comment.value.loc.end.line const newlineType = startLine === endLine ? option.singleline : option.multiline if (newlineType === 'ignore') { return } - checkCommentOpen(commentTokens, newlineType !== 'never') - checkCommentClose(commentTokens, newlineType !== 'never') + checkCommentOpen(comment, newlineType !== 'never') + checkCommentClose(comment, newlineType !== 'never') }) /** * Reports the newline before the contents of a given comment if it's invalid. - * @param {string} commentTokens - comment tokens. + * @param {HTMLComment} comment - comment data. * @param {boolean} requireNewline - `true` if line breaks are required. * @returns {void} */ - function checkCommentOpen (commentTokens, requireNewline) { - const beforeToken = commentTokens.openDecoration || commentTokens.open + function checkCommentOpen (comment, requireNewline) { + const beforeToken = comment.openDecoration || comment.open if (requireNewline) { - if (beforeToken.loc.end.line < commentTokens.value.loc.start.line) { + if (beforeToken.loc.end.line < comment.value.loc.start.line) { // Is valid return } context.report({ loc: { start: beforeToken.loc.end, - end: commentTokens.value.loc.start + end: comment.value.loc.start }, - messageId: commentTokens.openDecoration ? 'expectedAfterExceptionBlock' : 'expectedAfterHTMLCommentOpen', - fix: commentTokens.openDecoration ? undefined : (fixer) => fixer.insertTextAfter(beforeToken, '\n') + messageId: comment.openDecoration ? 'expectedAfterExceptionBlock' : 'expectedAfterHTMLCommentOpen', + fix: comment.openDecoration ? undefined : (fixer) => fixer.insertTextAfter(beforeToken, '\n') }) } else { - if (beforeToken.loc.end.line === commentTokens.value.loc.start.line) { + if (beforeToken.loc.end.line === comment.value.loc.start.line) { // Is valid return } context.report({ loc: { start: beforeToken.loc.end, - end: commentTokens.value.loc.start + end: comment.value.loc.start }, messageId: 'unexpectedAfterHTMLCommentOpen', - fix: (fixer) => fixer.replaceTextRange([beforeToken.range[1], commentTokens.value.range[0]], ' ') + fix: (fixer) => fixer.replaceTextRange([beforeToken.range[1], comment.value.range[0]], ' ') }) } } /** * Reports the space after the contents of a given comment if it's invalid. - * @param {string} commentTokens - comment tokens. + * @param {HTMLComment} comment - comment data. * @param {boolean} requireNewline - `true` if line breaks are required. * @returns {void} */ - function checkCommentClose (commentTokens, requireNewline) { - const afterToken = commentTokens.closeDecoration || commentTokens.close + function checkCommentClose (comment, requireNewline) { + const afterToken = comment.closeDecoration || comment.close if (requireNewline) { - if (commentTokens.value.loc.end.line < afterToken.loc.start.line) { + if (comment.value.loc.end.line < afterToken.loc.start.line) { // Is valid return } context.report({ loc: { - start: commentTokens.value.loc.end, + start: comment.value.loc.end, end: afterToken.loc.start }, - messageId: commentTokens.closeDecoration ? 'expectedBeforeExceptionBlock' : 'expectedBeforeHTMLCommentOpen', - fix: commentTokens.closeDecoration ? undefined : (fixer) => fixer.insertTextBefore(afterToken, '\n') + messageId: comment.closeDecoration ? 'expectedBeforeExceptionBlock' : 'expectedBeforeHTMLCommentOpen', + fix: comment.closeDecoration ? undefined : (fixer) => fixer.insertTextBefore(afterToken, '\n') }) } else { - if (commentTokens.value.loc.end.line === afterToken.loc.start.line) { + if (comment.value.loc.end.line === afterToken.loc.start.line) { // Is valid return } context.report({ loc: { - start: commentTokens.value.loc.end, + start: comment.value.loc.end, end: afterToken.loc.start }, messageId: 'unexpectedBeforeHTMLCommentOpen', - fix: (fixer) => fixer.replaceTextRange([commentTokens.value.range[1], afterToken.range[0]], ' ') + fix: (fixer) => fixer.replaceTextRange([comment.value.range[1], afterToken.range[0]], ' ') }) } } diff --git a/lib/rules/html-comment-content-spacing.js b/lib/rules/html-comment-content-spacing.js index 956b8fcc8..b55e3e7dd 100644 --- a/lib/rules/html-comment-content-spacing.js +++ b/lib/rules/html-comment-content-spacing.js @@ -10,6 +10,10 @@ const htmlComments = require('../utils/html-comments') +/** + * @typedef { import('../utils/html-comments').HTMLComment } HTMLComment + */ + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -20,7 +24,7 @@ module.exports = { docs: { description: 'enforce unified spacing in HTML comments', - category: undefined, + categories: undefined, url: 'https://eslint.vuejs.org/rules/html-comment-content-spacing.html' }, fixable: 'whitespace', @@ -54,100 +58,100 @@ module.exports = { create (context) { // Unless the first option is never, require a space const requireSpace = context.options[0] !== 'never' - return htmlComments.defineVisitor(context, context.options[1], (commentTokens) => { - if (!commentTokens.value) { + return htmlComments.defineVisitor(context, context.options[1], (comment) => { + if (!comment.value) { return } - checkCommentOpen(commentTokens) - checkCommentClose(commentTokens) + checkCommentOpen(comment) + checkCommentClose(comment) }, { includeDirectives: true }) /** * Reports the space before the contents of a given comment if it's invalid. - * @param {string} commentTokens - comment tokens. + * @param {HTMLComment} comment - comment data. * @returns {void} */ - function checkCommentOpen (commentTokens) { - const beforeToken = commentTokens.openDecoration || commentTokens.open - if (beforeToken.loc.end.line !== commentTokens.value.loc.start.line) { + function checkCommentOpen (comment) { + const beforeToken = comment.openDecoration || comment.open + if (beforeToken.loc.end.line !== comment.value.loc.start.line) { // Ignore newline return } if (requireSpace) { - if (beforeToken.range[1] < commentTokens.value.range[0]) { + if (beforeToken.range[1] < comment.value.range[0]) { // Is valid return } context.report({ loc: { start: beforeToken.loc.end, - end: commentTokens.value.loc.start + end: comment.value.loc.start }, - messageId: commentTokens.openDecoration ? 'expectedAfterExceptionBlock' : 'expectedAfterHTMLCommentOpen', - fix: commentTokens.openDecoration ? undefined : (fixer) => fixer.insertTextAfter(beforeToken, ' ') + messageId: comment.openDecoration ? 'expectedAfterExceptionBlock' : 'expectedAfterHTMLCommentOpen', + fix: comment.openDecoration ? undefined : (fixer) => fixer.insertTextAfter(beforeToken, ' ') }) } else { - if (commentTokens.openDecoration) { + if (comment.openDecoration) { // Ignore expection block return } - if (beforeToken.range[1] === commentTokens.value.range[0]) { + if (beforeToken.range[1] === comment.value.range[0]) { // Is valid return } context.report({ loc: { start: beforeToken.loc.end, - end: commentTokens.value.loc.start + end: comment.value.loc.start }, messageId: 'unexpectedAfterHTMLCommentOpen', - fix: (fixer) => fixer.removeRange([beforeToken.range[1], commentTokens.value.range[0]]) + fix: (fixer) => fixer.removeRange([beforeToken.range[1], comment.value.range[0]]) }) } } /** * Reports the space after the contents of a given comment if it's invalid. - * @param {string} commentTokens - comment tokens. + * @param {HTMLComment} comment - comment data. * @returns {void} */ - function checkCommentClose (commentTokens) { - const afterToken = commentTokens.closeDecoration || commentTokens.close - if (commentTokens.value.loc.end.line !== afterToken.loc.start.line) { + function checkCommentClose (comment) { + const afterToken = comment.closeDecoration || comment.close + if (comment.value.loc.end.line !== afterToken.loc.start.line) { // Ignore newline return } if (requireSpace) { - if (commentTokens.value.range[1] < afterToken.range[0]) { + if (comment.value.range[1] < afterToken.range[0]) { // Is valid return } context.report({ loc: { - start: commentTokens.value.loc.end, + start: comment.value.loc.end, end: afterToken.loc.start }, - messageId: commentTokens.closeDecoration ? 'expectedBeforeExceptionBlock' : 'expectedBeforeHTMLCommentOpen', - fix: commentTokens.closeDecoration ? undefined : (fixer) => fixer.insertTextBefore(afterToken, ' ') + messageId: comment.closeDecoration ? 'expectedBeforeExceptionBlock' : 'expectedBeforeHTMLCommentOpen', + fix: comment.closeDecoration ? undefined : (fixer) => fixer.insertTextBefore(afterToken, ' ') }) } else { - if (commentTokens.closeDecoration) { + if (comment.closeDecoration) { // Ignore expection block return } - if (commentTokens.value.range[1] === afterToken.range[0]) { + if (comment.value.range[1] === afterToken.range[0]) { // Is valid return } context.report({ loc: { - start: commentTokens.value.loc.end, + start: comment.value.loc.end, end: afterToken.loc.start }, messageId: 'unexpectedBeforeHTMLCommentOpen', - fix: (fixer) => fixer.removeRange([commentTokens.value.range[1], afterToken.range[0]]) + fix: (fixer) => fixer.removeRange([comment.value.range[1], afterToken.range[0]]) }) } } diff --git a/lib/rules/html-comment-indent.js b/lib/rules/html-comment-indent.js index 7a6cae38f..0efe8b716 100644 --- a/lib/rules/html-comment-indent.js +++ b/lib/rules/html-comment-indent.js @@ -10,6 +10,10 @@ const htmlComments = require('../utils/html-comments') +/** + * @typedef { import('../utils/html-comments').HTMLComment } HTMLComment + */ + // ------------------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------------------ @@ -70,7 +74,7 @@ module.exports = { docs: { description: 'enforce consistent indentation in HTML comments', - category: undefined, + categories: undefined, url: 'https://eslint.vuejs.org/rules/html-comment-indent.html' }, fixable: 'whitespace', @@ -94,25 +98,25 @@ module.exports = { create (context) { const options = parseOptions(context.options[0]) const sourceCode = context.getSourceCode() - return htmlComments.defineVisitor(context, null, (commentTokens) => { - const baseIndentText = getLineIndentText(commentTokens.open.loc.start.line) + return htmlComments.defineVisitor(context, null, (comment) => { + const baseIndentText = getLineIndentText(comment.open.loc.start.line) let endLine - if (commentTokens.value) { - const startLine = commentTokens.value.loc.start.line - endLine = commentTokens.value.loc.end.line + if (comment.value) { + const startLine = comment.value.loc.start.line + endLine = comment.value.loc.end.line - const checkStartLine = commentTokens.open.loc.end.line === startLine ? startLine + 1 : startLine + const checkStartLine = comment.open.loc.end.line === startLine ? startLine + 1 : startLine for (let line = checkStartLine; line <= endLine; line++) { validateIndentForLine(line, baseIndentText, 1) } } else { - endLine = commentTokens.open.loc.end.line + endLine = comment.open.loc.end.line } - if (endLine < commentTokens.close.loc.start.line) { + if (endLine < comment.close.loc.start.line) { // `-->` - validateIndentForLine(commentTokens.close.loc.start.line, baseIndentText, 0) + validateIndentForLine(comment.close.loc.start.line, baseIndentText, 0) } }, { includeDirectives: true }) diff --git a/lib/utils/html-comments.js b/lib/utils/html-comments.js index 81f145dbb..50498bbe6 100644 --- a/lib/utils/html-comments.js +++ b/lib/utils/html-comments.js @@ -1,25 +1,72 @@ +/** + * @typedef { import('eslint').SourceCode } SourceCode + * @typedef { import('eslint').Rule.RuleContext } RuleContext + * @typedef { import('vue-eslint-parser').AST.Token } ASTToken + * @typedef { import('vue-eslint-parser').AST.HasLocation } HasLocation + */ + +/** + * @typedef { { exceptions?: string[] } } CommentParserConfig + * @typedef { (comment: HTMLComment) => void } HTMLCommentVisitor + * @typedef { { includeDirectives?: boolean } } CommentVisitorOption + * @typedef { ASTToken & { type: 'HTMLComment' } } HTMLCommentToken + * + * @typedef { ASTToken & { type: 'HTMLCommentOpen' } } HTMLCommentOpen + * @typedef { ASTToken & { type: 'HTMLCommentOpenDecoration' } } HTMLCommentOpenDecoration + * @typedef { ASTToken & { type: 'HTMLCommentValue' } } HTMLCommentValue + * @typedef { ASTToken & { type: 'HTMLCommentClose' } } HTMLCommentClose + * @typedef { ASTToken & { type: 'HTMLCommentCloseDecoration' } } HTMLCommentCloseDecoration + * @typedef { { open: HTMLCommentOpen, openDecoration: HTMLCommentOpenDecoration | null, value: HTMLCommentValue | null, closeDecoration: HTMLCommentCloseDecoration | null, close: HTMLCommentClose } } HTMLComment + */ +// ----------------------------------------------------------------------------- +// Requirements +// ----------------------------------------------------------------------------- + const utils = require('./') +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + const COMMENT_DIRECTIVE = /^\s*eslint-(?:en|dis)able/ const IE_CONDITIONAL_IF = /^\[if\s+/ const IE_CONDITIONAL_ENDIF = /\[endif\]$/ +/** @type { 'HTMLCommentOpen' } */ +const TYPE_HTML_COMMENT_OPEN = 'HTMLCommentOpen' +/** @type { 'HTMLCommentOpenDecoration' } */ +const TYPE_HTML_COMMENT_OPEN_DECORATION = 'HTMLCommentOpenDecoration' +/** @type { 'HTMLCommentValue' } */ +const TYPE_HTML_COMMENT_VALUE = 'HTMLCommentValue' +/** @type { 'HTMLCommentClose' } */ +const TYPE_HTML_COMMENT_CLOSE = 'HTMLCommentClose' +/** @type { 'HTMLCommentCloseDecoration' } */ +const TYPE_HTML_COMMENT_CLOSE_DECORATION = 'HTMLCommentCloseDecoration' + +/** + * @param {HTMLCommentToken} comment + * @returns {boolean} + */ function isCommentDirective (comment) { return COMMENT_DIRECTIVE.test(comment.value) } +/** + * @param {HTMLCommentToken} comment + * @returns {boolean} + */ function isIEConditionalComment (comment) { return IE_CONDITIONAL_IF.test(comment.value) || IE_CONDITIONAL_ENDIF.test(comment.value) } /** - * Define HTML comment tokenizer + * Define HTML comment parser * * @param {SourceCode} sourceCode The source code instance. - * @param {object} config The config. - * @returns {object} HTML comment tokenizer. + * @param {CommentParserConfig} config The config. + * @returns { (node: ASTToken) => (HTMLComment | null) } HTML comment parser. */ -function defineTokenizer (sourceCode, config) { +function defineParser (sourceCode, config) { config = config || {} const exceptions = config.exceptions || [] @@ -67,11 +114,11 @@ function defineTokenizer (sourceCode, config) { } /** - * Tokenize HTMLComment. - * @param {ASTNode} node a comment token - * @returns {object} the result of HTMLComment tokens. + * Parse HTMLComment. + * @param {ASTToken} node a comment token + * @returns {HTMLComment | null} the result of HTMLComment tokens. */ - return function tokenizeHTMLComment (node) { + return function parseHTMLComment (node) { if (node.type !== 'HTMLComment') { // Is not HTMLComment return null @@ -103,7 +150,13 @@ function defineTokenizer (sourceCode, config) { } let tokenIndex = node.range[0] + /** + * @param {string} type + * @param {string} value + * @returns {any} + */ const createToken = (type, value) => { + /** @type {[number,number]} */ const range = [tokenIndex, tokenIndex + value.length] tokenIndex = range[1] let loc @@ -122,13 +175,19 @@ function defineTokenizer (sourceCode, config) { } } } - const open = createToken('HTMLCommentOpen', '') + /** @type {HTMLCommentCloseDecoration | null} */ + const closeDecoration = closeDecorationText ? createToken(TYPE_HTML_COMMENT_CLOSE_DECORATION, closeDecorationText) : null + /** @type {HTMLCommentClose} */ + const close = createToken(TYPE_HTML_COMMENT_CLOSE, '-->') return { /** HTML comment open (`