Skip to content

Comments

Add a direct sub classes data structure to the Painless lookup (#76955)#9

Open
MitchLewis930 wants to merge 1 commit intopr_019_beforefrom
pr_019_after
Open

Add a direct sub classes data structure to the Painless lookup (#76955)#9
MitchLewis930 wants to merge 1 commit intopr_019_beforefrom
pr_019_after

Conversation

@MitchLewis930
Copy link

@MitchLewis930 MitchLewis930 commented Jan 29, 2026

PR_019

Summary by CodeRabbit

  • Refactor

    • Improved class hierarchy tracking and resolution for more accurate method and field lookup.
    • Enhanced interface traversal logic to handle complex inheritance scenarios.
  • Tests

    • Added comprehensive unit tests for class hierarchy and subclass relationship validation.

✏️ Tip: You can customize this high-level summary in your review settings.

…ic#76955)

This change has two main components.

The first is to have method/field resolution for compile-time and run-time use the same code path for 
now. This removes copying of member methods between super and sub classes and instead does a 
resolution through the class hierarchy. This allows us to correctly implement the next change.

The second is a data structure that allows for the lookup of direct sub classes for all allow listed 
classes/interfaces within Painless.
@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

📝 Walkthrough

Walkthrough

This change introduces explicit tracking of direct subclass relationships in the Painless lookup system through a new field and corresponding getter. The builder now constructs class hierarchies during initialization. Lookup methods are refactored to use generalized traversal logic with improved breadth-first interface traversal and cycle detection.

Changes

Cohort / File(s) Summary
Core Lookup Enhancement
modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java
Added classesToDirectSubClasses field to track direct subclass mappings with public getter. Updated constructor to accept and store the mapping. Refactored lookupPainlessMethod and lookupPainlessField to use generalized lookupPainlessObject flow. Extended lookupRuntimePainlessObject with breadth-first interface traversal using HashSet for cycle detection and fallback to Object.class for interfaces.
Hierarchy Builder Refactoring
modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java
Replaced member copying logic with new buildPainlessClassHierarchy method that explicitly constructs direct subclass relationships. Infers direct superclass relationships by walking the superclass chain to find nearest allowed superclass. Propagates discovered hierarchy relationships and passes classesToDirectSubClasses map to PainlessLookup constructor.
Test Coverage
modules/lang-painless/src/test/java/org/elasticsearch/painless/LookupTests.java
New comprehensive unit test file validating direct subclass relationship tracking. Defines nested hierarchy of classes (A, B, C, D, AA, AB, AC, AD) and interfaces (X, Y, Z, U, V, T, S) with varying whitelist configurations. Tests getDirectSubClasses() across direct, inherited, empty, and null cases.
Test Resources
modules/lang-painless/src/test/resources/org/elasticsearch/painless/org.elasticsearch.painless.lookup
Whitelist entries for test classes including A, C, D, Z, V, U, T, S, AA, AC, AD to support test scenarios.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant PainlessLookup
    participant ClassHierarchy as Hierarchy Map
    participant Queue as Interface Queue
    
    Caller->>PainlessLookup: lookupPainlessObject(targetClass)
    PainlessLookup->>ClassHierarchy: Check targetClass mapping
    
    alt Class Found
        ClassHierarchy-->>PainlessLookup: Return match
    else Class Not Found
        PainlessLookup->>PainlessLookup: Check if interface?
        opt Is Interface
            PainlessLookup->>PainlessLookup: lookup on Object.class
        end
        
        PainlessLookup->>Queue: Initialize with getInterfaces()
        PainlessLookup->>PainlessLookup: Create resolvedInterfaces set
        
        loop While queue not empty
            Queue->>PainlessLookup: Next interface
            PainlessLookup->>ClassHierarchy: lookup on interface
            alt Match found
                ClassHierarchy-->>Caller: Return result
            else No match
                PainlessLookup->>Queue: Add parent interfaces (if not in resolvedInterfaces)
            end
        end
        
        PainlessLookup->>PainlessLookup: Walk superclass chain
        PainlessLookup->>ClassHierarchy: lookup on superclass
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 With whiskers twitching, I organize the trees,
Direct subclasses mapped with queues and BFS breeze,
No cycles shall confuse, no interfaces untrace—
The hierarchy stands clear, each class in its place! 🌳

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: introducing a direct subclasses data structure to the Painless lookup system.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java`:
- Around line 289-304: The traversal skips enqueuing parent interfaces when a
visited interface isn't allow‑listed because targetInterface.getInterfaces() is
only added inside the "if (targetPainlessClass != null)" block; modify the loop
in PainlessLookup so that after a targetInterface is marked visited
(resolvedInterfaces.add) you always enqueue its super-interfaces (call
targetInterfaces.addAll(Arrays.asList(targetInterface.getInterfaces())))
regardless of whether classesToPainlessClasses.containsKey(targetInterface) or
objectLookup returned null, preserving the existing checks for painlessObject
via objectLookup and avoiding duplicate visits via resolvedInterfaces.
🧹 Nitpick comments (1)
modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java (1)

86-88: Return an unmodifiable copy from getDirectSubClasses to prevent external mutation.

The current getter exposes internal mutable sets. A defensive copy keeps lookup state immutable and avoids accidental corruption from callers.

♻️ Suggested change
 public Set<Class<?>> getDirectSubClasses(Class<?> superClass) {
-    return classesToDirectSubClasses.get(superClass);
+    Set<Class<?>> direct = classesToDirectSubClasses.get(superClass);
+    return direct == null ? null : Set.copyOf(direct);
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a127154 and 718b163.

📒 Files selected for processing (4)
  • modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java
  • modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java
  • modules/lang-painless/src/test/java/org/elasticsearch/painless/LookupTests.java
  • modules/lang-painless/src/test/resources/org/elasticsearch/painless/org.elasticsearch.painless.lookup
🧰 Additional context used
🧬 Code graph analysis (1)
modules/lang-painless/src/test/java/org/elasticsearch/painless/LookupTests.java (2)
modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java (1)
  • PainlessLookup (27-313)
modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java (1)
  • PainlessLookupBuilder (73-1562)

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +289 to 304

while (targetInterfaces.isEmpty() == false) {
Class<?> targetInterface = targetInterfaces.remove(0);

if (resolvedInterfaces.add(targetInterface)) {
PainlessClass targetPainlessClass = classesToPainlessClasses.get(targetInterface);

if (targetPainlessClass != null) {
T painlessObject = objectLookup.apply(targetPainlessClass);

if (targetPainlessClass != null) {
T painlessObject = objectLookup.apply(targetPainlessClass);
if (painlessObject != null) {
return painlessObject;
}

if (painlessObject != null) {
return painlessObject;
targetInterfaces.addAll(Arrays.asList(targetInterface.getInterfaces()));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Ensure interface traversal reaches allow-listed ancestors even through non-allow-listed interfaces.

Currently, parent interfaces are only enqueued when the current interface is allow‑listed. If a class implements a non‑allow‑listed interface that extends an allow‑listed one, lookups can miss valid members. This is a correctness regression in hierarchy resolution.

🔧 Proposed fix
-                if (resolvedInterfaces.add(targetInterface)) {
-                    PainlessClass targetPainlessClass = classesToPainlessClasses.get(targetInterface);
-
-                    if (targetPainlessClass != null) {
-                        T painlessObject = objectLookup.apply(targetPainlessClass);
-
-                        if (painlessObject != null) {
-                            return painlessObject;
-                        }
-
-                        targetInterfaces.addAll(Arrays.asList(targetInterface.getInterfaces()));
-                    }
-                }
+                if (resolvedInterfaces.add(targetInterface)) {
+                    PainlessClass targetPainlessClass = classesToPainlessClasses.get(targetInterface);
+
+                    if (targetPainlessClass != null) {
+                        T painlessObject = objectLookup.apply(targetPainlessClass);
+                        if (painlessObject != null) {
+                            return painlessObject;
+                        }
+                    }
+                    // Always traverse parent interfaces, even when this interface isn't allow-listed.
+                    targetInterfaces.addAll(Arrays.asList(targetInterface.getInterfaces()));
+                }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while (targetInterfaces.isEmpty() == false) {
Class<?> targetInterface = targetInterfaces.remove(0);
if (resolvedInterfaces.add(targetInterface)) {
PainlessClass targetPainlessClass = classesToPainlessClasses.get(targetInterface);
if (targetPainlessClass != null) {
T painlessObject = objectLookup.apply(targetPainlessClass);
if (targetPainlessClass != null) {
T painlessObject = objectLookup.apply(targetPainlessClass);
if (painlessObject != null) {
return painlessObject;
}
if (painlessObject != null) {
return painlessObject;
targetInterfaces.addAll(Arrays.asList(targetInterface.getInterfaces()));
}
while (targetInterfaces.isEmpty() == false) {
Class<?> targetInterface = targetInterfaces.remove(0);
if (resolvedInterfaces.add(targetInterface)) {
PainlessClass targetPainlessClass = classesToPainlessClasses.get(targetInterface);
if (targetPainlessClass != null) {
T painlessObject = objectLookup.apply(targetPainlessClass);
if (painlessObject != null) {
return painlessObject;
}
}
// Always traverse parent interfaces, even when this interface isn't allow-listed.
targetInterfaces.addAll(Arrays.asList(targetInterface.getInterfaces()));
}
🤖 Prompt for AI Agents
In
`@modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java`
around lines 289 - 304, The traversal skips enqueuing parent interfaces when a
visited interface isn't allow‑listed because targetInterface.getInterfaces() is
only added inside the "if (targetPainlessClass != null)" block; modify the loop
in PainlessLookup so that after a targetInterface is marked visited
(resolvedInterfaces.add) you always enqueue its super-interfaces (call
targetInterfaces.addAll(Arrays.asList(targetInterface.getInterfaces())))
regardless of whether classesToPainlessClasses.containsKey(targetInterface) or
objectLookup returned null, preserving the existing checks for painlessObject
via objectLookup and avoiding duplicate visits via resolvedInterfaces.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants