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

Правила запахов кода (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) {
        // ...отправляет уведомление...
    }
}

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

  1. Разделите на два метода с описательными именами:

    public function save(): void { /* ... */ }
    public function saveAndNotify(): void { /* ... */ }
    
  2. Используйте enum, если вариантов больше двух:

    enum SaveMode {
        case Silent;
        case WithNotification;
        case WithAuditLog;
    }
    
    public function save(SaveMode $mode = SaveMode::Silent): void { /* ... */ }
    

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

# 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) {
    // ничего -- если отправка не удастся, никто не узнает
}

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

  1. Залогируйте исключение:

    try {
        $this->sendEmail($user);
    } catch (\Exception $e) {
        $this->logger->error('Failed to send email', ['exception' => $e]);
    }
    
  2. Перебросьте как доменное исключение:

    try {
        $this->sendEmail($user);
    } catch (\Exception $e) {
        throw new NotificationFailedException('Email sending failed', previous: $e);
    }
    
  3. Обработайте ошибку явно, если она ожидаема и восстановима.

  4. Подавите находку с причиной, если исключение игнорируется намеренно: комментарий в 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,
    ) {}
}

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

  1. Разделите класс на меньшие, сфокусированные сервисы (например, OrderValidator, OrderFulfiller, OrderNotifier).
  2. Используйте Фасад или Медиатор для группировки связанных зависимостей за единым интерфейсом.
  3. Внедрите Command Bus -- вместо прямого внедрения обработчиков отправляйте команды.

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

# qmx.yaml
rules:
  code-smell.constructor-overinjection:
    warning: 8
    error: 12
bin/qmx check src/ --rule-opt="code-smell.constructor-overinjection:warning=6"

Простой порог вместо раздельных уровней warning/error (threshold нельзя сочетать с warning или error — смешение считается ошибкой конфигурации, и прогон останавливается с кодом 3):

rules:
  code-smell.constructor-overinjection:
    threshold: 8   # warning=8, error=8 → все нарушения становятся ошибками
bin/qmx check src/ --rule-opt="code-smell.constructor-overinjection:threshold=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 (все должны быть истинны):

  1. Класс имеет модификатор readonly
  2. Все параметры конструктора — promoted-свойства (имеют модификатор видимости, readonly или асимметричной записи)
  3. Тело конструктора пустое (нет операторов)
  4. Класс открывает хотя бы одно из этих свойств: оно читается публично (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
    ) {}
}

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

  1. Используйте объект параметров (DTO):

    final readonly class CreateUserRequest
    {
        public function __construct(
            public string $name,
            public string $email,
            public string $phone,
            public Address $address,
        ) {}
    }
    
    public function createUser(CreateUserRequest $request): User { /* ... */ }
    
  2. Разделите метод, если параметры относятся к разным ответственностям.

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

# 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

Что измеряет

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

  1. Одинаковые операнды в бинарных операциях (identical_operands) -- одно и то же выражение по обе стороны оператора (например, $a === $a, $a - $a, $a && $a).
  2. Дублирующиеся условия в цепочках if/elseif (duplicate_condition) -- одно и то же условие проверяется дважды, что делает вторую ветку мёртвым кодом. else, в котором нет ничего, кроме другого if (else if или else { if (...) }), продолжает цепочку.
  3. Одинаковые ветки тернарного оператора (identical_ternary) -- тернарный оператор, в котором ветки «истина» и «ложь» совпадают, что делает условие бессмысленным.
  4. Дублирующиеся условия в match (duplicate_match_arm) -- повторяющиеся условия в выражении match, где выполнится только первый вариант.
  5. Дублирующиеся 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),  // никогда не выполнится
        };
    }
}

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

Это почти всегда баги -- проверьте каждое вхождение и исправьте предполагаемую логику:

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

    // Было: $order->total === $order->total (всегда true)
    // Исправление: сравнить с ожидаемым значением
    if ($order->total === $order->expectedTotal) {
        // ...
    }
    
  2. Дублирующиеся условия: Удалите дублирующую ветку или исправьте условие:

    if ($order->isPaid()) {
        return true;
    } elseif ($order->isRefunded()) {
        return false;
    }
    
  3. Одинаковые ветки тернарного оператора: Либо условие не нужно, либо одна из веток содержит неверное значение:

    $status = $order->isActive() ? 'active' : 'pending';
    
  4. Дублирующиеся условия match: Удалите дубликат или исправьте значение условия.

  5. Дублирующиеся 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';
}

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

# qmx.yaml
rules:
  code-smell.unreachable-code:
    warning: 1
    error: 1
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 → все нарушения становятся ошибками
bin/qmx check src/ --rule-opt="code-smell.unreachable-code:threshold=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 не меняются. См. конфигурацию.