Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report line of first usage #6

Merged
merged 3 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 5 additions & 5 deletions src/ComposerDependencyAnalyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
Expand All @@ -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);
}
}
}
Expand Down Expand Up @@ -148,7 +148,7 @@ private function getPackageNameFromVendorPath(string $realPath): string
}

/**
* @return list<string>
* @return array<string, int>
*/
private function getUsedSymbolsInFile(string $filePath): array
{
Expand Down
14 changes: 13 additions & 1 deletion src/Error/ClassmapEntryMissingError.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,4 +46,9 @@ public function getPackageName(): ?string
return null;
}

public function getExampleUsageLine(): int
{
return $this->exampleUsageLine;
}

}
14 changes: 13 additions & 1 deletion src/Error/DevDependencyInProductionCodeError.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -46,4 +53,9 @@ public function getExampleUsageFilepath(): string
return $this->exampleUsageFilepath;
}

public function getExampleUsageLine(): int
{
return $this->exampleUsageLine;
}

}
14 changes: 13 additions & 1 deletion src/Error/ShadowDependencyError.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -46,4 +53,9 @@ public function getExampleUsageFilepath(): string
return $this->exampleUsageFilepath;
}

public function getExampleUsageLine(): int
{
return $this->exampleUsageLine;
}

}
2 changes: 2 additions & 0 deletions src/Error/SymbolError.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ public function getSymbolName(): string;

public function getExampleUsageFilepath(): string;

public function getExampleUsageLine(): int;

}
2 changes: 1 addition & 1 deletion src/Printer.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ private function printErrors(string $title, string $subtitle, array $errors, boo
$this->printLine(" • <orange>{$error->getSymbolName()}</orange>$append");

if ($verbose) {
$this->printLine(" <gray>first usage in {$error->getExampleUsageFilepath()}</gray>" . PHP_EOL);
$this->printLine(" <gray>first usage in {$error->getExampleUsageFilepath()}:{$error->getExampleUsageLine()}</gray>" . PHP_EOL);
}
}

Expand Down
32 changes: 20 additions & 12 deletions src/UsedSymbolExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>
* @return array<string, int>
* @license Inspired by https://github.com/doctrine/annotations/blob/2.0.0/lib/Doctrine/Common/Annotations/TokenParser.php
*/
public function parseUsedClasses(): array
Expand All @@ -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();

Expand All @@ -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 {
Expand All @@ -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;
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions tests/ComposerDependencyAnalyserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
12 changes: 6 additions & 6 deletions tests/PrinterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand All @@ -52,23 +52,23 @@ 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



Found shadow dependencies!
(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



Found dev dependencies in production code!
(those are wrongly listed as dev dependency in composer.json)

• Baz (some/package)
first usage in baz.php
first usage in baz.php:33



Expand Down
42 changes: 21 additions & 21 deletions tests/UsedSymbolExtractorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class UsedSymbolExtractorTest extends TestCase
{

/**
* @param list<string> $expectedUsages
* @param array<string, int> $expectedUsages
* @dataProvider provideVariants
*/
public function test(string $path, array $expectedUsages): void
Expand All @@ -23,62 +23,62 @@ public function test(string $path, array $expectedUsages): void
}

/**
* @return iterable<array{string, list<string>}>
* @return iterable<array{string, array<string, int>}>
*/
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,
]
];
}
Expand Down
Loading
Loading