Repository: vuejs/eslint-plugin-vue
Pull request #430 (2018-03-14 10:28:36)

Spelling correction
Corrected Spelling.
enfore -> enforce


Patch:
diff --git a/docs/rules/attributes-order.md b/docs/rules/attributes-order.md
index 137fee21..0462bdf3 100644
--- a/docs/rules/attributes-order.md
+++ b/docs/rules/attributes-order.md
@@ -4,7 +4,7 @@
 
 ## :book: Rule Details
 
-This rule aims to enfore ordering of component attributes. The default order is specified in the [Vue styleguide](https://vuejs.org/v2/style-guide/#Element-attribute-order-recommended) and is:
+This rule aims to enforce ordering of component attributes. The default order is specified in the [Vue styleguide](https://vuejs.org/v2/style-guide/#Element-attribute-order-recommended) and is:
 - DEFINITION
 ex: 'is'
 - LIST_RENDERING
@@ -86,7 +86,7 @@ Specify custom order of attribute groups
 :+1: Examples of **correct** code with custom order`:
 
 ```html
-<!-- 'vue/attribute-order': [2, { order: ['LIST_RENDERING', 'CONDITIONALS', 'RENDER_MODIFIERS', 'GLOBAL', 'UNIQUE', 'BINDING', 'OTHER_ATTR', 'EVENTS', 'CONTENT', 'DEFINITION'] }] -->
+<!-- 'vue/attributes-order': [2, { order: ['LIST_RENDERING', 'CONDITIONALS', 'RENDER_MODIFIERS', 'GLOBAL', 'UNIQUE', 'BINDING', 'OTHER_ATTR', 'EVENTS', 'CONTENT', 'DEFINITION'] }] -->
 <div
   propOne="prop"
   propTwo="prop"
@@ -95,7 +95,7 @@ Specify custom order of attribute groups
 ```
 
 ```html
-<!-- 'vue/attribute-order': [2, { order: ['LIST_RENDERING', 'CONDITIONALS', 'RENDER_MODIFIERS', 'GLOBAL', 'UNIQUE', 'BINDING', 'DEFINITION', 'OTHER_ATTR', 'EVENTS', 'CONTENT'] }] -->
+<!-- 'vue/attributes-order': [2, { order: ['LIST_RENDERING', 'CONDITIONALS', 'RENDER_MODIFIERS', 'GLOBAL', 'UNIQUE', 'BINDING', 'DEFINITION', 'OTHER_ATTR', 'EVENTS', 'CONTENT'] }] -->
 <div
   ref="header"
   is="header"
@@ -107,10 +107,10 @@ Specify custom order of attribute groups
 :-1: Examples of **incorrect** code with custom order`:
 
 ```html
-<!-- 'vue/attribute-order': [2, { order: ['LIST_RENDERING', 'CONDITIONALS', 'RENDER_MODIFIERS', 'GLOBAL', 'UNIQUE', 'BINDING', 'DEFINITION', 'OTHER_ATTR', 'EVENTS', 'CONTENT'] }] -->
+<!-- 'vue/attributes-order': [2, { order: ['LIST_RENDERING', 'CONDITIONALS', 'RENDER_MODIFIERS', 'GLOBAL', 'UNIQUE', 'BINDING', 'DEFINITION', 'OTHER_ATTR', 'EVENTS', 'CONTENT'] }] -->
 <div
   ref="header"
   propOne="prop"
   is="header">
 </div>
-```
\ No newline at end of file
+```
diff --git a/lib/rules/prop-name-casing.js b/lib/rules/prop-name-casing.js
index f744aee5..04c2a16e 100644
--- a/lib/rules/prop-name-casing.js
+++ b/lib/rules/prop-name-casing.js
@@ -8,6 +8,20 @@ const utils = require('../utils')
 const casing = require('../utils/casing')
 const allowedCaseOptions = ['camelCase', 'snake_case']
 
+function canFixPropertyName (node, originalName) {
+  // Can not fix of computed property names & shorthand
+  if (node.computed || node.shorthand) {
+    return false
+  }
+  const key = node.key
+  // Can not fix of unknown types
+  if (key.type !== 'Literal' && key.type !== 'Identifier') {
+    return false
+  }
+  // Can fix of ASCII printable characters
+  return originalName.match(/[ -~]+/)
+}
+
 // ------------------------------------------------------------------------------
 // Rule Definition
 // ------------------------------------------------------------------------------
@@ -46,7 +60,12 @@ function create (context) {
           data: {
             name: propName,
             caseType: caseType
-          }
+          },
+          fix: canFixPropertyName(item, propName) ? fixer => {
+            return item.key.type === 'Literal'
+              ? fixer.replaceText(item.key, item.key.raw.replace(item.key.value, convertedName))
+              : fixer.replaceText(item.key, convertedName)
+          } : undefined
         })
       }
     }
@@ -63,7 +82,7 @@ module.exports = {
       description: 'enforce specific casing for the Prop name in Vue components',
       category: undefined // 'strongly-recommended'
     },
-    fixable: null, // or "code" or "whitespace"
+    fixable: 'code',  // null or "code" or "whitespace"
     schema: [
       {
         enum: allowedCaseOptions
diff --git a/lib/utils/indent-common.js b/lib/utils/indent-common.js
index dfc89d57..f93379da 100644
--- a/lib/utils/indent-common.js
+++ b/lib/utils/indent-common.js
@@ -1024,6 +1024,9 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
     ExportAllDeclaration (node) {
       const tokens = tokenStore.getTokens(node)
       const firstToken = tokens.shift()
+      if (isSemicolon(last(tokens))) {
+        tokens.pop()
+      }
       setOffset(tokens, 1, firstToken)
     },
 
@@ -1445,25 +1448,20 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
 
     // Process semicolons.
     ':statement' (node) {
-      const info = offsets.get(tokenStore.getFirstToken(node))
+      const firstToken = tokenStore.getFirstToken(node)
       const lastToken = tokenStore.getLastToken(node)
-      if (info == null) {
-        return
-      }
-      if (isSemicolon(lastToken)) {
-        offsets.set(lastToken, info)
+      if (isSemicolon(lastToken) && firstToken !== lastToken) {
+        setOffset(lastToken, 0, firstToken)
       }
 
       // Set to the semicolon of the previous token for semicolon-free style.
       // E.g.,
       //   foo
       //   ;[1,2,3].forEach(f)
-      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)
-        }
+      const info = offsets.get(firstToken)
+      const prevToken = tokenStore.getTokenBefore(firstToken)
+      if (info != null && isSemicolon(prevToken) && prevToken.loc.end.line === firstToken.loc.start.line) {
+        offsets.set(prevToken, info)
       }
     },
 


Test patch:
diff --git a/tests/fixtures/script-indent/export-named-declaration-04.vue b/tests/fixtures/script-indent/export-named-declaration-04.vue
index 410623c4..5cd53b33 100644
--- a/tests/fixtures/script-indent/export-named-declaration-04.vue
+++ b/tests/fixtures/script-indent/export-named-declaration-04.vue
@@ -2,7 +2,7 @@
 <script>
 export
   const a = 0
-;
+  ;
 export
   const
     b
diff --git a/tests/fixtures/script-indent/issue407.vue b/tests/fixtures/script-indent/issue407.vue
new file mode 100644
index 00000000..40ecda9d
--- /dev/null
+++ b/tests/fixtures/script-indent/issue407.vue
@@ -0,0 +1,12 @@
+<!--{"options":[4,{"baseIndent":1,"switchCase":2}]}-->
+<script>
+    function test() {
+        const variable = 0;
+
+        if (variable === 0) {
+            console.info('wrong indentation reported');
+        }
+    }
+
+    test();
+</script>
diff --git a/tests/lib/rules/prop-name-casing.js b/tests/lib/rules/prop-name-casing.js
index 3df60c8b..0c706635 100644
--- a/tests/lib/rules/prop-name-casing.js
+++ b/tests/lib/rules/prop-name-casing.js
@@ -121,6 +121,13 @@ ruleTester.run('prop-name-casing', rule, {
           }
         }
       `,
+      output: `
+        export default {
+          props: {
+            greetingText: String
+          }
+        }
+      `,
       parserOptions,
       errors: [{
         message: 'Prop "greeting_text" is not in camelCase.',
@@ -138,6 +145,13 @@ ruleTester.run('prop-name-casing', rule, {
         }
       `,
       options: ['camelCase'],
+      output: `
+        export default {
+          props: {
+            greetingText: String
+          }
+        }
+      `,
       parserOptions,
       errors: [{
         message: 'Prop "greeting_text" is not in camelCase.',
@@ -155,6 +169,13 @@ ruleTester.run('prop-name-casing', rule, {
         }
       `,
       options: ['snake_case'],
+      output: `
+        export default {
+          props: {
+            greeting_text: String
+          }
+        }
+      `,
       parserOptions,
       errors: [{
         message: 'Prop "greetingText" is not in snake_case.',
@@ -172,6 +193,13 @@ ruleTester.run('prop-name-casing', rule, {
         }
       `,
       options: ['camelCase'],
+      output: `
+        export default {
+          props: {
+            'greetingText': String
+          }
+        }
+      `,
       parserOptions,
       errors: [{
         message: 'Prop "greeting-text" is not in camelCase.',
@@ -189,12 +217,156 @@ ruleTester.run('prop-name-casing', rule, {
         }
       `,
       options: ['snake_case'],
+      output: `
+        export default {
+          props: {
+            'greeting_text': String
+          }
+        }
+      `,
       parserOptions,
       errors: [{
         message: 'Prop "greeting-text" is not in snake_case.',
         type: 'Property',
         line: 4
       }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            'greeting_text': String
+          }
+        }
+      `,
+      output: `
+        export default {
+          props: {
+            'greetingText': String
+          }
+        }
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Prop "greeting_text" is not in camelCase.',
+        type: 'Property',
+        line: 4
+      }]
+    },
+    {
+      // computed property name
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            ['greeting-text']: String
+          }
+        }
+      `,
+      output: null,
+      parserOptions,
+      errors: [{
+        message: 'Prop "greeting-text" is not in camelCase.',
+        type: 'Property',
+        line: 4
+      }]
+    },
+    {
+      // shorthand
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            greeting_text
+          }
+        }
+      `,
+      output: null,
+      parserOptions,
+      errors: [{
+        message: 'Prop "greeting_text" is not in camelCase.',
+        type: 'Property',
+        line: 4
+      }]
+    },
+    {
+      // valiable computed property name
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            [greeting_text]: String
+          }
+        }
+      `,
+      output: null,
+      parserOptions,
+      errors: [{
+        // bug ?
+        message: 'Prop "greeting_text" is not in camelCase.',
+        type: 'Property',
+        line: 4
+      }]
+    },
+    {
+      // emoji
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            '\u{1F37B}': String
+          }
+        }
+      `,
+      output: null,
+      parserOptions,
+      errors: [{
+        message: 'Prop "\u{1F37B}" is not in camelCase.',
+        type: 'Property',
+        line: 4
+      }]
+    },
+    {
+      // Japanese characters
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            '漢字': String
+          }
+        }
+      `,
+      output: null,
+      parserOptions,
+      errors: [{
+        message: 'Prop "漢字" is not in camelCase.',
+        type: 'Property',
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            'abc-123-def': String
+          }
+        }
+      `,
+      output: `
+        export default {
+          props: {
+            'abc123Def': String
+          }
+        }
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Prop "abc-123-def" is not in camelCase.',
+        type: 'Property',
+        line: 4
+      }]
     }
   ]
 })