Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,22 @@ public CustomASTVisitor(String query, SearchMatch match, QueryLocation location)
/*
* When visiting AST nodes, it may happen that we visit more nodes than
* needed. We need to ensure that we are only visiting ones that are found
* in the given search match. I wrote this for methods / constructors where
* I observed that node starts at the beginning of line whereas match starts
* at an offset within that line. However, both end on the same position. This
* could differ for other locations. In that case, change logic based on type of
* the node you get.
* in the given search match.
*
* The match offset/length points to the specific code location (e.g., the method
* call), while the AST node may have slightly different boundaries. We check if
* the match falls within the node's range to handle cases like qualified method
* references where the SearchMatch element is the containing method but the
* offset points to the actual call site.
*/
private boolean shouldVisit(ASTNode node) {
return (this.match.getOffset() + this.match.getLength()) ==
(node.getStartPosition() + node.getLength());
int matchStart = this.match.getOffset();
int matchEnd = matchStart + this.match.getLength();
int nodeStart = node.getStartPosition();
int nodeEnd = nodeStart + node.getLength();
boolean result = (matchStart >= nodeStart && matchEnd <= nodeEnd) ||
(matchEnd == nodeEnd);
return result;
}
Comment on lines 84 to 92
Copy link

@coderabbitai coderabbitai bot Dec 19, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Logic error: second condition allows matches extending before the node.

The condition (matchEnd == nodeEnd) on line 90 accepts matches that start before the node begins. For example, if matchStart = 10, matchEnd = 20 and nodeStart = 15, nodeEnd = 20, the second condition evaluates to true even though the match extends outside the node's start boundary. This contradicts the comment stating "match falls within the node's range" and could cause incorrect nodes to be visited.

🔎 Proposed fix

If the intent is to check whether the match is fully contained within the node:

-        boolean result = (matchStart >= nodeStart && matchEnd <= nodeEnd) ||
-               (matchEnd == nodeEnd);
+        boolean result = matchStart >= nodeStart && matchEnd <= nodeEnd;

Alternatively, if you need to handle overlapping boundaries where both start and end align:

-        boolean result = (matchStart >= nodeStart && matchEnd <= nodeEnd) ||
-               (matchEnd == nodeEnd);
+        boolean result = (matchStart >= nodeStart && matchEnd <= nodeEnd) ||
+               (matchStart == nodeStart && matchEnd == nodeEnd);

Or if you need to check for any overlap between match and node ranges:

-        boolean result = (matchStart >= nodeStart && matchEnd <= nodeEnd) ||
-               (matchEnd == nodeEnd);
+        boolean result = (matchStart < nodeEnd && matchEnd > nodeStart);

Please clarify the intended boundary semantics and adjust accordingly.

📝 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
private boolean shouldVisit(ASTNode node) {
return (this.match.getOffset() + this.match.getLength()) ==
(node.getStartPosition() + node.getLength());
int matchStart = this.match.getOffset();
int matchEnd = matchStart + this.match.getLength();
int nodeStart = node.getStartPosition();
int nodeEnd = nodeStart + node.getLength();
boolean result = (matchStart >= nodeStart && matchEnd <= nodeEnd) ||
(matchEnd == nodeEnd);
return result;
}
private boolean shouldVisit(ASTNode node) {
int matchStart = this.match.getOffset();
int matchEnd = matchStart + this.match.getLength();
int nodeStart = node.getStartPosition();
int nodeEnd = nodeStart + node.getLength();
boolean result = matchStart >= nodeStart && matchEnd <= nodeEnd;
return result;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@coderabbitai it is not possible to run into that situation as we will never have a method signature that encloses it container. is there a specific edge case you;re thinking of?

Copy link

Choose a reason for hiding this comment

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

@pranavgaikwad, the specific edge case I was concerned about was a scenario where matchStart < nodeStart but matchEnd == nodeEnd (e.g., match starting at offset 10, node starting at 15, both ending at 20). In that case, the second condition would evaluate to true even though the match extends before the node's start boundary.

However, I understand from your explanation that this won't occur in practice because of how JDT structures SearchMatch and AST nodes—the match won't extend outside its containing node's boundaries in this way. If that's based on your testing and understanding of JDT's behavior, then my concern doesn't apply, and the current logic is correct for your use case.

Thank you for the clarification! 🐰


🧠 Learnings used
Learnt from: shawn-hurley
Repo: konveyor/java-analyzer-bundle PR: 165
File: java-analyzer-bundle.test/projects/customers-tomcat-legacy/pom.xml:117-119
Timestamp: 2025-11-21T16:47:49.579Z
Learning: In the java-analyzer-bundle repository, test projects under `java-analyzer-bundle.test/projects/` (such as `customers-tomcat-legacy`) intentionally use legacy and vulnerable dependencies to provide realistic migration test fixtures. These vulnerable versions are required so that MTA (Migration Toolkit for Applications) rules can be validated for their ability to detect such issues. Do not flag vulnerable dependencies in these test fixture projects as security issues.


@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,15 @@ default boolean queryQualificationMatches(String query, IJavaElement matchedElem
// e.g. java.nio.file.Paths.get(String)/java.nio.file.Paths.get(*) -> java.nio.file.Paths.get
// Remove any parentheses and their contents
query = query.replaceAll("\\([^|]*\\)", "");
query = query.replaceAll("(?<!\\.)\\*", ".*");

String queryQualification = "";
int dotIndex = query.lastIndexOf('.');
if (dotIndex > 0) {
// for a query, java.io.paths.File*, queryQualification is java.io.paths
queryQualification = query.substring(0, dotIndex);
}

query = query.replaceAll("(?<!\\.)\\*", ".*");
// an element need not be imported if its referenced by fqn
if (!queryQualification.isEmpty() && (
matchedElement.getElementName().equals(queryQualification)
Expand Down Expand Up @@ -274,6 +276,15 @@ default boolean queryQualificationMatches(String query, IJavaElement matchedElem
return true;
}
}
// Handle same-package references: if queryQualification is a simple class name
// (no dots = no package prefix), the referenced class might be in the same package.
// Since same-package classes don't need imports, we should allow the match
// and let the AST visitor verify with binding information.
if (!queryQualification.contains(".")) {
logInfo("queryQualificationMatches: queryQualification '" + queryQualification +
"' has no package prefix, could be same-package reference. Allowing match.");
return true;
}
} catch (Exception e) {
logInfo("unable to determine accuracy of the match");
}
Expand Down
3 changes: 2 additions & 1 deletion java-analyzer-bundle.test/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ Require-Bundle: org.eclipse.jdt.junit4.runtime;bundle-version="1.1.0",
org.apache.commons.lang3,
org.junit;bundle-version="4.12",
org.eclipse.m2e.core,
org.eclipse.buildship.core
org.eclipse.buildship.core,
org.eclipse.m2e.maven.runtime
3 changes: 2 additions & 1 deletion java-analyzer-bundle.test/build.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
source.. = src/main/java/
output.. = target/classes/
bin.includes = META-INF/,\
.
.,\
projects/
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,22 @@ public void fileOperations() throws IOException {
public void initialize() {
System.out.println("Initializing SampleApplication");
}

/*
* This is a function that calls PackageUsageExample.merge()
* this is intended to test fully qualified method call and method
* method declaration queries. There are multiple merge() functions
* throughout the project, we want to only match on PackageUsageExample.merge()
*/
public static void callFullyQualifiedMethod() {
PackageUsageExample packageUsageExample = new PackageUsageExample();
packageUsageExample.merge(new Object());
}

/**
* See note on #callFullyQualifiedMethod()
*/
public void merge(Object o) {
System.out.println("Calling merge() from SampleApplication with object: " + o);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,11 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
String data = request.getParameter("data");
response.getWriter().println("Received: " + data);
}

/**
* See note on {@link SampleApplication#callFullyQualifiedMethod()}
*/
public void merge(Object o) {
System.out.println("Calling merge() from ServletExample with object: " + o);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package io.konveyor.tackle.core.internal.symbol;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.util.List;

import org.eclipse.lsp4j.SymbolInformation;
import org.junit.Test;

import io.konveyor.tackle.core.internal.testing.AbstractSymbolProviderTest;


public class MethodCallSymbolProviderIntegrationTest extends AbstractSymbolProviderTest {

@Test
public void testFullyQualifiedMethodCalls() {
List<SymbolInformation> results = searchMethodCalls("io.konveyor.demo.PackageUsageExample.merge");
results = inFile(results, "SampleApplication.java");
printResults(results);
assertTrue("[1] Should find usage of merge() call in test-project/SampleApplication.java#callFullyQualifiedMethod()",
results.size() == 1);

List<SymbolInformation> entityManagerMergeResults = searchMethodCalls("javax.persistence.EntityManager.merge");
entityManagerMergeResults = inFile(entityManagerMergeResults, "PackageUsageExample.java");
printResults(entityManagerMergeResults);
assertTrue("[2] Should find usage of entityManager.merge() call in test-project/PackageUsageExample.java#merge()",
entityManagerMergeResults.size() == 1);

// Half qualified method call should not work
List<SymbolInformation> halfQualifiedResults = searchMethodCalls("PackageUsageExample.merge");
printResults(halfQualifiedResults);
assertTrue("[3] Should not find any results matching PackageUsageExample.merge",
halfQualifiedResults.size() == 0);

// make sure patterns for fully qualified method calls work
List<SymbolInformation> mergeStarResults = searchMethodCalls("io.konveyor.demo.PackageUsageExample.merg*");
printResults(mergeStarResults);
assertTrue("[4] Should find 1 result matching io.konveyor.demo.PackageUsageExample.merg*",
mergeStarResults.size() == 1);
}


@Test
public void testNonQualifiedMethodCalls() {
List<SymbolInformation> allResults = searchMethodCalls("merge");

List<SymbolInformation> sampleAppResults = inFile(allResults, "SampleApplication.java");
printResults(sampleAppResults);
assertTrue("[1] Should find usage of merge() call in test-project/SampleApplication.java#callFullyQualifiedMethod()",
sampleAppResults.size() == 1);

List<SymbolInformation> packageExampleResults = inFile(allResults, "PackageUsageExample.java");
printResults(packageExampleResults);
assertTrue("[2] Should find usage of merge() call in test-project/PackageUsageExample.java#merge()",
packageExampleResults.size() == 1);

// make sure pattterns work
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix typo in comment.

The comment has a typo: "pattterns" should be "patterns".

🔎 Proposed fix
-        // make sure pattterns work
+        // make sure patterns work
📝 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
// make sure pattterns work
// make sure patterns work
🤖 Prompt for AI Agents
In
java-analyzer-bundle.test/src/main/java/io/konveyor/tackle/core/internal/symbol/MethodCallSymbolProviderIntegrationTest.java
around line 58, the inline comment contains a typo "pattterns"; update the
comment to read "patterns" (e.g., "// make sure patterns work") to correct the
spelling.

List<SymbolInformation> mergeStarResults = searchMethodCalls("merg*");
printResults(mergeStarResults);
assertTrue("[3] Should find usage of merge() call in test-project/SampleApplication.java#callFullyQualifiedMethod() and test-project/PackageUsageExample.java#merge()",
mergeStarResults.size() == 2);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package io.konveyor.tackle.core.internal.symbol;

import static org.junit.Assert.assertTrue;

import java.util.List;

import org.eclipse.lsp4j.SymbolInformation;
import org.junit.Test;

import io.konveyor.tackle.core.internal.testing.AbstractSymbolProviderTest;


public class MethodDeclarationSymbolProviderIntegrationTest extends AbstractSymbolProviderTest {

@Test
public void testFullyQualifiedMethodDeclarations() {
// Test fully qualified method declaration search for SampleApplication.merge
List<SymbolInformation> sampleAppResults = searchMethodDeclarations("io.konveyor.demo.SampleApplication.merge");
printResults(sampleAppResults);
assertTrue("[1] Should find merge() declaration in SampleApplication.java",
sampleAppResults.size() == 1);
assertTrue("[1b] Result should be in SampleApplication.java",
sampleAppResults.get(0).getLocation().getUri().contains("SampleApplication.java"));

// Test fully qualified method declaration search for PackageUsageExample.merge
List<SymbolInformation> packageExampleResults = searchMethodDeclarations("io.konveyor.demo.PackageUsageExample.merge");
printResults(packageExampleResults);
assertTrue("[2] Should find merge() declaration in PackageUsageExample.java",
packageExampleResults.size() == 1);
assertTrue("[2b] Result should be in PackageUsageExample.java",
packageExampleResults.get(0).getLocation().getUri().contains("PackageUsageExample.java"));

// Test fully qualified method declaration search for ServletExample.merge
List<SymbolInformation> servletResults = searchMethodDeclarations("io.konveyor.demo.ServletExample.merge");
printResults(servletResults);
assertTrue("[3] Should find merge() declaration in ServletExample.java",
servletResults.size() == 1);
assertTrue("[3b] Result should be in ServletExample.java",
servletResults.get(0).getLocation().getUri().contains("ServletExample.java"));

// Half qualified method declaration DOES work (different from method calls)
// This is because method declarations are matched by class name + method name
List<SymbolInformation> halfQualifiedResults = searchMethodDeclarations("SampleApplication.merge");
printResults(halfQualifiedResults);
assertTrue("[4] Should find 1 result matching SampleApplication.merge (half-qualified works for declarations)",
halfQualifiedResults.size() == 1);
assertTrue("[4b] Result should be in SampleApplication.java",
halfQualifiedResults.get(0).getLocation().getUri().contains("SampleApplication.java"));

// Test pattern matching with wildcard on class name
List<SymbolInformation> patternResults = searchMethodDeclarations("io.konveyor.demo.*.merge");
printResults(patternResults);
assertTrue("[5] Should find all 3 merge() declarations matching io.konveyor.demo.*.merge",
patternResults.size() == 3);
}


@Test
public void testNonQualifiedMethodDeclarations() {
// Search for all "merge" method declarations
List<SymbolInformation> allResults = searchMethodDeclarations("merge");
printResults(allResults);

// Should find all 3 merge() declarations in the project
assertTrue("[1] Should find all 3 merge() declarations in the project",
allResults.size() == 3);

// Verify each file has exactly one merge() declaration
List<SymbolInformation> sampleAppResults = inFile(allResults, "SampleApplication.java");
assertTrue("[2] Should find exactly 1 merge() declaration in SampleApplication.java",
sampleAppResults.size() == 1);

List<SymbolInformation> packageExampleResults = inFile(allResults, "PackageUsageExample.java");
assertTrue("[3] Should find exactly 1 merge() declaration in PackageUsageExample.java",
packageExampleResults.size() == 1);

List<SymbolInformation> servletResults = inFile(allResults, "ServletExample.java");
assertTrue("[4] Should find exactly 1 merge() declaration in ServletExample.java",
servletResults.size() == 1);

// Test pattern matching with wildcard
List<SymbolInformation> mergeStarResults = searchMethodDeclarations("merg*");
printResults(mergeStarResults);
assertTrue("[5] Should find all 3 merge() declarations matching merg*",
mergeStarResults.size() == 3);
}


@Test
public void testMethodDeclarationPatterns() {
// Test various pattern combinations

// Pattern: method name ending with 'e'
List<SymbolInformation> mergeResults = searchMethodDeclarations("*merge");
printResults(mergeResults);
assertTrue("[1] Should find all 3 merge() declarations matching *merge",
mergeResults.size() == 3);

// Test fully qualified pattern with class wildcard
List<SymbolInformation> demoPackageResults = searchMethodDeclarations("io.konveyor.demo.Sample*.merge");
printResults(demoPackageResults);
assertTrue("[2] Should find merge() in SampleApplication matching io.konveyor.demo.Sample*.merge",
demoPackageResults.size() == 1);

// Test fully qualified pattern with method wildcard
List<SymbolInformation> sampleAllMethods = searchMethodDeclarations("io.konveyor.demo.SampleApplication.*");
printResults(sampleAllMethods);
assertTrue("[3] Should find multiple method declarations in SampleApplication matching io.konveyor.demo.SampleApplication.*",
sampleAllMethods.size() > 1);
}
}
Loading
Loading