Правила аннотаций¶
Раньше встроенные аннотации @qmx-ignore и @qmx-threshold могли ломаться двумя тихими способами: аннотация, которая ни к чему не адресовалась (опечатка, удалённая метрика, имя правила там, где ожидалось имя канала), просто ничего не делала, а аннотация, которая когда-то что-то значила, но перестала совпадать с чем-либо в текущем прогоне, тоже не подавала никакого сигнала. Правила аннотаций закрывают оба пробела — теперь каждая написанная директива проверяется, и директива, которая не может делать то, что заявляет, публикуется как находка, а не молча игнорируется.
Валидация директив¶
Идентификатор правила: annotation.directive
Что измеряет¶
Каждая аннотация @qmx-ignore, @qmx-ignore-next-line, @qmx-ignore-file и @qmx-threshold, написанная в анализируемом коде, проверяется относительно собственной конфигурации прогона: существует ли имя, к которому она адресуется, тот ли это вид имени для данной директивы, и сделала ли она что-нибудь в этом прогоне?
Под именем продюсера annotation.directive никогда ничего не публикуется — оно существует только для того, чтобы у четырёх каналов ниже был один общий владелец для отключения и настройки как семьи. У каждого канала своё имя правила и свой смысл.
Почему это важно¶
Аннотация — это утверждение о коде: «эта находка ожидаема и принята». Когда утверждение неверно — имя написано с опечаткой, правило переименовано, метрика, на которую оно ссылается, удалена из конфигурации, — прежнее поведение состояло в том, чтобы промолчать. Это хуже обычного ложноотрицательного результата: рецензент, видящий @qmx-ignore, считает, что подавление работает, тогда как на самом деле не подавляется ничего. Громкий отказ на сломанной директиве и рутинное напоминание об убранной директиве, которая тихо перестала что-либо значить, сохраняют доверие к слою аннотаций.
Четыре канала¶
| Канал | Значение | Вид | Severity |
|---|---|---|---|
annotation.unresolved-directive |
Директива называет то, к чему ей не разрешено адресоваться, — опечатка, имя правила там, где ожидался канал, wildcard X.*, ничему не соответствующий, или повисшая ссылка на вычисляемую метрику, убранную из конфигурации. |
Ошибка конфигурации | Error, не настраивается |
annotation.unsupported-threshold |
@qmx-threshold адресуется к правилу, которое не объявляет поддержку переопределения порогов. |
Ошибка конфигурации | Error, не настраивается |
annotation.invalid-threshold |
Сам payload @qmx-threshold некорректен — неверная форма или неразбираемое значение для опций этого правила. |
Ошибка конфигурации | Error, не настраивается |
annotation.unused-directive |
Директива корректна и адресуется к чему-то реальному, но ничего из адресованного не сработало в этом прогоне. | Обычный долг | Info по умолчанию, настраивается через unused_directive_severity |
Первые три — ошибки конфигурации: они сообщают об ошибке в том, что написано, а не о долге в анализируемом коде. Как и архитектурные диагностики конфигурации (см. Правила архитектуры), они валят прогон безусловно, как только срабатывают — fail_on для них не учитывается вообще, даже fail_on: none, — и ни одну из них нельзя принять в baseline или заглушить ещё одним @qmx-ignore. Опция severity на любой из них выглядела бы переключателем поведения, ничего при этом не меняя, — поэтому её ни у одной из трёх нет.
annotation.unused-directive устроена иначе: директива была корректной и когда-то что-то значила, просто ничего не подавила и не переопределила в этом конкретном прогоне. Это обычный долг по уборке, а не ошибка — у неё настраиваемая severity, её можно принять в baseline, убрать корневым suppress_paths и сузить git-скоупом как любую другую находку. Двух исключений, которые есть у других каналов, для неё нет — и не было ещё до запрета: корневой suppress_namespaces до неё не достаёт, потому что субъект находки — файл, в котором написана аннотация, и неймспейса, с которым можно сравнивать, у него нет; а собственные suppress_paths / suppress_namespaces правила её не видят, потому что этот канал прогон собирает уже после исполнения правил, когда поканальный леджер исключений закрыт. Выключение всего правила annotation.directive её убирает — вместе с тремя диагностиками-ошибками конфигурации выше.
Единственное, чего с ней сделать нельзя, — подавить директивой. @qmx-ignore, @qmx-ignore-next-line и @qmx-ignore-file отвергаются, если их цель достаёт до annotation.unused-directive: по точному имени, через annotation.* или с :file после любого из них. Отказ публикуется как annotation.unresolved-directive на строке самой директивы. Директива, спрятавшая этот канал, спрятала бы ответ на вопрос, ради которого канал существует. Голый @qmx-ignore-file без канала не отвергается — отвергать нечего, он ничего не называет, — но и канал он больше не гасит.
Пример¶
complexity не называет ни правила, ни канала — это голый префикс, а голый
префикс больше не является группой. @qmx-ignore всегда адресует канал
(violationCode) — точно либо в форме X.*. Это публикует
annotation.unresolved-directive с сообщением, называющим ближайшие
допустимые каналы:
@qmx-threshold всегда адресуется к правилу, никогда к каналу, суженному
до уровня — coupling.cbo:class называет правило coupling.cbo на его
уровне class, а порог не различает уровни. Это тоже публикует
annotation.unresolved-directive:
@qmx-threshold "coupling.cbo:class" 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 "coupling.cbo", or set the level alone with --rule-opt coupling.cbo:class.<option>=<value>.
/**
* @qmx-ignore complexity.ccn:callable reason="stable for now"
*/
public function calculateShipping(Order $order): float
{
// метод был позже упрощён ниже порога сложности
}
Аннотация корректна и когда-то подавляла реальную находку, но
calculateShipping() больше не срабатывает по complexity.ccn на уровне
callable. Это публикует annotation.unused-directive с severity Info —
напоминание удалить бесполезную аннотацию, а не ошибка конфигурации.
@qmx-ignore-file — единственная форма, у которой канал необязателен, поэтому
слово сразу после тега по-настоящему неоднозначно: это может быть канал, а
может быть первое слово причины. Оно читается как канал, Generated ничему
не адресуется, и это публикует annotation.unresolved-directive:
Suppression "Generated" addresses no channel. No declared name is close to it. Prose belongs after "--".
Напишите -- перед прозой, чтобы сказать «дальше идёт причина»:
-- обязателен только для этого неоднозначного случая. У @qmx-ignore и
@qmx-ignore-next-line аргумент канала обязателен и всегда идёт первым,
поэтому @qmx-ignore complexity.ccn:callable Legacy state machine
однозначен и без разделителя — хотя написать -- и там тоже полезно, чтобы
все три тега читались одинаково.
Как исправить¶
annotation.unresolved-directive— исправьте имя. Используйте точное имя канала для@qmx-ignore(илиX.*для всех потомковX), и точное имя правила для@qmx-threshold. Если сломалась аннотация на вычисляемую метрику, либо верните метрику вcomputed_metrics:, либо удалите повисшую аннотацию. Если сообщение указывает на первое слово вашей причины — вы написали@qmx-ignore-fileсразу с прозой без канала; добавьте--перед причиной (см. пример выше).annotation.unsupported-threshold— удалите@qmx-threshold; у целевого правила нет опций, которые может переопределить порог. Посмотрите разделOptions(«Настройки») на странице этого правила, чтобы узнать, что оно принимает.annotation.invalid-threshold— приведите payload к форме опций правила (см. разделConfiguration(«Конфигурация») этого правила для ожидаемых ключей и типов значений).annotation.unused-directive— удалите аннотацию. Она ничего не делает, и её присутствие вводит следующего читателя в заблуждение, будто находка всё ещё подавляется. Если удалить пока нельзя — примите находку в baseline или исключите путь; ещё одна@qmx-ignoreв список вариантов не входит и будет отвергнута.
Область учёта¶
Учёт annotation.unused-directive намеренно узок, чтобы никогда не порождать шум из-за конфигурационных решений, которые и так работают как задумано:
- Учитываются только директивы, адресующиеся к включённым правилам. Отключение целой семьи правил — как это делает встроенный пресет
legacy— не делает её аннотации «неиспользуемыми». - Учитываются только директивы внутри анализируемого набора файлов. Сужение анализа через
--report=git:stagedили подобное не помечает аннотации в файлах вне этого прогона. @qmx-thresholdникогда не участвует в учёте unused-directive — переопределение порога либо корректно и молчаливо, либо является ошибкой конфигурации под одним из первых трёх каналов.- Одна написанная директива даёт ровно одну находку. Аннотация
@qmx-ignoreна уровне класса, которая также привязывается к каждому методу внутри класса, не печатается по разу на метод; субъект находки всегда — файл, потому что именно там аннотация физически написана.
Валидация происходит после разрешения конфигурации
Множество каналов, используемое для валидации, строится из уже разрешённой конфигурации самого прогона, включая семью вычисляемых метрик (health.* и любые метрики computed.*, которые определяет проект). Поэтому @qmx-ignore health.cohesion резолвится точно так же, как статически объявленный канал. Отсюда два следствия: удаление вычисляемой метрики из конфигурации превращает каждую ссылавшуюся на неё аннотацию в ошибку annotation.unresolved-directive, так же как опечатку; а @qmx-threshold на отключённом правиле корректен и молчалив — включённость — это фильтр исполнения, а не факт о существовании имени правила.
Аудит того, что директива всё ещё делает¶
Каналы выше отвечают на вопрос, адресуема ли директива: называет ли она что-нибудь и погасило ли подавление хоть что-то. Ни один из них не отвечает, что делает @qmx-threshold, потому что ничто из публикуемого правилом не говорит, с какой границей оно решало.
Это отдельно и сразу для обеих форм отвечает bin/qmx directives: каждая пороговая директива снимается поодиночке, и правила исполняются заново поверх измерений того же прогона. В qmx check этого нет — одно исполнение правил на директиву обычному прогону платить незачем, — и запускать команду предполагается осознанно или отдельным шагом CI.
Настройки¶
| Опция | По умолчанию | Описание |
|---|---|---|
enabled |
true |
Включить или выключить валидацию директив целиком. |
unused_directive_severity |
info |
Severity для annotation.unused-directive. Допустимо: info, warning, error. |
У остальных трёх каналов — annotation.unresolved-directive, annotation.unsupported-threshold и annotation.invalid-threshold — нет опции severity: они валят прогон безусловно, так же как пять архитектурных диагностик конфигурации (см. Правила архитектуры).