Completed
Push — master ( ba93dd...aaaf11 )
by Benjamin
10:06
created

BladeLinterCommand::checkFile()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 8.8977
c 0
b 0
f 0
cc 6
nc 5
nop 1
1
<?php
2
3
namespace Bdelespierre\LaravelBladeLinter;
4
5
use Illuminate\Console\Command;
6
use Illuminate\Support\Arr;
7
use Illuminate\Support\Facades\Blade;
8
use Illuminate\Support\Facades\Config;
9
10
class BladeLinterCommand extends Command
11
{
12
    protected $signature = 'blade:lint {--phpstan=} {path?*}';
13
14
    protected $description = 'Checks Blade template syntax';
15
16
    public function handle()
17
    {
18
        foreach ($this->getBladeFiles() as $file) {
19
            if (! $this->checkFile($file)) {
20
                $status = self::FAILURE;
21
            }
22
        }
23
24
        return $status ?? self::SUCCESS;
25
    }
26
27
    protected function getBladeFiles(): \Generator
28
    {
29
        $paths = Arr::wrap($this->argument('path') ?: Config::get('view.paths'));
30
31
        foreach ($paths as $path) {
32
            if (is_file($path)) {
33
                yield new \SplFileInfo($path);
34
                continue;
35
            }
36
37
            $it = new \RecursiveDirectoryIterator($path);
38
            $it = new \RecursiveIteratorIterator($it);
39
            $it = new \RegexIterator($it, '/\.blade\.php$/', \RegexIterator::MATCH);
40
41
            yield from $it;
42
        }
43
    }
44
45
    protected function checkFile(\SplFileInfo $file)
46
    {
47
        // compile the file and send it to the linter process
48
        $compiled = Blade::compileString(file_get_contents($file));
49
50
        $result = $this->lint($compiled, $output, $error);
51
52
        if (! $result) {
53
            $this->error(str_replace("Standard input code", $file->getPathname(), rtrim($error)));
54
            return false;
55
        }
56
57
        if ($this->option('phpstan') && $errors = $this->analyse($compiled)) {
58
            foreach ($errors as $error) {
59
                $this->error("PHPStan error:  {$error->message} in {$file->getPathname()} on line {$error->line}");
60
            }
61
            return false;
62
        }
63
64
        if ($this->getOutput()->isVerbose()) {
65
            $this->line("No syntax errors detected in {$file->getPathname()}");
66
        }
67
68
        return true;
69
    }
70
71
    protected function lint(string $code, ?string &$stdout = "", ?string &$stderr = ""): bool
72
    {
73
        $descriptors = [
74
            0 => ["pipe", "r"], // read from stdin
75
            1 => ["pipe", "w"], // write to stdout
76
            2 => ["pipe", "w"], // write to stderr
77
        ];
78
79
        // open linter process (php -l)
80
        $process = proc_open('php -l', $descriptors, $pipes);
81
82
        if (! is_resource($process)) {
83
            throw new \RuntimeException("unable to open process 'php -l'");
84
        }
85
86
        fwrite($pipes[0], $code);
87
        fclose($pipes[0]);
88
89
        $stdout = stream_get_contents($pipes[1]);
90
        fclose($pipes[1]);
91
92
        $stderr = stream_get_contents($pipes[2]);
93
        fclose($pipes[2]);
94
95
        // it is important that you close any pipes before calling
96
        // proc_close in order to avoid a deadlock
97
        $retval = proc_close($process);
98
99
        // zero actually means "no error"
100
        return $retval == "0";
101
    }
102
103
    protected function analyse(string $code): array
104
    {
105
        // write to a temporary file
106
        // (phpstan doesn't support stdin)
107
        $path = tempnam(sys_get_temp_dir(), 'laravel-blade-linter');
108
109
        if (! file_put_contents($path, $code)) {
110
            throw new \RuntimeException("unable to write to {$path}");
111
        }
112
113
        try {
114
            return $this->analyseFile($path);
115
        } finally {
116
            unlink($path);
117
        }
118
    }
119
120
    protected function analyseFile(string $path): array
121
    {
122
        $errors = [];
123
124
        $phpstan = $this->option('phpstan');
125
126
        if (! is_executable($phpstan)) {
127
            throw new \RuntimeException("unable to run {$phpstan}");
128
        }
129
130
        ob_start(); // shell_exec echoes stderr...
131
        $output = `{$phpstan} analyse --error-format json --no-ansi --no-progress -- {$path} 2>/dev/null`;
132
        $stderr = ob_get_clean();
0 ignored issues
show
Unused Code introduced by
$stderr is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
133
134
        $json = json_decode($output);
135
136
        if (! $json instanceof \stdClass) {
137
            throw new \RuntimeException("unable to parse PHPStan output");
138
        }
139
140
        foreach ($json->files as $filename => $descriptor) {
141
            foreach ($descriptor->messages as $message) {
142
                $message->message = rtrim(lcfirst($message->message), '.');
143
                $errors[] = $message;
144
            }
145
        }
146
147
        return $errors;
148
    }
149
}
150