본문 바로가기

FrontEnd

AST 활용 1편 : ESLint console.log 체크 플러그인 만들기

반응형

AST를 이용한 정적 소스 코드 분석 / 트랜스파일링 개념을 이해해 봅니다.

npm에 이보다 더 좋은 라이브러리들이 많음으로 활용하시기 바랍니다.

해당 소스 및 설명은 : https://github.com/kentcdodds/asts-workshop\ 에서 발췌했습니다.

 

GitHub - kentcdodds/asts-workshop: Improved productivity 💯 with the practical 🤓 use of the power 💪 of Abstract Syntax T

Improved productivity 💯 with the practical 🤓 use of the power 💪 of Abstract Syntax Trees 🌳 to lint ⚠️ and transform 🔀 your code - GitHub - kentcdodds/asts-workshop: Improved productivity 💯 with the...

github.com

아래 글을 읽기 전에 확인해 주세요

바벨 플러그인을 작성하며 AST 배우기

 

[Babel] 바벨 플러그인을 작성하며 AST 배우기

바벨 플러그인을 만들어보며 JS AST(Abstract syntax trees)를 배워봅시다. 해당 글의 번역입니다 : https://dev.to/viveknayyar/revealing-the-magic-of-ast-by-writing-babel-plugins-1h01 Revealing the magic..

itchallenger.tistory.com

AST의 다양한 활용사례

AST : code for computer

  • js 뿐만 아니라 graphql, 정규 표현식 등도 json 구조로 분석 가능.
  • 파서, 목적에 따라 다양한 구조로 나타날 수 있음

https://resources.jointjs.com/demos/rappid/apps/Ast/index.html

jointjs는 코드를 AST로 시각화해줌

https://astexplorer.net/

const a = 1;

ver,let,const 선언은 kind로 구분함

 

바벨 :  AST를 조작해 다른 AST를 만드는 툴

  • 토크나이저(tokenizer)는 일반적으로 공백(탭, 공백, 새 줄)을 찾아 텍스트 스트림을 토큰으로 나눕니다.
  • 어휘분석기(lexer)는 기본적으로 토크나이저이지만 일반적으로 토큰에 추가 컨텍스트를 첨부합니다.
    • 이 토큰은 숫자이고, 해당 토큰은 문자열 리터럴이며, 다른 토큰은 등호 연산자입니다.
  • 파서(parser)는 렉서에서 토큰 스트림을 가져와
    • 원본 텍스트가 설명하는 프로그램을 나타내는 추상 구문 트리로 바꿉니다.
    • ast가 아닌 다른 출력도 만들 수 있습니다.

https://stackoverflow.com/questions/380455/looking-for-a-clear-definition-of-what-a-tokenizer-parser-and-lexers-are

 

Looking for a clear definition of what a "tokenizer", "parser" and "lexers" are and how they are related to each other and used?

I am looking for a clear definition of what a "tokenizer", "parser" and "lexer" are and how they are related to each other (e.g., does a parser use a tokenizer or vice versa)? I need to create a pr...

stackoverflow.com

바벨 플러그인의 다양한 기능

  • 트리 셰이킹 (체리피킹)

http://slides.com/kentcdodds/a-beginners-guide-to-asts#/3/2

 

Writing custom Babel and ESLint plugins

The Abstract Syntax Tree. It sounds a lot worse than it is. It’s actually quite simple and enables some powerful tools. BabelJS uses it to transform your code from ES.Next to ES5. ESLint uses it to lint your code. And with a knowledge of how it works, yo

slides.com

ex) : 로다시를 자동으로 체리피킹 :  babel-plugin-lodash

ex) : prop-types 자동 제거 : babel-plugin-transform-react-remove-prop-types

ex) : babel-plugin-preval : 런타임이 아닌 빌드 타임에 해당 값을 평가

https://github.com/kentcdodds/babel-plugin-preval

const x = preval`module.exports = 1`

//      ↓ ↓ ↓ ↓ ↓ ↓

const x = 1
const x = preval`
  const fs = require('fs')
  const val = fs.readFileSync(__dirname + '/fixture1.md', 'utf8')
  module.exports = {
    val,
    getSplit: function(splitDelimiter) {
      return x.val.split(splitDelimiter)
    }
  }
`

//      ↓ ↓ ↓ ↓ ↓ ↓

const x = {
  val: '# fixture\n\nThis is some file thing...\n',
  getSplit: function getSplit(splitDelimiter) {
    return x.val.split(splitDelimiter)
  },
}

ex) : babel-plugin-module-alias : import 경로를 바꿔줌


ESLINT

정적 자바스크립트 분석 도구

문법적 컨벤션, 잠재적인 오류 발견

  • eslint-plugin-import
  • eslint-plugin-react
  • eslint-plugin-promise
  • eslint-plugin-secrity
  • eslint-plugin-jsx-a11y

Codemons

super awesome find and replace

특정 코드 구문을 찾아 변경해줌

  • react-codemon
    • 리액트 자동 업데이트 도구
  • 5to6-codemon
  • ava-codemons
    • ava 테스트 프레임워크를 jest 프레임워크로 변경해줌
  • jest-codemons

ESLINT 플러그인 개발하기

1. 직접 개발한 플러그인을 프로젝트에 적용하는 것은 .eslintrc 설정 방법을 확인하면 됩니다.

2. AST Explorer(https://astexplorer.net/)를 켜고 다음과 같이 설정합니다.

  • 파서 : espree
  • 트랜스폼 : 
    • Eslint v8

3. 예제 코드를 아래와 같이 입력합니다.

var csl = console
csl.log()

var lcs = csl
lcs.info()

var scl = lcs
scl.warn()

console.log();
console.warn();

4. 아래 코드를 입력해봅니다.

아래 코드가 이해되지 않는 분들은 바벨과 함께 AST 배우기 글을 참조해 주세요.

meta 규칙은 아래 문서를 참고합니다.

https://eslint.org/docs/latest/developer-guide/working-with-rules

 

Working with Rules - ESLint - Pluggable JavaScript Linter

A pluggable and configurable linter tool for identifying and reporting on patterns in JavaScript. Maintain your code quality with ease.

eslint.org

meta 부분과, identifier의 참조 관계(VariableDeclarator > binding)를 exit에서 처리하는 부분을 제외하면

나머지는 babel 개발 시 visitor 패턴을 사용하는 부분과 매우 유사합니다.

const disallowedMethods = ['log', 'info', 'warn', 'error', 'dir']

module.exports = {
 // 메타 설정에 관한 자세한 정보는
 // https://eslint.org/docs/latest/developer-guide/working-with-rules 를 참고
  meta: {
    docs: {
      description: 'Disallow use of console',
      category: 'Best Practices',
      recommended: true,
    },
    // context.option을 통해 접근 가능
    schema: [
      {
        type: 'object',
        properties: {
          allowedMethods: {
            type: 'array',
            items: {
              enum: ['log', 'info', 'warn', 'error', 'dir'],
            },
            minItems: 1,
            uniqueItems: true,
          },
        },
      },
    ],
  },
  create(context) {
    const config = context.options[0] || {}
    const allowedMethods = config.allowedMethods || []
    const consoleUsage = []
    return {
      Identifier(node) {
        if (node.name !== 'console') {
          return
        }
        consoleUsage.push(node)
      },
      // identifier 참조 관계는 모든 visitor 처리 완료 후 형성됩니다.
      // 따라서 중복 레퍼런스는 해당 부분에서 처리합니다.
      'Program:exit'() {
        consoleUsage.forEach(identifier => {
          if (isDisallowedFunctionCall(identifier)) {
            context.report({
              node: identifier.parent.property,
              message: 'Using console is not allowed',
            })
          } else {
            const variableDeclaratorParent = findParent(
              identifier,
              parent => parent.type === 'VariableDeclarator',
            )
            if (variableDeclaratorParent) {
              const references = context
                .getDeclaredVariables(variableDeclaratorParent)[0]
                .references.slice(1)
              references.forEach(reference => {
                if (
                  !looksLike(reference, {
                    identifier: {
                      parent: {
                        property: isDisallowedFunctionCall,
                      },
                    },
                  })
                ) {
                  return
                }
                context.report({
                  node: reference.identifier.parent.property,
                  message: 'Using console is not allowed',
                })
              })
            }
          }
        })
      },
    }

    function isDisallowedFunctionCall(identifier) {
      return looksLike(identifier, {
        parent: {
          type: 'MemberExpression',
          parent: {type: 'CallExpression'},
          property: {
            name: val =>
              !allowedMethods.includes(val) && disallowedMethods.includes(val),
          },
        },
      })
    }
  },
}

function findParent(node, test) {
  if (test(node)) {
    return node
  } else if (node.parent) {
    return findParent(node.parent, test)
  }
  return null
}

function looksLike(a, b) {
  return (
    a &&
    b &&
    Object.keys(b).every(bKey => {
      const bVal = b[bKey]
      const aVal = a[bKey]
      if (typeof bVal === 'function') {
        return bVal(aVal)
      }
      return isPrimitive(bVal) ? bVal === aVal : looksLike(aVal, bVal)
    })
  )
}

function isPrimitive(val) {
  return val == null || /^[sbn]/.test(typeof val)
}

화면은 다음과 같이 됩니다.

context.report객체를 사용하면, 해당 코드를 삭제하거나 수정하는 것도 가능합니다만, 이는 나중에 기회가 되면 다루어보도록 하겠습니다.

참고

http://slides.com/kentcdodds/a-beginners-guide-to-asts#/6

 

반응형