diff --git a/docs/rules/max-attributes-per-line.md b/docs/rules/max-attributes-per-line.md new file mode 100644 index 000000000..8493465b6 --- /dev/null +++ b/docs/rules/max-attributes-per-line.md @@ -0,0 +1,122 @@ +# Define the number of attributes allows per line (max-attributes-per-line) + +Limits the maximum number of attributes/properties per line to improve readability. + + +## :book: Rule Details + +This rule aims to enforce a number of attributes per line in templates. +It checks all the elements in a template and verifies that the number of attributes per line does not exceed the defined maximum. +An attribute is considered to be in a new line when there is a line break between two attributes. + +There is a configurable number of attributes that are acceptable in one-line case (default 3), as well as how many attributes are acceptable per line in multi-line case (default 1). + +:-1: Examples of **incorrect** code for this rule: + +```html + + + + + +``` + +:+1: Examples of **correct** code for this rule: + +```html + + + + + + +``` + +### :wrench: Options + +``` +{ + "vue/max-attributes-per-line": [{ + "singleline": 3, + "multiline": { + max: 1, + allowFirstLine: false + } + }] +} +``` + +#### `allowFirstLine` +For multi-line declarations, defines if allows attributes to be put in the first line. (Default false) + +:-1: Example of **incorrect** code for this setting: +```html +// [{ "multiline": { "allowFirstLine": false }}] + +; +``` + +:+1: Example of **correct** code for this setting: +```html +// [{ "multiline": { "allowFirstLine": false }}] + +; +``` + + +#### `singleline` +Number of maximum attributes per line when the opening tag is in a single line. (Default is 3) + +:-1: Example of **incorrect** code for this setting: +```html +// [{"singleline": 2,}] +; +``` + +:+1: Example of **correct** code for this setting: +```html +// [{"singleline": 3,}] +; +``` + + +#### `multiline` +Number of maximum attributes per line when a tag is in multiple lines. (Default is 1) + +:-1: Example of **incorrect** code for this setting: +```html +// [{"multiline": 1}] + +; +``` + +:+1: Example of **correct** code for this setting: +```html +// [{"multiline": 1}] + +; +``` + +## When Not To Use It + +If you do not want to check the number of attributes declared per line you can disable this rule. + diff --git a/lib/rules/max-attributes-per-line.js b/lib/rules/max-attributes-per-line.js new file mode 100644 index 000000000..f5309e4a1 --- /dev/null +++ b/lib/rules/max-attributes-per-line.js @@ -0,0 +1,161 @@ +/** + * @fileoverview Define the number of attributes allows per line + * @author Filipa Lacerda + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ +const utils = require('../utils') + +module.exports = { + meta: { + docs: { + description: 'Define the number of attributes allows per line', + category: 'Stylistic Issues', + recommended: false + }, + fixable: null, + schema: [ + { + type: 'object', + properties: { + singleline: { + anyOf: [ + { + type: 'number', + minimum: 1 + }, + { + type: 'object', + properties: { + max: { + type: 'number', + minimum: 1 + } + }, + additionalProperties: false + } + ] + }, + multiline: { + anyOf: [ + { + type: 'number', + minimum: 1 + }, + { + type: 'object', + properties: { + max: { + type: 'number', + minimum: 1 + }, + allowFirstLine: { + type: 'boolean' + } + }, + additionalProperties: false + } + ] + } + } + } + ] + }, + + create: function (context) { + const configuration = parseOptions(context.options[0]) + const multilineMaximum = configuration.multiline + const singlelinemMaximum = configuration.singleline + const canHaveFirstLine = configuration.allowFirstLine + + utils.registerTemplateBodyVisitor(context, { + 'VStartTag' (node) { + const numberOfAttributes = node.attributes.length + + if (!numberOfAttributes) return + + if (utils.isSingleLine(node) && numberOfAttributes > singlelinemMaximum) { + showErrors(node.attributes.slice(singlelinemMaximum), node) + } + + if (!utils.isSingleLine(node)) { + if (!canHaveFirstLine && node.attributes[0].loc.start.line === node.loc.start.line) { + showErrors([node.attributes[0]], node) + } + + groupAttrsByLine(node.attributes) + .filter(attrs => attrs.length > multilineMaximum) + .forEach(attrs => showErrors(attrs.splice(multilineMaximum), node)) + } + } + }) + + // ---------------------------------------------------------------------- + // Helpers + // ---------------------------------------------------------------------- + function parseOptions (options) { + const defaults = { + singleline: 3, + multiline: 1, + allowFirstLine: false + } + + if (options) { + if (typeof options.singleline === 'number') { + defaults.singleline = options.singleline + } else if (options.singleline && options.singleline.max) { + defaults.singleline = options.singleline.max + } + + if (options.multiline) { + if (typeof options.multiline === 'number') { + defaults.multiline = options.multiline + } else if (typeof options.multiline === 'object') { + if (options.multiline.max) { + defaults.multiline = options.multiline.max + } + + if (options.multiline.allowFirstLine) { + defaults.allowFirstLine = options.multiline.allowFirstLine + } + } + } + } + + return defaults + } + + function showErrors (attributes, node) { + attributes.forEach((prop) => { + context.report({ + node: prop, + loc: prop.loc, + message: 'Attribute "{{propName}}" should be on a new line.', + data: { + propName: prop.key.name + } + }) + }) + } + + function groupAttrsByLine (attributes) { + const propsPerLine = [[attributes[0]]] + + attributes.reduce((previous, current) => { + if (previous.loc.end.line === current.loc.start.line) { + propsPerLine[propsPerLine.length - 1].push(current) + } else { + propsPerLine.push([current]) + } + return current + }) + + return propsPerLine + } + + return {} + } +} diff --git a/lib/utils/index.js b/lib/utils/index.js index b2a5bacbf..2487ddd9a 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -587,5 +587,14 @@ module.exports = { } } } + }, + + /** + * Check whether the component is declared in a single line or not. + * @param {ASTNode} node + * @returns {boolean} + */ + isSingleLine (node) { + return node.loc.start.line === node.loc.end.line } } diff --git a/tests/lib/rules/max-attributes-per-line.js b/tests/lib/rules/max-attributes-per-line.js new file mode 100644 index 000000000..07d24197c --- /dev/null +++ b/tests/lib/rules/max-attributes-per-line.js @@ -0,0 +1,191 @@ +/** + * @fileoverview Define the number of attributes allows per line + * @author Filipa Lacerda + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/max-attributes-per-line') + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + parser: 'vue-eslint-parser', + parserOptions: { ecmaVersion: 2015 } +}) + +ruleTester.run('max-attributes-per-line', rule, { + valid: [ + { + code: `` + }, + { + code: `` + }, + { + code: ``, + options: [{ multiline: { allowFirstLine: true }}] + }, + { + code: `` + }, + { + code: ``, + options: [{ singleline: 1 }] + }, + { + code: ``, + options: [{ singleline: 1, multiline: { max: 1, allowFirstLine: false }}] + }, + { + code: ``, + options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: false }}] + }, + { + code: ``, + options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: true }}] + }, + { + code: ``, + options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: false }}] + }, + { + code: ``, + options: [{ singleline: 3, multiline: { max: 2, allowFirstLine: false }}] + } + ], + + invalid: [ + { + code: ``, + errors: ['Attribute "petname" should be on a new line.'] + }, + { + code: ``, + errors: [{ + message: 'Attribute "job" should be on a new line.', + type: 'VAttribute', + line: 1 + }] + }, + { + code: ``, + options: [{ singleline: { max: 1 }}], + errors: [{ + message: 'Attribute "age" should be on a new line.', + type: 'VAttribute', + line: 1 + }] + }, + { + code: ``, + options: [{ singleline: 1, multiline: { max: 1, allowFirstLine: false }}], + errors: [{ + message: 'Attribute "age" should be on a new line.', + type: 'VAttribute', + line: 1 + }, { + message: 'Attribute "job" should be on a new line.', + type: 'VAttribute', + line: 1 + }] + }, + { + code: ``, + options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: false }}], + errors: [{ + message: 'Attribute "name" should be on a new line.', + type: 'VAttribute', + line: 1 + }] + }, + { + code: ``, + options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: false }}], + errors: [{ + message: 'Attribute "age" should be on a new line.', + type: 'VAttribute', + line: 2 + }] + }, + { + code: ``, + options: [{ singleline: 3, multiline: 1 }], + errors: [{ + message: 'Attribute "age" should be on a new line.', + type: 'VAttribute', + line: 2 + }] + }, + { + code: ``, + options: [{ singleline: 3, multiline: { max: 2, allowFirstLine: false }}], + errors: [{ + message: 'Attribute "petname" should be on a new line.', + type: 'VAttribute', + line: 3 + }] + }, + { + code: ``, + options: [{ singleline: 3, multiline: { max: 2, allowFirstLine: false }}], + errors: [{ + message: 'Attribute "petname" should be on a new line.', + type: 'VAttribute', + line: 3 + }, { + message: 'Attribute "extra" should be on a new line.', + type: 'VAttribute', + line: 3 + }] + } + ] +})