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 * This file is part of fast-forward/dev-tools.
7 *
8 * This source file is subject to the license bundled
9 * with this source code in the file LICENSE.
10 *
11 * @copyright Copyright (c) 2026 Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
12 * @license   https://opensource.org/licenses/MIT MIT License
13 *
14 * @see       https://github.com/php-fast-forward/dev-tools
15 * @see       https://github.com/php-fast-forward
16 * @see       https://datatracker.ietf.org/doc/html/rfc2119
17 */
18
19namespace FastForward\DevTools\GitIgnore;
20
21use function Safe\preg_match;
22
23/**
24 * Classifies .gitignore entries as directory-oriented or file-oriented patterns.
25 *
26 * This classifier SHALL inspect a raw .gitignore entry and determine whether the
27 * entry expresses directory semantics or file semantics. Implementations MUST
28 * preserve deterministic classification for identical inputs. Blank entries and
29 * comment entries MUST be treated as file-oriented values to avoid incorrectly
30 * inferring directory intent where no effective pattern exists.
31 */
32 class Classifier implements ClassifierInterface
33{
34    /**
35     * Represents a classification result indicating directory semantics.
36     *
37     * This constant MUST be returned when an entry clearly targets a directory,
38     * such as entries ending with a slash or patterns that imply directory
39     * traversal.
40     */
41    private const string DIRECTORY = 'directory';
42
43    /**
44     * Represents a classification result indicating file semantics.
45     *
46     * This constant MUST be returned when an entry does not clearly express
47     * directory semantics, including blank values and comment lines.
48     */
49    private const string FILE = 'file';
50
51    /**
52     * Classifies a .gitignore entry as either a directory or a file pattern.
53     *
54     * The provided entry SHALL be normalized with trim() before any rule is
55     * evaluated. Empty entries and comment entries MUST be classified as files.
56     * Entries ending with "/" MUST be classified as directories. Patterns that
57     * indicate directory traversal or wildcard directory matching SHOULD also be
58     * classified as directories.
59     *
60     * @param string $entry The raw .gitignore entry to classify.
61     *
62     * @return string The classification result. The value MUST be either
63     *                self::DIRECTORY or self::FILE.
64     */
65    public function classify(string $entry): string
66    {
67        $entry = trim($entry);
68
69        if ('' === $entry) {
70            return self::FILE;
71        }
72
73        if (str_starts_with($entry, '#')) {
74            return self::FILE;
75        }
76
77        if (str_ends_with($entry, '/')) {
78            return self::DIRECTORY;
79        }
80
81        if (1 === preg_match('/^[^.*]+[\/*]+/', $entry)) {
82            return self::DIRECTORY;
83        }
84
85        if (str_starts_with($entry, '**/')) {
86            return self::DIRECTORY;
87        }
88
89        if (str_contains($entry, '*/')) {
90            return self::DIRECTORY;
91        }
92
93        return self::FILE;
94    }
95
96    /**
97     * Determines whether the given .gitignore entry represents a directory pattern.
98     *
99     * This method MUST delegate the effective classification to classify() and
100     * SHALL return true only when the resulting classification is
101     * self::DIRECTORY.
102     *
103     * @param string $entry The raw .gitignore entry to evaluate.
104     *
105     * @return bool true when the entry is classified as a directory pattern;
106     *              otherwise, false
107     */
108    public function isDirectory(string $entry): bool
109    {
110        return self::DIRECTORY === $this->classify($entry);
111    }
112
113    /**
114     * Determines whether the given .gitignore entry represents a file pattern.
115     *
116     * This method MUST delegate the effective classification to classify() and
117     * SHALL return true only when the resulting classification is self::FILE.
118     *
119     * @param string $entry The raw .gitignore entry to evaluate.
120     *
121     * @return bool true when the entry is classified as a file pattern;
122     *              otherwise, false
123     */
124    public function isFile(string $entry): bool
125    {
126        return self::FILE === $this->classify($entry);
127    }
128}