Skip to content

Commit 42ad689

Browse files
committed
Do not scan all classes in vendor
1 parent 1de4e76 commit 42ad689

2 files changed

Lines changed: 74 additions & 15 deletions

File tree

action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ runs:
123123
if: steps.pr.outputs.number && steps.preflight.outputs.should-run == 'true'
124124
shell: bash
125125
working-directory: ${{ github.action_path }}
126-
run: composer install --no-dev --no-interaction --no-progress --classmap-authoritative
126+
run: composer install --no-dev --no-interaction --no-progress
127127

128128
- name: Install project dependencies
129129
id: project-deps

src/Snapshotter.php

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55
namespace Composer\ApiSurfaceCheck;
66

77
use PhpParser\Node\Expr;
8+
use PhpParser\Node\Stmt\Class_;
9+
use PhpParser\Node\Stmt\Enum_;
10+
use PhpParser\Node\Stmt\Interface_;
11+
use PhpParser\Node\Stmt\Trait_;
12+
use PhpParser\NodeFinder;
13+
use PhpParser\NodeTraverser;
14+
use PhpParser\NodeVisitor\NameResolver;
15+
use PhpParser\Parser;
16+
use PhpParser\ParserFactory;
817
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
918
use Roave\BetterReflection\BetterReflection;
1019
use Roave\BetterReflection\Reflection\ReflectionClass;
@@ -18,6 +27,7 @@
1827
use Roave\BetterReflection\Reflection\ReflectionType;
1928
use Roave\BetterReflection\Reflection\ReflectionUnionType;
2029
use Roave\BetterReflection\Reflector\DefaultReflector;
30+
use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound;
2131
use Roave\BetterReflection\Reflector\Reflector;
2232
use Roave\BetterReflection\SourceLocator\Type\AggregateSourceLocator;
2333
use Roave\BetterReflection\SourceLocator\Type\DirectoriesSourceLocator;
@@ -34,13 +44,15 @@
3444
final class Snapshotter
3545
{
3646
private PrettyPrinter $prettyPrinter;
47+
private Parser $parser;
3748

3849
/**
3950
* @param list<string> $sourceRoots Directories used to resolve parent classes / interfaces.
4051
*/
4152
public function __construct(private array $sourceRoots)
4253
{
4354
$this->prettyPrinter = new PrettyPrinter();
55+
$this->parser = (new ParserFactory())->createForHostVersion();
4456
}
4557

4658
/**
@@ -64,22 +76,29 @@ public function snapshot(array $files): array
6476
$reflector = $this->buildReflector();
6577
$records = [];
6678

67-
foreach ($reflector->reflectAllClasses() as $class) {
68-
$fileName = $class->getFileName();
69-
if ($fileName === null) {
70-
continue;
71-
}
72-
$abs = realpath($fileName);
73-
if ($abs === false || !isset($targetFiles[$abs])) {
74-
continue;
75-
}
79+
// Enumerate target classes by parsing each target file directly with
80+
// PhpParser, then resolving each FQCN through better-reflection. We
81+
// deliberately avoid $reflector->reflectAllClasses(): that would force
82+
// the source locators to recursively parse every PHP file in every
83+
// source root (including vendor/ when install-dependencies is on),
84+
// which can take many minutes on real-world projects. With per-file
85+
// enumeration the upfront cost is bounded by the number of changed
86+
// files; vendor is only touched lazily during parent resolution.
87+
foreach ($targetFiles as $abs => $relative) {
88+
foreach ($this->extractClassFqcns($abs) as $fqcn) {
89+
try {
90+
$class = $reflector->reflectClass($fqcn);
91+
} catch (IdentifierNotFound) {
92+
continue;
93+
}
7694

77-
if ($class->isAnonymous()) {
78-
continue;
79-
}
95+
if ($class->isAnonymous()) {
96+
continue;
97+
}
8098

81-
foreach ($this->collectFromClass($class, $targetFiles[$abs]) as $record) {
82-
$records[] = $record;
99+
foreach ($this->collectFromClass($class, $relative) as $record) {
100+
$records[] = $record;
101+
}
83102
}
84103
}
85104

@@ -91,6 +110,46 @@ public function snapshot(array $files): array
91110
return $records;
92111
}
93112

113+
/**
114+
* @return list<string> Fully-qualified names of named classes/interfaces/traits/enums declared in the file.
115+
*/
116+
private function extractClassFqcns(string $absPath): array
117+
{
118+
$code = @file_get_contents($absPath);
119+
if ($code === false) {
120+
return [];
121+
}
122+
123+
$ast = $this->parser->parse($code);
124+
if ($ast === null) {
125+
return [];
126+
}
127+
128+
$traverser = new NodeTraverser();
129+
$traverser->addVisitor(new NameResolver());
130+
$ast = $traverser->traverse($ast);
131+
132+
$nodes = (new NodeFinder())->find(
133+
$ast,
134+
static fn ($node): bool =>
135+
$node instanceof Class_
136+
|| $node instanceof Interface_
137+
|| $node instanceof Trait_
138+
|| $node instanceof Enum_,
139+
);
140+
141+
$fqcns = [];
142+
foreach ($nodes as $node) {
143+
$name = $node->namespacedName ?? null;
144+
if ($name === null) {
145+
continue;
146+
}
147+
$fqcns[] = $name->toString();
148+
}
149+
150+
return $fqcns;
151+
}
152+
94153
private function buildReflector(): Reflector
95154
{
96155
$br = new BetterReflection();

0 commit comments

Comments
 (0)