diff --git a/README.md b/README.md index 2b9f890..e00a8ee 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Found shadow dependencies! ``` -You can add `--verbose` flag to see first usage of each class. +You can add `--verbose` flag to see first usage (file & line) of each class. ## What it does: This tool reads your `composer.json` and scans all paths listed in both `autoload` sections while analysing: diff --git a/src/ComposerDependencyAnalyser.php b/src/ComposerDependencyAnalyser.php index 9359d18..8cfc302 100644 --- a/src/ComposerDependencyAnalyser.php +++ b/src/ComposerDependencyAnalyser.php @@ -88,7 +88,7 @@ public function scan(array $scanPaths): array foreach ($scanPaths as $scanPath => $isDevPath) { foreach ($this->listPhpFilesIn($scanPath) as $filePath) { - foreach ($this->getUsedSymbolsInFile($filePath) as $usedSymbol) { + foreach ($this->getUsedSymbolsInFile($filePath) as $usedSymbol => $lineNumber) { if ($this->isInternalClass($usedSymbol)) { continue; } @@ -99,7 +99,7 @@ public function scan(array $scanPaths): array if (!isset($this->optimizedClassmap[$usedSymbol])) { if (!$this->isConstOrFunction($usedSymbol)) { - $errors[$usedSymbol] = new ClassmapEntryMissingError($usedSymbol, $filePath); + $errors[$usedSymbol] = new ClassmapEntryMissingError($usedSymbol, $filePath, $lineNumber); } continue; @@ -114,11 +114,11 @@ public function scan(array $scanPaths): array $packageName = $this->getPackageNameFromVendorPath($classmapPath); if ($this->isShadowDependency($packageName)) { - $errors[$usedSymbol] = new ShadowDependencyError($usedSymbol, $packageName, $filePath); + $errors[$usedSymbol] = new ShadowDependencyError($usedSymbol, $packageName, $filePath, $lineNumber); } if (!$isDevPath && $this->isDevDependency($packageName)) { - $errors[$usedSymbol] = new DevDependencyInProductionCodeError($usedSymbol, $packageName, $filePath); + $errors[$usedSymbol] = new DevDependencyInProductionCodeError($usedSymbol, $packageName, $filePath, $lineNumber); } } } @@ -148,7 +148,7 @@ private function getPackageNameFromVendorPath(string $realPath): string } /** - * @return list + * @return array */ private function getUsedSymbolsInFile(string $filePath): array { diff --git a/src/Error/ClassmapEntryMissingError.php b/src/Error/ClassmapEntryMissingError.php index f7dedc3..9d032d6 100644 --- a/src/Error/ClassmapEntryMissingError.php +++ b/src/Error/ClassmapEntryMissingError.php @@ -15,13 +15,20 @@ class ClassmapEntryMissingError implements SymbolError */ private $exampleUsageFilepath; + /** + * @var int + */ + private $exampleUsageLine; + public function __construct( string $className, - string $exampleUsageFilepath + string $exampleUsageFilepath, + int $exampleUsageLine ) { $this->className = $className; $this->exampleUsageFilepath = $exampleUsageFilepath; + $this->exampleUsageLine = $exampleUsageLine; } public function getSymbolName(): string @@ -39,4 +46,9 @@ public function getPackageName(): ?string return null; } + public function getExampleUsageLine(): int + { + return $this->exampleUsageLine; + } + } diff --git a/src/Error/DevDependencyInProductionCodeError.php b/src/Error/DevDependencyInProductionCodeError.php index 94da9dc..ae00817 100644 --- a/src/Error/DevDependencyInProductionCodeError.php +++ b/src/Error/DevDependencyInProductionCodeError.php @@ -20,15 +20,22 @@ class DevDependencyInProductionCodeError implements SymbolError */ private $exampleUsageFilepath; + /** + * @var int + */ + private $exampleUsageLine; + public function __construct( string $className, string $packageName, - string $exampleUsageFilepath + string $exampleUsageFilepath, + int $exampleUsageLine ) { $this->className = $className; $this->packageName = $packageName; $this->exampleUsageFilepath = $exampleUsageFilepath; + $this->exampleUsageLine = $exampleUsageLine; } public function getPackageName(): string @@ -46,4 +53,9 @@ public function getExampleUsageFilepath(): string return $this->exampleUsageFilepath; } + public function getExampleUsageLine(): int + { + return $this->exampleUsageLine; + } + } diff --git a/src/Error/ShadowDependencyError.php b/src/Error/ShadowDependencyError.php index d9ee3c8..dfe0e78 100644 --- a/src/Error/ShadowDependencyError.php +++ b/src/Error/ShadowDependencyError.php @@ -20,15 +20,22 @@ class ShadowDependencyError implements SymbolError */ private $exampleUsageFilepath; + /** + * @var int + */ + private $exampleUsageLine; + public function __construct( string $className, string $packageName, - string $exampleUsageFilepath + string $exampleUsageFilepath, + int $exampleUsageLine ) { $this->className = $className; $this->packageName = $packageName; $this->exampleUsageFilepath = $exampleUsageFilepath; + $this->exampleUsageLine = $exampleUsageLine; } public function getPackageName(): string @@ -46,4 +53,9 @@ public function getExampleUsageFilepath(): string return $this->exampleUsageFilepath; } + public function getExampleUsageLine(): int + { + return $this->exampleUsageLine; + } + } diff --git a/src/Error/SymbolError.php b/src/Error/SymbolError.php index efb222d..f7c4880 100644 --- a/src/Error/SymbolError.php +++ b/src/Error/SymbolError.php @@ -11,4 +11,6 @@ public function getSymbolName(): string; public function getExampleUsageFilepath(): string; + public function getExampleUsageLine(): int; + } diff --git a/src/Printer.php b/src/Printer.php index 46fb322..eec32e0 100644 --- a/src/Printer.php +++ b/src/Printer.php @@ -95,7 +95,7 @@ private function printErrors(string $title, string $subtitle, array $errors, boo $this->printLine(" • {$error->getSymbolName()}$append"); if ($verbose) { - $this->printLine(" first usage in {$error->getExampleUsageFilepath()}" . PHP_EOL); + $this->printLine(" first usage in {$error->getExampleUsageFilepath()}:{$error->getExampleUsageLine()}" . PHP_EOL); } } diff --git a/src/UsedSymbolExtractor.php b/src/UsedSymbolExtractor.php index 2e9aaa0..842e8a4 100644 --- a/src/UsedSymbolExtractor.php +++ b/src/UsedSymbolExtractor.php @@ -53,7 +53,7 @@ public function __construct(string $code) * It does not produce any local names in current namespace * - this results in very limited functionality in files without namespace * - * @return list + * @return array * @license Inspired by https://github.com/doctrine/annotations/blob/2.0.0/lib/Doctrine/Common/Annotations/TokenParser.php */ public function parseUsedClasses(): array @@ -62,6 +62,8 @@ public function parseUsedClasses(): array $useStatements = []; while ($token = $this->getNextEffectiveToken()) { + $tokenLine = is_array($token) ? $token[2] : 0; + if ($token[0] === T_USE) { $usedClass = $this->parseUseStatement(); @@ -76,22 +78,25 @@ public function parseUsedClasses(): array } if ($token[0] === T_NAME_FULLY_QUALIFIED) { - $usedSymbols[] = $this->normalizeBackslash($token[1]); + $symbolName = $this->normalizeBackslash($token[1]); + $usedSymbols[$symbolName] = $tokenLine; } if ($token[0] === T_NAME_QUALIFIED) { [$neededAlias] = explode('\\', $token[1], 2); if (isset($useStatements[$neededAlias])) { - $usedSymbols[] = $this->normalizeBackslash($useStatements[$neededAlias] . substr($token[1], strlen($neededAlias))); + $symbolName = $this->normalizeBackslash($useStatements[$neededAlias] . substr($token[1], strlen($neededAlias))); + $usedSymbols[$symbolName] = $tokenLine; } } if ($token[0] === T_STRING) { - $symbolName = $token[1]; + $name = $token[1]; - if (isset($useStatements[$symbolName])) { - $usedSymbols[] = $this->normalizeBackslash($useStatements[$symbolName]); + if (isset($useStatements[$name])) { + $symbolName = $this->normalizeBackslash($useStatements[$name]); + $usedSymbols[$symbolName] = $tokenLine; } } } else { @@ -105,20 +110,23 @@ public function parseUsedClasses(): array } if ($token[0] === T_NS_SEPARATOR) { // fully qualified name - $usedSymbols[] = $this->normalizeBackslash($this->parseNameForOldPhp()); + $symbolName = $this->normalizeBackslash($this->parseNameForOldPhp()); + $usedSymbols[$symbolName] = $tokenLine; } if ($token[0] === T_STRING) { - $symbolName = $this->parseNameForOldPhp(); + $name = $this->parseNameForOldPhp(); - if (isset($useStatements[$symbolName])) { // unqualified name - $usedSymbols[] = $this->normalizeBackslash($useStatements[$symbolName]); + if (isset($useStatements[$name])) { // unqualified name + $symbolName = $this->normalizeBackslash($useStatements[$name]); + $usedSymbols[$symbolName] = $tokenLine; } else { - [$neededAlias] = explode('\\', $symbolName, 2); + [$neededAlias] = explode('\\', $name, 2); if (isset($useStatements[$neededAlias])) { // qualified name - $usedSymbols[] = $this->normalizeBackslash($useStatements[$neededAlias] . substr($symbolName, strlen($neededAlias))); + $symbolName = $this->normalizeBackslash($useStatements[$neededAlias] . substr($name, strlen($neededAlias))); + $usedSymbols[$symbolName] = $tokenLine; } } } diff --git a/tests/ComposerDependencyAnalyserTest.php b/tests/ComposerDependencyAnalyserTest.php index 49ce7e2..7d64135 100644 --- a/tests/ComposerDependencyAnalyserTest.php +++ b/tests/ComposerDependencyAnalyserTest.php @@ -33,9 +33,9 @@ public function test(): void $result = $detector->scan([$scanPath => false]); self::assertEquals([ - 'Unknown\Clazz' => new ClassmapEntryMissingError('Unknown\Clazz', $scanPath), - 'Shadow\Package\Clazz' => new ShadowDependencyError('Shadow\Package\Clazz', 'shadow/package', $scanPath), - 'Dev\Package\Clazz' => new DevDependencyInProductionCodeError('Dev\Package\Clazz', 'dev/package', $scanPath), + 'Unknown\Clazz' => new ClassmapEntryMissingError('Unknown\Clazz', $scanPath, 11), + 'Shadow\Package\Clazz' => new ShadowDependencyError('Shadow\Package\Clazz', 'shadow/package', $scanPath, 15), + 'Dev\Package\Clazz' => new DevDependencyInProductionCodeError('Dev\Package\Clazz', 'dev/package', $scanPath, 16), ], $result); } diff --git a/tests/PrinterTest.php b/tests/PrinterTest.php index 5c0015b..6c7db50 100644 --- a/tests/PrinterTest.php +++ b/tests/PrinterTest.php @@ -39,9 +39,9 @@ public function testPrintResult(): void $output2 = $this->captureAndNormalizeOutput(static function () use ($printer): void { $printer->printResult([ - new ClassmapEntryMissingError('Foo', 'foo.php'), - new ShadowDependencyError('Bar', 'some/package', 'bar.php'), - new DevDependencyInProductionCodeError('Baz', 'some/package', 'baz.php'), + new ClassmapEntryMissingError('Foo', 'foo.php', 11), + new ShadowDependencyError('Bar', 'some/package', 'bar.php', 22), + new DevDependencyInProductionCodeError('Baz', 'some/package', 'baz.php', 33), ], false, true); }); @@ -52,7 +52,7 @@ public function testPrintResult(): void (those are not present in composer classmap, so we cannot check them) • Foo - first usage in foo.php + first usage in foo.php:11 @@ -60,7 +60,7 @@ public function testPrintResult(): void (those are used, but not listed as dependency in composer.json) • Bar (some/package) - first usage in bar.php + first usage in bar.php:22 @@ -68,7 +68,7 @@ public function testPrintResult(): void (those are wrongly listed as dev dependency in composer.json) • Baz (some/package) - first usage in baz.php + first usage in baz.php:33 diff --git a/tests/UsedSymbolExtractorTest.php b/tests/UsedSymbolExtractorTest.php index fb1acd8..a728b25 100644 --- a/tests/UsedSymbolExtractorTest.php +++ b/tests/UsedSymbolExtractorTest.php @@ -9,7 +9,7 @@ class UsedSymbolExtractorTest extends TestCase { /** - * @param list $expectedUsages + * @param array $expectedUsages * @dataProvider provideVariants */ public function test(string $path, array $expectedUsages): void @@ -23,62 +23,62 @@ public function test(string $path, array $expectedUsages): void } /** - * @return iterable}> + * @return iterable}> */ public function provideVariants(): iterable { yield 'use statements' => [ __DIR__ . '/data/used-symbols/use-statements.php', [ - 'PHPUnit\Framework\Exception', - 'PHPUnit\Framework\Warning', - 'PHPUnit\Framework\Error', - 'PHPUnit\Framework\OutputError', - 'PHPUnit\Framework\Constraint\IsNan', - 'PHPUnit\Framework\Constraint\IsFinite', - 'PHPUnit\Framework\Constraint\DirectoryExists', - 'PHPUnit\Framework\Constraint\FileExists', + 'PHPUnit\Framework\Exception' => 11, + 'PHPUnit\Framework\Warning' => 12, + 'PHPUnit\Framework\Error' => 13, + 'PHPUnit\Framework\OutputError' => 14, + 'PHPUnit\Framework\Constraint\IsNan' => 15, + 'PHPUnit\Framework\Constraint\IsFinite' => 16, + 'PHPUnit\Framework\Constraint\DirectoryExists' => 17, + 'PHPUnit\Framework\Constraint\FileExists' => 18, ] ]; yield 'various usages' => [ __DIR__ . '/data/used-symbols/various-usages.php', [ - 'DateTimeImmutable', - 'DateTimeInterface', - 'DateTime', - 'PHPUnit\Framework\Error', - 'LogicException', + 'DateTimeImmutable' => 12, + 'DateTimeInterface' => 12, + 'DateTime' => 12, + 'PHPUnit\Framework\Error' => 14, + 'LogicException' => 15, ] ]; yield 'bracket namespace' => [ __DIR__ . '/data/used-symbols/bracket-namespace.php', [ - 'DateTimeImmutable', - 'DateTime', + 'DateTimeImmutable' => 5, + 'DateTime' => 11, ] ]; yield 'other symbols' => [ __DIR__ . '/data/used-symbols/other-symbols.php', [ - 'DIRECTORY_SEPARATOR', - 'strlen', + 'DIRECTORY_SEPARATOR' => 9, + 'strlen' => 11, ] ]; yield 'relative namespace' => [ __DIR__ . '/data/used-symbols/relative-namespace.php', [ - 'DateTimeImmutable', + 'DateTimeImmutable' => 10, ] ]; yield 'global namespace' => [ __DIR__ . '/data/used-symbols/global-namespace.php', [ - 'DateTimeImmutable', + 'DateTimeImmutable' => 3, ] ]; } diff --git a/tests/data/shadow-dependencies.php b/tests/data/shadow-dependencies.php index 47e1e31..1618f33 100644 --- a/tests/data/shadow-dependencies.php +++ b/tests/data/shadow-dependencies.php @@ -8,12 +8,12 @@ use DateTimeImmutable; use DateTimeInterface; -new \Unknown\Clazz(); +new \Unknown\Clazz(); // reported as unknown new AppClazz(); new Intermediate\Clazz(); new Clazz(); -new ShadowClazz(); -new DevClazz(); +new ShadowClazz(); // reported as shadow +new DevClazz(); // reported as dev new DateTimeImmutable(); echo \DIRECTORY_SEPARATOR;