Skip to content

Commit cc1676c

Browse files
committed
Fix AST context resolving preference of primary resource content of same path when looking at supporting resource content
1 parent f35a75b commit cc1676c

5 files changed

Lines changed: 345 additions & 20 deletions

File tree

recaf-core/src/main/java/software/coley/recaf/services/source/AstService.java

Lines changed: 181 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717
import software.coley.recaf.info.JvmClassInfo;
1818
import software.coley.recaf.info.member.FieldMember;
1919
import software.coley.recaf.info.member.MethodMember;
20+
import software.coley.recaf.path.BundlePathNode;
2021
import software.coley.recaf.path.ClassPathNode;
22+
import software.coley.recaf.path.DirectoryPathNode;
2123
import software.coley.recaf.path.PathNode;
24+
import software.coley.recaf.path.PathNodes;
25+
import software.coley.recaf.path.ResourcePathNode;
2226
import software.coley.recaf.services.Service;
2327
import software.coley.recaf.services.mapping.Mappings;
2428
import software.coley.recaf.services.workspace.WorkspaceCloseListener;
@@ -28,6 +32,7 @@
2832
import software.coley.recaf.util.ReflectUtil;
2933
import software.coley.recaf.workspace.model.Workspace;
3034
import software.coley.recaf.workspace.model.bundle.AndroidClassBundle;
35+
import software.coley.recaf.workspace.model.bundle.ClassBundle;
3136
import software.coley.recaf.workspace.model.bundle.JvmClassBundle;
3237
import software.coley.recaf.workspace.model.resource.ResourceAndroidClassListener;
3338
import 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

Comments
 (0)