vue-class-componentでeslint-plugin-vueのno-unused-componentsを使えるようにする

## 要約
eslint-plugin-vue の [no-unused-components](https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/no-unused-components.md) を [クラススタイル Vue コンポーネント](https://jp.vuejs.org/v2/guide/typescript.html#%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB-Vue-%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88) でも動くようにした。

## 今回の成果物
こちらに置いてあります
[negibouze/customize-no-unused-components-rule](https://github.com/negibouze/customize-no-unused-components-rule)

## 詳細
### 1. vue/no-unused-components を入れる
Vue CLI で ESLint を有効にしてプロジェクトを作成している場合は、ここは飛ばして良いです。

#### 1-1. eslint 関連を入れる

```
yarn add -D @vue/cli-plugin-eslint @vue/eslint-config-typescript babel-eslint eslint eslint-plugin-vue
```
#### 1-2. 設定ファイルを追加

```javascript:.eslintrc.js
module.exports = {
  root: true,
  env: {
    node: true
  },
  extends: [
    'plugin:vue/essential'
  ],
  rules: {},
  parserOptions: {
    parser: '@typescript-eslint/parser'
  }
}
```

#### 1-3. 動作確認

![no-unused.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37319/502b7822-66a7-a6b5-f239-01cc4e62426e.png)

no-unused-components の動作が確認できたので、カスタムルールを作成していきます。
### 2. カスタムルールの準備
#### 2-1. EsLint ディレクトリの作成

カスタムルールを入れるディレクトリを作成します（今回はプロジェクト直下に作成）。

```
eslint
   ├── rules
   └── tests
```

#### 2-2. rule、test ファイルの追加

各ディレクトリ配下に必要なファイルを追加します。

```
eslint
   ├── rules
   │   └── no-unused-components.js
   └── tests
       └── no-unused-components.js
```

#### 2.3. 初期状態の作成

eslint-plugin-vue のルールをベースにさせてもらうので、まずはそのままコピーします。
元ルールの動きを壊していないことを確認するために、テストは2ファイル用意。

```
eslint
   ├── rules
   │   └── no-unused-components.js
   └── tests
       ├── no-unused-components-original.js <= eslint-plugin-vue のファイル
       └── no-unused-components.js
```

#### 2.4. ルールの修正

utils, casing は使わせてもらうのでパスを変更します。

```javascript:
// 変更前
const utils = require('../utils')
const casing = require('../utils/casing')
// 変更後
const utils = require('eslint-plugin-vue/lib/utils')
const casing = require('eslint-plugin-vue/lib/utils/casing')
```

#### 2.5. 設定変更

自作ルールを使うように設定を変更します。

```javascript:.eslintrc.js
rules: {
  'vue/no-unused-components': 'off',
  'no-unused-components': 'error'
},
```

```json:package.json
{
  "scripts": {
    "lint": "vue-cli-service lint --rulesdir eslint/rules"
  }
}
```

#### 2.6. 動作確認

```
yarn run lint
```

```
error: The "HelloWorld" component has been registered but not used (no-unused-components) at src/components/Example2.vue:10:5:
   8 | export default {
   9 |   components: {
> 10 |     HelloWorld
     |     ^
  11 |   }
  12 | }
  13 | </script>
```

カスタムルールで動くことが確認できたので、いよいよルールを変更していきます。

### 3. カスタムルールの追加
#### 3-1. 調査

少し調べると registeredComponents と usedComponents あたりが見つかります。
もう少し調べると、

- クラススタイルの場合は登録しても registeredComponents に入っていない
- そもそも utils.executeOnVue が呼ばれない

あたりが分かりますので、そこを解決すれば良さそうです。

```javascript:no-unused-components.js
  create (context) {
    // 中略
    return utils.defineTemplateBodyVisitor(context, {
      "VElement[name='template']:exit" (rootNode) {
　　　　　// 中略
        registeredComponents
          .filter(({ name }) => {
            if (casing.pascalCase(name) === name || casing.camelCase(name) === name) {
              return ![...usedComponents].some(n => {
                return n.indexOf('_') === -1 && (name === casing.pascalCase(n) || casing.camelCase(n) === name)
              })
            } else {
              return !usedComponents.has(name)
            }
          })
          .forEach(({ node, name }) => context.report({
            node,
            message: 'The "{{name}}" component has been registered but not used.',
            data: {
              name
            }
          }))
      }
    }, utils.executeOnVue(context, (obj) => {
      // クラススタイルだとここまで来ない。
      registeredComponents = utils.getRegisteredComponents(obj)
    }))
  }
```

がんばって調べると、isVueComponentFile あたりに手を入れれば良さそうなことが分かります。
executeOnVue -> executeOnVueComponent -> isVueComponentFile

```javascript:eslint-plugin-vue/utils/index.js
isVueComponentFile (node, path) {
  return this.isVueFile(path) &&
    node.type === 'ExportDefaultDeclaration' &&
    node.declaration.type === 'ObjectExpression'
},
```

#### 3-2. ルールの編集

調査の結果、isVueComponentFile を変更すれば良さそうなことが分かりましたが、
utils は使わせてもらいたいので、こんな感じに変更します。

```javascript:eslint/rules/no-unused-components.js
const cloneDeep = require('lodash/cloneDeep')

const myUtils = cloneDeep(utils)
const originalIsVueComponentFile = myUtils.isVueComponentFile
myUtils.isVueComponentFile = (node, path) => {
  return (
    originalIsVueComponentFile.call(myUtils, node, path) ||
    node.declaration.type === 'ClassDeclaration'
  )
}

// 以下、utils を使っているところを myUtils に変更
```

上記の変更で executeOnVue が呼ばれるようになりましたが、下記のエラーが発生しました。

```
TypeError: Cannot read property 'find' of undefined
```

ただ、ここは分岐を追加するだけで良さそうです。

```javascript:eslint/no-unused-components.js
// 変更
myUtils.executeOnVue(context, (obj) => {
  if (obj.type === 'ClassDeclaration') {
    registeredComponents = getRegisteredComponentsFromClassDeclaration(obj)
  } else {
    registeredComponents = myUtils.getRegisteredComponents(obj)
  }
}

// 追加
const getRegisteredComponentsFromClassDeclaration = obj => {
  const decorators = obj.decorators
  if (!decorators) return []
  const expression = decorators[0].expression
  const hasArguments = expression && expression.arguments && expression.arguments.length >= 1
  return hasArguments ? myUtils.getRegisteredComponents(expression.arguments[0]) : []
}
```

#### 3-3. 動作確認

```
yarn run lint
```

```
error: The "HelloWorld" component has been registered but not used (no-unused-components) at src/components/Example.vue:11:5:
   9 | @Component({
  10 |   components: {
> 11 |     HelloWorld
     |     ^
  12 |   }
  13 | })
  14 | export default class Example extends Vue { }


error: The "HelloWorld" component has been registered but not used (no-unused-components) at src/components/Example3.vue:10:5:
   8 | export default {
   9 |   components: {
> 10 |     HelloWorld
     |     ^
  11 |   }
  12 | }
  13 | </script>
```

両方検知できました！:thumbsup:

### 4. Visual Studio Code で動くようにする
#### 4-1. eslint-plugin-rulesdir を入れる

```
yarn add -D eslint-plugin-rulesdir
```

#### 4-2. 設定変更

```javascript:.eslintrc.js
// 追加
const rulesDirPlugin = require('eslint-plugin-rulesdir')
rulesDirPlugin.RULES_DIR = 'eslint/rules'

module.exports = {
  // 追加
  plugins: [
    'rulesdir'
  ],
  rules: {
    'vue/no-unused-components': 'off',
    'rulesdir/no-unused-components': 'error' // <= 変更
  },
}
```

#### 4-3. 動作確認

![no-unused-components-error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37319/d43b5106-76f2-2c84-6e5f-3b18cb234c8e.png)

検知できました！:tada:
！動かない場合は VSCode を再起動すると直るかも

### 5. 積み残し
1.この書き方だと AST の構造が変わるため動きません。

```javascript
<template>
  <div />
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import HelloWorld from './HelloWorld.vue'

@Component({
  components: {
    HelloWorld
  }
})
class Example extends Vue { }
export default Example
</script>
```

2.現在の [eslint-plugin-vue の master ブランチ](https://github.com/vuejs/eslint-plugin-vue) だと動きません  
`vue-eslint-parser` のバージョンアップにより AST の構造が変わっているため動かなくなります。

```javascript:eslint-plugin-vue/package.json
"dependencies": {
  "vue-eslint-parser": "^6.0.4"
}
```

rule を下記のように変更すると動くようになります。

```javascript:eslint/no-unused-components.js
// 修正前
"VAttribute[directive=true][key.name='bind'][key.argument='is']" (node) {
}
// 修正後
"VAttribute[directive=true][key.name.name='bind'][key.argument.name='is']" (node) {
}
```