From b1b2724d65f15567f5c822a37d0cfa03ced9d3bc Mon Sep 17 00:00:00 2001 From: tsanders Date: Fri, 14 Nov 2025 08:40:14 -0500 Subject: [PATCH 1/7] feat: Support parameter-specific matching for METHOD_CALL patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #182 This commit adds the ability to distinguish between overloaded methods with different parameter signatures in METHOD_CALL and CONSTRUCTOR_CALL patterns. Previous behavior: - Patterns with parameter types (e.g., setProperty(String, String)) were stripped and matched all overloads of the method New behavior: - Parameter types are extracted and compared against actual method signatures - Supports wildcard matching (*) for any parameter type - Supports subtype matching (Object pattern matches String actual) - Supports generic signature matching (List only matches exact type) Implementation details: - Added extractParameterTypes() to parse parameter types from query patterns - Added matchesParameterTypes() to validate actual vs. query parameters - Added type matching with wildcard, subtype, and generic support - Updated visit() methods for MethodInvocation, ConstructorInvocation, and ClassInstanceCreation to check parameter types Examples: 1. Exact matching: Pattern: DriverManager.getConnection(String, String, String) Matches: DriverManager.getConnection("url", "user", "password") ✓ Rejects: DriverManager.getConnection("url") ✗ 2. Wildcard matching: Pattern: method(*, String) Matches: method with any first param and String second param 3. Subtype matching: Pattern: method(Object) Matches: method(String), method(Integer), etc. Backward compatibility: - Patterns without parameters continue to match all overloads - Existing rules without parameter types are unaffected Signed-off-by: tsanders --- .../internal/symbol/CustomASTVisitor.java | 224 +++++++++++++++++- 1 file changed, 215 insertions(+), 9 deletions(-) diff --git a/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java b/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java index f53459e..e40be07 100644 --- a/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java +++ b/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java @@ -2,6 +2,10 @@ import static org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin.logInfo; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.ClassInstanceCreation; @@ -29,6 +33,7 @@ public class CustomASTVisitor extends ASTVisitor { private SearchMatch match; private boolean symbolMatches; private QueryLocation location; + private List queryParameterTypes; /* * we re-use this same class for different locations in a query @@ -41,12 +46,20 @@ public enum QueryLocation { } public CustomASTVisitor(String query, SearchMatch match, QueryLocation location) { + /* + * Extract parameter types from the query pattern if present + * e.g., "java.util.Properties.setProperty(java.lang.String, java.lang.String)" + * We'll extract ["java.lang.String", "java.lang.String"] and strip the parameters from the query + */ + this.queryParameterTypes = extractParameterTypes(query); + String processedQuery = query.replaceAll("\\([^)]*\\)", ""); + /* * When comparing query pattern with an actual java element's fqn * we need to make sure that * not preceded with a . are replaced * by .* so that java regex works as expected on them */ - this.query = query.replaceAll("(? ["Type1", "Type2"] + * Returns null if no parameters are specified in the query + */ + private List extractParameterTypes(String query) { + int openParen = query.indexOf('('); + int closeParen = query.lastIndexOf(')'); + + if (openParen == -1 || closeParen == -1 || openParen >= closeParen) { + return null; // No parameters specified in query + } + + String paramsString = query.substring(openParen + 1, closeParen).trim(); + if (paramsString.isEmpty()) { + return Collections.emptyList(); // Empty parameter list: method() + } + + List params = new ArrayList<>(); + // Split by comma, handling nested generics like Map + int depth = 0; + int start = 0; + for (int i = 0; i < paramsString.length(); i++) { + char c = paramsString.charAt(i); + if (c == '<') { + depth++; + } else if (c == '>') { + depth--; + } else if (c == ',' && depth == 0) { + params.add(paramsString.substring(start, i).trim()); + start = i + 1; + } + } + // Add the last parameter + params.add(paramsString.substring(start).trim()); + + return params; + } + + /** + * Checks if the actual parameter types from a method binding match the query parameter types. + * Supports wildcards (*) and subtype matching. + */ + private boolean matchesParameterTypes(ITypeBinding[] actualTypes) { + if (queryParameterTypes == null) { + return true; // No parameter filter specified in query + } + + if (queryParameterTypes.size() != actualTypes.length) { + return false; // Different number of parameters + } + + for (int i = 0; i < queryParameterTypes.size(); i++) { + if (!typeMatches(queryParameterTypes.get(i), actualTypes[i])) { + return false; + } + } + + return true; + } + + /** + * Checks if a query type pattern matches an actual type binding. + * Supports: + * - Wildcards: "*" matches any type + * - Subtype matching: "java.lang.Object" matches "java.lang.String" + * - Generic signatures: "java.util.List" matches only that exact generic type + */ + private boolean typeMatches(String queryType, ITypeBinding actualType) { + if (actualType == null) { + return false; + } + + // Wildcard matches any type + if ("*".equals(queryType)) { + return true; + } + + // Get the qualified name of the actual type + String actualTypeName = getQualifiedTypeName(actualType); + + // Exact match + if (queryType.equals(actualTypeName)) { + return true; + } + + // Subtype matching: check if actualType is assignable to queryType + // This requires resolving the queryType to a binding, which is complex + // For now, we'll do a simple check by walking up the type hierarchy + return isSubtypeOf(actualType, queryType); + } + + /** + * Gets the fully qualified name of a type, including generic parameters. + * e.g., "java.util.List" + */ + private String getQualifiedTypeName(ITypeBinding type) { + if (type == null) { + return ""; + } + + // Handle arrays + if (type.isArray()) { + return getQualifiedTypeName(type.getElementType()) + "[]"; + } + + // Handle primitives + if (type.isPrimitive()) { + return type.getName(); + } + + // Get erasure for generic types to get base qualified name + ITypeBinding erasure = type.getErasure(); + String baseName = erasure != null ? erasure.getQualifiedName() : type.getQualifiedName(); + + // Add generic type arguments if present + ITypeBinding[] typeArguments = type.getTypeArguments(); + if (typeArguments != null && typeArguments.length > 0) { + StringBuilder sb = new StringBuilder(baseName); + sb.append("<"); + for (int i = 0; i < typeArguments.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(getQualifiedTypeName(typeArguments[i])); + } + sb.append(">"); + return sb.toString(); + } + + return baseName; + } + + /** + * Checks if actualType is a subtype of (or equal to) queryTypeName. + * Walks up the type hierarchy checking superclasses and interfaces. + */ + private boolean isSubtypeOf(ITypeBinding actualType, String queryTypeName) { + if (actualType == null) { + return false; + } + + // Check the type itself (including with generics) + if (queryTypeName.equals(getQualifiedTypeName(actualType))) { + return true; + } + + // Check without generics (erasure) + ITypeBinding erasure = actualType.getErasure(); + if (erasure != null && queryTypeName.equals(erasure.getQualifiedName())) { + return true; + } + + // Check superclass + ITypeBinding superclass = actualType.getSuperclass(); + if (superclass != null && isSubtypeOf(superclass, queryTypeName)) { + return true; + } + + // Check interfaces + ITypeBinding[] interfaces = actualType.getInterfaces(); + if (interfaces != null) { + for (ITypeBinding interfaceType : interfaces) { + if (isSubtypeOf(interfaceType, queryTypeName)) { + return true; + } + } + } + + return false; } public boolean symbolMatches() { From bcd317158ce5f6a344926c07f45140837b7bf610 Mon Sep 17 00:00:00 2001 From: tsanders Date: Fri, 14 Nov 2025 08:56:30 -0500 Subject: [PATCH 2/7] test: Add unit tests for parameter type extraction Add comprehensive unit tests for the extractParameterTypes() method covering various scenarios: - No parameters, empty parameters, single/multiple parameters - Wildcard parameters - Simple and nested generics - Primitive types and arrays - Mixed types with whitespace handling - Real-world examples and varargs All 16 tests pass successfully. Signed-off-by: tsanders --- .../internal/symbol/CustomASTVisitor.java | 2 +- .../internal/symbol/CustomASTVisitorTest.java | 154 ++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 java-analyzer-bundle.test/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitorTest.java diff --git a/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java b/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java index e40be07..0d09aaa 100644 --- a/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java +++ b/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java @@ -289,7 +289,7 @@ public boolean visit(ClassInstanceCreation node) { * e.g., "ClassName.method(Type1, Type2)" -> ["Type1", "Type2"] * Returns null if no parameters are specified in the query */ - private List extractParameterTypes(String query) { + List extractParameterTypes(String query) { int openParen = query.indexOf('('); int closeParen = query.lastIndexOf(')'); diff --git a/java-analyzer-bundle.test/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitorTest.java b/java-analyzer-bundle.test/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitorTest.java new file mode 100644 index 0000000..8bea4c6 --- /dev/null +++ b/java-analyzer-bundle.test/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitorTest.java @@ -0,0 +1,154 @@ +package io.konveyor.tackle.core.internal.symbol; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +/** + * Unit tests for CustomASTVisitor parameter extraction logic + */ +public class CustomASTVisitorTest { + + private CustomASTVisitor visitor; + + @Before + public void setUp() { + // We only need to test extractParameterTypes, which doesn't require + // a full visitor setup, but we need an instance to call the method + visitor = new CustomASTVisitor("dummy", null, CustomASTVisitor.QueryLocation.METHOD_CALL); + } + + @Test + public void testExtractParameterTypes_noParameters() { + String query = "com.example.ClassName.method"; + List result = visitor.extractParameterTypes(query); + assertNull("Should return null when no parameters specified", result); + } + + @Test + public void testExtractParameterTypes_emptyParameters() { + String query = "com.example.ClassName.method()"; + List result = visitor.extractParameterTypes(query); + assertEquals("Should return empty list for empty parameters", + Collections.emptyList(), result); + } + + @Test + public void testExtractParameterTypes_singleParameter() { + String query = "com.example.ClassName.method(java.lang.String)"; + List result = visitor.extractParameterTypes(query); + assertEquals("Should extract single parameter", + Arrays.asList("java.lang.String"), result); + } + + @Test + public void testExtractParameterTypes_twoParameters() { + String query = "com.example.ClassName.method(java.lang.String, java.lang.Integer)"; + List result = visitor.extractParameterTypes(query); + assertEquals("Should extract two parameters", + Arrays.asList("java.lang.String", "java.lang.Integer"), result); + } + + @Test + public void testExtractParameterTypes_threeParameters() { + String query = "java.sql.DriverManager.getConnection(java.lang.String, java.lang.String, java.lang.String)"; + List result = visitor.extractParameterTypes(query); + assertEquals("Should extract three parameters", + Arrays.asList("java.lang.String", "java.lang.String", "java.lang.String"), result); + } + + @Test + public void testExtractParameterTypes_withWildcard() { + String query = "com.example.ClassName.method(*, java.lang.String)"; + List result = visitor.extractParameterTypes(query); + assertEquals("Should extract parameters including wildcard", + Arrays.asList("*", "java.lang.String"), result); + } + + @Test + public void testExtractParameterTypes_simpleGeneric() { + String query = "com.example.ClassName.method(java.util.List)"; + List result = visitor.extractParameterTypes(query); + assertEquals("Should handle simple generic types", + Arrays.asList("java.util.List"), result); + } + + @Test + public void testExtractParameterTypes_nestedGenerics() { + String query = "com.example.ClassName.method(java.util.Map)"; + List result = visitor.extractParameterTypes(query); + assertEquals("Should handle nested generics with comma", + Arrays.asList("java.util.Map"), result); + } + + @Test + public void testExtractParameterTypes_multipleGenerics() { + String query = "com.example.ClassName.method(java.util.List, java.util.Set)"; + List result = visitor.extractParameterTypes(query); + assertEquals("Should handle multiple generic parameters", + Arrays.asList("java.util.List", "java.util.Set"), result); + } + + @Test + public void testExtractParameterTypes_deeplyNestedGenerics() { + String query = "com.example.ClassName.method(java.util.Map>)"; + List result = visitor.extractParameterTypes(query); + assertEquals("Should handle deeply nested generics", + Arrays.asList("java.util.Map>"), result); + } + + @Test + public void testExtractParameterTypes_primitiveTypes() { + String query = "com.example.ClassName.method(int, boolean, double)"; + List result = visitor.extractParameterTypes(query); + assertEquals("Should handle primitive types", + Arrays.asList("int", "boolean", "double"), result); + } + + @Test + public void testExtractParameterTypes_mixedTypes() { + String query = "com.example.ClassName.method(int, java.lang.String, java.util.List)"; + List result = visitor.extractParameterTypes(query); + assertEquals("Should handle mixed primitive, object, and generic types", + Arrays.asList("int", "java.lang.String", "java.util.List"), result); + } + + @Test + public void testExtractParameterTypes_arrayTypes() { + String query = "com.example.ClassName.method(java.lang.String[], int[])"; + List result = visitor.extractParameterTypes(query); + assertEquals("Should handle array types", + Arrays.asList("java.lang.String[]", "int[]"), result); + } + + @Test + public void testExtractParameterTypes_withWhitespace() { + String query = "com.example.ClassName.method( java.lang.String , java.lang.Integer )"; + List result = visitor.extractParameterTypes(query); + assertEquals("Should trim whitespace from parameters", + Arrays.asList("java.lang.String", "java.lang.Integer"), result); + } + + @Test + public void testExtractParameterTypes_realWorldExample() { + String query = "java.util.Properties.setProperty(java.lang.String, java.lang.String)"; + List result = visitor.extractParameterTypes(query); + assertEquals("Should handle real-world example from issue", + Arrays.asList("java.lang.String", "java.lang.String"), result); + } + + @Test + public void testExtractParameterTypes_varargs() { + String query = "com.example.ClassName.method(java.lang.String...)"; + List result = visitor.extractParameterTypes(query); + assertEquals("Should handle varargs notation", + Arrays.asList("java.lang.String..."), result); + } +} From 5fa89f98b625874e82829107941d34ec4a0f32cc Mon Sep 17 00:00:00 2001 From: tsanders Date: Fri, 14 Nov 2025 08:58:53 -0500 Subject: [PATCH 3/7] docs: Add parameter matching feature documentation Add comprehensive documentation explaining the new parameter-specific matching feature for METHOD_CALL and CONSTRUCTOR_CALL patterns. Covers: - Basic usage and syntax - Advanced features (wildcards, subtype matching, generics) - Constructor calls - Backward compatibility - Edge cases and migration examples - Technical details Signed-off-by: tsanders --- docs/PARAMETER_MATCHING.md | 272 +++++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 docs/PARAMETER_MATCHING.md diff --git a/docs/PARAMETER_MATCHING.md b/docs/PARAMETER_MATCHING.md new file mode 100644 index 0000000..ff62e8e --- /dev/null +++ b/docs/PARAMETER_MATCHING.md @@ -0,0 +1,272 @@ +# Parameter-Specific Matching for METHOD_CALL and CONSTRUCTOR_CALL + +This document describes the parameter-specific matching feature for METHOD_CALL and CONSTRUCTOR_CALL patterns. + +## Overview + +Starting from this version, METHOD_CALL and CONSTRUCTOR_CALL patterns can distinguish between overloaded methods with different parameter signatures. This enables precise targeting of specific method overloads in migration rules. + +## Basic Usage + +### Syntax + +```yaml +when: + java.referenced: + location: METHOD_CALL + pattern: .(, , ...) +``` + +### Example: Exact Parameter Matching + +Target only the 3-parameter version of `DriverManager.getConnection`: + +```yaml +when: + java.referenced: + location: METHOD_CALL + pattern: java.sql.DriverManager.getConnection(java.lang.String, java.lang.String, java.lang.String) +``` + +**Matches:** +```java +DriverManager.getConnection("jdbc:mysql://localhost/db", "user", "password"); +``` + +**Does NOT match:** +```java +DriverManager.getConnection("jdbc:mysql://localhost/db"); // 1 param +DriverManager.getConnection("jdbc:mysql://localhost/db", props); // 2 params (String, Properties) +``` + +## Advanced Features + +### 1. Wildcard Matching + +Use `*` to match any parameter type at a specific position: + +```yaml +when: + java.referenced: + location: METHOD_CALL + pattern: com.example.MyClass.processData(*, java.lang.String) +``` + +**Matches:** +```java +processData(123, "output"); // (int, String) +processData(new Object(), "output"); // (Object, String) +``` + +**Does NOT match:** +```java +processData("input", 123); // Wrong order: (String, int) +``` + +### 2. Subtype Matching + +Patterns automatically support subtype matching based on Java type hierarchies: + +```yaml +when: + java.referenced: + location: METHOD_CALL + pattern: com.example.MyClass.accept(java.lang.Object) +``` + +**Matches:** +```java +accept("string"); // String extends Object +accept(123); // Integer extends Object +accept(new ArrayList()); // ArrayList extends Object +``` + +### 3. Generic Type Matching + +Generic type parameters are matched exactly: + +```yaml +when: + java.referenced: + location: METHOD_CALL + pattern: com.example.MyClass.process(java.util.List) +``` + +**Matches:** +```java +process(new ArrayList()); +process(Arrays.asList("a", "b", "c")); +``` + +**Does NOT match:** +```java +process(new ArrayList()); // Different generic type +process(new ArrayList()); // Raw type +``` + +### 4. Nested Generics + +Complex nested generic types are supported: + +```yaml +when: + java.referenced: + location: METHOD_CALL + pattern: com.example.MyClass.transform(java.util.Map>) +``` + +**Matches:** +```java +transform(new HashMap>()); +``` + +### 5. Array Types + +Array parameters can be specified: + +```yaml +when: + java.referenced: + location: METHOD_CALL + pattern: com.example.MyClass.process(java.lang.String[], int[]) +``` + +**Matches:** +```java +process(new String[]{"a", "b"}, new int[]{1, 2, 3}); +``` + +### 6. Primitive Types + +Primitive types are supported: + +```yaml +when: + java.referenced: + location: METHOD_CALL + pattern: com.example.MyClass.calculate(int, double, boolean) +``` + +**Matches:** +```java +calculate(10, 3.14, true); +``` + +## Constructor Calls + +The same parameter matching works for CONSTRUCTOR_CALL patterns: + +```yaml +when: + java.referenced: + location: CONSTRUCTOR_CALL + pattern: java.io.FileOutputStream(java.lang.String, boolean) +``` + +**Matches:** +```java +new FileOutputStream("/path/to/file", true); +``` + +**Does NOT match:** +```java +new FileOutputStream("/path/to/file"); // Different overload +``` + +## Backward Compatibility + +Patterns **without** parameter types continue to work as before, matching all overloads: + +```yaml +when: + java.referenced: + location: METHOD_CALL + pattern: java.sql.DriverManager.getConnection +``` + +**Matches all overloads:** +```java +DriverManager.getConnection("url"); +DriverManager.getConnection("url", props); +DriverManager.getConnection("url", "user", "password"); +``` + +## Edge Cases + +### Empty Parameter List + +To match only methods with no parameters, use empty parentheses: + +```yaml +pattern: com.example.MyClass.getInstance() +``` + +### Varargs + +Varargs can be specified with the `...` notation: + +```yaml +pattern: com.example.MyClass.format(java.lang.String, java.lang.Object...) +``` + +## Migration Examples + +### Example 1: Deprecated Overload + +Flag only the deprecated 2-param version of `setProperty`: + +```yaml +- ruleID: deprecated-setproperty-001 + when: + java.referenced: + location: METHOD_CALL + pattern: com.legacy.Config.setProperty(java.lang.String, java.lang.String) + message: "Use setProperty(String, String, boolean) instead" +``` + +### Example 2: Security Issue + +Detect insecure URL constructor usage: + +```yaml +- ruleID: security-url-constructor-001 + when: + java.referenced: + location: CONSTRUCTOR_CALL + pattern: java.net.URL(java.lang.String) + message: "Avoid using URL(String) constructor due to DNS lookup issues" +``` + +### Example 3: API Migration + +Match specific generic signatures during API migration: + +```yaml +- ruleID: api-migration-001 + when: + java.referenced: + location: METHOD_CALL + pattern: com.oldapi.Service.getData(java.util.List) + message: "Migrate to newapi.Service.fetchData(Collection)" +``` + +## Technical Details + +### Type Resolution + +- Parameter types are resolved using Eclipse JDT bindings +- Fully qualified names are required (e.g., `java.lang.String`, not `String`) +- Generic type arguments are preserved and matched exactly + +### Subtype Matching Algorithm + +The type matching algorithm: +1. Checks for exact type match (including generics) +2. Checks erasure type match (generics erased) +3. Recursively checks superclasses +4. Recursively checks interfaces + +### Wildcards + +- Wildcard `*` matches any single parameter type +- Wildcards do not match against parameter count (use appropriate number of parameters) From 80316399a20c371108c9a46c14b38a979e2e2090 Mon Sep 17 00:00:00 2001 From: tsanders Date: Fri, 14 Nov 2025 09:05:54 -0500 Subject: [PATCH 4/7] perf: Add performance optimizations to parameter matching Implement multiple performance optimizations to minimize overhead: 1. Early returns in matchesParameterTypes(): - Zero overhead for backward compatibility (no parameters specified) - Quick length check before iteration - Special case for zero parameters - Early exit on first parameter mismatch 2. Primitive type optimization in typeMatches(): - Primitives can only match exactly, skip expensive hierarchy traversal - Check exact match before erasure check - Check erasure before full hierarchy traversal 3. Type hierarchy traversal optimizations in isSubtypeOf(): - Added cycle detection with visited set to prevent infinite loops - Cache actualTypeName to avoid recomputation - Quick erasure name checks before recursing into superclass/interfaces - Reduces repeated getQualifiedTypeName() calls 4. Parameter extraction optimizations in extractParameterTypes(): - Early return on no opening parenthesis - Pre-size ArrayList to common case (4 params) to avoid resizing - Cache string length to avoid repeated method calls Performance characteristics: - 0% overhead for queries without parameters (backward compatibility) - Minimal overhead for exact matches (most common case) - Bounded complexity for type hierarchy traversal with cycle detection - Reduced allocations and method calls throughout All 16 unit tests continue to pass. Signed-off-by: tsanders --- .../internal/symbol/CustomASTVisitor.java | 114 ++++++++++++++---- 1 file changed, 88 insertions(+), 26 deletions(-) diff --git a/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java b/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java index 0d09aaa..6411108 100644 --- a/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java +++ b/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java @@ -4,7 +4,9 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.ASTVisitor; @@ -290,23 +292,31 @@ public boolean visit(ClassInstanceCreation node) { * Returns null if no parameters are specified in the query */ List extractParameterTypes(String query) { + // Performance: Quick check before indexOf int openParen = query.indexOf('('); - int closeParen = query.lastIndexOf(')'); - - if (openParen == -1 || closeParen == -1 || openParen >= closeParen) { + if (openParen == -1) { return null; // No parameters specified in query } + int closeParen = query.lastIndexOf(')'); + if (closeParen == -1 || openParen >= closeParen) { + return null; // Invalid or no parameters + } + String paramsString = query.substring(openParen + 1, closeParen).trim(); if (paramsString.isEmpty()) { return Collections.emptyList(); // Empty parameter list: method() } - List params = new ArrayList<>(); + // Performance: Pre-size ArrayList for common cases (most methods have 1-3 params) + List params = new ArrayList<>(4); + // Split by comma, handling nested generics like Map int depth = 0; int start = 0; - for (int i = 0; i < paramsString.length(); i++) { + int length = paramsString.length(); + + for (int i = 0; i < length; i++) { char c = paramsString.charAt(i); if (c == '<') { depth++; @@ -328,17 +338,26 @@ List extractParameterTypes(String query) { * Supports wildcards (*) and subtype matching. */ private boolean matchesParameterTypes(ITypeBinding[] actualTypes) { + // Performance: Early return if no parameter filter (most common case for backward compat) if (queryParameterTypes == null) { return true; // No parameter filter specified in query } - if (queryParameterTypes.size() != actualTypes.length) { + // Performance: Quick length check before iterating + int paramCount = queryParameterTypes.size(); + if (paramCount != actualTypes.length) { return false; // Different number of parameters } - for (int i = 0; i < queryParameterTypes.size(); i++) { + // Performance: Avoid loop overhead for common cases + if (paramCount == 0) { + return true; // Both have no parameters + } + + // Check each parameter type + for (int i = 0; i < paramCount; i++) { if (!typeMatches(queryParameterTypes.get(i), actualTypes[i])) { - return false; + return false; // Early exit on first mismatch } } @@ -362,18 +381,31 @@ private boolean typeMatches(String queryType, ITypeBinding actualType) { return true; } - // Get the qualified name of the actual type + // Performance optimization: primitives can only match exactly, not via subtype + if (actualType.isPrimitive()) { + return queryType.equals(actualType.getName()); + } + + // Get the qualified name of the actual type (cache to avoid recomputation) String actualTypeName = getQualifiedTypeName(actualType); - // Exact match + // Exact match - most common case, check first if (queryType.equals(actualTypeName)) { return true; } + // Quick check: try erasure type before expensive hierarchy traversal + ITypeBinding erasure = actualType.getErasure(); + if (erasure != null && erasure != actualType) { + String erasureName = erasure.getQualifiedName(); + if (queryType.equals(erasureName)) { + return true; + } + } + // Subtype matching: check if actualType is assignable to queryType - // This requires resolving the queryType to a binding, which is complex - // For now, we'll do a simple check by walking up the type hierarchy - return isSubtypeOf(actualType, queryType); + // Only do this expensive check if exact match failed + return isSubtypeOf(actualType, queryType, actualTypeName, new HashSet<>()); } /** @@ -420,34 +452,64 @@ private String getQualifiedTypeName(ITypeBinding type) { /** * Checks if actualType is a subtype of (or equal to) queryTypeName. * Walks up the type hierarchy checking superclasses and interfaces. + * + * @param actualType the type to check + * @param queryTypeName the query type name to match against + * @param actualTypeName cached qualified name of actualType (performance optimization) + * @param visited set of already visited types to prevent infinite loops */ - private boolean isSubtypeOf(ITypeBinding actualType, String queryTypeName) { + private boolean isSubtypeOf(ITypeBinding actualType, String queryTypeName, + String actualTypeName, Set visited) { if (actualType == null) { return false; } - // Check the type itself (including with generics) - if (queryTypeName.equals(getQualifiedTypeName(actualType))) { - return true; + // Performance: Check if we already examined this type (cycle detection) + String typeKey = actualType.getKey(); + if (typeKey != null && !visited.add(typeKey)) { + return false; // Already visited, avoid infinite loop } - // Check without generics (erasure) - ITypeBinding erasure = actualType.getErasure(); - if (erasure != null && queryTypeName.equals(erasure.getQualifiedName())) { + // Use cached actualTypeName (already computed in typeMatches) + if (queryTypeName.equals(actualTypeName)) { return true; } - // Check superclass + // Performance: Quick check of superclass before recursing ITypeBinding superclass = actualType.getSuperclass(); - if (superclass != null && isSubtypeOf(superclass, queryTypeName)) { - return true; + if (superclass != null) { + // Quick check: does superclass name match before recursing? + ITypeBinding superErasure = superclass.getErasure(); + if (superErasure != null) { + String superName = superErasure.getQualifiedName(); + if (queryTypeName.equals(superName)) { + return true; + } + } + + // Recurse to check full hierarchy + String superTypeName = getQualifiedTypeName(superclass); + if (isSubtypeOf(superclass, queryTypeName, superTypeName, visited)) { + return true; + } } - // Check interfaces + // Performance: Check interfaces with same optimization ITypeBinding[] interfaces = actualType.getInterfaces(); - if (interfaces != null) { + if (interfaces != null && interfaces.length > 0) { for (ITypeBinding interfaceType : interfaces) { - if (isSubtypeOf(interfaceType, queryTypeName)) { + // Quick check erasure name first + ITypeBinding intfErasure = interfaceType.getErasure(); + if (intfErasure != null) { + String intfName = intfErasure.getQualifiedName(); + if (queryTypeName.equals(intfName)) { + return true; + } + } + + // Recurse to check full hierarchy + String intfTypeName = getQualifiedTypeName(interfaceType); + if (isSubtypeOf(interfaceType, queryTypeName, intfTypeName, visited)) { return true; } } From dda95e801a886ff6c4417cd5d149fdcc78d40002 Mon Sep 17 00:00:00 2001 From: tsanders Date: Fri, 14 Nov 2025 09:21:33 -0500 Subject: [PATCH 5/7] fix: Normalize varargs notation to array notation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix issue identified by coderabbitai where varargs notation (Type...) in query patterns would not match JDT's array representation (Type[]). JDT's ITypeBinding.getQualifiedTypeName() returns array types as Type[] but users may write varargs as Type... in patterns. Now we normalize Type... → Type[] before matching to support both notations. Uses endsWith() check for precise normalization of only trailing varargs, avoiding potential issues with ... appearing elsewhere in type names. This ensures the documented varargs example works correctly: pattern: method(java.lang.Object...) matches: method with Object[] (varargs) parameter All 16 unit tests continue to pass. Signed-off-by: tsanders --- .../core/internal/symbol/CustomASTVisitor.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java b/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java index 6411108..bef6960 100644 --- a/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java +++ b/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java @@ -381,16 +381,23 @@ private boolean typeMatches(String queryType, ITypeBinding actualType) { return true; } + // Normalize varargs notation in the query ("Type..." -> "Type[]") + // so it matches JDT's representation of varargs parameters as arrays. + String normalizedQueryType = queryType; + if (queryType.endsWith("...")) { + normalizedQueryType = queryType.substring(0, queryType.length() - 3) + "[]"; + } + // Performance optimization: primitives can only match exactly, not via subtype if (actualType.isPrimitive()) { - return queryType.equals(actualType.getName()); + return normalizedQueryType.equals(actualType.getName()); } // Get the qualified name of the actual type (cache to avoid recomputation) String actualTypeName = getQualifiedTypeName(actualType); // Exact match - most common case, check first - if (queryType.equals(actualTypeName)) { + if (normalizedQueryType.equals(actualTypeName)) { return true; } @@ -398,14 +405,14 @@ private boolean typeMatches(String queryType, ITypeBinding actualType) { ITypeBinding erasure = actualType.getErasure(); if (erasure != null && erasure != actualType) { String erasureName = erasure.getQualifiedName(); - if (queryType.equals(erasureName)) { + if (normalizedQueryType.equals(erasureName)) { return true; } } // Subtype matching: check if actualType is assignable to queryType // Only do this expensive check if exact match failed - return isSubtypeOf(actualType, queryType, actualTypeName, new HashSet<>()); + return isSubtypeOf(actualType, normalizedQueryType, actualTypeName, new HashSet<>()); } /** From 6e8f53843cce9f9561dbf3539591fd58db4470df Mon Sep 17 00:00:00 2001 From: tsanders Date: Fri, 14 Nov 2025 17:02:09 -0500 Subject: [PATCH 6/7] fix: Handle null actualTypes in matchesParameterTypes When binding.getParameterTypes() returns null (which can happen with certain method bindings), we were getting a NullPointerException when trying to access actualTypes.length. This fix adds a null check before accessing the array length. When actualTypes is null, we match only if the query expects no parameters (i.e., queryParameterTypes is empty). This fixes the CI failure where the java-provider was crashing during analysis due to this NPE. Signed-off-by: tsanders --- .../tackle/core/internal/symbol/CustomASTVisitor.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java b/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java index bef6960..60b31ad 100644 --- a/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java +++ b/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java @@ -343,6 +343,11 @@ private boolean matchesParameterTypes(ITypeBinding[] actualTypes) { return true; // No parameter filter specified in query } + // Handle null actualTypes (can happen with certain bindings) + if (actualTypes == null) { + return queryParameterTypes.isEmpty(); // Match only if query expects no parameters + } + // Performance: Quick length check before iterating int paramCount = queryParameterTypes.size(); if (paramCount != actualTypes.length) { From eaa6838a6c1219bf93f3af5d433725dcd51fbc8f Mon Sep 17 00:00:00 2001 From: tsanders Date: Fri, 14 Nov 2025 19:34:00 -0500 Subject: [PATCH 7/7] fix: Distinguish method parameters from regex alternation groups The extractParameterTypes method was incorrectly treating regex alternation groups like (FileWriter|FileReader) as method parameters. This caused patterns like: java.io.(FileWriter|FileReader|PrintStream|File|PrintWriter|RandomAccessFile)* To be parsed as having parameters, which then filtered out valid constructor calls that didn't match the bogus parameter list. Fix: Add heuristics to detect regex alternation: - Check for pipe (|) character inside parentheses - Verify parentheses are at the end of the pattern (method params) vs in the middle (regex groups) This fixes the missing violations in local-storage-00001 and other rules that use regex alternation in CONSTRUCTOR_CALL patterns. Signed-off-by: tsanders --- .../internal/symbol/CustomASTVisitor.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java b/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java index 60b31ad..9e0ee2b 100644 --- a/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java +++ b/java-analyzer-bundle.core/src/main/java/io/konveyor/tackle/core/internal/symbol/CustomASTVisitor.java @@ -290,6 +290,9 @@ public boolean visit(ClassInstanceCreation node) { * Extracts parameter types from a query pattern. * e.g., "ClassName.method(Type1, Type2)" -> ["Type1", "Type2"] * Returns null if no parameters are specified in the query + * + * Note: Must distinguish between method parameters like "method(String, int)" + * and regex alternation groups like "java.io.(FileWriter|FileReader)" */ List extractParameterTypes(String query) { // Performance: Quick check before indexOf @@ -303,7 +306,26 @@ List extractParameterTypes(String query) { return null; // Invalid or no parameters } - String paramsString = query.substring(openParen + 1, closeParen).trim(); + // Check if this looks like method parameters vs regex alternation + // Method parameters: "ClassName.methodName(Type1, Type2)" - parens at END with optional * after + // Regex alternation: "java.io.(FileWriter|FileReader)*" - parens in MIDDLE with content after + // + // Heuristic: If there's a '|' (pipe) character inside the parentheses, it's likely + // a regex alternation group, not method parameters + String potentialParams = query.substring(openParen + 1, closeParen); + if (potentialParams.contains("|")) { + return null; // Regex alternation, not method parameters + } + + // Another check: method parameters should be near the end of the pattern + // Look for content after the closing paren (besides wildcards * which are common) + String afterParen = query.substring(closeParen + 1).trim(); + if (afterParen.length() > 0 && !afterParen.matches("\\**")) { + // There's significant content after the closing paren, not method parameters + return null; + } + + String paramsString = potentialParams.trim(); if (paramsString.isEmpty()) { return Collections.emptyList(); // Empty parameter list: method() }