eslint.org
Custom Rule Tutorial - 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.vuejs.org
Developer Guide | eslint-plugin-vue
Official ESLint plugin for Vue.js
ESLint를 활용해서 코딩 컨벤션을 정하는데, 이미 정의된 룰 외의 우리만의 규칙이 필요했다. Vue 3.4 이상에서 기존 update:modelValue 기반의 defineEmits 구문 대신 신규 API인 defineModel() 사용을 권장하고 싶었다. 그래서 커스텀 룰을 작성해서 적용했다.
이전에는 eslint-plugin-local-rules나 CommonJS 파일 로더를 억지로 우회 연결해야 했으나, 이젠 프로젝트 내부의 커스텀 룰 객체를 로컬 플러그인으로 바로 등록할 수 있다.
플러그인 모듈 등록
.
├── eslint.config.mjs
└── eslint/
├── local-plugin.mjs
└── rules/
├── prefer-define-model.mjs
└── prefer-define-model.test.mjs
위 구성으로 테스트까지 작성한다.
// eslint/local-plugin.mjs
import preferDefineModel from './rules/prefer-define-model.mjs'
export default {
rules: {
'prefer-define-model': preferDefineModel,
},
}
모듈 파일을 만들고 로컬 플러그인으로 등록해준다.
// eslint.config.mjs
import tsParser from '@typescript-eslint/parser'
import vueParser from 'vue-eslint-parser'
import localPlugin from './eslint/local-plugin.mjs'
export default [
{
files: ['**/*.vue'],
languageOptions: {
parser: vueParser,
parserOptions: {
parser: tsParser,
ecmaVersion: 'latest',
sourceType: 'module',
extraFileExtensions: ['.vue'],
},
},
plugins: {
local: localPlugin, // 로컬 플러그인 등록
},
rules: {
'local/prefer-define-model': 'warn',
},
},
]
plugins에 localPlugin 객체를 직접 주입한다. 커스텀 룰 ID는 local/prefer-define-model이 된다.
Vue SFC 파일을 파싱하려면 vue-eslint-parser를 사용하고, <script setup lang="ts"> 내부 코드는 @typescript-eslint/parser가 파싱하도록 한다.
AST Visitor 구현
ESLint는 파서가 생성한 AST의 노드를 순회하며 원하는 패턴을 찾는다. defineEmits 함수 호출의 인자에서 update:로 시작하는 이벤트 문자열이 존재하는지 검사했다.
// eslint/rules/prefer-define-model.mjs
export default {
meta: {
type: 'suggestion',
docs: {
description: 'update:이벤트를 defineModel로 바꿀 수 있는지 검사한다.',
},
schema: [],
messages: {
preferDefineModel:
'{{ eventName }} 이벤트는 defineModel()로 단순화할 수 있는지 확인하세요.',
},
},
create(context) {
return {
// defineEmits 함수 호출 노드를 타겟팅
'CallExpression[callee.name="defineEmits"]'(node) {
const eventList = node.arguments[0]
if (!eventList || eventList.type !== 'ArrayExpression') return
for (const eventNode of eventList.elements) {
if (
!eventNode ||
eventNode.type !== 'Literal' ||
typeof eventNode.value !== 'string' ||
!eventNode.value.startsWith('update:')
) {
continue
}
// 문제 지점 AST 노드 레포트
context.report({
node: eventNode,
messageId: 'preferDefineModel',
data: {
eventName: eventNode.value,
},
})
}
},
}
},
}
context.report()에 위반이 일어난 실제 AST 노드를 넘겨주므로, IDE에서 update:modelValue 위치에 노란색 밑줄 경고가 나온다. 파서를 다루는 것이 약간 예전 LLVM pass 다룰 때와 비슷한 느낌이다.
RuleTester 단위 테스트
커스텀 룰이 의도대로 동작하는지 시나리오 별로 코드를 작성해 테스트를 돌려볼 수 있다. 직관적으로 어떤 코드를 검사할 때, 룰이 걸리는지 안걸리는지 파악한다.
// eslint/rules/prefer-define-model.test.mjs
import { RuleTester } from 'eslint'
import tsParser from '@typescript-eslint/parser'
import vueParser from 'vue-eslint-parser'
import rule from './prefer-define-model.mjs'
const ruleTester = new RuleTester({
languageOptions: {
parser: vueParser,
parserOptions: {
parser: tsParser,
ecmaVersion: 'latest',
sourceType: 'module',
},
},
})
ruleTester.run('prefer-define-model', rule, {
valid: [
{
filename: 'Component.vue',
code: `<script setup lang="ts">
const emit = defineEmits(['click', 'close'])
</script>`,
},
{
filename: 'Component.vue',
code: `<script setup lang="ts">
const model = defineModel<string>()
</script>`,
},
],
invalid: [
{
filename: 'Component.vue',
code: `<script setup lang="ts">
const emit = defineEmits(['update:modelValue'])
</script>`,
errors: [
{
messageId: 'preferDefineModel',
data: { eventName: 'update:modelValue' },
},
],
},
],
})
filename을 Component.vue처럼 지정해 주어야 vue-eslint-parser가 올바르게 Vue SFC 구문을 테스트 파싱한다.