본문 바로가기

프론트엔드

ESLint custom rule(plugin) 만들기

  • 키워드: eslint, custom rule, eslint plugin, spellcheck, generator-eslint, Yeoman, AST
  • 상황
    1. 프로젝트에서 자주 쓰이는 단어들이 있는데, 디자이너에게 전달받은 문구에 오타가 있는 경우가 잦다.
    2. Lint를 이용하여 코드 레벨에서 오타를 잡아낼 수 있으면 좋겠다고 생각했다.
    3. IDE에 사전을 등록하는 기능은 있지만, 오타를 감지하고 수정해주는 rule까지는 설정할 수 없기 때문에 custom rule을 만들어보기로 한다.
  • 해결 과정
      1. eslint custom rule을 만들 수 있는 generator-eslint package를 이용하기로 한다.
        (https://www.npmjs.com/package/generator-eslint)

        README 대로 Yeoman과 generator-eslint 패키지를 설치한다.
          npm i -g yo
        npm i -g generator-eslint


      2. README대로 eslint plugin 개발 환경을 만든다.
          mkdir eslint-plugin-loplat-test
        cd eslint-plugin-loplat-test
        
        yo eslint:plugin
          // ? Does this plugin contain custom ESLint rules? Yes
          // ? Does this plugin contain one or more processors? No
        
        yo eslint:rule
          // ? Where will this rule be published? ESLint Plugin
        이 때 plugin 이름과 rule 이름은 원하는대로 설정한다.


      3. 그리고 lib/rules/spellcheck.js 에서 원하는 custom rule을 작성한다.
        rule 작성 규칙은 eslint 문서를 참고한다. (https://eslint.org/docs/developer-guide/working-with-rules#contextreport)
        rule을 적용할 node type은 AST explorer에서 확인한다.(https://astexplorer.net/
          // lib/rules/spellcheck.js
        
        /**
         * @fileoverview test
         * @author loplat
         */
        'use strict';
        
        //------------------------------------------------------------------------------
        // Rule Definition
        //------------------------------------------------------------------------------
        
        /**
         * @type {import('eslint').Rule.RuleModule}
         */
        
        module.exports = {
         meta: {
          type: 'problem', // `problem`, `suggestion`, or `layout`
          fixable: 'code', // Or `code` or `whitespace`
          schema: [] // Add a schema if the rule has options
         },
        
         create(context) {
          // variables should be defined here
          const dictionary = {
           메세지: '메시지',
           Loplat: 'loplat',
           'loplat x': 'loplat X',
           'loplat I': 'loplat i'
          };	
        
          return {
           // visitor functions for different types of nodes
           Literal(node) {  // Literal type 일때
            const text = node.raw;
        
            Object.entries(dictionary).forEach(([wrong, correct]) => {
             if (text.includes(wrong)) {  // 오타가 있으면
              context.report({
               node,
               message: "'{{ correct }}'가 맞는 표현입니다.",
               data: { correct },
               fix: (fixer) =>
                fixer.replaceText(node, text.replace(wrong, correct)) // 올바른 표현으로 교정
              });
             }
            });
           }
          };
         }
        };
        작성한 custom rule은 아래와 같은 상황의 오류를 감지/보고한다.
        - Literal type의 AST node일 경우 &&
        - node의 text가 '메세지', 'Loplat', 'loplat x', 'loplat I'를 포함할 경우 &&
        - context.report를 이용하여 오류 message를 보고하고, fix 속성을 이용하여 올바른 표현으로 교정한다.

      4. 작성한 rule을 tests/lib/rules/spellckeck.js를 통해 테스트한다.
        테스트 파일은 아래와 같이 작성했다.
          // tests/lib/rules/spellckeck.js
         
        /**
         * @fileoverview test
         * @author loplat
         */
        'use strict';
        
        //------------------------------------------------------------------------------
        // Requirements
        //------------------------------------------------------------------------------
        
        const rule = require('../../../lib/rules/spellcheck'),
         RuleTester = require('eslint').RuleTester;
        
        //------------------------------------------------------------------------------
        // Tests
        //------------------------------------------------------------------------------
        
        const dictionary = {
         메세지: '메시지',
         Loplat: 'loplat',
         'loplat x': 'loplat X',
         'loplat I': 'loplat i'
        };
        
        const ruleTester = new RuleTester();
        ruleTester.run('spellcheck', rule, {
         valid: Object.values(dictionary).map((correct) => ({
          code: `console.log('${correct}')`
         })),
         invalid: Object.entries(dictionary).map(([wrong, correct]) => ({
          code: `console.log('${wrong}')`,
          output: `console.log('${correct}')`,
          errors: [{ message: `'${correct}'가 맞는 표현입니다.`, type: 'Literal' }]
         }))
        });

        ruleTester를 통해 valid한 경우와 invalid한 경우에 대한 예시 code를 작성할 수 있다.
        invalid한 경우엔 error message가 어떻게 나오는 지(errors), 어떻게 fix 되어야하는지(output) 테스트할 수 있다.

        테스트 파일은 yarn test로 테스트 가능하다.
          yarn test
        
        // 결과
        spellckeck
            valid
              ✔ console.log('메시지')
              ✔ console.log('loplat')
              ✔ console.log('loplat X')
              ✔ console.log('loplat i')
            invalid
              ✔ console.log('메세지')
              ✔ console.log('Loplat')
              ✔ console.log('loplat x')
              ✔ console.log('loplat I')
        
        
          8 passing (56ms)


      5. 이제 custom rule을 실제 프로젝트에서 import 해야한다.
        먼저 custom eslint plugin을 npm에 배포한다.
          npm publish
      6. plugin이 정상적으로 배포되면, plugin을 사용할 프로젝트에서 설치한다.
          yarn add -D eslint-plugin-loplat-test


        .eslintrc.js 에서 plugin과 rule을 추가한다.
          // .eslintrc.js
        plugins: ['loplat-test'],
        rules: {
         'loplat-test/spellcheck': 'error'
        },


      7. 이제 ESLint custom rule이 적용된 것을 확인할 수 있다! 
        IDE에 custom rule이 적용된 모습
        eslint fix를 적용해 오타를 수정할 수 있다.
        eslint fix 적용 후