Baseline¶
Baseline хранит принятый технический долг, чтобы существующий проект мог начать использовать Qualimetrix без требования немедленно исправить все текущие нарушения. Версия 13 — это потолок по сообщаемой величине, а не список игнорируемых хешей: существующая группа остаётся принятой только пока она не растёт и не ухудшается.
Создание и использование baseline¶
Снимите текущий измеряемый набор нарушений в новый файл:
Затем проверяйте проект с ним:
Закоммитьте файл вместе с проектом, чтобы локальная разработка и CI использовали одну и ту же принятую границу.
Baseline может сделать warning ошибкой
Нарушение, которое сейчас срабатывает, но превышает принятую границу, повышается до Error. При стандартном --fail-on=error прогон завершится ошибкой, даже если настроенная серьёзность правила была Warning. Некорректная или неприменимая запись ничего не повышает: она сообщается как inert, а нарушение сохраняет обычную серьёзность.
Что измеряется¶
Lifecycle-команды baseline измеряют нарушения после подавления @qmx-ignore в исходниках и конфигурации, а также после настроенных исключений путей и неймспейсов. check использует тот же набор; его --suppress-path и --suppress-namespace могут безопасно сузить его ещё сильнее, но способны оставить запись inert. Lifecycle-команды не принимают эти CLI-исключения, чтобы capture и обслуживание не получили молча разную поверхность опций. --no-suppression-annotations влияет только на отчёт: он возвращает аннотированные нарушения после измерения baseline и никогда не расширяет набор. --report=git:... также сужает только представление.
Запись baseline идентифицирует канонический типизированный subject, канал, опциональное семантическое occurrence и опциональное ребро зависимости. Subject различает точные декларации, логические классы и агрегаты файла, namespace или проекта. Для канала с величиной файл хранит только сообщаемые значения группы — её число выводится из длины этого списка, а не хранится отдельным полем; для occurrence-канала хранится число. Текущая группа принимается, когда на каждом уровне серьёзности в ней не больше нарушений, чем в сохранённой группе. Так исправление работает без догадки, какой именно член группы исчез.
Baseline не заставляет несрабатывающее правило сработать. Исчезнувшее нарушение становится stale, но этим ещё не доказано, что оно исправлено.
Каналы конфигурационных ошибок никогда не попадают в baseline ни на каком пути: пять диагностик layer-policy (architecture.coverage-gap, architecture.unreachable-layer, architecture.pending-layer-matched, architecture.potential-shadow, architecture.empty-template) и три диагностики inline-директив (annotation.unresolved-directive, annotation.unsupported-threshold, annotation.invalid-threshold) вместо этого безусловно завершают прогон — см. Подавление в исходниках ниже.
Lifecycle-команды¶
Все baseline-команды, запускающие анализ, принимают одинаковые конфигурационные опции, нужные для воспроизведения измеряемого набора:
Они также принимают --config=CONFIG. Они не принимают --suppress-path и --suppress-namespace, потому что эти безопасные сужения check иначе сделали бы lifecycle-операции асимметричными. Они также не принимают --no-suppression-annotations: эта опция влияет только на отчёт и не может расширить измеряемый набор.
Generate¶
bin/qmx baseline:generate baseline.json src/
bin/qmx baseline:generate baseline.json src/ --mode=suppress --force
baseline:generate <baseline> [<paths>...] захватывает все текущие измеряемые нарушения. Стандартный --mode=ratchet записывает потолок; --mode=suppress принимает каждую захваченную идентичность независимо от последующего числа или величины. --force перезаписывает существующий baseline-файл и отбрасывает его принятые значения.
Замена старого baseline¶
Загружается только версия 13. Ни хеш версии 5, ни логический ключ символа версии 10 не позволяют вывести требуемые теперь точный subject декларации, семантическое occurrence и ребро зависимости; файл версии 11 не даёт ни укороченный occurrence-ключ, ни выводимый count; а ключ декларации версии 12 хранит байтовое смещение, по которому нельзя восстановить, какое объявление имелось в виду, — конвертера из предыдущей версии нет. Запусти свежий анализ, осознанно сопоставь или раздели каждую ранее принятую группу, проверь результат и запиши новый файл v13. baseline:generate --force может заменить байты только после этой проверки; это не автоматический конвертер, и он не выводит старую идентичность. Удалённая команда миграции не имеет alias или compatibility shim.
Ужесточение после исправлений¶
baseline:update <baseline> [<paths>...] сдвигает запись только к более строгой границе. Он не добавляет идентичности и не меняет отсутствующую идентичность. Команда отказывается работать, если проанализированная область не покрывает область, записанную в файле; --force снимает эту проверку области.
Проверка и явное удаление stale-записей¶
bin/qmx baseline:cleanup baseline.json src/
bin/qmx baseline:cleanup baseline.json src/ --remove=<selector>
Без --remove команда baseline:cleanup <baseline> [<paths>...] только выводит кандидатов и никогда не меняет файл. Повторяй --remove=<selector> ровно для проверенных записей. Массового удаления нет: отсутствие может быть вызвано сменой конфигурации, а не только исправлением. --force имеет то же значение проверки области, что и у baseline:update.
Перенос baseline на переименованные каналы¶
bin/qmx baseline:rename-channels baseline.json channels.tsv
bin/qmx baseline:rename-channels baseline.json channels.tsv --format=json
baseline:rename-channels <baseline> <map> переписывает поле channel тех
записей, которые названы в объявленной карте, и больше ничего. Команда не
запускает анализ: ключи субъектов, occurrence, count, magnitudes,
mode, edge, scope и generated переносятся нетронутыми, код проекта не
читается. Используй её, когда обновление переименовало канал, на котором у тебя
принят долг, вместо перегенерации — перегенерация молча примет всё, что дерево
накопило с тех пор.
Карта — табличный файл с шапкой old, new, reason, по строке на
переименование; пустые строки и комментарии # пропускаются:
По самому файлу команда отказывает ровно в тех случаях, в которых отказала бы
его загрузка, и ни в каких больше; у карты отказы свои. Значит, отказ с
побайтово неизменным файлом бывает, если:
файл не версии 13; конверт не читается как документ baseline, в том числе когда
generated не является датой ISO 8601, а scope — списком путей; две строки
переименовывают одно имя; две строки дают одно имя; обе стороны строки
совпадают; цель одной строки переименовывается другой; либо этот перенос дал
бы двум записям одного субъекта одну идентичность — дубликат, который уже был в
файле, переносится, а не отвергается, даже когда он стоит на переименовываемом
канале. Объявленное переименование, которое ничего не задело в этом файле,
сообщается, а не отвергается. Коды возврата: 0 — перенос выполнен (в том
числе «ничего не совпало»), 3 — отказ: по содержимому, по недоступности
baseline или карты как файла, либо по некорректному значению --format — все
причины дают один и тот же код. Отказ сообщается в выбранном формате: при
--format=json это тот же конверт {error, exit_code}, что и у любого
другого машиночитаемого отказа в инструменте, а не отдельный объект с одним
ключом error.
Два следствия, о которых стоит знать до запуска:
- Новое имя не проверяется по каналам, объявленным этой сборкой. Перенос
выходит релизом раньше переименований, ради которых существует, поэтому до
релиза, объявляющего новое имя,
checkсообщает о перенесённой записи как о неприменимой. Это промежуточное состояние задумано именно таким. - Селекторы записей меняются. Селектор — дайджест идентичности, в которую
входит имя канала, поэтому сохранённый
baseline:cleanup --remove=<selector>перестаёт адресовать перенесённую запись. Возьми селекторы заново из свежего выводаbaseline:cleanup.
Записи, которые эта сборка прочитать не может, переносятся, а не удаляются, и
попадают в счётчик отчёта. Счётчик намеренно уже, чем то, что check называет
inert: перенос не запускает анализ, поэтому считает только то, что видно по
самому документу. Из пяти видов, что он считает, у двух channel остаётся
читаемым, и карта переименовывает их наравне с остальными записями — это
запись с испорченным occurrence или edge, и запись, уже делившая
идентичность с другой. У остальных трёх карте нечего переименовывать: запись
не объект, запись без читаемого channel, и субъект, хранящий свои записи не
JSON-массивом — они переносятся без изменений. Попасть в счётчик не значит
быть удалённой ни в одном из случаев — непрочитанная запись из файла никогда
не пропадает, — но переименовываются на новое написание только первые два
вида.
Переименование канала может сдвинуть запись среди соседей. Перенесённый файл размещает все строки в том же каноническом порядке, в каком пишет сам продукт, поэтому следующая команда, переписывающая файл, строк уже не двигает. Байты каждой строки при этом остаются такими, как их написал файл, — именно это позволяет перенести файл чужой сборки без переформатирования; строка, поля которой расставлены вручную в необычном порядке, будет перерисована на месте — но не сдвинута — при следующей перезаписи файла.
Объяснение границы¶
bin/qmx baseline:explain 'callable:App\OrderService::calculate' src/ --baseline=baseline.json
bin/qmx baseline:explain 'callable:App\OrderService::calculate' src/ --channel=complexity.ccn
baseline:explain <symbol> [<paths>...] показывает принятую величину, текущее нарушение, порог из конфигурации и override @qmx-threshold. Используй --baseline=BASELINE, чтобы включить принятую величину, и --channel=CHANNEL, чтобы сузить ответ.
Символ, отсутствующий и в текущем анализе, и в baseline, считается неверным input, а не чистым результатом. Baseline-only символ остаётся объяснимым и помечается как отсутствующий в текущей области или результате.
Все lifecycle-команды требуют полного анализа. Ошибка parsing или processing
возвращает код 4 до интерпретации, классификации, создания или изменения baseline.
--force не снимает этот инвариант; существующий файл остаётся побайтово неизменным.
Stale, inert и resolved-записи¶
С --baseline команда check сообщает о stale- и inert-записях, а также о несовпадении области, но не завершает прогон ошибкой и не отключает остальные записи. Используй --show-resolved, чтобы посчитать записи, чья полная идентичность больше не встречается в измеряемом наборе. Уменьшившаяся, но ещё срабатывающая группа не считается resolved.
Запись о замыкании, о члене анонимного класса или об одном из двух объявлений с общим именем в одном файле ключуется рангом. Добавление, удаление или перемещение объявлений, по которым этот ранг считается, перенумеровывает запись, а освободившийся номер переиспользуется — поэтому запись не попадёт в stale, её принятие перейдёт на то объявление, которое теперь занимает этот номер. После такой правки бейзлайн нужно перегенерировать.
Подавление в исходниках¶
Используй inline-подавление для намеренного исключения, а не молча принимай его в baseline. Теги работают в PHPDoc, строчных и блочных комментариях; помещай их на отдельной строке перед целью.
| Тег | Область | Пример |
|---|---|---|
@qmx-ignore <channel> [-- reason] |
Символ | @qmx-ignore complexity.ccn:callable -- Legacy state machine |
@qmx-ignore * [-- reason] |
Все правила символа | @qmx-ignore * -- Generated mapper |
@qmx-ignore-next-line <channel> [-- reason] |
Следующая строка | @qmx-ignore-next-line code-smell.exit -- CLI entry point |
@qmx-ignore-file [channel] [-- reason] |
Весь файл | @qmx-ignore-file или @qmx-ignore-file -- Generated code |
Разделитель причины¶
Аргумент канала и причина — оба голые слова, поэтому -- — это способ их
различить. Он обязателен для @qmx-ignore-file, когда канал опущен, а
сразу за тегом идёт причина: @qmx-ignore-file Generated code, do not
analyse читает Generated как канал, который ничему не адресуется, и
падает с annotation.unresolved-directive:
Suppression "Generated" addresses no channel. No declared name is close to it. Prose belongs after "--".
Пишите так: @qmx-ignore-file -- Generated code, do not analyse. У
@qmx-ignore и @qmx-ignore-next-line канал не опционален — он всегда
первое слово, — поэтому -- перед причиной там необязателен; собственное
соглашение проекта — писать его всё равно, чтобы все три тега читались
одинаково.
Каналы, а не имена правил¶
@qmx-ignore, @qmx-ignore-next-line и @qmx-ignore-file адресуют канал — точный violationCode, под которым сообщается нарушение, — а не производящее правило. Селектор канала — это либо:
- точное имя канала (
complexity.wmc,code-smell.eval), опционально суженное через:<уровень>(complexity.ccn:callable), когда канал сообщает на нескольких уровнях, либо X.*строго для потомковX— самXв это не входит, поэтому для обоих смыслов нужны две директивы.
Голый префикс без звёздочки (@qmx-ignore complexity) — это ошибка, а не догадка о намерении; X.*, не совпавший ни с чем, тоже ошибка:
Теперь каждое правило сообщает ровно через один канал, но сам канал может сообщать на нескольких уровнях символьного дерева — например, на уровне класса и неймспейса для coupling, или на уровне метода и класса для complexity. Голое имя канала адресует все уровни сразу; ниже — правила, для которых это важно, потому что их два уровня расходятся достаточно часто, чтобы подавление только одного было обычным случаем:
| Канал | Уровни |
|---|---|
complexity.ccn |
callable, class |
complexity.cognitive |
callable, class |
complexity.npath |
callable, class |
coupling.cbo |
class, namespace |
coupling.instability |
class, namespace |
Подавляй все уровни голым именем канала или один уровень через :уровень, например @qmx-ignore complexity.ccn:callable.
Каналом может быть и вычисляемая метрика, например @qmx-ignore health.cohesion — это допустимо, пока computed_metrics: всё ещё определяет эту метрику. Удаление метрики превращает аннотацию в ошибку: висячая ссылка — та же ошибка, что и опечатка.
Пять каналов здесь никогда нельзя подавить
architecture.coverage-gap, architecture.unreachable-layer, architecture.pending-layer-matched, architecture.potential-shadow и architecture.empty-template — это конфигурационные ошибки, а не долг: @qmx-ignore не может их подавить, а baseline никогда не может их принять. Используй блок exclude: в конфигурации архитектуры или coverage-gap: ignore специально для диагностики покрытия. architecture.layer-violation это не касается — @qmx-ignore architecture.layer-violation и записи baseline для него по-прежнему работают.
Когда директива неверна¶
Директива, называющая что-то недопустимое, или директива, которая больше ничего не подавляет, не игнорируется молча — она сама становится нарушением встроенного правила annotation.directive, сообщаемым на файле, где находится директива. Полный справочник — Правила аннотаций. Три из четырёх её каналов — конфигурационные ошибки, которые безусловно завершают прогон независимо от --fail-on и никогда не могут быть приняты в baseline или подавлены:
| Канал | Когда срабатывает |
|---|---|
annotation.unresolved-directive |
директива называет несуществующий канал (опечатка, имя правила там, где нужен канал, X.*, не совпавший ни с чем, или удалённая вычисляемая метрика) |
annotation.unsupported-threshold |
@qmx-threshold нацелен на правило, не объявляющее поддержку override порога |
annotation.invalid-threshold |
сам payload @qmx-threshold некорректен |
annotation.unused-directive |
директива корректна, но ничего из адресованного ею не сработало за этот прогон — обычный долг по уборке |
Только annotation.unused-directive ведёт себя как обычное нарушение: по умолчанию это Info, серьёзность настраивается через опцию правила unused_directive_severity, его можно принять в baseline, убрать корневым suppress_paths или сузить git-скоупом как любой другой канал. suppress_namespaces до него не достаёт — субъект находки это файл, в котором написана аннотация, и неймспейса у него нет, — и собственные исключения правила тоже: они работают раньше, чем этот канал собирается. Это единственный канал, который нельзя погасить @qmx-ignore: директива, адресующая его, отвергается как annotation.unresolved-directive, — поэтому принять его на месте можно записью в baseline. @qmx-threshold никогда в него не засчитывается.
Inline-комментарий на той же строке не поддерживается.
Просмотр скрытого аннотациями¶
--show-suppressed показывает подавленные нарушения. --no-suppression-annotations возвращает нарушения, скрытые @qmx-ignore, только в отчёт: они остаются вне измеряемого baseline множества, сохраняют собственную серьёзность и не могут быть повышены записью baseline.
Переопределение порогов для символа с @qmx-threshold¶
Используй @qmx-threshold, когда символу нужна другая граница, но он должен оставаться проверяемым:
/**
* @qmx-threshold complexity.ccn warning=20 error=40 -- Legacy state machine
*/
final class ComplexStateMachine
{
}
@qmx-threshold <rule> <number> [-- <reason>]
@qmx-threshold <rule> warning=<number> [error=<number>] [-- <reason>]
@qmx-threshold адресует правило по точному имени — никогда канал и никогда уровень. Порог принадлежит единственному объекту опций правила, а не отдельному уровню, поэтому @qmx-threshold complexity.ccn:callable — ошибка, даже несмотря на то, что complexity.ccn сообщает на двух уровнях; используй имя правила complexity.ccn, а для настройки только одного уровня — --rule-opt:
@qmx-threshold "complexity.ccn:callable" addresses a rule at a level, and a threshold addresses the
producing rule by its own name: it does not distinguish levels (ADR 0024). Retune the whole rule
"complexity.ccn", or set the level alone with --rule-opt complexity.ccn:callable.<option>=<value>.
Это зеркальное отражение @qmx-ignore, который всегда адресует канал — асимметрия намеренная. @qmx-threshold на отключённом правиле допустим и работает молча: включённость — это фильтр исполнения, а не факт существования имени правила.
Числа неотрицательные. Явная форма принимает только warning и error; после -- или длинного тире пишется непустая причина. Override класса действует внутри класса, включая методы, override метода — только на этот метод, а при пересечении побеждает наименьшая исходная область. Предпочитай это @qmx-ignore, когда полезная граница всё ещё существует.