Test Setup Failed
Pull Request — master (#11)
by Benjamin
11:55 queued 02:07
created

BladeLinterCommand::getCodeClimateOutput()   A

Complexity

Conditions 5
Paths 2

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 8
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 11
rs 9.6111
1
<?php
2
declare(strict_types=1);
3
4
namespace Bdelespierre\LaravelBladeLinter;
5
6
use Illuminate\Console\Command;
7
use Illuminate\Support\Arr;
8
use Illuminate\Support\Facades\Blade;
9
use Illuminate\Support\Facades\Config;
10
use PhpParser\ParserFactory;
11
12
final class BladeLinterCommand extends Command
13
{
14
    protected $signature = 'blade:lint'
15
        . ' {--backend=*auto : Any of: auto, cli, eval, ext-ast, php-parser}'
16
        . ' {--fast}'
17
        . ' {--codeclimate=false : One of: stdout, stderr, false, or a FILE to open}'
18
        . ' {path?*}';
19
20
    protected $description = 'Checks Blade template syntax';
21
22
    public function handle(): int
23
    {
24
        $backends = $this->prepareBackends();
25
        $codeclimate = $this->getCodeClimateOutput();
26
        $allErrors = [];
27
        $nScanned = 0;
28
29
        if ($this->getOutput()->isVerbose()) {
30
            $this->info('blade-lint: Using backends: ' . join(', ', array_map(fn (Backend $backend) => $backend->name(), $backends)));
31
        }
32
33
        foreach ($this->getBladeFiles() as $file) {
34
            $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

34
            $errors = $this->checkFile(/** @scrutinizer ignore-type */ $file, ...$backends);
Loading history...
35
            if (count($errors) > 0) {
36
                $status = self::FAILURE;
37
                foreach ($errors as $error) {
38
                    $this->error($error->toString());
39
                }
40
            } elseif ($this->getOutput()->isVerbose()) {
41
                $this->line("No syntax errors detected in {$file->getPathname()}");
42
            }
43
44
            $allErrors = array_merge($allErrors, $errors);
45
            $nScanned++;
46
        }
47
48
        if ($codeclimate !== null) {
49
            fwrite($codeclimate, json_encode(
50
                array_map(function (ErrorRecord $error) {
51
                    return [
52
                        'type' => 'issue',
53
                        'check_name' => 'Laravel Blade Lint',
54
                        'description' => $error->message,
55
                        'fingerprint' => md5(join("|", [$error->message, $error->path, $error->line])),
56
                        'categories' => ['Bug Risk'],
57
                        'location' => [
58
                            'path' => $error->path,
59
                            'lines' => [
60
                                'begin' => $error->line,
61
                            ],
62
                        ],
63
                        'severity' => 'blocker'
64
                    ];
65
                }, $allErrors),
66
                JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR
67
            ));
68
        }
69
70
        $this->info('blade-lint: scanned: ' . $nScanned . ' files');
71
72
        return $status ?? self::SUCCESS;
73
    }
74
75
    /**
76
     * @return \Generator<\SplFileInfo>
77
     */
78
    protected function getBladeFiles(): \Generator
79
    {
80
        $paths = Arr::wrap($this->argument('path') ?: Config::get('view.paths'));
81
82
        foreach ($paths as $path) {
83
            if (is_file($path)) {
84
                yield new \SplFileInfo($path);
85
                continue;
86
            }
87
88
            $it = new \RecursiveDirectoryIterator($path);
89
            $it = new \RecursiveIteratorIterator($it);
90
            /** @var \RegexIterator<never, \SplFileInfo, \RecursiveIteratorIterator<\RecursiveDirectoryIterator>> $it */
91
            $it = new \RegexIterator($it, '/\.blade\.php$/', \RegexIterator::MATCH);
92
93
            yield from $it;
94
        }
95
    }
96
97
    /**
98
     * @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...
99
     */
100
    private function checkFile(\SplFileInfo $file, Backend ...$backends): array
101
    {
102
        $code = file_get_contents($file->getPathname());
103
104
        if ($code === false) {
105
            throw new \RuntimeException('Failed to open file ' . $file->getPathname());
106
        }
107
108
        // compile the file and send it to the linter process
109
        $compiled = Blade::compileString($code);
110
111
        $errors = [];
112
113
        foreach ($backends as $backend) {
114
            $errors = array_merge(
115
                $errors,
116
                $backend->analyze($file, $compiled)
117
            );
118
        }
119
120
        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...
121
    }
122
123
    /**
124
     * @return Backend[]
125
     */
126
    private function prepareBackends(): array
127
    {
128
        return array_map(function ($backendSpec) {
129
            switch ($backendSpec) {
130
                default: // case 'auto':
131
                    $fast = (bool)$this->option('fast');
132
                    if ($fast && extension_loaded('ast')) {
133
                        goto ext_ast;
134
                    } elseif ($fast && class_exists(ParserFactory::class)) {
135
                        goto php_parser;
136
                    }
137
                    goto cli;
138
139
                case 'cli':
140
                    cli:
141
                    return new Backend\Cli();
142
143
                case 'eval':
144
                    return new Backend\Evaluate();
145
146
                case 'ext-ast':
147
                    ext_ast:
148
                    return new Backend\ExtAst();
149
150
                case 'php-parser':
151
                    php_parser:
152
                    return new Backend\PhpParser();
153
            }
154
        }, (array) $this->option('backend'));
155
    }
156
157
    /**
158
     * @return ?resource
159
     */
160
    private function getCodeClimateOutput(): mixed
161
    {
162
        $codeclimate = $this->option('codeclimate') ?: 'stderr';
163
        if ($codeclimate === true || is_array($codeclimate)) {
164
            $codeclimate = 'stderr';
165
        }
166
        return match ($codeclimate) {
167
            'false' => null,
168
            'stderr' => STDERR,
169
            'stdout' => STDOUT,
170
            default => fopen($codeclimate, 'w') ?: null,
171
        };
172
    }
173
}
174