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

dragomano / scss-php / 23953478554

03 Apr 2026 04:26PM UTC coverage: 99.596% (+7.0%) from 92.642%
23953478554

push

github

dragomano
Adopt @PER-CS ruleset and reformat codebase

349 of 349 new or added lines in 51 files covered. (100.0%)

31 existing lines in 13 files now uncovered.

12325 of 12375 relevant lines covered (99.6%)

95.88 hits per line

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

95.12
/src/Parser/ModuleDirectiveParser.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Bugo\SCSS\Parser;
6

7
use Bugo\SCSS\Lexer\TokenStream;
8
use Bugo\SCSS\Lexer\TokenType;
9
use Bugo\SCSS\Nodes\AstNode;
10
use Bugo\SCSS\Nodes\ForwardNode;
11
use Bugo\SCSS\Nodes\ImportNode;
12
use Bugo\SCSS\Nodes\ListNode;
13
use Bugo\SCSS\Nodes\UseNode;
14
use Closure;
15

16
use function array_filter;
17
use function max;
18
use function strtolower;
19
use function trim;
20

21
final readonly class ModuleDirectiveParser
22
{
23
    /**
24
     * @param Closure(): string $parseString
25
     * @param Closure(): string $consumeIdentifier
26
     * @param Closure(array<int, TokenType>): ?AstNode $parseValueUntil
27
     * @param Closure(): array{global: bool, default: bool, important: bool} $parseValueModifiers
28
     */
29
    public function __construct(
30
        private TokenStream $stream,
31
        private Closure $parseString,
32
        private Closure $consumeIdentifier,
33
        private Closure $parseValueUntil,
34
        private Closure $parseValueModifiers,
35
    ) {}
1,057✔
36

37
    public function parseUseDirective(): UseNode
38
    {
39
        $this->stream->skipWhitespace();
251✔
40

41
        $path = ($this->parseString)();
251✔
42

43
        $namespace     = null;
251✔
44
        $configuration = [];
251✔
45

46
        $this->stream->skipWhitespace();
251✔
47

48
        if (StreamUtils::consumeKeyword($this->stream, 'as')) {
251✔
49
            if ($this->stream->consume(TokenType::STAR)) {
16✔
50
                $namespace = '*';
4✔
51
            } else {
52
                $namespace = ($this->consumeIdentifier)();
12✔
53
            }
54
        }
55

56
        $this->stream->skipWhitespace();
251✔
57

58
        if (StreamUtils::consumeKeyword($this->stream, 'with')) {
251✔
59
            $configuration = $this->parseUseConfiguration();
6✔
60
        }
61

62
        StreamUtils::consumeSemicolonFromStream($this->stream);
251✔
63

64
        return new UseNode($path, $namespace, $configuration);
251✔
65
    }
66

67
    public function parseImportDirective(): ImportNode
68
    {
69
        $this->stream->skipWhitespace();
12✔
70

71
        $imports = [];
12✔
72

73
        while (! $this->stream->isEof() && ! $this->stream->is(TokenType::SEMICOLON)) {
12✔
74
            $entry = $this->parseImportEntry();
12✔
75

76
            if ($entry !== '') {
12✔
77
                $imports[] = $entry;
12✔
78
            }
79

80
            if (! StreamUtils::consumeCommaSeparator($this->stream)) {
12✔
81
                break;
12✔
82
            }
83
        }
84

85
        StreamUtils::consumeSemicolonFromStream($this->stream);
12✔
86

87
        return new ImportNode($imports);
12✔
88
    }
89

90
    public function parseForwardDirective(): ForwardNode
91
    {
92
        $this->stream->skipWhitespace();
17✔
93

94
        $path = ($this->parseString)();
17✔
95

96
        $prefix        = null;
17✔
97
        $visibility    = null;
17✔
98
        $members       = [];
17✔
99
        $configuration = [];
17✔
100

101
        while (! $this->stream->isEof() && ! $this->stream->is(TokenType::SEMICOLON)) {
17✔
102
            $this->stream->skipWhitespace();
14✔
103

104
            if (! $this->stream->is(TokenType::IDENTIFIER)) {
14✔
105
                break;
×
106
            }
107

108
            $keyword = strtolower($this->stream->current()->value);
14✔
109

110
            if ($keyword === 'as') {
14✔
111
                StreamUtils::consumeKeyword($this->stream, $keyword, true);
4✔
112

113
                $prefix = ($this->consumeIdentifier)();
4✔
114

115
                $this->stream->skipWhitespace();
4✔
116
                $this->stream->consume(TokenType::STAR);
4✔
117
            } elseif ($keyword === 'hide' || $keyword === 'show') {
10✔
118
                $visibility = $keyword;
8✔
119

120
                StreamUtils::consumeKeyword($this->stream, $keyword, true);
8✔
121

122
                while (! $this->stream->isEof() && ! $this->stream->is(TokenType::SEMICOLON)) {
8✔
123
                    $isVariable = $this->stream->consume(TokenType::DOLLAR) !== null;
8✔
124
                    $name       = ($this->consumeIdentifier)();
8✔
125

126
                    if ($name !== '') {
8✔
127
                        $members[] = $isVariable ? '$' . $name : $name;
8✔
128
                    }
129

130
                    if (! StreamUtils::consumeCommaSeparator($this->stream)) {
8✔
131
                        break;
8✔
132
                    }
133
                }
134
            } elseif ($keyword === 'with') {
4✔
135
                StreamUtils::consumeKeyword($this->stream, $keyword, true);
4✔
136

137
                $configuration = $this->parseForwardConfiguration();
4✔
138
            } else {
UNCOV
139
                break;
×
140
            }
141
        }
142

143
        StreamUtils::consumeSemicolonFromStream($this->stream);
17✔
144

145
        return new ForwardNode($path, $prefix, $visibility, $members, $configuration);
17✔
146
    }
147

148
    /**
149
     * @return array<string, AstNode>
150
     */
151
    private function parseUseConfiguration(): array
152
    {
153
        $rawConfiguration = $this->parseModuleConfiguration(false);
6✔
154

155
        return array_filter(
6✔
156
            $rawConfiguration,
6✔
157
            fn(AstNode|array $value): bool => $value instanceof AstNode,
6✔
158
        );
6✔
159
    }
160

161
    private function parseImportEntry(): string
162
    {
163
        $entry        = '';
12✔
164
        $parenDepth   = 0;
12✔
165
        $bracketDepth = 0;
12✔
166

167
        while (! $this->stream->isEof()) {
12✔
168
            $token = $this->stream->current();
12✔
169

170
            if (
171
                $parenDepth === 0
12✔
172
                && $bracketDepth === 0
12✔
173
                && ($token->type === TokenType::COMMA || $token->type === TokenType::SEMICOLON)
12✔
174
            ) {
175
                break;
12✔
176
            }
177

178
            if ($token->type === TokenType::LPAREN) {
12✔
179
                $parenDepth++;
4✔
180
            } elseif ($token->type === TokenType::RPAREN) {
12✔
181
                $parenDepth = max(0, $parenDepth - 1);
4✔
182
            } elseif ($token->type === TokenType::LBRACKET) {
12✔
183
                $bracketDepth++;
1✔
184
            } elseif ($token->type === TokenType::RBRACKET) {
12✔
185
                $bracketDepth = max(0, $bracketDepth - 1);
1✔
186
            }
187

188
            if ($token->type === TokenType::WHITESPACE) {
12✔
189
                $entry .= ' ';
3✔
190
            } elseif ($token->type === TokenType::STRING) {
12✔
191
                $entry .= '"' . $token->value . '"';
11✔
192
            } else {
193
                $entry .= StreamUtils::tokenToRawString($token->type, $token->value);
4✔
194
            }
195

196
            $this->stream->advance();
12✔
197
        }
198

199
        return trim($entry);
12✔
200
    }
201

202
    /**
203
     * @return array<string, array{value: AstNode, default: bool}>
204
     */
205
    private function parseForwardConfiguration(): array
206
    {
207
        $rawConfiguration = $this->parseModuleConfiguration(true);
4✔
208

209
        $configuration = [];
4✔
210

211
        foreach ($rawConfiguration as $name => $entry) {
4✔
212
            if ($entry instanceof AstNode) {
4✔
UNCOV
213
                continue;
×
214
            }
215

216
            $configuration[$name] = [
4✔
217
                'value'   => $entry['value'],
4✔
218
                'default' => $entry['default'],
4✔
219
            ];
4✔
220
        }
221

222
        return $configuration;
4✔
223
    }
224

225
    /**
226
     * @return array<string, AstNode|array{value: AstNode, default: bool}>
227
     */
228
    private function parseModuleConfiguration(bool $withDefaultModifier): array
229
    {
230
        $configuration = [];
9✔
231

232
        if (! $this->stream->consume(TokenType::LPAREN)) {
9✔
233
            return $configuration;
1✔
234
        }
235

236
        while (! $this->stream->isEof()) {
8✔
237
            $this->stream->skipWhitespace();
8✔
238

239
            if ($this->stream->consume(TokenType::RPAREN)) {
8✔
UNCOV
240
                break;
×
241
            }
242

243
            if (! $this->stream->consume(TokenType::DOLLAR)) {
8✔
UNCOV
244
                break;
×
245
            }
246

247
            $name = ($this->consumeIdentifier)();
8✔
248

249
            $this->stream->skipWhitespace();
8✔
250

251
            if (! $this->stream->consume(TokenType::COLON)) {
8✔
UNCOV
252
                break;
×
253
            }
254

255
            $value = ($this->parseValueUntil)([TokenType::COMMA, TokenType::RPAREN]) ?? new ListNode([], 'comma');
8✔
256

257
            if ($withDefaultModifier) {
8✔
258
                $modifiers = ($this->parseValueModifiers)();
4✔
259

260
                $configuration[$name] = [
4✔
261
                    'value'   => $value,
4✔
262
                    'default' => $modifiers['default'],
4✔
263
                ];
4✔
264
            } else {
265
                $configuration[$name] = $value;
5✔
266
            }
267

268
            $this->stream->skipWhitespace();
8✔
269

270
            if ($this->stream->consume(TokenType::COMMA)) {
8✔
271
                continue;
5✔
272
            }
273

274
            if ($this->stream->consume(TokenType::RPAREN)) {
8✔
275
                break;
8✔
276
            }
277
        }
278

279
        return $configuration;
8✔
280
    }
281
}
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