Repository: joernio/joern (scala)

Issue:
[Bug]The TypeInferencePass fails to match method calls with null or unresolved parameters to their corresponding method declarations.
### Bug Description
The `TypeInferencePass`  fails to match method calls with `null`  or unresolved parameters to their corresponding method declarations. Specifically:

When a method call has `null`  arguments, they are assigned the type `"null"`  (instead of `"ANY"` )
The type matching logic in `doArgumentTypesMatch()` treats `"null"` as a distinct type that doesn't match any declared parameter type (e.g., `java.lang.String` )
This causes the `TypeInferencePass` to fail to resolve <unresolvedSignature> calls even when a matching method exists

Root Cause: The type compatibility check in TypeInferencePass is too strict. It doesn't account for:

"null" type being compatible with any reference type parameter
The special semantics of null in Java (which can be assigned to any reference type)
### To Reproduce
Method call with null parameter
```Java
// Source code
HttpUtils httpUtils = new HttpUtils();
String result = httpUtils.postRaw(url, body.toJSONString(), "application/json", null);
```
```Scala
// Method declaration in the CPG
method.name = "postRaw"
method.fullName = "com.example.HttpUtils.postRaw:java.lang.String(java.lang.String,java.lang.String,java.lang.String,java.lang.String)"
method.parameter = [
  Parameter(name="url", typeFullName="java.lang.String"),
  Parameter(name="rawBodyStr", typeFullName="java.lang.String"),
  Parameter(name="contentType", typeFullName="java.lang.String"),
  Parameter(name="traceId", typeFullName="java.lang.String")
]
```
Expected CPG result:
The call should be matched to the method
`call.methodFullName` should be updated to the actual method's full name
`call.signature` should be updated to `java.lang.String(java.lang.String,java.lang.String,java.lang.String,java.lang.String)`

Actual CPG result:
The call remains unmatched
`call.methodFullName` stays as `<unresolvedNamespace>.postRaw:<unresolvedSignature>(4)`
`call.signature` remains `<unresolvedSignature>(4)`
I add some log and output like this:
```
Checking method: com.example.HttpUtils.postRaw:java.lang.String(java.lang.String,java.lang.String,java.lang.String,java.lang.String)
  parameterSizesMatch: true (4 == 4)
  argTypesMatch: false
    [0] Param: url (java.lang.String) vs Arg: url (java.lang.String) -> Match: true
    [1] Param: rawBodyStr (java.lang.String) vs Arg: body.toJSONString() (ANY) -> Match: true
    [2] Param: contentType (java.lang.String) vs Arg: "application/json" (java.lang.String) -> Match: true
    [3] Param: traceId (java.lang.String) vs Arg: null (null) -> Match: false
  hasDifferingArg: true
  RESULT: false
```
### Desktop:

OS: macOS
Joern Version: latest
Java version: 21

Pull request:
Fixes https://github.com/joernio/joern/issues/5676

Just setting the type of `null` to any isn't quite correct, since this would incorrectly match parameters with primitive types. We'll probably need to handle this in the back-end as well.

Patch:
diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/passes/TypeInferencePass.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/passes/TypeInferencePass.scala
index bfe620e9dc52..8f3e98743388 100644
--- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/passes/TypeInferencePass.scala
+++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/passes/TypeInferencePass.scala
@@ -12,7 +12,7 @@ import org.slf4j.LoggerFactory
 import scala.jdk.OptionConverters.RichOptional
 import io.joern.x2cpg.Defines.UnresolvedNamespace
 import io.shiftleft.codepropertygraph.generated.PropertyNames
-import io.joern.javasrc2cpg.typesolvers.TypeInfoCalculator.TypeConstants
+import io.joern.javasrc2cpg.typesolvers.TypeInfoCalculator.{PrimitiveTypes, TypeConstants}
 
 class TypeInferencePass(cpg: Cpg) extends ForkJoinParallelCpgPass[Call](cpg) {
 
@@ -55,7 +55,9 @@ class TypeInferencePass(cpg: Cpg) extends ForkJoinParallelCpgPass[Call](cpg) {
 
     val hasDifferingArg = method.parameter.zip(callArgs).exists { case (parameter, argument) =>
       val maybeArgumentType = argument.propertyOption(Properties.TypeFullName).getOrElse(TypeConstants.Any)
-      val argMatches        = maybeArgumentType == TypeConstants.Any || maybeArgumentType == parameter.typeFullName
+      val argMatches =
+        maybeArgumentType == TypeConstants.Any || maybeArgumentType == parameter.typeFullName || (maybeArgumentType == TypeConstants.Null && !PrimitiveTypes
+          .contains(parameter.typeFullName))
 
       !argMatches
     }
diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/typesolvers/TypeInfoCalculator.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/typesolvers/TypeInfoCalculator.scala
index a37410c6384a..0ee470596cba 100644
--- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/typesolvers/TypeInfoCalculator.scala
+++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/typesolvers/TypeInfoCalculator.scala
@@ -264,6 +264,7 @@ object TypeInfoCalculator {
     val Record: String   = "java.lang.Record"
     val Void: String     = "void"
     val Any: String      = "ANY"
+    val Null: String     = "null"
   }
 
   object TypeNameConstants {
@@ -288,15 +289,9 @@ object TypeInfoCalculator {
     "registerNatives()"
   )
 
-  val NumericTypes: Set[String] = Set(
-    "byte",
-    "short",
-    "int",
-    "long",
-    "float",
-    "double",
-    "char",
-    "boolean",
+  val PrimitiveTypes: Set[String] = Set("byte", "short", "int", "long", "float", "double", "char", "boolean")
+
+  val NumericTypes: Set[String] = PrimitiveTypes ++ Set(
     "java.lang.Byte",
     "java.lang.Short",
     "java.lang.Integer",


Test patch:
diff --git a/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/TypeInferenceTests.scala b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/TypeInferenceTests.scala
index ce3a116dd730..1515edf849aa 100644
--- a/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/TypeInferenceTests.scala
+++ b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/TypeInferenceTests.scala
@@ -44,6 +44,41 @@ class NewTypeInferenceTests extends JavaSrcCode2CpgFixture {
       )
     }
 
+    "a call with an unresolved argument with a null type" when {
+      "there is only one matching method should be resolved" in {
+        val cpg = code(
+          """
+            |package foo;
+            |
+            |import a.b.Bar;
+            |
+            |public class Foo {
+            |  public static Bar test(String s) {
+            |    return new Bar(s);
+            |  }
+            |
+            |  public void sink(Bar b) {}
+            |}
+            |""".stripMargin,
+          fileName = "Foo.java"
+        ).moreCode("""
+            |import foo.Foo;
+            |
+            |import baz.Baz;
+            |
+            |class Test {
+            |  public static Bar stringCall() {
+            |    sink(Foo.test(null));
+            |  }
+            |}
+            |""".stripMargin)
+
+        cpg.method.name("stringCall").call.name("test").methodFullName.l shouldBe List(
+          "foo.Foo.test:a.b.Bar(java.lang.String)"
+        )
+      }
+    }
+
     "there are multiple matching methods should not be resolved" in {
       val cpg = code(
         """