• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

systemsdk / phpcpd / #31

16 Feb 2026 08:55PM UTC coverage: 78.378% (+2.6%) from 75.818%
#31

push

DKravtsov
### Added

* Added `--ignore-no-files option` to return a success exit code if no files were found.
* Added `#[SuppressCpd]` to ignore code clones inside a class or method (`use Systemsdk\PhpCPD\Attributes\SuppressCpd;`).

### Updated

* Improved Suffix Tree-based algorithm for code clone detection.
* Updated Dev environment: Updated XDebug, Phing, dev composer dependencies.

129 of 150 new or added lines in 6 files covered. (86.0%)

2 existing lines in 2 files now uncovered.

841 of 1073 relevant lines covered (78.38%)

8.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Cli/Application.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Systemsdk\PhpCPD\Cli;
6

7
use SebastianBergmann\FileIterator\Facade;
8
use SebastianBergmann\Timer\ResourceUsageFormatter;
9
use SebastianBergmann\Timer\Timer;
10
use SebastianBergmann\Version;
11
use Systemsdk\PhpCPD\Detector\Detector;
12
use Systemsdk\PhpCPD\Detector\Strategy\AbstractStrategy;
13
use Systemsdk\PhpCPD\Detector\Strategy\DefaultStrategy;
14
use Systemsdk\PhpCPD\Detector\Strategy\StrategyConfiguration;
15
use Systemsdk\PhpCPD\Detector\Strategy\SuffixTreeStrategy;
16
use Systemsdk\PhpCPD\Detector\SuppressionGuard;
17
use Systemsdk\PhpCPD\Exceptions\Exception;
18
use Systemsdk\PhpCPD\Exceptions\InvalidStrategyException;
19
use Systemsdk\PhpCPD\Exceptions\LoggerException;
20
use Systemsdk\PhpCPD\Exceptions\ProcessingResultException;
21
use Systemsdk\PhpCPD\Log\PMD;
22
use Systemsdk\PhpCPD\Log\Text;
23

24
use function count;
25
use function dirname;
26
use function printf;
27

28
use const PHP_EOL;
29

30
final class Application
31
{
32
    public const string VERSION = '8.3.0';
33

34
    /**
35
     * @param array<int, string> $argv
36
     */
37
    public function run(array $argv): int
38
    {
39
        $this->printVersion();
×
40

41
        try {
42
            $arguments = (new ArgumentsBuilder())->build($argv);
×
43
        } catch (Exception $exception) {
×
44
            print PHP_EOL . $exception->getMessage() . PHP_EOL;
×
45

46
            return 1;
×
47
        }
48

49
        print PHP_EOL;
×
50

51
        if ($arguments->version()) {
×
52
            return 0;
×
53
        }
54

55
        if ($arguments->help()) {
×
56
            $this->help();
×
57

58
            return 0;
×
59
        }
60

61
        /** @var list<non-empty-string> $paths */
62
        $paths = $arguments->directories();
×
63
        /** @var list<non-empty-string> $suffixes */
64
        $suffixes = $arguments->suffixes();
×
65
        /** @var list<non-empty-string> $exclude */
66
        $exclude = $arguments->exclude();
×
67
        $files = (new Facade())->getFilesAsArray(
×
68
            $paths,
×
69
            $suffixes,
×
70
            '',
×
71
            $exclude
×
72
        );
×
73

74
        if (empty($files)) {
×
75
            print 'No files found to scan' . PHP_EOL;
×
76

NEW
77
            if ($arguments->ignoreNoFiles()) {
×
NEW
78
                return 0;
×
79
            }
80

UNCOV
81
            return 1;
×
82
        }
83

84
        try {
NEW
85
            $strategy = $this->pickStrategy(
×
NEW
86
                $arguments->algorithm(),
×
NEW
87
                new StrategyConfiguration($arguments),
×
NEW
88
                new SuppressionGuard()
×
NEW
89
            );
×
90
        } catch (InvalidStrategyException $exception) {
×
91
            print $exception->getMessage() . PHP_EOL;
×
92

93
            return 1;
×
94
        }
95

96
        $timer = new Timer();
×
97
        $timer->start();
×
98

99
        try {
100
            $clones = (new Detector($strategy, true))->copyPasteDetection($files);
×
101
        } catch (ProcessingResultException $exception) {
×
102
            print 'Processing error: ' . $exception->getMessage() . PHP_EOL;
×
103

104
            return 1;
×
105
        }
106

107
        (new Text())->printResult($clones, $arguments->verbose());
×
108

109
        if ($arguments->pmdCpdXmlLogfile()) {
×
110
            try {
111
                (new PMD($arguments->pmdCpdXmlLogfile()))->processClones($clones);
×
112
            } catch (LoggerException $exception) {
×
113
                print 'Logger error: ' . $exception->getMessage() . PHP_EOL;
×
114

115
                return 1;
×
116
            }
117
        }
118

119
        print (new ResourceUsageFormatter())->resourceUsage($timer->stop()) . PHP_EOL;
×
120

121
        return count($clones) > 0 ? 1 : 0;
×
122
    }
123

124
    private function printVersion(): void
125
    {
126
        /** @var non-empty-string $path */
127
        $path = dirname(__DIR__);
×
128
        printf('%s %s', 'Copy/Paste Detector', (new Version(self::VERSION, $path))->asString());
×
129
    }
130

131
    /**
132
     * @throws InvalidStrategyException
133
     */
134
    private function pickStrategy(
135
        string $algorithm,
136
        StrategyConfiguration $config,
137
        SuppressionGuard $guard
138
    ): AbstractStrategy {
139
        return match ($algorithm) {
×
NEW
140
            ArgumentsBuilder::ALGORITHM_RABIN_KARP_NAME => new DefaultStrategy($config, $guard),
×
NEW
141
            ArgumentsBuilder::ALGORITHM_SUFFIX_TREE_NAME => new SuffixTreeStrategy($config, $guard),
×
142
            default => throw new InvalidStrategyException('Unsupported algorithm: ' . $algorithm),
×
143
        };
×
144
    }
145

146
    private function help(): void
147
    {
148
        print <<<'EOT'
149
Usage:
150
  phpcpd [options] <directory>
151

152
Options for selecting files:
153

154
  --suffix <suffix> Include files with names ending on <suffix> (default: .php; can be given multiple times)
155
  --exclude <path>  Exclude files with <path> in their path (can be given multiple times)
156

157
Options for analysing files:
158

159
  --algorithm <name>  Select which algorithm to use ('rabin-karp' (default) or 'suffix-tree')
160
  --fuzzy             Fuzz variable names
161
  --min-lines <N>     Minimum number of identical lines (default: 5)
162
  --min-tokens <N>    Minimum number of identical tokens (default: 70)
163
  --edit-distance <N> Distance in number of edits between two clones (only for suffix-tree; default: 0)
164
  --head-equality <N> Minimum equality at start of clone (only for suffix-tree; default 10)
165
  --verbose           Print results details
166
  --ignore-no-files   To return a success exit code if no files were found
167

168
Options for report generation:
169

170
  --log-pmd <file>  Write log in PMD-CPD XML format to <file>
171

172
General options:
173

174
  --version         Display version
175
  --help            Display help
176

177
EOT;
178
    }
179
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc