
Новая реализация работает, а старая ещё лежит где-то в репозитории. Её провайдер зарегистрирован в контейнере, тесты продолжают вызывать старый сервис, поиск находит ссылки на класс. Всё выглядит так, будто код нужен. Только само приложение им больше не пользуется.
Чтобы это выяснить, приходится пройти от точки запуска до обработчика, от обработчика — до зависимости, от зависимости — до провайдера. Заодно проверить, кто вызывает общие функции и что останется после удаления старой ветки. Именно на восстановление этих связей я хочу тратить меньше времени.
Я делаю Deadtrace — статический анализатор, который моделирует такие связи и собирает кандидатов на проверку. В этой статье расскажу, почему для этой задачи мне стало недостаточно подхода Vulture, как устроен Deadtrace и где заканчивается уверенность анализатора.
Что именно не устраивает в Vulture
Vulture разбирает Python через AST, собирает определённые и использованные имена, затем сообщает о неиспользованных объектах. В документации прямо сказано, что этот анализ учитывает имена, игнорируя области видимости. Помимо этого, Vulture умеет находить недостижимые участки, например код после безусловного return.
Для быстрого поиска подозрительных мест это понятная модель. Но у неё есть два ограничения, существенных для приложений.
Первое — использование имени не обязательно означает, что до него можно добраться при работе приложения. Две старые функции могут вызывать друг друга и поддерживать видимость жизни. Или функция может использоваться только тестом давно отключённой возможности.
Второе — отсутствие привычного обращения не означает отсутствия потребителя. HTTP-обработчик вызывается через таблицу маршрутов. Провайдер зависимости запрашивается контейнером. Тестовая фикстура выбирается по правилам pytest. Для анализа таких случаев нужно знать, как устроен соответствующий механизм.
У Vulture есть whitelist-файлы, встроенные списки исключений, игнорирование имён и декораторов. Это помогает снизить шум, но восстановление связей остаётся на разработчике. Добавляя функцию в whitelist, мы сообщаем анализатору, что её нужно считать используемой. При этом вопрос остаётся: подключён ли вообще роутер, на котором она зарегистрирована?
С DI возникает похожая проблема. Провайдер может быть зарегистрирован в контейнере, но ни один потребитель больше не запрашивает его зависимость. Сам факт регистрации ещё не делает эту ветку нужной приложению. Чтобы разобраться, нужно проследить цепочку от работающего приложения до потребителя и от него — до провайдера.
Именно этого я жду от инструмента поиска мёртвого кода. DI — распространённый способ организации приложения, и мне хочется, чтобы анализатор понимал его без постоянно растущего списка ручных исключений. С этой задачи и начался Deadtrace.
Начать приходится с того, как запускается приложение
У вопроса «используется ли функция» должен быть контекст. Используется кем: веб-приложением, воркером, CLI-командой, тестами или внешними пользователями библиотеки?
В Deadtrace этот контекст называется миром исполнения. У мира есть профиль, сценарий и корни — точки, от которых начинается анализ достижимости. Например, объект FastAPI-приложения может быть корнем production-мира, а собранные pytest тесты — отдельного тестового мира.
Миры можно задать явно. Если конфигурации нет, анализатор ищет известные способы запуска: скрипты, CLI entry points, приложения и воркеры, команды в файлах развёртывания. Для библиотечного проекта учитывается публичный API. Это ограниченная модель известных способов запуска, поэтому собственный загрузчик или внешний контракт иногда потребуется описать отдельно.

Если старый сервис вызывается только из тестов, обычный поиск ссылок покажет, что обращения к нему есть. Разделение миров позволяет уточнить картину: тесты до сервиса доходят, а от известных анализатору production-точек запуска путь не найден.
Это повод выяснить, почему пути нет. Возможно, тесты продолжают проверять реализацию, которую приложение уже не использует. Или сервис нужен обработчику, но его роутер забыли подключить. А может, анализатору указали не все точки запуска. От причины зависит следующий шаг: удалить старый код, исправить подключение или уточнить настройки анализа.

Регистрация зависимости ещё не означает, что она нужна
В эталонном приложении из репозитория есть FastAPI и Dishka. Приложение подключает роутер. Обработчик GET /dish получает сервис, сервис зависит от репозитория. В контейнере также зарегистрирован провайдер старой реализации, которую никто не запрашивает.
Рабочую цепочку можно описать простой схемой:

Чтобы восстановить эту цепочку, нужно понять, как собрано приложение: какие роутеры подключены к приложению, какие зависимости запрашивают обработчики и какие провайдеры их создают. Поиск текстовых вызовов этих связей не покажет — часть работы за разработчика делает фреймворк.
В нашем примере Deadtrace доходит от обработчика до сервиса и репозитория, а у старой зависимости не находит потребителя. Она зарегистрирована, но никто её не запрашивает — это находка RCH003. При этом общая функция normalize остаётся достижимой: ей продолжает пользоваться рабочая ветка.

С роутерами та же история: обработчик можно зарегистрировать, а сам роутер забыть подключить к приложению. Декоратор на месте, функция написана, но запросы до неё не дойдут. Для роутера, подключение которого анализатор не смог установить, есть находка RCH002.
Такие связи ревьюер обычно восстанавливает вручную, переходя от файла к файлу. Deadtrace собирает их в модель, чтобы разбор начинался с конкретной находки и её причины.
Как устроен анализ
Сканер читает исходники и конфигурацию, разбирает Python стандартным ast, затем строит представление определений и связей. Python-фронтенд учитывает ссылки, импорты, наследование и ограниченный поток значений. Модели фреймворков добавляют известные регистрации, корни и границы неопределённости.
Далее анализатор рассчитывает достижимость отдельно для каждого мира и формирует группы для ревью. В актуальном коде есть четыре основных вида находок:
-
RCH001— связанный участок кода, до которого анализатор не нашёл пути от известных production-точек запуска. -
RCH002— обработчики роутера, подключение которого к приложению не удалось установить. -
RCH003— зарегистрированная Dishka-зависимость, которую приложение, по результатам анализа, не запрашивает. -
RCH004— код приложения, до которого анализатор дошёл только из тестов.
Группировка нужна, чтобы человек разбирал связанный участок, а не десяток отдельных строк про каждую функцию. Если у старой ветки есть внутренние связи, они становятся частью контекста одной находки.
Обычный scan не импортирует и не исполняет анализируемый проект, не устанавливает его зависимости и не обращается к сети. FastAPI и Dishka не нужны сканеру как runtime-зависимости ради загрузки приложения. Их поддерживаемое поведение описано в собственных правилах анализатора.
Это существенная граница: запуск чужого приложения ради выяснения его структуры может потребовать секреты, соединиться с базой или выполнить код верхнего уровня. При статическом сканировании исходники остаются данными.
Что Deadtrace уже понимает

Пример в статье построен на FastAPI и Dishka, но ими поддержка уже не ограничивается. Инструмент уже умеет учитывать:
-
FastAPI: приложения, подключение и вложенность роутеров, зависимости через
Depends, жизненный цикл приложения и фоновые задачи. -
Dishka: провайдеры, внедрение зависимостей, aliases, значения из контекста, связь контейнера с FastAPI и завершение генераторных провайдеров.
-
Django: настройки проекта, модели и
adminустановленных приложений, management-команды, template tags,AppConfig, точки входа ASGI и WSGI. Отдельно учитывается код, который нужен историческим миграциям. -
pytest: фикстуры, параметризация, хуки, плагины и настройки обнаружения тестов. Тестовые потребители анализируются отдельно от production.
-
CLI и запуск приложения: команды Click и Typer, точки входа из конфигурации пакета, способы запуска из Dockerfile, Compose, Procfile, Makefile и shell-скриптов.
Для Flask, Celery, Starlette, aiohttp, FastStream и ряда других фреймворков Deadtrace также распознаёт известные способы запуска и отдельные хуки. Глубина поддержки различается: полный список механизмов и ограничений я веду в репозитории.
Для меня результат такой поддержки — меньше переходов между файлами при разборе находки. Если анализатор уже знает, как Django загружает management-команду или Dishka выбирает провайдер, мне и ИИ-агенту не приходится каждый раз восстанавливать это заново.
Что делать, когда Python оказывается слишком динамичным
Не все связи в Python можно восстановить по исходникам. Например, приложение выбирает плагин по имени из переменной окружения. Анализатор видит механизм загрузки, но не знает, какой плагин будет выбран при запуске.
В такой ситуации Deadtrace сохраняет потенциально используемый код. Он различает два случая: «нашёл путь вызова» и «не смог исключить вызов». Во втором случае защита распространяется и на зависимости этого кода — их тоже нельзя уверенно считать неиспользуемыми.
Из-за этого часть мёртвого кода может не попасть в отчёт. Чем больше неопределённости, тем меньше кандидатов на удаление. Поэтому вместе с находками Deadtrace показывает, какие связи ему не удалось восстановить. Пустой отчёт может означать, что анализатору не хватило информации.
Даже успешное завершение сканирования не означает, что анализатор разобрал всё поведение приложения. Его выводы зависят от известных точек запуска и поддерживаемых механизмов. Если важную часть сборки приложения восстановить не удалось, Deadtrace ограничивает выдачу кандидатов, для которых нужна полная картина.
В репозитории есть и отдельный механизм blocked_findings. Некоторые формы находок слишком часто оказывались ложными при оценке публичных проектов: например, методы внешних базовых классов или элементы документированного публичного API. Они сохраняются в JSON с причиной блокировки, но по умолчанию не попадают в обычный список и не ломают finding-политику CI. Их можно посмотреть отдельно.
Это осознанный обмен полноты на качество выдачи. При оценке такого инструмента нужно считать обе стороны: сколько шума ушло и сколько полезных кандидатов перестало показываться.
Что показало сравнение

Чтобы понять, насколько Deadtrace полезнее Vulture, я сравнил их на исходниках 224 публичных проектов. Меня интересовал простой вопрос: сколько предупреждений указывают на действительно неиспользуемый код, а сколько заставляют разработчика разбирать рабочие участки приложения?
Находки проверял Sonnet: читала исходники и искала использование отмеченного кода. У Vulture подтвердились 66 из 417 проверенных находок — 15,8%. Среди ложных предупреждений постоянно встречались знакомые механизмы: обработчики с декораторами, фреймворк-хуки, pytest-фикстуры, публичный API библиотек.
У Deadtrace подтвердились 178 из 266 находок — 66,9%. Учёт связей фреймворка давал заметный результат: большая часть проверенной выдачи оказалась полезной. При этом инструменты считают находки по-разному: Deadtrace объединяет связанный код в группы, Vulture сообщает об отдельных определениях. Настройки и полная методика сравнения есть в репозитории.
Но эти проекты уже участвовали в доработке Deadtrace. Следующий вопрос был важнее: как он справится с приложениями, на которых я его ещё не «подтюнил»?
После дальнейших доработок я проверил анализатор на отдельной выборке из 88 проектов. Там подтвердились 124 из 169 находок — 73,4%. И в этой же проверке обнаружился показательный провал: все 14 проверенных предупреждений о неподключённых роутерах оказались ложными.
Приложения подключали роутеры в цикле по модулям. Для разработчика это обычная сборка приложения: пройти по списку модулей и подключить роутер каждого. Deadtrace эту связь терял и сообщал о маршрутах, которые на самом деле были доступны.
Я добавил обработку такого подключения. В подобных случаях анализатор теперь сохраняет роутеры как потенциально подключённые. Этот эпизод хорошо показывает, зачем проверять инструмент на чужих приложениях: каждый новый способ сборки может обнаружить связь, которую мои собственные примеры ещё не покрывают.
Отчёт не должен терять свою актуальность
После первого запуска появляется список находок. Часть можно исправить сразу, часть нужно обсудить или отложить. Но приложение продолжает меняться: к моменту обсуждения код уже может выглядеть иначе.
Поэтому я добавил сохранение отчёта в JSON. Находку можно разобрать позже, опираясь на тот же результат анализа. Deadtrace объяснит её по сохранённому отчёту, даже если исходники с тех пор изменились.
Дальше возникает вопрос: как подключить проверку к CI, если в приложении уже накопился старый код? Для этого я добавил baseline — список находок, которые уже разобрали и решили пока оставить. Они остаются в отчёте, а CI можно настроить так, чтобы он реагировал только на новые. Обновление baseline само по себе новые находки не принимает: это отдельное решение.
При повторных запусках важно понимать и причину изменений. Предупреждение могло исчезнуть после исправления кода, а могло — после обновления правил Deadtrace. Поэтому при сравнении отчётов я учитываю версию анализа и настройки. Если результаты нельзя корректно сравнить, инструмент сообщает об этом.
С настройками исключений похожая история. Например, я хочу убрать из выдачи сгенерированный код. Но этот код может вызывать функции приложения. Если полностью исключить его из анализа, Deadtrace перестанет видеть эти обращения.
Поэтому я разделил две настройки: исключить файлы из анализа и скрыть их находки из отчёта. Во втором случае связи сохраняются. Можно сделать отчёт короче, сохранив информацию, от которой зависят выводы анализатора.
Никуда без ИИ-агентов
Я рассчитываю, что отчёт Deadtrace часто будет разбирать ИИ-агент. Он уже меняет код, запускает проверки и исправляет ошибки. Поиск оставшихся старых веток хорошо вписывается в этот процесс. Но длинный список подозрительных имён заставит агента заново искать связи по всему приложению. Поэтому в отчёте важны причина находки, связанный код, миры исполнения и ограничения анализа. В таком случае почти любая модель справится с допроверкой.
В нашем примере контейнер умеет создавать старый сервис, но приложение его не запрашивает. Deadtrace показывает провайдер и связанный с ним код. Агент проверяет эти файлы и готовит удаление старой ветки.
При этом старый сервис вызывает normalize, которой пользуется и рабочий сервис. Deadtrace указывает эту связь в объяснении находки. Агент видит, что общую функцию нужно сохранить, даже если остальной код старого сервиса больше не нужен.
После правки агент запускает тесты и повторный анализ, сравнивает отчёты. Человек получает изменение вместе с результатами проверок и вопросами, которые остались открытыми.
Я хочу, чтобы агент тратил меньше контекста на восстановление уже известных анализатору связей. Тогда больше внимания останется на саму правку и её последствия.
Как попробовать
Опубликованная альфа 0.1.0a6 запускается на CPython 3.12–3.14. Минимальный запуск через uv:
uv tool install --python 3.12 "deadtrace==0.1.0a6"deadtrace scan .
Deadtrace уже можно использовать для анализа production-кода. Сканер читает исходники и конфигурацию, не запускает приложение, не устанавливает его зависимости и не изменяет анализируемый код. Ему не нужны доступы к базе и рабочие секреты.
Для первого знакомства я советую проверить, какие приложения обнаружил анализатор и какие ограничения он отметил:
deadtrace doctor .
Затем — запустить анализ и сохранить отчёт:
deadtrace scan . --format json --output deadtrace-report.json
Отчёт можно разобрать самостоятельно или передать ИИ-агенту. Когда существующие находки проверены, baseline позволяет зафиксировать исходное состояние и настроить CI на проверку только новых находок. GitHub Action и pre-commit hooks уже есть в репозитории.
У FastAPI, Dishka и pytest есть специальные модели анализа. Поддерживаемые механизмы и ограничения описаны в документации. Deadtrace пока в альфе: я продолжаю расширять покрытие, а интерфейсы и формат отчётов могут меняться.
Куда дальше?
Следующая цель — довести Deadtrace до бета-версии. Для этого я продолжу проверять его на реальных приложениях. История с роутерами показала, сколько шума может создать один пропущенный способ подключения. Каждый такой случай хочу закреплять отдельной проверкой, чтобы исправленная ошибка не возвращалась.
Ещё два направления — скорость анализа больших проектов и работа с ИИ-агентами. Хочу сократить время запуска, расход памяти и объём отчётов, чтобы агенту было проще пройти весь цикл: понять находку, подготовить правку и проверить результат.
Новые интеграции буду выбирать по реальным задачам. Поэтому примеры приложений, с которыми Deadtrace пока не справляется, сейчас особенно полезны: они помогут мне понять, за что браться дальше.
Зачем я вообще делаю Deadtrace?
Отправить Vulture на пенсию — для меня серьёзное намерение. Я хочу тратить меньше времени на поиск связей, которые анализатор способен восстановить сам: от точки запуска до обработчика, от запроса зависимости до провайдера, от теста до старого сервиса.
Когда серьезную часть кода пишет ИИ-агент, эта задача становится ещё важнее. Он может быстро написать новую реализацию, но после неё остаются старые провайдеры, обработчики и тесты. Deadtrace должен помогать ему разобраться с этим следом: показывать связанные участки, объяснять находки и позволять сравнивать результат до и после правки.
Человек определяет, какое поведение должно сохраниться и какие внешние потребители важны для приложения. Агент готовит изменение и собирает результаты проверок. Я развиваю Deadtrace, чтобы обоим было проще разобраться в коде и проверить последствия правки. Чем быстрее появляется код, тем важнее понимать, что из него осталось нужным приложению.
Попробуйте Deadtrace на своём приложении и расскажите, что получилось: какие находки оказались полезными, где анализатор ошибся и каких связей ему не хватило. Если нашли ошибку, создайте issue — небольшой пример кода и вывод Deadtrace помогут мне разобраться и улучшить анализ.
Если вам интересны Deadtrace и другие инструменты для разработки с ИИ, подписывайтесь на мой Telegram. Буду рад вашим идеям, вопросам и обратной связи.
ссылка на оригинал статьи https://habr.com/ru/articles/1094514/