В прошлой статье я показывал CodeClone как инструмент для поиска структурного дублирования в Python-коде. Не только буквальной копипасты, но и повторяющихся функций, похожих блоков и одинаковых контрольных конструкций.
За это время проект заметно изменился. Если раньше это был в первую очередь детектор клонов, то 2.0.0b1 — уже скорее инструмент для контроля структурного здоровья проекта в CI.
То есть фокус сместился. Вопрос теперь не только в том, есть ли в коде дублирование, а в том:
становится ли проект хуже;
что именно деградирует;
что из этого уже принято как технический долг, а что пришло в свежем изменении;
можно ли использовать инструмент как вменяемый quality gate, а не просто как красивый HTML-отчёт.
Сразу оговорюсь: это beta-релиз. Уже рабочий, уже пригодный для реальных прогонов, но именно beta — с расчётом на обратную связь и шлифовку по живым проектам.
И да, я прекрасно понимаю, что рынок не пустой
Это тоже важно проговорить отдельно, чтобы не было ощущения “ещё один автор пришёл рассказать, что до него никто ничего не делал”.
Инструменты для оценки качества кода, дублирования и CI-гейтов, конечно, уже есть.
Если говорить про известные и понятные ориентиры, то это, например:
SonarQube/SonarCloud— большой и зрелый мир quality-платформ со своими quality gates, дублированием, метриками и governance-подходом;PMD CPD— классический copy/paste detector, который для многих вообще был первой точкой входа в тему clone detection;jscpd— известный multi-language инструмент для поиска дублирования, особенно если нужен быстрый и практичный clone scan в CI;Vulture— хорошо известный Python-инструмент для dead code;Radon/Xenon— привычные инструменты для complexity-контроля в Python;и более новые проекты вроде
pyscn, которые тоже идут в сторону structural/code-health анализа.
У pyscn, например, довольно интересная позиция: это уже не “только complexity” и не “только дублирование”, а попытка собрать несколько важных quality-сигналов в одном инструменте. И это, честно говоря, хороший знак для всей темы: значит запрос на такие инструменты реально есть.
То есть CodeClone не появляется в вакууме и не пытается делать вид, что до него был только мрак и один grep.
Скорее у него своя точка сборки:
акцент на структурное дублирование;
baseline-aware модель как центральный рабочий принцип;
жёсткая ставка на детерминизм и CI-контракты.
Я бы не называл это “убийцей условного SonarQube” или чем-то в таком духе. Это другой масштаб и другая цель. Скорее это попытка сделать компактный, воспроизводимый и достаточно строгий инструмент именно под ежедневную работу с Python-репозиториями.
Если совсем коротко, то ключевое отличие CodeClone для меня сейчас не в том, что он “тоже умеет искать клоны, dead code и complexity”. Таких сигналов у разных инструментов хватает.
Ключевая идея в другом: CodeClone baseline-aware. Он изначально строится вокруг разделения:
того, что в проекте уже принято как технический долг;
и того, что появилось как новая регрессия.
Именно вокруг этой модели у него построены baseline contract, trust semantics, структура отчёта и CI-гейтинг.
Почему пришлось идти в 2.0
После первых прогонов на реальных репозиториях быстро выяснилось, что одного ответа “вот тут у вас клоны” недостаточно.
В настоящем CI почти никогда не нужен абстрактный сигнал “в проекте есть проблемы”. Он и так есть почти у любого неигрушечного репозитория. Нужен другой тип ответа:
что появилось нового;
что уже было и осознанно принято;
что реально влияет на качество кода;
можно ли этим пользоваться как стабильным контрактом в пайплайне.
Параллельно стало видно и ещё одно ограничение первой версии: structural duplication — это только часть картины. В живом Python-проекте рядом почти всегда стоят:
высокая сложность отдельных функций;
низкая cohesion;
высокая связность;
dead code;
циклы зависимостей;
повторяющиеся ветки и семейства однотипных конструкций, которые ещё не обязательно являются “классическими клонами”, но уже сигнализируют о проблеме.
Отсюда и выросла идея 2.0: перестать смотреть на CodeClone как на точечный детектор и сделать из него baseline-aware инструмент для контроля здоровья кода.
Что изменилось по смыслу
Вторая версия строится вокруг довольно простой, но важной модели:
ядро анализирует код и собирает факты;
эти факты складываются в канонический JSON-отчёт;
HTML, Markdown, Text и SARIF — это уже проекции одного и того же документа;
текущее состояние сравнивается с baseline;
baseline отделяет исторический долг от новых регрессий.
На практике это означает, что инструмент начинает отвечать не только на вопрос “что нашлось”, но и на вопрос “что с этим делать в реальном процессе разработки”.
Главное, что появилось в 2.0.0b1
Health score вместо набора разрозненных сигналов
В 2.0.0b1 появился health score.
Он считается не “по вдохновению”, а из конкретных измерений:
clones
complexity
coupling
cohesion
dead code
dependencies
coverage
Для меня здесь было важно не просто вывести красивое число. Если у инструмента появляется интегральная оценка, она очень быстро превращается либо в игрушку, либо в источник недоверия.
Поэтому я старался, чтобы health можно было развернуть обратно в смысл:
из чего он складывается;
что именно тянет оценку вниз;
насколько проблема сосредоточена в production-коде, а насколько в тестах и fixture’ах;
какие зоны сейчас выглядят самыми “тяжёлыми”.
Именно отсюда вырос новый Overview в HTML-отчёте.
Baseline наконец стал нормальным контрактом
Одна из главных целей второй версии — сделать baseline не “каким-то JSON-файлом рядом”, а нормальной, жёстко определённой поверхностью.
Теперь baseline — это не просто список старых клонов. Это доверенный снимок состояния, который:
валидируется по схеме и trust semantics;
умеет отделять known от new;
участвует не только в clone-diff, но и в metrics gating;
ведёт себя предсказуемо и в обычном режиме, и в
--ci.
Для реального CI это, на мой взгляд, куда важнее самого факта наличия отчёта.
Потому что в живом проекте редко работает сценарий “нашли 400 проблем, всё блокируем”. Обычно он просто не проходит организационно.
Зато работает другая модель:
текущее состояние фиксируется как baseline;
старый долг не мешает жить;
новый долг перестаёт проходить в
main.
Именно это CodeClone 2.0 теперь и пытается делать системно.
Появился dead code, но без магии
Во второй версии появился полноценный dead-code слой.
И тут почти сразу проявилась типичная проблема Python: далеко не всё, что выглядит “неиспользуемым”, действительно мёртвое. В экосистеме полно мест, где символ вызывается не напрямую:
framework hooks;
reflection;
plugin loading;
декларативные callback’и;
runtime wiring.
Соблазн здесь очевиден: добавить побольше эвристик и тихо не ругаться на “подозрительные” места.
Мне такой путь не нравится. Он быстро превращает анализ в непрозрачную магию.
Поэтому в 2.0.0b1 для таких случаев сделан явный механизм inline suppressions:
# codeclone: ignore[dead-code] def handle_exception(exc: Exception) -> None: ...
или:
class Middleware: # codeclone: ignore[dead-code] ...
Здесь важна именно философия:
suppression — это локальная policy-механика;
она не подменяет результат анализа;
она не влияет на другие finding families;
suppressed dead-code элементы не участвуют в health impact, но остаются видимыми в отчётах.
Для Python это, как мне кажется, намного честнее, чем расползание неявных “умных исключений”.
Кроме клонов появился отдельный слой structural findings
Раньше CodeClone крутился в первую очередь вокруг function/block/segment clones.
Во второй версии появился ещё и слой structural findings. Пока там, например, есть:
duplicated_branchesclone_guard_exit_divergenceclone_cohort_drift
Мне это кажется важным шагом. В реальном коде архитектурные проблемы часто начинают проявляться раньше, чем их уже удобно назвать “клонами” в узком смысле.
И здесь для меня было принципиально не испортить архитектуру:
сначала core строит факты, потом они попадают в канонический отчёт, и только потом UI их показывает.
То есть HTML не должен “додумывать” вывод сам.
Отчёты стали не побочным эффектом, а нормальной частью продукта
Сейчас CodeClone умеет выдавать:
HTML
JSON
Text
Markdown
SARIF
Причём для меня было важно, чтобы это не были пять независимых веток кода. Всё строится из одного канонического отчётного документа. Это резко упрощает две вещи:
детерминизм;
согласованность между форматами.
SARIF как раз появился именно в 2.0.0b1. И я постарался сразу делать его не “для галочки”, а так, чтобы от него был практический толк в IDE и code-scanning сценариях: с нормальной привязкой путей, артефактами, %SRCROOT%, baselineState и вменяемыми метаданными правил.
HTML-отчёт сильно переделан
Самое заметное внешнее изменение — это, конечно, новый HTML.
Во второй версии отчёт стал гораздо ближе к тому, что реально хочется открыть после CI-прогона:
появился нормальный
Overview;сверху теперь есть health gauge и сетка KPI-карточек;
есть блок
Executive Summary;есть блок
Source Breakdown;есть профиль здоровья в виде радарного графика;
есть более понятная логика, что смотреть в первую очередь.
Мне хотелось уйти от ощущения “это debug dump, просто стилизованный CSS”.
При этом я старался не скатиться в другую крайность — когда интерфейс красивый, но бесполезный. Если какая-то карточка показывает проблему, за ней должен стоять реальный факт из report payload, а не UI-фантазия.
Порогов стало меньше, анализ — шире
Ещё одно важное изменение, которое не так бросается в глаза, но сильно влияет на практику: в 2.0.0b1 понижены пороги по умолчанию.
Например:
--min-locдля function-level анализа опущен с15до10;block gate стал мягче;
segment gate тоже стал мягче.
Это осознанный шаг. Инструмент теперь меньше “отбрасывает на входе” и видит больше реальных проблемных мест.
Да, у этого есть цена:
растёт объём анализа;
становится важнее производительность;
нужно аккуратнее следить за шумом.
Но с практической точки зрения это правильный компромисс. Иначе инструмент получается слишком благостным: вроде работает, а половину интересных мест просто не видит.
Что поменялось под капотом
Если не смотреть на UI, то внутри у 2.0.0b1 было много работы вокруг трёх тем.
1. Контракты
Во второй версии заметно жёстче оформлены:
baseline contract;
cache contract;
report contract;
семантика exit code’ов CLI.
Для CI-инструмента это, как по мне, важнее половины “фич”.
Инструменты такого класса чаще ломаются не на сложной AST-логике, а в тот момент, когда кто-то незаметно меняет семантику baseline, cache или report payload, и пайплайн начинает вести себя непредсказуемо.
2. Детерминизм
Я стараюсь держать CodeClone как инструмент, которому можно доверять в автоматизации.
Отсюда много довольно скучных, но необходимых решений:
стабильная сортировка;
канонизация report payload;
жёсткие invariants в тестах;
аккуратное отношение к ordering и trust fields;
запрет на UI-only эвристики, которые влияют на gating.
Снаружи это не очень заметно. Но именно эта часть определяет, можно ли инструментом пользоваться не один раз “ради интереса”, а каждый день.
3. Производительность
С ростом объёма анализа вопрос производительности сам собой стал гораздо важнее.
Во второй версии было несколько итераций оптимизации:
fast-path’ы для suppressions;
ленивые imports в report path;
оптимизация warm-run поведения;
улучшения cache path;
benchmark workflow в GitHub Actions.
При этом я старался держать жёсткое правило: пока fingerprint_version не меняется, нельзя “ускорять” проект за счёт изменения fingerprint semantics, clone identity или baseline compatibility.
CLI тоже стал взрослее
Базовый сценарий остался очень простым:
codeclone . codeclone . --html codeclone . --ci
Но вокруг него появилось много вещей, которых раньше не хватало в реальной работе:
codeclone . --html --open-html-report codeclone . --json --md --sarif --text codeclone . --html --json --timestamped-report-paths
Плюс теперь есть нормальный конфиг в pyproject.toml:
[tool.codeclone] min_loc = 10 min_stmt = 6 block_min_loc = 20 block_min_stmt = 8 segment_min_loc = 20 segment_min_stmt = 10 baseline = "codeclone.baseline.json"
То есть инструмент стало проще не просто “один раз запустить”, а реально встроить в проект.
Ещё одна важная вещь — документация
Во второй версии у проекта появилась нормальная документационная поверхность:
документация на MkDocs;
отдельные deep-dive-страницы;
live sample report.
Когда проект начинает обрастать контрактами и режимами, README уже перестаёт быть достаточным интерфейсом. Для меня это тоже был важный шаг взросления проекта.
Что я сам пока считаю beta-зоной
Я не хочу делать вид, что 2.0.0b1 — это уже “всё, можно больше не трогать”.
Что я сам пока отношу к beta-зоне:
дальнейшая шлифовка HTML UX;
больше прогонов на крупных и разношёрстных Python-репозиториях;
дополнительные сценарии для SARIF/IDE;
дальнейшая работа с производительностью повторных прогонов;
доводка edge cases вокруг suppressions и runtime false positives.
То есть это уже не сырая альфа, но и не тот релиз, про который хочется сказать: “теперь всё окончательно устаканилось”.
Как попробовать
Если хочется поставить именно beta-версию, то это можно сделать так:
pip install --pre codeclone
или:
uv tool install --pre codeclone==2.0.0b1
Дальше минимальный сценарий:
codeclone . codeclone . --html codeclone . --ci
Ссылки:
документация: https://orenlab.github.io/codeclone/
live sample report: https://orenlab.github.io/codeclone/examples/report/
Вместо вывода
Если коротко, CodeClone 2.0.0b1 для меня — это переход от инструмента, который просто ищет структурные клоны, к инструменту, который уже можно использовать как baseline-aware quality gate в реальном процессе.
Я не очень люблю тексты в духе “теперь всё стало в десять раз лучше”. Гораздо честнее сказать так:
проект стал заметно взрослее;
модель стала практичнее;
отчёты и UX стали полезнее;
но это всё ещё beta, и именно поэтому сейчас особенно важен фидбек с живых репозиториев.
Если тема зайдёт, следующим постом могу отдельно разобрать одну из вещей подробнее:
как устроен baseline и trust model;
как считается health score;
почему dead code в Python без явных suppressions быстро превращается в магию;
как история с чужим codeclone-cli (VS-плагин, промо-сайт и сопутствующая инфраструктура) за 10 минут переписки по почте закончилась полным удалением проекта вместо обычного rename — и мне потом ещё прислали детальный лог изменений. Хотя я просил только переименование, потому что мой проект был старше. Это, пожалуй, один из самых наглядных примеров того, как AI-агент с широкими доступами может принять слишком радикальное решение.
