diff --git a/.eslintrc.js b/.eslintrc.js index 4b8396bec..4b2731d67 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,7 +19,7 @@ module.exports = { 'eslint-plugin' ], rules: { - 'eslint-plugin/report-message-format': ['error', '^[A-Z`\'].*\\.$'], + 'eslint-plugin/report-message-format': ['error', '^[A-Z`\'{].*\\.$'], 'eslint-plugin/prefer-placeholders': 'error', 'eslint-plugin/consistent-output': 'error' }, diff --git a/lib/rules/max-attributes-per-line.js b/lib/rules/max-attributes-per-line.js index 517a4ef89..421e55394 100644 --- a/lib/rules/max-attributes-per-line.js +++ b/lib/rules/max-attributes-per-line.js @@ -70,6 +70,7 @@ module.exports = { const multilineMaximum = configuration.multiline const singlelinemMaximum = configuration.singleline const canHaveFirstLine = configuration.allowFirstLine + const template = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore() return utils.defineTemplateBodyVisitor(context, { 'VStartTag' (node) { @@ -78,17 +79,17 @@ module.exports = { if (!numberOfAttributes) return if (utils.isSingleLine(node) && numberOfAttributes > singlelinemMaximum) { - showErrors(node.attributes.slice(singlelinemMaximum), node) + showErrors(node.attributes.slice(singlelinemMaximum)) } if (!utils.isSingleLine(node)) { if (!canHaveFirstLine && node.attributes[0].loc.start.line === node.loc.start.line) { - showErrors([node.attributes[0]], node) + showErrors([node.attributes[0]]) } groupAttrsByLine(node.attributes) .filter(attrs => attrs.length > multilineMaximum) - .forEach(attrs => showErrors(attrs.splice(multilineMaximum), node)) + .forEach(attrs => showErrors(attrs.splice(multilineMaximum))) } } }) @@ -128,16 +129,45 @@ module.exports = { return defaults } - function showErrors (attributes, node) { + function getPropData (prop) { + let propType = 'Attribute' + let propName = prop.key.name + + if (utils.isBindingAttribute(prop)) { + propType = 'Binding' + propName = prop.key.raw.argument + } else if (utils.isEventAttribute(prop)) { + propType = 'Event' + propName = prop.key.raw.argument + } else if (prop.directive) { + propType = 'Directive' + } + + return { propType, propName } + } + + function showErrors (attributes) { attributes.forEach((prop, i) => { + const fix = (fixer) => { + if (i !== 0) return null + + // Find the closest token before the current prop + // that is not a white space + const prevToken = template.getTokenBefore(prop, { + filter: (token) => token.type !== 'HTMLWhitespace' + }) + + const range = [prevToken.range[1], prop.range[0]] + + return fixer.replaceTextRange(range, '\n') + } + context.report({ node: prop, loc: prop.loc, - message: 'Attribute "{{propName}}" should be on a new line.', - data: { - propName: prop.key.name - }, - fix: i === 0 ? (fixer) => fixer.insertTextBefore(prop, '\n') : undefined + message: '{{propType}} "{{propName}}" should be on a new line.', + data: getPropData(prop), + fix }) }) } diff --git a/lib/utils/index.js b/lib/utils/index.js index 28caa444d..bc9cc0407 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -294,6 +294,26 @@ module.exports = { return VOID_ELEMENT_NAMES.has(name) }, + /** + * Check whether the given attribute node is a binding + * @param {ASTNode} name The attribute to check. + * @returns {boolean} + */ + isBindingAttribute (attribute) { + return attribute.directive && + attribute.key.name === 'bind' && + attribute.key.argument + }, + + /** + * Check whether the given attribute node is an event + * @param {ASTNode} name The attribute to check. + * @returns {boolean} + */ + isEventAttribute (attribute) { + return attribute.directive && attribute.key.name === 'on' + }, + /** * Parse member expression node to get array with all of its parts * @param {ASTNode} MemberExpression diff --git a/tests/lib/rules/max-attributes-per-line.js b/tests/lib/rules/max-attributes-per-line.js index 0e8ab915f..ce4d45ffe 100644 --- a/tests/lib/rules/max-attributes-per-line.js +++ b/tests/lib/rules/max-attributes-per-line.js @@ -91,17 +91,53 @@ ruleTester.run('max-attributes-per-line', rule, { invalid: [ { code: ``, - output: ``, errors: ['Attribute "age" should be on a new line.'] }, + { + code: ``, + output: ``, + errors: ['Binding "age" should be on a new line.'] + }, + { + code: ``, + output: ``, + errors: ['Directive "bind" should be on a new line.'] + }, + { + code: ``, + output: ``, + errors: ['Event "buy" should be on a new line.'] + }, + { + code: ``, + output: ``, + errors: ['Event "click" should be on a new line.'] + }, + { + code: ``, + output: ``, + errors: ['Directive "if" should be on a new line.'] + }, + { + code: ``, + output: ``, + errors: ['Binding "age" should be on a new line.'] + }, { code: ``, - output: ``, { code: ``, options: [{ singleline: 1, multiline: { max: 1, allowFirstLine: false }}], - output: ``, errors: [{ message: 'Attribute "age" should be on a new line.', @@ -145,7 +181,7 @@ age="30" job="Vet">`, `, options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: false }}], - output: ``, options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: false }}], output: ``, options: [{ singleline: 3, multiline: 1 }], output: `