Skip to content

Commit cf221e4

Browse files
committed
Support @includeFirst, @componentFirst, @each
1 parent e2aaf8c commit cf221e4

File tree

10 files changed

+184
-190
lines changed

10 files changed

+184
-190
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace IxDFCodingStandard\Sniffs\Laravel;
4+
5+
use BadMethodCallException;
6+
7+
final class BladeTemplateExtractor
8+
{
9+
private const INVALID_METHOD_CALL = 'Invalid method call';
10+
11+
// @include, @component, @extends
12+
private const INCLUDE_BLADE_DIRECTIVE = '/@(include|component|extends)\(\'([^\']++)\'/';
13+
14+
// @includeIf, @includeWhen
15+
private const CONDITIONAL_INCLUDE_BLADE_DIRECTIVE = '/@(includeIf|includeWhen)\([^,]++,\s*+\'([^\']++)\'/';
16+
17+
// @includeFirst, @componentFirst - takes array parameter: @includeFirst(['view1', 'view2'])
18+
private const FIRST_BLADE_DIRECTIVE = '/@(includeFirst|componentFirst)\(\s*+\[\s*+\'([^\']++)\'/';
19+
20+
// @each directive: @each('view.name', $items, 'item', 'empty.view')
21+
private const EACH_BLADE_DIRECTIVE = '/@each\(\s*+\'([^\']++)\'/';
22+
23+
public function isBladeIncludeDirective(string $tokenContent): bool
24+
{
25+
return preg_match(self::INCLUDE_BLADE_DIRECTIVE, $tokenContent) === 1;
26+
}
27+
28+
public function isConditionalBladeIncludeDirective(string $tokenContent): bool
29+
{
30+
return preg_match(self::CONDITIONAL_INCLUDE_BLADE_DIRECTIVE, $tokenContent) === 1;
31+
}
32+
33+
public function isEachBladeDirective(string $tokenContent): bool
34+
{
35+
return preg_match(self::EACH_BLADE_DIRECTIVE, $tokenContent) === 1;
36+
}
37+
38+
public function isFirstBladeDirective(string $tokenContent): bool
39+
{
40+
return preg_match(self::FIRST_BLADE_DIRECTIVE, $tokenContent) === 1;
41+
}
42+
43+
public function getBladeTemplateName(string $tokenContent): string
44+
{
45+
if (!$this->isBladeIncludeDirective($tokenContent)) {
46+
throw new BadMethodCallException(self::INVALID_METHOD_CALL);
47+
}
48+
49+
$matches = [];
50+
preg_match(self::INCLUDE_BLADE_DIRECTIVE, $tokenContent, $matches);
51+
52+
if (!isset($matches[2])) {
53+
throw new BadMethodCallException(self::INVALID_METHOD_CALL);
54+
}
55+
56+
return (string) $matches[2];
57+
}
58+
59+
public function getConditionalBladeTemplateName(string $tokenContent): string
60+
{
61+
if (!$this->isConditionalBladeIncludeDirective($tokenContent)) {
62+
throw new BadMethodCallException(self::INVALID_METHOD_CALL);
63+
}
64+
65+
$matches = [];
66+
preg_match(self::CONDITIONAL_INCLUDE_BLADE_DIRECTIVE, $tokenContent, $matches);
67+
68+
if (!isset($matches[2])) {
69+
throw new BadMethodCallException(self::INVALID_METHOD_CALL);
70+
}
71+
72+
return (string) $matches[2];
73+
}
74+
75+
public function getEachBladeTemplateName(string $tokenContent): string
76+
{
77+
if (!$this->isEachBladeDirective($tokenContent)) {
78+
throw new BadMethodCallException(self::INVALID_METHOD_CALL);
79+
}
80+
81+
$matches = [];
82+
preg_match(self::EACH_BLADE_DIRECTIVE, $tokenContent, $matches);
83+
84+
if (!isset($matches[1])) {
85+
throw new BadMethodCallException(self::INVALID_METHOD_CALL);
86+
}
87+
88+
return (string) $matches[1];
89+
}
90+
91+
public function getFirstBladeTemplateName(string $tokenContent): string
92+
{
93+
if (!$this->isFirstBladeDirective($tokenContent)) {
94+
throw new BadMethodCallException(self::INVALID_METHOD_CALL);
95+
}
96+
97+
$matches = [];
98+
preg_match(self::FIRST_BLADE_DIRECTIVE, $tokenContent, $matches);
99+
100+
if (!isset($matches[2])) {
101+
throw new BadMethodCallException(self::INVALID_METHOD_CALL);
102+
}
103+
104+
return (string) $matches[2];
105+
}
106+
}

IxDFCodingStandard/Sniffs/Laravel/NonExistingBladeTemplateSniff.php

Lines changed: 20 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,9 @@
77

88
final class NonExistingBladeTemplateSniff implements Sniff
99
{
10-
private const INVALID_METHOD_CALL = 'Invalid method call';
11-
1210
public const CODE_TEMPLATE_NOT_FOUND = 'TemplateNotFound';
1311
public const CODE_UNKNOWN_VIEW_NAMESPACE = 'UnknownViewNamespace';
1412

15-
// @include
16-
private const INCLUDE_BLADE_DIRECTIVE = '/@(include|component|extends)\(\'([^\']++)\'/';
17-
18-
// @includeIf
19-
private const CONDITIONAL_INCLUDE_BLADE_DIRECTIVE = '/@(includeIf|includeWhen)\([^,]++,\s*+\'([^\']++)\'/';
20-
2113
/** @var list<non-empty-string> The same as for config('view.paths') */
2214
public array $viewPaths = [
2315
'resources/views',
@@ -32,6 +24,15 @@ final class NonExistingBladeTemplateSniff implements Sniff
3224
/** @var array<string, bool> */
3325
private array $checkedFiles = [];
3426

27+
private BladeTemplateExtractor $bladeExtractor;
28+
private PhpViewExtractor $phpExtractor;
29+
30+
public function __construct()
31+
{
32+
$this->bladeExtractor = new BladeTemplateExtractor();
33+
$this->phpExtractor = new PhpViewExtractor();
34+
}
35+
3536
/** @inheritDoc */
3637
public function register(): array
3738
{
@@ -56,68 +57,24 @@ public function process(File $phpcsFile, $stackPtr): int // phpcs:ignore Generic
5657
$tokenContent = $token['content'];
5758

5859
// Handle Blade directives (found in T_INLINE_HTML tokens)
59-
if ($this->isBladeIncludeDirective($tokenContent)) {
60-
$this->validateTemplateName($this->getBladeTemplateName($tokenContent), $phpcsFile, $position);
61-
} elseif ($this->isConditionalBladeIncludeDirective($tokenContent)) {
62-
$this->validateTemplateName($this->getConditionalBladeTemplateName($tokenContent), $phpcsFile, $position);
63-
}
64-
// Handle PHP code (found in T_STRING tokens)
65-
elseif ($token['type'] === 'T_STRING') {
66-
if ($this->isViewFacade($tokens, $position)) {
67-
$this->validateTemplateName($this->getViewFacadeTemplateName($tokens, $position), $phpcsFile, $position);
68-
} elseif ($this->isViewFunctionFactory($tokens, $position)) {
69-
$this->validateTemplateName($this->getViewFunctionFactoryTemplateName($tokens, $position), $phpcsFile, $position);
70-
} elseif ($this->isViewFunction($tokens, $position)) {
71-
$this->validateTemplateName($this->getViewFunctionTemplateName($tokens, $position), $phpcsFile, $position);
60+
if ($this->bladeExtractor->isBladeIncludeDirective($tokenContent)) {
61+
$this->validateTemplateName($this->bladeExtractor->getBladeTemplateName($tokenContent), $phpcsFile, $position);
62+
} elseif ($this->bladeExtractor->isConditionalBladeIncludeDirective($tokenContent)) {
63+
$this->validateTemplateName($this->bladeExtractor->getConditionalBladeTemplateName($tokenContent), $phpcsFile, $position);
64+
} elseif ($this->bladeExtractor->isEachBladeDirective($tokenContent)) {
65+
$this->validateTemplateName($this->bladeExtractor->getEachBladeTemplateName($tokenContent), $phpcsFile, $position);
66+
} elseif ($this->bladeExtractor->isFirstBladeDirective($tokenContent)) {
67+
$this->validateTemplateName($this->bladeExtractor->getFirstBladeTemplateName($tokenContent), $phpcsFile, $position);
68+
} elseif ($token['type'] === 'T_STRING') { // Handle PHP code (found in T_STRING tokens)
69+
if ($this->phpExtractor->isViewFunction($tokens, $position)) {
70+
$this->validateTemplateName($this->phpExtractor->getViewFunctionTemplateName($tokens, $position), $phpcsFile, $position);
7271
}
7372
}
7473
}
7574

7675
return 0;
7776
}
7877

79-
private function isBladeIncludeDirective(string $tokenContent): bool
80-
{
81-
return preg_match(self::INCLUDE_BLADE_DIRECTIVE, $tokenContent) === 1;
82-
}
83-
84-
private function isConditionalBladeIncludeDirective(string $tokenContent): bool
85-
{
86-
return preg_match(self::CONDITIONAL_INCLUDE_BLADE_DIRECTIVE, $tokenContent) === 1;
87-
}
88-
89-
private function getBladeTemplateName(string $tokenContent): string
90-
{
91-
if (!$this->isBladeIncludeDirective($tokenContent)) {
92-
throw new \BadMethodCallException(self::INVALID_METHOD_CALL);
93-
}
94-
95-
$matches = [];
96-
preg_match(self::INCLUDE_BLADE_DIRECTIVE, $tokenContent, $matches);
97-
98-
if (!isset($matches[2])) {
99-
throw new \BadMethodCallException(self::INVALID_METHOD_CALL);
100-
}
101-
102-
return (string) $matches[2];
103-
}
104-
105-
private function getConditionalBladeTemplateName(string $tokenContent): string
106-
{
107-
if (!$this->isConditionalBladeIncludeDirective($tokenContent)) {
108-
throw new \BadMethodCallException(self::INVALID_METHOD_CALL);
109-
}
110-
111-
$matches = [];
112-
preg_match(self::CONDITIONAL_INCLUDE_BLADE_DIRECTIVE, $tokenContent, $matches);
113-
114-
if (!isset($matches[2])) {
115-
throw new \BadMethodCallException(self::INVALID_METHOD_CALL);
116-
}
117-
118-
return (string) $matches[2];
119-
}
120-
12178
/**
12279
* @param string $templateName In dot notation
12380
* @throws \OutOfBoundsException
@@ -232,89 +189,4 @@ private function validateTemplateName(string $templateName, File $phpcsFile, int
232189
$this->reportUnknownViewNamespace($phpcsFile, $stackPtr, $templateName);
233190
}
234191
}
235-
236-
/** @param array<array<string>> $tokens */
237-
private function isViewFacade(array $tokens, int $position): bool
238-
{
239-
if (!isset($tokens[$position + 3])) {
240-
return false;
241-
}
242-
243-
return ($tokens[$position]['content'] === 'View' || $tokens[$position]['content'] === 'ViewFacade') &&
244-
$tokens[$position + 1]['type'] === 'T_DOUBLE_COLON' &&
245-
$tokens[$position + 2]['content'] === 'make' &&
246-
$tokens[$position + 3]['type'] === 'T_CONSTANT_ENCAPSED_STRING';
247-
}
248-
249-
/** @param array<array<string>> $tokens */
250-
private function getViewFacadeTemplateName(array $tokens, int $position): string // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
251-
{
252-
if (!$this->isViewFacade($tokens, $position)) {
253-
throw new \BadMethodCallException(self::INVALID_METHOD_CALL);
254-
}
255-
256-
$maxLookupPosition = $position + 14;
257-
for ($lookupPosition = $position + 4; $lookupPosition < $maxLookupPosition && isset($tokens[$lookupPosition]); $lookupPosition++) {
258-
if ($tokens[$lookupPosition]['type'] !== 'T_WHITESPACE') {
259-
return mb_trim($tokens[$lookupPosition]['content'], '\'"');
260-
}
261-
}
262-
263-
throw new \BadMethodCallException('Unable to find the template name');
264-
}
265-
266-
/** @param array<array<string>> $tokens */
267-
private function isViewFunctionFactory(array $tokens, int $position): bool
268-
{
269-
if (!isset($tokens[$position + 6])) {
270-
return false;
271-
}
272-
273-
return $tokens[$position]['content'] === 'view' &&
274-
$tokens[$position + 1]['content'] === '(' &&
275-
$tokens[$position + 2]['content'] === ')' &&
276-
$tokens[$position + 3]['content'] === '->' &&
277-
$tokens[$position + 4]['content'] === 'make' &&
278-
$tokens[$position + 6]['type'] !== 'T_VARIABLE';
279-
}
280-
281-
/** @param array<array<string>> $tokens */
282-
private function getViewFunctionFactoryTemplateName(array $tokens, int $position): string // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
283-
{
284-
if (!$this->isViewFunctionFactory($tokens, $position)) {
285-
throw new \BadMethodCallException(self::INVALID_METHOD_CALL);
286-
}
287-
288-
$maxLookupPosition = $position + 16;
289-
for ($lookupPosition = $position + 6; $lookupPosition < $maxLookupPosition && isset($tokens[$lookupPosition]); $lookupPosition++) {
290-
if ($tokens[$lookupPosition]['type'] !== 'T_WHITESPACE') {
291-
return mb_trim($tokens[$lookupPosition]['content'], '\'"');
292-
}
293-
}
294-
295-
throw new \BadMethodCallException('Unable to find the template name');
296-
}
297-
298-
/** @param array<array<string>> $tokens */
299-
private function isViewFunction(array $tokens, int $position): bool
300-
{
301-
if (!isset($tokens[$position - 1], $tokens[$position + 2])) {
302-
return false;
303-
}
304-
305-
return $tokens[$position - 1]['type'] === 'T_WHITESPACE' &&
306-
$tokens[$position]['content'] === 'view' &&
307-
$tokens[$position + 1]['content'] === '(' &&
308-
$tokens[$position + 2]['type'] === 'T_CONSTANT_ENCAPSED_STRING';
309-
}
310-
311-
/** @param array<array<string>> $tokens */
312-
private function getViewFunctionTemplateName(array $tokens, int $position): string
313-
{
314-
if (!$this->isViewFunction($tokens, $position)) {
315-
throw new \BadMethodCallException(self::INVALID_METHOD_CALL);
316-
}
317-
318-
return mb_trim($tokens[$position + 2]['content'], '\'"');
319-
}
320192
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace IxDFCodingStandard\Sniffs\Laravel;
4+
5+
use BadMethodCallException;
6+
7+
final class PhpViewExtractor
8+
{
9+
private const INVALID_METHOD_CALL = 'Invalid method call';
10+
11+
/** @param array<array<string>> $tokens */
12+
public function isViewFunction(array $tokens, int $position): bool
13+
{
14+
if (!isset($tokens[$position - 1], $tokens[$position + 2])) {
15+
return false;
16+
}
17+
18+
return $tokens[$position - 1]['type'] === 'T_WHITESPACE' &&
19+
$tokens[$position]['content'] === 'view' &&
20+
$tokens[$position + 1]['content'] === '(' &&
21+
$tokens[$position + 2]['type'] === 'T_CONSTANT_ENCAPSED_STRING';
22+
}
23+
24+
/** @param array<array<string>> $tokens */
25+
public function getViewFunctionTemplateName(array $tokens, int $position): string
26+
{
27+
if (!$this->isViewFunction($tokens, $position)) {
28+
throw new BadMethodCallException(self::INVALID_METHOD_CALL);
29+
}
30+
31+
return mb_trim($tokens[$position + 2]['content'], '\'"');
32+
}
33+
}

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
"require": {
77
"php": "^8.2",
88
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
9-
"slevomat/coding-standard": "^8.19.1",
9+
"slevomat/coding-standard": "^8.22",
1010
"squizlabs/php_codesniffer": "^3.13"
1111
},
1212
"require-dev": {
13-
"friendsofphp/php-cs-fixer": "^3.75",
13+
"friendsofphp/php-cs-fixer": "^3.87",
1414
"phpunit/phpunit": "^11 || ^12",
1515
"vimeo/psalm": "^6.12"
1616
},

docs/laravel.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Do not allow using [Mass Assignment](https://laravel.com/docs/master/eloquent#ma
66

77
### IxDFCodingStandard.Laravel.NonExistingBladeTemplate
88

9-
Detects missing Blade templates in `@include`, `view()`, and `View::make()` calls.
9+
Detects missing Blade templates in `@include`, `@includeFirst`, `@componentFirst`, `@each`, and `view()` calls.
1010

1111
```xml
1212
<rule ref="IxDFCodingStandard.Laravel.NonExistingBladeTemplate">

0 commit comments

Comments
 (0)