Правила дублирования кода¶
Дублирование кода -- один из самых распространённых источников технического долга. Когда одна и та же логика существует в нескольких местах, каждое исправление бага или добавление функциональности должно быть применено ко всем копиям -- и неизбежно какие-то копии будут пропущены. Эти правила обнаруживают структурно идентичные блоки кода по всей кодовой базе с помощью анализа потока токенов.
Дублирование кода (Code Duplication)¶
Идентификатор правила: duplication.code-duplication
Что измеряет¶
Обнаруживает дублированные блоки кода между файлами с использованием хеширования потока токенов. Алгоритм работает в три этапа:
- Токенизация -- PHP-код разбирается в поток токенов.
- Нормализация -- токены трансформируются для игнорирования косметических различий: переменные заменяются на
$_, строковые литералы на'_', числа на0, пробелы и комментарии удаляются. Имена функций, методов и классов сохраняются, поэтому помечается только структурно идентичный код с разными именами переменных. - Обнаружение -- дублированные последовательности находятся с помощью скользящего хеша (алгоритм Рабина-Карпа). Блоки короче минимальных пороговых значений игнорируются.
Представьте это как сравнение рецептов: если два рецепта содержат одни и те же шаги в том же порядке, но с разными названиями ингредиентов -- это дубликаты.
Пороговые значения¶
| Значение | Серьёзность | Значение |
|---|---|---|
| < 50 дублированных строк | Warning | Заметное дублирование, стоит подумать об извлечении общей логики |
| >= 50 дублированных строк | Error | Значительное дублирование, рефакторинг настоятельно рекомендован |
Минимальный размер блока (настраивается):
| Опция | По умолчанию | Значение |
|---|---|---|
min_lines |
5 | Минимальное количество строк для проверки блока |
min_tokens |
70 | Минимальное количество токенов для пометки блока |
Пример¶
Эти два метода имеют идентичную структуру, но разные имена переменных -- правило помечает их как дубликаты:
// В OrderService.php
public function calculateOrderTotal(Order $order): float
{
$total = 0.0;
foreach ($order->getItems() as $item) {
$price = $item->getPrice();
$quantity = $item->getQuantity();
$subtotal = $price * $quantity;
if ($item->hasDiscount()) {
$subtotal *= (1 - $item->getDiscount());
}
$total += $subtotal;
}
return $total;
}
// В InvoiceService.php -- структурно идентичный
public function calculateInvoiceAmount(Invoice $invoice): float
{
$amount = 0.0;
foreach ($invoice->getLineItems() as $lineItem) {
$rate = $lineItem->getPrice();
$qty = $lineItem->getQuantity();
$lineTotal = $rate * $qty;
if ($lineItem->hasDiscount()) {
$lineTotal *= (1 - $lineItem->getDiscount());
}
$amount += $lineTotal;
}
return $amount;
}
После нормализации оба метода дают одинаковую последовательность токенов -- переменные заменяются на $_, но имена методов/классов вроде getPrice, getQuantity, hasDiscount сохраняются.
Как исправить¶
-
Извлечение метода (Extract Method) -- перенесите общую логику в общий метод или сервис:
final class PriceCalculator { /** @param iterable<PriceableItem> $items */ public function calculateTotal(iterable $items): float { $total = 0.0; foreach ($items as $item) { $subtotal = $item->getPrice() * $item->getQuantity(); if ($item->hasDiscount()) { $subtotal *= (1 - $item->getDiscount()); } $total += $subtotal; } return $total; } } -
Паттерн Стратегия -- когда дублированные блоки отличаются в нескольких шагах, вынесите варьирующиеся части в объекты-стратегии.
-
Шаблонный метод (Template Method) -- когда общая структура одинакова, но подклассы различаются в конкретных шагах, используйте абстрактный базовый класс с шаблонными методами.
Детали реализации¶
Qualimetrix использует алгоритм скользящего хеша Рабина-Карпа для эффективного обнаружения. Ключевой шаг -- нормализация, которая позволяет находить "почти-дубликаты", отличающиеся только именами переменных, строковыми значениями или числовыми константами. Этот подход аналогичен инструментам PMD CPD и Simian.
Поскольку имена функций/методов/классов сохраняются при нормализации, детектор не помечает два метода, вызывающих совершенно разные API, даже если их структура потока управления идентична.
Интеграция с IDE
При использовании вывода в формате SARIF (--format=sarif) дублированные копии связываются через relatedLocations. Это означает, что пары дубликатов отображаются как кликабельные перекрёстные ссылки в VS Code (расширение SARIF Viewer) и JetBrains IDE, что упрощает навигацию между всеми копиями дублированного блока.
Превью содержимого¶
Нарушения дублирования включают превью содержимого, показывающее первые токены дублированного блока. Это помогает быстро определить, какой именно код дублирован, без перехода к файлу:
src/Service/OrderService.php:10-25: Duplicated block (16 lines, 120 tokens)
Preview: public function calculate ( $_ ) { $_ = 0.0 ; foreach ( ...
Also in: src/Service/InvoiceService.php:15-30
Превью использует нормализованные токены (переменные заменены на $_, строки на '_'), что соответствует внутреннему представлению, используемому для обнаружения.
Конфигурация¶
# Увеличить минимальный порог токенов для снижения шума
bin/qmx check src/ --rule-opt="duplication.code-duplication:min_tokens=100"
# Увеличить минимальное количество строк
bin/qmx check src/ --rule-opt="duplication.code-duplication:min_lines=10"
Также можно полностью отключить правило:
Потребление памяти
Обнаружение дубликатов использует алгоритм скользящего хеша Рабина-Карпа, который требует хранения нормализованных токенов всех файлов с совпавшими хешами в памяти одновременно. На больших кодовых базах (500+ файлов) это может потреблять значительный объём памяти. Отключение правила через --disable-rule=duplication полностью пропускает фазу обнаружения и освобождает память.