Test Setup Failed
Push — master ( f5019c...2203ff )
by Benjamin
04:49
created

BladeLinterCommand   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 163
Duplicated Lines 0 %

Importance

Changes 6
Bugs 1 Features 0
Metric Value
eloc 89
c 6
b 1
f 0
dl 0
loc 163
rs 10
wmc 28

5 Methods

Rating   Name   Duplication   Size   Complexity  
A getBladeFiles() 0 16 4
A checkFile() 0 21 3
B prepareBackends() 0 30 9
A getCodeClimateOutput() 0 14 5
B handle() 0 51 7
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Bdelespierre\LaravelBladeLinter;
6
7
use Illuminate\Console\Command;
8
use Illuminate\Support\Arr;
9
use Illuminate\Support\Facades\Blade;
10
use Illuminate\Support\Facades\Config;
11
use PhpParser\ParserFactory;
12
13
final class BladeLinterCommand extends Command
14
{
15
    protected $signature = 'blade:lint'
16
        . ' {--backend=*auto : Any of: auto, cli, eval, ext-ast, php-parser}'
17
        . ' {--fast}'
18
        . ' {--codeclimate=false : One of: stdout, stderr, false, or a FILE to open}'
19
        . ' {path?*}';
20
21
    protected $description = 'Checks Blade template syntax';
22
23
    public function handle(): int
24
    {
25
        $backends = $this->prepareBackends();
26
        $codeclimate = $this->getCodeClimateOutput();
27
        $allErrors = [];
28
        $nScanned = 0;
29
30
        if ($this->getOutput()->isVerbose()) {
31
            $this->info('blade-lint: Using backends: ' . join(', ', array_map(fn (Backend $backend) => $backend->name(), $backends)));
32
        }
33
34
        foreach ($this->getBladeFiles() as $file) {
35
            $errors = $this->checkFile($file, ...$backends);
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type RegexIterator; however, parameter $file of Bdelespierre\LaravelBlad...terCommand::checkFile() does only seem to accept SplFileInfo, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

35
            $errors = $this->checkFile(/** @scrutinizer ignore-type */ $file, ...$backends);
Loading history...
36
            if (count($errors) > 0) {
37
                $status = self::FAILURE;
38
                foreach ($errors as $error) {
39
                    $this->error($error->toString());
40
                }
41
            } elseif ($this->getOutput()->isVerbose()) {
42
                $this->line("No syntax errors detected in {$file->getPathname()}");
43
            }
44
45
            $allErrors = array_merge($allErrors, $errors);
46
            $nScanned++;
47
        }
48
49
        if ($codeclimate !== null) {
50
            fwrite($codeclimate, json_encode(
51
                array_map(function (ErrorRecord $error) {
52
                    return [
53
                        'type' => 'issue',
54
                        'check_name' => 'Laravel Blade Lint',
55
                        'description' => $error->message,
56
                        'fingerprint' => md5(join("|", [$error->message, $error->path, $error->line])),
57
                        'categories' => ['Bug Risk'],
58
                        'location' => [
59
                            'path' => $error->path,
60
                            'lines' => [
61
                                'begin' => $error->line,
62
                            ],
63
                        ],
64
                        'severity' => 'blocker'
65
                    ];
66
                }, $allErrors),
67
                JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR
68
            ));
69
        }
70
71
        $this->info('blade-lint: scanned: ' . $nScanned . ' files');
72
73
        return $status ?? self::SUCCESS;
74
    }
75
76
    /**
77
     * @return \Generator<\SplFileInfo>
78
     */
79
    protected function getBladeFiles(): \Generator
80
    {
81
        $paths = Arr::wrap($this->argument('path') ?: Config::get('view.paths'));
82
83
        foreach ($paths as $path) {
84
            if (is_file($path)) {
85
                yield new \SplFileInfo($path);
86
                continue;
87
            }
88
89
            $it = new \RecursiveDirectoryIterator($path);
90
            $it = new \RecursiveIteratorIterator($it);
91
            /** @var \RegexIterator<never, \SplFileInfo, \RecursiveIteratorIterator<\RecursiveDirectoryIterator>> $it */
92
            $it = new \RegexIterator($it, '/\.blade\.php$/', \RegexIterator::MATCH);
93
94
            yield from $it;
95
        }
96
    }
97
98
    /**
99
     * @return list<ErrorRecord>
0 ignored issues
show
Bug introduced by
The type Bdelespierre\LaravelBladeLinter\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
100
     */
101
    private function checkFile(\SplFileInfo $file, Backend ...$backends): array
102
    {
103
        $code = file_get_contents($file->getPathname());
104
105
        if ($code === false) {
106
            throw new \RuntimeException('Failed to open file ' . $file->getPathname());
107
        }
108
109
        // compile the file and send it to the linter process
110
        $compiled = Blade::compileString($code);
111
112
        $errors = [];
113
114
        foreach ($backends as $backend) {
115
            $errors = array_merge(
116
                $errors,
117
                $backend->analyze($file, $compiled)
118
            );
119
        }
120
121
        return $errors;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $errors returns the type array which is incompatible with the documented return type Bdelespierre\LaravelBladeLinter\list.
Loading history...
122
    }
123
124
    /**
125
     * @return Backend[]
126
     */
127
    private function prepareBackends(): array
128
    {
129
        return array_map(function ($backendSpec) {
130
            switch ($backendSpec) {
131
                default: // case 'auto':
132
                    $fast = $this->option('fast');
133
                    if ($fast && extension_loaded('ast')) {
134
                        goto ext_ast;
135
                    } elseif ($fast && class_exists(ParserFactory::class)) {
136
                        goto php_parser;
137
                    }
138
                    goto cli;
139
140
                case 'cli':
141
                    cli:
142
                    return new Backend\Cli();
143
144
                case 'eval':
145
                    return new Backend\Evaluate();
146
147
                case 'ext-ast':
148
                    ext_ast:
149
                    return new Backend\ExtAst();
150
151
                case 'php-parser':
152
                    php_parser:
153
                    return new Backend\PhpParser();
154
            }
155
            /** @phpstan-ignore-next-line */
156
        }, (array) $this->option('backend'));
157
    }
158
159
    /**
160
     * @return ?resource
161
     */
162
    private function getCodeClimateOutput(): mixed
163
    {
164
        $codeclimate = $this->option('codeclimate') ?: 'stderr';
165
166
        /** @phpstan-ignore-next-line */
167
        if ($codeclimate === true || is_array($codeclimate)) {
168
            $codeclimate = 'stderr';
169
        }
170
171
        return match ($codeclimate) {
172
            'false' => null,
173
            'stderr' => STDERR,
174
            'stdout' => STDOUT,
175
            default => fopen($codeclimate, 'w') ?: null,
176
        };
177
    }
178
}
179