Repository: pantsbuild/pants

Issue:
JVM: Dependency inference for 3rd-party FQTN use
Guice tests are full of cases where fully-qualified type names **inline in code, rather than in import statements** are used with 3rd-party dependencies. We'll need to infer these.


Discussion:
I think that we already extract them correctly... do you see them in the output of #13543 as extracted consumed types?

If so, you would need to make sure that the producers of those types are declared, probably by adding them to the thirdparty mapping.
At the moment we only do import analysis for 3rd-party types.
> At the moment we only do import analysis for 3rd-party types.

Not sure what you mean. Consumed types are extracted from firstparty sources, and then looked up in mappings for both firstparty and thirdparty produced types: https://github.com/pantsbuild/pants/blob/392e613e5f16d56d07a2e8cfe5cecea0cd60d428/src/python/pants/backend/java/dependency_inference/rules.py#L84-L92
`third_party_matches` is only filled based on the `third_party_imports` statement?
From `./pants_from_sources java-dump-source-analysis`: 

* We only handle _unqualified_ consumed types, or types used in variable/field declarations
* Third party types are inferred from imports


One specific case I'm seeing is in `guice`'s `LineNumbers.java`:

```
core/src/com/google/inject/internal/util/LineNumbers.java:126: error: package org.objectweb.asm does not exist
        sb.append(org.objectweb.asm.Type.getDescriptor(param));
                                   ^
```

@tdyas when you get a moment, can you confirm my understanding?
This test shows one case where we do extract a consumed type (`some.other.Thing`), and another (`some.qualified.ref.Foo.bar()`) where we don't: https://github.com/pantsbuild/pants/blob/392e613e5f16d56d07a2e8cfe5cecea0cd60d428/src/python/pants/backend/java/dependency_inference/java_parser_test.py#L127-L133 ... so yea, it seems like this is a gap in the extracted consumed types.
> @tdyas when you get a moment, can you confirm my understanding?

Seems like that is the case One is the type and the other is a method call. The parse tree for the method call is probably a tree consisting of each identifier, whereas the types seem to parse as a single string for the type name.

Resolving patch:
diff --git a/src/python/pants/backend/java/dependency_inference/rules.py b/src/python/pants/backend/java/dependency_inference/rules.py
--- a/src/python/pants/backend/java/dependency_inference/rules.py
+++ b/src/python/pants/backend/java/dependency_inference/rules.py
@@ -68,13 +68,16 @@ async def infer_java_dependencies_via_imports(
         types.update(dependency_name(imp.name, imp.is_static) for imp in analysis.imports)
     if java_infer_subsystem.consumed_types:
         package = analysis.declared_package
-        if package:
-            types.update(
-                f"{package}.{consumed_type}"
-                for consumed_type in analysis.consumed_unqualified_types
-            )
-        else:
-            types.update(analysis.consumed_unqualified_types)
+
+        # 13545: `analysis.consumed_types` may be unqualified (package-local or imported) or qualified
+        # (prefixed by package name). Heuristic for now is that if there's a `.` in the type name, it's
+        # probably fully qualified. This is probably fine for now.
+        maybe_qualify_types = (
+            f"{package}.{consumed_type}" if package and "." not in consumed_type else consumed_type
+            for consumed_type in analysis.consumed_types
+        )
+
+        types.update(maybe_qualify_types)
 
     dep_map = first_party_dep_map.package_rooted_dependency_map
 
diff --git a/src/python/pants/backend/java/dependency_inference/types.py b/src/python/pants/backend/java/dependency_inference/types.py
--- a/src/python/pants/backend/java/dependency_inference/types.py
+++ b/src/python/pants/backend/java/dependency_inference/types.py
@@ -34,7 +34,7 @@ class JavaSourceDependencyAnalysis:
     declared_package: str | None
     imports: Sequence[JavaImport]
     top_level_types: Sequence[str]
-    consumed_unqualified_types: Sequence[str]
+    consumed_types: Sequence[str]
 
     @classmethod
     def from_json_dict(cls, analysis: dict[str, Any]) -> JavaSourceDependencyAnalysis:
@@ -42,7 +42,7 @@ def from_json_dict(cls, analysis: dict[str, Any]) -> JavaSourceDependencyAnalysi
             declared_package=analysis.get("declaredPackage"),
             imports=tuple(JavaImport.from_json_dict(imp) for imp in analysis["imports"]),
             top_level_types=tuple(analysis["topLevelTypes"]),
-            consumed_unqualified_types=tuple(analysis["consumedUnqualifiedTypes"]),
+            consumed_types=tuple(analysis["consumedTypes"]),
         )
 
     def to_debug_json_dict(self) -> dict[str, Any]:
@@ -50,5 +50,5 @@ def to_debug_json_dict(self) -> dict[str, Any]:
             "declared_package": self.declared_package,
             "imports": [imp.to_debug_json_dict() for imp in self.imports],
             "top_level_types": self.top_level_types,
-            "consumed_unqualified_types": self.consumed_unqualified_types,
+            "consumed_types": self.consumed_types,
         }


Test patch:
diff --git a/src/python/pants/backend/java/dependency_inference/java_parser_test.py b/src/python/pants/backend/java/dependency_inference/java_parser_test.py
--- a/src/python/pants/backend/java/dependency_inference/java_parser_test.py
+++ b/src/python/pants/backend/java/dependency_inference/java_parser_test.py
@@ -124,7 +124,7 @@ class Foo {}
         "org.pantsbuild.example.SimpleSource",
         "org.pantsbuild.example.Foo",
     )
-    assert sorted(analysis.consumed_unqualified_types) == [
+    assert sorted(analysis.consumed_types) == [
         "Date",
         "System",
         "date",  # note: false positive on a variable identifier
@@ -223,11 +223,11 @@ class Foo {}
     assert analysis.declared_package is None
     assert analysis.imports == ()
     assert analysis.top_level_types == ("SimpleSource", "Foo")
-    assert analysis.consumed_unqualified_types == ("System",)
+    assert analysis.consumed_types == ("System",)
 
 
 @maybe_skip_jdk_test
-def test_java_parser_consumed_unqualified_types(rule_runner: RuleRunner) -> None:
+def test_java_parser_consumed_types(rule_runner: RuleRunner) -> None:
     rule_runner.write_files(
         {
             "BUILD": dedent(
@@ -283,7 +283,7 @@ def test_java_parser_consumed_unqualified_types(rule_runner: RuleRunner) -> None
     assert analysis.declared_package == "org.pantsbuild.test"
     assert analysis.imports == ()
     assert analysis.top_level_types == ("org.pantsbuild.test.AnImpl",)
-    assert sorted(analysis.consumed_unqualified_types) == [
+    assert sorted(analysis.consumed_types) == [
         "AThrownException",
         "ClassAnnotation",
         "FieldAnnotation",
diff --git a/src/python/pants/backend/java/dependency_inference/rules_test.py b/src/python/pants/backend/java/dependency_inference/rules_test.py
--- a/src/python/pants/backend/java/dependency_inference/rules_test.py
+++ b/src/python/pants/backend/java/dependency_inference/rules_test.py
@@ -631,6 +631,49 @@ def test_third_party_dep_inference(rule_runner: RuleRunner) -> None:
     )
 
 
+@maybe_skip_jdk_test
+def test_third_party_dep_inference_fqtn(rule_runner: RuleRunner) -> None:
+    rule_runner.set_options(
+        ["--java-infer-third-party-import-mapping={'org.joda.time.**': 'joda-time:joda-time'}"],
+        env_inherit=PYTHON_BOOTSTRAP_ENV,
+    )
+    rule_runner.write_files(
+        {
+            "BUILD": dedent(
+                """\
+                jvm_artifact(
+                    name = "joda-time_joda-time",
+                    group = "joda-time",
+                    artifact = "joda-time",
+                    version = "2.10.10",
+                )
+
+                java_sources(name = 'lib')
+                """
+            ),
+            "PrintDate.java": dedent(
+                """\
+                package org.pantsbuild.example;
+
+                public class PrintDate {
+                    public static void main(String[] args) {
+                        org.joda.time.DateTime dt = new DateTime();
+                        System.out.println(dt.toString());
+                    }
+                }
+                """
+            ),
+        }
+    )
+
+    lib = rule_runner.get_target(
+        Address("", target_name="lib", relative_file_path="PrintDate.java")
+    )
+    assert rule_runner.request(Addresses, [DependenciesRequest(lib[Dependencies])]) == Addresses(
+        [Address("", target_name="joda-time_joda-time")]
+    )
+
+
 @maybe_skip_jdk_test
 def test_third_party_dep_inference_nonrecursive(rule_runner: RuleRunner) -> None:
     rule_runner.set_options(