Skip to content

Releases: phpcpd-next/phpcpd

v1.1 - Headless + Presets

Choose a tag to compare

@lucianofedericopereira lucianofedericopereira released this 28 Jun 08:43
4184c2e

[1.1.0] - 2026-06-28
Added — integrations

Headless mode (PhpcpdNext\Phpcpd::detect()): a one-call, in-process API that finds files, runs the same engine the CLI uses, and returns the raw CodeCloneMap — no banner, no argv parsing, no file I/O. The CLI and all embedders now share a single detection core (Engine), so they can never disagree about what a clone is.

Framework presets (--preset=, and preset: in the headless API): a named bundle of paths, suffixes, and excludes — pure configuration, no runtime dependency. Ships with a laravel preset (scans app routes database config; skips vendor, storage, bootstrap/cache, public, Blade views, and migration boilerplate). Explicit flags seed-then-override the preset. New presets are a single Preset entry in src/Presets.php.

PHPUnit integration (integration/phpunit/): an AssertNoDuplication trait and a DuplicationConstraint that turn copy/paste detection into a regression test, with offending locations (and [inconsistent] flags) printed on failure. Shipped in the production autoloader under LucianoPereira\PhpcpdNext\PHPUnit, so it works for any project that requires phpcpd-next (even as --dev). phpcpd-next dogfoods it — SelfDryTest now keeps src/ clean through this exact trait.
Laravel via Artisan: documented (no extra package) by wiring the headless API into a command.

Packaging & distribution
Published to Packagist as phpcpd-next/phpcpd: composer require --dev phpcpd-next/phpcpd.
composer.json: added type, keywords, and a suggest for phpunit/phpunit (the optional PHPUnit integration); moved the PHPUnit\ namespace into the production autoloader.
Added .gitattributes with export-ignore rules so the dist tarball ships only runtime code (src/, integration/, the binary), not tests, benchmarks, or tool configs.

Tooling
Committed a .php-cs-fixer.dist.php codifying the existing code style, so composer lint / composer check run non-interactively.

Documentation
Reworked the README to document the full feature surface accurately: the real default (Rabin-Karp + TokenBag) and --rk, all four output formats, the complete option reference split into stable vs. advanced/research flags, presets, headless mode, and the PHPUnit integration.

v1.0.0 - phpcpd-next initial release

Choose a tag to compare

@lucianofedericopereira lucianofedericopereira released this 27 Jun 22:42

Changelog

All notable changes to phpcpd-next are documented here.

Format: Keep a Changelog
Versioning: Semantic Versioning

For the complete technical diff against upstream (every changed line with a Why explanation),
see MODERNIZATION.md.


1.0.0 - 2026-06-27

Performance

  • Banded edit-distance DP in the suffix-tree engine. Profiling showed the approximate-matching
    DP — not construction — dominated findClones and grew super-linearly with --edit-distance. Since
    a cell (i,j) with |i−j| > maxErrors can never lie on a sub-threshold path, the DP is restricted
    to the diagonal band of width 2·maxErrors+1 (Ukkonen cutoff), turning the per-clone cost from
    O(L²) to O(L·maxErrors). Measured ~3.5× faster at every edit distance on a Firefly III slice,
    with byte-identical clone output.

Fixed

  • Degenerate zero-line clones are no longer reported by the suffix-tree engine. A clone whose
    in-file span collapsed to zero lines (its matched run lay almost entirely beyond a file boundary) was
    emitted as meaningless (0 lines) noise; such clones are now skipped.

Added — detection

  • Type-2 detection on every engine via --fuzzy: a shared TokenNormalizer abstracts
    identifiers and literals to type classes (previously --fuzzy only touched variables, and only
    in the default engine — the suffix tree had no Type-2 at all).
  • Inconsistent-clone reporting: gapped (Type-3) clones are distinguished from exact copies
    (CodeClone::isGapped()), marked [inconsistent] in console output and surfaced as warning
    severity in SARIF.
  • Type-aware edit weights in the suffix-tree engine: a changed control keyword (ifwhile)
    costs more of the --edit-distance budget than a renamed identifier.
  • New tokenbag engine (--algorithm=tokenbag): a SourcererCC-style order-invariant token
    bag + inverted index that detects reordered clones the contiguous engines miss. Threshold
    via --min-similarity (default 0.7).

Added — CI

  • Incremental result cache (--cache / --cache-dir): keyed by a fingerprint of the
    configuration and a manifest of file hashes; a re-run on unchanged files skips detection
    entirely and prints (cache hit). Designed to be mounted with actions/cache.
  • Per-file incremental index (--incremental, Rabin–Karp only): Hummel-style index that
    persists each file's tokenization and re-tokenizes only the files that changed, replaying the
    rest from the index. Finer-grained than --cache (one edit no longer invalidates the whole run)
    and provably equivalent to a full scan. Prints (incremental index: N reused, M scanned).

Added — output

  • JSON report (--log-json) and SARIF 2.1.0 report (--log-sarif, for GitHub Code
    Scanning), alongside the existing PMD-CPD XML. A shared Log\Logger contract unifies them.

Changed

  • Zero runtime Composer dependencies: sebastian/version, sebastian/cli-parser,
    phpunit/php-file-iterator, and phpunit/php-timer were removed — replaced with owned,
    improved code (a declarative self-documenting CLI parser with value validation; a file finder
    that prunes excluded directories and supports glob excludes; a timer that reports throughput).
  • Namespace migrated to LucianoPereira\PhpcpdNext; autoloading switched from classmap to PSR-4.
  • PMD XML logger simplified to use DOM-native escaping.

0.1.0 — 2026-06-26

First release of phpcpd-next. Picks up where Sebastian Bergmann's archived
sebastianbergmann/phpcpd (7.0-dev) left off and brings the tool forward to PHP 8.5.

Platform

  • Requires PHP ≥ 8.5 (upstream required ≥ 8.1)
  • composer.json platform locked to 8.5.0

Fixed

  • sebastian/version v4 API breakgetVersion() renamed to asString() in v4;
    the banner was crashing silently on import.
  • empty($object) always falseSuffixTreeStrategy used empty($this->result) on
    a CodeCloneMap object; empty() on any object always returns false. Fixed to
    === null.
  • Division by zeroCodeCloneMap::averageSize() divided by count() without
    guarding the empty case. Fixed with an early return 0.0.
  • current() returning falseCodeClone::lines() called current() on an
    associative array and used the result as a CodeCloneFile; current() returns false
    on an empty array. Replaced with array_values($this->files)[0] which is guaranteed safe
    after the existing non-empty guard.
  • file_get_contents() false return — both DefaultStrategy and SuffixTreeStrategy
    passed the raw string|false return directly into tokenisation. Added if ($buffer === false) { return; } guards.
  • file() returning falseCodeClone::lines() called file() without checking
    the return. Fixed with ?: [] fallback.
  • mb_convert_encoding() returning falseAbstractXmlLogger did not check the
    return of mb_convert_encoding(), which returns false on encoding failure. Fixed with
    an explicit false-check and fallback to the original string.
  • preg_replace() returning nullAbstractXmlLogger::toUtf8String() could return
    string|null from preg_replace. Fixed with ?? $string fallback.

Changed

  • Banner updated to credit both the original author and the fork:
    phpcpd 0.1.0 by Luciano Federico Pereira based on phpcpd 7.0-dev by Sebastian Bergmann.

Modernised (PHP 8.0 – 8.5)

  • readonly class applied to Arguments, CodeCloneFile, StrategyConfiguration,
    CloneInfo — immutability enforced at the class level.
  • Constructor property promotion on all eligible classes — eliminates boilerplate
    $this->x = $x assignments.
  • #[\Override] attribute on every method that implements or overrides a contract.
  • Typed class constants (private const string, private const int) throughout.
  • foreach ($array as $item) replaces foreach (array_keys($array) as $k) where the key
    was never used.
  • $result === null replaces empty($result) wherever the variable is an object or
    nullable type.

Improved

  • Duplicate code eliminatedDefaultStrategy::processFile() contained two identical
    17-line blocks that built and recorded a CodeClone. Extracted to
    recordCloneIfValid(). Running the tool on its own source now reports zero clones.
  • PHPDoc generics (list<T>, @template, @implements) on all collection classes.

Toolchain (new files)

  • PHPStan level 9 — zero errors. phpstan.neon + phpstan-stubs.php for the
    untyped sebastian/cli-parser return.
  • PHP-CS-Fixer@PER-CS2.0 + risky fixers (declare_strict_types,
    native_function_invocation, strict_param, …).
  • Rectorphp85 set, CODE_QUALITY, TYPE_DECLARATION.
  • PHPUnit 12phpunit.xml wired; test writing is the next milestone.
  • GitHub Actions CIaudit → lint → analyse → test on every push.
  • .editorconfig — consistent whitespace before any tool runs.
  • Composer scriptslint, lint:fix, analyse, test, check.