Правила запахов кода (Code Smell)¶
Запахи кода -- это паттерны, которые указывают на возможные проблемы. Они не обязательно являются багами, но обозначают места, где код можно улучшить. Эти правила обнаруживают распространенные плохие практики, которых обычно стоит избегать.
Все правила запахов кода можно включать и выключать по отдельности.
Сбор доказательств, декодирование вхождений и правила находятся вместе в
возможности Analysis\Evidence\CodeSmell. Идентификаторы правил и конфигурация
не изменяются.
Находки внутри именованных объявлений несут символ класса, метода или функции; доказательства на уровне файла и анонимного объявления сохраняют файловый символ и null-пространство имён.
Имена функций сравниваются так, как они написаны в вызове. Импорты не
разрешаются, поэтому функция, вызванная через псевдоним (use function var_dump
as vd; vd($x);), не распознаётся правилами, которые ищут функцию по имени
(debug-code, count-in-loop).
Булевые аргументы (Boolean Arguments)¶
Идентификатор правила: code-smell.boolean-argument
Серьезность: Warning
Что измеряет¶
Обнаруживает методы, принимающие параметры типа bool. Булевый аргумент обычно означает, что метод делает две разные вещи в зависимости от флага, что нарушает принцип единственной ответственности.
Доказательство -- объявленный тип: bool, ?bool или объединение, содержащее bool. Параметр без типа не помечается, даже с булевым значением по умолчанию ($overwrite = false) или PHPDoc @param bool.
Промотированные свойства конструктора (public function __construct(public bool $x)) по умолчанию не помечаются: промотированный параметр объявляет поле, а не переключатель поведения, поэтому рекомендация «разделить на два метода» к нему неприменима. Установите flag_promoted_properties: true, чтобы помечать и их тоже.
Пример¶
// Плохо: что означает `true`? Трудно читать в месте вызова.
$user->save(true);
// Сигнатура метода раскрывает проблему:
public function save(bool $sendNotification): void
{
// ...сохраняет пользователя...
if ($sendNotification) {
// ...отправляет уведомление...
}
}
Как исправить¶
-
Разделите на два метода с описательными именами:
-
Используйте enum, если вариантов больше двух:
Конфигурация¶
# qmx.yaml
rules:
code-smell.boolean-argument:
allowed_prefixes: [is, has, can, should, will, did, was] # по умолчанию
flag_promoted_properties: false # по умолчанию
Булевые параметры, имена которых начинаются с одного из этих префиксов, считаются самодокументируемыми и не помечаются. Например, $isActive, $hasPermission или $is_active (snake_case) будут пропущены с настройками по умолчанию. Установите [], чтобы помечать все булевые параметры.
Одна строка означает список из одного элемента: allowed_prefixes: is — это ровно allowed_prefixes: [is]. Строка из цифр — такой же префикс, как любой другой: allowed_prefixes: "2" — это список ["2"].
flag_promoted_properties определяет, помечаются ли промотированные свойства конструктора (public bool $x) как обычные булевые аргументы. По умолчанию false, так как промотированный параметр — это объявление поля, а не переключатель поведения.
# CLI-дверь несёт один скаляр, поэтому здесь задаётся единственный префикс `is` —
# текст после `=` не разбивается. Несколько префиксов пишутся списком в qmx.yaml.
bin/qmx check src/ --rule-opt="code-smell.boolean-argument:allowed_prefixes=is"
bin/qmx check src/ --rule-opt="code-smell.boolean-argument:flag_promoted_properties=true"
count() в цикле (count() in Loop)¶
Идентификатор правила: code-smell.count-in-loop
Серьезность: Warning
Что измеряет¶
Обнаруживает вызовы count() (или sizeof()) внутри условий цикла. Когда count() находится в условии цикла, он пересчитывается на каждой итерации, что расточительно.
Пример¶
// Плохо: count() выполняется на каждой итерации
for ($i = 0; $i < count($items); $i++) {
processItem($items[$i]);
}
Как исправить¶
Сохраните результат count() в переменную перед циклом:
// Хорошо: count() выполняется один раз
$itemCount = count($items);
for ($i = 0; $i < $itemCount; $i++) {
processItem($items[$i]);
}
// Еще лучше: используйте foreach, когда возможно
foreach ($items as $item) {
processItem($item);
}
Отладочный код (Debug Code)¶
Идентификатор правила: code-smell.debug-code
Серьезность: Error
Что измеряет¶
Обнаруживает отладочные функции, оставленные в коде: var_dump(), print_r(), var_export(), dd(), dump(), debug_print_backtrace() и debug_zval_dump().
Режим возврата (var_export($value, true), print_r($value, return: true)) освобождает от находки только print_r() и var_export(). Позиционный или именованный true не освобождает var_dump(), dd() и dump(). Вызовы внутри методов отладочного API, например dump() или debug(), тоже помечаются; для намеренного вызова используйте @qmx-ignore code-smell.debug-code с причиной.
debug_backtrace() не помечается: он возвращает трассировку, а не печатает её, и часто используется в обработке ошибок.
Пример¶
public function processPayment(Order $order): void
{
var_dump($order); // забытый отладочный вывод
print_r($order->items); // забытый отладочный вывод
// основная логика...
}
Как исправить¶
Удалите все отладочные вызовы перед коммитом. Если нужно инспектировать данные:
- Используйте настоящий логгер (
$this->logger->debug(...)) - Используйте отладчик (Xdebug)
- Используйте инструменты профилирования
Внимание
Отладочный вывод может раскрыть конфиденциальную информацию (пароли, токены, персональные данные) конечным пользователям. Поэтому правило имеет уровень Error, а не Warning.
Пустой catch (Empty Catch)¶
Идентификатор правила: code-smell.empty-catch
Серьезность: Error
Что измеряет¶
Обнаруживает блоки catch, которые полностью пусты -- перехватывают исключение и не делают с ним абсолютно ничего. Это беззвучно проглатывает ошибки, делая баги крайне трудными для диагностики.
catch, в котором есть только комментарий, всё равно пуст: комментарий объясняет молчание, но не прекращает его.
Цепочка попыток. Одна форма не помечается: foreach, который перебирает элементы, пока один не сработает. try должен быть непосредственной инструкцией тела цикла и должен уметь завершить поиск после успешной работы: в нём есть return или break либо continue, пропускающий инструкции после try, -- на верхнем уровне try или внутри его веток if:
foreach ($this->resolvers as $resolver) {
try {
return $resolver->resolve($key); // успех завершает поиск
} catch (ResolverException $e) {
// неудача означает «попробовать следующий»
}
}
try, вложенный глубже в тело цикла, try внутри замыкания и try, из которого есть только continue без инструкций после try (и поэтому он ничего не пропускает), помечаются как обычно.
Непосредственный if верхнего уровня в try считается ранней проверкой,
только если его ветка then содержит ровно один простой continue, а позже в том же
try выполняется работа; остальные ветки сохраняют обычную семантику попытки.
break, return, дополнительная инструкция в основной ветке и проверка,
размещённая в else/elseif или вложенном if, этим правилом ранними не считаются: каждая
такая форма может завершать успешную попытку. Остальные условия исключения
по-прежнему действуют, поэтому такая ветка без подготовительной работы перед ней
оценивается как обычно.
Пример¶
// Плохо: исключение беззвучно игнорируется
try {
$this->sendEmail($user);
} catch (\Exception $e) {
// ничего -- если отправка не удастся, никто не узнает
}
Как исправить¶
-
Залогируйте исключение:
-
Перебросьте как доменное исключение:
-
Обработайте ошибку явно, если она ожидаема и восстановима.
-
Подавите находку с причиной, если исключение игнорируется намеренно: комментарий в
catchнаходку не снимает, а@qmx-ignore code-smell.empty-catch -- <причина>снимает.
Подавление ошибок (Error Suppression)¶
Идентификатор правила: code-smell.error-suppression
Серьезность: Warning
Что измеряет¶
Обнаруживает использование оператора подавления ошибок @. Оператор @ скрывает ошибки и предупреждения PHP, затрудняя поиск и исправление проблем.
Пример¶
// Плохо: если файл не существует, вы не узнаете, почему дальше что-то ломается
$data = @file_get_contents('/path/to/file');
// Плохо: подавление предупреждений от функции
$result = @json_decode($input);
Как исправить¶
Обрабатывайте ошибки явно:
// Хорошо: проверяем перед вызовом
if (!file_exists($path)) {
throw new FileNotFoundException($path);
}
$data = file_get_contents($path);
// Хорошо: используем функции обработки ошибок
$result = json_decode($input, flags: JSON_THROW_ON_ERROR);
Конфигурация¶
# qmx.yaml
rules:
code-smell.error-suppression:
allowed_functions: # по умолчанию: []
- fopen
- file_get_contents
- unlink
Функции, для которых подавление ошибок через @ допустимо. Некоторые функции ввода-вывода возвращают false и генерируют предупреждение при ошибке -- использование @ с ними является распространённой практикой, когда возвращаемое значение проверяется явно. По умолчанию список пуст.
Одна строка означает список из одного элемента: allowed_functions: fopen — это ровно allowed_functions: [fopen]. Строка из цифр — такое же имя, как любое другое.
# CLI-дверь несёт один скаляр, поэтому здесь разрешается единственная функция `fopen` —
# текст после `=` не разбивается. Несколько имён пишутся списком в qmx.yaml.
bin/qmx check src/ --rule-opt="code-smell.error-suppression:allowed_functions=fopen"
eval()¶
Идентификатор правила: code-smell.eval
Серьезность: Error
Что измеряет¶
Обнаруживает использование eval(), которая выполняет произвольный PHP-код из строки. Это серьезный риск безопасности: если пользовательский ввод достигнет eval(), злоумышленник может выполнить любой код на вашем сервере.
Пример¶
// Плохо: уязвимость безопасности
$formula = $_GET['formula'];
$result = eval("return $formula;");
// Плохо: даже с "безопасным" вводом eval трудно отлаживать и поддерживать
eval('$config = ' . var_export($data, true) . ';');
Как исправить¶
- Используйте замыкания или вызываемые объекты вместо генерации кода в виде строк.
- Используйте паттерн Strategy вместо динамического выполнения кода.
- Используйте
json_decode()для парсинга данных, а неeval(). - Используйте шаблонизаторы для генерации динамического вывода.
exit() / die()¶
Идентификатор правила: code-smell.exit
Серьезность: Warning
Что измеряет¶
Обнаруживает использование exit() и die(), включая полностью квалифицированный вызов функции \exit() / \die(), появившийся в PHP 8.4. Эти функции немедленно завершают весь PHP-процесс, что:
- Предотвращает правильную обработку ошибок
- Делает код нетестируемым (PHPUnit не может перехватить
exit) - Обходит обработчики завершения и деструкторы
Пример¶
// Плохо: нетестируемо, нет обработки ошибок
if (!$user->isAdmin()) {
die('Access denied');
}
// Плохо: предотвращает правильную обработку ответа
if ($error) {
exit(1);
}
Как исправить¶
Бросайте исключения вместо этого:
// Хорошо: можно перехватить, протестировать и обработать
if (!$user->isAdmin()) {
throw new AccessDeniedException('Access denied');
}
Примечание
exit() допустим в точках входа CLI (например, bin/console), где он устанавливает код выхода процесса. Это правило в основном касается использования exit/die внутри логики приложения.
goto¶
Идентификатор правила: code-smell.goto
Серьезность: Error
Что измеряет¶
Обнаруживает использование оператора goto. goto делает поток управления непредсказуемым -- читателю приходится искать целевую метку, которая может быть в любом месте функции. Это делает код очень трудным для чтения и отладки.
Пример¶
// Плохо: спагетти-поток управления
function process(array $items): void
{
foreach ($items as $item) {
if ($item->isInvalid()) {
goto cleanup;
}
// обработка элемента...
}
cleanup:
// код очистки
}
Как исправить¶
Используйте стандартные конструкции управления потоком:
// Хорошо: понятный поток управления
function process(array $items): void
{
foreach ($items as $item) {
if ($item->isInvalid()) {
$this->cleanup();
return;
}
// обработка элемента...
}
}
Используйте циклы, функции, ранние возвраты или исключения -- все они выражают намерение яснее, чем goto.
Суперглобальные переменные (Superglobals)¶
Идентификатор правила: code-smell.superglobals
Серьезность: Warning
Что измеряет¶
Обнаруживает прямой доступ к суперглобальным переменным PHP: $_GET, $_POST, $_REQUEST, $_SERVER, $_SESSION, $_COOKIE, $_FILES, $_ENV и $GLOBALS. Литеральные имена переменных переменных (${'_GET'}, ${'_'.'GET'}) и литеральный доступ $GLOBALS['_GET'] тоже обнаруживаются. Неизвестное динамическое имя вроде $$name определить нельзя.
Прямой доступ к суперглобальным переменным создает скрытые зависимости от глобального состояния, делая код трудным для тестирования и непредсказуемым.
Пример¶
// Плохо: жестко связан с глобальным состоянием
class UserController
{
public function register(): void
{
$name = $_POST['name'];
$email = $_POST['email'];
$ip = $_SERVER['REMOTE_ADDR'];
// ...
}
}
Как исправить¶
Используйте внедрение зависимостей с объектами запроса:
// Хорошо: зависимости явные и тестируемые
class UserController
{
public function register(ServerRequestInterface $request): ResponseInterface
{
$body = $request->getParsedBody();
$name = $body['name'];
$email = $body['email'];
$ip = $request->getServerParams()['REMOTE_ADDR'];
// ...
}
}
Распространенные абстракции запросов:
- PSR-7:
ServerRequestInterface(фреймворко-независимый) - Symfony:
Requestиз HttpFoundation - Laravel:
Illuminate\Http\Request
Избыточное внедрение в конструктор (Constructor Over-injection)¶
Идентификатор правила: code-smell.constructor-overinjection
Судимая метрика: code-smell.parameter-count
Что измеряет¶
Обнаруживает конструкторы со слишком большим количеством внедрённых зависимостей. Длинный список параметров конструктора в кодовых базах с DI -- прямой сигнал нарушения принципа единственной ответственности (SRP): класс имеет слишком много коллабораторов.
Конструктор value object несёт поля, а не зависимости, и этим правилом не судится: его судит code-smell.long-parameter-list с VO-порогами. JSON-отчёт перечисляет такие конструкторы в abstentions с гейтом value-object-constructor.
У каждого конструктора ровно один владелец. Это правило владеет каждым __construct класса, абстрактного класса, интерфейса, трейта или анонимного класса (имя сравнивается без учёта регистра), который не является конструктором value object.
Если отключить это правило или исключить его через --only-rule, его конструкторы остаются несудимыми: code-smell.long-parameter-list их не подхватывает. Глобальная функция с именем __construct не принадлежит классу и остаётся у long-parameter-list.
Пороговые значения¶
| Значение | Серьёзность | Что означает |
|---|---|---|
| 8 | Warning | Слишком много зависимостей, рассмотрите разделение |
| 12+ | Error | Класс явно нарушает SRP |
Пример¶
// Плохо: конструктор зависит от слишком многих сервисов
class OrderProcessor
{
public function __construct(
private UserRepository $users,
private ProductRepository $products,
private InventoryService $inventory,
private PricingService $pricing,
private TaxCalculator $tax,
private ShippingService $shipping,
private NotificationService $notifications,
private AuditLogger $audit,
private MetricsCollector $metrics,
) {}
}
Как исправить¶
- Разделите класс на меньшие, сфокусированные сервисы (например,
OrderValidator,OrderFulfiller,OrderNotifier). - Используйте Фасад или Медиатор для группировки связанных зависимостей за единым интерфейсом.
- Внедрите Command Bus -- вместо прямого внедрения обработчиков отправляйте команды.
Конфигурация¶
Простой порог вместо раздельных уровней warning/error
(threshold нельзя сочетать с warning или error — смешение считается
ошибкой конфигурации, и прогон останавливается с кодом 3):
rules:
code-smell.constructor-overinjection:
threshold: 8 # warning=8, error=8 → все нарушения становятся ошибками
Длинный список параметров (Long Parameter List)¶
Идентификатор правила: code-smell.long-parameter-list
Судимая метрика: code-smell.parameter-count
Что измеряет¶
Обнаруживает методы и функции со слишком большим количеством параметров. Длинный список параметров затрудняет правильный вызов метода, его тестирование и часто указывает на то, что метод делает слишком много. Рассмотрите использование объекта параметров или разделение метода.
Из всех конструкторов это правило судит только конструкторы value object. Любой другой конструктор принадлежит code-smell.constructor-overinjection, поэтому один конструктор не попадает в отчёт дважды; JSON-отчёт перечисляет его в abstentions с гейтом injection-constructor. Отключение constructor-overinjection оставляет такие конструкторы несудимыми. Глобальная функция с именем __construct — функция, а не конструктор, и судится стандартными порогами.
Пороговые значения¶
Стандартные пороги (методы и функции, кроме конструкторов):
| Значение | Серьёзность | Что означает |
|---|---|---|
| 4 | Warning | Стоит сгруппировать параметры |
| 6+ | Error | Слишком много параметров, необходим рефакторинг |
Пороги для VO конструкторов (readonly класс, все promoted, пустое тело, состояние открыто):
| Значение | Серьёзность | Что означает |
|---|---|---|
| 8 | Warning | Стоит разделить value object |
| 12+ | Error | Слишком много полей, разделите value object |
Исключение для конструкторов Value Object¶
Readonly классы с promoted-параметрами конструктора (PHP 8.2+) рассматриваются как Value Objects.
readonly class с 6 promoted-свойствами и без логики в теле — это корректный дизайн, типизированный
контейнер данных, а не признак плохой декомпозиции. Для таких конструкторов используются отдельные,
повышенные пороговые значения.
Критерии определения VO (все должны быть истинны):
- Класс имеет модификатор
readonly - Все параметры конструктора — promoted-свойства (имеют модификатор видимости,
readonlyили асимметричной записи) - Тело конструктора пустое (нет операторов)
- Класс открывает хотя бы одно из этих свойств: оно читается публично (
publicили толькоreadonly/private(set)), либо метод самого класса возвращает его ровно какreturn $this->name;
Если любое условие не выполнено, конструктор не считается конструктором value object: его судит code-smell.constructor-overinjection со своими порогами (8/12), а long-parameter-list воздерживается.
Четвёртый критерий отделяет контейнер данных от readonly-сервиса. final readonly class, все
promoted-свойства которого приватны и нигде не возвращаются, хранит внедрённые зависимости, а не данные,
и судится как любой другой конструктор. Геттер, возвращающий производное значение ($this->name->value,
['name' => $this->name]), открытием не считается; как и return внутри замыкания или стрелочной
функции — так сервисы передают fn() => $this->dependency ленивым фабрикам — и геттер из подключённого
трейта. Value object, чьё состояние доступно только так, судится code-smell.constructor-overinjection. @qmx-threshold code-smell.long-parameter-list на конструкторе, которым владеет то правило, нечего настраивать: используйте там code-smell.constructor-overinjection.
Пример¶
// Плохо: слишком много параметров, трудно запомнить порядок
public function createUser(
string $name,
string $email,
string $phone,
string $address,
string $city,
string $country,
string $zipCode,
): User {
// ...
}
// OK: readonly VO с promoted-свойствами использует повышенные пороги
final readonly class CreateUserRequest
{
public function __construct(
public string $name,
public string $email,
public string $phone,
public string $address,
public string $city,
public string $country,
public string $zipCode, // 7 параметров — ниже vo-warning=8
) {}
}
Как исправить¶
-
Используйте объект параметров (DTO):
-
Разделите метод, если параметры относятся к разным ответственностям.
Конфигурация¶
# qmx.yaml
rules:
code-smell.long-parameter-list:
warning: 4 # методы и функции
error: 6
vo-warning: 8 # readonly VO конструкторы
vo-error: 12
bin/qmx check src/ --rule-opt="code-smell.long-parameter-list:warning=5"
bin/qmx check src/ --rule-opt="code-smell.long-parameter-list:error=8"
bin/qmx check src/ --rule-opt="code-smell.long-parameter-list:vo-warning=10"
bin/qmx check src/ --rule-opt="code-smell.long-parameter-list:vo-error=15"
Простой порог вместо раздельных уровней warning/error, независимо на каждой из осей:
rules:
code-smell.long-parameter-list:
threshold: 6 # warning=6, error=6 → все нарушения для обычных методов становятся ошибками
vo_threshold: 12 # warning=12, error=12 → все нарушения для VO-конструкторов становятся ошибками
bin/qmx check src/ --rule-opt="code-smell.long-parameter-list:threshold=6"
bin/qmx check src/ --rule-opt="code-smell.long-parameter-list:vo_threshold=12"
Идентичные подвыражения (Identical Sub-expression)¶
Идентификатор правила: code-smell.identical-subexpression
Серьезность: Warning
Что измеряет¶
Обнаруживает идентичные подвыражения, указывающие на ошибки копирования или логические баги. Правило выявляет пять паттернов:
- Одинаковые операнды в бинарных операциях (
identical_operands) -- одно и то же выражение по обе стороны оператора (например,$a === $a,$a - $a,$a && $a). - Дублирующиеся условия в цепочках if/elseif (
duplicate_condition) -- одно и то же условие проверяется дважды, что делает вторую ветку мёртвым кодом.else, в котором нет ничего, кроме другогоif(else ifилиelse { if (...) }), продолжает цепочку. - Одинаковые ветки тернарного оператора (
identical_ternary) -- тернарный оператор, в котором ветки «истина» и «ложь» совпадают, что делает условие бессмысленным. - Дублирующиеся условия в match (
duplicate_match_arm) -- повторяющиеся условия в выраженииmatch, где выполнится только первый вариант. - Дублирующиеся case в switch (
duplicate_switch_case) -- одно и то же значениеcaseповторяется вswitch, где выполнится только первый вариант.
Операторы с легитимным использованием одинаковых операндов не помечаются: +, *, **, ., &, |, <<, >>.
Побочные эффекты учитываются по-разному для разных паттернов:
- Операнды и ветки тернарного оператора с побочными эффектами (вызовы функций и методов, присваивания, инкременты и т.д.) исключаются, так как два вычисления могут дать разные результаты.
- Условия (паттерны 2, 4 и 5) сравниваются вместе с вызовами: повторный вызов в цепочке условий почти всегда ошибка копирования, как в примере с
$order->isPaid()ниже. Цепочка, которая намеренно повторяет вызов с побочным эффектом ($stack->pop()), помечается, и её нужно подавить inline-директивой с причиной.
Пример¶
class OrderService
{
public function validate(Order $order): bool
{
// Плохо: одинаковые операнды -- всегда true, вероятно опечатка
if ($order->total === $order->total) {
// ...
}
// Плохо: вычитание значения из самого себя -- всегда 0
$diff = $order->price - $order->price;
// Плохо: дублирующееся условие -- вторая ветка -- мёртвый код
if ($order->isPaid()) {
return true;
} elseif ($order->isPaid()) {
return false;
}
// Плохо: одинаковые ветки тернарного оператора -- условие бессмысленно
$status = $order->isActive() ? 'pending' : 'pending';
// Плохо: дублирующееся условие match
return match ($order->type) {
'retail' => $this->handleRetail($order),
'wholesale' => $this->handleWholesale($order),
'retail' => $this->handleSpecial($order), // никогда не выполнится
};
}
}
Как исправить¶
Это почти всегда баги -- проверьте каждое вхождение и исправьте предполагаемую логику:
-
Одинаковые операнды: Одна из сторон обычно является опечаткой. Замените на правильную переменную:
-
Дублирующиеся условия: Удалите дублирующую ветку или исправьте условие:
-
Одинаковые ветки тернарного оператора: Либо условие не нужно, либо одна из веток содержит неверное значение:
-
Дублирующиеся условия match: Удалите дубликат или исправьте значение условия.
-
Дублирующиеся case в switch: Удалите недостижимый
caseили исправьте его значение.
Неиспользуемые приватные члены (Unused Private Members)¶
Идентификатор правила: code-smell.unused-private
Судимая метрика: code-smell.unused-private.total
Серьезность: Warning
Что измеряет¶
Обнаруживает приватные методы, свойства и константы, которые объявлены, но нигде не используются внутри класса. Неиспользуемые приватные члены -- это мёртвый код: они создают шум, увеличивают когнитивную нагрузку и могут указывать на незавершённый рефакторинг.
Правило учитывает граничные случаи:
- Магические методы: классы с
__call/__callStaticпропускают проверку методов; классы с__get/__setпропускают проверку свойств. Магический метод учитывается при любой видимости и в любом регистре, а также когда его объявляет трейт из того же файла - Промоутнутые свойства: свойства, продвигаемые через конструктор, отслеживаются корректно
- Анонимные классы: приватные члены анонимных классов изолированы и не «утекают» в родительский класс
- Анализируемые типы: классы и перечисления; интерфейсы и трейты не анализируются
- Паттерны доступа: распознаёт
$this->method(),self::method(),static::method(), доступ к свойствам и константам. Имена методов сравниваются без учёта регистра, как их разрешает PHP; имена свойств и констант регистрозависимы - Callable: литеральный callable-массив считается использованием метода:
[$this, 'method'],[self::class, 'method'],[static::class, 'method'],[__CLASS__, 'method'], в том числе внутриClosure::fromCallable()илиarray_map(). Литеральная строка с именем того же класса, например'self::method'или'App\Subject::method', тоже считается использованием. Метод, названный переменной ([$this, $name],$this->$name()), остаётся неизвестным; короткая строка'Subject::method'не разрешается через текущее пространство имён - Рекурсия: приватный метод, который вызывается только из собственного тела, считается неиспользуемым; callable-массив с именем метода внутри его собственного тела — тоже ссылка на себя
- Разрешение трейтов: вызовы методов, определённых в трейтах, используемых тем же классом (в том же файле), распознаются, что снижает количество ложных срабатываний
Пример¶
class OrderService
{
private string $unusedField = ''; // нигде не читается и не записывается
private const LEGACY_LIMIT = 100; // нигде не используется
public function process(Order $order): void
{
// ... использует $order, но не обращается к $unusedField и LEGACY_LIMIT
}
private function oldHelper(): void // нигде не вызывается
{
// осталось от предыдущей реализации
}
}
Как исправить¶
- Удалите неиспользуемый член, если это действительно мёртвый код.
- Измените видимость на
protectedилиpublic, если член используется подклассами или внешним кодом. - Если член сохранён намеренно, поставь
@qmx-ignore code-smell.unused-privateна сам член. Подавление достигает лишь находок на его строках; продвигаемый параметр также достигает соответствующей находки свойства. См. границы члена.
Недостижимый код (Unreachable Code)¶
Идентификатор правила: code-smell.unreachable-code
Судимая метрика: code-smell.unreachable-code
Что измеряет¶
Обнаруживает код, который никогда не может быть выполнен, потому что находится после терминального оператора (return, throw, exit/die, continue, break, goto). Мёртвый код добавляет шум, сбивает с толку читателей и может указывать на логическую ошибку.
Каждый список инструкций метода, функции, замыкания или хука свойства проверяется отдельно: само тело и каждый вложенный блок в if/elseif/else, циклах, try/catch/finally, ветках switch и простых блоках. Мёртвая инструкция считается один раз, без заглядывания внутрь, а метка goto снова делает достижимыми инструкции после неё. break после return в ветке switch тоже недостижим и учитывается.
Терминальность определяется для каждого списка инструкций отдельно, без анализа потока между ветками: код после if/else, у которого каждая ветка завершается return, не помечается.
Пример¶
public function process(Order $order): string
{
if ($order->isPaid()) {
return 'processed';
}
return 'pending';
// Плохо: этот код никогда не выполнится
$this->logger->info('Processing complete');
$this->notify($order);
}
Как исправить¶
Удалите недостижимый код. Если код должен был выполняться, исправьте поток управления:
public function process(Order $order): string
{
if ($order->isPaid()) {
$this->logger->info('Processing complete');
$this->notify($order);
return 'processed';
}
return 'pending';
}
Конфигурация¶
bin/qmx check src/ --rule-opt="code-smell.unreachable-code:warning=1"
bin/qmx check src/ --rule-opt="code-smell.unreachable-code:error=1"
Простой порог вместо раздельных уровней warning/error
(threshold нельзя сочетать с warning или error — смешение считается
ошибкой конфигурации, и прогон останавливается с кодом 3):
rules:
code-smell.unreachable-code:
threshold: 1 # warning=1, error=1 → все нарушения становятся ошибками
Конфигурация¶
Все правила запахов кода имеют одинаковую простую конфигурацию -- просто включить или выключить:
# qmx.yaml
rules:
code-smell.boolean-argument:
enabled: true
allowed_prefixes: [is, has, can, should, will, did, was]
flag_promoted_properties: false
code-smell.debug-code:
enabled: true
code-smell.empty-catch:
enabled: true
code-smell.eval:
enabled: false # выключить, если есть легитимное использование eval
code-smell.exit:
enabled: true
code-smell.goto:
enabled: true
code-smell.superglobals:
enabled: true
code-smell.constructor-overinjection:
warning: 8
error: 12
code-smell.count-in-loop:
enabled: true
code-smell.error-suppression:
enabled: true
allowed_functions: []
code-smell.long-parameter-list:
warning: 4
error: 6
code-smell.unreachable-code:
warning: 1
error: 1
code-smell.unused-private:
enabled: true
code-smell.identical-subexpression:
enabled: true
Также можно отключить отдельные правила через опцию --disable-rule в CLI:
# Отключить конкретное правило
bin/qmx check src/ --disable-rule=code-smell.exit
# Отключить все правила запахов кода сразу (wildcard-сопоставление; захватывает только потомков, не сам "code-smell")
bin/qmx check src/ --disable-rule=code-smell.*
Авторское включение и формы¶
Написанный enabled:true может снять disable нижнего пресета; сохрани его, если это намеренно. Пустые maps сохраняют нижние options, а true правила пишет только enabled и не сбрасывает severity/options. Malformed lower writes отказывают до discovery. Code-smell algorithms и fixed severities не меняются. См. конфигурацию.