Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
ExportIgnoreFilter
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
3 / 3
10
100.00% covered (success)
100.00%
1 / 1
 filter
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 isKeptPath
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 normalizePath
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5/**
6 * Fast Forward Development Tools for PHP projects.
7 *
8 * This file is part of fast-forward/dev-tools project.
9 *
10 * @author   Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
11 * @license  https://opensource.org/licenses/MIT MIT License
12 *
13 * @see      https://github.com/php-fast-forward/
14 * @see      https://github.com/php-fast-forward/dev-tools
15 * @see      https://github.com/php-fast-forward/dev-tools/issues
16 * @see      https://php-fast-forward.github.io/dev-tools/
17 * @see      https://datatracker.ietf.org/doc/html/rfc2119
18 */
19
20namespace FastForward\DevTools\GitAttributes;
21
22use function Safe\preg_replace;
23
24/**
25 * Filters export-ignore candidates using normalized path comparisons.
26 *
27 * This filter SHALL compare configured keep-in-export paths against canonical
28 * candidates while ignoring leading and trailing slash differences. It MUST
29 * preserve the original candidate ordering in the filtered result.
30 */
31 class ExportIgnoreFilter implements ExportIgnoreFilterInterface
32{
33    /**
34     * Filters export-ignore candidates using the configured keep-in-export paths.
35     *
36     * @param list<string> $candidates the canonical candidate paths
37     * @param list<string> $keepInExportPaths the paths that MUST remain exportable
38     *
39     * @return list<string> the filtered export-ignore candidates
40     */
41    public function filter(array $candidates, array $keepInExportPaths): array
42    {
43        $keptPaths = [];
44
45        foreach ($keepInExportPaths as $path) {
46            $normalizedPath = $this->normalizePath($path);
47
48            if ('' === $normalizedPath) {
49                continue;
50            }
51
52            $keptPaths[] = $normalizedPath;
53        }
54
55        return array_values(array_filter(
56            $candidates,
57            fn(string $candidate): bool => ! $this->isKeptPath($this->normalizePath($candidate), $keptPaths)
58        ));
59    }
60
61    /**
62     * @param string $candidate
63     * @param list<string> $keptPaths
64     */
65    private function isKeptPath(string $candidate, array $keptPaths): bool
66    {
67        foreach ($keptPaths as $keptPath) {
68            if ($candidate === $keptPath || str_starts_with($candidate . '/', $keptPath . '/')) {
69                return true;
70            }
71        }
72
73        return false;
74    }
75
76    /**
77     * Normalizes a configured path for stable matching.
78     *
79     * @param string $path the raw path from candidates or Composer extra config
80     *
81     * @return string the normalized path used for comparisons
82     */
83    private function normalizePath(string $path): string
84    {
85        $trimmedPath = trim($path);
86
87        if ('' === $trimmedPath) {
88            return '';
89        }
90
91        $normalizedPath = preg_replace('#/+#', '/', '/' . ltrim($trimmedPath, '/')) ?? $trimmedPath;
92
93        return '/' === $normalizedPath ? $normalizedPath : rtrim($normalizedPath, '/');
94    }
95}