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

nette / latte / 20837225455

09 Jan 2026 12:47AM UTC coverage: 95.017%. Remained the same
20837225455

push

github

dg
Helpers::sortBeforeAfter() reimplemented using Kahn's algorithm for topological sorting

38 of 38 new or added lines in 1 file covered. (100.0%)

23 existing lines in 7 files now uncovered.

5549 of 5840 relevant lines covered (95.02%)

0.95 hits per line

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

97.56
/src/Latte/Helpers.php
1
<?php
2

3
/**
4
 * This file is part of the Latte (https://latte.nette.org)
5
 * Copyright (c) 2008 David Grudl (https://davidgrudl.com)
6
 */
7

8
declare(strict_types=1);
9

10
namespace Latte;
11

12
use function array_filter, array_keys, array_search, array_slice, array_unique, count, is_array, is_object, is_string, levenshtein, max, min, strlen, strpos;
13
use const PHP_VERSION_ID;
14

15

16
/**
17
 * Latte helpers.
18
 * @internal
19
 */
20
class Helpers
21
{
22
        /**
23
         * Finds the best suggestion.
24
         * @param  string[]  $items
25
         */
26
        public static function getSuggestion(array $items, string $value): ?string
1✔
27
        {
28
                $best = null;
1✔
29
                $min = (strlen($value) / 4 + 1) * 10 + .1;
1✔
30
                foreach (array_unique($items) as $item) {
1✔
31
                        if (($len = levenshtein($item, $value, 10, 11, 10)) > 0 && $len < $min) {
1✔
32
                                $min = $len;
1✔
33
                                $best = $item;
1✔
34
                        }
35
                }
36

37
                return $best;
1✔
38
        }
39

40

41
        /** intentionally without callable typehint, because it generates bad error messages */
42
        public static function toReflection($callable): \ReflectionFunctionAbstract
43
        {
44
                if (is_string($callable) && strpos($callable, '::')) {
1✔
45
                        return PHP_VERSION_ID < 80300
1✔
46
                                ? new \ReflectionMethod($callable)
×
47
                                : \ReflectionMethod::createFromMethodName($callable);
1✔
48
                } elseif (is_array($callable)) {
1✔
49
                        return new \ReflectionMethod($callable[0], $callable[1]);
1✔
50
                } elseif (is_object($callable) && !$callable instanceof \Closure) {
1✔
51
                        return new \ReflectionMethod($callable, '__invoke');
1✔
52
                } else {
53
                        return new \ReflectionFunction($callable);
1✔
54
                }
55
        }
56

57

58
        /**
59
         * Sorts items using topological sort based on before/after constraints.
60
         * @param  array<string, mixed|\stdClass>  $list
61
         * @return array<string, mixed|\stdClass>
62
         */
63
        public static function sortBeforeAfter(array $list): array
1✔
64
        {
65
                $names = array_keys($list);
1✔
66

67
                // Build adjacency list and in-degree count
68
                // Edge A → B means "A must come before B"
69
                $graph = array_fill_keys($names, []);
1✔
70
                $inDegree = array_fill_keys($names, 0);
1✔
71

72
                foreach ($list as $name => $info) {
1✔
73
                        if (!$info instanceof \stdClass) {
1✔
74
                                continue;
1✔
75
                        }
76

77
                        // "before: X" means this node → X (this comes before X)
78
                        foreach ((array) ($info->before ?? []) as $target) {
1✔
79
                                if ($target === '*') {
1✔
80
                                        foreach ($names as $other) {
1✔
81
                                                if ($other !== $name && !in_array($other, $graph[$name], true)) {
1✔
82
                                                        $graph[$name][] = $other;
1✔
83
                                                        $inDegree[$other]++;
1✔
84
                                                }
85
                                        }
86
                                } elseif (isset($list[$target]) && $target !== $name) {
1✔
87
                                        if (!in_array($target, $graph[$name], true)) {
1✔
88
                                                $graph[$name][] = $target;
1✔
89
                                                $inDegree[$target]++;
1✔
90
                                        }
91
                                }
92
                        }
93

94
                        // "after: X" means X → this node (X comes before this)
95
                        foreach ((array) ($info->after ?? []) as $target) {
1✔
96
                                if ($target === '*') {
1✔
97
                                        foreach ($names as $other) {
1✔
98
                                                if ($other !== $name && !in_array($name, $graph[$other], true)) {
1✔
99
                                                        $graph[$other][] = $name;
1✔
100
                                                        $inDegree[$name]++;
1✔
101
                                                }
102
                                        }
103
                                } elseif (isset($list[$target]) && $target !== $name) {
1✔
104
                                        if (!in_array($name, $graph[$target], true)) {
1✔
105
                                                $graph[$target][] = $name;
1✔
106
                                                $inDegree[$name]++;
1✔
107
                                        }
108
                                }
109
                        }
110
                }
111

112
                // Kahn's algorithm
113
                $queue = [];
1✔
114
                foreach ($names as $name) {
1✔
115
                        if ($inDegree[$name] === 0) {
1✔
116
                                $queue[] = $name;
1✔
117
                        }
118
                }
119

120
                $result = [];
1✔
121
                while ($queue) {
1✔
122
                        $node = array_shift($queue);
1✔
123
                        $result[$node] = $list[$node];
1✔
124

125
                        foreach ($graph[$node] as $neighbor) {
1✔
126
                                $inDegree[$neighbor]--;
1✔
127
                                if ($inDegree[$neighbor] === 0) {
1✔
128
                                        $queue[] = $neighbor;
1✔
129
                                }
130
                        }
131
                }
132

133
                if (count($result) !== count($list)) {
1✔
134
                        $cycle = array_diff($names, array_keys($result));
1✔
135
                        throw new \LogicException('Circular dependency detected among: ' . implode(', ', $cycle));
1✔
136
                }
137

138
                return $result;
1✔
139
        }
140

141

142
        /** @param  mixed[]  $items */
143
        public static function removeNulls(array &$items): void
1✔
144
        {
145
                $items = array_values(array_filter($items, fn($item) => $item !== null));
1✔
146
        }
1✔
147

148

149
        /**
150
         * Attempts to map the compiled template to the source.
151
         * @return array{name: ?string, line: ?int, column: ?int}|null
152
         */
153
        public static function mapCompiledToSource(string $compiledFile, ?int $compiledLine = null): ?array
1✔
154
        {
155
                if (!Cache::isCacheFile($compiledFile)) {
1✔
156
                        return null;
1✔
157
                }
158

159
                $content = file_get_contents($compiledFile);
1✔
160
                $name = preg_match('#^/\*\* source: (\S.+) \*/#m', $content, $m) ? $m[1] : null;
1✔
161
                $compiledLine && preg_match('~/\* pos (\d+)(?::(\d+))? \*/~', explode("\n", $content)[$compiledLine - 1], $pos);
1✔
162
                $line = isset($pos[1]) ? (int) $pos[1] : null;
1✔
163
                $column = isset($pos[2]) ? (int) $pos[2] : null;
1✔
164
                return $name || $line ? compact('name', 'line', 'column') : null;
1✔
165
        }
166

167

168
        /**
169
         * Tries to guess the position in the template from the backtrace
170
         */
171
        public static function guessTemplatePosition(): ?string
172
        {
173
                $trace = debug_backtrace();
1✔
174
                foreach ($trace as $item) {
1✔
175
                        if (isset($item['file']) && ($source = self::mapCompiledToSource($item['file'], $item['line']))) {
1✔
176
                                $res = [];
1✔
177
                                if ($source['name'] && is_file($source['name'])) {
1✔
UNCOV
178
                                        $res[] = "in '" . str_replace(dirname($source['name'], 2), '...', $source['name']) . "'";
×
179
                                }
180
                                if ($source['line']) {
1✔
181
                                        $res[] = 'on line ' . $source['line'] . ($source['column'] ? ' at column ' . $source['column'] : '');
1✔
182
                                }
183
                                return implode(' ', $res);
1✔
184
                        }
185
                }
186
                return null;
1✔
187
        }
188
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc