Использование scope-анализа

AST-анализ в ESLint опирается на систему областей видимости (scope analysis), которая позволяет сопоставлять идентификаторы переменных с их декларациями, отслеживать ссылки на них и определять контекст использования кода. Эта модель критична для правил, связанных с неиспользуемыми переменными, теневыми объявлениями, утечками в глобальную область, корректностью импортов и анализом замыканий.

JavaScript имеет лексическую область видимости, в которой каждая функция, блок или модуль формирует собственный scope. Внутри этой модели выделяются несколько базовых типов:

  • глобальная область
  • функциональная область
  • блочная область (ES6+)
  • область модуля

Каждая из них может содержать объявления переменных (var, let, const, функции, классы, импорты), а также ссылки на идентификаторы, определённые выше по цепочке scopes.

Важный аспект анализа заключается в том, что одно и то же имя может существовать в нескольких вложенных областях, и корректное разрешение зависит от ближайшего объявления по цепочке.

Библиотека eslint-scope как основа анализа

Внутри ESLint используется библиотека eslint-scope, которая строит дерево областей видимости на основе AST, полученного после парсинга кода.

Основные задачи этой системы:

  • построение иерархии scopes
  • связывание идентификаторов с декларациями
  • отслеживание ссылок (references)
  • определение «неразрешённых» или «глобальных» переменных
  • классификация переменных по типам использования

Результатом работы становится объект ScopeManager, содержащий дерево scope-узлов и набор переменных с их ссылками.

Структура Scope и её элементы

Каждый scope представляет собой объект, включающий:

  • type — тип области (function, block, global, module)
  • set — таблица объявленных переменных
  • references — список ссылок на идентификаторы
  • through — ссылки, не разрешённые в текущем scope
  • childScopes — вложенные области
  • upper — ссылка на родительскую область

Переменная в рамках scope представлена объектом Variable, который включает:

  • список defs (декларации)
  • список references
  • имя идентификатора

Ссылки (Reference) связываются либо с переменной, либо остаются «неразрешёнными», если декларация отсутствует в доступных scope.

Построение scope-дерева

Scope-анализ выполняется на этапе обхода AST. После парсинга ESLint запускает анализатор, который проходит по узлам дерева и создаёт scope-структуру.

Процесс включает несколько фаз:

  1. Создание глобального scope
  2. Обнаружение функций и создание function-scope
  3. Обработка блоков {} и формирование block-scope
  4. Регистрация деклараций переменных
  5. Связывание идентификаторов с переменными
  6. Разрешение ссылок через цепочку scopes

Каждый новый узел AST может инициировать создание нового scope, если он соответствует правилам (например, FunctionDeclaration, ArrowFunctionExpression, BlockStatement с let/const).

Использование scope-анализа в правилах ESLint

Scope-анализ становится доступным через контекст правила. В API правила существует метод:

context.getScope()

Он возвращает текущий scope, связанный с узлом AST, на котором выполняется проверка.

Это позволяет правилам выполнять точные проверки без повторной реализации анализа переменных.

Пример использования

create(context) {
  return {
    Identifier(node) {
      const scope = context.getScope();

      const variable = scope.set.get(node.name);

      if (!variable) {
        return;
      }

      if (variable.references.length === 0) {
        context.report({
          node,
          message: 'Переменная не используется'
        });
      }
    }
  };
}

В реальных правилах анализ более сложный, так как необходимо учитывать shadowing, параметры функций, импорты и глобальные объекты.

Разрешение идентификаторов

Механизм разрешения работает по цепочке scopes:

  1. Проверка текущего scope
  2. Переход к upper
  3. Повтор до глобального scope
  4. Если не найдено — ссылка помещается в through

Такой подход позволяет точно определять:

  • локальные переменные
  • переменные внешних замыканий
  • глобальные объекты (window, process)
  • импортированные символы

Through-ссылки и их значение

Список through содержит ссылки, которые не были разрешены ни в одном scope.

Это используется для:

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

Например:

function test() {
  console.log(notDefinedVar);
}

notDefinedVar попадёт в through, так как отсутствует декларация в доступных scopes.

Shadowing и конфликт имён

Shadowing возникает, когда внутренняя область объявляет переменную с тем же именем, что и внешняя.

const value = 1;

function fn() {
  const value = 2;
  return value;
}

Scope-анализ фиксирует:

  • две разные переменные value
  • отдельные scopes для каждой декларации
  • независимые reference-цепочки

Это позволяет правилам ESLint выявлять нежелательное перекрытие переменных, особенно в сложных кодовых базах.

Анализ замыканий

Scope-модель используется для анализа замыканий, где внутренние функции обращаются к внешним переменным.

function outer() {
  const data = 10;

  return function inner() {
    return data;
  };
}

Здесь inner scope содержит reference к переменной из внешнего scope. Такая связь фиксируется через цепочку upper.

Это позволяет определять:

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

Обработка модулей

В модульной системе ES Modules создаётся отдельный module-scope.

import fs from 'fs';

const x = 1;

Импорты регистрируются как переменные в module-scope и участвуют в общем анализе:

  • проверка использования импортов
  • выявление неиспользуемых зависимостей
  • контроль переопределения импортов

Интеграция с правилами анализа качества кода

Scope-анализ является основой для множества правил ESLint:

  • no-unused-vars
  • no-undef
  • no-shadow
  • consistent-return (частично)
  • import/no-unresolved (в экосистеме плагинов)

Каждое правило использует данные scope-дерева, чтобы избежать повторного обхода AST и повысить точность диагностики.

Производительность scope-анализа

Scope-анализ выполняется один раз на файл и кешируется внутри процесса линтинга. Оптимизации включают:

  • повторное использование AST
  • ленивое разрешение некоторых ссылок
  • минимизацию повторного обхода узлов
  • структурное хранение scope-дерева

При больших проектах эффективность анализа зависит от глубины вложенности scopes и количества идентификаторов.

Ограничения модели

Несмотря на точность, scope-анализ имеет ограничения:

  • динамическое создание переменных через eval
  • изменения глобального объекта во время выполнения
  • рефлексия через with
  • метапрограммирование

Такие конструкции либо частично игнорируются, либо обрабатываются как «неразрешимые» ссылки.

Роль scope-анализатора в архитектуре ESLint

Scope-анализ является промежуточным слоем между парсером и правилами. Он абстрагирует сложность AST и предоставляет единый интерфейс для работы с переменными.

Архитектурно он располагается следующим образом:

  • Parser (Espree / Babel / TypeScript parser)
  • AST
  • Scope Analyzer (eslint-scope)
  • Rule Engine
  • Rules

Такое разделение позволяет писать правила, не учитывающие детали парсинга и структуры AST на низком уровне, опираясь на семантическое представление кода через scopes.