Проблемы совместимости парсера и плагинов

ESLint опирается на промежуточное представление исходного кода — AST (Abstract Syntax Tree), которое формируется парсером. Основной парсер по умолчанию — Espree, реализующий разбор стандарта ECMAScript с поддержкой современных версий языка.

Плагины и правила ESLint не работают напрямую с текстом кода. Они зависят от структуры AST, которую возвращает парсер. Любое расхождение между ожиданиями плагина и фактическим AST приводит к ошибкам, пропуску правил или некорректному анализу.

Ключевая проблема заключается в том, что ESLint сам по себе не фиксирует единый формат AST для всех сценариев. Он лишь ожидает совместимость с ESTree-совместимым деревом, но конкретная реализация может отличаться.


Различия AST между парсерами

Разные парсеры формируют различные AST даже при одинаковом исходном коде:

  • @babel/eslint-parser добавляет расширения синтаксиса, которых нет в стандартном ESTree
  • @typescript-eslint/parser вводит собственные узлы для типов и аннотаций
  • Acorn может использоваться как базовый движок с плагинами синтаксиса

Каждое расширение меняет форму дерева: добавляются новые типы узлов, изменяется структура выражений, появляются дополнительные поля (typeAnnotation, range, loc, parent).

Плагины ESLint, написанные под один AST, часто ломаются при переключении на другой парсер.


Конфликт ожиданий правил и структуры AST

ESLint-правила используют селекторы AST:

  • CallExpression
  • MemberExpression
  • ImportDeclaration

Если парсер изменяет структуру узлов, селекторы перестают срабатывать.

Пример проблемы:

  • правило ожидает ArrowFunctionExpression
  • парсер возвращает расширенный узел с дополнительной обёрткой (TSAsExpression, TSTypeAssertion)
  • правило не находит совпадение и игнорирует участок кода

В TypeScript-окружении это проявляется особенно часто: один и тот же синтаксис может иметь разные AST-формы в зависимости от конфигурации parserOptions.


Несовместимость версий ESLint и плагинов

Плагины ESLint часто завязаны на внутренние API:

  • context.getSourceCode()
  • context.report()
  • AST traversal utilities

При изменении версии ESLint меняется структура внутренних объектов.

Типичный сценарий конфликта:

  • плагин написан под ESLint 7
  • проект обновлён до ESLint 9
  • изменяется механизм rule context и работа с sourceCode
  • правило начинает выдавать ошибки или перестаёт выполняться

Особенно критично это при использовании кастомных парсеров, так как они часто тестируются только на ограниченном наборе версий ESLint.


Проблемы с JSX и нестандартным синтаксисом

Поддержка JSX требует отдельного парсера или расширения синтаксиса. При использовании @babel/eslint-parser или аналогов появляются дополнительные узлы:

  • JSXElement
  • JSXFragment
  • JSXOpeningElement

Плагины, не учитывающие JSX AST, могут:

  • игнорировать JSX-структуры
  • падать при обходе дерева
  • некорректно анализировать вложенные выражения

Аналогичная ситуация возникает с современными Stage 3–4 предложениями ECMAScript, такими как pipeline operator или decorators, где AST может радикально отличаться в зависимости от парсера.


TypeScript и семантические расхождения AST

@typescript-eslint/parser создаёт AST, который отличается не только синтаксически, но и семантически.

Основные отличия:

  • добавление типов (TSTypeReference, TSInterfaceDeclaration)
  • расширение выражений (TSAsExpression, NonNullExpression)
  • разделение узлов на type/value контексты

Проблема возникает, когда плагин ожидает JavaScript AST, но получает TypeScript-расширение. В результате:

  • селекторы не совпадают
  • проверки типов узлов дают undefined
  • логика обхода дерева ломается

Некоторые плагины вынуждены реализовывать отдельные ветки логики для TypeScript AST, что усложняет поддержку.


ParserOptions как источник скрытых конфликтов

Конфигурация parserOptions напрямую влияет на структуру AST:

  • ecmaVersion
  • sourceType: "module" | "script"
  • ecmaFeatures.jsx

Различные комбинации параметров могут менять:

  • наличие ImportDeclaration
  • структуру модулей
  • обработку top-level await
  • поддержку JSX внутри TSX

Неправильная настройка приводит к ситуации, когда:

  • правило написано под module-код
  • проект использует script-режим
  • AST не содержит ожидаемых узлов

Это создаёт ложное ощущение, что правило “сломалось”, хотя фактически изменился входной формат.


Несовместимость плагинов между собой через общий AST

Плагины ESLint не изолированы. Они работают в одном дереве AST.

Если один плагин:

  • модифицирует scope analysis
  • добавляет кастомные свойства в узлы
  • рассчитывает дополнительные метаданные

другой плагин может:

  • перезаписать эти свойства
  • ожидать другой формат данных
  • некорректно интерпретировать структуру

Особенно это заметно при комбинации:

  • TypeScript плагинов
  • React/JSX плагинов
  • импорт-анализа (import plugin)

Конфликты из-за кастомных парсеров

Использование нестандартного парсера — частая причина несовместимости.

Типичные сценарии:

  • подключение Babel для поддержки экспериментального синтаксиса
  • переход на TypeScript парсер
  • использование обёрток над Acorn

Каждый парсер может:

  • по-разному определять границы узлов (range, loc)
  • иначе трактовать операторы
  • добавлять/удалять промежуточные узлы

В результате плагины, ориентированные на стандартный ESTree, начинают работать непредсказуемо.


Проблемы строгой типизации AST-узлов в правилах

Многие правила ESLint написаны с предположением о фиксированной структуре узлов:

if (node.type === "CallExpression") {
  node.callee.object.name
}

При изменении парсера:

  • callee может быть не MemberExpression, а обёрткой
  • object может отсутствовать
  • структура может содержать дополнительные слои

Это приводит к runtime-ошибкам:

  • Cannot read properties of undefined
  • падение линтинга
  • частичная проверка файлов

Версионный разрыв между ESLint и экосистемой

Эволюция ESLint привела к значительным изменениям внутреннего API, особенно в новых архитектурных версиях. Плагины, ориентированные на старую модель:

  • используют устаревшие хуки обхода AST
  • зависят от deprecated API
  • не учитывают изменения конфигурации

В экосистеме возникает эффект “фрагментации”:

  • часть плагинов поддерживает только старые версии
  • часть требует новые парсеры
  • часть работает только в строго фиксированной комбинации

Практические последствия несовместимости

Типичные проявления проблем:

  • правила ESLint не срабатывают на части кода
  • линтер падает с ошибками AST traversal
  • увеличивается время анализа из-за fallback-логики
  • появляются ложноположительные или ложоотрицательные срабатывания
  • разные среды разработки показывают разные результаты линтинга

На уровне проекта это приводит к нестабильности качества кода: одна и та же база может проходить проверку локально и падать в CI.


Скрытые зависимости между парсером и плагином

Некоторые плагины фактически зависят от конкретного парсера, хотя формально это не указано:

  • опираются на нестандартные поля AST
  • используют internal properties
  • рассчитывают на специфическое поведение scope analyzer

Такие зависимости сложно обнаружить, пока не произойдёт смена парсера или обновление версии ESLint.


Стратегии минимизации конфликтов

Проблемы совместимости возникают системно, поэтому их снижение обычно достигается архитектурными ограничениями:

  • фиксация одного парсера на весь проект
  • ограничение числа одновременно используемых AST-расширений
  • согласование версий ESLint и плагинов
  • проверка peerDependencies при обновлениях
  • избегание смешивания TypeScript и Babel парсеров без необходимости

Главный источник нестабильности — неоднородность AST в одном анализируемом контексте, где разные части системы ожидают разные структуры данных.