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/22] 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/22] 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/22] 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/22] [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/22] 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 58908087f93a0b5584bfd338200c5165b7242423 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 16 Feb 2018 12:18:16 +0900 Subject: [PATCH 06/22] Fix: no-async-in-computed-properties crash (fixes #390) --- lib/utils/index.js | 9 ++++++--- tests/lib/rules/no-async-in-computed-properties.js | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/utils/index.js b/lib/utils/index.js index 4f03abd23..677fff6b5 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -385,11 +385,14 @@ module.exports = { callee.object.name === 'Vue' && callee.property.type === 'Identifier' && ['component', 'mixin', 'extend'].indexOf(callee.property.name) > -1 && - node.arguments.length && + node.arguments.length >= 1 && node.arguments.slice(-1)[0].type === 'ObjectExpression' - const isDestructedVueComponent = callee.type === 'Identifier' && - callee.name === 'component' + const isDestructedVueComponent = node.type === 'CallExpression' && + callee.type === 'Identifier' && + callee.name === 'component' && + node.arguments.length >= 1 && + node.arguments.slice(-1)[0].type === 'ObjectExpression' return isFullVueComponent || isDestructedVueComponent }, diff --git a/tests/lib/rules/no-async-in-computed-properties.js b/tests/lib/rules/no-async-in-computed-properties.js index e7cd02694..746ba319b 100644 --- a/tests/lib/rules/no-async-in-computed-properties.js +++ b/tests/lib/rules/no-async-in-computed-properties.js @@ -60,6 +60,20 @@ ruleTester.run('no-async-in-computed-properties', rule, { } `, parserOptions + }, + { + filename: 'test.vue', + code: ` + async function resolveComponents(components) { + return await Promise.all(components.map(async (component) => { + if(typeof component === 'function') { + return await component() + } + return component; + })); + } + `, + parserOptions: parserOptions8 } ], From e79dd8be2c1184cc724e3a1df469e2b258281639 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 16 Feb 2018 19:18:22 +0900 Subject: [PATCH 07/22] Fix: valid-v-on false positive (fixes #351) --- lib/rules/valid-v-on.js | 84 ++++++++++++++++++++++++++++++++++- tests/lib/rules/valid-v-on.js | 24 ++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/lib/rules/valid-v-on.js b/lib/rules/valid-v-on.js index 498fd54fa..f84412bb2 100644 --- a/lib/rules/valid-v-on.js +++ b/lib/rules/valid-v-on.js @@ -23,6 +23,88 @@ const VALID_MODIFIERS = new Set([ const VERB_MODIFIERS = new Set([ 'stop', 'prevent' ]) +// https://www.w3.org/TR/uievents-key/ +const KEY_ALIASES = new Set([ + 'unidentified', 'alt', 'alt-graph', 'caps-lock', 'control', 'fn', 'fn-lock', + 'meta', 'num-lock', 'scroll-lock', 'shift', 'symbol', 'symbol-lock', 'hyper', + 'super', 'enter', 'tab', 'arrow-down', 'arrow-left', 'arrow-right', + 'arrow-up', 'end', 'home', 'page-down', 'page-up', 'backspace', 'clear', + 'copy', 'cr-sel', 'cut', 'delete', 'erase-eof', 'ex-sel', 'insert', 'paste', + 'redo', 'undo', 'accept', 'again', 'attn', 'cancel', 'context-menu', 'escape', + 'execute', 'find', 'help', 'pause', 'select', 'zoom-in', 'zoom-out', + 'brightness-down', 'brightness-up', 'eject', 'log-off', 'power', + 'print-screen', 'hibernate', 'standby', 'wake-up', 'all-candidates', + 'alphanumeric', 'code-input', 'compose', 'convert', 'dead', 'final-mode', + 'group-first', 'group-last', 'group-next', 'group-previous', 'mode-change', + 'next-candidate', 'non-convert', 'previous-candidate', 'process', + 'single-candidate', 'hangul-mode', 'hanja-mode', 'junja-mode', 'eisu', + 'hankaku', 'hiragana', 'hiragana-katakana', 'kana-mode', 'kanji-mode', + 'katakana', 'romaji', 'zenkaku', 'zenkaku-hankaku', 'f1', 'f2', 'f3', 'f4', + 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12', 'soft1', 'soft2', 'soft3', + 'soft4', 'channel-down', 'channel-up', 'close', 'mail-forward', 'mail-reply', + 'mail-send', 'media-close', 'media-fast-forward', 'media-pause', + 'media-play-pause', 'media-record', 'media-rewind', 'media-stop', + 'media-track-next', 'media-track-previous', 'new', 'open', 'print', 'save', + 'spell-check', 'key11', 'key12', 'audio-balance-left', 'audio-balance-right', + 'audio-bass-boost-down', 'audio-bass-boost-toggle', 'audio-bass-boost-up', + 'audio-fader-front', 'audio-fader-rear', 'audio-surround-mode-next', + 'audio-treble-down', 'audio-treble-up', 'audio-volume-down', + 'audio-volume-up', 'audio-volume-mute', 'microphone-toggle', + 'microphone-volume-down', 'microphone-volume-up', 'microphone-volume-mute', + 'speech-correction-list', 'speech-input-toggle', 'launch-application1', + 'launch-application2', 'launch-calendar', 'launch-contacts', 'launch-mail', + 'launch-media-player', 'launch-music-player', 'launch-phone', + 'launch-screen-saver', 'launch-spreadsheet', 'launch-web-browser', + 'launch-web-cam', 'launch-word-processor', 'browser-back', + 'browser-favorites', 'browser-forward', 'browser-home', 'browser-refresh', + 'browser-search', 'browser-stop', 'app-switch', 'call', 'camera', + 'camera-focus', 'end-call', 'go-back', 'go-home', 'headset-hook', + 'last-number-redial', 'notification', 'manner-mode', 'voice-dial', 't-v', + 't-v3-d-mode', 't-v-antenna-cable', 't-v-audio-description', + 't-v-audio-description-mix-down', 't-v-audio-description-mix-up', + 't-v-contents-menu', 't-v-data-service', 't-v-input', 't-v-input-component1', + 't-v-input-component2', 't-v-input-composite1', 't-v-input-composite2', + 't-v-input-h-d-m-i1', 't-v-input-h-d-m-i2', 't-v-input-h-d-m-i3', + 't-v-input-h-d-m-i4', 't-v-input-v-g-a1', 't-v-media-context', 't-v-network', + 't-v-number-entry', 't-v-power', 't-v-radio-service', 't-v-satellite', + 't-v-satellite-b-s', 't-v-satellite-c-s', 't-v-satellite-toggle', + 't-v-terrestrial-analog', 't-v-terrestrial-digital', 't-v-timer', + 'a-v-r-input', 'a-v-r-power', 'color-f0-red', 'color-f1-green', + 'color-f2-yellow', 'color-f3-blue', 'color-f4-grey', 'color-f5-brown', + 'closed-caption-toggle', 'dimmer', 'display-swap', 'd-v-r', 'exit', + 'favorite-clear0', 'favorite-clear1', 'favorite-clear2', 'favorite-clear3', + 'favorite-recall0', 'favorite-recall1', 'favorite-recall2', + 'favorite-recall3', 'favorite-store0', 'favorite-store1', 'favorite-store2', + 'favorite-store3', 'guide', 'guide-next-day', 'guide-previous-day', 'info', + 'instant-replay', 'link', 'list-program', 'live-content', 'lock', + 'media-apps', 'media-last', 'media-skip-backward', 'media-skip-forward', + 'media-step-backward', 'media-step-forward', 'media-top-menu', 'navigate-in', + 'navigate-next', 'navigate-out', 'navigate-previous', 'next-favorite-channel', + 'next-user-profile', 'on-demand', 'pairing', 'pin-p-down', 'pin-p-move', + 'pin-p-toggle', 'pin-p-up', 'play-speed-down', 'play-speed-reset', + 'play-speed-up', 'random-toggle', 'rc-low-battery', 'record-speed-next', + 'rf-bypass', 'scan-channels-toggle', 'screen-mode-next', 'settings', + 'split-screen-toggle', 's-t-b-input', 's-t-b-power', 'subtitle', 'teletext', + 'video-mode-next', 'wink', 'zoom-toggle', 'audio-volume-down', + 'audio-volume-up', 'audio-volume-mute', 'browser-back', 'browser-forward', + 'channel-down', 'channel-up', 'context-menu', 'eject', 'end', 'enter', 'home', + 'media-fast-forward', 'media-play', 'media-play-pause', 'media-record', + 'media-rewind', 'media-stop', 'media-next-track', 'media-pause', + 'media-previous-track', 'power', 'unidentified' +]) + +function isValidModifier (modifier) { + return ( + // built-in aliases + VALID_MODIFIERS.has(modifier) || + // keyCode + Number.isInteger(parseInt(modifier, 10)) || + // keyAlias (an Unicode character) + Array.from(modifier).length === 1 || + // keyAlias (special keys) + KEY_ALIASES.has(modifier) + ) +} // ------------------------------------------------------------------------------ // Rule Definition @@ -43,7 +125,7 @@ module.exports = { return utils.defineTemplateBodyVisitor(context, { "VAttribute[directive=true][key.name='on']" (node) { for (const modifier of node.key.modifiers) { - if (!VALID_MODIFIERS.has(modifier) && !Number.isInteger(parseInt(modifier, 10))) { + if (!isValidModifier(modifier)) { context.report({ node, loc: node.loc, diff --git a/tests/lib/rules/valid-v-on.js b/tests/lib/rules/valid-v-on.js index e7179c109..46dd8b4ee 100644 --- a/tests/lib/rules/valid-v-on.js +++ b/tests/lib/rules/valid-v-on.js @@ -43,6 +43,30 @@ tester.run('valid-v-on', rule, { filename: 'test.vue', code: '' }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, { filename: 'test.vue', code: '' From 733172c7ec7bef4af34ed6a31126ea685a8d3846 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 16 Feb 2018 19:55:15 +0900 Subject: [PATCH 08/22] Fix: indent rules false positive (fixes #375) --- lib/utils/indent-common.js | 44 +++++++++++++------ tests/fixtures/html-indent/v-start-tag-02.vue | 5 +++ tests/fixtures/html-indent/v-start-tag-03.vue | 6 +++ tests/fixtures/html-indent/v-start-tag-04.vue | 7 +++ tests/fixtures/html-indent/v-start-tag-05.vue | 5 +++ .../script-indent/array-expression-02.vue | 5 +++ .../script-indent/function-declaration-04.vue | 7 +++ .../script-indent/object-expression-03.vue | 5 +++ 8 files changed, 70 insertions(+), 14 deletions(-) create mode 100644 tests/fixtures/html-indent/v-start-tag-02.vue create mode 100644 tests/fixtures/html-indent/v-start-tag-03.vue create mode 100644 tests/fixtures/html-indent/v-start-tag-04.vue create mode 100644 tests/fixtures/html-indent/v-start-tag-05.vue create mode 100644 tests/fixtures/script-indent/array-expression-02.vue create mode 100644 tests/fixtures/script-indent/function-declaration-04.vue create mode 100644 tests/fixtures/script-indent/object-expression-03.vue diff --git a/lib/utils/indent-common.js b/lib/utils/indent-common.js index 1b93f2069..dfc89d57c 100644 --- a/lib/utils/indent-common.js +++ b/lib/utils/indent-common.js @@ -18,6 +18,7 @@ const KNOWN_NODES = new Set(['ArrayExpression', 'ArrayPattern', 'ArrowFunctionEx const LT_CHAR = /[\r\n\u2028\u2029]/ const LINES = /[^\r\n\u2028\u2029]+(?:$|\r\n|[\r\n\u2028\u2029])/g const BLOCK_COMMENT_PREFIX = /^\s*\*/ +const TRIVIAL_PUNCTUATOR = /^[(){}[\],;]$/ /** * Normalize options. @@ -244,6 +245,21 @@ function isClosingToken (token) { ) } +/** + * Check whether a given token is trivial or not. + * @param {Token} token The token to check. + * @returns {boolean} `true` if the token is trivial. + */ +function isTrivialToken (token) { + return token != null && ( + (token.type === 'Punctuator' && TRIVIAL_PUNCTUATOR.test(token.value)) || + token.type === 'HTMLTagOpen' || + token.type === 'HTMLEndTagOpen' || + token.type === 'HTMLTagClose' || + token.type === 'HTMLSelfClosingTagClose' + ) +} + /** * Creates AST event handlers for html-indent. * @@ -262,6 +278,7 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti * @param {Token|Token[]} token The token to set. * @param {number} offset The offset of the tokens. * @param {Token} baseToken The token of the base offset. + * @param {boolean} [trivial=false] The flag for trivial tokens. * @returns {void} */ function setOffset (token, offset, baseToken) { @@ -352,9 +369,7 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti if (lastToken != null) { t = lastToken while ((t = tokenStore.getTokenAfter(t)) != null && t.range[1] <= elementTokens.firstToken.range[0]) { - if (lastToken.loc.end.line !== t.loc.start.line) { - alignTokens.push(t) - } + alignTokens.push(t) } } alignTokens.push(elementTokens.firstToken) @@ -550,13 +565,15 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti * @returns {number} Correct indentation. If it failed to calculate then `Number.MAX_SAFE_INTEGER`. */ function getExpectedIndent (tokens) { + const trivial = isTrivialToken(tokens[0]) let expectedIndent = Number.MAX_SAFE_INTEGER for (let i = 0; i < tokens.length; ++i) { const token = tokens[i] const offsetInfo = offsets.get(token) - if (offsetInfo != null) { + // If the first token is not trivial then ignore trivial following tokens. + if (offsetInfo != null && (trivial || !isTrivialToken(token))) { if (offsetInfo.expectedIndent != null) { expectedIndent = Math.min(expectedIndent, offsetInfo.expectedIndent) } else { @@ -1429,24 +1446,23 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti // Process semicolons. ':statement' (node) { const info = offsets.get(tokenStore.getFirstToken(node)) + const lastToken = tokenStore.getLastToken(node) if (info == null) { return } + if (isSemicolon(lastToken)) { + offsets.set(lastToken, info) + } // Set to the semicolon of the previous token for semicolon-free style. // E.g., // foo // ;[1,2,3].forEach(f) - const tokens = [ - tokenStore.getTokenBefore(node), - tokenStore.getLastToken(node) - ].filter(isSemicolon) - - // Set offsets if the semicolon is at beginning of line. - for (const token of tokens) { - const prevToken = tokenStore.getTokenBefore(token) - if (prevToken == null || token.loc.end.line !== prevToken.loc.start.line) { - offsets.set(token, info) + const prevToken = tokenStore.getTokenBefore(node) + if (isSemicolon(prevToken)) { + const prevPrevToken = tokenStore.getTokenBefore(prevToken) + if (prevPrevToken == null || prevToken.loc.end.line !== prevPrevToken.loc.start.line) { + offsets.set(prevToken, info) } } }, diff --git a/tests/fixtures/html-indent/v-start-tag-02.vue b/tests/fixtures/html-indent/v-start-tag-02.vue new file mode 100644 index 000000000..7ac1ee0da --- /dev/null +++ b/tests/fixtures/html-indent/v-start-tag-02.vue @@ -0,0 +1,5 @@ + + diff --git a/tests/fixtures/html-indent/v-start-tag-03.vue b/tests/fixtures/html-indent/v-start-tag-03.vue new file mode 100644 index 000000000..4a4d918aa --- /dev/null +++ b/tests/fixtures/html-indent/v-start-tag-03.vue @@ -0,0 +1,6 @@ + + diff --git a/tests/fixtures/html-indent/v-start-tag-04.vue b/tests/fixtures/html-indent/v-start-tag-04.vue new file mode 100644 index 000000000..69ac40a5d --- /dev/null +++ b/tests/fixtures/html-indent/v-start-tag-04.vue @@ -0,0 +1,7 @@ + + diff --git a/tests/fixtures/html-indent/v-start-tag-05.vue b/tests/fixtures/html-indent/v-start-tag-05.vue new file mode 100644 index 000000000..5d7d7155d --- /dev/null +++ b/tests/fixtures/html-indent/v-start-tag-05.vue @@ -0,0 +1,5 @@ + + diff --git a/tests/fixtures/script-indent/array-expression-02.vue b/tests/fixtures/script-indent/array-expression-02.vue new file mode 100644 index 000000000..d398bde99 --- /dev/null +++ b/tests/fixtures/script-indent/array-expression-02.vue @@ -0,0 +1,5 @@ + + diff --git a/tests/fixtures/script-indent/function-declaration-04.vue b/tests/fixtures/script-indent/function-declaration-04.vue new file mode 100644 index 000000000..d4ce7897b --- /dev/null +++ b/tests/fixtures/script-indent/function-declaration-04.vue @@ -0,0 +1,7 @@ + + diff --git a/tests/fixtures/script-indent/object-expression-03.vue b/tests/fixtures/script-indent/object-expression-03.vue new file mode 100644 index 000000000..f65063ecc --- /dev/null +++ b/tests/fixtures/script-indent/object-expression-03.vue @@ -0,0 +1,5 @@ + + From e887a8d8794f2dbeb23414c57ece02d79bf4f9ae Mon Sep 17 00:00:00 2001 From: y Date: Mon, 11 Dec 2017 04:12:43 +0900 Subject: [PATCH 09/22] [New] Add `prop-name-casing` --- docs/rules/prop-name-casing.md | 35 ++++++ lib/rules/prop-name-casing.js | 73 +++++++++++ lib/utils/casing.js | 13 ++ tests/lib/rules/prop-name-casing.js | 180 ++++++++++++++++++++++++++++ tests/lib/utils/casing.js | 9 ++ 5 files changed, 310 insertions(+) create mode 100644 docs/rules/prop-name-casing.md create mode 100644 lib/rules/prop-name-casing.js create mode 100644 tests/lib/rules/prop-name-casing.js diff --git a/docs/rules/prop-name-casing.md b/docs/rules/prop-name-casing.md new file mode 100644 index 000000000..fcd8207f9 --- /dev/null +++ b/docs/rules/prop-name-casing.md @@ -0,0 +1,35 @@ +# enforce specific casing for the Prop name in Vue components(prop-name-casing) + +This rule would enforce proper casing of props in vue components(camelCase). + +## :book: Rule Details + +(https://vuejs.org/v2/style-guide/#Prop-name-casing-strongly-recommended). + +:+1: Examples of **correct** code for `camelCase`: + +```js +export default { + props: { + greetingText: String + } +} +``` + +:-1: Examples of **incorrect** code for `camelCase`: + +```js +export default { + props: { + 'greeting-text': String + } +} +``` + +## :wrench: Options + +Default casing is set to `camelCase`. + +``` +"vue/prop-name-casing": ["error", "camelCase|snake_case"] +``` diff --git a/lib/rules/prop-name-casing.js b/lib/rules/prop-name-casing.js new file mode 100644 index 000000000..23e04a483 --- /dev/null +++ b/lib/rules/prop-name-casing.js @@ -0,0 +1,73 @@ +/** + * @fileoverview Requires specific casing for the Prop name in Vue components + * @author Yu Kimura + */ +'use strict' + +const utils = require('../utils') +const casing = require('../utils/casing') +const allowedCaseOptions = ['camelCase', 'snake_case'] + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +function create (context) { + const options = context.options[0] + const caseType = allowedCaseOptions.indexOf(options) !== -1 ? options : 'camelCase' + const converter = casing.getConverter(caseType) + + // ---------------------------------------------------------------------- + // Public + // ---------------------------------------------------------------------- + + return utils.executeOnVue(context, (obj) => { + const node = obj.properties.find(p => + p.type === 'Property' && + p.key.type === 'Identifier' && + p.key.name === 'props' + ) + if (!node) return + + const items = node.value.type === 'ObjectExpression' ? node.value.properties : node.value.elements + for (const item of items) { + if (item.type !== 'Property') { + return + } + + const propName = item.key.type === 'Literal' ? item.key.value : item.key.name + const convertedName = converter(propName) + if (convertedName !== propName) { + context.report({ + node: item, + message: 'Prop "{{name}}" is not {{caseType}}.', + data: { + name: propName, + caseType: caseType + } + }) + } + } + }) +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: 'enforce specific casing for the Prop name in Vue components', + category: 'strongly-recommended' + }, + fixable: null, // or "code" or "whitespace" + schema: [ + { + enum: allowedCaseOptions + } + ] + }, + + create +} diff --git a/lib/utils/casing.js b/lib/utils/casing.js index 2baca2356..db73a5a23 100644 --- a/lib/utils/casing.js +++ b/lib/utils/casing.js @@ -14,6 +14,18 @@ function kebabCase (str) { .toLowerCase() } +/** + * Convert text to snake_case + * @param {string} str Text to be converted + * @return {string} + */ +function snakeCase (str) { + return str + .replace(/([a-z])([A-Z])/g, match => match[0] + '_' + match[1]) + .replace(invalidChars, '_') + .toLowerCase() +} + /** * Convert text to camelCase * @param {string} str Text to be converted @@ -42,6 +54,7 @@ function pascalCase (str) { const convertersMap = { 'kebab-case': kebabCase, + 'snake_case': snakeCase, 'camelCase': camelCase, 'PascalCase': pascalCase } diff --git a/tests/lib/rules/prop-name-casing.js b/tests/lib/rules/prop-name-casing.js new file mode 100644 index 000000000..fcab592a8 --- /dev/null +++ b/tests/lib/rules/prop-name-casing.js @@ -0,0 +1,180 @@ +/** + * @fileoverview Define a style for the name property casing for consistency purposes + * @author Yu Kimura + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/prop-name-casing') +const RuleTester = require('eslint').RuleTester + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const parserOptions = { + ecmaVersion: 6, + sourceType: 'module', + ecmaFeatures: { experimentalObjectRestSpread: true } +} + +const ruleTester = new RuleTester() +ruleTester.run('prop-name-casing', rule, { + + valid: [ + { + filename: 'test.vue', + code: ` + export default { + props: ['greetingText'] + } + `, + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default { + props: ['greetingText'] + } + `, + options: ['camelCase'], + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default { + props: ['greetingText'] + } + `, + options: ['snake_case'], + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + greetingText: String + } + } + `, + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + greetingText: String + } + } + `, + options: ['camelCase'], + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + greeting_text: String + } + } + `, + options: ['snake_case'], + parserOptions + } + ], + + invalid: [ + { + filename: 'test.vue', + code: ` + export default { + props: { + greeting_text: String + } + } + `, + parserOptions, + errors: [{ + message: 'Prop "greeting_text" is not camelCase.', + type: 'Property', + line: 4 + }] + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + greeting_text: String + } + } + `, + options: ['camelCase'], + parserOptions, + errors: [{ + message: 'Prop "greeting_text" is not camelCase.', + type: 'Property', + line: 4 + }] + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + greetingText: String + } + } + `, + options: ['snake_case'], + parserOptions, + errors: [{ + message: 'Prop "greetingText" is not snake_case.', + type: 'Property', + line: 4 + }] + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + 'greeting-text': String + } + } + `, + options: ['camelCase'], + parserOptions, + errors: [{ + message: 'Prop "greeting-text" is not camelCase.', + type: 'Property', + line: 4 + }] + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + 'greeting-text': String + } + } + `, + options: ['snake_case'], + parserOptions, + errors: [{ + message: 'Prop "greeting-text" is not snake_case.', + type: 'Property', + line: 4 + }] + } + ] +}) diff --git a/tests/lib/utils/casing.js b/tests/lib/utils/casing.js index 743d24fd4..e9cbe921a 100644 --- a/tests/lib/utils/casing.js +++ b/tests/lib/utils/casing.js @@ -32,4 +32,13 @@ describe('getConverter()', () => { assert.ok(converter('FooBar') === 'foo-bar') assert.ok(converter('Foo1Bar') === 'foo1bar') }) + + it('should conver string to snake_case', () => { + const converter = casing.getConverter('snake_case') + + assert.ok(converter('fooBar') === 'foo_bar') + assert.ok(converter('foo-bar') === 'foo_bar') + assert.ok(converter('FooBar') === 'foo_bar') + assert.ok(converter('Foo1Bar') === 'foo1bar') + }) }) From 22a3a75705f88fee3e23e72966aeb6399289f0c9 Mon Sep 17 00:00:00 2001 From: y Date: Thu, 4 Jan 2018 15:23:30 +0900 Subject: [PATCH 10/22] fix message and category --- lib/rules/prop-name-casing.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rules/prop-name-casing.js b/lib/rules/prop-name-casing.js index 23e04a483..2c49b2c69 100644 --- a/lib/rules/prop-name-casing.js +++ b/lib/rules/prop-name-casing.js @@ -40,7 +40,7 @@ function create (context) { if (convertedName !== propName) { context.report({ node: item, - message: 'Prop "{{name}}" is not {{caseType}}.', + message: 'Prop "{{name}}" is not in {{caseType}}.', data: { name: propName, caseType: caseType @@ -59,7 +59,7 @@ module.exports = { meta: { docs: { description: 'enforce specific casing for the Prop name in Vue components', - category: 'strongly-recommended' + category: 'upcoming' }, fixable: null, // or "code" or "whitespace" schema: [ From a846dd1ceeef2248687a328178b8be1bd4be48d8 Mon Sep 17 00:00:00 2001 From: y Date: Thu, 4 Jan 2018 15:27:07 +0900 Subject: [PATCH 11/22] fix category --- lib/rules/prop-name-casing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/prop-name-casing.js b/lib/rules/prop-name-casing.js index 2c49b2c69..f72ab0c67 100644 --- a/lib/rules/prop-name-casing.js +++ b/lib/rules/prop-name-casing.js @@ -59,7 +59,7 @@ module.exports = { meta: { docs: { description: 'enforce specific casing for the Prop name in Vue components', - category: 'upcoming' + category: 'upcoming' // 'strongly-recommended' }, fixable: null, // or "code" or "whitespace" schema: [ From b1a7cca7bb0a42af320a63a477e7bbd7ee743300 Mon Sep 17 00:00:00 2001 From: y Date: Thu, 4 Jan 2018 15:31:11 +0900 Subject: [PATCH 12/22] fix test message --- tests/lib/rules/prop-name-casing.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/lib/rules/prop-name-casing.js b/tests/lib/rules/prop-name-casing.js index fcab592a8..4679d6747 100644 --- a/tests/lib/rules/prop-name-casing.js +++ b/tests/lib/rules/prop-name-casing.js @@ -103,7 +103,7 @@ ruleTester.run('prop-name-casing', rule, { `, parserOptions, errors: [{ - message: 'Prop "greeting_text" is not camelCase.', + message: 'Prop "greeting_text" is not in camelCase.', type: 'Property', line: 4 }] @@ -120,7 +120,7 @@ ruleTester.run('prop-name-casing', rule, { options: ['camelCase'], parserOptions, errors: [{ - message: 'Prop "greeting_text" is not camelCase.', + message: 'Prop "greeting_text" is not in camelCase.', type: 'Property', line: 4 }] @@ -137,7 +137,7 @@ ruleTester.run('prop-name-casing', rule, { options: ['snake_case'], parserOptions, errors: [{ - message: 'Prop "greetingText" is not snake_case.', + message: 'Prop "greetingText" is not in snake_case.', type: 'Property', line: 4 }] @@ -154,7 +154,7 @@ ruleTester.run('prop-name-casing', rule, { options: ['camelCase'], parserOptions, errors: [{ - message: 'Prop "greeting-text" is not camelCase.', + message: 'Prop "greeting-text" is not in camelCase.', type: 'Property', line: 4 }] @@ -171,7 +171,7 @@ ruleTester.run('prop-name-casing', rule, { options: ['snake_case'], parserOptions, errors: [{ - message: 'Prop "greeting-text" is not snake_case.', + message: 'Prop "greeting-text" is not in snake_case.', type: 'Property', line: 4 }] From 17d8c2b7c84dcc959e95a2d16b46512b71e4b87c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sajn=C3=B3g?= Date: Sat, 6 Jan 2018 23:11:45 +0100 Subject: [PATCH 13/22] Set category to undefined --- lib/rules/prop-name-casing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/prop-name-casing.js b/lib/rules/prop-name-casing.js index f72ab0c67..d5df99c54 100644 --- a/lib/rules/prop-name-casing.js +++ b/lib/rules/prop-name-casing.js @@ -59,7 +59,7 @@ module.exports = { meta: { docs: { description: 'enforce specific casing for the Prop name in Vue components', - category: 'upcoming' // 'strongly-recommended' + category: undefined // 'strongly-recommended' }, fixable: null, // or "code" or "whitespace" schema: [ From 789a2fc9e831d65ceb68f5153007e379bfcf1ca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sajn=C3=B3g?= Date: Sun, 28 Jan 2018 23:51:36 +0100 Subject: [PATCH 14/22] Add more tests and fix edge case scenario --- lib/rules/prop-name-casing.js | 4 +++- tests/lib/rules/prop-name-casing.js | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/rules/prop-name-casing.js b/lib/rules/prop-name-casing.js index d5df99c54..02070af22 100644 --- a/lib/rules/prop-name-casing.js +++ b/lib/rules/prop-name-casing.js @@ -25,8 +25,10 @@ function create (context) { const node = obj.properties.find(p => p.type === 'Property' && p.key.type === 'Identifier' && - p.key.name === 'props' + p.key.name === 'props' && + (p.value.type === 'ObjectExpression' || p.value.type === 'ArrayExpression') ) + if (!node) return const items = node.value.type === 'ObjectExpression' ? node.value.properties : node.value.elements diff --git a/tests/lib/rules/prop-name-casing.js b/tests/lib/rules/prop-name-casing.js index 4679d6747..3df60c8ba 100644 --- a/tests/lib/rules/prop-name-casing.js +++ b/tests/lib/rules/prop-name-casing.js @@ -34,6 +34,26 @@ ruleTester.run('prop-name-casing', rule, { `, parserOptions }, + { + filename: 'test.vue', + code: ` + export default { + props: some_props + } + `, + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + ...some_props, + } + } + `, + parserOptions + }, { filename: 'test.vue', code: ` From e804ca288d2c086ac7c2d7fa781d197dbfd73d98 Mon Sep 17 00:00:00 2001 From: erindepew Date: Thu, 4 Jan 2018 22:11:01 -0500 Subject: [PATCH 15/22] attribute order linting --- README.md | 1 + docs/rules/attributes-order.md | 113 ++++++++++++++++++ lib/index.js | 1 + lib/rules/attributes-order.js | 104 +++++++++++++++++ tests/lib/rules/attributes-order.js | 172 ++++++++++++++++++++++++++++ 5 files changed, 391 insertions(+) create mode 100644 docs/rules/attributes-order.md create mode 100644 lib/rules/attributes-order.js create mode 100644 tests/lib/rules/attributes-order.js diff --git a/README.md b/README.md index 26d89faf6..0ff269645 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi | | Rule ID | Description | |:---|:--------|:------------| +| | [vue/attributes-order](./docs/rules/attributes-order.md) | enforce order of attributes | | :wrench: | [vue/html-closing-bracket-newline](./docs/rules/html-closing-bracket-newline.md) | require or disallow a line break before tag's closing brackets | | :wrench: | [vue/html-closing-bracket-spacing](./docs/rules/html-closing-bracket-spacing.md) | require or disallow a space before tag's closing brackets | | :wrench: | [vue/script-indent](./docs/rules/script-indent.md) | enforce consistent indentation in `