Skip to content

Commit

Permalink
refactor the collection of views used in other views to new class
Browse files Browse the repository at this point in the history
  • Loading branch information
canvural committed Nov 7, 2022
1 parent 610eec3 commit d52d8a7
Show file tree
Hide file tree
Showing 18 changed files with 309 additions and 37 deletions.
20 changes: 19 additions & 1 deletion extension.neon
Expand Up @@ -5,6 +5,8 @@ parameters:
earlyTerminatingFunctionCalls:
- abort
- dd
excludePaths:
- *.blade.php
mixinExcludeClasses:
- Eloquent
bootstrapFiles:
Expand All @@ -16,6 +18,7 @@ parameters:
noUnnecessaryCollectionCallExcept: []
squashedMigrationsPath: []
databaseMigrationsPath: []
viewDirectories: []
checkModelProperties: false
checkPhpDocMissingReturn: false

Expand All @@ -26,6 +29,7 @@ parametersSchema:
noUnnecessaryCollectionCallOnly: listOf(string())
noUnnecessaryCollectionCallExcept: listOf(string())
databaseMigrationsPath: listOf(string())
viewDirectories: listOf(string())
squashedMigrationsPath: listOf(string())
checkModelProperties: bool()

Expand Down Expand Up @@ -450,10 +454,24 @@ services:
class: NunoMaduro\Larastan\Collectors\UsedEmailViewCollector
tags:
- phpstan.collector

-
class: NunoMaduro\Larastan\Collectors\UsedViewInAnotherViewCollector
class: NunoMaduro\Larastan\Collectors\UsedViewMakeCollector
tags:
- phpstan.collector

-
class: NunoMaduro\Larastan\Collectors\UsedViewFacadeMakeCollector
tags:
- phpstan.collector
-
class: NunoMaduro\Larastan\Collectors\UsedViewInAnotherViewCollector
arguments:
parser: @currentPhpVersionSimpleDirectParser
-
class: NunoMaduro\Larastan\Support\ViewFileHelper
arguments:
viewDirectories: %viewDirectories%
rules:
- NunoMaduro\Larastan\Rules\RelationExistenceRule
- NunoMaduro\Larastan\Rules\UselessConstructs\NoUselessWithFunctionCallsRule
Expand Down
2 changes: 1 addition & 1 deletion src/Collectors/UsedEmailViewCollector.php
Expand Up @@ -29,7 +29,7 @@ public function processNode(Node $node, Scope $scope): ?string
return null;
}

if ($name->name !== 'markdown') {
if (! in_array($name->name, ['markdown', 'view'], true)) {
return null;
}

Expand Down
59 changes: 59 additions & 0 deletions src/Collectors/UsedViewFacadeMakeCollector.php
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace NunoMaduro\Larastan\Collectors;

use Illuminate\Support\Facades\View;
use Illuminate\View\ViewName;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Type\ObjectType;

/** @implements Collector<Node\Expr\StaticCall, string> */
final class UsedViewFacadeMakeCollector implements Collector
{
public function getNodeType(): string
{
return Node\Expr\StaticCall::class;
}

/** @param Node\Expr\StaticCall $node */
public function processNode(Node $node, Scope $scope): ?string
{
$name = $node->name;

if (! $name instanceof Node\Identifier) {
return null;
}

if ($name->name !== 'make') {
return null;
}

if (count($node->getArgs()) < 1) {
return null;
}

$class = $node->class;

if (! $class instanceof Node\Name) {
return null;
}

$class = $scope->resolveName($class);

if (! (new ObjectType(View::class))->isSuperTypeOf(new ObjectType($class))->yes()) {
return null;
}

$template = $node->getArgs()[0]->value;

if (! $template instanceof Node\Scalar\String_) {
return null;
}

return ViewName::normalize($template->value);
}
}
39 changes: 29 additions & 10 deletions src/Collectors/UsedViewInAnotherViewCollector.php
Expand Up @@ -2,30 +2,49 @@

namespace NunoMaduro\Larastan\Collectors;

use NunoMaduro\Larastan\Support\ViewFileHelper;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Node\FileNode;
use PHPStan\Parser\Parser;
use PHPStan\Parser\ParserErrorsException;

/** @implements Collector<FileNode, string[]> */
class UsedViewInAnotherViewCollector implements Collector
final class UsedViewInAnotherViewCollector
{
/** @see https://regex101.com/r/8gosof/1 */
private const VIEW_NAME_REGEX = '/@(extends|include(If|Unless|When|First)?)(\(.*?\'(.*?)\'(\)|,))/m';

public function getNodeType(): string
public function __construct(private Parser $parser, private ViewFileHelper $viewFileHelper)
{
return FileNode::class;
}

public function processNode(Node $node, Scope $scope): ?array
/** @return list<string> */
public function getUsedViews(): array
{
$nodes = array_filter($node->getNodes(), function (Node $node) {
$usedViews = [];
foreach ($this->viewFileHelper->getAllViewFilePaths() as $viewFile) {
try {
$parserNodes = $this->parser->parseFile($viewFile);

$usedViews = array_merge($usedViews, $this->processNodes($parserNodes));
} catch (ParserErrorsException $e) {
continue;
}
}

return $usedViews;
}

/**
* @param Node\Stmt[] $nodes
* @return list<string>
*/
private function processNodes(array $nodes): array
{
$nodes = array_filter($nodes, function (Node $node) {
return $node instanceof Node\Stmt\InlineHTML;
});

if (count($nodes) === 0) {
return null;
return [];
}

$usedViews = [];
Expand Down
53 changes: 53 additions & 0 deletions src/Collectors/UsedViewMakeCollector.php
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace NunoMaduro\Larastan\Collectors;

use Illuminate\Contracts\View\Factory;
use Illuminate\View\ViewName;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Type\ObjectType;

/** @implements Collector<Node\Expr\MethodCall, string> */
final class UsedViewMakeCollector implements Collector
{
public function getNodeType(): string
{
return Node\Expr\MethodCall::class;
}

/** @param Node\Expr\MethodCall $node */
public function processNode(Node $node, Scope $scope): ?string
{
$name = $node->name;

if (! $name instanceof Node\Identifier) {
return null;
}

if ($name->name !== 'make') {
return null;
}

if (count($node->getArgs()) < 1) {
return null;
}

$class = $node->var;

if (! (new ObjectType(Factory::class))->isSuperTypeOf($scope->getType($class))->yes()) {
return null;
}

$template = $node->getArgs()[0]->value;

if (! $template instanceof Node\Scalar\String_) {
return null;
}

return ViewName::normalize($template->value);
}
}
2 changes: 1 addition & 1 deletion src/Methods/ViewWithMethodsClassReflectionExtension.php
Expand Up @@ -11,7 +11,7 @@ class ViewWithMethodsClassReflectionExtension implements MethodsClassReflectionE
{
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
if ($classReflection->getName() !== 'Illuminate\View\View') {
if (! in_array($classReflection->getName(), ['Illuminate\View\View', 'Illuminate\Contracts\View\View'], true)) {
return false;
}

Expand Down
33 changes: 21 additions & 12 deletions src/Rules/UnusedViewsRule.php
Expand Up @@ -5,58 +5,67 @@
namespace NunoMaduro\Larastan\Rules;

use function collect;
use Illuminate\Support\Facades\File;
use Illuminate\View\Factory;
use NunoMaduro\Larastan\Collectors\UsedEmailViewCollector;
use NunoMaduro\Larastan\Collectors\UsedViewFacadeMakeCollector;
use NunoMaduro\Larastan\Collectors\UsedViewFunctionCollector;
use NunoMaduro\Larastan\Collectors\UsedViewInAnotherViewCollector;
use NunoMaduro\Larastan\Collectors\UsedViewMakeCollector;
use NunoMaduro\Larastan\Support\ViewFileHelper;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\CollectedDataNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use Symfony\Component\Finder\SplFileInfo;

/** @implements Rule<CollectedDataNode> */
final class UnusedViewsRule implements Rule
{
/** @var list<string>|null */
private ?array $viewsUsedInOtherViews = null;

public function __construct(private UsedViewInAnotherViewCollector $usedViewInAnotherViewCollector, private ViewFileHelper $viewFileHelper)
{
}

public function getNodeType(): string
{
return CollectedDataNode::class;
}

public function processNode(Node $node, Scope $scope): array
{
if ($this->viewsUsedInOtherViews === null) {
$this->viewsUsedInOtherViews = $this->usedViewInAnotherViewCollector->getUsedViews();
}

$usedViews = collect([
$node->get(UsedViewFunctionCollector::class),
$node->get(UsedEmailViewCollector::class),
$node->get(UsedViewInAnotherViewCollector::class),
$node->get(UsedViewMakeCollector::class),
$node->get(UsedViewFacadeMakeCollector::class),
$this->viewsUsedInOtherViews,
])->flatten()->unique()->toArray();

$allViews = array_map(function (SplFileInfo $file) {
return $file->getPathname();
}, array_filter(File::allFiles(resource_path('views')), function (SplFileInfo $file) {
return ! str_contains($file->getPathname(), 'views/vendor') && str_ends_with($file->getFilename(), '.blade.php');
}));
$allViews = iterator_to_array($this->viewFileHelper->getAllViewNames());

$existingViews = [];

/** @var Factory $view */
$view = view();

foreach ($usedViews as $viewName) {
// Not existing views are reported with `view-string` type
if ($view->exists($viewName)) {
$existingViews[] = $view->getFinder()->find($viewName);
$existingViews[] = $viewName;
}
}

$unusedViews = array_diff($allViews, $existingViews);
$unusedViews = array_diff($allViews, array_filter($existingViews));

$errors = [];
foreach ($unusedViews as $file) {
$errors[] = RuleErrorBuilder::message('This view is not used in the project.')
->file($file)
->file($file.'.blade.php')
->line(0)
->build();
}
Expand Down
74 changes: 74 additions & 0 deletions src/Support/ViewFileHelper.php
@@ -0,0 +1,74 @@
<?php

declare(strict_types=1);

namespace NunoMaduro\Larastan\Support;

use Generator;
use PHPStan\File\FileHelper;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;

final class ViewFileHelper
{
/**
* @param list<non-empty-string> $viewDirectories
*/
public function __construct(private array $viewDirectories, private FileHelper $fileHelper)
{
if (count($viewDirectories) === 0) {
$this->viewDirectories = [resource_path('views')]; // @phpstan-ignore-line
}
}

public function getAllViewFilePaths(): Generator
{
foreach ($this->viewDirectories as $viewDirectory) {
$absolutePath = $this->fileHelper->absolutizePath($viewDirectory);

if (! is_dir($absolutePath)) {
continue;
}

$views = iterator_to_array(
new RegexIterator(
new RecursiveIteratorIterator(new RecursiveDirectoryIterator($absolutePath)),
'/\.blade\.php$/i'
)
);

foreach ($views as $view) {
yield $view->getPathname();
}
}
}

public function getAllViewNames(): Generator
{
foreach ($this->viewDirectories as $viewDirectory) {
$absolutePath = $this->fileHelper->absolutizePath($viewDirectory);

if (! is_dir($absolutePath)) {
continue;
}

$views = iterator_to_array(
new RegexIterator(
new RecursiveIteratorIterator(new RecursiveDirectoryIterator($absolutePath)),
'/\.blade\.php$/i'
)
);

foreach ($views as $view) {
if (str_contains($view->getPathname(), 'views/vendor')) {
continue;
}

$viewName = explode($viewDirectory.'/', $view->getPathname());

yield str_replace(['/', '.blade.php'], ['.', ''], $viewName[1]);
}
}
}
}
@@ -0,0 +1 @@
// Used with $this->markdown('emails.markdown')

0 comments on commit d52d8a7

Please sign in to comment.