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

MyIntervals / PHP-CSS-Parser / 19928429819

04 Dec 2025 12:08PM UTC coverage: 62.487% (+0.02%) from 62.467%
19928429819

push

github

web-flow
[CLEANUP] Tighten `DeclarationBlock::selectors` type (#1407)

Use a local variable in `setSelectors()` rather than temporarily assigning the
property with a type it's not supposed to have.

Ensure that the property is set to a non-sparse numerical array.

Also tighten the return type of `getSelectors()`.

(Precursor to #1330.)

4 of 6 new or added lines in 1 file covered. (66.67%)

1171 of 1874 relevant lines covered (62.49%)

25.78 hits per line

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

85.83
/src/RuleSet/DeclarationBlock.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Sabberworm\CSS\RuleSet;
6

7
use Sabberworm\CSS\Comment\CommentContainer;
8
use Sabberworm\CSS\CSSElement;
9
use Sabberworm\CSS\CSSList\CSSList;
10
use Sabberworm\CSS\CSSList\CSSListItem;
11
use Sabberworm\CSS\CSSList\KeyFrame;
12
use Sabberworm\CSS\OutputFormat;
13
use Sabberworm\CSS\Parsing\OutputException;
14
use Sabberworm\CSS\Parsing\ParserState;
15
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
16
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
17
use Sabberworm\CSS\Position\Position;
18
use Sabberworm\CSS\Position\Positionable;
19
use Sabberworm\CSS\Property\KeyframeSelector;
20
use Sabberworm\CSS\Property\Selector;
21
use Sabberworm\CSS\Rule\Rule;
22

23
/**
24
 * This class represents a `RuleSet` constrained by a `Selector`.
25
 *
26
 * It contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the
27
 * matching elements.
28
 *
29
 * Declaration blocks usually appear directly inside a `Document` or another `CSSList` (mostly a `MediaQuery`).
30
 *
31
 * Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented.
32
 */
33
class DeclarationBlock implements CSSElement, CSSListItem, Positionable, RuleContainer
34
{
35
    use CommentContainer;
36
    use Position;
37

38
    /**
39
     * @var list<Selector>
40
     */
41
    private $selectors = [];
42

43
    /**
44
     * @var RuleSet
45
     */
46
    private $ruleSet;
47

48
    /**
49
     * @param int<1, max>|null $lineNumber
50
     */
51
    public function __construct(?int $lineNumber = null)
530✔
52
    {
53
        $this->ruleSet = new RuleSet($lineNumber);
530✔
54
        $this->setPosition($lineNumber);
530✔
55
    }
530✔
56

57
    /**
58
     * @throws UnexpectedTokenException
59
     * @throws UnexpectedEOFException
60
     *
61
     * @internal since V8.8.0
62
     */
63
    public static function parse(ParserState $parserState, ?CSSList $list = null): ?DeclarationBlock
169✔
64
    {
65
        $comments = [];
169✔
66
        $result = new DeclarationBlock($parserState->currentLine());
169✔
67
        try {
68
            $selectors = [];
169✔
69
            $selectorParts = [];
169✔
70
            $stringWrapperCharacter = null;
169✔
71
            $functionNestingLevel = 0;
169✔
72
            $consumedNextCharacter = false;
169✔
73
            static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ','];
169✔
74
            do {
75
                if (!$consumedNextCharacter) {
169✔
76
                    $selectorParts[] = $parserState->consume(1);
169✔
77
                }
78
                $selectorParts[] = $parserState->consumeUntil($stopCharacters, false, false, $comments);
169✔
79
                $nextCharacter = $parserState->peek();
169✔
80
                $consumedNextCharacter = false;
169✔
81
                switch ($nextCharacter) {
169✔
82
                    case '\'':
169✔
83
                        // The fallthrough is intentional.
84
                    case '"':
169✔
85
                        if (!\is_string($stringWrapperCharacter)) {
46✔
86
                            $stringWrapperCharacter = $nextCharacter;
46✔
87
                        } elseif ($stringWrapperCharacter === $nextCharacter) {
46✔
88
                            if (\substr(\end($selectorParts), -1) !== '\\') {
46✔
89
                                $stringWrapperCharacter = null;
46✔
90
                            }
91
                        }
92
                        break;
46✔
93
                    case '(':
169✔
94
                        if (!\is_string($stringWrapperCharacter)) {
87✔
95
                            ++$functionNestingLevel;
49✔
96
                        }
97
                        break;
87✔
98
                    case ')':
169✔
99
                        if (!\is_string($stringWrapperCharacter)) {
86✔
100
                            if ($functionNestingLevel <= 0) {
48✔
101
                                throw new UnexpectedTokenException('anything but', ')');
2✔
102
                            }
103
                            --$functionNestingLevel;
47✔
104
                        }
105
                        break;
85✔
106
                    case ',':
167✔
107
                        if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) {
147✔
108
                            $selectors[] = \implode('', $selectorParts);
144✔
109
                            $selectorParts = [];
144✔
110
                            $parserState->consume(1);
144✔
111
                            $consumedNextCharacter = true;
144✔
112
                        }
113
                        break;
147✔
114
                }
115
            } while (!\in_array($nextCharacter, ['{', '}'], true) || \is_string($stringWrapperCharacter));
168✔
116
            if ($functionNestingLevel !== 0) {
167✔
117
                throw new UnexpectedTokenException(')', $nextCharacter);
2✔
118
            }
119
            $selectors[] = \implode('', $selectorParts); // add final or only selector
165✔
120
            $result->setSelectors($selectors, $list);
165✔
121
            if ($parserState->comes('{')) {
162✔
122
                $parserState->consume(1);
162✔
123
            }
124
        } catch (UnexpectedTokenException $e) {
7✔
125
            if ($parserState->getSettings()->usesLenientParsing()) {
7✔
126
                if (!$parserState->comes('}')) {
7✔
127
                    $parserState->consumeUntil('}', false, true);
5✔
128
                }
129
                return null;
7✔
130
            } else {
131
                throw $e;
×
132
            }
133
        }
134
        $result->setComments($comments);
162✔
135

136
        RuleSet::parseRuleSet($parserState, $result->getRuleSet());
162✔
137

138
        return $result;
162✔
139
    }
140

141
    /**
142
     * @param array<Selector|string>|string $selectors
143
     *
144
     * @throws UnexpectedTokenException
145
     */
146
    public function setSelectors($selectors, ?CSSList $list = null): void
167✔
147
    {
148
        if (\is_array($selectors)) {
167✔
149
            $selectorsToSet = $selectors;
167✔
150
        } else {
NEW
151
            $selectorsToSet = \explode(',', $selectors);
×
152
        }
153

154
        // Convert all items to a `Selector` if not already
155
        foreach ($selectorsToSet as $key => $selector) {
167✔
156
            if (!($selector instanceof Selector)) {
167✔
157
                if ($list === null || !($list instanceof KeyFrame)) {
166✔
158
                    if (!Selector::isValid($selector)) {
166✔
159
                        throw new UnexpectedTokenException(
3✔
160
                            "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.",
3✔
161
                            $selector,
162
                            'custom'
3✔
163
                        );
164
                    }
165
                    $selectorsToSet[$key] = new Selector($selector);
163✔
166
                } else {
167
                    if (!KeyframeSelector::isValid($selector)) {
×
168
                        throw new UnexpectedTokenException(
×
169
                            "Selector did not match '" . KeyframeSelector::SELECTOR_VALIDATION_RX . "'.",
×
170
                            $selector,
171
                            'custom'
×
172
                        );
173
                    }
NEW
174
                    $selectorsToSet[$key] = new KeyframeSelector($selector);
×
175
                }
176
            }
177
        }
178

179
        // Discard the keys and reindex the array
180
        $this->selectors = \array_values($selectorsToSet);
164✔
181
    }
164✔
182

183
    /**
184
     * Remove one of the selectors of the block.
185
     *
186
     * @param Selector|string $selectorToRemove
187
     */
188
    public function removeSelector($selectorToRemove): bool
×
189
    {
190
        if ($selectorToRemove instanceof Selector) {
×
191
            $selectorToRemove = $selectorToRemove->getSelector();
×
192
        }
193
        foreach ($this->selectors as $key => $selector) {
×
194
            if ($selector->getSelector() === $selectorToRemove) {
×
195
                unset($this->selectors[$key]);
×
196
                return true;
×
197
            }
198
        }
199
        return false;
×
200
    }
201

202
    /**
203
     * @return list<Selector>
204
     */
205
    public function getSelectors(): array
157✔
206
    {
207
        return $this->selectors;
157✔
208
    }
209

210
    public function getRuleSet(): RuleSet
173✔
211
    {
212
        return $this->ruleSet;
173✔
213
    }
214

215
    /**
216
     * @see RuleSet::addRule()
217
     */
218
    public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void
215✔
219
    {
220
        $this->ruleSet->addRule($ruleToAdd, $sibling);
215✔
221
    }
215✔
222

223
    /**
224
     * @see RuleSet::getRules()
225
     *
226
     * @return array<int<0, max>, Rule>
227
     */
228
    public function getRules(?string $searchPattern = null): array
177✔
229
    {
230
        return $this->ruleSet->getRules($searchPattern);
177✔
231
    }
232

233
    /**
234
     * @see RuleSet::setRules()
235
     *
236
     * @param array<Rule> $rules
237
     */
238
    public function setRules(array $rules): void
346✔
239
    {
240
        $this->ruleSet->setRules($rules);
346✔
241
    }
346✔
242

243
    /**
244
     * @see RuleSet::getRulesAssoc()
245
     *
246
     * @return array<string, Rule>
247
     */
248
    public function getRulesAssoc(?string $searchPattern = null): array
50✔
249
    {
250
        return $this->ruleSet->getRulesAssoc($searchPattern);
50✔
251
    }
252

253
    /**
254
     * @see RuleSet::removeRule()
255
     */
256
    public function removeRule(Rule $ruleToRemove): void
22✔
257
    {
258
        $this->ruleSet->removeRule($ruleToRemove);
22✔
259
    }
22✔
260

261
    /**
262
     * @see RuleSet::removeMatchingRules()
263
     */
264
    public function removeMatchingRules(string $searchPattern): void
28✔
265
    {
266
        $this->ruleSet->removeMatchingRules($searchPattern);
28✔
267
    }
28✔
268

269
    /**
270
     * @see RuleSet::removeAllRules()
271
     */
272
    public function removeAllRules(): void
4✔
273
    {
274
        $this->ruleSet->removeAllRules();
4✔
275
    }
4✔
276

277
    /**
278
     * @return non-empty-string
279
     *
280
     * @throws OutputException
281
     */
282
    public function render(OutputFormat $outputFormat): string
6✔
283
    {
284
        $formatter = $outputFormat->getFormatter();
6✔
285
        $result = $formatter->comments($this);
6✔
286
        if (\count($this->selectors) === 0) {
6✔
287
            // If all the selectors have been removed, this declaration block becomes invalid
288
            throw new OutputException(
×
289
                'Attempt to print declaration block with missing selector',
×
290
                $this->getLineNumber()
×
291
            );
292
        }
293
        $result .= $outputFormat->getContentBeforeDeclarationBlock();
6✔
294
        $result .= $formatter->implode(
6✔
295
            $formatter->spaceBeforeSelectorSeparator() . ',' . $formatter->spaceAfterSelectorSeparator(),
6✔
296
            $this->selectors
6✔
297
        );
298
        $result .= $outputFormat->getContentAfterDeclarationBlockSelectors();
6✔
299
        $result .= $formatter->spaceBeforeOpeningBrace() . '{';
6✔
300
        $result .= $this->ruleSet->render($outputFormat);
6✔
301
        $result .= '}';
6✔
302
        $result .= $outputFormat->getContentAfterDeclarationBlock();
6✔
303

304
        return $result;
6✔
305
    }
306
}
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