Правила сложности¶
Правила сложности измеряют, насколько запутан и ветвист ваш код. Чем больше ветвлений, циклов и условий в методе, тем труднее его понять, протестировать и изменить, не внеся баги.
Представьте, что вы объясняете дорогу: "едешь прямо, потом поворачиваешь налево" -- это просто. А "едешь прямо, но если ремонт дороги -- поверни направо, разве что сегодня вторник, тогда..." -- это уже сложно.
Цикломатическая сложность¶
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-обращение к свойству) — +1xor(логический оператор 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:
# 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), где это уместно.
Настройка¶
Сокращённая запись с threshold:
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:
Некоторые другие инструменты (в частности pdepend) используют мультипликативный подход для match, что может давать экстремальные значения (миллионы) для методов с большими match-выражениями. Аддитивный подход Qualimetrix даёт практичные и применимые значения.
Настройка¶
# qmx.yaml
rules:
complexity.npath:
method:
warning: 300
error: 2000
class:
enabled: false # disabled by default
Сокращённая запись с threshold:
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 будут соответственно выше, чем у других инструментов.
Настройка¶
Сокращённая запись с threshold: