Парсер в ESLint отвечает за преобразование исходного JavaScript-кода в абстрактное синтаксическое дерево (AST), которое используется всеми правилами анализа. Вся система линтинга опирается на корректность и полноту AST, включая информацию о позициях, комментариях, типах узлов и дополнительных метаданных.
ESLint не привязывает разработчика к единственному парсеру. Вместо этого используется контракт, позволяющий подключать собственные реализации, если они удовлетворяют требованиям структуры AST и интерфейса взаимодействия.
Основные задачи парсера:
ESLint ориентируется на спецификацию ESTree. Это де-факто стандарт представления JavaScript-кода в виде дерева.
Каждый узел AST должен содержать:
type — тип узла (например,
BinaryExpression, Identifier)loc — позиции в исходном кодеrange — числовые индексы начала и концаleft,
right, operator)Пример узла:
{
"type": "BinaryExpression",
"operator": "+",
"left": {
"type": "Identifier",
"name": "a",
"range": [0, 1],
"loc": { "start": { "line": 1, "column": 0 }, "end": { "line": 1, "column": 1 } }
},
"right": {
"type": "Identifier",
"name": "b",
"range": [4, 5],
"loc": { "start": { "line": 1, "column": 4 }, "end": { "line": 1, "column": 5 } }
},
"range": [0, 5]
}
Любое отклонение от ESTree приводит к некорректной работе правил ESLint.
ESLint ожидает от парсера реализацию одного из двух интерфейсов:
parse(code, options)parseForESLint(code, options)Современный формат предполагает использование
parseForESLint, так как он позволяет возвращать
дополнительные сервисы.
parseForESLint(code, options) {
return {
ast,
services,
visitorKeys,
scopeManager
};
}
Обязательным элементом является только ast, остальные
поля используются для расширенной функциональности.
Простейший парсер может быть построен поверх существующих решений (например, Acorn или Espree), с последующим преобразованием результата.
import { parse } from "acorn";
export function parseForESLint(code) {
const ast = parse(code, {
ecmaVersion: "latest",
locations: true,
ranges: true,
sourceType: "module"
});
return {
ast
};
}
Такая реализация уже совместима с ESLint, но лишена расширенных возможностей.
Полностью собственный парсер требует реализации следующих этапов:
На этом этапе исходный код разбивается на токены:
Пример токенов:
let a = 10;
→
LET, IDENTIFIER(a), EQUALS, NUMBER(10), SEMICOLON
Токены преобразуются в дерево согласно грамматике языка.
Пример:
let a = 10;
AST:
VariableDeclaration
└── VariableDeclarator
├── Identifier (a)
└── Literal (10)
Ключевая техника — рекурсивный спуск (recursive descent parser).
Пример структуры:
function parseEx * pression() {
return parseAdditive();
}
function parseAdditive() {
let node = parsePrimary();
while (match("+")) {
node = {
type: "BinaryExpression",
operator: "+",
left: node,
right: parsePrimary()
};
}
return node;
}
ESLint требует точной привязки узлов к исходному коду.
Необходимо хранить:
range: [start, end]loc.start.line, loc.start.columnloc.end.line, loc.end.columnПример добавления метаданных:
function attachLocation(node, start, end, source) {
node.range = [start, end];
node.loc = computeLoc(source, start, end);
return node;
}
Функция computeLoc должна учитывать переносы строк и
табуляции.
Комментарии не входят в AST как узлы, но ESLint использует их для:
eslint-disable,
eslint-enable)Парсер должен собирать комментарии отдельно:
comments.push({
type: "Line",
value: "eslint-disable",
range: [start, end],
loc: { start, end }
});
И возвращать их через ast.comments или через
дополнительные сервисы.
visitorKeys описывает, какие поля узла содержат дочерние
узлы.
Пример:
const visitorKeys = {
BinaryExpression: ["left", "right"],
VariableDeclaration: ["declarations"],
VariableDeclarator: ["id", "init"]
};
Без этого ESLint не сможет корректно обходить AST.
Для полноценной работы некоторых правил требуется информация о scope.
Scope включает:
let/constПарсер может делегировать построение scope-дерева сторонним
библиотекам (например, eslint-scope), но обязан вернуть
результат.
import { analyze } from "eslint-scope";
const scopeManager = analyze(ast, {
ecmaVersion: 2022,
sourceType: "module"
});
services — механизм расширения возможностей AST.
Чаще всего используется для TypeScript и JSX парсеров.
Пример:
services: {
getType(node) {
return typeChecker.getTypeAtLocation(node);
}
}
В @typescript-eslint/parser services предоставляют
доступ к TypeScript Program.
Ключевой критерий качества парсера — возможность работы стандартных правил ESLint:
no-undefno-unused-varseqeqeqconsistent-returnОшибки в AST приводят к:
Современные парсеры должны поддерживать:
Каждое расширение требует дополнительных типов узлов:
OptionalMemberExpression
TSInterfaceDeclaration
JSXElement
ClassProperty
Подключение кастомного парсера осуществляется через конфигурацию:
export default {
parser: "./my-parser.js",
rules: {
"no-unused-vars": "error"
}
};
Парсер становится первым этапом конвейера обработки кода:
Source Code
↓
Parser
↓
AST
↓
Rules (AST traversal)
↓
Lint Results
Парсер может сообщать об ошибках синтаксиса через исключения или специальные структуры:
throw {
message: "Unexpected token",
index: 10,
lineNumber: 2,
column: 5
};
ESLint преобразует их в диагностические сообщения.
Критические факторы:
Оптимизации:
Хороший парсер должен учитывать взаимодействие с:
Особенно важна совместимость с форматами AST, используемыми другими инструментами, чтобы избежать расхождений в анализе одного и того же кода.
Типичная организация модуля:
parser/
├── tokenizer.js
├── parser.js
├── ast-builder.js
├── location.js
├── comments.js
├── visitor-keys.js
└── index.js
index.js экспортирует parseForESLint.
AST должен быть:
Любое расхождение делает невозможным корректный линтинг и нарушает работу правил.