From 98dbf8aeaa4999ee3d09eac133139df6adcc3e29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sajn=C3=B3g?= Date: Wed, 3 Jan 2018 00:36:02 +0100 Subject: [PATCH 01/17] Add Vue.extend support, add missing info about Vue.mixin check in readme --- README.md | 2 ++ lib/utils/index.js | 2 +- tests/lib/utils/vue-component.js | 6 ++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d6b4d7389..26d89faf6 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ module.exports = { All component-related rules are being applied to code that passes any of the following checks: * `Vue.component()` expression +* `Vue.extend()` expression +* `Vue.mixin()` expression * `export default {}` in `.vue` or `.jsx` file If you however want to take advantage of our rules in any of your custom objects that are Vue components, you might need to use special comment `// @vue/component` that marks object in the next line as a Vue component in any file, e.g.: diff --git a/lib/utils/index.js b/lib/utils/index.js index 4667c437d..4f03abd23 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -384,7 +384,7 @@ module.exports = { callee.object.type === 'Identifier' && callee.object.name === 'Vue' && callee.property.type === 'Identifier' && - (callee.property.name === 'component' || callee.property.name === 'mixin') && + ['component', 'mixin', 'extend'].indexOf(callee.property.name) > -1 && node.arguments.length && node.arguments.slice(-1)[0].type === 'ObjectExpression' diff --git a/tests/lib/utils/vue-component.js b/tests/lib/utils/vue-component.js index 0dd2d0cf3..87b8738af 100644 --- a/tests/lib/utils/vue-component.js +++ b/tests/lib/utils/vue-component.js @@ -126,6 +126,12 @@ function invalidTests (ext) { parserOptions, errors: [makeError(1)] }, + { + filename: `test.${ext}`, + code: `Vue.extend({})`, + parserOptions, + errors: [makeError(1)] + }, { filename: `test.${ext}`, code: ` From 7c4a1d227a481928a9419828c19882d9ba930790 Mon Sep 17 00:00:00 2001 From: Sam Turrell Date: Thu, 1 Feb 2018 09:15:12 +0000 Subject: [PATCH 02/17] Docs: fixes wording in docs (#372) --- docs/rules/no-multi-spaces.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/no-multi-spaces.md b/docs/rules/no-multi-spaces.md index 196bee568..d68fe02bb 100644 --- a/docs/rules/no-multi-spaces.md +++ b/docs/rules/no-multi-spaces.md @@ -5,7 +5,7 @@ The `--fix` option on the command line can automatically fix some of the problems reported by this rule. -This rule aims to remove multiple spaces in a row between attributes witch are not used for indentation. +This rule aims to remove multiple spaces in a row between attributes which are not used for indentation. ## Rule Details From cd22a28870177732af3b04e27fc575b0b7616f85 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 16 Feb 2018 11:10:44 +0900 Subject: [PATCH 03/17] Fix: fix script-indent to prevent removing From ee5cc0a49f1abe7c2dd83e15e8dd962d26d36791 Mon Sep 17 00:00:00 2001 From: ota Date: Fri, 16 Feb 2018 11:11:21 +0900 Subject: [PATCH 04/17] [Update] Make `vue/max-attributes-per-line` fixable (#380) * [Update] Make `vue/max-attributes-per-line` fixable * [fix] bug and style * [fix] Switch indent calculation method with node and attribute * [fix] don't handle indentation * [add] autofix test max-attributes-per-line.js --- lib/rules/max-attributes-per-line.js | 7 ++-- tests/lib/rules/max-attributes-per-line.js | 41 ++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/lib/rules/max-attributes-per-line.js b/lib/rules/max-attributes-per-line.js index 9a54fd1e0..0d05f0bea 100644 --- a/lib/rules/max-attributes-per-line.js +++ b/lib/rules/max-attributes-per-line.js @@ -16,7 +16,7 @@ module.exports = { category: 'strongly-recommended', url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.2.2/docs/rules/max-attributes-per-line.md' }, - fixable: null, + fixable: 'whitespace', // or "code" or "whitespace" schema: [ { type: 'object', @@ -129,14 +129,15 @@ module.exports = { } function showErrors (attributes, node) { - attributes.forEach((prop) => { + attributes.forEach((prop, i) => { 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 }) }) } diff --git a/tests/lib/rules/max-attributes-per-line.js b/tests/lib/rules/max-attributes-per-line.js index 773f1b661..0e8ab915f 100644 --- a/tests/lib/rules/max-attributes-per-line.js +++ b/tests/lib/rules/max-attributes-per-line.js @@ -91,6 +91,8 @@ ruleTester.run('max-attributes-per-line', rule, { invalid: [ { code: ``, + output: ``, errors: ['Attribute "age" should be on a new line.'] }, { @@ -99,6 +101,12 @@ ruleTester.run('max-attributes-per-line', rule, { age="30"> `, + output: ``, errors: [{ message: 'Attribute "job" should be on a new line.', type: 'VAttribute', @@ -108,6 +116,8 @@ ruleTester.run('max-attributes-per-line', rule, { { code: ``, options: [{ singleline: { max: 2 }}], + output: ``, errors: [{ message: 'Attribute "job" should be on a new line.', type: 'VAttribute', @@ -117,6 +127,8 @@ ruleTester.run('max-attributes-per-line', rule, { { code: ``, options: [{ singleline: 1, multiline: { max: 1, allowFirstLine: false }}], + output: ``, errors: [{ message: 'Attribute "age" should be on a new line.', type: 'VAttribute', @@ -133,6 +145,11 @@ ruleTester.run('max-attributes-per-line', rule, { `, options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: false }}], + output: ``, errors: [{ message: 'Attribute "name" should be on a new line.', type: 'VAttribute', @@ -146,6 +163,12 @@ ruleTester.run('max-attributes-per-line', rule, { `, options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: false }}], + output: ``, errors: [{ message: 'Attribute "age" should be on a new line.', type: 'VAttribute', @@ -159,6 +182,12 @@ ruleTester.run('max-attributes-per-line', rule, { `, options: [{ singleline: 3, multiline: 1 }], + output: ``, errors: [{ message: 'Attribute "age" should be on a new line.', type: 'VAttribute', @@ -172,6 +201,12 @@ ruleTester.run('max-attributes-per-line', rule, { `, options: [{ singleline: 3, multiline: { max: 2, allowFirstLine: false }}], + output: ``, errors: [{ message: 'Attribute "petname" should be on a new line.', type: 'VAttribute', @@ -185,6 +220,12 @@ ruleTester.run('max-attributes-per-line', rule, { `, options: [{ singleline: 3, multiline: { max: 2, allowFirstLine: false }}], + output: ``, errors: [{ message: 'Attribute "petname" should be on a new line.', type: 'VAttribute', From 6861c818dfac569516d777ecdd34c51895a7c886 Mon Sep 17 00:00:00 2001 From: ota Date: Sat, 17 Feb 2018 17:49:18 +0900 Subject: [PATCH 05/17] Update: make `vue/order-in-components` fixable (#381) * [Update] Make `vue/order-in-components` fixable This Commit makes `vue/order-in-components` fixable. In case of `The "A" property should be above the "B" property` error, autofix will move A before B * [fix] fail test at eslint@3.18.0 * [fix] If there is a possibility of a side effect, don't autofix * [fix] failed test at node v4 * [update] use Traverser * [fix] failed test eslint@3.18.0 * [fix] I used `output: null` to specify "not fix" --- lib/rules/order-in-components.js | 102 +++++- tests/lib/rules/order-in-components.js | 429 +++++++++++++++++++++++++ 2 files changed, 530 insertions(+), 1 deletion(-) diff --git a/lib/rules/order-in-components.js b/lib/rules/order-in-components.js index 6bd0fb22a..3d9a21af2 100644 --- a/lib/rules/order-in-components.js +++ b/lib/rules/order-in-components.js @@ -5,6 +5,7 @@ 'use strict' const utils = require('../utils') +const Traverser = require('eslint/lib/util/traverser') const defaultOrder = [ 'el', @@ -56,6 +57,75 @@ function getOrderMap (order) { return orderMap } +function isComma (node) { + return node.type === 'Punctuator' && node.value === ',' +} + +const ARITHMETIC_OPERATORS = ['+', '-', '*', '/', '%', '**'] +const BITWISE_OPERATORS = ['&', '|', '^', '~', '<<', '>>', '>>>'] +const COMPARISON_OPERATORS = ['==', '!=', '===', '!==', '>', '>=', '<', '<='] +const RELATIONAL_OPERATORS = ['in', 'instanceof'] +const ALL_BINARY_OPERATORS = [].concat( + ARITHMETIC_OPERATORS, + BITWISE_OPERATORS, + COMPARISON_OPERATORS, + RELATIONAL_OPERATORS +) +const LOGICAL_OPERATORS = ['&&', '||'] + +/* + * Result `true` if the node is sure that there are no side effects + * + * Currently known side effects types + * + * node.type === 'CallExpression' + * node.type === 'NewExpression' + * node.type === 'UpdateExpression' + * node.type === 'AssignmentExpression' + * node.type === 'TaggedTemplateExpression' + * node.type === 'UnaryExpression' && node.operator === 'delete' + * + * @param {ASTNode} node target node + * @param {Object} visitorKeys sourceCode.visitorKey + * @returns {Boolean} no side effects + */ +function isNotSideEffectsNode (node, visitorKeys) { + let result = true + new Traverser().traverse(node, { + visitorKeys, + enter (node, parent) { + if ( + node.type === 'FunctionExpression' || + node.type === 'Identifier' || + node.type === 'Literal' || + // es2015 + node.type === 'ArrowFunctionExpression' || + node.type === 'TemplateElement' + ) { + // no side effects node + this.skip() + } else if ( + node.type !== 'Property' && + node.type !== 'ObjectExpression' && + node.type !== 'ArrayExpression' && + (node.type !== 'UnaryExpression' || ['!', '~', '+', '-', 'typeof'].indexOf(node.operator) < 0) && + (node.type !== 'BinaryExpression' || ALL_BINARY_OPERATORS.indexOf(node.operator) < 0) && + (node.type !== 'LogicalExpression' || LOGICAL_OPERATORS.indexOf(node.operator) < 0) && + node.type !== 'MemberExpression' && + node.type !== 'ConditionalExpression' && + // es2015 + node.type !== 'SpreadElement' && + node.type !== 'TemplateLiteral' + ) { + // Can not be sure that a node has no side effects + result = false + this.break() + } + } + }) + return result +} + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -67,7 +137,7 @@ module.exports = { category: 'recommended', url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.2.2/docs/rules/order-in-components.md' }, - fixable: null, + fixable: 'code', // null or "code" or "whitespace" schema: [ { type: 'object', @@ -86,6 +156,7 @@ module.exports = { const order = options.order || defaultOrder const extendedOrder = order.map(property => groups[property] || property) const orderMap = getOrderMap(extendedOrder) + const sourceCode = context.getSourceCode() function checkOrder (propertiesNodes, orderMap) { const properties = propertiesNodes @@ -109,6 +180,35 @@ module.exports = { name: property.name, firstUnorderedPropertyName: firstUnorderedProperty.name, line + }, + fix (fixer) { + const propertyNode = property.parent + const firstUnorderedPropertyNode = firstUnorderedProperty.parent + const hasSideEffectsPossibility = propertiesNodes + .slice( + propertiesNodes.indexOf(firstUnorderedPropertyNode), + propertiesNodes.indexOf(propertyNode) + 1 + ) + .some((property) => !isNotSideEffectsNode(property, sourceCode.visitorKeys)) + if (hasSideEffectsPossibility) { + return undefined + } + const comma = sourceCode.getTokenAfter(propertyNode) + const hasAfterComma = isComma(comma) + + const codeStart = sourceCode.getTokenBefore(propertyNode).range[1] // to include comments + const codeEnd = hasAfterComma ? comma.range[1] : propertyNode.range[1] + + const propertyCode = sourceCode.text.slice(codeStart, codeEnd) + (hasAfterComma ? '' : ',') + const insertTarget = sourceCode.getTokenBefore(firstUnorderedPropertyNode) + // If we can upgrade requirements to `eslint@>4.1.0`, this code can be replaced by: + // return [ + // fixer.removeRange([codeStart, codeEnd]), + // fixer.insertTextAfter(insertTarget, propertyCode) + // ] + const insertStart = insertTarget.range[1] + const newCode = propertyCode + sourceCode.text.slice(insertStart, codeStart) + return fixer.replaceTextRange([insertStart, codeEnd], newCode) } }) } diff --git a/tests/lib/rules/order-in-components.js b/tests/lib/rules/order-in-components.js index 485015411..2eff407ec 100644 --- a/tests/lib/rules/order-in-components.js +++ b/tests/lib/rules/order-in-components.js @@ -144,6 +144,19 @@ ruleTester.run('order-in-components', rule, { } `, parserOptions, + output: ` + export default { + name: 'app', + props: { + propA: Number, + }, + data () { + return { + msg: 'Welcome to Your Vue.js App' + } + }, + } + `, errors: [{ message: 'The "props" property should be above the "data" property on line 4.', line: 9 @@ -170,6 +183,24 @@ ruleTester.run('order-in-components', rule, { } `, parserOptions: { ecmaVersion: 6, sourceType: 'module', ecmaFeatures: { jsx: true }}, + output: ` + export default { + name: 'app', + render (h) { + return ( + { this.msg } + ) + }, + data () { + return { + msg: 'Welcome to Your Vue.js App' + } + }, + props: { + propA: Number, + }, + } + `, errors: [{ message: 'The "name" property should be above the "render" property on line 3.', line: 8 @@ -196,6 +227,18 @@ ruleTester.run('order-in-components', rule, { }) `, parserOptions: { ecmaVersion: 6 }, + output: ` + Vue.component('smart-list', { + name: 'app', + components: {}, + data () { + return { + msg: 'Welcome to Your Vue.js App' + } + }, + template: '
' + }) + `, errors: [{ message: 'The "components" property should be above the "data" property on line 4.', line: 9 @@ -217,6 +260,19 @@ ruleTester.run('order-in-components', rule, { }) `, parserOptions: { ecmaVersion: 6 }, + output: ` + const { component } = Vue; + component('smart-list', { + name: 'app', + components: {}, + data () { + return { + msg: 'Welcome to Your Vue.js App' + } + }, + template: '
' + }) + `, errors: [{ message: 'The "components" property should be above the "data" property on line 5.', line: 10 @@ -238,6 +294,19 @@ ruleTester.run('order-in-components', rule, { }) `, parserOptions: { ecmaVersion: 6 }, + output: ` + new Vue({ + el: '#app', + name: 'app', + data () { + return { + msg: 'Welcome to Your Vue.js App' + } + }, + components: {}, + template: '
' + }) + `, errors: [{ message: 'The "el" property should be above the "name" property on line 3.', line: 4 @@ -267,6 +336,24 @@ ruleTester.run('order-in-components', rule, { }; `, parserOptions, + output: ` + export default { + name: 'burger', + data() { + return { + isActive: false, + }; + }, + methods: { + toggleMenu() { + this.isActive = !this.isActive; + }, + closeMenu() { + this.isActive = false; + } + }, + }; + `, errors: [{ message: 'The "name" property should be above the "data" property on line 3.', line: 16 @@ -283,11 +370,353 @@ ruleTester.run('order-in-components', rule, { }; `, parserOptions, + output: ` + export default { + data() { + }, + test: 'ok', + name: 'burger', + }; + `, options: [{ order: ['data', 'test', 'name'] }], errors: [{ message: 'The "test" property should be above the "name" property on line 5.', line: 6 }] + }, + { + filename: 'example.vue', + code: ` + export default { + /** data provider */ + data() { + }, + /** name of vue component */ + name: 'burger' + }; + `, + parserOptions, + output: ` + export default { + /** name of vue component */ + name: 'burger', + /** data provider */ + data() { + }, + }; + `, + errors: [{ + message: 'The "name" property should be above the "data" property on line 4.', + line: 7 + }] + }, + { + filename: 'example.vue', + code: `export default {data(){},name:'burger'};`, + parserOptions, + output: `export default {name:'burger',data(){},};`, + errors: [{ + message: 'The "name" property should be above the "data" property on line 1.', + line: 1 + }] + }, + { + // side-effects CallExpression + filename: 'example.vue', + code: ` + export default { + data() { + }, + test: obj.fn(), + name: 'burger', + }; + `, + parserOptions, + output: null, + errors: [{ + message: 'The "name" property should be above the "data" property on line 3.', + line: 6 + }] + }, + { + // side-effects NewExpression + filename: 'example.vue', + code: ` + export default { + data() { + }, + test: new MyClass(), + name: 'burger', + }; + `, + parserOptions, + output: null, + errors: [{ + message: 'The "name" property should be above the "data" property on line 3.', + line: 6 + }] + }, + { + // side-effects UpdateExpression + filename: 'example.vue', + code: ` + export default { + data() { + }, + test: i++, + name: 'burger', + }; + `, + parserOptions, + output: null, + errors: [{ + message: 'The "name" property should be above the "data" property on line 3.', + line: 6 + }] + }, + { + // side-effects AssignmentExpression + filename: 'example.vue', + code: ` + export default { + data() { + }, + test: i = 0, + name: 'burger', + }; + `, + parserOptions, + output: null, + errors: [{ + message: 'The "name" property should be above the "data" property on line 3.', + line: 6 + }] + }, + { + // side-effects TaggedTemplateExpression + filename: 'example.vue', + code: ` + export default { + data() { + }, + test: template\`\${foo}\`, + name: 'burger', + }; + `, + parserOptions, + output: null, + errors: [{ + message: 'The "name" property should be above the "data" property on line 3.', + line: 6 + }] + }, + { + // side-effects key + filename: 'example.vue', + code: ` + export default { + data() { + }, + [obj.fn()]: 'test', + name: 'burger', + }; + `, + parserOptions, + output: null, + errors: [{ + message: 'The "name" property should be above the "data" property on line 3.', + line: 6 + }] + }, + { + // side-effects object deep props + filename: 'example.vue', + code: ` + export default { + data() { + }, + test: {test: obj.fn()}, + name: 'burger', + }; + `, + parserOptions, + output: null, + errors: [{ + message: 'The "name" property should be above the "data" property on line 3.', + line: 6 + }] + }, + { + // side-effects array elements + filename: 'example.vue', + code: ` + export default { + data() { + }, + test: [obj.fn(), 1], + name: 'burger', + }; + `, + parserOptions, + output: null, + errors: [{ + message: 'The "name" property should be above the "data" property on line 3.', + line: 6 + }] + }, + { + // side-effects call at middle + filename: 'example.vue', + code: ` + export default { + data() { + }, + test: obj.fn().prop, + name: 'burger', + }; + `, + parserOptions, + output: null, + errors: [{ + message: 'The "name" property should be above the "data" property on line 3.', + line: 6 + }] + }, + { + // side-effects delete + filename: 'example.vue', + code: ` + export default { + data() { + }, + test: delete obj.prop, + name: 'burger', + }; + `, + parserOptions, + output: null, + errors: [{ + message: 'The "name" property should be above the "data" property on line 3.', + line: 6 + }] + }, + { + // side-effects within BinaryExpression + filename: 'example.vue', + code: ` + export default { + data() { + }, + test: fn() + a + b, + name: 'burger', + }; + `, + parserOptions, + output: null, + errors: [{ + message: 'The "name" property should be above the "data" property on line 3.', + line: 6 + }] + }, + { + // side-effects within ConditionalExpression + filename: 'example.vue', + code: ` + export default { + data() { + }, + test: a ? fn() : null, + name: 'burger', + }; + `, + parserOptions, + output: null, + errors: [{ + message: 'The "name" property should be above the "data" property on line 3.', + line: 6 + }] + }, + { + // side-effects within TemplateLiteral + filename: 'example.vue', + code: ` + export default { + data() { + }, + test: \`test \${fn()} \${a}\`, + name: 'burger', + }; + `, + parserOptions, + output: null, + errors: [{ + message: 'The "name" property should be above the "data" property on line 3.', + line: 6 + }] + }, + { + // without side-effects + filename: 'example.vue', + code: ` + export default { + data() { + }, + name: 'burger', + test: fn(), + }; + `, + parserOptions, + output: ` + export default { + name: 'burger', + data() { + }, + test: fn(), + }; + `, + errors: [{ + message: 'The "name" property should be above the "data" property on line 3.', + line: 5 + }] + }, + { + // don't side-effects + filename: 'example.vue', + code: ` + export default { + data() { + }, + testArray: [1, 2, 3, true, false, 'a', 'b', 'c'], + testRegExp: /[a-z]*/, + testSpreadElement: [...array], + testOperator: (!!(a - b + c * d / e % f)) || (a && b), + testArrow: (a) => a, + testConditional: a ? b : c, + testYield: function* () {}, + testTemplate: \`a:\${a},b:\${b},c:\${c}.\`, + name: 'burger', + }; + `, + parserOptions, + output: ` + export default { + name: 'burger', + data() { + }, + testArray: [1, 2, 3, true, false, 'a', 'b', 'c'], + testRegExp: /[a-z]*/, + testSpreadElement: [...array], + testOperator: (!!(a - b + c * d / e % f)) || (a && b), + testArrow: (a) => a, + testConditional: a ? b : c, + testYield: function* () {}, + testTemplate: \`a:\${a},b:\${b},c:\${c}.\`, + }; + `, + errors: [{ + message: 'The "name" property should be above the "data" property on line 3.', + line: 13 + }] } ] }) From 2b4798b626c69f659f894a18284eebeba47f6bb7 Mon Sep 17 00:00:00 2001 From: ota Date: Mon, 19 Feb 2018 23:05:39 +0900 Subject: [PATCH 06/17] [New] Add `vue/component-name-in-template-casing` --- .../component-name-in-template-casing.md | 54 ++++ lib/configs/strongly-recommended.js | 1 + lib/index.js | 1 + .../component-name-in-template-casing.js | 88 +++++++ .../component-name-in-template-casing.js | 234 ++++++++++++++++++ 5 files changed, 378 insertions(+) create mode 100644 docs/rules/component-name-in-template-casing.md create mode 100644 lib/rules/component-name-in-template-casing.js create mode 100644 tests/lib/rules/component-name-in-template-casing.js diff --git a/docs/rules/component-name-in-template-casing.md b/docs/rules/component-name-in-template-casing.md new file mode 100644 index 000000000..ed1b1b767 --- /dev/null +++ b/docs/rules/component-name-in-template-casing.md @@ -0,0 +1,54 @@ +# enforce specific casing for the component name in tamplates (vue/component-name-in-template-casing) + +- :gear: This rule is included in `"plugin:vue/strongly-recommended"` and `"plugin:vue/recommended"`. +- :wrench: The `--fix` option on the [command line](http://eslint.org/docs/user-guide/command-line-interface#fix) can automatically fix some of the problems reported by this rule. + +Define a style for the `component name` in tamplates casing for consistency purposes. + +## :book: Rule Details + +:+1: Examples of **correct** code for `PascalCase`: + +```html + +``` + +:-1: Examples of **incorrect** code for `PascalCase`: + +```html + +``` + +:+1: Examples of **correct** code for `kebab-case`: + +```html + +``` + +:-1: Examples of **incorrect** code for `kebab-case`: + +```html + +``` + +## :wrench: Options + +Default casing is set to `PascalCase`. + +``` +"vue/component-name-in-template-casing": ["error", "PascalCase|kebab-case"] +``` + diff --git a/lib/configs/strongly-recommended.js b/lib/configs/strongly-recommended.js index 993c78d31..308860a47 100644 --- a/lib/configs/strongly-recommended.js +++ b/lib/configs/strongly-recommended.js @@ -7,6 +7,7 @@ module.exports = { extends: require.resolve('./essential'), rules: { 'vue/attribute-hyphenation': 'error', + 'vue/component-name-in-template-casing': 'error', 'vue/html-end-tags': 'error', 'vue/html-indent': 'error', 'vue/html-self-closing': 'error', diff --git a/lib/index.js b/lib/index.js index b2871f547..f16216f39 100644 --- a/lib/index.js +++ b/lib/index.js @@ -9,6 +9,7 @@ module.exports = { rules: { 'attribute-hyphenation': require('./rules/attribute-hyphenation'), 'comment-directive': require('./rules/comment-directive'), + 'component-name-in-template-casing': require('./rules/component-name-in-template-casing'), 'html-closing-bracket-newline': require('./rules/html-closing-bracket-newline'), 'html-closing-bracket-spacing': require('./rules/html-closing-bracket-spacing'), 'html-end-tags': require('./rules/html-end-tags'), diff --git a/lib/rules/component-name-in-template-casing.js b/lib/rules/component-name-in-template-casing.js new file mode 100644 index 000000000..b2fd1718b --- /dev/null +++ b/lib/rules/component-name-in-template-casing.js @@ -0,0 +1,88 @@ +/** + * @author Yosuke Ota + * issue https://github.com/vuejs/eslint-plugin-vue/issues/250 + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const utils = require('../utils') +const casing = require('../utils/casing') + +const allowedCaseOptions = ['PascalCase', 'kebab-case'] + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: 'enforce specific casing for the component name in tamplates', + category: 'strongly-recommended', + url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.2.2/docs/rules/component-name-in-template-casing.md' + }, + fixable: 'code', + schema: [ + { + enum: allowedCaseOptions + } + ] + }, + + create (context) { + const options = context.options[0] + const caseType = allowedCaseOptions.indexOf(options) !== -1 ? options : 'PascalCase' + const tokens = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore() + const sourceCode = context.getSourceCode() + + let hasInvalidEOF = false + + return utils.defineTemplateBodyVisitor(context, { + 'VElement' (node) { + if (hasInvalidEOF) { + return + } + + if (!utils.isCustomComponent(node)) { + return + } + + const name = node.rawName + const casingName = casing.getConverter(caseType)(name) + if (casingName !== name) { + const startTag = node.startTag + const open = tokens.getFirstToken(startTag) + context.report({ + node: open, + loc: open.loc, + message: 'Component name "{{name}}" is not {{caseType}}.', + data: { + name, + caseType: caseType + }, + fix: fixer => { + const endTag = node.endTag + if (!endTag) { + return fixer.replaceText(open, `<${casingName}`) + } + // If we can upgrade requirements to `eslint@>4.1.0`, this code can be replaced by: + // return [ + // fixer.replaceText(open, `<${casingName}`), + // fixer.replaceText(endTag, ``) + // ] + const code = `<${casingName}${sourceCode.text.slice(open.range[1], endTag.range[0])}` + return fixer.replaceTextRange([open.range[0], endTag.range[1]], code) + } + }) + } + } + }, { + Program (node) { + hasInvalidEOF = utils.hasInvalidEOF(node) + } + }) + } +} diff --git a/tests/lib/rules/component-name-in-template-casing.js b/tests/lib/rules/component-name-in-template-casing.js new file mode 100644 index 000000000..107e100f6 --- /dev/null +++ b/tests/lib/rules/component-name-in-template-casing.js @@ -0,0 +1,234 @@ +/** + * @author Yosuke Ota + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/component-name-in-template-casing') +const RuleTester = require('eslint').RuleTester + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const tester = new RuleTester({ + parser: 'vue-eslint-parser' +}) + +tester.run('html-self-closing', rule, { + valid: [ + // default + '', + '', + '', + '', + '', + '', + + // kebab-case + { + code: '', + output: null, + options: ['kebab-case'] + }, + { + code: '', + output: null, + options: ['kebab-case'] + }, + { + code: '', + output: null, + options: ['kebab-case'] + }, + { + code: '', + output: null, + options: ['kebab-case'] + }, + { + code: '', + output: null, + options: ['kebab-case'] + }, + // Invalid EOF + ' +`, + output: ` + +`, + errors: ['Component name "the-component" is not PascalCase.'] + }, + { + code: ` + +`, + output: ` + +`, + errors: ['Component name "the-component" is not PascalCase.'] + }, + { + code: ` + +`, + options: ['kebab-case'], + output: ` + +`, + errors: ['Component name "TheComponent" is not kebab-case.'] + }, + { + code: ` + +`, + options: ['kebab-case'], + output: ` + +`, + errors: ['Component name "TheComponent" is not kebab-case.'] + }, + { + code: ` + +`, + output: ` + +`, + errors: ['Component name "the-component" is not PascalCase.'] + }, + { + code: ` + +`, + output: ` + +`, + errors: ['Component name "the-component" is not PascalCase.'] + }, + { + code: ` + +`, + output: ` + +`, + errors: ['Component name "the-component" is not PascalCase.'] + }, + { + code: ` + +`, + output: ` + +`, + errors: ['Component name "theComponent" is not PascalCase.'] + }, + { + code: ` + +`, + options: ['kebab-case'], + output: ` + +`, + errors: ['Component name "theComponent" is not kebab-case.'] + }, + { + code: ` + +`, + output: ` + +`, + errors: ['Component name "The-component" is not PascalCase.'] + }, + { + code: ` + +`, + options: ['kebab-case'], + output: ` + +`, + errors: ['Component name "The-component" is not kebab-case.'] + }, + { + code: ` + +`, + options: ['kebab-case'], + output: ` + +`, + errors: ['Component name "Thecomponent" is not kebab-case.'] + } + ] +}) From 3f86365d33f379e9278190c25e3ed0b50349e03d Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Mon, 19 Feb 2018 23:17:54 +0900 Subject: [PATCH 07/17] [update] documents --- README.md | 1 + docs/rules/component-name-in-template-casing.md | 4 ++-- lib/rules/component-name-in-template-casing.js | 5 ++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 26d89faf6..ace0ad11e 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi | | Rule ID | Description | |:---|:--------|:------------| | :wrench: | [vue/attribute-hyphenation](./docs/rules/attribute-hyphenation.md) | enforce attribute naming style in template | +| :wrench: | [vue/component-name-in-template-casing](./docs/rules/component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | | :wrench: | [vue/html-end-tags](./docs/rules/html-end-tags.md) | enforce end tag style | | :wrench: | [vue/html-indent](./docs/rules/html-indent.md) | enforce consistent indentation in `