AST를 이용한 정적 소스 코드 분석 / 트랜스파일링 개념을 이해해 봅니다.
npm에 이보다 더 좋은 라이브러리들이 많음으로 활용하시기 바랍니다.
해당 소스 및 설명은 : https://github.com/kentcdodds/asts-workshop\ 에서 발췌했습니다.
아래 글을 읽기 전에 확인해 주세요
AST의 다양한 활용사례
AST : code for computer
- js 뿐만 아니라 graphql, 정규 표현식 등도 json 구조로 분석 가능.
- 파서, 목적에 따라 다양한 구조로 나타날 수 있음
https://resources.jointjs.com/demos/rappid/apps/Ast/index.html
const a = 1;
바벨 : AST를 조작해 다른 AST를 만드는 툴
- 토크나이저(tokenizer)는 일반적으로 공백(탭, 공백, 새 줄)을 찾아 텍스트 스트림을 토큰으로 나눕니다.
- 어휘분석기(lexer)는 기본적으로 토크나이저이지만 일반적으로 토큰에 추가 컨텍스트를 첨부합니다.
- 이 토큰은 숫자이고, 해당 토큰은 문자열 리터럴이며, 다른 토큰은 등호 연산자입니다.
- 파서(parser)는 렉서에서 토큰 스트림을 가져와
- 원본 텍스트가 설명하는 프로그램을 나타내는 추상 구문 트리로 바꿉니다.
- ast가 아닌 다른 출력도 만들 수 있습니다.
바벨 플러그인의 다양한 기능
- 트리 셰이킹 (체리피킹)
http://slides.com/kentcdodds/a-beginners-guide-to-asts#/3/2
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
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
'FrontEnd' 카테고리의 다른 글
소프트웨어 합성 : 트랜스듀서(Transducers) (0) | 2022.09.20 |
---|---|
소프트웨어 합성 : 리듀서(reducer) (0) | 2022.09.20 |
[React] React.cloneElement 사용 사례 (0) | 2022.09.15 |
리액트 디자인 패턴 : 컴파운드 컴포넌트 패턴 [Compound Component Pattern] 2 (0) | 2022.09.15 |
[Babel] 바벨 플러그인을 작성하며 AST 배우기 (0) | 2022.09.14 |