+ */
+ private $edBuffer = [];
+
+ /**
+ * Number of units that must be equal at the start of a clone.
+ *
+ * @var int
+ */
+ private $headEquality = 10;
+
+ /**
+ * Create a new suffix tree from a given word. The word given as parameter
+ * is used internally and should not be modified anymore, so copy it before
+ * if required.
+ *
+ * This only word correctly if the given word is closed using a sentinel
+ * character.
+ *
+ * @param AbstractToken[] $word List of tokens to analyze
+ */
+ public function __construct(array $word)
+ {
+ parent::__construct($word);
+
+ $arr = array_fill(0, $this->MAX_LENGTH, 0);
+ $this->edBuffer = array_fill(0, $this->MAX_LENGTH, $arr);
+ $this->ensureChildLists();
+ $this->leafCount = array_fill(0, $this->numNodes, 0);
+ $this->initLeafCount(0);
+ }
+
+ /**
+ * @todo Add options:
+ * --min-tokens
+ * --min-lines
+ * --edit-distance
+ * @todo Possibly add consumer from original code.
+ */
+
+ /**
+ * Finds all clones in the string (List) used in the constructor.
+ *
+ * @param int $minLength the minimal length of a clone in tokens (not lines)
+ * @param int $maxErrors the maximal number of errors/gaps allowed
+ * @param int $headEquality the number of elements which have to be the same at the beginning of a clone
+ *
+ * @return CloneInfo[]
+ */
+ public function findClones(int $minLength, int $maxErrors, int $headEquality): array
+ {
+ $this->minLength = $minLength;
+ $this->headEquality = $headEquality;
+ $this->cloneInfos = [];
+
+ for ($i = 0; $i < count($this->word); $i++) {
+ // Do quick start, as first character has to match anyway.
+ $node = $this->nextNode->get(0, $this->word[$i]);
+
+ if ($node < 0 || $this->leafCount[$node] <= 1) {
+ continue;
+ }
+
+ // we know that we have an exact match of at least 'length'
+ // characters, as the word itself is part of the suffix tree.
+ $length = $this->nodeWordEnd[$node] - $this->nodeWordBegin[$node];
+ $numReported = 0;
+
+ for ($e = $this->nodeChildFirst[$node]; $e >= 0; $e = $this->nodeChildNext[$e]) {
+ if ($this->matchWord(
+ $i,
+ $i + $length,
+ $this->nodeChildNode[$e],
+ $length,
+ $maxErrors
+ )) {
+ $numReported++;
+ }
+ }
+
+ if ($length >= $this->minLength && $numReported != 1) {
+ $this->reportClone($i, $i + $length, $node, $length, $length);
+ }
+ }
+
+ $map = [];
+
+ for ($index = 0; $index <= count($this->word); $index++) {
+ /** @var CloneInfo[] */
+ $existingClones = $this->cloneInfos[$index] ?? null;
+
+ if (!empty($existingClones)) {
+ foreach ($existingClones as $ci) {
+ // length = number of tokens
+ // TODO: min token length
+ if ($ci->length > $minLength) {
+ $previousCi = $map[$ci->token->line] ?? null;
+
+ if ($previousCi === null) {
+ $map[$ci->token->line] = $ci;
+ } elseif ($ci->length > $previousCi->length) {
+ $map[$ci->token->line] = $ci;
+ }
+ }
+ }
+ }
+ }
+
+ /** @var CloneInfo[] */
+ $values = array_values($map);
+ usort($values, static function (CloneInfo $a, CloneInfo $b): int {
+ return $b->length - $a->length;
+ });
+
+ return $values;
+ }
+
+ /**
+ * This should return true, if the provided character is not allowed to
+ * match with anything else (e.g. is a sentinel).
+ */
+ protected function mayNotMatch(AbstractToken $token): bool
+ {
+ return $token instanceof Sentinel;
+ }
+
+ /**
+ * This method is called whenever the {@link #MAX_LENGTH} is to small and
+ * hence the {@link #edBuffer} was not large enough. This may cause that a
+ * really large clone is reported in multiple chunks of size
+ * {@link #MAX_LENGTH} and potentially minor parts of such a clone might be
+ * lost.
+ */
+ protected function reportBufferShortage(int $leafStart, int $leafLength): void
+ {
+ print 'Encountered buffer shortage: ' . $leafStart . ' ' . $leafLength . "\n";
+ }
+
+ /**
+ * Initializes the {@link #leafCount} array which given for each node the
+ * number of leaves reachable from it (where leaves obtain a value of 1).
+ */
+ private function initLeafCount(int $node): void
+ {
+ $this->leafCount[$node] = 0;
+
+ for ($e = $this->nodeChildFirst[$node]; $e >= 0; $e = $this->nodeChildNext[$e]) {
+ $this->initLeafCount($this->nodeChildNode[$e]);
+ $this->leafCount[$node] += $this->leafCount[$this->nodeChildNode[$e]];
+ }
+
+ if ($this->leafCount[$node] == 0) {
+ $this->leafCount[$node] = 1;
+ }
+ }
+
+ /**
+ * Performs the approximative matching between the input word and the tree.
+ *
+ * @param int $wordStart the start position of the currently matched word (position in
+ * the input word)
+ * @param int $wordPosition the current position along the input word
+ * @param int $node the node we are currently at (i.e. the edge leading to this
+ * node is relevant to us).
+ * @param int $nodeWordLength the length of the word found along the nodes (this may be
+ * different from the length along the input word due to gaps)
+ * @param int $maxErrors the number of errors still allowed
+ *
+ * @return bool whether some clone was reported
+ */
+ private function matchWord(int $wordStart, int $wordPosition, int $node, int $nodeWordLength, int $maxErrors): bool
+ {
+ // We are aware that this method is longer than desirable for code
+ // reading. However, we currently do not see a refactoring that has a
+ // sensible cost-benefit ratio. Suggestions are welcome!
+
+ // self match?
+ if ($this->leafCount[$node] == 1 && $this->nodeWordBegin[$node] == $wordPosition) {
+ return false;
+ }
+
+ $currentNodeWordLength = min($this->nodeWordEnd[$node] - $this->nodeWordBegin[$node], $this->MAX_LENGTH - 1);
+
+ // Do min edit distance
+ $currentLength = $this->calculateMaxLength(
+ $wordStart,
+ $wordPosition,
+ $node,
+ $maxErrors,
+ $currentNodeWordLength
+ );
+
+ if ($currentLength == 0) {
+ return false;
+ }
+
+ if ($currentLength >= $this->MAX_LENGTH - 1) {
+ $this->reportBufferShortage($this->nodeWordBegin[$node], $currentNodeWordLength);
+ }
+
+ // calculate cheapest match
+ $best = $maxErrors + 42;
+ $iBest = 0;
+ $jBest = 0;
+
+ for ($k = 0; $k <= $currentLength; $k++) {
+ $i = $currentLength - $k;
+ $j = $currentLength;
+
+ if ($this->edBuffer[$i][$j] < $best) {
+ $best = $this->edBuffer[$i][$j];
+ $iBest = $i;
+ $jBest = $j;
+ }
+
+ $i = $currentLength;
+ $j = $currentLength - $k;
+
+ if ($this->edBuffer[$i][$j] < $best) {
+ $best = $this->edBuffer[$i][$j];
+ $iBest = $i;
+ $jBest = $j;
+ }
+ }
+
+ while ($wordPosition + $iBest < count($this->word) &&
+ $jBest < $currentNodeWordLength &&
+ $this->word[$wordPosition + $iBest] != $this->word[$this->nodeWordBegin[$node] + $jBest] &&
+ $this->word[$wordPosition + $iBest]->equals(
+ $this->word[$this->nodeWordBegin[$node] + $jBest]
+ )) {
+ $iBest++;
+ $jBest++;
+ }
+
+ $numReported = 0;
+
+ if ($currentLength == $currentNodeWordLength) {
+ // we may proceed
+ for ($e = $this->nodeChildFirst[$node]; $e >= 0; $e = $this->nodeChildNext[$e]) {
+ if ($this->matchWord(
+ $wordStart,
+ $wordPosition + $iBest,
+ $this->nodeChildNode[$e],
+ $nodeWordLength + $jBest,
+ $maxErrors
+ - $best
+ )) {
+ $numReported++;
+ }
+ }
+ }
+
+ // do not report locally if had reports in exactly one subtree (would be
+ // pure subclone)
+ if ($numReported == 1) {
+ return true;
+ }
+
+ // disallow tail changes
+ while ($iBest > 0 &&
+ $jBest > 0 &&
+ !$this->word[$wordPosition + $iBest - 1]->equals(
+ $this->word[$this->nodeWordBegin[$node] + $jBest - 1]
+ )) {
+ if ($iBest > 1 &&
+ $this->word[$wordPosition + $iBest - 2]->equals(
+ $this->word[$this->nodeWordBegin[$node] + $jBest - 1]
+ )) {
+ $iBest--;
+ } elseif ($jBest > 1 &&
+ $this->word[$wordPosition + $iBest - 1]->equals(
+ $this->word[$this->nodeWordBegin[$node] + $jBest - 2]
+ )) {
+ $jBest--;
+ } else {
+ $iBest--;
+ $jBest--;
+ }
+ }
+
+ // report if real clone
+ if ($iBest > 0 && $jBest > 0) {
+ $numReported++;
+ $this->reportClone($wordStart, $wordPosition + $iBest, $node, $jBest, $nodeWordLength + $jBest);
+ }
+
+ return $numReported > 0;
+ }
+
+ /**
+ * Calculates the maximum length we may take along the word to the current
+ * $node (respecting the number of errors to make). *.
+ *
+ * @param int $wordStart the start position of the currently matched word (position in
+ * the input word)
+ * @param int $wordPosition the current position along the input word
+ * @param int $node the node we are currently at (i.e. the edge leading to this
+ * node is relevant to us).
+ * @param int $maxErrors the number of errors still allowed
+ * @param int $currentNodeWordLength the length of the word found along the nodes (this may be
+ * different from the actual length due to buffer limits)
+ *
+ * @return int the maximal length that can be taken
+ */
+ private function calculateMaxLength(
+ int $wordStart,
+ int $wordPosition,
+ int $node,
+ int $maxErrors,
+ int $currentNodeWordLength
+ ): int {
+ $this->edBuffer[0][0] = 0;
+ $currentLength = 1;
+
+ for (; $currentLength <= $currentNodeWordLength; $currentLength++) {
+ /** @var int */
+ $best = $currentLength;
+ $this->edBuffer[0][$currentLength] = $currentLength;
+ $this->edBuffer[$currentLength][0] = $currentLength;
+
+ if ($wordPosition + $currentLength >= count($this->word)) {
+ break;
+ }
+
+ // deal with case that character may not be matched (sentinel!)
+ $iChar = $this->word[$wordPosition + $currentLength - 1];
+ $jChar = $this->word[$this->nodeWordBegin[$node] + $currentLength - 1];
+
+ if ($this->mayNotMatch($iChar) || $this->mayNotMatch($jChar)) {
+ break;
+ }
+
+ // usual matrix completion for edit distance
+ for ($k = 1; $k < $currentLength; $k++) {
+ $best = min(
+ $best,
+ $this->fillEDBuffer(
+ $k,
+ $currentLength,
+ $wordPosition,
+ $this->nodeWordBegin[$node]
+ )
+ );
+ }
+
+ for ($k = 1; $k < $currentLength; $k++) {
+ $best = min(
+ $best,
+ $this->fillEDBuffer(
+ $currentLength,
+ $k,
+ $wordPosition,
+ $this->nodeWordBegin[$node]
+ )
+ );
+ }
+ $best = min(
+ $best,
+ $this->fillEDBuffer(
+ $currentLength,
+ $currentLength,
+ $wordPosition,
+ $this->nodeWordBegin[$node]
+ )
+ );
+
+ if ($best > $maxErrors ||
+ $wordPosition - $wordStart + $currentLength <= $this->headEquality &&
+ $best > 0) {
+ break;
+ }
+ }
+ $currentLength--;
+
+ return $currentLength;
+ }
+
+ private function reportClone(
+ int $wordBegin,
+ int $wordEnd,
+ int $currentNode,
+ int $nodeWordPos,
+ int $nodeWordLength
+ ): void {
+ $length = $wordEnd - $wordBegin;
+
+ if ($length < $this->minLength || $nodeWordLength < $this->minLength) {
+ return;
+ }
+
+ // NB: 0 and 0 are two indicate the template S and T for Psalm, in lack of generics.
+ $otherClones = new PairList(16, 0, 0);
+ $this->findRemainingClones(
+ $otherClones,
+ $nodeWordLength,
+ $currentNode,
+ $this->nodeWordEnd[$currentNode] - $this->nodeWordBegin[$currentNode] - $nodeWordPos,
+ $wordBegin
+ );
+
+ $occurrences = 1 + $otherClones->size();
+
+ // check whether we may start from here
+ $t = $this->word[$wordBegin];
+ $newInfo = new CloneInfo($length, $wordBegin, $occurrences, $t, $otherClones);
+
+ for ($index = max(0, $wordBegin - $this->INDEX_SPREAD + 1); $index <= $wordBegin; $index++) {
+ $existingClones = $this->cloneInfos[$index] ?? null;
+
+ if ($existingClones != null) {
+ //for (CloneInfo cloneInfo : $existingClones) {
+ foreach ($existingClones as $cloneInfo) {
+ if ($cloneInfo->dominates($newInfo, $wordBegin - $index)) {
+ // we already have a dominating clone, so ignore
+ return;
+ }
+ }
+ }
+ }
+
+ // add clone to $otherClones to avoid getting more duplicates
+ for ($i = $wordBegin; $i < $wordEnd; $i += $this->INDEX_SPREAD) {
+ $this->cloneInfos[$i][] = new CloneInfo($length - ($i - $wordBegin), $wordBegin, $occurrences, $t, $otherClones);
+ }
+ $t = $this->word[$wordBegin];
+
+ for ($clone = 0; $clone < $otherClones->size(); $clone++) {
+ $start = $otherClones->getFirst($clone);
+ $otherLength = $otherClones->getSecond($clone);
+
+ for ($i = 0; $i < $otherLength; $i += $this->INDEX_SPREAD) {
+ $this->cloneInfos[$start + $i][] = new CloneInfo($otherLength - $i, $wordBegin, $occurrences, $t, $otherClones);
+ }
+ }
+ }
+
+ /**
+ * Fills the edit distance buffer at position (i,j).
+ *
+ * @param int $i the first index of the buffer
+ * @param int $j the second index of the buffer
+ * @param int $iOffset the offset where the word described by $i starts
+ * @param int $jOffset the offset where the word described by $j starts
+ *
+ * @return int the value inserted into the buffer
+ */
+ private function fillEDBuffer(int $i, int $j, int $iOffset, int $jOffset): int
+ {
+ $iChar = $this->word[$iOffset + $i - 1];
+ $jChar = $this->word[$jOffset + $j - 1];
+
+ $insertDelete = 1 + min($this->edBuffer[$i - 1][$j], $this->edBuffer[$i][$j - 1]);
+ $change = $this->edBuffer[$i - 1][$j - 1] + ($iChar->equals($jChar) ? 0 : 1);
+
+ return $this->edBuffer[$i][$j] = min($insertDelete, $change);
+ }
+
+ /**
+ * Fills a list of pairs giving the start positions and lengths of the
+ * remaining clones.
+ *
+ * @param PairList $clonePositions the clone positions being filled (start position and length)
+ * @param int $nodeWordLength the length of the word along the nodes
+ * @param int $currentNode the node we are currently at
+ * @param int $distance the distance along the word leading to the current node
+ * @param int $wordStart the start of the currently searched word
+ */
+ private function findRemainingClones(
+ PairList $clonePositions,
+ int $nodeWordLength,
+ int $currentNode,
+ int $distance,
+ int $wordStart
+ ): void {
+ for ($nextNode = $this->nodeChildFirst[$currentNode]; $nextNode >= 0; $nextNode = $this->nodeChildNext[$nextNode]) {
+ $node = $this->nodeChildNode[$nextNode];
+ $this->findRemainingClones($clonePositions, $nodeWordLength, $node, $distance
+ + $this->nodeWordEnd[$node] - $this->nodeWordBegin[$node], $wordStart);
+ }
+
+ if ($this->nodeChildFirst[$currentNode] < 0) {
+ $start = count($this->word) - $distance - $nodeWordLength;
+
+ if ($start != $wordStart) {
+ $clonePositions->add($start, $nodeWordLength);
+ }
+ }
+ }
+}
diff --git a/src/Detector/Strategy/SuffixTree/CloneInfo.php b/src/Detector/Strategy/SuffixTree/CloneInfo.php
new file mode 100644
index 00000000..3414eb65
--- /dev/null
+++ b/src/Detector/Strategy/SuffixTree/CloneInfo.php
@@ -0,0 +1,68 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTree;
+
+/** Stores information on a clone. */
+class CloneInfo
+{
+ /**
+ * Length of the clone in tokens.
+ *
+ * @var int
+ */
+ public $length;
+
+ /**
+ * Position in word list.
+ *
+ * @var int
+ */
+ public $position;
+
+ /**
+ * @var AbstractToken
+ */
+ public $token;
+
+ /**
+ * Related clones.
+ *
+ * @var PairList
+ */
+ public $otherClones;
+
+ /**
+ * Number of occurrences of the clone.
+ *
+ * @var int
+ */
+ private $occurrences;
+
+ /** Constructor. */
+ public function __construct(int $length, int $position, int $occurrences, AbstractToken $token, PairList $otherClones)
+ {
+ $this->length = $length;
+ $this->position = $position;
+ $this->occurrences = $occurrences;
+ $this->token = $token;
+ $this->otherClones = $otherClones;
+ }
+
+ /**
+ * Returns whether this clone info dominates the given one, i.e. whether
+ * both {@link #length} and {@link #occurrences} s not smaller.
+ *
+ * @param int $later the amount the given clone starts later than the "this" clone
+ */
+ public function dominates(self $ci, int $later): bool
+ {
+ return $this->length - $later >= $ci->length && $this->occurrences >= $ci->occurrences;
+ }
+}
diff --git a/src/Detector/Strategy/SuffixTree/PairList.php b/src/Detector/Strategy/SuffixTree/PairList.php
new file mode 100644
index 00000000..e255f3e6
--- /dev/null
+++ b/src/Detector/Strategy/SuffixTree/PairList.php
@@ -0,0 +1,228 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTree;
+
+use SebastianBergmann\PHPCPD\OutOfBoundsException;
+
+/**
+ * A list for storing pairs in a specific order.
+ *
+ * @author $Author: hummelb $
+ *
+ * @version $Rev: 51770 $
+ *
+ * @ConQAT.Rating GREEN Hash: 7459D6D0F59028B37DD23DD091BDCEEA
+ *
+ * @template T
+ * @template S
+ */
+class PairList
+{
+ /**
+ * Version used for serialization.
+ *
+ * @var int
+ */
+ private $serialVersionUID = 1;
+
+ /**
+ * The current size.
+ *
+ * @var int
+ */
+ private $size = 0;
+
+ /**
+ * The array used for storing the S.
+ *
+ * @var S[]
+ */
+ private $firstElements;
+
+ /**
+ * The array used for storing the T.
+ *
+ * @var T[]
+ */
+ private $secondElements;
+
+ /**
+ * @param S $firstType
+ * @param T $secondType
+ */
+ public function __construct(int $initialCapacity, $firstType, $secondType)
+ {
+ if ($initialCapacity < 1) {
+ $initialCapacity = 1;
+ }
+ $this->firstElements = array_fill(0, $initialCapacity, null);
+ $this->secondElements = array_fill(0, $initialCapacity, null);
+ }
+
+ /** Returns whether the list is empty. */
+ public function isEmpty(): bool
+ {
+ return $this->size == 0;
+ }
+
+ /** Returns the size of the list. */
+ public function size(): int
+ {
+ return $this->size;
+ }
+
+ /**
+ * Add the given pair to the list.
+ *
+ * @param S $first
+ * @param T $second
+ */
+ public function add($first, $second): void
+ {
+ $this->firstElements[$this->size] = $first;
+ $this->secondElements[$this->size] = $second;
+ $this->size++;
+ }
+
+ /** Adds all pairs from another list. */
+ public function addAll(self $other): void
+ {
+ // we have to store this in a local var, as other.$this->size may change if
+ // other == this
+ $otherSize = $other->size;
+
+ for ($i = 0; $i < $otherSize; $i++) {
+ $this->firstElements[$this->size] = $other->firstElements[$i];
+ $this->secondElements[$this->size] = $other->secondElements[$i];
+ $this->size++;
+ }
+ }
+
+ /**
+ * Returns the first element at given index.
+ *
+ * @return S
+ */
+ public function getFirst(int $i)
+ {
+ $this->checkWithinBounds($i);
+
+ return $this->firstElements[$i];
+ }
+
+ /**
+ * Sets the first element at given index.
+ *
+ * @param S $value
+ */
+ public function setFirst(int $i, $value): void
+ {
+ $this->checkWithinBounds($i);
+ $this->firstElements[$i] = $value;
+ }
+
+ /**
+ * Returns the second element at given index.
+ *
+ * @return T
+ */
+ public function getSecond(int $i)
+ {
+ $this->checkWithinBounds($i);
+
+ return $this->secondElements[$i];
+ }
+
+ /**
+ * Sets the first element at given index.
+ *
+ * @param T $value
+ */
+ public function setSecond(int $i, $value): void
+ {
+ $this->checkWithinBounds($i);
+ $this->secondElements[$i] = $value;
+ }
+
+ /**
+ * Creates a new list containing all first elements.
+ *
+ * @return S[]
+ */
+ public function extractFirstList(): array
+ {
+ $result = [];
+
+ for ($i = 0; $i < $this->size; $i++) {
+ $result[] = $this->firstElements[$i];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Creates a new list containing all second elements.
+ *
+ * @return T[]
+ */
+ public function extractSecondList(): array
+ {
+ $result = [];
+
+ for ($i = 0; $i < $this->size; $i++) {
+ $result[] = $this->secondElements[$i];
+ }
+
+ return $result;
+ }
+
+ /** Swaps the entries located at indexes $i and $j. */
+ public function swapEntries(int $i, int $j): void
+ {
+ $tmp1 = $this->getFirst($i);
+ $tmp2 = $this->getSecond($i);
+ $this->setFirst($i, $this->getFirst($j));
+ $this->setSecond($i, $this->getSecond($j));
+ $this->setFirst($j, $tmp1);
+ $this->setSecond($j, $tmp2);
+ }
+
+ /** Clears this list. */
+ public function clear(): void
+ {
+ $this->size = 0;
+ }
+
+ /** Removes the last element of the list. */
+ public function removeLast(): void
+ {
+ $this->size--;
+ }
+
+ public function hashCode(): int
+ {
+ $prime = 31;
+ $hash = $this->size;
+ $hash = $prime * $hash + crc32(serialize($this->firstElements));
+
+ return $prime * $hash + crc32(serialize($this->secondElements));
+ }
+
+ /**
+ * Checks whether the given $i is within the bounds. Throws an
+ * exception otherwise.
+ */
+ private function checkWithinBounds(int $i): void
+ {
+ if ($i < 0 || $i >= $this->size) {
+ throw new OutOfBoundsException('Out of bounds: ' . $i);
+ }
+ }
+}
diff --git a/src/Detector/Strategy/SuffixTree/Sentinel.php b/src/Detector/Strategy/SuffixTree/Sentinel.php
new file mode 100644
index 00000000..bfdd4236
--- /dev/null
+++ b/src/Detector/Strategy/SuffixTree/Sentinel.php
@@ -0,0 +1,50 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTree;
+
+use function random_int;
+
+/**
+ * A sentinel character which can be used to produce explicit leaves for all
+ * suffixes. The sentinel just has to be appended to the list before handing
+ * it to the suffix tree. For the sentinel equality and object identity are
+ * the same!
+ */
+class Sentinel extends AbstractToken
+{
+ /** @var int The hash value used. */
+ private $hash;
+
+ public function __construct()
+ {
+ $this->hash = random_int(0, PHP_INT_MAX);
+ $this->tokenCode = -1;
+ $this->line = -1;
+ $this->file = '';
+ $this->tokenName = '';
+ $this->content = '';
+ }
+
+ public function __toString(): string
+ {
+ return '$';
+ }
+
+ public function hashCode(): int
+ {
+ return $this->hash;
+ }
+
+ public function equals(AbstractToken $other): bool
+ {
+ // Original code uses physical object equality, not present in PHP.
+ return $other instanceof self;
+ }
+}
diff --git a/src/Detector/Strategy/SuffixTree/SuffixTree.php b/src/Detector/Strategy/SuffixTree/SuffixTree.php
new file mode 100644
index 00000000..cf8e1d57
--- /dev/null
+++ b/src/Detector/Strategy/SuffixTree/SuffixTree.php
@@ -0,0 +1,315 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTree;
+
+/**
+ * Efficient linear time constructible suffix tree using Ukkonen's online
+ * construction algorithm (E. Ukkonen: "On-line construction of suffix trees").
+ * Most of the comments reference this paper and it might be hard to follow
+ * without knowing at least the basics of it.
+ *
+ * We use some conventions which are slightly different from the paper however:
+ *
+ * - The names of the variables are different, but we give a translation into
+ * Ukkonen's names.
+ * - Many variables are made "global" by realizing them as fields. This way we
+ * can easily deal with those tuple return values without constructing extra
+ * classes.
+ * - String indices start at 0 (not at 1).
+ * - Substrings are marked by the first index and the index after the last one
+ * (just as in C++ STL) instead of the first and the last index (i.e. intervals
+ * are right-open instead of closed). This makes it more intuitive to express
+ * the empty string (i.e. (i,i) instead of (i,i-1)).
+ *
+ *
+ * Everything but the construction itself is protected to simplify increasing
+ * its functionality by subclassing but without introducing new method calls.
+ *
+ * @author Benjamin Hummel
+ * @author $Author: kinnen $
+ *
+ * @version $Revision: 41751 $
+ *
+ * @ConQAT.Rating GREEN Hash: 4B2EF0606B3085A6831764ED042FF20D
+ */
+class SuffixTree
+{
+ /**
+ * Infinity in this context.
+ *
+ * @var int
+ */
+ protected $INFTY;
+
+ /**
+ * The word we are working on.
+ *
+ * @var AbstractToken[]
+ */
+ protected $word;
+
+ /**
+ * The number of nodes created so far.
+ *
+ * @var int
+ */
+ protected $numNodes = 0;
+
+ /**
+ * For each node this holds the index of the first character of
+ * {@link #word} labeling the transition to this node. This
+ * corresponds to the k for a transition used in Ukkonen's paper.
+ *
+ * @var int[]
+ */
+ protected $nodeWordBegin;
+
+ /**
+ * For each node this holds the index of the one after the last character of
+ * {@link #word} labeling the transition to this node. This
+ * corresponds to the p for a transition used in Ukkonen's paper.
+ *
+ * @var int[]
+ */
+ protected $nodeWordEnd;
+
+ /** For each node its suffix link (called function f by Ukkonen).
+ * @var int[] */
+ protected $suffixLink;
+
+ /**
+ * The next node function realized as a hash table. This corresponds to the
+ * g function used in Ukkonen's paper.
+ *
+ * @var SuffixTreeHashTable
+ */
+ protected $nextNode;
+
+ /**
+ * An array giving for each node the index where the first child will be
+ * stored (or -1 if it has no children). It is initially empty and will be
+ * filled "on demand" using
+ * {@link org.conqat.engine.code_clones.detection.suffixtree.SuffixTreeHashTable#extractChildLists(int[], int[], int[])}
+ * .
+ *
+ * @var int[]
+ */
+ protected $nodeChildFirst = [];
+
+ /**
+ * This array gives the next index of the child list or -1 if this is the
+ * last one. It is initially empty and will be filled "on demand" using
+ * {@link org.conqat.engine.code_clones.detection.suffixtree.SuffixTreeHashTable#extractChildLists(int[], int[], int[])}
+ * .
+ *
+ * @var int[]
+ */
+ protected $nodeChildNext = [];
+
+ /**
+ * This array stores the actual name (=number) of the mode in the child
+ * list. It is initially empty and will be filled "on demand" using
+ * {@link org.conqat.engine.code_clones.detection.suffixtree.SuffixTreeHashTable#extractChildLists(int[], int[], int[])}
+ * .
+ *
+ * @var int[]
+ */
+ protected $nodeChildNode = [];
+
+ /**
+ * The node we are currently at as a "global" variable (as it is always
+ * passed unchanged). This is called s in Ukkonen's paper.
+ *
+ * @var int
+ */
+ private $currentNode = 0;
+
+ /**
+ * Beginning of the word part of the reference pair. This is kept "global"
+ * (in constrast to the end) as this is passed unchanged to all functions.
+ * Ukkonen calls this k.
+ *
+ * @var int
+ */
+ private $refWordBegin = 0;
+
+ /**
+ * This is the new (or old) explicit state as returned by
+ * {@link #testAndSplit(int, Object)}. Ukkonen calls this r.
+ *
+ * @var int
+ */
+ private $explicitNode = 0;
+
+ /**
+ * Create a new suffix tree from a given word. The word given as parameter
+ * is used internally and should not be modified anymore, so copy it before
+ * if required.
+ *
+ * @param AbstractToken[] $word
+ */
+ public function __construct($word)
+ {
+ $this->word = $word;
+ $size = count($word);
+ $this->INFTY = $size;
+
+ $expectedNodes = 2 * $size;
+ $this->nodeWordBegin = array_fill(0, $expectedNodes, 0);
+ $this->nodeWordEnd = array_fill(0, $expectedNodes, 0);
+ $this->suffixLink = array_fill(0, $expectedNodes, 0);
+ $this->nextNode = new SuffixTreeHashTable($expectedNodes);
+
+ $this->createRootNode();
+
+ for ($i = 0; $i < $size; $i++) {
+ $this->update($i);
+ $this->canonize($i + 1);
+ }
+ }
+
+ /**
+ * This method makes sure the child lists are filled (required for
+ * traversing the tree).
+ */
+ protected function ensureChildLists(): void
+ {
+ if ($this->nodeChildFirst == null || count($this->nodeChildFirst) < $this->numNodes) {
+ $this->nodeChildFirst = array_fill(0, $this->numNodes, 0);
+ $this->nodeChildNext = array_fill(0, $this->numNodes, 0);
+ $this->nodeChildNode = array_fill(0, $this->numNodes, 0);
+ $this->nextNode->extractChildLists($this->nodeChildFirst, $this->nodeChildNext, $this->nodeChildNode);
+ }
+ }
+
+ /**
+ * Creates the root node.
+ */
+ private function createRootNode(): void
+ {
+ $this->numNodes = 1;
+ $this->nodeWordBegin[0] = 0;
+ $this->nodeWordEnd[0] = 0;
+ $this->suffixLink[0] = -1;
+ }
+
+ /**
+ * The update function as defined in Ukkonen's paper. This inserts
+ * the character at charPos into the tree. It works on the canonical
+ * reference pair ({@link #currentNode}, ({@link #refWordBegin}, charPos)).
+ */
+ private function update(int $charPos): void
+ {
+ $lastNode = 0;
+
+ while (!$this->testAndSplit($charPos, $this->word[$charPos])) {
+ $newNode = $this->numNodes++;
+ $this->nodeWordBegin[$newNode] = $charPos;
+ $this->nodeWordEnd[$newNode] = $this->INFTY;
+ $this->nextNode->put($this->explicitNode, $this->word[$charPos], $newNode);
+
+ if ($lastNode != 0) {
+ $this->suffixLink[$lastNode] = $this->explicitNode;
+ }
+ $lastNode = $this->explicitNode;
+ $this->currentNode = $this->suffixLink[$this->currentNode];
+ $this->canonize($charPos);
+ }
+
+ if ($lastNode != 0) {
+ $this->suffixLink[$lastNode] = $this->currentNode;
+ }
+ }
+
+ /**
+ * The test-and-split function as defined in Ukkonen's paper. This
+ * checks whether the state given by the canonical reference pair (
+ * {@link #currentNode}, ({@link #refWordBegin}, refWordEnd)) is the end
+ * point (by checking whether a transition for the
+ * nextCharacter exists). Additionally the state is made
+ * explicit if it not already is and this is not the end-point. It returns
+ * true if the end-point was reached. The newly created (or reached)
+ * explicit node is returned in the "global" variable.
+ */
+ private function testAndSplit(int $refWordEnd, AbstractToken $nextCharacter): bool
+ {
+ if ($this->currentNode < 0) {
+ // trap state is always end state
+ return true;
+ }
+
+ if ($refWordEnd <= $this->refWordBegin) {
+ if ($this->nextNode->get($this->currentNode, $nextCharacter) < 0) {
+ $this->explicitNode = $this->currentNode;
+
+ return false;
+ }
+
+ return true;
+ }
+
+ $next = $this->nextNode->get($this->currentNode, $this->word[$this->refWordBegin]);
+
+ if ($nextCharacter->equals($this->word[$this->nodeWordBegin[$next] + $refWordEnd - $this->refWordBegin])) {
+ return true;
+ }
+
+ // not an end-point and not explicit, so make it explicit.
+ $this->explicitNode = $this->numNodes++;
+ $this->nodeWordBegin[$this->explicitNode] = $this->nodeWordBegin[$next];
+ $this->nodeWordEnd[$this->explicitNode] = $this->nodeWordBegin[$next] + $refWordEnd - $this->refWordBegin;
+ $this->nextNode->put($this->currentNode, $this->word[$this->refWordBegin], $this->explicitNode);
+
+ $this->nodeWordBegin[$next] += $refWordEnd - $this->refWordBegin;
+ $this->nextNode->put($this->explicitNode, $this->word[$this->nodeWordBegin[$next]], $next);
+
+ return false;
+ }
+
+ /**
+ * The canonize function as defined in Ukkonen's paper. Changes the
+ * reference pair (currentNode, (refWordBegin, refWordEnd)) into a canonical
+ * reference pair. It works on the "global" variables {@link #currentNode}
+ * and {@link #refWordBegin} and the parameter, writing the result back to
+ * the globals.
+ *
+ * @param int $refWordEnd one after the end index for the word of the reference pair
+ */
+ private function canonize(int $refWordEnd): void
+ {
+ if ($this->currentNode === -1) {
+ // explicitly handle trap state
+ $this->currentNode = 0;
+ $this->refWordBegin++;
+ }
+
+ if ($refWordEnd <= $this->refWordBegin) {
+ // empty word, so already canonical
+ return;
+ }
+
+ $next = $this->nextNode->get(
+ $this->currentNode,
+ $this->word[$this->refWordBegin]
+ );
+
+ while ($this->nodeWordEnd[$next] - $this->nodeWordBegin[$next] <= $refWordEnd
+ - $this->refWordBegin) {
+ $this->refWordBegin += $this->nodeWordEnd[$next] - $this->nodeWordBegin[$next];
+ $this->currentNode = $next;
+
+ if ($refWordEnd > $this->refWordBegin) {
+ $next = $this->nextNode->get($this->currentNode, $this->word[$this->refWordBegin]);
+ } else {
+ break;
+ }
+ }
+ }
+}
diff --git a/src/Detector/Strategy/SuffixTree/SuffixTreeHashTable.php b/src/Detector/Strategy/SuffixTree/SuffixTreeHashTable.php
new file mode 100644
index 00000000..86d9f58f
--- /dev/null
+++ b/src/Detector/Strategy/SuffixTree/SuffixTreeHashTable.php
@@ -0,0 +1,233 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTree;
+
+/**
+ * The hash table used for the {@link SuffixTree} class. It is specifically
+ * written and optimized for its implementation and is thus probably of little
+ * use for any other application.
+ *
+ * It hashes from (node, character) pairs to the next node, where nodes are
+ * represented by integers and the type of characters is determined by the
+ * generic parameter.
+ *
+ * @author Benjamin Hummel
+ * @author $Author: juergens $
+ *
+ * @version $Revision: 34670 $
+ *
+ * @ConQAT.Rating GREEN Hash: 6A7A830078AF0CA9C2D84C148F336DF4
+ */
+class SuffixTreeHashTable
+{
+ /**
+ * These numbers were taken from
+ * http://planetmath.org/encyclopedia/GoodHashTablePrimes.html.
+ *
+ * @var int[]
+ */
+ private $allowedSizes = [53, 97, 193, 389, 769, 1543,
+ 3079, 6151, 12289, 24593, 49157, 98317, 196613, 393241, 786433,
+ 1572869, 3145739, 6291469, 12582917, 25165843, 50331653, 100663319,
+ 201326611, 402653189, 805306457, 1610612741, ];
+
+ /**
+ * The size of the hash table.
+ *
+ * @var int
+ */
+ private $tableSize;
+
+ /**
+ * Storage space for the node part of the key.
+ *
+ * @var int[]
+ */
+ private $keyNodes;
+
+ /**
+ * Storage space for the character part of the key.
+ *
+ * @var array
+ */
+ private $keyChars;
+
+ /**
+ * Storage space for the result node.
+ *
+ * @var int[]
+ */
+ private $resultNodes;
+
+ /**
+ * Debug info: number of stored nodes.
+ *
+ * @var int
+ */
+ private $_numStoredNodes = 0;
+
+ /**
+ * Debug info: number of calls to find so far.
+ *
+ * @var int
+ */
+ private $_numFind = 0;
+
+ /**
+ * Debug info: number of collisions (i.e. wrong finds) during find so far.
+ *
+ * @var int
+ */
+ private $_numColl = 0;
+
+ /**
+ * Creates a new hash table for the given number of nodes. Trying to add
+ * more nodes will result in worse performance down to entering an infinite
+ * loop on some operations.
+ */
+ public function __construct(int $numNodes)
+ {
+ $minSize = (int) ceil(1.5 * $numNodes);
+ $sizeIndex = 0;
+
+ while ($this->allowedSizes[$sizeIndex] < $minSize) {
+ $sizeIndex++;
+ }
+ $this->tableSize = $this->allowedSizes[$sizeIndex];
+
+ $this->keyNodes = array_fill(0, $this->tableSize, 0);
+ $this->keyChars = array_fill(0, $this->tableSize, null);
+ $this->resultNodes = array_fill(0, $this->tableSize, 0);
+ }
+
+ /**
+ * Returns the next node for the given (node, character) key pair or a
+ * negative value if no next node is stored for this key.
+ */
+ public function get(int $keyNode, AbstractToken $keyChar): int
+ {
+ $pos = $this->hashFind($keyNode, $keyChar);
+
+ if ($this->keyChars[$pos] === null) {
+ return -1;
+ }
+
+ return $this->resultNodes[$pos];
+ }
+
+ /**
+ * Inserts the given result node for the (node, character) key pair.
+ */
+ public function put(int $keyNode, AbstractToken $keyChar, int $resultNode): void
+ {
+ $pos = $this->hashFind($keyNode, $keyChar);
+
+ if ($this->keyChars[$pos] == null) {
+ $this->_numStoredNodes++;
+ $this->keyChars[$pos] = $keyChar;
+ $this->keyNodes[$pos] = $keyNode;
+ }
+ $this->resultNodes[$pos] = $resultNode;
+ }
+
+ /**
+ * Extracts the list of child nodes for each node from the hash table
+ * entries as a linked list. All arrays are expected to be initially empty
+ * and of suitable size (i.e. for n nodes it should have size
+ * n given that nodes are numbered 0 to n-1). Those arrays will be
+ * filled from this method.
+ *
+ * The method is package visible, as it is tighly coupled to the
+ * {@link SuffixTree} class.
+ *
+ * @param int[] $nodeFirstIndex an array giving for each node the index where the first child
+ * will be stored (or -1 if it has no children)
+ * @param int[] $nodeNextIndex this array gives the next index of the child list or -1 if
+ * this is the last one
+ * @param int[] $nodeChild this array stores the actual name (=number) of the mode in the
+ * child list
+ */
+ public function extractChildLists(array &$nodeFirstIndex, array &$nodeNextIndex, array &$nodeChild): void
+ {
+ // Instead of Arrays.fill($nodeFirstIndex, -1);
+ foreach (array_keys($nodeFirstIndex) as $k) {
+ $nodeFirstIndex[$k] = -1;
+ }
+ $free = 0;
+
+ for ($i = 0; $i < $this->tableSize; $i++) {
+ if ($this->keyChars[$i] !== null) {
+ // insert $this->keyNodes[$i] -> $this->resultNodes[$i]
+ $nodeChild[$free] = $this->resultNodes[$i];
+ $nodeNextIndex[$free] = $nodeFirstIndex[$this->keyNodes[$i]];
+ $nodeFirstIndex[$this->keyNodes[$i]] = $free++;
+ }
+ }
+ }
+
+ /**
+ * Returns the position of the (node,char) key in the hash map or the
+ * position to insert it into if it is not yet in.
+ */
+ private function hashFind(int $keyNode, AbstractToken $keyChar): int
+ {
+ $this->_numFind++;
+ $hash = $keyChar->hashCode();
+ $pos = $this->posMod($this->primaryHash($keyNode, $hash));
+ $secondary = $this->secondaryHash($keyNode, $hash);
+
+ while ($this->keyChars[$pos] !== null) {
+ if ($this->keyNodes[$pos] === $keyNode && $keyChar->equals($this->keyChars[$pos])) {
+ break;
+ }
+ $this->_numColl++;
+ $pos = ($pos + $secondary) % $this->tableSize;
+ }
+
+ return $pos;
+ }
+
+ /**
+ * Returns the primary hash value for a (node, character) key pair.
+ */
+ private function primaryHash(int $keyNode, int $keyCharHash): int
+ {
+ return $keyCharHash ^ (13 * $keyNode);
+ }
+
+ /**
+ * Returns the secondary hash value for a (node, character) key pair.
+ */
+ private function secondaryHash(int $keyNode, int $keyCharHash): int
+ {
+ $result = $this->posMod(($keyCharHash ^ (1025 * $keyNode)));
+
+ if ($result == 0) {
+ return 2;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the smallest non-negative number congruent to x modulo
+ * {@link #tableSize}.
+ */
+ private function posMod(int $x): int
+ {
+ $x %= $this->tableSize;
+
+ if ($x < 0) {
+ $x += $this->tableSize;
+ }
+
+ return $x;
+ }
+}
diff --git a/src/Detector/Strategy/SuffixTree/Token.php b/src/Detector/Strategy/SuffixTree/Token.php
new file mode 100644
index 00000000..80115024
--- /dev/null
+++ b/src/Detector/Strategy/SuffixTree/Token.php
@@ -0,0 +1,42 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTree;
+
+class Token extends AbstractToken
+{
+ public function __construct(
+ int $tokenCode,
+ string $tokenName,
+ int $line,
+ string $file,
+ string $content
+ ) {
+ $this->tokenCode = $tokenCode;
+ $this->tokenName = $tokenName;
+ $this->line = $line;
+ $this->content = $content;
+ $this->file = $file;
+ }
+
+ public function __toString(): string
+ {
+ return $this->tokenName;
+ }
+
+ public function hashCode(): int
+ {
+ return crc32($this->content);
+ }
+
+ public function equals(AbstractToken $other): bool
+ {
+ return $other->hashCode() === $this->hashCode();
+ }
+}
diff --git a/src/Detector/Strategy/SuffixTreeStrategy.php b/src/Detector/Strategy/SuffixTreeStrategy.php
new file mode 100644
index 00000000..bb96b89d
--- /dev/null
+++ b/src/Detector/Strategy/SuffixTreeStrategy.php
@@ -0,0 +1,110 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace SebastianBergmann\PHPCPD\Detector\Strategy;
+
+use function array_keys;
+use function file_get_contents;
+use function is_array;
+use function token_get_all;
+use SebastianBergmann\PHPCPD\CodeClone;
+use SebastianBergmann\PHPCPD\CodeCloneFile;
+use SebastianBergmann\PHPCPD\CodeCloneMap;
+use SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTree\AbstractToken;
+use SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTree\ApproximateCloneDetectingSuffixTree;
+use SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTree\Sentinel;
+use SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTree\Token;
+use SebastianBergmann\PHPCPD\MissingResultException;
+
+/**
+ * The suffix tree strategy was implemented in PHP for PHPCPD by Olle Härstedt.
+ *
+ * This PHP implementation is based on the Java implementation archived that is
+ * available at https://www.cqse.eu/en/news/blog/conqat-end-of-life/ under the
+ * Apache License 2.0.
+ *
+ * The aforementioned Java implementation is based on the algorithm described in
+ * https://dl.acm.org/doi/10.1109/ICSE.2009.5070547. This paper is available at
+ * https://www.cqse.eu/fileadmin/content/news/publications/2009-do-code-clones-matter.pdf.
+ */
+final class SuffixTreeStrategy extends AbstractStrategy
+{
+ /**
+ * @psalm-var list
+ */
+ private array $word = [];
+
+ private ?CodeCloneMap $result = null;
+
+ public function processFile(string $file, CodeCloneMap $result): void
+ {
+ $content = file_get_contents($file);
+ $tokens = token_get_all($content);
+
+ foreach (array_keys($tokens) as $key) {
+ $token = $tokens[$key];
+
+ if (is_array($token) && !isset($this->tokensIgnoreList[$token[0]])) {
+ $this->word[] = new Token(
+ $token[0],
+ token_name($token[0]),
+ $token[2],
+ $file,
+ $token[1]
+ );
+ }
+ }
+
+ $this->result = $result;
+ }
+
+ /**
+ * @throws MissingResultException
+ */
+ public function postProcess(): void
+ {
+ if (empty($this->result)) {
+ throw new MissingResultException('Missing result');
+ }
+
+ // Sentinel = End of word
+ $this->word[] = new Sentinel;
+
+ $cloneInfos = (new ApproximateCloneDetectingSuffixTree($this->word))->findClones(
+ $this->config->minTokens(),
+ $this->config->editDistance(),
+ $this->config->headEquality()
+ );
+
+ foreach ($cloneInfos as $cloneInfo) {
+ /** @var int[] */
+ $others = $cloneInfo->otherClones->extractFirstList();
+
+ for ($j = 0; $j < count($others); $j++) {
+ $otherStart = $others[$j];
+ $t = $this->word[$otherStart];
+ $lastToken = $this->word[$cloneInfo->position + $cloneInfo->length];
+ // If we stumbled upon the Sentinel, rewind one step.
+ if ($lastToken instanceof Sentinel) {
+ $lastToken = $this->word[$cloneInfo->position + $cloneInfo->length - 2];
+ }
+ $lines = $lastToken->line - $cloneInfo->token->line;
+ $this->result->add(
+ new CodeClone(
+ new CodeCloneFile($cloneInfo->token->file, $cloneInfo->token->line),
+ new CodeCloneFile($t->file, $t->line),
+ $lines,
+ // TODO: Double check this
+ $otherStart + 1 - $cloneInfo->position
+ )
+ );
+ }
+ }
+ }
+}
diff --git a/src/Exceptions/ArgumentsBuilderException.php b/src/Exceptions/ArgumentsBuilderException.php
new file mode 100644
index 00000000..c84e6e27
--- /dev/null
+++ b/src/Exceptions/ArgumentsBuilderException.php
@@ -0,0 +1,16 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace SebastianBergmann\PHPCPD;
+
+use RuntimeException;
+
+final class ArgumentsBuilderException extends RuntimeException implements Exception
+{
+}
diff --git a/src/Exceptions/Exception.php b/src/Exceptions/Exception.php
new file mode 100644
index 00000000..84a622cf
--- /dev/null
+++ b/src/Exceptions/Exception.php
@@ -0,0 +1,16 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace SebastianBergmann\PHPCPD;
+
+use Throwable;
+
+interface Exception extends Throwable
+{
+}
diff --git a/src/Exceptions/InvalidStrategyException.php b/src/Exceptions/InvalidStrategyException.php
new file mode 100644
index 00000000..70b4314c
--- /dev/null
+++ b/src/Exceptions/InvalidStrategyException.php
@@ -0,0 +1,16 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace SebastianBergmann\PHPCPD;
+
+use RuntimeException;
+
+final class InvalidStrategyException extends RuntimeException implements Exception
+{
+}
diff --git a/src/Exceptions/MissingResultException.php b/src/Exceptions/MissingResultException.php
new file mode 100644
index 00000000..8368e029
--- /dev/null
+++ b/src/Exceptions/MissingResultException.php
@@ -0,0 +1,16 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace SebastianBergmann\PHPCPD;
+
+use RuntimeException;
+
+final class MissingResultException extends RuntimeException implements Exception
+{
+}
diff --git a/src/Exceptions/OutOfBoundsException.php b/src/Exceptions/OutOfBoundsException.php
new file mode 100644
index 00000000..ade3a533
--- /dev/null
+++ b/src/Exceptions/OutOfBoundsException.php
@@ -0,0 +1,14 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace SebastianBergmann\PHPCPD;
+
+final class OutOfBoundsException extends \OutOfBoundsException implements Exception
+{
+}
diff --git a/src/Log/AbstractXmlLogger.php b/src/Log/AbstractXmlLogger.php
index 97d7da6c..51b386bd 100644
--- a/src/Log/AbstractXmlLogger.php
+++ b/src/Log/AbstractXmlLogger.php
@@ -1,128 +1,73 @@
-.
- * All rights reserved.
+ * (c) Sebastian Bergmann
*
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in
- * the documentation and/or other materials provided with the
- * distribution.
- *
- * * Neither the name of Sebastian Bergmann nor the names of his
- * contributors may be used to endorse or promote products derived
- * from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
- * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
- * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
- * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
- * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- *
- * @package phpcpd
- * @author Sebastian Bergmann
- * @copyright 2009-2013 Sebastian Bergmann
- * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
- * @since File available since Release 1.0.0
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
*/
-
namespace SebastianBergmann\PHPCPD\Log;
+use const ENT_COMPAT;
+use function file_put_contents;
+use function htmlspecialchars;
+use function mb_convert_encoding;
+use function ord;
+use function preg_replace;
+use function strlen;
+use DOMDocument;
use SebastianBergmann\PHPCPD\CodeCloneMap;
-/**
- * Base class for XML loggers.
- *
- * @author Sebastian Bergmann
- * @copyright 2009-2013 Sebastian Bergmann
- * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
- * @link http://github.com/sebastianbergmann/phpcpd/tree
- * @since Class available since Release 1.0.0
- */
abstract class AbstractXmlLogger
{
- protected $document;
-
- /**
- * Constructor.
- *
- * @param string $filename
- */
- public function __construct($filename)
+ protected DOMDocument $document;
+
+ private string $filename;
+
+ public function __construct(string $filename)
{
- $this->document = new \DOMDocument('1.0', 'UTF-8');
+ $this->document = new DOMDocument('1.0', 'UTF-8');
$this->document->formatOutput = true;
$this->filename = $filename;
}
- /**
- * Writes the XML document to the file.
- */
- protected function flush()
+ abstract public function processClones(CodeCloneMap $clones): void;
+
+ protected function flush(): void
{
file_put_contents($this->filename, $this->document->saveXML());
}
- /**
- * Converts a string to UTF-8 encoding.
- *
- * @param string $string
- * @return string
- */
- protected function convertToUtf8($string)
+ protected function convertToUtf8(string $string): string
{
if (!$this->isUtf8($string)) {
- if (function_exists('mb_convert_encoding')) {
- $string = mb_convert_encoding($string, 'UTF-8');
- } else {
- $string = utf8_encode($string);
- }
+ $string = mb_convert_encoding($string, 'UTF-8');
}
return $string;
}
- /**
- * Checks a string for UTF-8 encoding.
- *
- * @param string $string
- * @return boolean
- */
- protected function isUtf8($string)
+ protected function isUtf8(string $string): bool
{
$length = strlen($string);
for ($i = 0; $i < $length; $i++) {
if (ord($string[$i]) < 0x80) {
$n = 0;
- } elseif ((ord($string[$i]) & 0xE0) == 0xC0) {
+ } elseif ((ord($string[$i]) & 0xE0) === 0xC0) {
$n = 1;
- } elseif ((ord($string[$i]) & 0xF0) == 0xE0) {
+ } elseif ((ord($string[$i]) & 0xF0) === 0xE0) {
$n = 2;
- } elseif ((ord($string[$i]) & 0xF0) == 0xF0) {
+ } elseif ((ord($string[$i]) & 0xF0) === 0xF0) {
$n = 3;
} else {
return false;
}
for ($j = 0; $j < $n; $j++) {
- if ((++$i == $length) || ((ord($string[$i]) & 0xC0) != 0x80)) {
+ if ((++$i === $length) || ((ord($string[$i]) & 0xC0) !== 0x80)) {
return false;
}
}
@@ -131,10 +76,16 @@ protected function isUtf8($string)
return true;
}
- /**
- * Processes a list of clones.
- *
- * @param CodeCloneMap $clones
- */
- abstract public function processClones(CodeCloneMap $clones);
+ protected function escapeForXml(string $string): string
+ {
+ $string = $this->convertToUtf8($string);
+
+ $string = preg_replace(
+ '/[^\x09\x0A\x0D\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]/u',
+ "\xEF\xBF\xBD",
+ $string
+ );
+
+ return htmlspecialchars($string, ENT_COMPAT);
+ }
}
diff --git a/src/Log/PMD.php b/src/Log/PMD.php
index 021a32f9..b2a02621 100644
--- a/src/Log/PMD.php
+++ b/src/Log/PMD.php
@@ -1,69 +1,23 @@
-.
- * All rights reserved.
+ * (c) Sebastian Bergmann
*
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in
- * the documentation and/or other materials provided with the
- * distribution.
- *
- * * Neither the name of Sebastian Bergmann nor the names of his
- * contributors may be used to endorse or promote products derived
- * from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
- * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
- * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
- * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
- * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- *
- * @package phpcpd
- * @author Sebastian Bergmann
- * @copyright 2009-2013 Sebastian Bergmann
- * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
- * @since File available since Release 1.0.0
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
*/
-
namespace SebastianBergmann\PHPCPD\Log;
use SebastianBergmann\PHPCPD\CodeCloneMap;
-/**
- * Implementation of AbstractXmlLogger that writes in PMD-CPD format.
- *
- * @author Sebastian Bergmann
- * @copyright 2009-2013 Sebastian Bergmann
- * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
- * @link http://github.com/sebastianbergmann/phpcpd/tree
- * @since Class available since Release 1.0.0
- */
-class PMD extends AbstractXmlLogger
+final class PMD extends AbstractXmlLogger
{
- /**
- * Processes a list of clones.
- *
- * @param CodeCloneMap $clones
- */
- public function processClones(CodeCloneMap $clones)
+ /** @noinspection UnusedFunctionResultInspection */
+ public function processClones(CodeCloneMap $clones): void
{
$cpd = $this->document->createElement('pmd-cpd');
+
$this->document->appendChild($cpd);
foreach ($clones as $clone) {
@@ -71,27 +25,22 @@ public function processClones(CodeCloneMap $clones)
$this->document->createElement('duplication')
);
- $duplication->setAttribute('lines', $clone->getSize());
- $duplication->setAttribute('tokens', $clone->getTokens());
+ $duplication->setAttribute('lines', (string) $clone->numberOfLines());
+ $duplication->setAttribute('tokens', (string) $clone->numberOfTokens());
- foreach ($clone->getFiles() as $codeCloneFile) {
+ foreach ($clone->files() as $codeCloneFile) {
$file = $duplication->appendChild(
$this->document->createElement('file')
);
- $file->setAttribute('path', $codeCloneFile->getName());
- $file->setAttribute('line', $codeCloneFile->getStartLine());
-
+ $file->setAttribute('path', $codeCloneFile->name());
+ $file->setAttribute('line', (string) $codeCloneFile->startLine());
}
$duplication->appendChild(
$this->document->createElement(
'codefragment',
- htmlspecialchars(
- $this->convertToUtf8($clone->getLines()),
- ENT_COMPAT,
- 'UTF-8'
- )
+ $this->escapeForXml($clone->lines())
)
);
}
diff --git a/src/Log/Text.php b/src/Log/Text.php
index 35192fa2..5acb0fbc 100644
--- a/src/Log/Text.php
+++ b/src/Log/Text.php
@@ -1,123 +1,68 @@
-.
- * All rights reserved.
+ * (c) Sebastian Bergmann
*
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in
- * the documentation and/or other materials provided with the
- * distribution.
- *
- * * Neither the name of Sebastian Bergmann nor the names of his
- * contributors may be used to endorse or promote products derived
- * from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
- * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
- * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
- * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
- * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- *
- * @package phpcpd
- * @author Sebastian Bergmann
- * @copyright 2009-2013 Sebastian Bergmann
- * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
- * @since File available since Release 2.0.0
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
*/
-
namespace SebastianBergmann\PHPCPD\Log;
+use const PHP_EOL;
+use function count;
+use function printf;
use SebastianBergmann\PHPCPD\CodeCloneMap;
-use SebastianBergmann\PHPCPD\CodeClone;
-use Symfony\Component\Console\Output\OutputInterface;
-/**
- * A ResultPrinter for the TextUI.
- *
- * @author Sebastian Bergmann
- * @copyright 2009-2013 Sebastian Bergmann
- * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
- * @link http://github.com/sebastianbergmann/phpcpd/tree
- * @since Class available since Release 2.0.0
- */
-class Text
+final class Text
{
- /**
- * Prints a result set from Detector::copyPasteDetection().
- *
- * @param OutputInterface $output
- * @param CodeCloneMap $clones
- */
- public function printResult(OutputInterface $output, CodeCloneMap $clones)
+ public function printResult(CodeCloneMap $clones, bool $verbose): void
{
- $numClones = count($clones);
- $verbose = $output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL;
+ if (count($clones) > 0) {
+ printf(
+ 'Found %d code clones with %d duplicated lines in %d files:' . PHP_EOL . PHP_EOL,
+ count($clones),
+ $clones->numberOfDuplicatedLines(),
+ $clones->numberOfFilesWithClones()
+ );
+ }
- if ($numClones > 0) {
- $buffer = '';
- $files = array();
- $lines = 0;
+ foreach ($clones as $clone) {
+ $firstOccurrence = true;
- foreach ($clones as $clone) {
- foreach ($clone->getFiles() as $file) {
- $filename = $file->getName();
+ foreach ($clone->files() as $file) {
+ printf(
+ ' %s%s:%d-%d%s' . PHP_EOL,
+ $firstOccurrence ? '- ' : ' ',
+ $file->name(),
+ $file->startLine(),
+ $file->startLine() + $clone->numberOfLines(),
+ $firstOccurrence ? ' (' . $clone->numberOfLines() . ' lines)' : ''
+ );
- if (!isset($files[$filename])) {
- $files[$filename] = true;
- }
- }
+ $firstOccurrence = false;
+ }
- $lines += $clone->getSize() * (count($clone->getFiles()) - 1);
- $buffer .= "\n -";
+ if ($verbose) {
+ print PHP_EOL . $clone->lines(' ');
+ }
- foreach ($clone->getFiles() as $file) {
- $buffer .= sprintf(
- "\r\t%s:%d-%d\n ",
- $file->getName(),
- $file->getStartLine(),
- $file->getStartLine() + $clone->getSize()
- );
- }
+ print PHP_EOL;
+ }
- if ($verbose) {
- $buffer .= "\n" . $clone->getLines(' ');
- }
- }
+ if ($clones->isEmpty()) {
+ print 'No code clones found.' . PHP_EOL . PHP_EOL;
- $output->write(
- sprintf(
- "Found %d exact clones with %d duplicated lines in %d files:\n%s",
- $numClones,
- $lines,
- count($files),
- $buffer
- )
- );
+ return;
}
- $output->write(
- sprintf(
- "%s%s duplicated lines out of %d total lines of code.\n\n",
- $numClones > 0 ? "\n" : '',
- $clones->getPercentage(),
- $clones->getNumLines()
- )
+ printf(
+ '%s duplicated lines out of %d total lines of code.' . PHP_EOL .
+ 'Average code clone size is %d lines, the largest code clone has %d lines' . PHP_EOL . PHP_EOL,
+ $clones->percentage(),
+ $clones->numberOfLines(),
+ $clones->averageSize(),
+ $clones->largestSize()
);
}
}
diff --git a/src/autoload.php b/src/autoload.php
deleted file mode 100644
index c951e6e0..00000000
--- a/src/autoload.php
+++ /dev/null
@@ -1,72 +0,0 @@
-.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in
- * the documentation and/or other materials provided with the
- * distribution.
- *
- * * Neither the name of Sebastian Bergmann nor the names of his
- * contributors may be used to endorse or promote products derived
- * from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
- * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
- * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
- * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
- * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- *
- * @package phpcpd
- * @author Sebastian Bergmann
- * @copyright 2009-2013 Sebastian Bergmann
- * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
- * @since File available since Release 1.1.0
- */
-
-require_once 'SebastianBergmann/FinderFacade/autoload.php';
-require_once 'SebastianBergmann/Version/autoload.php';
-require_once 'Symfony/Component/Console/autoloader.php';
-require_once 'PHP/Timer/Autoload.php';
-
-spl_autoload_register(
- function ($class) {
- static $classes = null;
- if ($classes === null) {
- $classes = array(
- 'sebastianbergmann\\phpcpd\\cli\\application' => '/CLI/Application.php',
- 'sebastianbergmann\\phpcpd\\cli\\command' => '/CLI/Command.php',
- 'sebastianbergmann\\phpcpd\\codeclone' => '/CodeClone.php',
- 'sebastianbergmann\\phpcpd\\codeclonefile' => '/CodeCloneFile.php',
- 'sebastianbergmann\\phpcpd\\codeclonemap' => '/CodeCloneMap.php',
- 'sebastianbergmann\\phpcpd\\detector\\detector' => '/Detector/Detector.php',
- 'sebastianbergmann\\phpcpd\\detector\\strategy\\abstractstrategy' => '/Detector/Strategy/Abstract.php',
- 'sebastianbergmann\\phpcpd\\detector\\strategy\\defaultstrategy' => '/Detector/Strategy/Default.php',
- 'sebastianbergmann\\phpcpd\\log\\abstractxmllogger' => '/Log/AbstractXmlLogger.php',
- 'sebastianbergmann\\phpcpd\\log\\pmd' => '/Log/PMD.php',
- 'sebastianbergmann\\phpcpd\\log\\text' => '/Log/Text.php'
- );
- }
- $cn = strtolower($class);
- if (isset($classes[$cn])) {
- require __DIR__ . $classes[$cn];
- }
- }
-);
diff --git a/src/autoload.php.in b/src/autoload.php.in
deleted file mode 100644
index 2f41bdbb..00000000
--- a/src/autoload.php.in
+++ /dev/null
@@ -1,62 +0,0 @@
-.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in
- * the documentation and/or other materials provided with the
- * distribution.
- *
- * * Neither the name of Sebastian Bergmann nor the names of his
- * contributors may be used to endorse or promote products derived
- * from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
- * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
- * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
- * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
- * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- *
- * @package phpcpd
- * @author Sebastian Bergmann
- * @copyright 2009-2013 Sebastian Bergmann
- * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
- * @since File available since Release 1.1.0
- */
-
-require_once 'SebastianBergmann/FinderFacade/autoload.php';
-require_once 'SebastianBergmann/Version/autoload.php';
-require_once 'Symfony/Component/Console/autoloader.php';
-require_once 'PHP/Timer/Autoload.php';
-
-spl_autoload_register(
- function ($class) {
- static $classes = null;
- if ($classes === null) {
- $classes = array(
- ___CLASSLIST___
- );
- }
- $cn = strtolower($class);
- if (isset($classes[$cn])) {
- require ___BASEDIR___$classes[$cn];
- }
- }
-);
diff --git a/tests/DetectorTest.php b/tests/DetectorTest.php
deleted file mode 100644
index 49f62d5a..00000000
--- a/tests/DetectorTest.php
+++ /dev/null
@@ -1,321 +0,0 @@
-.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in
- * the documentation and/or other materials provided with the
- * distribution.
- *
- * * Neither the name of Sebastian Bergmann nor the names of his
- * contributors may be used to endorse or promote products derived
- * from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
- * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
- * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
- * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
- * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- *
- * @package phpcpd
- * @author Sebastian Bergmann
- * @copyright 2009-2013 Sebastian Bergmann
- * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
- * @since File available since Release 1.0.0
- */
-
-if (!defined('TEST_FILES_PATH')) {
- define(
- 'TEST_FILES_PATH',
- dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR
- );
-}
-
-/**
- * Tests for the PHPCPD code analyser.
- *
- * @author Sebastian Bergmann
- * @copyright 2009-2013 Sebastian Bergmann
- * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
- * @link http://github.com/sebastianbergmann/phpcpd/tree
- * @since Class available since Release 1.0.0
- */
-class PHPCPD_DetectorTest extends PHPUnit_Framework_TestCase
-{
- /**
- * @covers SebastianBergmann\PHPCPD\Detector\Detector::copyPasteDetection
- * @covers SebastianBergmann\PHPCPD\CodeClone::getLines
- * @dataProvider strategyProvider
- */
- public function testDetectingSimpleClonesWorks($strategy)
- {
- $detector = new SebastianBergmann\PHPCPD\Detector\Detector(new $strategy);
-
- $clones = $detector->copyPasteDetection(
- array(TEST_FILES_PATH . 'Math.php')
- );
-
- $clones = $clones->getClones();
- $files = $clones[0]->getFiles();
- $file = current($files);
-
- $this->assertEquals(TEST_FILES_PATH . 'Math.php', $file->getName());
- $this->assertEquals(85, $file->getStartLine());
-
- $file = next($files);
-
- $this->assertEquals(TEST_FILES_PATH . 'Math.php', $file->getName());
- $this->assertEquals(149, $file->getStartLine());
- $this->assertEquals(59, $clones[0]->getSize());
- $this->assertEquals(136, $clones[0]->getTokens());
-
- $this->assertEquals(
- ' public function div($v1, $v2)
- {
- $v3 = $v1 / ($v2 + $v1);
- if ($v3 > 14)
- {
- $v4 = 0;
- for ($i = 0; $i < $v3; $i++)
- {
- $v4 += ($v2 * $i);
- }
- }
- $v5 = ($v4 < $v3 ? ($v3 - $v4) : ($v4 - $v3));
-
- $v6 = ($v1 * $v2 * $v3 * $v4 * $v5);
-
- $d = array($v1, $v2, $v3, $v4, $v5, $v6);
-
- $v7 = 1;
- for ($i = 0; $i < $v6; $i++)
- {
- shuffle( $d );
- $v7 = $v7 + $i * end($d);
- }
-
- $v8 = $v7;
- foreach ( $d as $x )
- {
- $v8 *= $x;
- }
-
- $v3 = $v1 / ($v2 + $v1);
- if ($v3 > 14)
- {
- $v4 = 0;
- for ($i = 0; $i < $v3; $i++)
- {
- $v4 += ($v2 * $i);
- }
- }
- $v5 = ($v4 < $v3 ? ($v3 - $v4) : ($v4 - $v3));
-
- $v6 = ($v1 * $v2 * $v3 * $v4 * $v5);
-
- $d = array($v1, $v2, $v3, $v4, $v5, $v6);
-
- $v7 = 1;
- for ($i = 0; $i < $v6; $i++)
- {
- shuffle( $d );
- $v7 = $v7 + $i * end($d);
- }
-
- $v8 = $v7;
- foreach ( $d as $x )
- {
- $v8 *= $x;
- }
-
- return $v8;
-',
- $clones[0]->getLines()
- );
- }
-
- /**
- * @covers SebastianBergmann\PHPCPD\Detector\Detector::copyPasteDetection
- * @dataProvider strategyProvider
- */
- public function testDetectingExactDuplicateFilesWorks($strategy)
- {
- $detector = new SebastianBergmann\PHPCPD\Detector\Detector(new $strategy);
-
- $clones = $detector->copyPasteDetection(array(
- TEST_FILES_PATH . 'a.php',
- TEST_FILES_PATH . 'b.php'
- ), 20, 60);
-
- $clones = $clones->getClones();
- $files = $clones[0]->getFiles();
- $file = current($files);
-
- $this->assertCount(1, $clones);
- $this->assertEquals(TEST_FILES_PATH . 'a.php', $file->getName());
- $this->assertEquals(4, $file->getStartLine());
-
- $file = next($files);
-
- $this->assertEquals(TEST_FILES_PATH . 'b.php', $file->getName());
- $this->assertEquals(4, $file->getStartLine());
- $this->assertEquals(20, $clones[0]->getSize());
- $this->assertEquals(60, $clones[0]->getTokens());
- }
-
- /**
- * @covers SebastianBergmann\PHPCPD\Detector\Detector::copyPasteDetection
- * @dataProvider strategyProvider
- */
- public function testDetectingClonesInMoreThanTwoFiles($strategy)
- {
- $detector = new SebastianBergmann\PHPCPD\Detector\Detector(new $strategy);
-
- $clones = $detector->copyPasteDetection(
- array(
- TEST_FILES_PATH . 'a.php',
- TEST_FILES_PATH . 'b.php',
- TEST_FILES_PATH . 'c.php',
- ),
- 20,
- 60
- );
-
- $clones = $clones->getClones();
- $files = $clones[0]->getFiles();
- $file = current($files);
-
- $this->assertCount(1, $clones);
- $this->assertEquals(TEST_FILES_PATH . 'a.php', $file->getName());
- $this->assertEquals(4, $file->getStartLine());
-
- $file = next($files);
-
- $this->assertEquals(TEST_FILES_PATH . 'b.php', $file->getName());
- $this->assertEquals(4, $file->getStartLine());
-
- $file = next($files);
-
- $this->assertEquals(TEST_FILES_PATH . 'c.php', $file->getName());
- $this->assertEquals(4, $file->getStartLine());
- }
-
- /**
- * @covers SebastianBergmann\PHPCPD\Detector\Detector::copyPasteDetection
- * @dataProvider strategyProvider
- */
- public function testClonesAreIgnoredIfTheySpanLessTokensThanMinTokens($strategy)
- {
- $detector = new SebastianBergmann\PHPCPD\Detector\Detector(new $strategy);
-
- $clones = $detector->copyPasteDetection(
- array(
- TEST_FILES_PATH . 'a.php',
- TEST_FILES_PATH . 'b.php'
- ),
- 20,
- 61
- );
-
- $this->assertCount(0, $clones->getClones());
- }
-
- /**
- * @covers SebastianBergmann\PHPCPD\Detector\Detector::copyPasteDetection
- * @dataProvider strategyProvider
- */
- public function testClonesAreIgnoredIfTheySpanLessLinesThanMinLines($strategy)
- {
- $detector = new SebastianBergmann\PHPCPD\Detector\Detector(new $strategy);
-
- $clones = $detector->copyPasteDetection(
- array(
- TEST_FILES_PATH . 'a.php',
- TEST_FILES_PATH . 'b.php'
- ),
- 21,
- 60
- );
-
- $this->assertCount(0, $clones->getClones());
- }
-
- /**
- * @covers SebastianBergmann\PHPCPD\Detector\Detector::copyPasteDetection
- * @dataProvider strategyProvider
- */
- public function testFuzzyClonesAreFound($strategy)
- {
- $detector = new SebastianBergmann\PHPCPD\Detector\Detector(new $strategy);
-
- $clones = $detector->copyPasteDetection(
- array(
- TEST_FILES_PATH . 'a.php',
- TEST_FILES_PATH . 'd.php'
- ),
- 5,
- 20,
- TRUE
- );
-
- $clones = $clones->getClones();
-
- $this->assertCount(1, $clones);
- }
-
- /**
- * @covers SebastianBergmann\PHPCPD\Detector\Detector::copyPasteDetection
- * @dataProvider strategyProvider
- */
- public function testStripComments($strategy)
- {
- $detector = new SebastianBergmann\PHPCPD\Detector\Detector(new $strategy);
- $clones = $detector->copyPasteDetection(
- array(
- TEST_FILES_PATH . 'e.php',
- TEST_FILES_PATH . 'f.php'
- ),
- 8,
- 10,
- TRUE
- );
- $clones = $clones->getClones();
- $this->assertCount(0, $clones);
-
- $clones = $detector->copyPasteDetection(
- array(
- TEST_FILES_PATH . 'e.php',
- TEST_FILES_PATH . 'f.php'
- ),
- 7,
- 10,
- TRUE
- );
- $clones = $clones->getClones();
- $this->assertCount(1, $clones);
- }
-
- public function strategyProvider()
- {
- return array(
- array('SebastianBergmann\\PHPCPD\\Detector\\Strategy\\DefaultStrategy')
- );
- }
-}
diff --git a/tests/_files/Math.php b/tests/fixture/Math.php
similarity index 89%
rename from tests/_files/Math.php
rename to tests/fixture/Math.php
index 10d935f0..9460c7fe 100644
--- a/tests/_files/Math.php
+++ b/tests/fixture/Math.php
@@ -34,22 +34,12 @@
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
- * @package Example
- * @author Manuel Pichler
- * @copyright 2007-2011 Manuel Pichler. All rights reserved.
- * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
* @version SVN: $Id$
- * @link http://www.phpundercontrol.org/
*/
/**
* Simple math class.
*
- * @package Example
- * @author Manuel Pichler
- * @copyright 2007-2011 Manuel Pichler. All rights reserved.
- * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
- * @link http://www.phpundercontrol.org/
*/
class PhpUnderControl_Example_Math
{
diff --git a/tests/_files/a.php b/tests/fixture/a.php
similarity index 100%
rename from tests/_files/a.php
rename to tests/fixture/a.php
diff --git a/tests/_files/b.php b/tests/fixture/b.php
similarity index 100%
rename from tests/_files/b.php
rename to tests/fixture/b.php
diff --git a/tests/_files/c.php b/tests/fixture/c.php
similarity index 100%
rename from tests/_files/c.php
rename to tests/fixture/c.php
diff --git a/tests/_files/d.php b/tests/fixture/d.php
similarity index 100%
rename from tests/_files/d.php
rename to tests/fixture/d.php
diff --git a/tests/_files/e.php b/tests/fixture/e.php
similarity index 91%
rename from tests/_files/e.php
rename to tests/fixture/e.php
index 9474ff89..ee2acbe1 100644
--- a/tests/_files/e.php
+++ b/tests/fixture/e.php
@@ -11,4 +11,4 @@
$d = 4;
$e = 5;
$f = 6;
-$g = 7;
\ No newline at end of file
+$g = 7;
diff --git a/tests/fixture/editdistance1.php b/tests/fixture/editdistance1.php
new file mode 100644
index 00000000..61a13c3a
--- /dev/null
+++ b/tests/fixture/editdistance1.php
@@ -0,0 +1,27 @@
+question_l10ns->rows->row)) {
+ // Edit difference here.
+ if ($bTranslateLinksFields) {
+ $insertdata['question'] = translateLinks('survey', $iOldSID, $iNewSID, $insertdata['question']);
+ $insertdata['help'] = translateLinks('survey', $iOldSID, $iNewSID, $insertdata['help']);
+ }
+ $oQuestionL10n = new QuestionL10n();
+ $oQuestionL10n->question = $insertdata['question'];
+ $oQuestionL10n->help = $insertdata['help'];
+ $oQuestionL10n->language = $insertdata['language'];
+ unset($insertdata['question']);
+ unset($insertdata['help']);
+ unset($insertdata['language']);
+}
+
+// For some reason, two exact files will lead to one 0-line clone.
+$a = 10;
diff --git a/tests/fixture/editdistance2.php b/tests/fixture/editdistance2.php
new file mode 100644
index 00000000..14b44676
--- /dev/null
+++ b/tests/fixture/editdistance2.php
@@ -0,0 +1,24 @@
+question_l10ns->rows->row)) {
+ // Edit difference here.
+ if ($options['translinkfields']) {
+ $insertdata['question'] = translateLinks('survey', $iOldSID, $iNewSID, $insertdata['question']);
+ $insertdata['help'] = translateLinks('survey', $iOldSID, $iNewSID, $insertdata['help']);
+ }
+ $oQuestionL10n = new QuestionL10n();
+ $oQuestionL10n->question = $insertdata['question'];
+ $oQuestionL10n->help = $insertdata['help'];
+ $oQuestionL10n->language = $insertdata['language'];
+ unset($insertdata['question']);
+ unset($insertdata['help']);
+ unset($insertdata['language']);
+}
+
+foo();
diff --git a/tests/_files/f.php b/tests/fixture/f.php
similarity index 91%
rename from tests/_files/f.php
rename to tests/fixture/f.php
index 9474ff89..ee2acbe1 100644
--- a/tests/_files/f.php
+++ b/tests/fixture/f.php
@@ -11,4 +11,4 @@
$d = 4;
$e = 5;
$f = 6;
-$g = 7;
\ No newline at end of file
+$g = 7;
diff --git a/tests/fixture/pmd_expected.xml b/tests/fixture/pmd_expected.xml
new file mode 100644
index 00000000..c2e1fe6b
--- /dev/null
+++ b/tests/fixture/pmd_expected.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+ function getAsciiEscapeChar()
+{
+ return "�";
+}
+
+
+
diff --git a/tests/fixture/type3_clone.php b/tests/fixture/type3_clone.php
new file mode 100644
index 00000000..5557e0bd
--- /dev/null
+++ b/tests/fixture/type3_clone.php
@@ -0,0 +1,40 @@
+ $b) {
+ return 'foo';
+ } else {
+ return 'bar';
+ }
+}
+
+function bar()
+{
+ $a = 10;
+ $b = 20;
+ if ($a > $b) {
+ } else {
+ return 'bar';
+ }
+}
+
+function bar()
+{
+ $a = 10;
+ $b = '20';
+ if ($a) {
+ return 'foo';
+ } else {
+ return 'bar';
+ }
+}
diff --git a/tests/fixture/with_ascii_escape.php b/tests/fixture/with_ascii_escape.php
new file mode 100644
index 00000000..5cbf7b37
--- /dev/null
+++ b/tests/fixture/with_ascii_escape.php
@@ -0,0 +1,11 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace SebastianBergmann\PHPCPD\Detector;
+
+use function current;
+use function next;
+use function sort;
+use PHPUnit\Framework\TestCase;
+use SebastianBergmann\PHPCPD\ArgumentsBuilder;
+use SebastianBergmann\PHPCPD\Detector\Strategy\AbstractStrategy;
+use SebastianBergmann\PHPCPD\Detector\Strategy\DefaultStrategy;
+use SebastianBergmann\PHPCPD\Detector\Strategy\StrategyConfiguration;
+
+/**
+ * @covers \SebastianBergmann\PHPCPD\Arguments
+ * @covers \SebastianBergmann\PHPCPD\ArgumentsBuilder
+ * @covers \SebastianBergmann\PHPCPD\Detector\Detector
+ * @covers \SebastianBergmann\PHPCPD\Detector\Strategy\AbstractStrategy
+ * @covers \SebastianBergmann\PHPCPD\Detector\Strategy\DefaultStrategy
+ * @covers \SebastianBergmann\PHPCPD\Detector\Strategy\StrategyConfiguration
+ *
+ * @uses \SebastianBergmann\PHPCPD\CodeClone
+ * @uses \SebastianBergmann\PHPCPD\CodeCloneFile
+ * @uses \SebastianBergmann\PHPCPD\CodeCloneMap
+ */
+final class DetectorTest extends TestCase
+{
+ /**
+ * @dataProvider strategyProvider
+ *
+ * @psalm-param AbstractStrategy $strategy
+ */
+ public function testDetectingSimpleClonesWorks(AbstractStrategy $strategy): void
+ {
+ $clones = (new Detector($strategy))->copyPasteDetection(
+ [__DIR__ . '/../fixture/Math.php']
+ );
+
+ $clones = $clones->clones();
+ $files = $clones[0]->files();
+ $file = current($files);
+
+ $this->assertSame(__DIR__ . '/../fixture/Math.php', $file->name());
+ $this->assertSame(75, $file->startLine());
+
+ $file = next($files);
+
+ $this->assertSame(__DIR__ . '/../fixture/Math.php', $file->name());
+ $this->assertSame(139, $file->startLine());
+ $this->assertSame(59, $clones[0]->numberOfLines());
+ $this->assertSame(136, $clones[0]->numberOfTokens());
+
+ $this->assertSame(
+ ' public function div($v1, $v2)
+ {
+ $v3 = $v1 / ($v2 + $v1);
+ if ($v3 > 14)
+ {
+ $v4 = 0;
+ for ($i = 0; $i < $v3; $i++)
+ {
+ $v4 += ($v2 * $i);
+ }
+ }
+ $v5 = ($v4 < $v3 ? ($v3 - $v4) : ($v4 - $v3));
+
+ $v6 = ($v1 * $v2 * $v3 * $v4 * $v5);
+
+ $d = array($v1, $v2, $v3, $v4, $v5, $v6);
+
+ $v7 = 1;
+ for ($i = 0; $i < $v6; $i++)
+ {
+ shuffle( $d );
+ $v7 = $v7 + $i * end($d);
+ }
+
+ $v8 = $v7;
+ foreach ( $d as $x )
+ {
+ $v8 *= $x;
+ }
+
+ $v3 = $v1 / ($v2 + $v1);
+ if ($v3 > 14)
+ {
+ $v4 = 0;
+ for ($i = 0; $i < $v3; $i++)
+ {
+ $v4 += ($v2 * $i);
+ }
+ }
+ $v5 = ($v4 < $v3 ? ($v3 - $v4) : ($v4 - $v3));
+
+ $v6 = ($v1 * $v2 * $v3 * $v4 * $v5);
+
+ $d = array($v1, $v2, $v3, $v4, $v5, $v6);
+
+ $v7 = 1;
+ for ($i = 0; $i < $v6; $i++)
+ {
+ shuffle( $d );
+ $v7 = $v7 + $i * end($d);
+ }
+
+ $v8 = $v7;
+ foreach ( $d as $x )
+ {
+ $v8 *= $x;
+ }
+
+ return $v8;
+',
+ $clones[0]->lines()
+ );
+ }
+
+ /**
+ * @dataProvider strategyProvider
+ */
+ public function testDetectingExactDuplicateFilesWorks(AbstractStrategy $strategy): void
+ {
+ $argv = [1 => '.', '--min-lines', '20', '--min-tokens', '50'];
+ $arguments = (new ArgumentsBuilder)->build($argv);
+ $config = new StrategyConfiguration($arguments);
+ $strategy->setConfig($config);
+
+ $clones = (new Detector($strategy))->copyPasteDetection(
+ [
+ __DIR__ . '/../fixture/a.php',
+ __DIR__ . '/../fixture/b.php',
+ ]
+ );
+
+ $clones = $clones->clones();
+ $files = $clones[0]->files();
+ $file = current($files);
+
+ $this->assertCount(1, $clones);
+ $this->assertSame(__DIR__ . '/../fixture/a.php', $file->name());
+ $this->assertSame(4, $file->startLine());
+
+ $file = next($files);
+
+ $this->assertSame(__DIR__ . '/../fixture/b.php', $file->name());
+ $this->assertSame(4, $file->startLine());
+ $this->assertSame(20, $clones[0]->numberOfLines());
+ $this->assertSame(60, $clones[0]->numberOfTokens());
+ }
+
+ /**
+ * @dataProvider strategyProvider
+ */
+ public function testDetectingClonesInMoreThanTwoFiles(AbstractStrategy $strategy): void
+ {
+ $argv = [1 => '.', '--min-lines', '20', '--min-tokens', '60'];
+ $arguments = (new ArgumentsBuilder)->build($argv);
+ $config = new StrategyConfiguration($arguments);
+ $strategy->setConfig($config);
+
+ $clones = (new Detector($strategy))->copyPasteDetection(
+ [
+ __DIR__ . '/../fixture/a.php',
+ __DIR__ . '/../fixture/b.php',
+ __DIR__ . '/../fixture/c.php',
+ ]
+ );
+
+ $clones = $clones->clones();
+ //var_dump($clones);
+ $files = $clones[0]->files();
+ sort($files);
+
+ $file = current($files);
+
+ $this->assertCount(1, $clones);
+ $this->assertSame(__DIR__ . '/../fixture/a.php', $file->name());
+ $this->assertSame(4, $file->startLine());
+
+ $file = next($files);
+
+ $this->assertSame(__DIR__ . '/../fixture/b.php', $file->name());
+ $this->assertSame(4, $file->startLine());
+
+ $file = next($files);
+
+ $this->assertSame(__DIR__ . '/../fixture/c.php', $file->name());
+ $this->assertSame(4, $file->startLine());
+ }
+
+ /**
+ * @dataProvider strategyProvider
+ */
+ public function testClonesAreIgnoredIfTheySpanLessTokensThanMinTokens(AbstractStrategy $strategy): void
+ {
+ $argv = [1 => '.', '--min-lines', '20', '--min-tokens', '61'];
+ $arguments = (new ArgumentsBuilder)->build($argv);
+ $config = new StrategyConfiguration($arguments);
+ $strategy->setConfig($config);
+ $clones = (new Detector($strategy))->copyPasteDetection(
+ [
+ __DIR__ . '/../fixture/a.php',
+ __DIR__ . '/../fixture/b.php',
+ ]
+ );
+
+ $this->assertCount(0, $clones->clones());
+ }
+
+ /**
+ * @dataProvider strategyProvider
+ */
+ public function testClonesAreIgnoredIfTheySpanLessLinesThanMinLines(AbstractStrategy $strategy): void
+ {
+ $argv = [1 => '.', '--min-lines', '21', '--min-tokens', '60'];
+ $arguments = (new ArgumentsBuilder)->build($argv);
+ $config = new StrategyConfiguration($arguments);
+ $strategy->setConfig($config);
+ $clones = (new Detector($strategy))->copyPasteDetection(
+ [
+ __DIR__ . '/../fixture/a.php',
+ __DIR__ . '/../fixture/b.php',
+ ]
+ );
+
+ $this->assertCount(0, $clones->clones());
+ }
+
+ /**
+ * @dataProvider strategyProvider
+ */
+ public function testFuzzyClonesAreFound(AbstractStrategy $strategy): void
+ {
+ $argv = [1 => '.', '--min-lines', '5', '--min-tokens', '20', '--fuzzy', 'true'];
+ $arguments = (new ArgumentsBuilder)->build($argv);
+ $config = new StrategyConfiguration($arguments);
+ $strategy->setConfig($config);
+ $clones = (new Detector($strategy))->copyPasteDetection(
+ [
+ __DIR__ . '/../fixture/a.php',
+ __DIR__ . '/../fixture/d.php',
+ ]
+ );
+
+ $this->assertCount(1, $clones->clones());
+ }
+
+ /**
+ * @dataProvider strategyProvider
+ */
+ public function testStripComments(AbstractStrategy $strategy): void
+ {
+ $argv = [1 => '.', '--min-lines', '8', '--min-tokens', '10', '--fuzzy', 'true'];
+ $arguments = (new ArgumentsBuilder)->build($argv);
+ $config = new StrategyConfiguration($arguments);
+ $strategy->setConfig($config);
+
+ $detector = new Detector($strategy);
+
+ $clones = $detector->copyPasteDetection(
+ [
+ __DIR__ . '/../fixture/e.php',
+ __DIR__ . '/../fixture/f.php',
+ ]
+ );
+
+ $this->assertCount(0, $clones->clones());
+
+ $argv = [1 => '.', '--min-lines', '7', '--min-tokens', '10', '--fuzzy', 'true'];
+ $arguments = (new ArgumentsBuilder)->build($argv);
+ $config = new StrategyConfiguration($arguments);
+ $strategy->setConfig($config);
+
+ $clones = $detector->copyPasteDetection(
+ [
+ __DIR__ . '/../fixture/e.php',
+ __DIR__ . '/../fixture/f.php',
+ ],
+ 7,
+ 10,
+ true
+ );
+
+ $this->assertCount(1, $clones->clones());
+ }
+
+ /**
+ * @psalm-return list
+ */
+ public function strategyProvider(): array
+ {
+ // Build default config.
+ $argv = [1 => '.'];
+ $arguments = (new ArgumentsBuilder)->build($argv);
+ $config = new StrategyConfiguration($arguments);
+
+ return [
+ [new DefaultStrategy($config)],
+ ];
+ }
+}
diff --git a/tests/unit/EditDistanceTest.php b/tests/unit/EditDistanceTest.php
new file mode 100644
index 00000000..51fbf14b
--- /dev/null
+++ b/tests/unit/EditDistanceTest.php
@@ -0,0 +1,75 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace SebastianBergmann\PHPCPD\Detector;
+
+use PHPUnit\Framework\TestCase;
+use SebastianBergmann\PHPCPD\ArgumentsBuilder;
+use SebastianBergmann\PHPCPD\Detector\Strategy\DefaultStrategy;
+use SebastianBergmann\PHPCPD\Detector\Strategy\StrategyConfiguration;
+use SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTreeStrategy;
+
+/**
+ * @covers \SebastianBergmann\PHPCPD\Arguments
+ * @covers \SebastianBergmann\PHPCPD\ArgumentsBuilder
+ * @covers \SebastianBergmann\PHPCPD\Detector\Detector
+ * @covers \SebastianBergmann\PHPCPD\Detector\Strategy\AbstractStrategy
+ * @covers \SebastianBergmann\PHPCPD\Detector\Strategy\DefaultStrategy
+ * @covers \SebastianBergmann\PHPCPD\Detector\Strategy\StrategyConfiguration
+ * @covers \SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTree\ApproximateCloneDetectingSuffixTree
+ * @covers \SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTree\CloneInfo
+ * @covers \SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTree\PairList
+ * @covers \SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTree\Sentinel
+ * @covers \SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTree\SuffixTree
+ * @covers \SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTree\SuffixTreeHashTable
+ * @covers \SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTree\Token
+ * @covers \SebastianBergmann\PHPCPD\Detector\Strategy\SuffixTreeStrategy
+ *
+ * @uses \SebastianBergmann\PHPCPD\CodeClone
+ * @uses \SebastianBergmann\PHPCPD\CodeCloneFile
+ * @uses \SebastianBergmann\PHPCPD\CodeCloneMap
+ */
+final class EditDistanceTest extends TestCase
+{
+ public function testEditDistanceWithSuffixtree(): void
+ {
+ $argv = [1 => '.', '--min-tokens', '60'];
+ $arguments = (new ArgumentsBuilder)->build($argv);
+ $config = new StrategyConfiguration($arguments);
+ $strategy = new SuffixTreeStrategy($config);
+
+ $clones = (new Detector($strategy))->copyPasteDetection(
+ [
+ __DIR__ . '/../fixture/editdistance1.php',
+ __DIR__ . '/../fixture/editdistance2.php',
+ ],
+ );
+
+ $clones = $clones->clones();
+ $this->assertCount(1, $clones);
+ }
+
+ public function testEditDistanceWithRabinkarp(): void
+ {
+ $argv = [1 => '.', '--min-tokens', '60'];
+ $arguments = (new ArgumentsBuilder)->build($argv);
+ $config = new StrategyConfiguration($arguments);
+ $strategy = new DefaultStrategy($config);
+
+ $clones = (new Detector($strategy))->copyPasteDetection(
+ [
+ __DIR__ . '/../fixture/editdistance1.php',
+ __DIR__ . '/../fixture/editdistance2.php',
+ ],
+ );
+
+ $clones = $clones->clones();
+ $this->assertCount(0, $clones);
+ }
+}
diff --git a/tests/unit/PMDTest.php b/tests/unit/PMDTest.php
new file mode 100644
index 00000000..fac9974b
--- /dev/null
+++ b/tests/unit/PMDTest.php
@@ -0,0 +1,95 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace SebastianBergmann\PHPCPD\Log;
+
+use function file_exists;
+use function file_get_contents;
+use function file_put_contents;
+use function strtr;
+use function sys_get_temp_dir;
+use function tempnam;
+use function unlink;
+use PHPUnit\Framework\TestCase;
+use SebastianBergmann\PHPCPD\CodeClone;
+use SebastianBergmann\PHPCPD\CodeCloneFile;
+use SebastianBergmann\PHPCPD\CodeCloneMap;
+
+/**
+ * @covers \SebastianBergmann\PHPCPD\Log\AbstractXmlLogger
+ * @covers \SebastianBergmann\PHPCPD\Log\PMD
+ *
+ * @uses \SebastianBergmann\PHPCPD\CodeClone
+ * @uses \SebastianBergmann\PHPCPD\CodeCloneFile
+ * @uses \SebastianBergmann\PHPCPD\CodeCloneMap
+ * @uses \SebastianBergmann\PHPCPD\CodeCloneMapIterator
+ */
+final class PMDTest extends TestCase
+{
+ private string $testFile1;
+
+ private string $testFile2;
+
+ private string|false $pmdLogFile;
+
+ private string|false $expectedPmdLogFile;
+
+ private PMD $pmdLogger;
+
+ protected function setUp(): void
+ {
+ $this->testFile1 = __DIR__ . '/../fixture/with_ascii_escape.php';
+ $this->testFile2 = __DIR__ . '/../fixture/with_ascii_escape2.php';
+
+ $this->pmdLogFile = tempnam(sys_get_temp_dir(), 'pmd');
+
+ $this->expectedPmdLogFile = tempnam(sys_get_temp_dir(), 'pmd');
+ $expectedPmdLogTemplate = __DIR__ . '/../fixture/pmd_expected.xml';
+
+ $expectedPmdLogContents = strtr(
+ file_get_contents($expectedPmdLogTemplate),
+ [
+ '%file1%' => $this->testFile1,
+ '%file2%' => $this->testFile2,
+ ]
+ );
+
+ file_put_contents($this->expectedPmdLogFile, $expectedPmdLogContents);
+
+ $this->pmdLogger = new PMD($this->pmdLogFile);
+ }
+
+ protected function tearDown(): void
+ {
+ if (file_exists($this->pmdLogFile)) {
+ unlink($this->pmdLogFile);
+ }
+
+ if (file_exists($this->expectedPmdLogFile)) {
+ unlink($this->expectedPmdLogFile);
+ }
+ }
+
+ public function testSubstitutesDisallowedCharacters(): void
+ {
+ $file1 = new CodeCloneFile($this->testFile1, 8);
+ $file2 = new CodeCloneFile($this->testFile2, 8);
+ $clone = new CodeClone($file1, $file2, 4, 4);
+ $cloneMap = new CodeCloneMap;
+
+ $cloneMap->add($clone);
+
+ $this->pmdLogger->processClones($cloneMap);
+
+ $this->assertXmlFileEqualsXmlFile(
+ $this->expectedPmdLogFile,
+ $this->pmdLogFile
+ );
+ }
+}
diff --git a/tools/composer b/tools/composer
new file mode 100755
index 00000000..decd0873
Binary files /dev/null and b/tools/composer differ
diff --git a/tools/php-cs-fixer b/tools/php-cs-fixer
new file mode 100755
index 00000000..bdacf822
Binary files /dev/null and b/tools/php-cs-fixer differ
diff --git a/tools/phpab b/tools/phpab
new file mode 100755
index 00000000..e2ca8045
--- /dev/null
+++ b/tools/phpab
@@ -0,0 +1,939 @@
+#!/usr/bin/env php
+ '/vendor/zetacomponents/base/src/base.php',
+ 'ezcbaseconfigurationinitializer' => '/vendor/zetacomponents/base/src/interfaces/configuration_initializer.php',
+ 'ezcbasedoubleclassrepositoryprefixexception' => '/vendor/zetacomponents/base/src/exceptions/double_class_repository_prefix.php',
+ 'ezcbaseexception' => '/vendor/zetacomponents/base/src/exceptions/exception.php',
+ 'ezcbaseexportable' => '/vendor/zetacomponents/base/src/interfaces/exportable.php',
+ 'ezcbaseextensionnotfoundexception' => '/vendor/zetacomponents/base/src/exceptions/extension_not_found.php',
+ 'ezcbasefeatures' => '/vendor/zetacomponents/base/src/features.php',
+ 'ezcbasefile' => '/vendor/zetacomponents/base/src/file.php',
+ 'ezcbasefileexception' => '/vendor/zetacomponents/base/src/exceptions/file_exception.php',
+ 'ezcbasefilefindcontext' => '/vendor/zetacomponents/base/src/structs/file_find_context.php',
+ 'ezcbasefileioexception' => '/vendor/zetacomponents/base/src/exceptions/file_io.php',
+ 'ezcbasefilenotfoundexception' => '/vendor/zetacomponents/base/src/exceptions/file_not_found.php',
+ 'ezcbasefilepermissionexception' => '/vendor/zetacomponents/base/src/exceptions/file_permission.php',
+ 'ezcbasefunctionalitynotsupportedexception' => '/vendor/zetacomponents/base/src/exceptions/functionality_not_supported.php',
+ 'ezcbaseinit' => '/vendor/zetacomponents/base/src/init.php',
+ 'ezcbaseinitcallbackconfiguredexception' => '/vendor/zetacomponents/base/src/exceptions/init_callback_configured.php',
+ 'ezcbaseinitinvalidcallbackclassexception' => '/vendor/zetacomponents/base/src/exceptions/invalid_callback_class.php',
+ 'ezcbaseinvalidparentclassexception' => '/vendor/zetacomponents/base/src/exceptions/invalid_parent_class.php',
+ 'ezcbasemetadata' => '/vendor/zetacomponents/base/src/metadata.php',
+ 'ezcbasemetadatapearreader' => '/vendor/zetacomponents/base/src/metadata/pear.php',
+ 'ezcbasemetadatatarballreader' => '/vendor/zetacomponents/base/src/metadata/tarball.php',
+ 'ezcbaseoptions' => '/vendor/zetacomponents/base/src/options.php',
+ 'ezcbasepersistable' => '/vendor/zetacomponents/base/src/interfaces/persistable.php',
+ 'ezcbasepropertynotfoundexception' => '/vendor/zetacomponents/base/src/exceptions/property_not_found.php',
+ 'ezcbasepropertypermissionexception' => '/vendor/zetacomponents/base/src/exceptions/property_permission.php',
+ 'ezcbaserepositorydirectory' => '/vendor/zetacomponents/base/src/structs/repository_directory.php',
+ 'ezcbasesettingnotfoundexception' => '/vendor/zetacomponents/base/src/exceptions/setting_not_found.php',
+ 'ezcbasesettingvalueexception' => '/vendor/zetacomponents/base/src/exceptions/setting_value.php',
+ 'ezcbasestruct' => '/vendor/zetacomponents/base/src/struct.php',
+ 'ezcbasevalueexception' => '/vendor/zetacomponents/base/src/exceptions/value.php',
+ 'ezcbasewhateverexception' => '/vendor/zetacomponents/base/src/exceptions/whatever.php',
+ 'ezcconsoleargument' => '/vendor/zetacomponents/console-tools/src/input/argument.php',
+ 'ezcconsoleargumentalreadyregisteredexception' => '/vendor/zetacomponents/console-tools/src/exceptions/argument_already_registered.php',
+ 'ezcconsoleargumentexception' => '/vendor/zetacomponents/console-tools/src/exceptions/argument.php',
+ 'ezcconsoleargumentmandatoryviolationexception' => '/vendor/zetacomponents/console-tools/src/exceptions/argument_mandatory_violation.php',
+ 'ezcconsolearguments' => '/vendor/zetacomponents/console-tools/src/input/arguments.php',
+ 'ezcconsoleargumenttypeviolationexception' => '/vendor/zetacomponents/console-tools/src/exceptions/argument_type_violation.php',
+ 'ezcconsoledialog' => '/vendor/zetacomponents/console-tools/src/interfaces/dialog.php',
+ 'ezcconsoledialogabortexception' => '/vendor/zetacomponents/console-tools/src/exceptions/dialog_abort.php',
+ 'ezcconsoledialogoptions' => '/vendor/zetacomponents/console-tools/src/options/dialog.php',
+ 'ezcconsoledialogvalidator' => '/vendor/zetacomponents/console-tools/src/interfaces/dialog_validator.php',
+ 'ezcconsoledialogviewer' => '/vendor/zetacomponents/console-tools/src/dialog_viewer.php',
+ 'ezcconsoleexception' => '/vendor/zetacomponents/console-tools/src/exceptions/exception.php',
+ 'ezcconsoleinput' => '/vendor/zetacomponents/console-tools/src/input.php',
+ 'ezcconsoleinputhelpgenerator' => '/vendor/zetacomponents/console-tools/src/interfaces/input_help_generator.php',
+ 'ezcconsoleinputstandardhelpgenerator' => '/vendor/zetacomponents/console-tools/src/input/help_generators/standard.php',
+ 'ezcconsoleinputvalidator' => '/vendor/zetacomponents/console-tools/src/interfaces/input_validator.php',
+ 'ezcconsoleinvalidoptionnameexception' => '/vendor/zetacomponents/console-tools/src/exceptions/invalid_option_name.php',
+ 'ezcconsoleinvalidoutputtargetexception' => '/vendor/zetacomponents/console-tools/src/exceptions/invalid_output_target.php',
+ 'ezcconsolemenudialog' => '/vendor/zetacomponents/console-tools/src/dialog/menu_dialog.php',
+ 'ezcconsolemenudialogdefaultvalidator' => '/vendor/zetacomponents/console-tools/src/dialog/validators/menu_dialog_default.php',
+ 'ezcconsolemenudialogoptions' => '/vendor/zetacomponents/console-tools/src/options/menu_dialog.php',
+ 'ezcconsolemenudialogvalidator' => '/vendor/zetacomponents/console-tools/src/interfaces/menu_dialog_validator.php',
+ 'ezcconsolenopositionstoredexception' => '/vendor/zetacomponents/console-tools/src/exceptions/no_position_stored.php',
+ 'ezcconsolenovaliddialogresultexception' => '/vendor/zetacomponents/console-tools/src/exceptions/no_valid_dialog_result.php',
+ 'ezcconsoleoption' => '/vendor/zetacomponents/console-tools/src/input/option.php',
+ 'ezcconsoleoptionalreadyregisteredexception' => '/vendor/zetacomponents/console-tools/src/exceptions/option_already_registered.php',
+ 'ezcconsoleoptionargumentsviolationexception' => '/vendor/zetacomponents/console-tools/src/exceptions/option_arguments_violation.php',
+ 'ezcconsoleoptiondependencyviolationexception' => '/vendor/zetacomponents/console-tools/src/exceptions/option_dependency_violation.php',
+ 'ezcconsoleoptionexception' => '/vendor/zetacomponents/console-tools/src/exceptions/option.php',
+ 'ezcconsoleoptionexclusionviolationexception' => '/vendor/zetacomponents/console-tools/src/exceptions/option_exclusion_violation.php',
+ 'ezcconsoleoptionmandatoryviolationexception' => '/vendor/zetacomponents/console-tools/src/exceptions/option_mandatory_violation.php',
+ 'ezcconsoleoptionmissingvalueexception' => '/vendor/zetacomponents/console-tools/src/exceptions/option_missing_value.php',
+ 'ezcconsoleoptionnoaliasexception' => '/vendor/zetacomponents/console-tools/src/exceptions/option_no_alias.php',
+ 'ezcconsoleoptionnotexistsexception' => '/vendor/zetacomponents/console-tools/src/exceptions/option_not_exists.php',
+ 'ezcconsoleoptionrule' => '/vendor/zetacomponents/console-tools/src/structs/option_rule.php',
+ 'ezcconsoleoptionstringnotwellformedexception' => '/vendor/zetacomponents/console-tools/src/exceptions/option_string_not_wellformed.php',
+ 'ezcconsoleoptiontoomanyvaluesexception' => '/vendor/zetacomponents/console-tools/src/exceptions/option_too_many_values.php',
+ 'ezcconsoleoptiontypeviolationexception' => '/vendor/zetacomponents/console-tools/src/exceptions/option_type_violation.php',
+ 'ezcconsoleoutput' => '/vendor/zetacomponents/console-tools/src/output.php',
+ 'ezcconsoleoutputformat' => '/vendor/zetacomponents/console-tools/src/structs/output_format.php',
+ 'ezcconsoleoutputformats' => '/vendor/zetacomponents/console-tools/src/structs/output_formats.php',
+ 'ezcconsoleoutputoptions' => '/vendor/zetacomponents/console-tools/src/options/output.php',
+ 'ezcconsoleprogressbar' => '/vendor/zetacomponents/console-tools/src/progressbar.php',
+ 'ezcconsoleprogressbaroptions' => '/vendor/zetacomponents/console-tools/src/options/progressbar.php',
+ 'ezcconsoleprogressmonitor' => '/vendor/zetacomponents/console-tools/src/progressmonitor.php',
+ 'ezcconsoleprogressmonitoroptions' => '/vendor/zetacomponents/console-tools/src/options/progressmonitor.php',
+ 'ezcconsolequestiondialog' => '/vendor/zetacomponents/console-tools/src/dialog/question_dialog.php',
+ 'ezcconsolequestiondialogcollectionvalidator' => '/vendor/zetacomponents/console-tools/src/dialog/validators/question_dialog_collection.php',
+ 'ezcconsolequestiondialogmappingvalidator' => '/vendor/zetacomponents/console-tools/src/dialog/validators/question_dialog_mapping.php',
+ 'ezcconsolequestiondialogoptions' => '/vendor/zetacomponents/console-tools/src/options/question_dialog.php',
+ 'ezcconsolequestiondialogregexvalidator' => '/vendor/zetacomponents/console-tools/src/dialog/validators/question_dialog_regex.php',
+ 'ezcconsolequestiondialogtypevalidator' => '/vendor/zetacomponents/console-tools/src/dialog/validators/question_dialog_type.php',
+ 'ezcconsolequestiondialogvalidator' => '/vendor/zetacomponents/console-tools/src/interfaces/question_dialog_validator.php',
+ 'ezcconsolestandardinputvalidator' => '/vendor/zetacomponents/console-tools/src/input/validators/standard.php',
+ 'ezcconsolestatusbar' => '/vendor/zetacomponents/console-tools/src/statusbar.php',
+ 'ezcconsolestatusbaroptions' => '/vendor/zetacomponents/console-tools/src/options/statusbar.php',
+ 'ezcconsolestringtool' => '/vendor/zetacomponents/console-tools/src/tools/string.php',
+ 'ezcconsoletable' => '/vendor/zetacomponents/console-tools/src/table.php',
+ 'ezcconsoletablecell' => '/vendor/zetacomponents/console-tools/src/table/cell.php',
+ 'ezcconsoletableoptions' => '/vendor/zetacomponents/console-tools/src/options/table.php',
+ 'ezcconsoletablerow' => '/vendor/zetacomponents/console-tools/src/table/row.php',
+ 'ezcconsoletoomanyargumentsexception' => '/vendor/zetacomponents/console-tools/src/exceptions/argument_too_many.php',
+ 'theseer\\autoload\\application' => '/phpab/Application.php',
+ 'theseer\\autoload\\applicationexception' => '/phpab/Application.php',
+ 'theseer\\autoload\\autoloadbuilderexception' => '/phpab/AutoloadRenderer.php',
+ 'theseer\\autoload\\autoloadrenderer' => '/phpab/AutoloadRenderer.php',
+ 'theseer\\autoload\\cache' => '/phpab/Cache.php',
+ 'theseer\\autoload\\cacheentry' => '/phpab/CacheEntry.php',
+ 'theseer\\autoload\\cacheexception' => '/phpab/Cache.php',
+ 'theseer\\autoload\\cachewarminglistrenderer' => '/phpab/CacheWarmingListRenderer.php',
+ 'theseer\\autoload\\cachingparser' => '/phpab/CachingParser.php',
+ 'theseer\\autoload\\classdependencysorter' => '/phpab/DependencySorter.php',
+ 'theseer\\autoload\\classdependencysorterexception' => '/phpab/DependencySorter.php',
+ 'theseer\\autoload\\cli' => '/phpab/CLI.php',
+ 'theseer\\autoload\\clienvironmentexception' => '/phpab/CLI.php',
+ 'theseer\\autoload\\collector' => '/phpab/Collector.php',
+ 'theseer\\autoload\\collectorexception' => '/phpab/Collector.php',
+ 'theseer\\autoload\\collectorresult' => '/phpab/CollectorResult.php',
+ 'theseer\\autoload\\collectorresultexception' => '/phpab/CollectorResult.php',
+ 'theseer\\autoload\\composeriterator' => '/phpab/ComposerIterator.php',
+ 'theseer\\autoload\\composeriteratorexception' => '/phpab/ComposerIterator.php',
+ 'theseer\\autoload\\config' => '/phpab/Config.php',
+ 'theseer\\autoload\\factory' => '/phpab/Factory.php',
+ 'theseer\\autoload\\logger' => '/phpab/Logger.php',
+ 'theseer\\autoload\\parser' => '/phpab/Parser.php',
+ 'theseer\\autoload\\parseresult' => '/phpab/ParseResult.php',
+ 'theseer\\autoload\\parserexception' => '/phpab/Parser.php',
+ 'theseer\\autoload\\parserinterface' => '/phpab/ParserInterface.php',
+ 'theseer\\autoload\\pathcomparator' => '/phpab/PathComparator.php',
+ 'theseer\\autoload\\pharbuilder' => '/phpab/PharBuilder.php',
+ 'theseer\\autoload\\runner' => '/phpab/Runner.php',
+ 'theseer\\autoload\\sourcefile' => '/phpab/SourceFile.php',
+ 'theseer\\autoload\\staticlistrenderer' => '/phpab/StaticListRenderer.php',
+ 'theseer\\autoload\\staticrenderer' => '/phpab/StaticRenderer.php',
+ 'theseer\\autoload\\staticrequirelistrenderer' => '/phpab/StaticRequireListRenderer.php',
+ 'theseer\\autoload\\unitvisitor' => '/phpab/UnitVisitor.php',
+ 'theseer\\autoload\\version' => '/phpab/Version.php',
+ 'theseer\\directoryscanner\\directoryscanner' => '/vendor/theseer/directoryscanner/src/directoryscanner.php',
+ 'theseer\\directoryscanner\\exception' => '/vendor/theseer/directoryscanner/src/directoryscanner.php',
+ 'theseer\\directoryscanner\\filesonlyfilteriterator' => '/vendor/theseer/directoryscanner/src/filesonlyfilter.php',
+ 'theseer\\directoryscanner\\includeexcludefilteriterator' => '/vendor/theseer/directoryscanner/src/includeexcludefilter.php',
+ 'theseer\\directoryscanner\\phpfilteriterator' => '/vendor/theseer/directoryscanner/src/phpfilter.php'
+ );
+ }
+
+ $class = strtolower($class);
+
+ if (isset($classes[$class])) {
+ require 'phar://phpab.phar' . $classes[$class];
+ }
+ }
+);
+
+Phar::mapPhar('phpab.phar');
+define('PHPAB_VERSION', '1.27.1');
+$factory = new \TheSeer\Autoload\Factory();
+$factory->getCLI()->run();
+exit(0);
+
+__HALT_COMPILER(); ?>
+�* �
+ phpab.phar 8 vendor/theseer/directoryscanner/src/directoryscanner.php�" ���aA �P�� 7 vendor/theseer/directoryscanner/src/filesonlyfilter.php�
+ ���a� ��;l� <