Skip to content

Commit 734c72e

Browse files
authored
[TASK] Add (and use) a CSSListItem type (#1212)
This allows a single type to be used for the contents of a `CSSList`, instead of a long list of orred types, and helps with static analysis. Various `assertInstanceOf()` tests are added to the test cases to confirm that the list items are of the type expected. Some `implements` and `exetends` lists are now alphabetically sorted. Also don't implement interfaces extended by another that is also implemented PHP<7.4 does not allow this. Instead, for clarity, add a DocBlock comment stating which additional interfaces should be implemented that are not explicitly listed in the `implements` section. When our minimum PHP version becomes 7.4 or above, we can revisit this.
1 parent acfe85e commit 734c72e

File tree

8 files changed

+97
-33
lines changed

8 files changed

+97
-33
lines changed

config/phpstan-baseline.neon

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ parameters:
1818
count: 1
1919
path: ../src/CSSList/CSSList.php
2020

21+
-
22+
message: '#^Parameters should have "Sabberworm\\CSS\\CSSList\\CSSListItem\|array" types as the only types passed to this method$#'
23+
identifier: typePerfect.narrowPublicClassMethodParamType
24+
count: 1
25+
path: ../src/CSSList/CSSList.php
26+
2127
-
2228
message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#'
2329
identifier: ternary.shortNotAllowed
@@ -54,12 +60,6 @@ parameters:
5460
count: 2
5561
path: ../src/RuleSet/RuleSet.php
5662

57-
-
58-
message: '#^Parameters should have "Sabberworm\\CSS\\Rule\\Rule" types as the only types passed to this method$#'
59-
identifier: typePerfect.narrowPublicClassMethodParamType
60-
count: 1
61-
path: ../src/RuleSet/RuleSet.php
62-
6363
-
6464
message: '#^Parameters should have "string" types as the only types passed to this method$#'
6565
identifier: typePerfect.narrowPublicClassMethodParamType

src/CSSList/CSSList.php

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@
3030
* `RuleSet`s as well as other `CSSList` objects.
3131
*
3232
* It can also contain `Import` and `Charset` objects stemming from at-rules.
33+
*
34+
* Note that `CSSListItem` extends both `Commentable` and `Renderable`,
35+
* so those interfaces must also be implemented by concrete subclasses.
3336
*/
34-
abstract class CSSList implements Renderable, Commentable
37+
abstract class CSSList implements CSSListItem
3538
{
3639
/**
3740
* @var list<Comment>
@@ -41,7 +44,7 @@ abstract class CSSList implements Renderable, Commentable
4144
protected $comments = [];
4245

4346
/**
44-
* @var array<int<0, max>, RuleSet|CSSList|Import|Charset>
47+
* @var array<int<0, max>, CSSListItem>
4548
*
4649
* @internal since 8.8.0
4750
*/
@@ -105,7 +108,7 @@ public static function parseList(ParserState $parserState, CSSList $list): void
105108
}
106109

107110
/**
108-
* @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|DeclarationBlock|false|null
111+
* @return CSSListItem|false|null
109112
* If `null` is returned, it means the end of the list has been reached.
110113
* If `false` is returned, it means an invalid item has been encountered,
111114
* but parsing of the next item should proceed.
@@ -156,13 +159,11 @@ private static function parseListItem(ParserState $parserState, CSSList $list)
156159
}
157160

158161
/**
159-
* @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|null
160-
*
161162
* @throws SourceException
162163
* @throws UnexpectedTokenException
163164
* @throws UnexpectedEOFException
164165
*/
165-
private static function parseAtRule(ParserState $parserState)
166+
private static function parseAtRule(ParserState $parserState): ?CSSListItem
166167
{
167168
$parserState->consume('@');
168169
$identifier = $parserState->parseIdentifier();
@@ -265,28 +266,24 @@ public function getLineNo(): int
265266

266267
/**
267268
* Prepends an item to the list of contents.
268-
*
269-
* @param RuleSet|CSSList|Import|Charset $item
270269
*/
271-
public function prepend($item): void
270+
public function prepend(CSSListItem $item): void
272271
{
273272
\array_unshift($this->contents, $item);
274273
}
275274

276275
/**
277276
* Appends an item to the list of contents.
278-
*
279-
* @param RuleSet|CSSList|Import|Charset $item
280277
*/
281-
public function append($item): void
278+
public function append(CSSListItem $item): void
282279
{
283280
$this->contents[] = $item;
284281
}
285282

286283
/**
287284
* Splices the list of contents.
288285
*
289-
* @param array<int, RuleSet|CSSList|Import|Charset> $replacement
286+
* @param array<int, CSSListItem> $replacement
290287
*/
291288
public function splice(int $offset, ?int $length = null, ?array $replacement = null): void
292289
{
@@ -296,11 +293,8 @@ public function splice(int $offset, ?int $length = null, ?array $replacement = n
296293
/**
297294
* Inserts an item in the CSS list before its sibling. If the desired sibling cannot be found,
298295
* the item is appended at the end.
299-
*
300-
* @param RuleSet|CSSList|Import|Charset $item
301-
* @param RuleSet|CSSList|Import|Charset $sibling
302296
*/
303-
public function insertBefore($item, $sibling): void
297+
public function insertBefore(CSSListItem $item, CSSListItem $sibling): void
304298
{
305299
if (\in_array($sibling, $this->contents, true)) {
306300
$this->replace($sibling, [$item, $sibling]);
@@ -312,13 +306,13 @@ public function insertBefore($item, $sibling): void
312306
/**
313307
* Removes an item from the CSS list.
314308
*
315-
* @param RuleSet|Import|Charset|CSSList $itemToRemove
309+
* @param CSSListItem $itemToRemove
316310
* May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`,
317311
* a `Charset` or another `CSSList` (most likely a `MediaQuery`)
318312
*
319313
* @return bool whether the item was removed
320314
*/
321-
public function remove($itemToRemove): bool
315+
public function remove(CSSListItem $itemToRemove): bool
322316
{
323317
$key = \array_search($itemToRemove, $this->contents, true);
324318
if ($key !== false) {
@@ -332,12 +326,12 @@ public function remove($itemToRemove): bool
332326
/**
333327
* Replaces an item from the CSS list.
334328
*
335-
* @param RuleSet|Import|Charset|CSSList $oldItem
329+
* @param CSSListItem $oldItem
336330
* May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`, a `Charset`
337331
* or another `CSSList` (most likely a `MediaQuery`)
338-
* @param RuleSet|Import|Charset|CSSList|array<RuleSet|Import|Charset|CSSList> $newItem
332+
* @param CSSListItem|array<CSSListItem> $newItem
339333
*/
340-
public function replace($oldItem, $newItem): bool
334+
public function replace(CSSListItem $oldItem, $newItem): bool
341335
{
342336
$key = \array_search($oldItem, $this->contents, true);
343337
if ($key !== false) {
@@ -353,7 +347,7 @@ public function replace($oldItem, $newItem): bool
353347
}
354348

355349
/**
356-
* @param array<int, RuleSet|Import|Charset|CSSList> $contents
350+
* @param array<int, CSSListItem> $contents
357351
*/
358352
public function setContents(array $contents): void
359353
{
@@ -444,7 +438,7 @@ abstract public function isRootList(): bool;
444438
/**
445439
* Returns the stored items.
446440
*
447-
* @return array<int<0, max>, RuleSet|Import|Charset|CSSList>
441+
* @return array<int<0, max>, CSSListItem>
448442
*/
449443
public function getContents(): array
450444
{

src/CSSList/CSSListItem.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sabberworm\CSS\CSSList;
6+
7+
use Sabberworm\CSS\Comment\Commentable;
8+
use Sabberworm\CSS\Renderable;
9+
10+
/**
11+
* Represents anything that can be in the `$contents` of a `CSSList`.
12+
*
13+
* The interface does not define any methods to implement.
14+
* It's purpose is to allow a single type to be specified for `CSSList::$contents` and manipulation methods thereof.
15+
* It extends `Commentable` and `Renderable` because all `CSSListItem`s are both.
16+
* This allows implementations to call methods from those interfaces without any additional type checks.
17+
*/
18+
interface CSSListItem extends Commentable, Renderable {}

src/Property/AtRule.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@
55
namespace Sabberworm\CSS\Property;
66

77
use Sabberworm\CSS\Comment\Commentable;
8+
use Sabberworm\CSS\CSSList\CSSListItem;
89
use Sabberworm\CSS\Renderable;
910

10-
interface AtRule extends Renderable, Commentable
11+
/**
12+
* Note that `CSSListItem` extends both `Commentable` and `Renderable`,
13+
* so concrete classes implementing this interface must also implement those.
14+
*/
15+
interface AtRule extends CSSListItem
1116
{
1217
/**
1318
* Since there are more set rules than block rules,

src/RuleSet/RuleSet.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Sabberworm\CSS\Comment\Comment;
88
use Sabberworm\CSS\Comment\Commentable;
9+
use Sabberworm\CSS\CSSList\CSSListItem;
910
use Sabberworm\CSS\OutputFormat;
1011
use Sabberworm\CSS\Parsing\ParserState;
1112
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
@@ -21,8 +22,11 @@
2122
*
2223
* If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $rule)`, `getRules()` and `removeRule($rule)`
2324
* (which accepts either a `Rule` or a rule name; optionally suffixed by a dash to remove all related rules).
25+
*
26+
* Note that `CSSListItem` extends both `Commentable` and `Renderable`,
27+
* so those interfaces must also be implemented by concrete subclasses.
2428
*/
25-
abstract class RuleSet implements Renderable, Commentable
29+
abstract class RuleSet implements CSSListItem
2630
{
2731
/**
2832
* the rules in this rule set, using the property name as the key,

tests/CSSList/AtRuleBlockListTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Sabberworm\CSS\Tests\CSSList;
66

77
use PHPUnit\Framework\TestCase;
8+
use Sabberworm\CSS\CSSList\AtRuleBlockList;
89
use Sabberworm\CSS\Parser;
910
use Sabberworm\CSS\Settings;
1011

@@ -60,6 +61,7 @@ public function parsesRuleNameOfMediaQueries(string $css): void
6061
$contents = (new Parser($css))->parse()->getContents();
6162
$atRuleBlockList = $contents[0];
6263

64+
self::assertInstanceOf(AtRuleBlockList::class, $atRuleBlockList);
6365
self::assertSame('media', $atRuleBlockList->atRuleName());
6466
}
6567

@@ -73,6 +75,7 @@ public function parsesArgumentsOfMediaQueries(string $css): void
7375
$contents = (new Parser($css))->parse()->getContents();
7476
$atRuleBlockList = $contents[0];
7577

78+
self::assertInstanceOf(AtRuleBlockList::class, $atRuleBlockList);
7679
self::assertSame('(min-width: 768px)', $atRuleBlockList->atRuleArgs());
7780
}
7881

tests/ParserTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
namespace Sabberworm\CSS\Tests;
66

77
use PHPUnit\Framework\TestCase;
8+
use Sabberworm\CSS\Comment\Commentable;
9+
use Sabberworm\CSS\CSSList\CSSList;
810
use Sabberworm\CSS\CSSList\Document;
911
use Sabberworm\CSS\CSSList\KeyFrame;
1012
use Sabberworm\CSS\OutputFormat;
@@ -973,9 +975,30 @@ public function lineNumbersParsing(): void
973975

974976
$actual = [];
975977
foreach ($document->getContents() as $contentItem) {
978+
// PHPStan can see what `assertInstanceOf()` does,
979+
// but does not understand `LogicalOr` with multiple `IsIntanceOf` constraints.
980+
// So a more explicit type check is required.
981+
// TODO: tidy this up when an interface with `getLineNo()` is added.
982+
if (
983+
!$contentItem instanceof Charset &&
984+
!$contentItem instanceof CSSList &&
985+
!$contentItem instanceof CSSNamespace &&
986+
!$contentItem instanceof Import &&
987+
!$contentItem instanceof RuleSet
988+
) {
989+
self::fail('Content item is not of an expected type. It\'s a `' . \get_class($contentItem) . '`.');
990+
}
976991
$actual[$contentItem->getLineNo()] = [\get_class($contentItem)];
977992
if ($contentItem instanceof KeyFrame) {
978993
foreach ($contentItem->getContents() as $block) {
994+
if (
995+
!$block instanceof CSSList &&
996+
!$block instanceof RuleSet
997+
) {
998+
self::fail(
999+
'KeyFrame content item is not of an expected type. It\'s a `' . \get_class($block) . '`.'
1000+
);
1001+
}
9791002
$actual[$contentItem->getLineNo()][] = $block->getLineNo();
9801003
}
9811004
}
@@ -1037,37 +1060,44 @@ public function commentExtracting(): void
10371060
$nodes = $document->getContents();
10381061

10391062
// Import property.
1063+
self::assertInstanceOf(Commentable::class, $nodes[0]);
10401064
$importComments = $nodes[0]->getComments();
10411065
self::assertCount(2, $importComments);
10421066
self::assertSame("*\n * Comments\n ", $importComments[0]->getComment());
10431067
self::assertSame(' Hell ', $importComments[1]->getComment());
10441068

10451069
// Declaration block.
10461070
$fooBarBlock = $nodes[1];
1071+
self::assertInstanceOf(Commentable::class, $fooBarBlock);
10471072
$fooBarBlockComments = $fooBarBlock->getComments();
10481073
// TODO Support comments in selectors.
10491074
// $this->assertCount(2, $fooBarBlockComments);
10501075
// $this->assertSame("* Number 4 *", $fooBarBlockComments[0]->getComment());
10511076
// $this->assertSame("* Number 5 *", $fooBarBlockComments[1]->getComment());
10521077

10531078
// Declaration rules.
1079+
self::assertInstanceOf(RuleSet::class, $fooBarBlock);
10541080
$fooBarRules = $fooBarBlock->getRules();
10551081
$fooBarRule = $fooBarRules[0];
10561082
$fooBarRuleComments = $fooBarRule->getComments();
10571083
self::assertCount(1, $fooBarRuleComments);
10581084
self::assertSame(' Number 6 ', $fooBarRuleComments[0]->getComment());
10591085

10601086
// Media property.
1087+
self::assertInstanceOf(Commentable::class, $nodes[2]);
10611088
$mediaComments = $nodes[2]->getComments();
10621089
self::assertCount(0, $mediaComments);
10631090

10641091
// Media children.
1092+
self::assertInstanceOf(CSSList::class, $nodes[2]);
10651093
$mediaRules = $nodes[2]->getContents();
1094+
self::assertInstanceOf(Commentable::class, $mediaRules[0]);
10661095
$fooBarComments = $mediaRules[0]->getComments();
10671096
self::assertCount(1, $fooBarComments);
10681097
self::assertSame('* Number 10 *', $fooBarComments[0]->getComment());
10691098

10701099
// Media -> declaration -> rule.
1100+
self::assertInstanceOf(RuleSet::class, $mediaRules[0]);
10711101
$fooBarRules = $mediaRules[0]->getRules();
10721102
$fooBarChildComments = $fooBarRules[0]->getComments();
10731103
self::assertCount(1, $fooBarChildComments);
@@ -1083,6 +1113,7 @@ public function flatCommentExtractingOneComment(): void
10831113
$document = $parser->parse();
10841114

10851115
$contents = $document->getContents();
1116+
self::assertInstanceOf(RuleSet::class, $contents[0]);
10861117
$divRules = $contents[0]->getRules();
10871118
$comments = $divRules[0]->getComments();
10881119

@@ -1099,6 +1130,7 @@ public function flatCommentExtractingTwoConjoinedCommentsForOneRule(): void
10991130
$document = $parser->parse();
11001131

11011132
$contents = $document->getContents();
1133+
self::assertInstanceOf(RuleSet::class, $contents[0]);
11021134
$divRules = $contents[0]->getRules();
11031135
$comments = $divRules[0]->getComments();
11041136

@@ -1116,6 +1148,7 @@ public function flatCommentExtractingTwoSpaceSeparatedCommentsForOneRule(): void
11161148
$document = $parser->parse();
11171149

11181150
$contents = $document->getContents();
1151+
self::assertInstanceOf(RuleSet::class, $contents[0]);
11191152
$divRules = $contents[0]->getRules();
11201153
$comments = $divRules[0]->getComments();
11211154

@@ -1133,6 +1166,7 @@ public function flatCommentExtractingCommentsForTwoRules(): void
11331166
$document = $parser->parse();
11341167

11351168
$contents = $document->getContents();
1169+
self::assertInstanceOf(RuleSet::class, $contents[0]);
11361170
$divRules = $contents[0]->getRules();
11371171
$rule1Comments = $divRules[0]->getComments();
11381172
$rule2Comments = $divRules[1]->getComments();
@@ -1151,6 +1185,7 @@ public function topLevelCommentExtracting(): void
11511185
$parser = new Parser('/*Find Me!*/div {left:10px; text-align:left;}');
11521186
$document = $parser->parse();
11531187
$contents = $document->getContents();
1188+
self::assertInstanceOf(Commentable::class, $contents[0]);
11541189
$comments = $contents[0]->getComments();
11551190
self::assertCount(1, $comments);
11561191
self::assertSame('Find Me!', $comments[0]->getComment());
@@ -1216,6 +1251,7 @@ public function escapedSpecialCaseTokens(): void
12161251
{
12171252
$document = self::parsedStructureForFile('escaped-tokens');
12181253
$contents = $document->getContents();
1254+
self::assertInstanceOf(RuleSet::class, $contents[0]);
12191255
$rules = $contents[0]->getRules();
12201256
$urlRule = $rules[0];
12211257
$calcRule = $rules[1];

0 commit comments

Comments
 (0)