Skip to content

Commit ffc9927

Browse files
authored
Report line of first usage (#6)
1 parent 924de19 commit ffc9927

12 files changed

+101
-55
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Found shadow dependencies!
3636
3737
```
3838

39-
You can add `--verbose` flag to see first usage of each class.
39+
You can add `--verbose` flag to see first usage (file & line) of each class.
4040

4141
## What it does:
4242
This tool reads your `composer.json` and scans all paths listed in both `autoload` sections while analysing:

src/ComposerDependencyAnalyser.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public function scan(array $scanPaths): array
8888

8989
foreach ($scanPaths as $scanPath => $isDevPath) {
9090
foreach ($this->listPhpFilesIn($scanPath) as $filePath) {
91-
foreach ($this->getUsedSymbolsInFile($filePath) as $usedSymbol) {
91+
foreach ($this->getUsedSymbolsInFile($filePath) as $usedSymbol => $lineNumber) {
9292
if ($this->isInternalClass($usedSymbol)) {
9393
continue;
9494
}
@@ -99,7 +99,7 @@ public function scan(array $scanPaths): array
9999

100100
if (!isset($this->optimizedClassmap[$usedSymbol])) {
101101
if (!$this->isConstOrFunction($usedSymbol)) {
102-
$errors[$usedSymbol] = new ClassmapEntryMissingError($usedSymbol, $filePath);
102+
$errors[$usedSymbol] = new ClassmapEntryMissingError($usedSymbol, $filePath, $lineNumber);
103103
}
104104

105105
continue;
@@ -114,11 +114,11 @@ public function scan(array $scanPaths): array
114114
$packageName = $this->getPackageNameFromVendorPath($classmapPath);
115115

116116
if ($this->isShadowDependency($packageName)) {
117-
$errors[$usedSymbol] = new ShadowDependencyError($usedSymbol, $packageName, $filePath);
117+
$errors[$usedSymbol] = new ShadowDependencyError($usedSymbol, $packageName, $filePath, $lineNumber);
118118
}
119119

120120
if (!$isDevPath && $this->isDevDependency($packageName)) {
121-
$errors[$usedSymbol] = new DevDependencyInProductionCodeError($usedSymbol, $packageName, $filePath);
121+
$errors[$usedSymbol] = new DevDependencyInProductionCodeError($usedSymbol, $packageName, $filePath, $lineNumber);
122122
}
123123
}
124124
}
@@ -148,7 +148,7 @@ private function getPackageNameFromVendorPath(string $realPath): string
148148
}
149149

150150
/**
151-
* @return list<string>
151+
* @return array<string, int>
152152
*/
153153
private function getUsedSymbolsInFile(string $filePath): array
154154
{

src/Error/ClassmapEntryMissingError.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,20 @@ class ClassmapEntryMissingError implements SymbolError
1515
*/
1616
private $exampleUsageFilepath;
1717

18+
/**
19+
* @var int
20+
*/
21+
private $exampleUsageLine;
22+
1823
public function __construct(
1924
string $className,
20-
string $exampleUsageFilepath
25+
string $exampleUsageFilepath,
26+
int $exampleUsageLine
2127
)
2228
{
2329
$this->className = $className;
2430
$this->exampleUsageFilepath = $exampleUsageFilepath;
31+
$this->exampleUsageLine = $exampleUsageLine;
2532
}
2633

2734
public function getSymbolName(): string
@@ -39,4 +46,9 @@ public function getPackageName(): ?string
3946
return null;
4047
}
4148

49+
public function getExampleUsageLine(): int
50+
{
51+
return $this->exampleUsageLine;
52+
}
53+
4254
}

src/Error/DevDependencyInProductionCodeError.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,22 @@ class DevDependencyInProductionCodeError implements SymbolError
2020
*/
2121
private $exampleUsageFilepath;
2222

23+
/**
24+
* @var int
25+
*/
26+
private $exampleUsageLine;
27+
2328
public function __construct(
2429
string $className,
2530
string $packageName,
26-
string $exampleUsageFilepath
31+
string $exampleUsageFilepath,
32+
int $exampleUsageLine
2733
)
2834
{
2935
$this->className = $className;
3036
$this->packageName = $packageName;
3137
$this->exampleUsageFilepath = $exampleUsageFilepath;
38+
$this->exampleUsageLine = $exampleUsageLine;
3239
}
3340

3441
public function getPackageName(): string
@@ -46,4 +53,9 @@ public function getExampleUsageFilepath(): string
4653
return $this->exampleUsageFilepath;
4754
}
4855

56+
public function getExampleUsageLine(): int
57+
{
58+
return $this->exampleUsageLine;
59+
}
60+
4961
}

src/Error/ShadowDependencyError.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,22 @@ class ShadowDependencyError implements SymbolError
2020
*/
2121
private $exampleUsageFilepath;
2222

23+
/**
24+
* @var int
25+
*/
26+
private $exampleUsageLine;
27+
2328
public function __construct(
2429
string $className,
2530
string $packageName,
26-
string $exampleUsageFilepath
31+
string $exampleUsageFilepath,
32+
int $exampleUsageLine
2733
)
2834
{
2935
$this->className = $className;
3036
$this->packageName = $packageName;
3137
$this->exampleUsageFilepath = $exampleUsageFilepath;
38+
$this->exampleUsageLine = $exampleUsageLine;
3239
}
3340

3441
public function getPackageName(): string
@@ -46,4 +53,9 @@ public function getExampleUsageFilepath(): string
4653
return $this->exampleUsageFilepath;
4754
}
4855

56+
public function getExampleUsageLine(): int
57+
{
58+
return $this->exampleUsageLine;
59+
}
60+
4961
}

src/Error/SymbolError.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ public function getSymbolName(): string;
1111

1212
public function getExampleUsageFilepath(): string;
1313

14+
public function getExampleUsageLine(): int;
15+
1416
}

src/Printer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ private function printErrors(string $title, string $subtitle, array $errors, boo
9595
$this->printLine(" • <orange>{$error->getSymbolName()}</orange>$append");
9696

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

src/UsedSymbolExtractor.php

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public function __construct(string $code)
5353
* It does not produce any local names in current namespace
5454
* - this results in very limited functionality in files without namespace
5555
*
56-
* @return list<string>
56+
* @return array<string, int>
5757
* @license Inspired by https://github.com/doctrine/annotations/blob/2.0.0/lib/Doctrine/Common/Annotations/TokenParser.php
5858
*/
5959
public function parseUsedClasses(): array
@@ -62,6 +62,8 @@ public function parseUsedClasses(): array
6262
$useStatements = [];
6363

6464
while ($token = $this->getNextEffectiveToken()) {
65+
$tokenLine = is_array($token) ? $token[2] : 0;
66+
6567
if ($token[0] === T_USE) {
6668
$usedClass = $this->parseUseStatement();
6769

@@ -76,22 +78,25 @@ public function parseUsedClasses(): array
7678
}
7779

7880
if ($token[0] === T_NAME_FULLY_QUALIFIED) {
79-
$usedSymbols[] = $this->normalizeBackslash($token[1]);
81+
$symbolName = $this->normalizeBackslash($token[1]);
82+
$usedSymbols[$symbolName] = $tokenLine;
8083
}
8184

8285
if ($token[0] === T_NAME_QUALIFIED) {
8386
[$neededAlias] = explode('\\', $token[1], 2);
8487

8588
if (isset($useStatements[$neededAlias])) {
86-
$usedSymbols[] = $this->normalizeBackslash($useStatements[$neededAlias] . substr($token[1], strlen($neededAlias)));
89+
$symbolName = $this->normalizeBackslash($useStatements[$neededAlias] . substr($token[1], strlen($neededAlias)));
90+
$usedSymbols[$symbolName] = $tokenLine;
8791
}
8892
}
8993

9094
if ($token[0] === T_STRING) {
91-
$symbolName = $token[1];
95+
$name = $token[1];
9296

93-
if (isset($useStatements[$symbolName])) {
94-
$usedSymbols[] = $this->normalizeBackslash($useStatements[$symbolName]);
97+
if (isset($useStatements[$name])) {
98+
$symbolName = $this->normalizeBackslash($useStatements[$name]);
99+
$usedSymbols[$symbolName] = $tokenLine;
95100
}
96101
}
97102
} else {
@@ -105,20 +110,23 @@ public function parseUsedClasses(): array
105110
}
106111

107112
if ($token[0] === T_NS_SEPARATOR) { // fully qualified name
108-
$usedSymbols[] = $this->normalizeBackslash($this->parseNameForOldPhp());
113+
$symbolName = $this->normalizeBackslash($this->parseNameForOldPhp());
114+
$usedSymbols[$symbolName] = $tokenLine;
109115
}
110116

111117
if ($token[0] === T_STRING) {
112-
$symbolName = $this->parseNameForOldPhp();
118+
$name = $this->parseNameForOldPhp();
113119

114-
if (isset($useStatements[$symbolName])) { // unqualified name
115-
$usedSymbols[] = $this->normalizeBackslash($useStatements[$symbolName]);
120+
if (isset($useStatements[$name])) { // unqualified name
121+
$symbolName = $this->normalizeBackslash($useStatements[$name]);
122+
$usedSymbols[$symbolName] = $tokenLine;
116123

117124
} else {
118-
[$neededAlias] = explode('\\', $symbolName, 2);
125+
[$neededAlias] = explode('\\', $name, 2);
119126

120127
if (isset($useStatements[$neededAlias])) { // qualified name
121-
$usedSymbols[] = $this->normalizeBackslash($useStatements[$neededAlias] . substr($symbolName, strlen($neededAlias)));
128+
$symbolName = $this->normalizeBackslash($useStatements[$neededAlias] . substr($name, strlen($neededAlias)));
129+
$usedSymbols[$symbolName] = $tokenLine;
122130
}
123131
}
124132
}

tests/ComposerDependencyAnalyserTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ public function test(): void
3333
$result = $detector->scan([$scanPath => false]);
3434

3535
self::assertEquals([
36-
'Unknown\Clazz' => new ClassmapEntryMissingError('Unknown\Clazz', $scanPath),
37-
'Shadow\Package\Clazz' => new ShadowDependencyError('Shadow\Package\Clazz', 'shadow/package', $scanPath),
38-
'Dev\Package\Clazz' => new DevDependencyInProductionCodeError('Dev\Package\Clazz', 'dev/package', $scanPath),
36+
'Unknown\Clazz' => new ClassmapEntryMissingError('Unknown\Clazz', $scanPath, 11),
37+
'Shadow\Package\Clazz' => new ShadowDependencyError('Shadow\Package\Clazz', 'shadow/package', $scanPath, 15),
38+
'Dev\Package\Clazz' => new DevDependencyInProductionCodeError('Dev\Package\Clazz', 'dev/package', $scanPath, 16),
3939
], $result);
4040
}
4141

tests/PrinterTest.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ public function testPrintResult(): void
3939

4040
$output2 = $this->captureAndNormalizeOutput(static function () use ($printer): void {
4141
$printer->printResult([
42-
new ClassmapEntryMissingError('Foo', 'foo.php'),
43-
new ShadowDependencyError('Bar', 'some/package', 'bar.php'),
44-
new DevDependencyInProductionCodeError('Baz', 'some/package', 'baz.php'),
42+
new ClassmapEntryMissingError('Foo', 'foo.php', 11),
43+
new ShadowDependencyError('Bar', 'some/package', 'bar.php', 22),
44+
new DevDependencyInProductionCodeError('Baz', 'some/package', 'baz.php', 33),
4545
], false, true);
4646
});
4747

@@ -52,23 +52,23 @@ public function testPrintResult(): void
5252
(those are not present in composer classmap, so we cannot check them)
5353
5454
• Foo
55-
first usage in foo.php
55+
first usage in foo.php:11
5656
5757
5858
5959
Found shadow dependencies!
6060
(those are used, but not listed as dependency in composer.json)
6161
6262
• Bar (some/package)
63-
first usage in bar.php
63+
first usage in bar.php:22
6464
6565
6666
6767
Found dev dependencies in production code!
6868
(those are wrongly listed as dev dependency in composer.json)
6969
7070
• Baz (some/package)
71-
first usage in baz.php
71+
first usage in baz.php:33
7272

7373

7474

tests/UsedSymbolExtractorTest.php

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class UsedSymbolExtractorTest extends TestCase
99
{
1010

1111
/**
12-
* @param list<string> $expectedUsages
12+
* @param array<string, int> $expectedUsages
1313
* @dataProvider provideVariants
1414
*/
1515
public function test(string $path, array $expectedUsages): void
@@ -23,62 +23,62 @@ public function test(string $path, array $expectedUsages): void
2323
}
2424

2525
/**
26-
* @return iterable<array{string, list<string>}>
26+
* @return iterable<array{string, array<string, int>}>
2727
*/
2828
public function provideVariants(): iterable
2929
{
3030
yield 'use statements' => [
3131
__DIR__ . '/data/used-symbols/use-statements.php',
3232
[
33-
'PHPUnit\Framework\Exception',
34-
'PHPUnit\Framework\Warning',
35-
'PHPUnit\Framework\Error',
36-
'PHPUnit\Framework\OutputError',
37-
'PHPUnit\Framework\Constraint\IsNan',
38-
'PHPUnit\Framework\Constraint\IsFinite',
39-
'PHPUnit\Framework\Constraint\DirectoryExists',
40-
'PHPUnit\Framework\Constraint\FileExists',
33+
'PHPUnit\Framework\Exception' => 11,
34+
'PHPUnit\Framework\Warning' => 12,
35+
'PHPUnit\Framework\Error' => 13,
36+
'PHPUnit\Framework\OutputError' => 14,
37+
'PHPUnit\Framework\Constraint\IsNan' => 15,
38+
'PHPUnit\Framework\Constraint\IsFinite' => 16,
39+
'PHPUnit\Framework\Constraint\DirectoryExists' => 17,
40+
'PHPUnit\Framework\Constraint\FileExists' => 18,
4141
]
4242
];
4343

4444
yield 'various usages' => [
4545
__DIR__ . '/data/used-symbols/various-usages.php',
4646
[
47-
'DateTimeImmutable',
48-
'DateTimeInterface',
49-
'DateTime',
50-
'PHPUnit\Framework\Error',
51-
'LogicException',
47+
'DateTimeImmutable' => 12,
48+
'DateTimeInterface' => 12,
49+
'DateTime' => 12,
50+
'PHPUnit\Framework\Error' => 14,
51+
'LogicException' => 15,
5252
]
5353
];
5454

5555
yield 'bracket namespace' => [
5656
__DIR__ . '/data/used-symbols/bracket-namespace.php',
5757
[
58-
'DateTimeImmutable',
59-
'DateTime',
58+
'DateTimeImmutable' => 5,
59+
'DateTime' => 11,
6060
]
6161
];
6262

6363
yield 'other symbols' => [
6464
__DIR__ . '/data/used-symbols/other-symbols.php',
6565
[
66-
'DIRECTORY_SEPARATOR',
67-
'strlen',
66+
'DIRECTORY_SEPARATOR' => 9,
67+
'strlen' => 11,
6868
]
6969
];
7070

7171
yield 'relative namespace' => [
7272
__DIR__ . '/data/used-symbols/relative-namespace.php',
7373
[
74-
'DateTimeImmutable',
74+
'DateTimeImmutable' => 10,
7575
]
7676
];
7777

7878
yield 'global namespace' => [
7979
__DIR__ . '/data/used-symbols/global-namespace.php',
8080
[
81-
'DateTimeImmutable',
81+
'DateTimeImmutable' => 3,
8282
]
8383
];
8484
}

0 commit comments

Comments
 (0)