55namespace Composer \ApiSurfaceCheck ;
66
77use 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 ;
817use PhpParser \PrettyPrinter \Standard as PrettyPrinter ;
918use Roave \BetterReflection \BetterReflection ;
1019use Roave \BetterReflection \Reflection \ReflectionClass ;
1827use Roave \BetterReflection \Reflection \ReflectionType ;
1928use Roave \BetterReflection \Reflection \ReflectionUnionType ;
2029use Roave \BetterReflection \Reflector \DefaultReflector ;
30+ use Roave \BetterReflection \Reflector \Exception \IdentifierNotFound ;
2131use Roave \BetterReflection \Reflector \Reflector ;
2232use Roave \BetterReflection \SourceLocator \Type \AggregateSourceLocator ;
2333use Roave \BetterReflection \SourceLocator \Type \DirectoriesSourceLocator ;
3444final 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