From 0c0ef9ab8de94ea653fac5af58cd91151675be1b Mon Sep 17 00:00:00 2001 From: barthy Date: Sat, 12 Mar 2022 19:28:37 +0100 Subject: [PATCH 1/9] added rule/empty-line-between-options --- docs/rules/README.md | 1 + docs/rules/empty-line-between-options.md | 35 ++++ lib/configs/no-layout-rules.js | 1 + lib/index.js | 1 + lib/rules/empty-line-between-options.js | 197 ++++++++++++++++++ tests/lib/rules/empty-line-between-options.js | 125 +++++++++++ 6 files changed, 360 insertions(+) create mode 100644 docs/rules/empty-line-between-options.md create mode 100644 lib/rules/empty-line-between-options.js create mode 100644 tests/lib/rules/empty-line-between-options.js diff --git a/docs/rules/README.md b/docs/rules/README.md index 3d4a9b2e3..9b08158d5 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -313,6 +313,7 @@ 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/component-options-name-casing](./component-options-name-casing.md) | enforce the casing of component name in `components` options | :wrench::bulb: | | [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name | | +| [vue/empty-line-between-options](./empty-line-between-options.md) | enforce empty lines between top-level options | :wrench: | | [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute | | | [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: | diff --git a/docs/rules/empty-line-between-options.md b/docs/rules/empty-line-between-options.md new file mode 100644 index 000000000..b5829c9b8 --- /dev/null +++ b/docs/rules/empty-line-between-options.md @@ -0,0 +1,35 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/empty-line-between-options +description: enforce empty lines between top-level options +--- +# vue/empty-line-between-options + +> enforce empty lines between top-level options + +- :exclamation: ***This rule has not been released yet.*** +- :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 .... + + + +```vue + +``` + + + +## :wrench: Options + +Nothing. + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/empty-line-between-options.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/empty-line-between-options.js) diff --git a/lib/configs/no-layout-rules.js b/lib/configs/no-layout-rules.js index 4c88287af..0a9a4313a 100644 --- a/lib/configs/no-layout-rules.js +++ b/lib/configs/no-layout-rules.js @@ -15,6 +15,7 @@ module.exports = { 'vue/comma-spacing': 'off', 'vue/comma-style': 'off', 'vue/dot-location': 'off', + 'vue/empty-line-between-options': 'off', 'vue/first-attribute-linebreak': 'off', 'vue/func-call-spacing': 'off', 'vue/html-closing-bracket-newline': 'off', diff --git a/lib/index.js b/lib/index.js index 97ff805d3..4187a9487 100644 --- a/lib/index.js +++ b/lib/index.js @@ -29,6 +29,7 @@ module.exports = { 'custom-event-name-casing': require('./rules/custom-event-name-casing'), 'dot-location': require('./rules/dot-location'), 'dot-notation': require('./rules/dot-notation'), + 'empty-line-between-options': require('./rules/empty-line-between-options'), eqeqeq: require('./rules/eqeqeq'), 'experimental-script-setup-vars': require('./rules/experimental-script-setup-vars'), 'first-attribute-linebreak': require('./rules/first-attribute-linebreak'), diff --git a/lib/rules/empty-line-between-options.js b/lib/rules/empty-line-between-options.js new file mode 100644 index 000000000..7ecd885dc --- /dev/null +++ b/lib/rules/empty-line-between-options.js @@ -0,0 +1,197 @@ +/** + * @author Barthy Bonhomme + * @author Sergio Arbeo + * Adapted from https://github.com/DockYard/eslint-plugin-ember-suave/blob/master/lib/rules/lines-between-object-properties.js + * + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const path = require('path') +const utils = require('../utils') + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +// ... + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'layout', + docs: { + description: 'enforce empty lines between top-level options', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/empty-line-between-options.html' + }, + fixable: 'whitespace', + schema: [{ enum: ['always', 'never'] }], + messages: { + never: 'Unexpected blank line between Vue component options.', + always: 'Expected blank line between Vue component options.' + } + }, + /** @param {RuleContext} context */ + create(context) { + const isVueFile = utils.isVueFile(context.getFilename()) + if (!isVueFile) { + return {} + } + + const sourceCode = context.getSourceCode() + const shouldPad = (context.options[0] || 'always') === 'always' + + const fixFunctions = { + /** + * Removes newlines between component options + * @param {RuleFixer} fixer + * @param {Token} currentLast + * @param {Token} nextFirst + * @return {Fix} + */ + never(fixer, currentLast, nextFirst) { + return fixer.replaceTextRange( + [currentLast.range[1], nextFirst.range[0]], + ',\n' + ) + }, + /** + * Add newlines between component options + * @param {RuleFixer} fixer + * @param {Token} currentLast + * @return {Fix} + */ + always(fixer, currentLast /*, nextFirst*/) { + const tokenAfterLastToken = sourceCode.getTokenAfter(currentLast) + const tokenToLineBreakAfter = + tokenAfterLastToken.value === ',' ? tokenAfterLastToken : currentLast + + return fixer.insertTextAfter(tokenToLineBreakAfter, '\n') + } + } + + /** + * Checks if there is an empty line between two tokens + * @param {Token} first The first token + * @param {Token} second The second token + * @returns {boolean} True if there is at least a line between the tokens + */ + function isPaddingBetweenTokens(first, second) { + const comments = sourceCode.getCommentsBefore(second) + const len = comments.length + + // If there is no comments + if (len === 0) { + const linesBetweenFstAndSnd = + second.loc.start.line - first.loc.end.line - 1 + + return linesBetweenFstAndSnd >= 1 + } + + // If there are comments + let sumOfCommentLines = 0 // the numbers of lines of comments + let prevCommentLineNum = -1 // line number of the end of the previous comment + + for (let i = 0; i < len; i++) { + const commentLinesOfThisComment = + comments[i].loc.end.line - comments[i].loc.start.line + 1 + + sumOfCommentLines += commentLinesOfThisComment + + /* + * If this comment and the previous comment are in the same line, + * the count of comment lines is duplicated. So decrement sumOfCommentLines. + */ + if (prevCommentLineNum === comments[i].loc.start.line) { + sumOfCommentLines -= 1 + } + + prevCommentLineNum = comments[i].loc.end.line + } + + /* + * If the first block and the first comment are in the same line, + * the count of comment lines is duplicated. So decrement sumOfCommentLines. + */ + if (first.loc.end.line === comments[0].loc.start.line) { + sumOfCommentLines -= 1 + } + + /* + * If the last comment and the second block are in the same line, + * the count of comment lines is duplicated. So decrement sumOfCommentLines. + */ + if (comments[len - 1].loc.end.line === second.loc.start.line) { + sumOfCommentLines -= 1 + } + + const linesBetweenFstAndSnd = + second.loc.start.line - first.loc.end.line - 1 + + return linesBetweenFstAndSnd - sumOfCommentLines >= 1 + } + + /** + * Report error based on configuration + * @param {ASTNode} node Where to report errors + * @param {boolean} isPadded True if the option is followed by an empty line + * @param {Token} currentLast End of checked token + * @param {Token} nextFirst Start of next token + */ + function reportError(node, isPadded, currentLast, nextFirst) { + const key = isPadded ? 'never' : 'always' + const fixFunction = fixFunctions[key] + + context.report({ + node, + messageId: key, + fix: (fixer) => fixFunction(fixer, currentLast, nextFirst) + }) + } + + /** + * Compares options and decides what to do + * @param {ASTNode} option current option to check + * @param {ASTNode} nextNode next node to check against + */ + function checkOption(option, nextNode) { + const currentLast = sourceCode.getLastToken(option) + const nextFirst = sourceCode.getFirstToken(nextNode) + const isPadded = isPaddingBetweenTokens(currentLast, nextFirst) + + if (shouldPad === isPadded) { + return + } + + reportError(nextNode, isPadded, currentLast, nextFirst) + } + + return { + /** + * @param {import('vue-eslint-parser/ast').Node} node + */ + ObjectExpression(node) { + if (node.parent && node.parent.type !== 'ExportDefaultDeclaration') { + return + } + + const { properties } = node + + for (let i = 0; i < properties.length - 1; i++) { + const property = properties[i] + const nextProperty = properties[i + 1] + + checkOption(property, nextProperty) + } + } + } + } +} diff --git a/tests/lib/rules/empty-line-between-options.js b/tests/lib/rules/empty-line-between-options.js new file mode 100644 index 000000000..27c27d9d8 --- /dev/null +++ b/tests/lib/rules/empty-line-between-options.js @@ -0,0 +1,125 @@ +/** + * @author Barthy Bonhomme (https://github.com/barthy-koeln) + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/empty-line-between-options') + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('empty-line-between-options', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + + + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + + + `, + output: ` + + + + `, + errors: [ + { + message: 'Expected blank line between Vue component options.', + line: 12, + column: 9 + }, + { + message: 'Expected blank line between Vue component options.', + line: 15, + column: 19 + }, + { + message: 'Expected blank line between Vue component options.', + line: 16, + column: 9 + }, + { + message: 'Expected blank line between Vue component options.', + line: 19, + column: 9 + } + ] + } + ] +}) From ea082d5618bf6ac2498e618d3721b805bbe04935 Mon Sep 17 00:00:00 2001 From: barthy Date: Wed, 16 Mar 2022 19:22:31 +0100 Subject: [PATCH 2/9] added rule --- lib/rules/empty-line-between-options.js | 132 +++++------------- tests/lib/rules/empty-line-between-options.js | 78 ++++++++++- 2 files changed, 112 insertions(+), 98 deletions(-) diff --git a/lib/rules/empty-line-between-options.js b/lib/rules/empty-line-between-options.js index 7ecd885dc..f586223e9 100644 --- a/lib/rules/empty-line-between-options.js +++ b/lib/rules/empty-line-between-options.js @@ -1,8 +1,5 @@ /** - * @author Barthy Bonhomme - * @author Sergio Arbeo - * Adapted from https://github.com/DockYard/eslint-plugin-ember-suave/blob/master/lib/rules/lines-between-object-properties.js - * + * @author Barthy Bonhomme (https://github.com/barthy-koeln) * See LICENSE file in root directory for full license. */ 'use strict' @@ -11,7 +8,6 @@ // Requirements // ------------------------------------------------------------------------------ -const path = require('path') const utils = require('../utils') // ------------------------------------------------------------------------------ @@ -52,126 +48,74 @@ module.exports = { const fixFunctions = { /** * Removes newlines between component options + * * @param {RuleFixer} fixer - * @param {Token} currentLast - * @param {Token} nextFirst + * @param {number} endOfCurrent + * @param {number} startOfNext * @return {Fix} */ - never(fixer, currentLast, nextFirst) { - return fixer.replaceTextRange( - [currentLast.range[1], nextFirst.range[0]], - ',\n' - ) + never(fixer, endOfCurrent, startOfNext) { + return fixer.replaceTextRange([endOfCurrent, startOfNext], '\n') }, /** - * Add newlines between component options + * Add newlines between component options. + * * @param {RuleFixer} fixer - * @param {Token} currentLast + * @param {number} endOfCurrent * @return {Fix} */ - always(fixer, currentLast /*, nextFirst*/) { - const tokenAfterLastToken = sourceCode.getTokenAfter(currentLast) - const tokenToLineBreakAfter = - tokenAfterLastToken.value === ',' ? tokenAfterLastToken : currentLast - - return fixer.insertTextAfter(tokenToLineBreakAfter, '\n') - } - } - - /** - * Checks if there is an empty line between two tokens - * @param {Token} first The first token - * @param {Token} second The second token - * @returns {boolean} True if there is at least a line between the tokens - */ - function isPaddingBetweenTokens(first, second) { - const comments = sourceCode.getCommentsBefore(second) - const len = comments.length - - // If there is no comments - if (len === 0) { - const linesBetweenFstAndSnd = - second.loc.start.line - first.loc.end.line - 1 - - return linesBetweenFstAndSnd >= 1 - } - - // If there are comments - let sumOfCommentLines = 0 // the numbers of lines of comments - let prevCommentLineNum = -1 // line number of the end of the previous comment - - for (let i = 0; i < len; i++) { - const commentLinesOfThisComment = - comments[i].loc.end.line - comments[i].loc.start.line + 1 - - sumOfCommentLines += commentLinesOfThisComment - - /* - * If this comment and the previous comment are in the same line, - * the count of comment lines is duplicated. So decrement sumOfCommentLines. - */ - if (prevCommentLineNum === comments[i].loc.start.line) { - sumOfCommentLines -= 1 - } - - prevCommentLineNum = comments[i].loc.end.line - } - - /* - * If the first block and the first comment are in the same line, - * the count of comment lines is duplicated. So decrement sumOfCommentLines. - */ - if (first.loc.end.line === comments[0].loc.start.line) { - sumOfCommentLines -= 1 + always(fixer, endOfCurrent /*, startOfNext*/) { + return fixer.insertTextAfterRange([0, endOfCurrent], '\n') } - - /* - * If the last comment and the second block are in the same line, - * the count of comment lines is duplicated. So decrement sumOfCommentLines. - */ - if (comments[len - 1].loc.end.line === second.loc.start.line) { - sumOfCommentLines -= 1 - } - - const linesBetweenFstAndSnd = - second.loc.start.line - first.loc.end.line - 1 - - return linesBetweenFstAndSnd - sumOfCommentLines >= 1 } /** - * Report error based on configuration + * Report error based on configuration. + * * @param {ASTNode} node Where to report errors * @param {boolean} isPadded True if the option is followed by an empty line - * @param {Token} currentLast End of checked token - * @param {Token} nextFirst Start of next token + * @param {number} endOfCurrent End of checked token + * @param {number} startOfNext Start of next token */ - function reportError(node, isPadded, currentLast, nextFirst) { + function reportError(node, isPadded, endOfCurrent, startOfNext) { const key = isPadded ? 'never' : 'always' const fixFunction = fixFunctions[key] context.report({ node, messageId: key, - fix: (fixer) => fixFunction(fixer, currentLast, nextFirst) + fix: (fixer) => fixFunction(fixer, endOfCurrent, startOfNext) }) } /** - * Compares options and decides what to do - * @param {ASTNode} option current option to check - * @param {ASTNode} nextNode next node to check against + * Compares options and decides what to do. + * This takes into account comments before options, but not empty lines between multiple comments. + * + * @param {ASTNode} current current option to check + * @param {ASTNode} next next node to check against */ - function checkOption(option, nextNode) { - const currentLast = sourceCode.getLastToken(option) - const nextFirst = sourceCode.getFirstToken(nextNode) - const isPadded = isPaddingBetweenTokens(currentLast, nextFirst) + function checkOption(current, next) { + const endOfCurrent = + sourceCode.getIndexFromLoc({ + line: current.loc.end.line + 1, + column: 0 + }) - 1 /* start of next line, -1 for previous line */ + + const comments = sourceCode.getCommentsBefore(next) + const nextNode = comments.length ? comments[0] : next + + const startOfNext = sourceCode.getIndexFromLoc({ + line: nextNode.loc.start.line, + column: 0 + }) + const isPadded = startOfNext !== endOfCurrent + 1 if (shouldPad === isPadded) { return } - reportError(nextNode, isPadded, currentLast, nextFirst) + reportError(next, isPadded, endOfCurrent, startOfNext) } return { diff --git a/tests/lib/rules/empty-line-between-options.js b/tests/lib/rules/empty-line-between-options.js index 27c27d9d8..514f58910 100644 --- a/tests/lib/rules/empty-line-between-options.js +++ b/tests/lib/rules/empty-line-between-options.js @@ -100,26 +100,96 @@ tester.run('empty-line-between-options', rule, { `, errors: [ { - message: 'Expected blank line between Vue component options.', + message: rule.meta.messages.always, line: 12, column: 9 }, { - message: 'Expected blank line between Vue component options.', + message: rule.meta.messages.always, line: 15, column: 19 }, { - message: 'Expected blank line between Vue component options.', + message: rule.meta.messages.always, line: 16, column: 9 }, { - message: 'Expected blank line between Vue component options.', + message: rule.meta.messages.always, line: 19, column: 9 } ] + }, + { + filename: 'test-never.vue', + code: ` + + + + `, + output: ` + + + + `, + errors: [ + { + message: rule.meta.messages.never, + line: 11, + column: 9 + }, + { + message: rule.meta.messages.never, + line: 15, + column: 19 + }, + { + message: rule.meta.messages.never, + line: 17, + column: 9 + }, + { + message: rule.meta.messages.never, + line: 21, + column: 9 + } + ], + options: ['never'] } ] }) From af7d4e674a082421015cf0fdc8538c0e880f9f0c Mon Sep 17 00:00:00 2001 From: barthy Date: Wed, 16 Mar 2022 19:49:54 +0100 Subject: [PATCH 3/9] added docs --- docs/rules/empty-line-between-options.md | 96 ++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 5 deletions(-) diff --git a/docs/rules/empty-line-between-options.md b/docs/rules/empty-line-between-options.md index b5829c9b8..e875aa319 100644 --- a/docs/rules/empty-line-between-options.md +++ b/docs/rules/empty-line-between-options.md @@ -13,21 +13,107 @@ description: enforce empty lines between top-level options ## :book: Rule Details -This rule .... +This rule enforces consistent format of empty lines between Vue component options by either adding or removing them. - + ```vue -