Перейти к содержанию

Правила аннотаций

Раньше встроенные аннотации @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 без канала не отвергается — отвергать нечего, он ничего не называет, — но и канал он больше не гасит.

Пример

/**
 * @qmx-ignore complexity reason="legacy algorithm"
 */
final class PricingEngine
{
    // ...
}

complexity не называет ни правила, ни канала — это голый префикс, а голый префикс больше не является группой. @qmx-ignore всегда адресует канал (violationCode) — точно либо в форме X.*. Это публикует annotation.unresolved-directive с сообщением, называющим ближайшие допустимые каналы:

Suppression "complexity" addresses no channel. Addressable names closest to it: complexity.wmc.
/**
 * @qmx-threshold coupling.cbo:class warning=20
 */
final class OrderAggregate
{
    // ...
}

@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 code, do not analyse
 */

@qmx-ignore-file — единственная форма, у которой канал необязателен, поэтому слово сразу после тега по-настоящему неоднозначно: это может быть канал, а может быть первое слово причины. Оно читается как канал, 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 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: они валят прогон безусловно, так же как пять архитектурных диагностик конфигурации (см. Правила архитектуры).

Конфигурация

# qmx.yaml
rules:
  annotation.directive:
    enabled: true
    unused_directive_severity: warning   # поднять напоминания об уборке до warning
bin/qmx check src/ --rule-opt="annotation.directive:unused_directive_severity=warning"