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) 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 1828e94..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 @@ -2,6 +2,12 @@ import static org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin.logInfo; +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; import org.eclipse.jdt.core.dom.ClassInstanceCreation; @@ -29,6 +35,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 @@ -42,11 +49,11 @@ public enum QueryLocation { public CustomASTVisitor(String query, SearchMatch match, QueryLocation location) { /* - * Strip parameter types from the query pattern before matching - * The AST provides method names without parameter types, but users - * may specify patterns like "ClassName.method(ParamType1, ParamType2)" - * We need to remove the parameter types to match correctly + * 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("\\([^)]*\\)", ""); /* @@ -157,8 +164,15 @@ public boolean visit(MethodInvocation node) { String fullyQualifiedName = declaringClass.getQualifiedName() + "." + binding.getName(); // match fqn with query pattern if (fullyQualifiedName.matches(this.query)) { - this.symbolMatches = true; - return false; + // Check parameter types if specified in the query + ITypeBinding[] parameterTypes = binding.getParameterTypes(); + if (matchesParameterTypes(parameterTypes)) { + this.symbolMatches = true; + return false; + } else { + logInfo("method parameters did not match query parameters"); + return true; + } } else { logInfo("method fqn " + fullyQualifiedName + " did not match with " + query); return true; @@ -197,8 +211,15 @@ public boolean visit(ConstructorInvocation node) { String fullyQualifiedName = declaringClass.getQualifiedName(); // match fqn with query pattern if (fullyQualifiedName.matches(this.query)) { - this.symbolMatches = true; - return false; + // Check parameter types if specified in the query + ITypeBinding[] parameterTypes = binding.getParameterTypes(); + if (matchesParameterTypes(parameterTypes)) { + this.symbolMatches = true; + return false; + } else { + logInfo("constructor parameters did not match query parameters"); + return true; + } } else { logInfo("constructor fqn " + fullyQualifiedName + " did not match with " + query); return true; @@ -215,7 +236,7 @@ public boolean visit(ConstructorInvocation node) { // this is so that we fallback and don't lose a match when we fail this.symbolMatches = true; return false; - } + } } /* @@ -237,8 +258,15 @@ public boolean visit(ClassInstanceCreation node) { String fullyQualifiedName = declaringClass.getQualifiedName(); // match fqn with query pattern if (fullyQualifiedName.matches(this.query)) { - this.symbolMatches = true; - return false; + // Check parameter types if specified in the query + ITypeBinding[] parameterTypes = binding.getParameterTypes(); + if (matchesParameterTypes(parameterTypes)) { + this.symbolMatches = true; + return false; + } else { + logInfo("constructor parameters did not match query parameters"); + return true; + } } else { logInfo("constructor fqn " + fullyQualifiedName + " did not match with " + query); return true; @@ -255,7 +283,273 @@ public boolean visit(ClassInstanceCreation node) { // this is so that we fallback and don't lose a match when we fail this.symbolMatches = true; return false; - } + } + } + + /** + * 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 + int openParen = query.indexOf('('); + 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 + } + + // 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() + } + + // 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; + int length = paramsString.length(); + + for (int i = 0; i < 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) { + // Performance: Early return if no parameter filter (most common case for backward compat) + if (queryParameterTypes == null) { + 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) { + return false; // Different number of parameters + } + + // 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; // Early exit on first mismatch + } + } + + 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; + } + + // 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 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 (normalizedQueryType.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 (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, normalizedQueryType, actualTypeName, new HashSet<>()); + } + + /** + * 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. + * + * @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, + String actualTypeName, Set visited) { + if (actualType == null) { + return false; + } + + // 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 + } + + // Use cached actualTypeName (already computed in typeMatches) + if (queryTypeName.equals(actualTypeName)) { + return true; + } + + // Performance: Quick check of superclass before recursing + ITypeBinding superclass = actualType.getSuperclass(); + 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; + } + } + + // Performance: Check interfaces with same optimization + ITypeBinding[] interfaces = actualType.getInterfaces(); + if (interfaces != null && interfaces.length > 0) { + for (ITypeBinding interfaceType : interfaces) { + // 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; + } + } + } + + return false; } public boolean symbolMatches() { 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); + } +}