Skip to content

Commit c30bc76

Browse files
authored
Laravel: add Blade view data support (#333)
1 parent 49efe23 commit c30bc76

6 files changed

Lines changed: 836 additions & 198 deletions

File tree

rules.neon

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,16 @@ services:
9191
configDir: %shipmonkDeadCode.usageProviders.symfony.configDir%
9292
containerXmlPaths: %shipmonkDeadCode.usageProviders.symfony.containerXmlPaths%
9393

94+
-
95+
class: ShipMonk\PHPStan\DeadCode\Provider\TemplateViewDataTraverser
96+
arguments:
97+
analysedPaths: %analysedPaths%
98+
9499
-
95100
class: ShipMonk\PHPStan\DeadCode\Provider\TwigUsageProvider
96101
tags:
97102
- shipmonk.deadCode.memberUsageProvider
98103
arguments:
99-
analysedPaths: %analysedPaths%
100104
enabled: %shipmonkDeadCode.usageProviders.twig.enabled%
101105

102106
-
@@ -127,6 +131,13 @@ services:
127131
arguments:
128132
enabled: %shipmonkDeadCode.usageProviders.laravel.enabled%
129133

134+
-
135+
class: ShipMonk\PHPStan\DeadCode\Provider\BladeUsageProvider
136+
tags:
137+
- shipmonk.deadCode.memberUsageProvider
138+
arguments:
139+
enabled: %shipmonkDeadCode.usageProviders.blade.enabled%
140+
130141
-
131142
class: ShipMonk\PHPStan\DeadCode\Provider\NetteUsageProvider
132143
tags:
@@ -271,6 +282,8 @@ parameters:
271282
enabled: null
272283
laravel:
273284
enabled: null
285+
blade:
286+
enabled: null
274287
nette:
275288
enabled: null
276289
netteTester:
@@ -360,6 +373,9 @@ parametersSchema:
360373
laravel: structure([
361374
enabled: schema(bool(), nullable())
362375
])
376+
blade: structure([
377+
enabled: schema(bool(), nullable())
378+
])
363379
nette: structure([
364380
enabled: schema(bool(), nullable())
365381
])
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\DeadCode\Provider;
4+
5+
use Composer\InstalledVersions;
6+
use PhpParser\Node;
7+
use PhpParser\Node\Arg;
8+
use PhpParser\Node\Expr;
9+
use PhpParser\Node\Expr\FuncCall;
10+
use PhpParser\Node\Expr\MethodCall;
11+
use PhpParser\Node\Expr\StaticCall;
12+
use PhpParser\Node\Identifier;
13+
use PhpParser\Node\Name;
14+
use PHPStan\Analyser\Scope;
15+
use PHPStan\Reflection\ReflectionProvider;
16+
use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberUsage;
17+
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
18+
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage;
19+
use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
20+
use function strpos;
21+
use function substr;
22+
23+
final class BladeUsageProvider implements MemberUsageProvider
24+
{
25+
26+
private const VIEW_FACADE_METHODS = [
27+
'make' => true,
28+
'first' => true,
29+
'composer' => true,
30+
'creator' => true,
31+
];
32+
33+
private readonly ReflectionProvider $reflectionProvider;
34+
35+
private readonly TemplateViewDataTraverser $traverser;
36+
37+
private readonly bool $enabled;
38+
39+
public function __construct(
40+
ReflectionProvider $reflectionProvider,
41+
TemplateViewDataTraverser $traverser,
42+
?bool $enabled,
43+
)
44+
{
45+
$this->reflectionProvider = $reflectionProvider;
46+
$this->traverser = $traverser;
47+
$this->enabled = $enabled ?? InstalledVersions::isInstalled('laravel/framework');
48+
}
49+
50+
public function getUsages(
51+
Node $node,
52+
Scope $scope,
53+
): array
54+
{
55+
if (!$this->enabled) {
56+
return [];
57+
}
58+
59+
$usages = [];
60+
61+
if ($node instanceof FuncCall) {
62+
$usages = [...$usages, ...$this->getUsagesFromViewHelper($node, $scope)];
63+
}
64+
65+
if ($node instanceof StaticCall) {
66+
$usages = [...$usages, ...$this->getUsagesFromViewFacade($node, $scope)];
67+
}
68+
69+
if ($node instanceof MethodCall) {
70+
$usages = [...$usages, ...$this->getUsagesFromMethodCall($node, $scope)];
71+
}
72+
73+
return $usages;
74+
}
75+
76+
/**
77+
* Handles view('template', ['key' => $model])
78+
*
79+
* @return list<ClassMemberUsage>
80+
*/
81+
private function getUsagesFromViewHelper(
82+
FuncCall $node,
83+
Scope $scope,
84+
): array
85+
{
86+
if (!$node->name instanceof Name) {
87+
return [];
88+
}
89+
90+
if ($node->name->toString() !== 'view') {
91+
return [];
92+
}
93+
94+
$args = $node->getArgs();
95+
96+
if (!isset($args[1])) {
97+
return [];
98+
}
99+
100+
return $this->traverseDataArg($args[1]->value, $node, $scope);
101+
}
102+
103+
/**
104+
* Handles View::make/first('template', $data) and View::composer/creator('view', Class::class)
105+
*
106+
* @return list<ClassMemberUsage>
107+
*/
108+
private function getUsagesFromViewFacade(
109+
StaticCall $node,
110+
Scope $scope,
111+
): array
112+
{
113+
if (!$node->name instanceof Identifier) {
114+
return [];
115+
}
116+
117+
$methodName = $node->name->toString();
118+
119+
if (!isset(self::VIEW_FACADE_METHODS[$methodName])) {
120+
return [];
121+
}
122+
123+
$callerType = $node->class instanceof Expr
124+
? $scope->getType($node->class)
125+
: $scope->resolveTypeByName($node->class);
126+
127+
foreach ($callerType->getObjectClassNames() as $className) {
128+
if (!$this->reflectionProvider->hasClass($className)) {
129+
continue;
130+
}
131+
132+
$classReflection = $this->reflectionProvider->getClass($className);
133+
134+
if (
135+
$classReflection->is('Illuminate\Support\Facades\View')
136+
|| $classReflection->is('Illuminate\Contracts\View\Factory')
137+
) {
138+
if ($methodName === 'make' || $methodName === 'first') {
139+
$args = $node->getArgs();
140+
141+
if (!isset($args[1])) {
142+
return [];
143+
}
144+
145+
return $this->traverseDataArg($args[1]->value, $node, $scope);
146+
}
147+
148+
// View::composer('view', Class::class) / View::creator('view', Class::class)
149+
return $this->getUsagesFromComposerOrCreator($node, $node->getArgs(), $scope, $methodName);
150+
}
151+
}
152+
153+
return [];
154+
}
155+
156+
/**
157+
* @param array<Arg> $args
158+
* @return list<ClassMethodUsage>
159+
*/
160+
private function getUsagesFromComposerOrCreator(
161+
Node $node,
162+
array $args,
163+
Scope $scope,
164+
string $methodName,
165+
): array
166+
{
167+
if (!isset($args[1])) {
168+
return [];
169+
}
170+
171+
$callbackType = $scope->getType($args[1]->value);
172+
$usages = [];
173+
174+
// View::composer → compose, View::creator → create (see Illuminate\View\Concerns\ManagesEvents::classEventMethodForPrefix)
175+
$defaultMethod = $methodName === 'composer' ? 'compose' : 'create';
176+
177+
foreach ($callbackType->getConstantStrings() as $stringType) {
178+
$value = $stringType->getValue();
179+
180+
// Support 'Class@method' syntax
181+
$atPos = strpos($value, '@');
182+
183+
if ($atPos !== false) {
184+
$callbackClassName = substr($value, 0, $atPos);
185+
$calledMethod = substr($value, $atPos + 1);
186+
} else {
187+
$callbackClassName = $value;
188+
$calledMethod = $defaultMethod;
189+
}
190+
191+
foreach ([$calledMethod, '__construct'] as $method) {
192+
$usages[] = new ClassMethodUsage(
193+
UsageOrigin::createRegular($node, $scope),
194+
new ClassMethodRef($callbackClassName, $method, possibleDescendant: false),
195+
);
196+
}
197+
}
198+
199+
return $usages;
200+
}
201+
202+
/**
203+
* Handles:
204+
* - $factory->make('template', $data) / $factory->first($views, $data)
205+
* - $factory->composer('view', Class::class) / $factory->creator('view', Class::class)
206+
* - $response->view('template', $data)
207+
* - $view->with('key', $value) / $view->with(['key' => $value])
208+
*
209+
* @return list<ClassMemberUsage>
210+
*/
211+
private function getUsagesFromMethodCall(
212+
MethodCall $node,
213+
Scope $scope,
214+
): array
215+
{
216+
if (!$node->name instanceof Identifier) {
217+
return [];
218+
}
219+
220+
$methodName = $node->name->toString();
221+
$callerType = $scope->getType($node->var);
222+
223+
foreach ($callerType->getObjectClassNames() as $className) {
224+
if (!$this->reflectionProvider->hasClass($className)) {
225+
continue;
226+
}
227+
228+
$classReflection = $this->reflectionProvider->getClass($className);
229+
230+
if ($classReflection->is('Illuminate\Contracts\View\Factory')) {
231+
if ($methodName === 'make' || $methodName === 'first') {
232+
$args = $node->getArgs();
233+
234+
if (!isset($args[1])) {
235+
return [];
236+
}
237+
238+
return $this->traverseDataArg($args[1]->value, $node, $scope);
239+
}
240+
241+
if ($methodName === 'composer' || $methodName === 'creator') {
242+
return $this->getUsagesFromComposerOrCreator($node, $node->getArgs(), $scope, $methodName);
243+
}
244+
}
245+
246+
if ($classReflection->is('Illuminate\Contracts\Routing\ResponseFactory')) {
247+
if ($methodName === 'view') {
248+
$args = $node->getArgs();
249+
250+
if (!isset($args[1])) {
251+
return [];
252+
}
253+
254+
return $this->traverseDataArg($args[1]->value, $node, $scope);
255+
}
256+
}
257+
258+
if ($classReflection->is('Illuminate\Contracts\View\View')) {
259+
if ($methodName === 'with') {
260+
return $this->getUsagesFromWithCall($node, $scope);
261+
}
262+
}
263+
}
264+
265+
return [];
266+
}
267+
268+
/**
269+
* @return list<ClassMemberUsage>
270+
*/
271+
private function getUsagesFromWithCall(
272+
MethodCall $node,
273+
Scope $scope,
274+
): array
275+
{
276+
$args = $node->getArgs();
277+
278+
// with('key', $value) — extract from value arg
279+
if (isset($args[1])) {
280+
return $this->traverseDataArg($args[1]->value, $node, $scope);
281+
}
282+
283+
// with(['key' => $value]) — extract from array arg
284+
if (isset($args[0])) {
285+
return $this->traverseDataArg($args[0]->value, $node, $scope);
286+
}
287+
288+
return [];
289+
}
290+
291+
/**
292+
* @return list<ClassMemberUsage>
293+
*/
294+
private function traverseDataArg(
295+
Expr $dataExpr,
296+
Node $node,
297+
Scope $scope,
298+
): array
299+
{
300+
$referencedClassNames = $scope->getType($dataExpr)->getReferencedClasses();
301+
$rootContext = $this->getRootContext($node, $scope);
302+
303+
return $this->traverser->getUsages($referencedClassNames, $rootContext, $this);
304+
}
305+
306+
/**
307+
* @return non-empty-string
308+
*/
309+
private function getRootContext(
310+
Node $node,
311+
Scope $scope,
312+
): string
313+
{
314+
$functionName = $scope->getFunctionName();
315+
316+
if (!$scope->isInClass() || $functionName === null) {
317+
return 'unknown';
318+
}
319+
320+
return "{$scope->getClassReflection()->getName()}::{$functionName}({$node->getStartLine()})";
321+
}
322+
323+
}

0 commit comments

Comments
 (0)