Содержание
- Мёртвый код: определение и виды
- Инструменты по языкам
- Knip (JavaScript, TypeScript)
- ts-prune (TypeScript)
- Vulture (Python)
- deadcode и staticcheck (Go)
- IntelliJ и другие IDE (Java, Kotlin)
- Где ошибаются все, включая нас
- Сколько находят на больших проектах
- Как чистить, не сломав прод
- Где здесь repowise
- Как мы считали
- Частые вопросы
Короткий ответ такой: для JS/TS берите Knip, для Python берите Vulture с белым списком. Для Go есть официальный deadcode, который строит граф вызовов от main, а в дополнение к нему staticcheck (проверка U1000). Для Java и Kotlin есть инспекции IntelliJ. Для многоязычного репозитория, где нужны история и владельцы, подойдёт repowise. Любой результат стоит перепроверить вторым способом.
| Инструмент | Язык | Недостижимые файлы | Неиспользуемые экспорты | Неиспользуемые зависимости | Как ищет |
|---|---|---|---|---|---|
| Knip | JS/TS | да | да | да | граф модулей от точек входа |
| ts-prune | TS | нет | да | нет | архивирован в 2023, автор советует Knip |
| Vulture | Python | частично | да | нет | статический анализ + белый список |
deadcode | Go | нет | функции | нет | граф вызовов от main (RTA) |
staticcheck U1000 | Go | нет | да | нет | статический анализ пакета |
| Инспекции IntelliJ | Java, Kotlin и др. | ограниченно | да | нет | анализ проекта в IDE |
| repowise | 16 языков | да | да | пакеты-зомби | граф импортов + история git |
Scroll the table sideways to see every column.
Мёртвый код: определение и виды
Мёртвым называют код, который можно удалить без изменения поведения программы. На реальном проекте инструменты дают по этому определению разные ответы, потому что по-разному понимают точки входа, рефлексию, конфиги и сгенерированный код.
Его удобно делить на три вида.
Недостижимые файлы. Это файлы, которые не загружаются ни по одному пути выполнения: утилита, оставшаяся после рефакторинга, скрипт, который когда-то запускали руками. Чтобы найти такой файл, нужен граф всего проекта, по самому файлу ничего не понять.
Неиспользуемые экспорты. Это публичные функции, классы, типы и константы, которые никто не импортирует. С них проще всего начинать.
Пакеты-зомби. Это зависимости, которые остались в манифесте, хотя код их уже не использует. В JS это лишние строки в package.json, а в монорепозитории это целые каталоги, на которые никто не ссылается.
Инструменты по языкам
Knip (JavaScript, TypeScript)
На мой взгляд, это лучший открытый вариант для JS/TS. Находит неиспользуемые файлы, экспорты и зависимости, строит граф модулей от точек входа и знает про конфиги популярных фреймворков. Умеет удалять найденное автоматически.
Платить за это приходится настройкой точек входа. Если указать не все, Knip назовёт мёртвым живой код. Если указать слишком много, мёртвый код спрячется.
ts-prune (TypeScript)
Ищет только неиспользуемые экспорты. Репозиторий архивирован 17 декабря 2023 года, автор прямо рекомендует переходить на Knip. В старом проекте, где он уже настроен, пусть работает, но в новый я бы его не ставил.
Vulture (Python)
Классика для Python: ищет неиспользуемые функции, классы и переменные. Плохо понимает декораторы, динамические импорты и магию фреймворков, поэтому без белого списка почти не обходится. Vulture годится, чтобы отобрать кандидатов на удаление, а удалять по его отчёту вслепую нельзя.
deadcode и staticcheck (Go)
Для Go в декабре 2023 года в golang.org/x/tools появился официальный инструмент deadcode:
go install golang.org/x/tools/cmd/deadcode@latest
deadcode -test ./...
Он строит граф вызовов от функции main методом Rapid Type Analysis и сообщает о функциях, до которых нельзя дойти. Важное свойство: если deadcode назвал функцию мёртвой, её нельзя вызвать даже динамически. Исключение составляют директивы //go:linkname, которые инструмент не учитывает. Флаг -test добавляет в анализ тестовые бинарники, иначе функции, нужные только тестам, тоже попадут в мёртвые.
staticcheck с проверкой U1000 ищет неиспользуемые идентификаторы внутри пакета. Вдвоём они покрывают большую часть задач в Go-проекте.
IntelliJ и другие IDE (Java, Kotlin)
Инспекции IntelliJ находят неиспользуемые объявления прямо во время правки. Для локальной работы этого хватает, а для всего репозитория не хватает: IDE не знает про SPI, плагины, сервисы, которые поднимаются по имени из конфига, и точки входа в других модулях.
Где ошибаются все, включая нас
Полезнее всего в этой теме понять, почему инструменты ошибаются. Покажу на реальных находках нашего собственного детектора в repowise, которые я проверил по исходникам.
Gitea: функция, которую вызывают по значению. Детектор пометил CountRunnersWithoutBelongingOwner в models/actions/runner.go как неиспользуемый экспорт с уверенностью 1,0. На деле её использует gitea doctor: в services/doctor/dbconsistency.go на строке 153 она передаётся как значение поля, Counter: actions_model.CountRunnersWithoutBelongingOwner. Вызова в привычном виде нет, есть только ссылка на функцию, и наш граф её не увидел. При этом соседняя находка в том же отчёте, ContainsCategory в models/auth/access_token_scope.go, нигде больше не встречается, и похоже, что это действительно мёртвый код.
OpenViking: обработчики FastAPI. В OpenViking детектор с уверенностью 1,0 назвал неиспользуемыми create_collection и search_by_vector из openviking/storage/vectordb/service/api_fastapi.py. Это обработчики HTTP-маршрутов, зарегистрированные декораторами @collection_router.post(...) и @search_router.post("/vector"). Их никто не импортирует, потому что их вызывает фреймворк. То же самое с моделями запросов Pydantic вроде SearchRequest: FastAPI использует их как тело запроса. Это наша ошибка, причём с максимальной уверенностью. Вывод из неё общий: граф импортов не видит регистрацию через декораторы.
alibaba/open-code-review: использование внутри пакета Go. Тип CommentWorkerPool в internal/agent/agent.go помечен как неиспользуемый с уверенностью 1,0, хотя он используется в том же пакете: это поле структуры на строке 96 и конструктор NewCommentWorkerPool на строке 195. В Go код внутри одного пакета не импортирует сам себя, поэтому граф импортов тут бесполезен. deadcode строит граф вызовов, поэтому здесь он ошибся бы реже. А bin/ocr.js, помеченный как недостижимый файл, на самом деле точка входа CLI из поля bin в npm. Его детектор оценил в 0,4 и удалять не предлагал.
Grafana: плагины, которые грузятся по соглашению. Среди находок с уверенностью 1,0 в Grafana есть файлы module.js тестовых плагинов в e2e-playwright/test-plugins/. Grafana загружает плагины по соглашению об именах, без импорта, поэтому в графе такой файл выглядит сиротой.
Keycloak: пакеты-зомби, которые на самом деле сборка. В Keycloak пакетами-зомби помечены distribution и themes. Из кода их никто не импортирует, но по названию и роли это сборка дистрибутива и темы интерфейса, которые подключаются на этапе сборки и выполнения. Для Java-проекта с SPI и ресурсами это типичная картина.
В deepseek-harness виден обратный случай: из 270 находок у всех уверенность 0,4, ни одна не помечена как безопасная к удалению, и строк к удалению ноль. Например, tsdown.config.ts никто не импортирует, но это конфиг, который читает сборщик. Детектор так и пишет в обосновании: пакет использует динамические импорты, есть риск загрузки во время выполнения. Удалять файл он не предлагает.
Ложные срабатывания почти всегда растут из точек входа, которых нет в графе: обработчиков и моделей фреймворка, плагинов, SPI, рефлексии, ссылок на функцию как значение, конфигов сборки. Оценка уверенности полезна, но проверку она не заменяет.
Сколько находят на больших проектах
| Репозиторий | Находок | Неиспольз. экспорты | Недостижимые файлы | Зомби | Строк к удалению |
|---|---|---|---|---|---|
| grafana/grafana | 1 730 | н/д | н/д | н/д | 16 115 |
| keycloak/keycloak | 343 | 312 | 29 | 2 | 12 260 |
| volcengine/openviking | 434 | 289 | 143 | 2 | 6 485 |
| go-gitea/gitea | 292 | 188 | 102 | 2 | 2 793 |
| deepseek-ai/deepseek-harness | 270 | 101 | 169 | 0 | 0 |
Scroll the table sideways to see every column.
В «Строк к удалению» входят только находки, которые детектор считает безопасными. После разбора выше понятно, что и это число нужно проверять.
Как чистить, не сломав прод
- Начните с одного вида. Например, только с неиспользуемых экспортов. Так ревью остаётся обозримым.
- Сортируйте по уверенности, но не верьте ей вслепую. Начинайте с приватных хелперов и модулей без зависимых.
- Проверьте точки входа. К ним относятся маршруты и обработчики фреймворка, плагины и SPI (
META-INF/services), CLI-команды, рефлексия, импорты только из тестов. - Подтвердите вторым способом. Подойдут поиск по коду,
deadcodeотmainили прогон тестов. - Посмотрите историю. Файл, который правили на прошлой неделе, скорее всего живой, даже если граф говорит иное. Старый файл без владельца стоит рассмотреть первым.
- Удаляйте маленькими PR. Их легко откатить, и если CI упадёт, сразу понятно почему.
- Оставьте детектор в CI с базовым уровнем (baseline), чтобы новый мёртвый код не появлялся.
Где здесь repowise
Пятый пункт обычно делается руками: git log по каждому файлу. repowise ставит рядом с каждой находкой дату последнего коммита, число правок за 90 дней, владельца и объяснение, почему файл попал в список. Всё это берётся из того же индекса, что и граф. Как видно выше, он тоже ошибается на декораторах и ссылках на функции, поэтому рядом с каждой находкой мы показываем уверенность и обоснование. Открытый код, AGPL-3.0:
pip install repowise && repowise init
Как мы считали
Находки взяты из публичного API repowise (/snapshots/<id>/dead-code) для последнего индекса каждого репозитория: Grafana и Keycloak от 11 августа 2026, Gitea от 17 июля, OpenViking от 14 июня, open-code-review от 7 июня, deepseek-harness от 28 сентября. Ложные и верные срабатывания я проверил по исходникам: для OpenViking и open-code-review по исходным файлам на тех же коммитах, для Gitea поиском по коду в текущей ветке main. Проверено 6 октября 2026 года. Для Grafana разбивка по видам не приведена, потому что API отдаёт только первые 500 находок из 1 730.
Посмотреть мёртвый код своего репозитория: вставьте ссылку на GitHub на repowise.dev.
Частые вопросы
Как найти мёртвый код в Go-проекте?
Официальный инструмент называется `deadcode`, он входит в `golang.org/x/tools` и запускается так: `deadcode -test ./...`. Он строит граф вызовов от `main`, и если называет функцию мёртвой, это почти наверняка так. Исключение составляют функции, на которые ссылается `//go:linkname`. Для неиспользуемых идентификаторов внутри пакета добавьте `staticcheck` (U1000).
Чем искать неиспользуемый код в JavaScript и TypeScript?
Берите Knip: он находит неиспользуемые файлы, экспорты и зависимости. ts-prune архивирован в 2023 году, и его автор сам советует Knip.
Как найти мёртвый код в Python?
Подойдёт Vulture, но для декораторов и магии фреймворков заведите белый список, иначе он будет помечать обработчики Django и FastAPI как неиспользуемые.
Почему инструмент говорит, что код не используется, хотя он используется?
Потому что точка входа не видна в графе импортов: код вызывает фреймворк по декоратору, плагин грузится по имени файла, класс поднимается через SPI или рефлексию, функция передаётся как значение. Это главный источник ложных срабатываний у всех детекторов.
Можно ли удалять мёртвый код автоматически?
Технически да: Knip умеет удалять сам. Я бы оставлял удаление за ревью и проверкой вторым способом, особенно в проектах с фреймворками и плагинами.
Как найти неиспользуемые зависимости в монорепозитории?
Для JS/TS есть Knip. Для любого языка repowise показывает пакеты, на которые никто не ссылается из других пакетов. Как показывает пример Keycloak, часть таких пакетов на деле оказывается сборкой или ресурсами, поэтому их нужно проверять руками.