Правила сложности¶
Правила сложности измеряют, насколько запутан и ветвист ваш код. Чем больше ветвлений, циклов и условий в методе, тем труднее его понять, протестировать и изменить, не внеся баги.
Представьте, что вы объясняете дорогу: "едешь прямо, потом поворачиваешь налево" -- это просто. А "едешь прямо, но если ремонт дороги -- поверни направо, разве что сегодня вторник, тогда..." -- это уже сложно.
Цикломатическая сложность¶
Rule ID: complexity.ccn
Судимые метрики: complexity.ccn, complexity.ccn.max
Что измеряет¶
Цикломатическая сложность (часто сокращается как CCN) считает количество точек принятия решений в методе. Каждый if, elseif, while, for, foreach, case, catch, &&, ||, ?? и ?: добавляет 1 к счётчику. Метод без ветвлений имеет сложность 1.
Это число примерно показывает минимальное количество тест-кейсов, необходимых для полного покрытия метода.
Как читать значение:
| CCN | Интерпретация |
|---|---|
| 1--4 | Простой, легко тестировать |
| 5--10 | Умеренная сложность |
| 11--20 | Сложный -- стоит рефакторить |
| 21--50 | Очень сложный, трудно поддерживать |
| 50+ | Экстремальная сложность -- разделите метод |
Пороговые значения¶
Уровень вызываемого объекта (Callable) (включён по умолчанию):
| Уровень | Порог | Серьёзность |
|---|---|---|
| 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, &&, ||, ?:) плюс операторы PHP, перечисленные в отклонении ниже.
Отклонение от оригинальной спецификации
У метрики Маккейба нет аналога этих операторов PHP. Qualimetrix считает каждый из них точкой принятия решения:
??(оператор null coalescing) и??=(присваивание с 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.ccn:
callable:
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.ccn:callable.warning=15"
bin/qmx check src/ --rule-opt="complexity.ccn:callable.error=25"
bin/qmx check src/ --rule-opt="complexity.ccn:class.max_warning=40"
bin/qmx check src/ --rule-opt="complexity.ccn:class.enabled=false"
warning: / error: — опции уровня, а не правила
Написанные прямо под complexity.ccn: — вне callable: и class: —
они ничего не настраивают, и прогон теперь останавливается с кодом 3.
Вкладывайте их в тот уровень, к которому они относятся. У уровня
class: границы называются max_warning / max_error, то есть два
уровня принимают разные ключи; отказ перечисляет допустимое именно в
той позиции, где вы написали ключ.
Когнитивная сложность¶
Rule ID: complexity.cognitive
Судимые метрики: complexity.cognitive, complexity.cognitive.max
Что измеряет¶
Когнитивная сложность измеряет, насколько трудно прочитать и понять код человеку. В отличие от цикломатической сложности, которая механически считает точки решений, когнитивная сложность учитывает, как код воспринимается читателем.
Ключевые отличия от цикломатической сложности:
- Вложенность увеличивает штраф.
ifвнутри другогоifстоит больше, чем дваifна одном уровне, потому что вложенную логику труднее удерживать в голове. - Короткие конструкции стоят меньше.
switchс 10 кейсами добавляет только 1 балл (это одна мысленная структура), тогда как 10 отдельныхifдобавляют по 1 баллу каждый плюс вложенность. - Разрывы линейного потока добавляют баллы.
gotoиbreak/continueс номером уровня (break 2) стоят баллов, потому что нарушают поток чтения.
Как читать значение:
| Cognitive | Интерпретация |
|---|---|
| 0--5 | Простой, легко понять |
| 6--15 | Умеренная сложность |
| 16--30 | Сложный, трудно отслеживать логику |
| 30+ | Очень трудно понять -- нужен рефакторинг |
Пороговые значения¶
Уровень вызываемого объекта (Callable) (включён по умолчанию):
| Уровень | Порог | Серьёзность |
|---|---|---|
| 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), где это уместно.
Особенности реализации¶
Qualimetrix реализует спецификацию SonarSource (Cognitive Complexity, версия 1.7, приложение B):
- Инкременты (+1):
if,elseif,else, тернарный?:,switch,for,foreach,while,do-while,catch,goto,break/continueс номером уровня, каждая последовательность одинаковых логических операторов и метод, вызывающий сам себя (+1 один раз на метод, сколько бы рекурсивных вызовов в нём ни было). - Последовательности логических операторов читаются в порядке записи, как в спецификации: каждый оператор, отличный от предыдущего, начинает новую последовательность.
$a && $b && $c— одна,$a && $b || $c— две,$a && ($b || $c) && $d— три.&&иand— один оператор, как и||иor. Всё остальное между двумя операторами —!, вызов, присваивание — завершает выражение, поэтому$a && !($b && $c)— две последовательности. else if, записанный двумя словами, — этоelse ifспецификации: +1 без надбавки за вложенность, так же какelseif.ifвнутриelse { ... }с фигурными скобками — вложенныйif.- Надбавка за вложенность:
if, тернарный оператор,switch, циклы иcatchдобавляют ещё текущий уровень вложенности.elseif,else if,else,gotoи переходы с номером — нет. - Уровень вложенности: растёт внутри тел
if,elseif,else, ветвей тернарного оператора, ветокswitch, циклов,catchи лямбд. Спецификация не говорит, где находится условие; Qualimetrix считает условие или проверяемое выражение не вложенным в структуру, которой оно управляет: тернарный оператор в условииif (...)добавляет +1, а в телеif— +2. match— switch-выражение PHP, оценивается какswitch(+1 плюс уровень вложенности, ветви вложены на один уровень): спецификация засчитывает собственное написание перечисленного ключевого слова в языке.- Замыкания и стрелочные функции ничего не добавляют вмещающему методу и поднимают уровень вложенности своего тела на один. Как и все остальные метрики callable, Qualimetrix измеряет каждое замыкание как отдельную единицу (см. отклонение ниже).
- Именованные функции и методы анонимных классов, объявленные внутри метода, измеряются как отдельные единицы с уровня вложенности 0 (см. отклонение ниже).
- Операторы null coalescing ничего не добавляют:
??,??=и?->дают 0, как предписывает спецификация (раздел «Ignore shorthand», страница 6): они сокращают проверку на null до одного выражения, а не прерывают линейный поток. Цикломатическая сложность учитывает каждый оператор как +1; NPath считает??и??=выбором значения как у тернарного оператора (+2 на оператор).
public function activeNames(array $users): array // когнитивная сложность: 0
{
return array_map( // +0 (сама стрелочная функция)
fn (User $u) => $u->isActive() // стрелочная функция: +2 (тернарный оператор, вложенность 1)
? $u->name()
: 'inactive',
$users,
);
}
Отклонение от оригинальной спецификации
- Замыкания и стрелочные функции внутри метода: спецификация добавляет тело лямбды к вмещающему методу. Qualimetrix оставляет лямбду отдельной единицей, как это делают его цикломатическая, NPath-метрика и индекс сопровождаемости, но продолжает уровень вложенности с того места, где лямбда написана. Поэтому оценка метода по спецификации равна значению метода плюс значения лямбд внутри него; в примере выше 0 + 2 = 2. Находка о сложном замыкании выдаётся на само замыкание.
- Именованные функции и методы анонимных классов внутри метода: спецификация вкладывает их на один уровень внутрь вмещающей структуры и добавляет их тело к методу. Qualimetrix измеряет каждую такую функцию как отдельную единицу с уровня вложенности 0.
- Рекурсия: обнаруживается только прямой вызов самого себя:
$this->method()или$this?->method(),self::,static::или имя, разрешённое в собственный класс: импортированный алиас, импортированный префикс пространства имён, полностью квалифицированное или относительное (namespace\) имя, в любом регистре.parent::не считается вызовом самого себя. Функция может вызвать себя по короткому, полностью квалифицированному или относительному (namespace\) имени. Спецификация засчитывает и каждый метод косвенного цикла (A вызывает B, B вызывает A); Qualimetrix — нет и не отслеживает вызов через переменную или callable.
Настройка¶
Сокращённая запись с threshold:
bin/qmx check src/ --rule-opt="complexity.cognitive:callable.warning=20"
bin/qmx check src/ --rule-opt="complexity.cognitive:callable.error=40"
warning: / error: — опции уровня, а не правила
Написанные прямо под complexity.cognitive: — вне callable: и class: —
они ничего не настраивают, и прогон теперь останавливается с кодом 3.
Вкладывайте их в тот уровень, к которому они относятся. У уровня
class: границы называются max_warning / max_error, то есть два
уровня принимают разные ключи; отказ перечисляет допустимое именно в
той позиции, где вы написали ключ.
NPath-сложность¶
Rule ID: complexity.npath
Судимые метрики: complexity.npath, complexity.npath.max
Что измеряет¶
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.
Пороговые значения¶
Уровень вызываемого объекта (Callable) (включён по умолчанию):
| Уровень | Порог | Серьёзность |
|---|---|---|
| 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, а решение, вложенное в ветвь, лишь добавляет свои пути к этой ветви: два
ifподряд дают 4, те же два вложенных — 3.
Особенности реализации¶
Qualimetrix следует Немеху (1988) с расширениями для PHP:
- Логические операторы в условиях: Каждый
&&/||в условии добавляет 1 к числу путей этого условия. Например,if ($a && $b || $c)даёт 4 пути (базовых 2 + 2 оператора). - Тернарный оператор: Вносит 2 базовых пути плюс любую сложность в подвыражениях.
??и??=: Каждый вносит 2 обычных пути выражения, как тернарный выбор значения.return $x ?? 5и$x ??= []имеют NPath 2; цепочка из двух coalesce имеет NPath 4.?->: Каждое nullsafe-звено добавляет 1 отдельно от обычных путей выражения. На границе statement в callable формулаmax(1, ordinary) + nullsafeсохраняет ровно один базовый путь statement.- Расширения для PHP:
match,foreach,??и?->обрабатываются как конструкции, порождающие пути. Слоты выражений вfor,foreach,switchиechoобходятся через прозрачные AST-обёртки. switchбезdefault: Вносит один путь «ни один case не совпал», какifбезelseвносит путь пропуска. Checkstyle использует ту же интерпретацию, а pdepend/PHPMD — нет.
Голые блоки { ... } и declare(...) { ... } сохраняют произведение путей
дочерних инструкций. Выражения в unset, динамическом global и
инициализаторах static тоже вносят пути. Арифметика насыщается на
1 000 000 000: опубликованное значение на потолке — нижняя граница, а не
точное число путей. Текстовая метка > 1M (extreme) обозначает категорию,
а не потолок насыщения.
Выражения match: Qualimetrix использует аддитивный подход, согласованный с оригинальной формулой Немеха для switch:
Каждая ветвь вносит как минимум один путь, поэтому простой match совпадает с
эквивалентным switch, если у него есть default. match без default не
добавляет путь несовпадения, потому что несовпавший match выбрасывает
исключение. Nullsafe-вызов или доступ добавляет свой условный путь, а
вложенные тернарные и логические операторы не теряются в обёртках. Некоторые другие
инструменты (в частности pdepend) используют мультипликативный подход для match, который может
давать экстремальные значения. Значения этих конструкций меняются относительно прежних
версий Qualimetrix; пороги и baseline могут потребовать перекалибровки.
Отклонение от оригинальной спецификации
Работа Немеха (1988) предшествует ??, ??= и ?->. Qualimetrix считает
?? и ??= тернарным выбором значения (+2 обычных пути), а ?-> добавляет
один отдельный nullsafe-путь на звено. Поэтому $x ?? $y ?? 5 даёт 4,
как вложенный тернарный $x ? 1 : ($y ? 2 : 3), хотя у каждого три
различимых исхода. Так сохраняется существующая рекуррентная формула
тернарного оператора, а не подсчёт различных исходов во время выполнения.
Обработка try/catch в NPath — осознанное расширение qmx. Qualimetrix считает
(NPath(try) + Σ NPath(catch) + 1) × NPath(finally): +1 — это аддитивный член
в стиле PMD для пути без исключения. Поэтому голый try{}catch{} даёт NPath 3,
тогда как оригинальная схема Немеха (1988) — которая появилась до исключений —
подразумевала бы 2. Это отличается от phpmd/pdepend и сделано намеренно.
Настройка¶
# qmx.yaml
rules:
complexity.npath:
callable:
warning: 300
error: 2000
class:
enabled: false # disabled by default
Сокращённая запись с threshold:
rules:
complexity.npath:
callable:
threshold: 300 # warning=300, error=300 → все нарушения становятся ошибками
bin/qmx check src/ --rule-opt="complexity.npath:callable.warning=300"
bin/qmx check src/ --rule-opt="complexity.npath:class.enabled=true"
warning: / error: — опции уровня, а не правила
Написанные прямо под complexity.npath: — вне callable: и class: —
они ничего не настраивают, и прогон теперь останавливается с кодом 3.
Вкладывайте их в тот уровень, к которому они относятся. У уровня
class: границы называются max_warning / max_error, то есть два
уровня принимают разные ключи; отказ перечисляет допустимое именно в
той позиции, где вы написали ключ.
WMC -- Взвешенные методы класса¶
Rule ID: complexity.wmc
Судимая метрика: 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 вычисляется как сумма цикломатической сложности всех методов класса. Коллектор Complexity формирует её на точном объявлении класса, включая 0 у класса без методов. Хуки свойств и методы анонимного класса в неё не входят. Правило Complexity проверяет её и может исключать data-классы. Поскольку Qualimetrix использует вариант CCN2+ (который учитывает ?? и ?-> как точки принятия решений), значения WMC будут соответственно выше, чем у других инструментов.
Настройка¶
Сокращённая запись с threshold:
bin/qmx check src/ --rule-opt="complexity.wmc:warning=60"
bin/qmx check src/ --rule-opt="complexity.wmc:error=100"
Сокращение слоя и class activation¶
Верхнее сокращение CCN/cognitive/NPath меняет callable и сохраняет class band. В одном слое конфликтуют явные записи в те же раскрытые листья; независимые настройки, например callable.enabled, сохраняются. Разные слои сливают раскрытые листья. Написанная class-пара NPath действует без лишнего true, class.enabled:false выключает её. Integer bands отказывают на дробных значениях; итоговый порядок учитывает default halves. Academic algorithms и default numbers не меняются. См. формы конфигурации.