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 + }] } ] })