Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
Classifier
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
3 / 3
9
100.00% covered (success)
100.00%
1 / 1
 classify
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 isDirectory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isFile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
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\GitIgnore;
21
22use function Safe\preg_match;
23
24/**
25 * Classifies .gitignore entries as directory-oriented or file-oriented patterns.
26 *
27 * This classifier SHALL inspect a raw .gitignore entry and determine whether the
28 * entry expresses directory semantics or file semantics. Implementations MUST
29 * preserve deterministic classification for identical inputs. Blank entries and
30 * comment entries MUST be treated as file-oriented values to avoid incorrectly
31 * inferring directory intent where no effective pattern exists.
32 */
33 class Classifier implements ClassifierInterface
34{
35    /**
36     * Represents a classification result indicating directory semantics.
37     *
38     * This constant MUST be returned when an entry clearly targets a directory,
39     * such as entries ending with a slash or patterns that imply directory
40     * traversal.
41     */
42    private const string DIRECTORY = 'directory';
43
44    /**
45     * Represents a classification result indicating file semantics.
46     *
47     * This constant MUST be returned when an entry does not clearly express
48     * directory semantics, including blank values and comment lines.
49     */
50    private const string FILE = 'file';
51
52    /**
53     * Classifies a .gitignore entry as either a directory or a file pattern.
54     *
55     * The provided entry SHALL be normalized with trim() before any rule is
56     * evaluated. Empty entries and comment entries MUST be classified as files.
57     * Entries ending with "/" MUST be classified as directories. Patterns that
58     * indicate directory traversal or wildcard directory matching SHOULD also be
59     * classified as directories.
60     *
61     * @param string $entry The raw .gitignore entry to classify.
62     *
63     * @return string The classification result. The value MUST be either
64     *                self::DIRECTORY or self::FILE.
65     */
66    public function classify(string $entry): string
67    {
68        $entry = trim($entry);
69
70        if ('' === $entry) {
71            return self::FILE;
72        }
73
74        if (str_starts_with($entry, '#')) {
75            return self::FILE;
76        }
77
78        if (str_ends_with($entry, '/')) {
79            return self::DIRECTORY;
80        }
81
82        if (1 === preg_match('/^[^.*]+[\/*]+/', $entry)) {
83            return self::DIRECTORY;
84        }
85
86        if (str_starts_with($entry, '**/')) {
87            return self::DIRECTORY;
88        }
89
90        if (str_contains($entry, '*/')) {
91            return self::DIRECTORY;
92        }
93
94        return self::FILE;
95    }
96
97    /**
98     * Determines whether the given .gitignore entry represents a directory pattern.
99     *
100     * This method MUST delegate the effective classification to classify() and
101     * SHALL return true only when the resulting classification is
102     * self::DIRECTORY.
103     *
104     * @param string $entry The raw .gitignore entry to evaluate.
105     *
106     * @return bool true when the entry is classified as a directory pattern;
107     *              otherwise, false
108     */
109    public function isDirectory(string $entry): bool
110    {
111        return self::DIRECTORY === $this->classify($entry);
112    }
113
114    /**
115     * Determines whether the given .gitignore entry represents a file pattern.
116     *
117     * This method MUST delegate the effective classification to classify() and
118     * SHALL return true only when the resulting classification is self::FILE.
119     *
120     * @param string $entry The raw .gitignore entry to evaluate.
121     *
122     * @return bool true when the entry is classified as a file pattern;
123     *              otherwise, false
124     */
125    public function isFile(string $entry): bool
126    {
127        return self::FILE === $this->classify($entry);
128    }
129}