Skip to content

Commit 67d8e1e

Browse files
committed
add enforce:imports command
1 parent 42dee51 commit 67d8e1e

File tree

3 files changed

+233
-0
lines changed

3 files changed

+233
-0
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<?php
2+
3+
namespace Imanghafoori\LaravelMicroscope\Features\EnforceImports;
4+
5+
use Imanghafoori\LaravelMicroscope\Check;
6+
use Imanghafoori\LaravelMicroscope\ErrorReporters\ErrorPrinter;
7+
use Imanghafoori\LaravelMicroscope\Features\CheckExtraFQCN\ExtraFQCN;
8+
use Imanghafoori\LaravelMicroscope\Foundations\PhpFileDescriptor;
9+
use Imanghafoori\LaravelMicroscope\SearchReplace\CachedFiles;
10+
use Imanghafoori\SearchReplace\Searcher;
11+
use Imanghafoori\TokenAnalyzer\ImportsAnalyzer;
12+
13+
class EnforceImports implements Check
14+
{
15+
public static function check(PhpFileDescriptor $file, $imports = [])
16+
{
17+
if (CachedFiles::isCheckedBefore('EnforceImports', $file)) {
18+
return;
19+
}
20+
21+
$tokens = $file->getTokens();
22+
$absFilePath = $file->getAbsolutePath();
23+
$class = $imports[1];
24+
$imports = ($imports[0])($file);
25+
26+
$classRefs = ImportsAnalyzer::findClassRefs($tokens, $absFilePath, $imports);
27+
28+
$hasError = self::checkClassRef($classRefs, $imports, $file, $class);
29+
30+
if ($hasError === false) {
31+
CachedFiles::put('EnforceImports', $file);
32+
}
33+
}
34+
35+
private static function checkClassRef(array $classRefs, array $imports, PhpFileDescriptor $file, $class): bool
36+
{
37+
$hasError = false;
38+
$namespace = $classRefs[1];
39+
$imports = array_values($imports)[0];
40+
$replacedRefs = [];
41+
$deletes = [];
42+
$original = file_get_contents($file->getAbsolutePath());
43+
44+
foreach ($classRefs[0] as $classRef) {
45+
if ($classRef['class'][0] !== '\\') {
46+
continue;
47+
}
48+
49+
if (self::isDirectlyImported($classRef['class'], $imports)) {
50+
continue;
51+
} elseif ($namespace && self::isInSameNamespace($namespace, $classRef['class'])) {
52+
continue;
53+
} else {
54+
$imports2 = self::restructureImports($imports);
55+
if (isset($imports2[ltrim($classRef['class'])])) {
56+
continue;
57+
}
58+
}
59+
$shouldBeSkipped = $class && strpos(basename($classRef['class']), $class) === false;
60+
61+
if ($shouldBeSkipped) {
62+
$hasError = true;
63+
continue;
64+
}
65+
66+
$className = basename($classRef['class']);
67+
68+
if ($namespace && ! (isset($deletes[$className]) && $deletes[$className] !== $classRef['class'])) {
69+
$absFilePath = $file->getAbsolutePath();
70+
if ($file->getFileName() !== $className.'.php') {
71+
ExtraFQCN::deleteFQCN($absFilePath, $classRef);
72+
$deletes[$className] = $classRef['class'];
73+
$replacedRefs[$classRef['class']] = $classRef['line'];
74+
}
75+
}
76+
}
77+
78+
$reverted = false;
79+
foreach ($replacedRefs as $classRef => $_) {
80+
$replacements = self::insertImport($file, $classRef);
81+
// in case we are not able to insert imports at the top:
82+
if (count($replacements) === 0) {
83+
file_put_contents($file->getAbsolutePath(), $original);
84+
$hasError = $reverted = true;
85+
break;
86+
}
87+
}
88+
89+
$header = 'FQCN got imported at the top';
90+
if (! $reverted) {
91+
foreach ($replacedRefs as $classRef => $line) {
92+
ErrorPrinter::singleton()->simplePendError($classRef, $file->getAbsolutePath(), $line, 'force_import', $header);
93+
}
94+
}
95+
96+
return $hasError;
97+
}
98+
99+
private static function insertImport(PhpFileDescriptor $file, $classRef)
100+
{
101+
[$string, $replacements] = Searcher::searchReplaceFirst([
102+
[
103+
'ignore_whitespaces' => false,
104+
'name' => 'enforceImports',
105+
'search' => 'namespace <any>;<white_space>?',
106+
'replace' => 'namespace <1>;'.PHP_EOL.PHP_EOL.'use '.ltrim($classRef, '\\').';'.PHP_EOL,
107+
],
108+
], $file->getTokens(true));
109+
file_put_contents($file->getAbsolutePath(), $string);
110+
111+
return $replacements;
112+
}
113+
114+
private static function isDirectlyImported($class, $imports): bool
115+
{
116+
return isset($imports[basename($class)]);
117+
}
118+
119+
private static function isInSameNamespace($namespace, $ref)
120+
{
121+
return trim(self::beforeLast($ref, '\\'), '\\') === $namespace;
122+
}
123+
124+
private static function beforeLast($subject, $search)
125+
{
126+
$pos = mb_strrpos($subject, $search) ?: 0;
127+
128+
return mb_substr($subject, 0, $pos, 'UTF-8');
129+
}
130+
131+
private static function restructureImports(array $imports): array
132+
{
133+
foreach ($imports as $key => $import) {
134+
$imports['\\'.$import[0]] = [$import[1], $key];
135+
unset($imports[$key]);
136+
}
137+
138+
return $imports;
139+
}
140+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
namespace Imanghafoori\LaravelMicroscope\Features\EnforceImports;
4+
5+
use Illuminate\Console\Command;
6+
use Imanghafoori\LaravelMicroscope\ErrorReporters\ErrorPrinter;
7+
use Imanghafoori\LaravelMicroscope\ErrorReporters\Psr4ReportPrinter;
8+
use Imanghafoori\LaravelMicroscope\Features\CheckImports\Reporters\CheckImportReporter;
9+
use Imanghafoori\LaravelMicroscope\Features\CheckImports\Reporters\Psr4Report;
10+
use Imanghafoori\LaravelMicroscope\ForAutoloadedPsr4Classes;
11+
use Imanghafoori\LaravelMicroscope\Foundations\PhpFileDescriptor;
12+
use Imanghafoori\LaravelMicroscope\Iterators\ForAutoloadedClassMaps;
13+
use Imanghafoori\LaravelMicroscope\Iterators\ForAutoloadedFiles;
14+
use Imanghafoori\LaravelMicroscope\PathFilterDTO;
15+
use Imanghafoori\LaravelMicroscope\SearchReplace\CachedFiles;
16+
use Imanghafoori\LaravelMicroscope\Traits\LogsErrors;
17+
use Imanghafoori\TokenAnalyzer\ParseUseStatement;
18+
use JetBrains\PhpStorm\Pure;
19+
20+
class EnforceImportsCommand extends Command
21+
{
22+
use LogsErrors;
23+
24+
protected $signature = 'enforce:imports
25+
{--class= : Fix references of the specified class}
26+
{--f|file= : Pattern for file names to scan}
27+
{--d|folder= : Pattern for file names to scan}
28+
{--F|except-file= : Pattern for file names to avoid}
29+
{--D|except-folder= : Pattern for folder names to avoid}';
30+
31+
protected $description = 'Enforces the imports to be at the top.';
32+
33+
protected $customMsg = 'All the class references are imported. \(^_^)/';
34+
35+
public function handle()
36+
{
37+
event('microscope.start.command');
38+
$this->line('');
39+
$this->info('Checking class references...');
40+
41+
$pathDTO = PathFilterDTO::makeFromOption($this);
42+
43+
$class = $this->option('class');
44+
45+
$useStatementParser = [self::useStatementParser(), $class];
46+
47+
$checks = [EnforceImports::class];
48+
49+
$classMapStats = ForAutoloadedClassMaps::check(base_path(), $checks, $useStatementParser, $pathDTO);
50+
$autoloadedFiles = ForAutoloadedFiles::check(base_path(), $checks, $useStatementParser, $pathDTO);
51+
$psr4Stats = ForAutoloadedPsr4Classes::check($checks, $useStatementParser, $pathDTO);
52+
53+
$errorPrinter = ErrorPrinter::singleton($this->output);
54+
55+
$consoleOutput = Psr4Report::getConsoleMessages($psr4Stats, $classMapStats, $autoloadedFiles);
56+
57+
$messages = self::getMessages($consoleOutput);
58+
59+
Psr4ReportPrinter::printAll($messages, $this->getOutput());
60+
61+
$this->finishCommand($errorPrinter);
62+
63+
$errorPrinter->printTime();
64+
65+
CachedFiles::writeCacheFiles();
66+
67+
return self::hasError() > 0 ? 1 : 0;
68+
}
69+
70+
#[Pure]
71+
private static function useStatementParser()
72+
{
73+
return function (PhpFileDescriptor $file) {
74+
$imports = ParseUseStatement::parseUseStatements($file->getTokens());
75+
76+
return $imports[0] ?: [$imports[1]];
77+
};
78+
}
79+
80+
private static function getMessages($autoloadStats)
81+
{
82+
return [
83+
CheckImportReporter::totalImportsMsg(),
84+
$autoloadStats,
85+
];
86+
}
87+
88+
private static function hasError()
89+
{
90+
return isset(ErrorPrinter::singleton()->errorsList['enforce_imports']);
91+
}
92+
}

src/ServiceProvider/CommandsRegistry.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ trait CommandsRegistry
4141
Commands\CheckExtraSemiColons::class,
4242
Commands\EnforceArrowFunctions::class,
4343
Features\CheckExtraFQCN\CheckExtraFQCNCommand::class,
44+
Features\EnforceImports\EnforceImportsCommand::class,
4445
];
4546

4647
private function registerCommands()

0 commit comments

Comments
 (0)