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

Правила сложности

Правила сложности измеряют, насколько запутан и ветвист ваш код. Чем больше ветвлений, циклов и условий в методе, тем труднее его понять, протестировать и изменить, не внеся баги.

Представьте, что вы объясняете дорогу: "едешь прямо, потом поворачиваешь налево" -- это просто. А "едешь прямо, но если ремонт дороги -- поверни направо, разве что сегодня вторник, тогда..." -- это уже сложно.


Цикломатическая сложность

Rule ID: complexity.cyclomatic

Что измеряет

Цикломатическая сложность (часто сокращается как CCN) считает количество точек принятия решений в методе. Каждый if, elseif, while, for, foreach, case, catch, &&, ||, ?? и ?: добавляет 1 к счётчику. Метод без ветвлений имеет сложность 1.

Это число примерно показывает минимальное количество тест-кейсов, необходимых для полного покрытия метода.

Как читать значение:

CCN Интерпретация
1--4 Простой, легко тестировать
5--10 Умеренная сложность
11--20 Сложный -- стоит рефакторить
21--50 Очень сложный, трудно поддерживать
50+ Экстремальная сложность -- разделите метод

Пороговые значения

Уровень метода (включён по умолчанию):

Уровень Порог Серьёзность
Warning >= 10 Warning
Error >= 20 Error

Уровень класса (включён по умолчанию) -- проверяет максимальную CCN среди всех методов класса:

Уровень Порог Серьёзность
Warning >= 30 Warning
Error >= 50 Error

Пример

Этот метод имеет цикломатическую сложность 5 (1 базовая + 4 точки принятия решений):

function processOrder(Order $order): void
{
    if ($order->isPaid()) {               // +1
        if ($order->hasDiscount()) {      // +1
            $this->applyDiscount($order);
        }
        foreach ($order->getItems() as $item) {  // +1
            $this->ship($item);
        }
    } elseif ($order->isPending()) {      // +1
        $this->notify($order);
    }
}

Как исправить

  • Выделяйте методы. Переносите вложенную логику в хорошо именованные вспомогательные методы. Каждый метод становится проще и легче тестируется.
  • Используйте ранние возвраты. Вместо вложенных блоков if проверяйте невалидные условия в начале и сразу возвращайтесь.
  • Заменяйте условия полиморфизмом. Если у вас длинный switch или цепочка if/elseif, рассмотрите паттерн Strategy или State.
  • Упрощайте булевы выражения. Сложные условия вроде if ($a && ($b || $c) && !$d) часто можно разбить на именованные булевы переменные или отдельные методы.

Особенности реализации

Qualimetrix использует расширенный вариант цикломатической сложности, иногда называемый CCN2+. Помимо стандартных точек принятия решений (if, elseif, while, for, foreach, case, catch, &&, ||, ?:), Qualimetrix также учитывает:

  • ?? (оператор null coalescing) — +1
  • ?-> (nullsafe-вызов метода) — +1
  • ?-> (nullsafe-обращение к свойству) — +1
  • xor (логический оператор XOR) — +1

Это осознанное решение: все эти конструкции представляют скрытое ветвление. Например, $a ?? $b эквивалентно $a !== null ? $a : $b — точка принятия решения, которую легко упустить из виду.

Ветви match: Каждое условие в match-ветви с несколькими значениями считается отдельно. Например, 1, 2, 3 => ... даёт 3 точки принятия решений, аналогично проваливанию (fall-through) в switch case.

Замыкания и стрелочные функции: Замыкания и стрелочные функции измеряются как отдельные единицы — они не добавляются к CCN вмещающего метода. Это соответствует их практическому использованию как самостоятельных вызываемых объектов.

Сравнение с другими инструментами

Из-за этих дополнительных точек принятия решений Qualimetrix будет показывать более высокие значения CCN, чем phpmd или pdepend, для кода, использующего null coalescing или nullsafe-операторы. Это не баг — это отражение более строгого определения сложности. Разница особенно заметна в коде с цепочками выражений ??.

Настройка

# qmx.yaml
rules:
  complexity.cyclomatic:
    method:
      warning: 15
      error: 25
    class:
      max_warning: 40
      enabled: true   # set to false to disable class-level check

Сокращённая запись с threshold — единый порог для CI:

rules:
  complexity.cyclomatic:
    method:
      threshold: 15   # warning=15, error=15 → все нарушения — ошибки
# CLI overrides
bin/qmx check src/ --rule-opt="complexity.cyclomatic:method.warning=15"
bin/qmx check src/ --rule-opt="complexity.cyclomatic:method.error=25"
bin/qmx check src/ --rule-opt="complexity.cyclomatic:class.max_warning=40"
bin/qmx check src/ --rule-opt="complexity.cyclomatic:class.enabled=false"

Когнитивная сложность

Rule ID: complexity.cognitive

Что измеряет

Когнитивная сложность измеряет, насколько трудно прочитать и понять код человеку. В отличие от цикломатической сложности, которая механически считает точки решений, когнитивная сложность учитывает, как код воспринимается читателем.

Ключевые отличия от цикломатической сложности:

  • Вложенность увеличивает штраф. if внутри другого if стоит больше, чем два if на одном уровне, потому что вложенную логику труднее удерживать в голове.
  • Короткие конструкции стоят меньше. switch с 10 кейсами добавляет только 1 балл (это одна мысленная структура), тогда как 10 отдельных if добавляют по 1 баллу каждый плюс вложенность.
  • Разрывы линейного потока добавляют баллы. break, continue и goto стоят баллов, потому что нарушают поток чтения.

Как читать значение:

Cognitive Интерпретация
0--5 Простой, легко понять
6--15 Умеренная сложность
16--30 Сложный, трудно отслеживать логику
30+ Очень трудно понять -- нужен рефакторинг

Пороговые значения

Уровень метода (включён по умолчанию):

Уровень Порог Серьёзность
Warning >= 15 Warning
Error >= 30 Error

Уровень класса (включён по умолчанию) -- проверяет максимальную когнитивную сложность среди всех методов:

Уровень Порог Серьёзность
Warning >= 30 Warning
Error >= 50 Error

Пример

function calculate(array $items): float  // когнитивная сложность: 9
{
    $total = 0;
    foreach ($items as $item) {                  // +1 (вложенность 0)
        if ($item->isActive()) {                 // +2 (1 + вложенность 1)
            if ($item->hasDiscount()) {          // +3 (1 + вложенность 2)
                $total += $item->discountedPrice();
            } else {                             // +1
                $total += $item->price();
            }
        }
    }
    return $total;
}

Обратите внимание, как вложенность увеличивает штраф. Глубоко вложенный if ($item->hasDiscount()) стоит 3 балла, а не 1, потому что находится внутри двух других структур.

Как исправить

  • Уменьшайте глубину вложенности. Это самый эффективный способ. Используйте ранние возвраты для "выравнивания" кода.
  • Выделяйте глубоко вложенные блоки в отдельные методы с описательными именами.
  • Избегайте else после return. Если ветка if возвращает значение, else не нужен.
  • Заменяйте циклы методами коллекций (например, array_filter, array_map), где это уместно.

Настройка

# qmx.yaml
rules:
  complexity.cognitive:
    method:
      warning: 20
      error: 40
    class:
      max_warning: 40

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

rules:
  complexity.cognitive:
    method:
      threshold: 20   # warning=20, error=20
bin/qmx check src/ --rule-opt="complexity.cognitive:method.warning=20"
bin/qmx check src/ --rule-opt="complexity.cognitive:method.error=40"

NPath-сложность

Rule ID: complexity.npath

Что измеряет

NPath-сложность считает общее количество уникальных путей выполнения через метод. Если цикломатическая сложность добавляет 1 за каждую точку решения, то NPath умножает по ветвлениям.

Представьте: если в методе 3 независимых if, каждый может быть истинным или ложным. Это даёт 2 x 2 x 2 = 8 возможных путей. NPath будет 8, а цикломатическая сложность -- всего 4.

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

Как читать значение:

NPath Категория Интерпретация
1--1,000 moderate Небольшой рефакторинг (выделить 1--2 метода)
1,001--10,000 high Значительный рефакторинг необходим
10,001--1,000,000 very high Требуется серьёзная реструктуризация
> 1,000,000 extreme Необходим фундаментальный редизайн

Категория указывается в сообщениях о нарушениях (например, "NPath complexity is 36120 (very high)"), чтобы помочь расставить приоритеты без запоминания шкалы NPath.

Пороговые значения

Уровень метода (включён по умолчанию):

Уровень Порог Серьёзность
Warning >= 200 Warning
Error >= 1000 Error

Уровень класса (отключён по умолчанию) -- проверяет максимальный NPath среди методов.

Пример

function validate(Request $request): bool
{
    if ($request->hasName()) { /* ... */ }       // 2 пути
    if ($request->hasEmail()) { /* ... */ }      // x 2 = 4 пути
    if ($request->hasPhone()) { /* ... */ }      // x 2 = 8 путей
    if ($request->hasAddress()) { /* ... */ }    // x 2 = 16 путей
    if ($request->hasCity()) { /* ... */ }       // x 2 = 32 пути
    if ($request->hasCountry()) { /* ... */ }    // x 2 = 64 пути
    if ($request->hasZip()) { /* ... */ }        // x 2 = 128 путей
    if ($request->hasState()) { /* ... */ }      // x 2 = 256 путей -> WARNING
    return true;
}

Всего 8 независимых if уже дают 256 путей.

Как исправить

  • Выделяйте группы связанных проверок в отдельные методы. Разделение валидации на validateContactInfo() и validateAddress() резко сокращает количество путей.
  • Сокращайте независимые ветвления. Объединяйте связанные условия или используйте валидацию на основе данных (например, цикл по списку обязательных полей).
  • Избегайте глубоко вложенных условий -- они умножают NPath ещё быстрее, чем последовательные.

Особенности реализации

Qualimetrix следует Немеху (1988) с расширениями для PHP:

  • Логические операторы в условиях: Каждый &&/|| в условии добавляет 1 к числу путей этого условия. Например, if ($a && $b || $c) даёт 4 пути (базовых 2 + 2 оператора).
  • Тернарный оператор: Вносит 2 базовых пути плюс любую сложность в подвыражениях.
  • ?? (null coalescing): Трактуется как +1 дополнительный путь, аналогично тернарному оператору.
  • Расширения для PHP: match, foreach, ?? и ?-> обрабатываются как конструкции, порождающие пути.

Выражения match: Qualimetrix использует аддитивный подход, согласованный с оригинальной формулой Немеха для switch:

NPath(match) = 1 + sum of NPath(each arm body)

Некоторые другие инструменты (в частности pdepend) используют мультипликативный подход для match, что может давать экстремальные значения (миллионы) для методов с большими match-выражениями. Аддитивный подход Qualimetrix даёт практичные и применимые значения.

Настройка

# qmx.yaml
rules:
  complexity.npath:
    method:
      warning: 300
      error: 2000
    class:
      enabled: false   # disabled by default

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

rules:
  complexity.npath:
    method:
      threshold: 500   # warning=500, error=500
bin/qmx check src/ --rule-opt="complexity.npath:method.warning=300"
bin/qmx check src/ --rule-opt="complexity.npath:class.enabled=true"

WMC -- Взвешенные методы класса

Rule ID: complexity.wmc

Что измеряет

WMC (Weighted Methods per Class) -- это сумма цикломатической сложности всех методов класса. Показывает общую нагрузку сложности на весь класс.

Класс с 20 простыми геттерами/сеттерами (каждый со сложностью 1) имеет WMC = 20. Класс с 5 методами, каждый со сложностью 10, имеет WMC = 50. Оба "тяжёлые" по-разному: в первом слишком много методов, во втором -- слишком сложные методы.

Как читать значение:

WMC Интерпретация
1--20 Управляемый класс
21--50 Большой класс
51--80 Очень большой класс
80+ Чрезмерный -- стоит разделить

Пороговые значения

Уровень Порог Серьёзность
Warning > 50 Warning
Error > 80 Error

Пример

class OrderProcessor
{
    public function process(): void { /* CCN = 8 */ }
    public function validate(): void { /* CCN = 12 */ }
    public function calculateTax(): void { /* CCN = 6 */ }
    public function applyDiscounts(): void { /* CCN = 10 */ }
    public function generateInvoice(): void { /* CCN = 7 */ }
    public function sendNotification(): void { /* CCN = 5 */ }
    public function logResult(): void { /* CCN = 3 */ }
    // WMC = 8 + 12 + 6 + 10 + 7 + 5 + 3 = 51 -> WARNING
}

Как исправить

  • Разделите класс на более мелкие, сфокусированные классы. Если WMC высок из-за большого количества методов, класс, вероятно, имеет слишком много обязанностей.
  • Упростите отдельные методы. Если WMC высок из-за нескольких очень сложных методов, рефакторите сначала их.
  • Учитывайте принцип единственной ответственности. У класса должна быть только одна причина для изменения.

Особенности реализации

WMC вычисляется как сумма цикломатической сложности всех методов класса. Поскольку Qualimetrix использует вариант CCN2+ (который учитывает ?? и ?-> как точки принятия решений), значения WMC будут соответственно выше, чем у других инструментов.

Настройка

# qmx.yaml
rules:
  complexity.wmc:
    warning: 60
    error: 100
    exclude_data_classes: true

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

rules:
  complexity.wmc:
    threshold: 60   # warning=60, error=60
bin/qmx check src/ --rule-opt="complexity.wmc:warning=60"
bin/qmx check src/ --rule-opt="complexity.wmc:error=100"