1717import software .coley .recaf .info .JvmClassInfo ;
1818import software .coley .recaf .info .member .FieldMember ;
1919import software .coley .recaf .info .member .MethodMember ;
20+ import software .coley .recaf .path .BundlePathNode ;
2021import software .coley .recaf .path .ClassPathNode ;
22+ import software .coley .recaf .path .DirectoryPathNode ;
2123import software .coley .recaf .path .PathNode ;
24+ import software .coley .recaf .path .PathNodes ;
25+ import software .coley .recaf .path .ResourcePathNode ;
2226import software .coley .recaf .services .Service ;
2327import software .coley .recaf .services .mapping .Mappings ;
2428import software .coley .recaf .services .workspace .WorkspaceCloseListener ;
2832import software .coley .recaf .util .ReflectUtil ;
2933import software .coley .recaf .workspace .model .Workspace ;
3034import software .coley .recaf .workspace .model .bundle .AndroidClassBundle ;
35+ import software .coley .recaf .workspace .model .bundle .ClassBundle ;
3136import software .coley .recaf .workspace .model .bundle .JvmClassBundle ;
3237import software .coley .recaf .workspace .model .resource .ResourceAndroidClassListener ;
3338import software .coley .recaf .workspace .model .resource .ResourceJvmClassListener ;
@@ -162,6 +167,33 @@ public ResolverAdapter newJavaResolver(@Nonnull Workspace workspace, @Nonnull Co
162167 return newJavaResolver (workspace , poolFromWorkspace (workspace ), unit );
163168 }
164169
170+ /**
171+ * @param workspace
172+ * Workspace with classes to link resolved references to.
173+ * @param classContextPath
174+ * Path to the class that represents the code outlined by the compilation unit.
175+ * @param unit
176+ * Unit to resolve references within.
177+ *
178+ * @return Resolver to link results to {@link PathNode paths in the provided workspace}.
179+ */
180+ @ Nonnull
181+ public ResolverAdapter newJavaResolver (@ Nonnull Workspace workspace ,
182+ @ Nonnull ClassPathNode classContextPath ,
183+ @ Nonnull CompilationUnitModel unit ) {
184+ EntryPool pool = poolFromWorkspace (workspace );
185+ prefillReferencedClasses (workspace , pool , unit );
186+
187+ // Wrap the entry pool in a type that resolves against content in the given context path first
188+ // before using the default workspace lookup order.
189+ if (!isPrimary (classContextPath ) && pool instanceof WorkspaceBackedEntryPool workspacePool )
190+ pool = new ContextOverlayEntryPool (workspacePool , classContextPath );
191+
192+ ResolverAdapter resolver = new ResolverAdapter (workspace , unit , pool );
193+ resolver .setClassContext (classContextPath );
194+ return resolver ;
195+ }
196+
165197 /**
166198 * @param workspace
167199 * Workspace with classes to link resolved references to.
@@ -249,6 +281,26 @@ public AstServiceConfig getServiceConfig() {
249281 return config ;
250282 }
251283
284+ /**
285+ * In some cases we want context-sensitive resolution which may not be the default workspace resolution order.
286+ * This only occurs when the path does not belong to the primary resource in the workspace.
287+ *
288+ * @param classPath
289+ * Path to check.
290+ *
291+ * @return {@code true} if the given class path is from a primary resource, {@code false} otherwise.
292+ */
293+ private static boolean isPrimary (@ Nonnull ClassPathNode classPath ) {
294+ DirectoryPathNode directory = classPath .getParent ();
295+ if (directory == null )
296+ return false ;
297+ BundlePathNode bundle = directory .getParent ();
298+ if (bundle == null )
299+ return false ;
300+ ResourcePathNode resource = bundle .getParent ();
301+ return resource != null && resource .isPrimary ();
302+ }
303+
252304 /**
253305 * Stub class for cases where we cannot find information about a type in the workspace.
254306 */
@@ -286,20 +338,25 @@ public List<ClassEntry> getClassesInPackage(@Nullable String packageName) {
286338 * Pool that pulls classes from a {@link Workspace}.
287339 */
288340 private static class WorkspaceBackedEntryPool implements EntryPool , ResourceJvmClassListener , ResourceAndroidClassListener {
289- private final Map <String , ClassEntry > cache = new ConcurrentHashMap <>();
290- private final ClassEntry OBJECT_ENTRY ;
291- private final Set <String > hydratedRuntimePackages = ConcurrentHashMap .newKeySet ();
292- private final Workspace workspace ;
341+ protected final Map <String , ClassEntry > cache = new ConcurrentHashMap <>();
342+ protected final ClassEntry OBJECT_ENTRY ;
343+ protected final Set <String > hydratedRuntimePackages = ConcurrentHashMap .newKeySet ();
344+ protected final Workspace workspace ;
293345
294346 private WorkspaceBackedEntryPool (@ Nonnull Workspace workspace ) {
347+ this (workspace , true );
348+ }
349+
350+ private WorkspaceBackedEntryPool (@ Nonnull Workspace workspace , boolean attachListeners ) {
295351 this .workspace = workspace ;
296352
297353 // Special case, we want to have 'Object' built using the default reflective logic.
298354 OBJECT_ENTRY = ReflectiveClassEntry .build (cache , Object .class );
299355
300356 // TODO: When we have classes update, we will want to invalidate their child classes
301357 // in the cache as well.
302- workspace .getPrimaryResource ().addListener (this );
358+ if (attachListeners )
359+ workspace .getPrimaryResource ().addListener (this );
303360 }
304361
305362 @ Override
@@ -383,7 +440,7 @@ private void hydrateRuntimePackage(@Nullable String packageName, @Nonnull Naviga
383440 private void hydrateRuntimeClass (@ Nonnull String className ) {
384441 // Workspace will always allow us to pull classes from the runtime classpath,
385442 // so we can just ask it for the class and if it's there we'll add it to the pool.
386- ClassPathNode path = workspace . findClass (className );
443+ ClassPathNode path = findClass (className );
387444 if (path != null )
388445 computeEntry (path .getValue (), DEFAULT_TTL );
389446 }
@@ -399,21 +456,38 @@ private void hydrateRuntimeClass(@Nonnull String className) {
399456 * @return Class entry by the given name, if cached or discoverable in the workspace.
400457 */
401458 @ Nullable
402- private ClassEntry getClass (@ Nonnull String name , int ttl ) {
459+ protected ClassEntry getClass (@ Nonnull String name , int ttl ) {
460+ // Check for prior resolution.
403461 ClassEntry entry = cache .get (name );
404462 if (entry != null )
405463 return entry ;
464+
465+ // Abort if we've reached the end of our TTL.
406466 if (ttl <= 1 )
407467 return null ;
408468
409- ClassPathNode path = workspace .findClass (name );
469+ // Attempt to find the class in the workspace and compute an entry for it.
470+ ClassPathNode path = findClass (name );
410471 if (path == null )
411472 return null ;
412-
413473 ClassInfo info = path .getValue ();
414474 return computeEntry (info , ttl );
415475 }
416476
477+ /**
478+ * Find a class in the workspace by name.
479+ * Can be overridden to change lookup order / context.
480+ *
481+ * @param name
482+ * Name of class to find.
483+ *
484+ * @return Path to class in the workspace, or {@code null} if not found.
485+ */
486+ @ Nullable
487+ protected ClassPathNode findClass (@ Nonnull String name ) {
488+ return workspace .findClass (name );
489+ }
490+
417491 /**
418492 * @param info
419493 * Class model in the workspace to map to a form for context resolution.
@@ -423,7 +497,7 @@ private ClassEntry getClass(@Nonnull String name, int ttl) {
423497 * @return Newly created entry modeling the given class.
424498 */
425499 @ Nullable
426- private ClassEntry computeEntry (@ Nonnull ClassInfo info , int ttl ) {
500+ protected ClassEntry computeEntry (@ Nonnull ClassInfo info , int ttl ) {
427501 String className = info .getName ();
428502 ClassEntry entry = cache .get (className );
429503 if (entry != null )
@@ -505,15 +579,15 @@ private ClassEntry computeEntry(@Nonnull ClassInfo info, int ttl) {
505579 }
506580
507581 @ Nonnull
508- private ClassEntry lookupClassEntry (@ Nonnull String name , int ttl ) {
582+ protected ClassEntry lookupClassEntry (@ Nonnull String name , int ttl ) {
509583 ClassEntry entry = getClass (name , ttl );
510584 if (entry != null )
511585 return entry ;
512586 return fallbackClassEntry (name );
513587 }
514588
515589 @ Nonnull
516- private ClassEntry fallbackClassEntry (@ Nonnull String name ) {
590+ protected ClassEntry fallbackClassEntry (@ Nonnull String name ) {
517591 ClassEntry entry = cache .get (name );
518592 if (entry != null )
519593 return entry ;
@@ -559,6 +633,101 @@ public void onRemoveClass(@Nonnull WorkspaceResource resource, @Nonnull JvmClass
559633 }
560634 }
561635
636+ /**
637+ * Per-resolver overlay that changes class resolution order to favor a given path's associated bundle/resource.
638+ */
639+ private static class ContextOverlayEntryPool extends WorkspaceBackedEntryPool {
640+ private final WorkspaceBackedEntryPool delegate ;
641+ private final ClassPathNode classContextPath ;
642+ private final BundlePathNode bundleContextPath ;
643+ private final WorkspaceResource resourceContext ;
644+ private final String classContextName ;
645+
646+ private ContextOverlayEntryPool (@ Nonnull WorkspaceBackedEntryPool delegate , @ Nonnull ClassPathNode classContextPath ) {
647+ super (delegate .workspace , false ); // As a wrapper, don't attach listeners to the workspace. The delegate will handle that.
648+
649+ this .delegate = delegate ;
650+ this .classContextPath = classContextPath ;
651+
652+ // Extract context from path
653+ bundleContextPath = classContextPath .getParent ().getParent ();
654+ resourceContext = classContextPath .getValueOfType (WorkspaceResource .class );
655+ classContextName = classContextPath .getValue ().getName ();
656+ }
657+
658+ @ Nullable
659+ @ Override
660+ protected ClassEntry getClass (@ Nonnull String name , int ttl ) {
661+ ClassEntry entry = cache .get (name );
662+ if (entry != null )
663+ return entry ;
664+
665+ // Use our contextual lookup first, then fall back to the delegate pool.
666+ ClassPathNode contextualPath = findContextualClass (name );
667+ if (contextualPath != null )
668+ return computeEntry (contextualPath .getValue (), ttl );
669+ return delegate .getClass (name , ttl );
670+ }
671+
672+ @ Nonnull
673+ @ Override
674+ public List <ClassEntry > getClassesInPackage (@ Nullable String packageName ) {
675+ // Fill in the package with classes from the delegate pool first.
676+ Map <String , ClassEntry > entries = new LinkedHashMap <>();
677+ for (ClassEntry entry : delegate .getClassesInPackage (packageName ))
678+ entries .put (entry .getName (), entry );
679+
680+ // Then override with classes from the contextual resource.
681+ if (resourceContext != null ) {
682+ for (ClassBundle <? extends ClassInfo > bundle : resourceContext .classBundleStream ().toList ()) {
683+ for (ClassInfo info : bundle .values ()) {
684+ if (Objects .equals (packageName , info .getPackageName ())) {
685+ ClassEntry entry = getClass (info .getName ());
686+ if (entry != null )
687+ entries .put (entry .getName (), entry );
688+ }
689+ }
690+ }
691+ }
692+
693+ return List .copyOf (entries .values ());
694+ }
695+
696+ @ Nullable
697+ @ Override
698+ protected ClassPathNode findClass (@ Nonnull String name ) {
699+ // Use our contextual lookup first, then fall back to the delegate pool.
700+ ClassPathNode contextualPath = findContextualClass (name );
701+ if (contextualPath != null )
702+ return contextualPath ;
703+ return delegate .findClass (name );
704+ }
705+
706+ @ Nullable
707+ private ClassPathNode findContextualClass (@ Nonnull String name ) {
708+ // First check if the class context is the class we're looking for.
709+ if (classContextName .equals (name ))
710+ return classContextPath ;
711+
712+ // Next check the same bundle as the context.
713+ ClassInfo classInfo = (ClassInfo ) bundleContextPath .getValue ().get (name );
714+ if (classInfo != null )
715+ return bundleContextPath .child (classInfo );
716+
717+ // Then check the same resource as the context, across its other class bundles.
718+ if (resourceContext != null ) {
719+ for (ClassBundle <? extends ClassInfo > bundle : resourceContext .classBundleStream ().toList ()) {
720+ if (bundle == bundleContextPath .getValue ()) // We already checked this bundle, so skip it.
721+ continue ;
722+ classInfo = bundle .get (name );
723+ if (classInfo != null )
724+ return PathNodes .classPath (workspace , resourceContext , bundle , classInfo );
725+ }
726+ }
727+ return null ;
728+ }
729+ }
730+
562731 /**
563732 * Translated from reflective logic in {@link GenericTypes}
564733 * but adapted to use ASM's signature parser for our binary class models.
0 commit comments