Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
15 / 15
86.67% covered (warning)
86.67%
26 / 30
9.52% covered (danger)
9.52%
4 / 42
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
Suggestion
100.00% covered (success)
100.00%
15 / 15
86.67% covered (warning)
86.67%
26 / 30
9.52% covered (danger)
9.52%
4 / 42
100.00% covered (success)
100.00%
2 / 2
55.40
100.00% covered (success)
100.00%
1 / 1
 find
100.00% covered (success)
100.00%
11 / 11
93.33% covered (success)
93.33%
14 / 15
4.17% covered (danger)
4.17%
1 / 24
100.00% covered (success)
100.00%
1 / 1
27.00
 distance
100.00% covered (success)
100.00%
4 / 4
80.00% covered (warning)
80.00%
12 / 15
16.67% covered (danger)
16.67%
3 / 18
100.00% covered (success)
100.00%
1 / 1
8.21
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Console;
6
7/**
8 * Finds the name a mistyped one was meant to be, for "Did you mean"
9 * hints.
10 *
11 * @internal
12 */
13final class Suggestion
14{
15    /**
16     * The candidate nearest to the typed name, if it is near enough: one
17     * typo per three characters, at least one. A single character has to
18     * match: every other one is just a typo away.
19     *
20     * @param list<string> $candidates The first one wins a tie
21     * @param string $ignore Leading characters that do not count, like
22     *     the dashes of options
23     */
24    public static function find(string $typed, array $candidates, string $ignore = ''): ?string
25    {
26        $typed = ltrim($typed, $ignore);
27        $length = strlen($typed);
28        $best = '';
29        $bestDistance = PHP_INT_MAX;
30
31        foreach ($candidates as $candidate) {
32            $distance = self::distance($typed, ltrim($candidate, $ignore));
33
34            if ($distance < $bestDistance) {
35                $bestDistance = $distance;
36                $best = $candidate;
37            }
38        }
39
40        $limit = $length > 1 ? max(1, intdiv($length, 3)) : 0;
41
42        return $bestDistance <= $limit ? $best : null;
43    }
44
45    /**
46     * The number of typos between two names. The beginning of a name and
47     * a swapped pair of neighbours count as one, where levenshtein() sees
48     * more.
49     */
50    private static function distance(string $typed, string $name): int
51    {
52        $distance = levenshtein($typed, $name);
53
54        // The XOR is zero up to the first difference.
55        $at = strspn($typed ^ $name, "\0");
56        $swapped = substr_replace($typed, strrev(substr($typed, $at, 2)), $at, 2);
57
58        return $swapped === $name || str_starts_with($name, $typed) ? min($distance, 1) : $distance;
59    }
60}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

Suggestion->distance
50    private static function distance(string $typed, string $name): int
51    {
52        $distance = levenshtein($typed, $name);
53
54        // The XOR is zero up to the first difference.
55        $at = strspn($typed ^ $name, "\0");
56        $swapped = substr_replace($typed, strrev(substr($typed, $at, 2)), $at, 2);
56        $swapped = substr_replace($typed, strrev(substr($typed, $at, 2)), $at, 2);
56        $swapped = substr_replace($typed, strrev(substr($typed, $at, 2)), $at, 2);
56        $swapped = substr_replace($typed, strrev(substr($typed, $at, 2)), $at, 2);
57
58        return $swapped === $name || str_starts_with($name, $typed) ? min($distance, 1) : $distance;
58        return $swapped === $name || str_starts_with($name, $typed) ? min($distance, 1) : $distance;
58        return $swapped === $name || str_starts_with($name, $typed) ? min($distance, 1) : $distance;
58        return $swapped === $name || str_starts_with($name, $typed) ? min($distance, 1) : $distance;
58        return $swapped === $name || str_starts_with($name, $typed) ? min($distance, 1) : $distance;
58        return $swapped === $name || str_starts_with($name, $typed) ? min($distance, 1) : $distance;
58        return $swapped === $name || str_starts_with($name, $typed) ? min($distance, 1) : $distance;
58        return $swapped === $name || str_starts_with($name, $typed) ? min($distance, 1) : $distance;
58        return $swapped === $name || str_starts_with($name, $typed) ? min($distance, 1) : $distance;
58        return $swapped === $name || str_starts_with($name, $typed) ? min($distance, 1) : $distance;
58        return $swapped === $name || str_starts_with($name, $typed) ? min($distance, 1) : $distance;
58        return $swapped === $name || str_starts_with($name, $typed) ? min($distance, 1) : $distance;
59    }
Suggestion->find
24    public static function find(string $typed, array $candidates, string $ignore = ''): ?string
25    {
26        $typed = ltrim($typed, $ignore);
27        $length = strlen($typed);
28        $best = '';
29        $bestDistance = PHP_INT_MAX;
30
31        foreach ($candidates as $candidate) {
31        foreach ($candidates as $candidate) {
32            $distance = self::distance($typed, ltrim($candidate, $ignore));
33
34            if ($distance < $bestDistance) {
31        foreach ($candidates as $candidate) {
32            $distance = self::distance($typed, ltrim($candidate, $ignore));
33
34            if ($distance < $bestDistance) {
35                $bestDistance = $distance;
31        foreach ($candidates as $candidate) {
31        foreach ($candidates as $candidate) {
32            $distance = self::distance($typed, ltrim($candidate, $ignore));
33
34            if ($distance < $bestDistance) {
35                $bestDistance = $distance;
36                $best = $candidate;
37            }
38        }
39
40        $limit = $length > 1 ? max(1, intdiv($length, 3)) : 0;
40        $limit = $length > 1 ? max(1, intdiv($length, 3)) : 0;
40        $limit = $length > 1 ? max(1, intdiv($length, 3)) : 0;
40        $limit = $length > 1 ? max(1, intdiv($length, 3)) : 0;
40        $limit = $length > 1 ? max(1, intdiv($length, 3)) : 0;
40        $limit = $length > 1 ? max(1, intdiv($length, 3)) : 0;
40        $limit = $length > 1 ? max(1, intdiv($length, 3)) : 0;
41
42        return $bestDistance <= $limit ? $best : null;
42        return $bestDistance <= $limit ? $best : null;
42        return $bestDistance <= $limit ? $best : null;
42        return $bestDistance <= $limit ? $best : null;
43    }