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.
- Headless mode (
LucianoPereira\PhpcpdNext\Phpcpd::detect()): a one-call, in-process API that finds files, runs the same engine the CLI uses, and returns the rawCodeCloneMap— 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=<name>, andpreset:in the headless API): a named bundle of paths, suffixes, and excludes — pure configuration, no runtime dependency. Ships with alaravelpreset (scansapp routes database config; skipsvendor,storage,bootstrap/cache,public, Blade views, and migration boilerplate). Explicit flags seed-then-override the preset. New presets are a singlePresetentry insrc/Presets.php. - PHPUnit integration (
integration/phpunit/): anAssertNoDuplicationtrait and aDuplicationConstraintthat turn copy/paste detection into a regression test, with offending locations (and[inconsistent]flags) printed on failure. Shipped in the production autoloader underLucianoPereira\PhpcpdNext\PHPUnit\, so it works for any project that requires phpcpd-next (even as--dev). phpcpd-next dogfoods it —SelfDryTestnow keepssrc/clean through this exact trait. - Laravel via Artisan: documented (no extra package) by wiring the headless API into a command.
- Published to Packagist as
phpcpd-next/phpcpd:composer require --dev phpcpd-next/phpcpd. composer.json: addedtype,keywords, and asuggestforphpunit/phpunit(the optional PHPUnit integration); moved thePHPUnit\namespace into the production autoloader.- Added
.gitattributeswithexport-ignorerules so the dist tarball ships only runtime code (src/,integration/, the binary), not tests, benchmarks, or tool configs.
- Committed a
.php-cs-fixer.dist.phpcodifying the existing code style, socomposer lint/composer checkrun non-interactively.
- 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.
1.0.0 - 2026-06-27
- Banded edit-distance DP in the suffix-tree engine. Profiling showed the approximate-matching
DP — not construction — dominated
findClonesand grew super-linearly with--edit-distance. Since a cell(i,j)with|i−j| > maxErrorscan never lie on a sub-threshold path, the DP is restricted to the diagonal band of width2·maxErrors+1(Ukkonen cutoff), turning the per-clone cost fromO(L²)toO(L·maxErrors). Measured ~3.5× faster at every edit distance on a Firefly III slice, with byte-identical clone output.
- 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.
- Type-2 detection on every engine via
--fuzzy: a sharedTokenNormalizerabstracts identifiers and literals to type classes (previously--fuzzyonly 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 aswarningseverity in SARIF. - Type-aware edit weights in the suffix-tree engine: a changed control keyword (
if→while) costs more of the--edit-distancebudget than a renamed identifier. - New
tokenbagengine (--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).
- 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 withactions/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).
- JSON report (
--log-json) and SARIF 2.1.0 report (--log-sarif, for GitHub Code Scanning), alongside the existing PMD-CPD XML. A sharedLog\Loggercontract unifies them.
- Zero runtime Composer dependencies:
sebastian/version,sebastian/cli-parser,phpunit/php-file-iterator, andphpunit/php-timerwere 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.
- Requires PHP ≥ 8.5 (upstream required ≥ 8.1)
composer.jsonplatform locked to8.5.0
sebastian/versionv4 API break —getVersion()renamed toasString()in v4; the banner was crashing silently on import.empty($object)always false —SuffixTreeStrategyusedempty($this->result)on aCodeCloneMapobject;empty()on any object always returnsfalse. Fixed to=== null.- Division by zero —
CodeCloneMap::averageSize()divided bycount()without guarding the empty case. Fixed with an earlyreturn 0.0. current()returningfalse—CodeClone::lines()calledcurrent()on an associative array and used the result as aCodeCloneFile;current()returnsfalseon an empty array. Replaced witharray_values($this->files)[0]which is guaranteed safe after the existing non-empty guard.file_get_contents()false return — bothDefaultStrategyandSuffixTreeStrategypassed the rawstring|falsereturn directly into tokenisation. Addedif ($buffer === false) { return; }guards.file()returningfalse—CodeClone::lines()calledfile()without checking the return. Fixed with?: []fallback.mb_convert_encoding()returningfalse—AbstractXmlLoggerdid not check the return ofmb_convert_encoding(), which returnsfalseon encoding failure. Fixed with an explicit false-check and fallback to the original string.preg_replace()returningnull—AbstractXmlLogger::toUtf8String()could returnstring|nullfrompreg_replace. Fixed with?? $stringfallback.
- 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.
readonly classapplied toArguments,CodeCloneFile,StrategyConfiguration,CloneInfo— immutability enforced at the class level.- Constructor property promotion on all eligible classes — eliminates boilerplate
$this->x = $xassignments. #[\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)replacesforeach (array_keys($array) as $k)where the key was never used.$result === nullreplacesempty($result)wherever the variable is an object or nullable type.
- Duplicate code eliminated —
DefaultStrategy::processFile()contained two identical 17-line blocks that built and recorded aCodeClone. Extracted torecordCloneIfValid(). Running the tool on its own source now reports zero clones. - PHPDoc generics (
list<T>,@template,@implements) on all collection classes.
- PHPStan level 9 — zero errors.
phpstan.neon+phpstan-stubs.phpfor the untypedsebastian/cli-parserreturn. - PHP-CS-Fixer —
@PER-CS2.0+ risky fixers (declare_strict_types,native_function_invocation,strict_param, …). - Rector —
php85set,CODE_QUALITY,TYPE_DECLARATION. - PHPUnit 12 —
phpunit.xmlwired; test writing is the next milestone. - GitHub Actions CI —
audit → lint → analyse → teston every push. .editorconfig— consistent whitespace before any tool runs.- Composer scripts —
lint,lint:fix,analyse,test,check.