Repository: vuejs/eslint-plugin-vue
Pull request #381 (2018-02-07 02:37:46)

[Update] Make `vue/order-in-components` fixable
This PR makes `vue/order-in-components` fixable. #299
In case of `The "A" property should be above the "B" property` error, autofix will move A before B


Patch:
diff --git a/lib/rules/max-attributes-per-line.js b/lib/rules/max-attributes-per-line.js
index 9a54fd1e..0d05f0be 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/lib/rules/order-in-components.js b/lib/rules/order-in-components.js
index 6bd0fb22..3d9a21af 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/lib/utils/indent-common.js b/lib/utils/indent-common.js
index c8719a98..1b93f206 100644
--- a/lib/utils/indent-common.js
+++ b/lib/utils/indent-common.js
@@ -631,8 +631,16 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
    */
   function validateCore (token, expectedIndent, optionalExpectedIndent) {
     const line = token.loc.start.line
-    const actualIndent = token.loc.start.column
     const indentText = getIndentText(token)
+
+    // If there is no line terminator after the `<script>` start tag,
+    // `indentText` contains non-whitespace characters.
+    // In that case, do nothing in order to prevent removing the `<script>` tag.
+    if (indentText.trim() !== '') {
+      return
+    }
+
+    const actualIndent = token.loc.start.column
     const unit = (options.indentChar === '\t' ? 'tab' : 'space')
 
     for (let i = 0; i < indentText.length; ++i) {


Test patch:
diff --git a/tests/fixtures/script-indent/no-linebreak-script.vue b/tests/fixtures/script-indent/no-linebreak-script.vue
new file mode 100644
index 00000000..b8555f2d
--- /dev/null
+++ b/tests/fixtures/script-indent/no-linebreak-script.vue
@@ -0,0 +1,3 @@
+<!--{}-->
+<script>var a
+</script>
diff --git a/tests/lib/rules/max-attributes-per-line.js b/tests/lib/rules/max-attributes-per-line.js
index 773f1b66..0e8ab915 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: `<template><component name="John Doe" age="30"></component></template>`,
+      output: `<template><component name="John Doe" 
+age="30"></component></template>`,
       errors: ['Attribute "age" should be on a new line.']
     },
     {
@@ -99,6 +101,12 @@ ruleTester.run('max-attributes-per-line', rule, {
         age="30">
         </component>
       </template>`,
+      output: `<template><component 
+job="Vet"
+        name="John Doe"
+        age="30">
+        </component>
+      </template>`,
       errors: [{
         message: 'Attribute "job" should be on a new line.',
         type: 'VAttribute',
@@ -108,6 +116,8 @@ ruleTester.run('max-attributes-per-line', rule, {
     {
       code: `<template><component name="John Doe" age="30" job="Vet"></component></template>`,
       options: [{ singleline: { max: 2 }}],
+      output: `<template><component name="John Doe" age="30" 
+job="Vet"></component></template>`,
       errors: [{
         message: 'Attribute "job" should be on a new line.',
         type: 'VAttribute',
@@ -117,6 +127,8 @@ ruleTester.run('max-attributes-per-line', rule, {
     {
       code: `<template><component name="John Doe" age="30" job="Vet"></component></template>`,
       options: [{ singleline: 1, multiline: { max: 1, allowFirstLine: false }}],
+      output: `<template><component name="John Doe" 
+age="30" job="Vet"></component></template>`,
       errors: [{
         message: 'Attribute "age" should be on a new line.',
         type: 'VAttribute',
@@ -133,6 +145,11 @@ ruleTester.run('max-attributes-per-line', rule, {
         </component>
       </template>`,
       options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: false }}],
+      output: `<template><component 
+name="John Doe"
+        age="30">
+        </component>
+      </template>`,
       errors: [{
         message: 'Attribute "name" should be on a new line.',
         type: 'VAttribute',
@@ -146,6 +163,12 @@ ruleTester.run('max-attributes-per-line', rule, {
         </component>
       </template>`,
       options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: false }}],
+      output: `<template><component
+        name="John Doe" 
+age="30"
+        job="Vet">
+        </component>
+      </template>`,
       errors: [{
         message: 'Attribute "age" should be on a new line.',
         type: 'VAttribute',
@@ -159,6 +182,12 @@ ruleTester.run('max-attributes-per-line', rule, {
         </component>
       </template>`,
       options: [{ singleline: 3, multiline: 1 }],
+      output: `<template><component
+        name="John Doe" 
+age="30"
+        job="Vet">
+        </component>
+      </template>`,
       errors: [{
         message: 'Attribute "age" should be on a new line.',
         type: 'VAttribute',
@@ -172,6 +201,12 @@ ruleTester.run('max-attributes-per-line', rule, {
         </component>
       </template>`,
       options: [{ singleline: 3, multiline: { max: 2, allowFirstLine: false }}],
+      output: `<template><component
+        name="John Doe" age="30"
+        job="Vet" pet="dog" 
+petname="Snoopy">
+        </component>
+      </template>`,
       errors: [{
         message: 'Attribute "petname" should be on a new line.',
         type: 'VAttribute',
@@ -185,6 +220,12 @@ ruleTester.run('max-attributes-per-line', rule, {
         </component>
       </template>`,
       options: [{ singleline: 3, multiline: { max: 2, allowFirstLine: false }}],
+      output: `<template><component
+        name="John Doe" age="30"
+        job="Vet" pet="dog" 
+petname="Snoopy" extra="foo">
+        </component>
+      </template>`,
       errors: [{
         message: 'Attribute "petname" should be on a new line.',
         type: 'VAttribute',
diff --git a/tests/lib/rules/order-in-components.js b/tests/lib/rules/order-in-components.js
index 48501541..2eff407e 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 (
+              <span>{ this.msg }</span>
+            )
+          },
+          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: '<div></div>'
+        })
+      `,
       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: '<div></div>'
+        })
+      `,
       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: '<div></div>'
+        })
+      `,
       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
+      }]
     }
   ]
 })