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

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

Qualimetrix работает из коробки с разумными настройками по умолчанию. Файл конфигурации позволяет настроить пороговые значения, отключить правила и исключить пути, чтобы адаптировать инструмент под ваш проект.


Файл конфигурации

Создайте файл qmx.yaml в корне проекта. Qualimetrix автоматически ищет этот файл.

Также можно указать файл явно:

vendor/bin/qmx check src/ --config=my-config.yaml

Секции конфигурации

Пути для анализа (paths)

Директории для анализа:

paths:
  - src/

Примечание

Если вы передаёте пути через аргументы командной строки (например, vendor/bin/qmx check src/ lib/), они имеют приоритет над конфигурационным файлом.

Исключения (exclude)

Директории, которые полностью пропускаются. Файлы в этих директориях не анализируются вообще:

exclude:
  - vendor/
  - tests/Fixtures/

Включение сгенерированных файлов (include_generated)

По умолчанию файлы с аннотацией @generated в первых 2 КБ автоматически пропускаются при анализе. Чтобы включить их:

include_generated: true

Эквивалент в CLI: --include-generated

Подавление путей в отчёте (suppress_paths)

Паттерны путей для подавления нарушений. В отличие от exclude, эти файлы всё равно анализируются (их метрики собираются), но нарушения не выводятся в отчёт. Поддерживаются префиксы директорий и glob-паттерны:

suppress_paths:
  - src/Entity                # префикс: все файлы в src/Entity/
  - src/Metrics/*Visitor.php  # glob: только файлы визиторов

Также доступна как CLI-опция: --suppress-path (объединяется с YAML-конфигурацией).

Не действует на архитектурные находки уровня проекта

suppress_paths--suppress-path) никогда не подавляют нарушения architecture.layer-violation и architecture.circular-dependency — по той же причине, что и suppress_namespaces ниже: нарушение архитектурной границы — не метрика, и исключение пути, нацеленное на подавление шумных метрик, не должно незаметно становиться способом выключить контроль архитектуры.

Какие находки освобождены — это объявленное свойство канала, а не следствие того, как пишется имя правила: правило не получает иммунитет только потому, что называется architecture.что-то.

Что остаётся для подавления такой находки, зависит от канала. architecture.layer-violation — настоящий долг кода, поэтому к нему по-прежнему применимы и @qmx-ignore architecture.layer-violation, и запись в baseline. Пять диагностик рядом с ним — architecture.coverage-gap, architecture.unreachable-layer, architecture.pending-layer-matched, architecture.potential-shadow и architecture.empty-template — сообщают об ошибке в конфигурации, поэтому к ним неприменимо ни то, ни другое; см. «Правила > Архитектура». Для них остаются блок exclude: внутри самой конфигурации архитектурных слоёв и, отдельно для покрытия, coverage-gap: ignore.

Как и для suppress_namespaces, это исключение действует только для глобального механизма — исключение на уровне правила suppress_paths, описанное ниже, для архитектурных правил по-прежнему работает.

Подавление неймспейсов (suppress_namespaces)

Подавление нарушений для классов из определённых неймспейсов (сопоставление по префиксу). Как и suppress_paths, файлы всё равно анализируются и метрики собираются, но нарушения не выводятся. Применяется ко всем правилам глобально:

suppress_namespaces:
  - App\Tests
  - App\Generated

Это полезно, когда целые поддеревья неймспейсов не должны генерировать нарушения. Для исключений на уровне отдельного правила используйте suppress_namespaces внутри конфигурации правила (см. ниже).

Также доступна как CLI-опция: --suppress-namespace (объединяется с YAML-конфигурацией).

Не действует на архитектурные находки уровня проекта

suppress_namespaces--suppress-namespace) никогда не подавляют нарушения architecture.layer-violation и architecture.circular-dependency. Нарушение архитектурной границы — не метрика: если бы оно тоже подавлялось, исключение шумной метрики незаметно выключало бы контроль архитектуры. Какие находки освобождены — объявленное свойство канала, а не следствие написания имени правила.

К architecture.layer-violation по-прежнему применимы @qmx-ignore architecture.layer-violation и запись в baseline. К четырём диагностикам — architecture.coverage-gap, architecture.unreachable-layer, architecture.potential-shadow и architecture.empty-template — они неприменимы: те сообщают об ошибке конфигурации, а не о долге кода. Для них используйте блок exclude: внутри конфигурации архитектурных слоёв, а для диагностики покрытия — coverage-gap: ignore.

Это исключение действует только для глобального механизма. Исключение на уровне правила suppress_namespaces / suppress_paths, описанное ниже (rules: {architecture.layer-violation: {suppress_namespaces: [...]}}), для архитектурных правил по-прежнему работает — как и для любого другого правила. См. «Правила» ниже и раздел «Подавление нарушений» архитектурного правила — там объяснено, почему эта асимметрия осознанная: явное указание имени правила — однозначный, проверяемый выбор, а глобальная запись suppress_namespaces — нет.

Правила (rules)

Управление активными правилами и настройка пороговых значений.

Полное отключение правила:

rules:
  code-smell.boolean-argument:
    enabled: false

Переопределение пороговых значений:

Каждое правило определяет уровни серьёзности. Когда метрика превышает порог, фиксируется нарушение соответствующего уровня. Например, правило цикломатической сложности имеет пороги для методов:

rules:
  complexity.ccn:
    callable:
      warning: 15
      error: 25

Это означает: выдавать предупреждение (warning), когда цикломатическая сложность метода достигает 15, и ошибку (error), когда достигает 25.

Сокращённая запись threshold:

Если вам нужен простой порог "прошёл/не прошёл" (все нарушения — ошибки), используйте threshold вместо отдельных warning и error:

rules:
  complexity.ccn:
    callable:
      threshold: 15    # warning=15 и error=15 → все нарушения становятся ошибками

  size.method-count:
    threshold: 25      # то же самое, что warning: 25, error: 25

Это эквивалентно warning: 15 + error: 15. Полезно в CI, где нужен бинарный результат.

Ограничение

Нельзя смешивать threshold с явными ключами warning/error в пределах одного слоя конфигурации (одного пресета, одного qmx.yaml, одного вызова CLI) для одного и того же уровня правила — это конфигурационная ошибка.

Переопределение режима между слоями: более приоритетный слой (файл конфигурации поверх пресета, CLI поверх файла конфигурации) может свободно переключать режим для уровня правила — например, пресет strict задаёт warning/error, а --rule-opt=size.method-count:threshold=25 в командной строке переключает это правило на единый порог. threshold из CLI перекрывает warning/error из пресета, а не конфликтует с ними:

# qmx.yaml (или пресет)
rules:
  size.method-count:
    warning: 10
    error: 20
# CLI переключает правило в режим простого порога "прошёл/не прошёл" — без ошибки "cannot mix"
bin/qmx check src/ --rule-opt=size.method-count:threshold=25

То же самое работает и в обратную сторону (threshold из нижнего слоя перекрывается парой warning/error из более приоритетного), а также для составных правил — на том уровне вложенности, где заданы ключи (например, callable:/class: у complexity.ccn).

Голый threshold на собственном верхнем уровне составного правила ЗАМЕНЯЕТ блоки уровней, а не дополняет их. Написанный там, он выбирает сокращённую форму, и блоки callable: / class: / namespace:, стоящие рядом, уже не читаются — молча, без единого слова о них. Какие уровни накрывает само сокращение, у двух семейств различается:

  • complexity.ccn, complexity.cognitive и complexity.npath применяют его к уровню callable и выключают уровень class. То есть вот это не сообщит о классах ничего:

    rules:
      complexity.ccn:
        threshold: 5      # callable: warning=error=5
        class:
          max_warning: 2  # не будет прочитано — и уровень class выключен
    

    Чтобы настроить оба уровня, голый ключ писать не нужно: напишите threshold: внутри каждого блока.

    rules:
      complexity.ccn:
        callable:
          threshold: 5
        class:
          max_warning: 2
          max_error: 3
    
  • coupling.cbo и coupling.instability применяют его РАВНОМЕРНО к ОБОИМ уровням сразу, поскольку их дефолты для class/namespace совпадают:

    rules:
      coupling.cbo:
        threshold: 15   # class И namespace: warning=error=15
    

    Замена действует и здесь: блок class: или namespace:, написанный рядом с голым ключом, не читается.

Каждое измерение покрытия типами -- отдельное правило, и у каждого свой обычный threshold:

rules:
  design.type-coverage.param:
    threshold: 90
  design.type-coverage.return:
    threshold: 90
  design.type-coverage.property:
    threshold: 80

Вычисляемые метрики (computed metrics) также поддерживают threshold.

Написание ключа опции взаимозаменяемо, сам ключ — нет. max_warning, maxWarning и max-warning — один и тот же ключ, и все три применяются, на обеих глубинах. Ключ, которого у правила в этой позиции нет, не угадывается: прогон останавливается — см. «Неизвестные ключи опций правила» в разделе «Валидация конфигурации» ниже.

Пустой блок уровня равнозначен отсутствующему. callable: без тела оставляет уровень на значениях по умолчанию. Чтобы выключить уровень, пишите callable: {enabled: false}callable: false отвергается: у уровня нет собственного выключателя, а значение выглядит как выключатель правила.

Подавление неймспейсов для правила:

Любое правило может исключить конкретные неймспейсы по префиксу. Нарушения из совпадающих неймспейсов подавляются:

rules:
  complexity.ccn:
    suppress_namespaces:
      - App\Tests
      - App\Legacy
    callable:
      warning: 15
      error: 25

  coupling.cbo:
    suppress_namespaces:
      - App\Tests
    suppress_paths:
      - src/Infrastructure/DependencyInjection

Это полезно, когда определённые неймспейсы (например, тесты, сгенерированный код, legacy-модули) не должны вызывать нарушения для конкретного правила, но при этом всё равно анализируются для сбора метрик.

Подавление отдельных агрегатных каналов неймспейсов для правила:

Используйте suppress_namespace_channels, когда один канал нарушений неприменим к части дерева неймспейсов, но нарушения уровня классов и остальные каналы правила должны сохраниться:

rules:
  health.cohesion:
    suppress_namespace_channels:
      health.cohesion:
        - App\Metrics\Coupling
        - App\Generated\*

Опция представляет собой непустое отображение: селектор канала → непустой список префиксов или glob-паттернов неймспейсов. Ключ читается по той же грамматике, что и везде: точное имя канала либо X.* для строгих потомков X, каждое из них можно сузить до уровня, дописав :namespace; см. «Селекторы правил и каналов» ниже. Голый префикс вроде health — ошибка, а не группа: пишите health.*. Точный health.cohesion не затрагивает соседний канал health.coupling.

Имя канала пишется точно так, как канал называется, вместе с дефисами: ключи этой карты не нормализуются, поэтому size.class-count, code-smell.* и вычисляемая метрика computed.my-score записываются здесь так же, как и везде.

Ключ обязан адресовать канал, который действительно эмитит то правило, под которым он написан. Ключ, называющий чужой канал или вовсе несуществующий, завершает прогон с кодом 3 и сообщением, перечисляющим каналы этого правила, — раньше он принимался и не исключал ничего. Ключ с уровнем проверяется как одно целое: названный канал обязан и производиться этим правилом, и сообщать на этом уровне, поэтому coupling.*:namespace под coupling.class-rank отвергается, хотя соседний канал семейства coupling на уровне неймспейса сообщает.

namespace — единственный уровень, который такой ключ может назвать: опции достаются только агрегатные нарушения уровня неймспейса. coupling.cbo:class отвергается по имени — раньше он принимался и не мог сработать никогда:

rules:
  coupling.cbo:
    suppress_namespace_channels:
      # только агрегат по неймспейсу; нарушения уровня класса того же канала остаются
      coupling.cbo:namespace:
        - App\Legacy

Писать уровень необязательно: ключ без уровня и так достаёт только агрегаты по неймспейсам, поэтому coupling.cbo и coupling.cbo:namespace исключают одно и то же. Пара имеет смысл, когда ключ должен прямо сказать, о какой из двух половин двухуровневого канала речь.

Удаляются только агрегатные нарушения уровня Namespace

Опция фильтрует нарушения, чей субъект — неймспейс. У правила, которое сообщает поштучно (code-smell.*, security.*, architecture.layer-violation) или только на уровне класса (cohesion.lcom), удалять ей нечего: ключ с таким каналом принимается и ничего не делает. Диагностики политики слоёв — architecture.coverage-gap, architecture.unreachable-layer, architecture.potential-shadow, architecture.empty-template, architecture.pending-layer-matched — сообщают о проекте целиком и тоже вне её досягаемости; для них используйте блок exclude: внутри конфигурации архитектурных слоёв.

Удаляются только агрегатные нарушения уровня Namespace. Нарушения уровня класса health.cohesion в том же неймспейсе и соседние каналы остаются. Существующая опция suppress_namespaces не меняется и по-прежнему действует на всё правило, включая нарушения классов и неймспейсов.

Работает для любого правила, включая архитектурные

suppress_namespaces, suppress_namespace_channels и suppress_paths извлекаются и применяются на уровне фреймворка для любого имени правила, независимо от того, объявляет ли класс Options этого правила такое поле — это намеренно не opt-in для каждого правила по отдельности. Это касается и architecture.layer-violation, и architecture.circular-dependency — они исключены из глобальных suppress_namespaces и suppress_paths выше, но не из этой формы на уровне правила: явное указание имени правила делает подавление однозначным, проверяемым выбором, а не побочным эффектом project-wide исключения. См. раздел «Подавление нарушений» архитектурного правила — там объяснена логика.

Подавление путей для правила:

Любое правило может исключить конкретные файлы по префиксу пути или glob-паттерну. Нарушения из совпадающих файлов подавляются:

rules:
  coupling.cbo:
    suppress_paths:
      - src/Metrics                # префикс: все файлы в src/Metrics/
      - src/Metrics/*Visitor.php   # glob: только файлы визиторов

Работает совместно с suppress_namespaces -- оба фильтра применяются. В отличие от глобального suppress_paths, эта опция на уровне правила влияет только на конкретное правило, а не на все.

Видимость: в отличие от @qmx-ignore, эти подавления по умолчанию происходят незаметно — ничто в стандартном выводе не намекает на то, что нарушения были отброшены. Запустите с -v, чтобы увидеть разбивку по правилам — сколько нарушений подавлено таким образом (namespace-бакет объединяет suppress_namespaces и suppress_namespace_channels и показывается отдельно от suppress_paths), и добавьте --show-suppressed, чтобы также вывести список каждого подавленного нарушения — вместе с нарушениями, подавленными через @qmx-ignore.

Переопределение порогов для символа через @qmx-threshold:

Помимо общих для проекта порогов в YAML, можно переопределять пороги для отдельных классов или методов с помощью аннотаций @qmx-threshold прямо в исходном коде:

/**
 * @qmx-threshold complexity.ccn warning=20 error=40
 */
class ComplexStateMachine
{
    // Методы этого класса используют повышенные пороги сложности
}

Переопределение на классе также применяется к проверкам методов внутри него; более узкое переопределение на методе имеет приоритет.

Полный синтаксис и примеры смотрите в разделе Baseline > @qmx-threshold.

Селекторы правил и каналов

Везде, где называется правило или канал нарушений — disabled_rules, only_rules, их CLI-эквиваленты, suppress_namespace_channels и семейство @qmx-ignore в исходном коде, — имя читается одинаково:

Форма Что означает
complexity.ccn ровно это имя и ничего больше
complexity.* строго потомки complexitycomplexity.ccn, complexity.wmc и так далее. Сам complexity не входит; если имя одновременно называет правило и канал, адресуйте их отдельными записями
coupling.cbo:namespace канал, сужённый до одного уровня дерева агрегации. Уровень — один из callable, class, file, namespace, project, и канал обязан на нём сообщать

Голый префикс группой не является. complexity сам по себе не выбирает ничего и отвергается:

Rule selector "complexity" does not match any registered producer, group, or channel.

X.*, у которого нет потомков, отвергается по той же причине. Большинство правил публикуют единственный канал, чьё имя равно имени правила, поэтому code-smell.eval.* — ошибка; пишите code-smell.eval.

Неразрешимый селектор в конфигурации или в командной строке завершает прогон с кодом 3 ещё до того, как будет сформирован отчёт. Прежнее поведение — догадка о намерении — молча превращало опечатку в «ничего не выбрано».

Какую половину пары называет имя, решает директива, а не само имя. Одна и та же строка означает разное в зависимости от того, где она написана:

  • @qmx-ignore code-smell.boolean-argument — всегда канал: подавление принадлежит каналу;
  • @qmx-threshold code-smell.boolean-argument — всегда правило: порог принадлежит единственному объекту опций правила. Поэтому @qmx-threshold принимает точное имя правила и не принимает wildcard вообще — групповая переустановка порогов была футганом, а не возможностью;
  • ключи секции rules: и владелец перед : в --rule-opt RULE:option=value называют правило, точно. Ключ вида rules: { complexity: {...} } раньше проходил валидацию и не настраивал ничего; теперь он отвергается.

Полный синтаксис инлайн-директив — в разделе «Baseline», а о том, что инструмент сообщает, когда директива ничего не адресует, — в разделе «Правила > Аннотации».

Отключение правил (disabled_rules)

Отключение конкретных правил, каналов или целых групп:

disabled_rules:
  - code-smell.boolean-argument
  - duplication.clone

Эквивалент в CLI: --disable-rule=code-smell.boolean-argument --disable-rule=duplication.clone

Чтобы отключить группу целиком, используйте wildcard:

disabled_rules:
  - code-smell.*

Только указанные правила (only_rules)

Запустить только указанные правила (все остальные отключаются):

only_rules:
  - complexity.ccn
  - complexity.cognitive

Эквивалент в CLI: --only-rule=complexity.ccn --only-rule=complexity.cognitive

Условие завершения с ошибкой (fail_on)

Управление тем, какие уровни серьёзности приводят к ненулевому коду завершения:

fail_on: error    # Завершение с ошибкой только при error (по умолчанию)
# fail_on: warning  # Завершение с ошибкой и при warning
# fail_on: none     # Никогда не завершаться с ошибкой из-за нарушений

Примечание

По умолчанию fail_on установлен в error. Предупреждения и Info-диагностики по-прежнему отображаются в выводе, но не приводят к ненулевому коду завершения. Используйте fail_on: warning, чтобы блокировать сборку и предупреждениями.

info — только для отчёта и не является значением fail_on

fail_on принимает лишь warning и error — значение fail_on: info отклоняется с ошибкой, перечисляющей допустимые значения. Severity info означает «наблюдать, но не гейтить»: прогон, где нашлись только Info-диагностики, всегда завершается кодом 0, независимо от fail_on. Чтобы гейтить диагностику, поставляемую как info, поднимите серьёзность самого правила. Например, annotation.unused-directive (подавление, которое больше ничего не подавляет) по умолчанию info и поднимается опцией правила:

rules:
  annotation.directive:
    unused_directive_severity: warning

fail_on не управляет ошибками конфигурации

Часть каналов сообщает об ошибке в конфигурации, а не о долге в коде: пять диагностик политики слоёв (architecture.coverage-gap, architecture.unreachable-layer, architecture.pending-layer-matched, architecture.potential-shadow, architecture.empty-template) и три диагностики инлайн-директив (annotation.unresolved-directive, annotation.unsupported-threshold, annotation.invalid-threshold). Они вообще не участвуют в сравнении с fail_on: прогон завершается ненулевым кодом даже при fail_on: none, и принять их не может ни запись в baseline, ни @qmx-ignore. Это не суждение о качестве кода, а сообщение «я не могу сделать то, что ты просишь».

Исключение измерений здоровья (exclude_health)

Исключить конкретные измерения здоровья из оценки. Исключённые измерения не отображаются в сводке здоровья и не влияют на общую оценку:

exclude_health:
  - typing
  - maintainability

Эквивалент в CLI: --exclude-health=typing --exclude-health=maintainability

Лимит памяти (memory_limit)

Лимит памяти PHP для анализа. По умолчанию используется значение memory_limit из php.ini.

memory_limit: 1G    # 1 гигабайт
# memory_limit: -1  # Без ограничений

Эквивалент в CLI: --memory-limit=1G

Формат вывода (format)

Формат отчёта по умолчанию:

format: summary   # По умолчанию
# format: json
# format: html

Кэширование (cache)

Управление кэшированием AST для ускорения повторных запусков:

cache:
  enabled: true         # По умолчанию: true
  dir: .qmx-cache       # По умолчанию: .qmx-cache

dir — это путь к директории, записанный строкой. Имя из одних цифр — такое же имя директории, как любое другое: dir: "7331" — это директория 7331; кавычки обязательны, потому что 7331 без них — число, а число путём не является.

Эквивалент в CLI: --no-cache для отключения, --cache-dir=DIR для изменения директории.

Параллельная обработка (parallel)

Количество параллельных воркеров для анализа файлов:

parallel:
  workers: 4     # Фиксированное количество воркеров
  # workers: 0   # Отключить параллелизм (последовательно)
  # workers: 1   # Отключить параллелизм (один процесс)

По умолчанию Qualimetrix автоматически определяет оптимальное количество воркеров по числу ядер CPU. Эквивалент в CLI: --workers=4

Совет

Используйте workers: 1 для отладки или однопроцессного окружения. workers: 0 отключает параллелизм (последовательное выполнение); автоопределение — поведение по умолчанию, когда опция не задана.

Определение неймспейсов (namespace)

Qualimetrix определяет неймспейсы проекта по composer.json в рабочей директории запуска и при необходимости разбирает исходный код. Настройка определения неймспейсов не поддерживается: namespace.strategy и namespace.composer_json отклоняются как неизвестные ключи.

Связанность (coupling)

Настройка префиксов неймспейсов фреймворка для метрики CBO (Coupling Between Objects). Зависимости от неймспейсов фреймворка отслеживаются отдельно как coupling.cbo-app и coupling.ce-framework:

coupling:
  framework-namespaces:
    - Symfony
    - Doctrine
    - Psr
    - Illuminate

Если framework-namespaces не указаны, coupling.cbo-app равен coupling.cbo (без эффекта).

Агрегация (aggregation)

Агрегация неймспейсов следует анализируемым объявлениям. Пользовательские настройки aggregation.prefixes и aggregation.auto_depth больше не поддерживаются и отклоняются как неизвестные ключи.

Архитектура (architecture)

Правила слоёв и allow-list для правила architecture.layer-violation живут под верхнеуровневым ключом architecture:. Полная схема описана в правилах архитектуры; ключи, наиболее релевантные для общей конфигурации:

architecture:
  layers:
    - name: 'domain-{module}'                         # шаблонный слой с переменной захвата
      patterns: ['App\Module\{module}\Domain\**']
    - name: shared-kernel
      patterns: ['App\Shared\**']

  allow:
    'domain-{m}':
      - shared-kernel
      - target: 'domain-{m}'
        relations: [implements, extends]              # whitelist видов зависимостей

  coverage-gap: ignore                                # ignore | warn | error
  max_expanded_layers: 500                            # кумулятивный лимит на раскрытие шаблонов

Переменные захвата в именах слоёв и паттернах используют синтаксис {name} (один сегмент namespace) или {name:**} (cross-segment). Одно и то же имя переменной в пределах одной записи слоя привязывается к одному значению (co-binding); переменные в разных записях независимы. Полную грамматику см. в секции про шаблонные слои.

max_expanded_layers ограничивает общее количество конкретных слоёв, производимых раскрытием шаблонов по всем шаблонам (по умолчанию 500). Лимит защищает от патологически широких шаблонов, чьи binding tuples взорвали бы число слоёв. Поднимайте предел явно, когда монорепо легитимно содержит больше bounded contexts, чем дефолт; превышение отклоняется на стадии раскрытия с понятной ошибкой.

Вычисляемые метрики (computed_metrics)

Корневой ключ computed_metrics: настраивает шесть встроенных оценок здоровья (health.complexity, health.cohesion, health.coupling, health.typing, health.maintainability, health.overall) и любые пользовательские вычисляемые метрики. Это достаточно большая поверхность — допустимые ключи, пороги, собственные формулы, доступные переменные и функции, — чтобы иметь собственный справочник: полную схему см. в «Оценки здоровья > Настройка».


Пресеты

Пресеты — это именованные наборы конфигурации, которые применяют предопределённые настройки: пороговые значения, отключённые правила, поведение при ошибках — одним флагом. Вместо ручной настройки десятков параметров выберите пресет, соответствующий зрелости вашего проекта.

Пресет Описание
strict Строгие пороговые значения для новых проектов. Устанавливает fail_on: warning
legacy Ослабленные пороговые значения для легаси-кодовых баз. Отключает шумные правила
ci Явный режим CI. Устанавливает fail_on: error
# Использовать один пресет
vendor/bin/qmx check src/ --preset=strict

# Комбинировать несколько пресетов
vendor/bin/qmx check src/ --preset=strict,ci

# Использовать пользовательский файл пресета
vendor/bin/qmx check src/ --preset=./my-preset.yaml

Порядок приоритетов: Пресеты применяются после обнаружения composer.json, но до qmx.yaml. Ваш файл конфигурации всегда переопределяет значения из пресетов.

Несколько пресетов: При комбинировании пресетов они объединяются слева направо — последующие пресеты переопределяют предыдущие, за исключением списковых ключей вроде disabled_rules, которые накапливаются. Например, --preset=legacy,ci даёт ослабленные пороговые значения с поведением CI при ошибках.

Внимание

only_rules не накапливается между пресетами — значение из последнего пресета полностью заменяет предыдущие. Это сделано намеренно: only_rules — это ограничивающий фильтр, и объединение расширяло бы область действия.

Пользовательские пресеты: Любой YAML-файл со структурой qmx.yaml можно использовать как пресет. Передайте путь к файлу вместо имени встроенного пресета.


Полный пример

# Или начните с пресета и настройте:
# vendor/bin/qmx check src/ --preset=strict

paths:
  - src/

exclude:
  - vendor/
  - tests/Fixtures/

suppress_paths:
  - src/Entity
  - src/DTO

suppress_namespaces:
  - App\Tests

include_generated: false

format: summary
fail_on: error

cache:
  enabled: true
  dir: .qmx-cache

parallel:
  workers: 4

coupling:
  framework-namespaces:
    - Symfony
    - Doctrine

exclude_health:
  - typing

disabled_rules:
  - code-smell.boolean-argument
  - duplication.clone

rules:
  complexity.ccn:
    suppress_namespaces:
      - App\Tests
    suppress_paths:
      - src/Generated
    callable:
      warning: 15
      error: 25

  size.method-count:
    warning: 25
    error: 40

Параметры CLI имеют приоритет

Параметры командной строки всегда имеют приоритет над значениями из конфигурационного файла. Например:

# В конфиге указано paths: [src/], но CLI переопределяет
vendor/bin/qmx check lib/

# Добавить дополнительное подавление поверх конфига
vendor/bin/qmx check src/ --suppress-path='src/Generated/*'

Это позволяет экспериментировать без редактирования файла конфигурации.

Что верхний слой заменяет, а что оставляет как есть

Слой заменяет те ключи, которые написал, и ничего больше.

threshold: N — это сокращение для обеих половин полосы warning/error, поэтому верхний слой, переписавший только одну половину, сохраняет значение сокращения во второй:

# пресет
rules: {complexity.ccn: {callable: {threshold: 5}}}
vendor/bin/qmx check src/ --preset=my-preset.yaml \
  --rule-opt='complexity.ccn:callable.warning=2'

Результат — warning 2 с командной строки и error 5 из сокращения пресета, а не скомпилированный дефолт правила.

Ключ, написанный ~, не пишет ничего: он оставляет значение таким, каким оно было бы иначе, а через границу слоёв это значит — как в слое ниже.

# пресет задал warning 2 / error 3; это оставит обе величины на месте
rules: {complexity.ccn: {callable: {warning: ~}}}

# а это оставит на месте всю конфигурацию правила
rules: {complexity.ccn: ~}

Чтобы выключить правило, пишите false, а не ~:

rules: {complexity.ccn: false}

Смешивать threshold с warning/error в ОДНОМ слое по-прежнему запрещено — это два написания одного и того же, и написать оба значит не сказать, что имелось в виду.


Валидация конфигурации

Qualimetrix валидирует конфигурационный файл и сообщает о типичных ошибках понятными сообщениями.

Неизвестные ключи

Любой нераспознанный ключ — на верхнем уровне или внутри секции — вызывает ошибку с подсказкой:

Invalid configuration in qmx.yaml:
  Unknown key "workes" in "parallel" section. Did you mean "workers"?

Ошибки типов

Если значение имеет неверный тип, вы получите понятное сообщение вместо молчаливого отката на значение по умолчанию:

Invalid value for "cache.enabled": expected boolean, got string

Неизвестные имена правил

Опечатки в именах правил в секции rules: отклоняются:

Unknown rule "complexty.cyclomatic" in qmx.yaml. Did you mean "complexity.ccn"?

Неизвестные ключи опций правила

Ключ опции, которого у правила нет, — ошибка конфигурации на любой глубине, где его можно написать: прогон останавливается с кодом 3, анализ не выполняется:

Configuration error: Option "warningThreshold" is not an option of rule "complexity.ccn". Options here: callable, class, enabled, suppress-namespace-channels, suppress-namespaces, suppress-paths, threshold.

Внутри слота уровня сообщение называет слот, потому что множество допустимых ключей своё у каждого уровня, а не у правила целиком:

Configuration error: Option "warning" is not an option of rule "complexity.ccn" at level "class". Options at that level: enabled, max-error, max-warning, threshold. Other levels of this rule take different options.

То же верно для --rule-opt в командной строке — тот же код и то же сообщение.

Ключ цитируется в свёрнутом написании

Разделители сворачиваются до того, как ключ доходит до правила, поэтому на опечатку max_warnign ответ придёт как "maxWarnign". Буквы — а именно их и путает опечатка — не меняются. Допустимые ключи всегда перечисляются в каноническом kebab-написании.

Форма значения

Два правила ниже действуют для любого ключа этого документа — для корней, секций, поддерева rules: и слотов уровня внутри него одинаково. Это не свойство опций правил; опции правил — просто то место, где вы встречаете их чаще всего.

Каждый ключ объявляет форму, которую может принять его значение. Значение иной формы завершает прогон с кодом 3. Оно никогда не приводится к чему-то пригодному и никогда не отбрасывается в пользу значения по умолчанию при успешном отчёте. Судится каждый источник, из которого приходит значение, а не только победивший: неверный format: или cache_dir: в файле отклоняется даже тогда, когда его перекрыли из командной строки, — значение, которым никто не воспользуется, всё равно кем-то написано:

Configuration error: Option "warning" of rule "complexity.ccn" at level "callable" must be a whole number or null, got a string.
Configuration error: Invalid value for "only_rules": expected a list of entries, got a map.
Configuration error: Invalid value for "cache": expected a section of named keys (dir, enabled), got a list.

Формы, которые может запросить ключ, в тех словах, которыми их называет отказ:

Форма Принимает
a boolean true / false
a whole number 15 — не 10.5 и не "15"
a number 15 или 10.5
a string любую строку, включая пустую
a non-empty string строку хотя бы с одним непробельным символом
a list of X YAML-последовательность, каждый элемент формы X
a map of X YAML-отображение, ключи которого называете вы; значения формы X
a block of options отображение, за собственные ключи которого отвечает другое объявление

Пять следствий стоит проговорить отдельно — каждое из них раньше проходило незамеченным:

  • Число в кавычках — это строка. warning: "15" отклоняется там, где объявлено целое; пишите warning: 15. Различие относится только к YAML: значения, пришедшие из командной строки, по построению являются текстом и приводятся до проверки формы, поэтому --rule-opt="size.method-count:threshold=25" и --rule-opt="complexity.ccn:enabled=false" работают как раньше.
  • Целое — это не дробь, кроме перечисленных здесь ключей, объявивших «a number». warning: 10.5 отклоняется там, где объявлено целое, а таким образом объявлено большинство порогов этого документа. Ключи ниже объявляют «a number» вместо этого и потому принимают дробь точно в написанном виде — каждый измеряет непрерывную величину, а не считает что-то: индекс сопровождаемости (maintainability.mi.error / .warning / .threshold), нестабильность (coupling.instability.max-error / .max-warning / .threshold, те же три и на слотах уровня class. и namespace.), дистанция от главной последовательности (coupling.distance.max-distance-error / .max-distance-warning / .threshold), ClassRank (coupling.class-rank.error / .warning / .threshold), покрытие типами (design.type-coverage.param.error / .warning / .threshold, те же три под .property. и .return.) и порог TCC на design.god-class.tcc-threshold. Это свойство объявленной формы КОНКРЕТНОГО ключа, а не его правила или семейства целиком: ключи, стоящие рядом с принимающим, остаются целыми и отклоняют дробь, потому что считают что-то, а не измеряют, — coupling.distance.min-class-count, coupling.instability.min-afferent (на каждом слоте уровня) и coupling.instability.namespace.min-class-count каждый раз считают классы или файлы, а не отношение.
  • Список и карта не взаимозаменяемы. only_rules: {a: complexity.ccn} и exclude_methods: {a: getName} отклоняются; пишите only_rules: [complexity.ccn] и exclude_methods: [getName]. То же в другую сторону: cache: [dir] отклоняется, потому что cache: — секция именованных ключей.
  • Корневой suppress_paths: — исключение из правила «число не строка». Каталог может законно называться 2024, и YAML отдаёт такой сегмент без кавычек числом, поэтому этот корень читает его как имя, которым он и является. Три соседа — нет: suppress_namespaces: отклоняет, потому что сегмент неймспейса не может начинаться с цифры; пер-правило rules.<name>.suppress_paths: тоже отклоняет, будучи объявлен строками; а paths: и exclude: отбрасывают такой элемент без единого слова. Везде, кроме корневого ключа, пишите его в кавычках — ['2024'].
  • Пустая строка — самостоятельная форма. Она отклоняется везде, где требуется непустое значение, в том числе в командной строке: --fail-on=, --format=, --cache-dir= и --memory-limit= называют, что они ожидали вместо неё, а не молча берут значение по умолчанию.

Ключ, написанный без значения

Написать ключ и оставить его пустым — key: или явный YAML null key: ~ — означает, что для значения этого ключа будет взято значение по умолчанию; это верно на любой глубине и в любой секции, включая rules::

cache: ~                 # то же, что не писать cache:
coupling: ~              # то же, что не писать coupling:
cache:
  dir: ~                 # то же, что не писать dir:
rules:
  complexity.ccn:
    enabled: ~           # то же, что не писать enabled:
    callable: ~          # то же, что не писать callable:

Это относится и к тем ключам внутри rules:, которые выбирают, как читать правило, а не только к тем, чьё значение читают. Пороговый ключ выбирает сокращённую форму иерархического правила — threshold: у complexity.ccn, complexity.cognitive и complexity.npath; threshold:, warning: или error: у coupling.cbo; threshold:, max_warning: или max_error: у coupling.instability, — и выбирает её значение, написанное в этом ключе. ~ не пишет значения, поэтому ничего не выбирает, и блоки callable: / class: / namespace:, стоящие рядом, читаются ровно так же, как если бы ключа не было:

rules:
  coupling.cbo:
    warning: ~       # то же, что не писать его
    class:
      warning: 0     # будет прочитано
      error: 0

По той же причине threshold: ~, написанный рядом с warning: / error:, не называет второго режима, и смешивать его не с чем:

rules:
  cohesion.lcom:
    threshold: ~     # значения нет, значит и режима нет
    warning: 5       # градуированный режим, как если бы threshold: не писали

Два значения — по-прежнему два режима, и это по-прежнему ошибка конфигурации:

rules:
  cohesion.lcom:
    threshold: 3     # Configuration error, exit 3:
    warning: 5       # Cannot mix "threshold" with "warning"/"error"

Элемент списка — единственное место, где это не действует, и по причине: элемент является значением, а не оставленным без значения ключом, поэтому означать ему нечего. Там, где список объявлен как список непустых строк, элемент ~ отклоняется:

rules:
  complexity.ccn:
    suppress_namespaces:
      - App\Legacy
      - ~              # отказ: exit 3

Обработка конфигурации

Формат конфигурационного файла и поведение CLI не изменились. Внутри Qualimetrix обрабатывает defaults, presets, файлы, Composer discovery и параметры CLI через границу Analysis Configuration. Эта деталь реализации не меняет ни один документированный ключ или правило приоритета.


Что дальше?

Смотрите справочник параметров CLI для полного списка параметров командной строки.