Статический анализ — мощный инструмент, с помощью которого можно решать разные задачи. Особенно:

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

Экосистема JavaScript содержит все необходимые библиотеки, которые помогут в этом. Вам не нужно писать парсер ECMAScript самостоятельно — вы можете взять парсер по своему выбору. Таким образом, для статического анализа кода вам просто нужно описать правила над абстрактным синтаксическим деревом (AST), например, если мы видим Identifier, то увеличиваем переменную identifierUsed.

В этой статье я покажу, как мы автоматизировали одну плоскость рефакторинга — сигнатурное изменение функции из внутренней библиотеки.

В Motorway у нас есть парк микросервисов. Многие из них имеют уровень общей инфраструктуры, который мы инкапсулировали в общие библиотеки. Что, если мы захотим обновить сигнатуру одной из функций из такой общей библиотеки? Тогда у нас есть несколько вариантов:

  1. Полуавтоматический поиск и замена. Это было бы хорошо, если бы вы могли указать чистый шаблон такой операции, например. г. заменить foo(param) на foo({ bar: param }). Но не каждый случай подходит для этого.
  2. Ручной поиск и замена. Будет работать для небольшого количества вызовов функций, но что, если у нас есть более 1000 ссылок?
  3. Динамический анализ. Просто добавьте функцию входа в систему с именем вызывающего абонента и параметрами. Через какое-то время мы сможем понять, как используется эта функция и какие проекты следует настроить на новую подпись. Опять же, будет работать для небольшого количества вызовов функций, совпадающих критериев для изменения (обратите внимание на то, что нам придется проверять только совпавшие вызовы, а не все вызовы в случае ручного поиска). Другая проблема заключается в том, что некоторые функции можно назвать довольно редкими, и мы не хотим ждать месяц или год (какие-нибудь годовые отчеты?), чтобы собрать достаточно данных.
  4. И вот это предпочтительный (ИМХО) вариант — автоматизированный анализ исходного кода. Мы указываем необходимый шаблон, который может быть очень гибким, потому что он написан на программном языке, а не на регулярных выражениях в лучшем случае для полуавтоматического подхода.

Чтобы быть еще более конкретным, задача состояла бы в том, чтобы найти вызовы с более чем 1 аргументом любого метода, импортированного из библиотеки mw-lib-foo.

Первым шагом будет получение всех проектов для выполнения анализа кода. Мы не используем монорепозиторий, а придерживаемся подхода «проект на репозиторий». Вот почему самый простой способ получить весь код для анализа — использовать GitHub/GitLab/… API.

Структура скрипта довольно проста — возьмите список репозиториев для организации, отфильтруйте их по некоторым критериям и запустите несколько асинхронных воркеров (не вебворкеров, даже если это резко повысит производительность) параллельно. Каждый рабочий загрузит package.json, чтобы определить, следует ли анализировать проект (импортирует необходимую библиотеку), а затем загрузит все исходное дерево и запустит анализатор ECMAScript.

С точки зрения кода:

Рабочий также очень прямолинеен:

Теперь мы можем начать проверять файлы JavaScript один за другим, чтобы собрать необходимую информацию. В качестве парсера ECMAScript я выбрал @babel/parser, он достаточно мощный и поддерживает современные редакции ECMAScript (и все предложения, которые мы используем).

API парсера очень простое — вы вызываете parse(), а затем получаете AST, который точно соответствует спецификации ESTree с небольшими нюансами (https://babeljs.io/docs/en/babel-parser#output).

Исходя из этого, высокоуровневый код для создания синтаксического дерева будет выглядеть так:

Мы хотим быть уверены, что все вызовы распознаются правильно и мы не пропустили ни одного использования метода из библиотеки mw-lib-foo. Поэтому findAllUsageForModuleMethods() возвращает все упоминания переменной, связанной с целевым модулем. Мы можем либо расширить наш процессор AST, либо проверить его использование вручную.

После загрузки и разбора исходного кода следующим шагом будет обработка AST. Это обычная древовидная структура, поэтому выполнение обхода не является большой проблемой, но для упрощения жизни мы можем использовать @babel/traverse, который предоставляет дополнительный контекст для каждого узла и позволяет вам устанавливать перехватчики для определенных вхождений токена.

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

Предположим, что мы используем CJS-модули, и весь импорт выполняется через вызовы require('mw-lib-foo'). Чтобы найти этот импорт, нам нужно проанализировать CallExpression с именем вызываемого абонента в виде строки require.

В результате анализа этой части мы хотим получить список связанных переменных с этим модулем. Это может быть либо const lib = require('mw-lib-foo'), либо прямое разложение const { methodFoo, methodBar } = require('mw-lib-foo'). Чтобы отслеживать использование библиотечных методов, мы должны поддерживать обе формы импорта.

Хорошо, теперь у нас есть список переменных, которые можно использовать для вызова метода из библиотеки, пора отслеживать эти вызовы:

Теперь задача почти решена, осталось только убедиться, что мы ничего не упустили:

Статический анализ исходного кода — мощный и гибкий инструмент, который можно активно использовать для надежного рефакторинга. Даже если у вас хорошее тестовое покрытие, это не гарантирует, что вы не пропустите какой-нибудь очень редкий звонок.

С другой стороны, с @babel/parser писать код для анализа AST довольно просто и понятно.

Надеюсь, эта статья дала вам несколько указаний и советов по добавлению статического анализа в ваши проекты!