Repository: pantsbuild/pants

Issue:
Port analysis extraction to jdeps
As mentioned in #6239, it would help lower our dependence on zinc if we ported the [zinc analysis extraction task](https://github.com/pantsbuild/pants/blob/14f079f03bc0fa792241d80944e890c844efedce/src/python/pants/backend/jvm/tasks/analysis_extraction.py#L18-L19) away from... zinc analysis... and towards using [jdeps](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jdeps.html), which is built into all modern JVMs.


Discussion:
Tentative steps for this task:

0) Understand what the AnalysisExtraction consumers (jvm_dependency_usage / jvm_dependency_check) actually use from `product_deps_by_src`. There is some extra information in `product_deps_by_src`.
1) deprecate `classes_by_source` product, it is unconsumed.
2) find a mapping between jdeps output and the replacement for products_deps_by_source.
3) Use the PATH to look for the jdeps tool, run it instead of zinc for analysis extraction tasks, parse output into the correct product data structure for the dep usage consumers, as determined in step 0.
4) extra credit: use a native tool abstraction to look for the jvm tools. find jdeps there.

Looks great! I'm not 100% sure how that maps to commits. But it feels like this is likely to all be one PR.

Resolving patch:
diff --git a/src/python/pants/backend/jvm/tasks/analysis_extraction.py b/src/python/pants/backend/jvm/tasks/analysis_extraction.py
--- a/src/python/pants/backend/jvm/tasks/analysis_extraction.py
+++ b/src/python/pants/backend/jvm/tasks/analysis_extraction.py
@@ -1,17 +1,20 @@
 # Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
 # Licensed under the Apache License, Version 2.0 (see LICENSE).
 
+import io
 import json
 import os
-from collections import defaultdict
+import re
+import subprocess
+from contextlib import contextmanager
 
 from pants.backend.jvm.subsystems.dependency_context import DependencyContext
 from pants.backend.jvm.subsystems.zinc import Zinc
 from pants.backend.jvm.tasks.nailgun_task import NailgunTask
-from pants.base.build_environment import get_buildroot
-from pants.base.exceptions import TaskError
-from pants.base.workunit import WorkUnitLabel
-from pants.goal.products import MultipleRootedProducts
+from pants.base.deprecated import deprecated_conditional
+from pants.java.distribution.distribution import DistributionLocator
+from pants.java.executor import SubprocessExecutor
+from pants.util.contextutil import temporary_dir
 from pants.util.memo import memoized_property
 
 
@@ -33,12 +36,11 @@ def register_options(cls, register):
   @classmethod
   def prepare(cls, options, round_manager):
     super().prepare(options, round_manager)
-    round_manager.require_data('zinc_analysis')
     round_manager.require_data('runtime_classpath')
 
   @classmethod
   def product_types(cls):
-    return ['classes_by_source', 'product_deps_by_src']
+    return ['product_deps_by_target', 'classes_by_source', 'product_deps_by_src']
 
   def _create_products_if_should_run(self):
     """If this task should run, initialize empty products that it will populate.
@@ -47,106 +49,126 @@ def _create_products_if_should_run(self):
     """
 
     should_run = False
-    if self.context.products.is_required_data('classes_by_source'):
+    if self.context.products.is_required_data('product_deps_by_target'):
       should_run = True
-      make_products = lambda: defaultdict(MultipleRootedProducts)
-      self.context.products.safe_create_data('classes_by_source', make_products)
-
-    if self.context.products.is_required_data('product_deps_by_src'):
-      should_run = True
-      self.context.products.safe_create_data('product_deps_by_src', dict)
+      self.context.products.safe_create_data('product_deps_by_target', dict)
     return should_run
 
   @memoized_property
   def _zinc(self):
     return Zinc.Factory.global_instance().create(self.context.products, self.get_options().execution_strategy)
 
-  def _summary_json_file(self, vt):
-    return os.path.join(vt.results_dir, 'summary.json')
+  def _jdeps_output_json(self, vt):
+    return os.path.join(vt.results_dir, 'jdeps_output.json')
 
-  @memoized_property
-  def _analysis_by_runtime_entry(self):
-    zinc_analysis = self.context.products.get_data('zinc_analysis')
-    return {cp_entry: analysis_file for _, cp_entry, analysis_file in zinc_analysis.values()}
+  @contextmanager
+  def aliased_classpaths(self, classpaths):
+    """
+    Create unique names for each classpath entry as symlinks in
+    a temporary directory.  returns: dict[str -> classpath entry]
+    which maps string paths of symlinks to classpaths.
+
+    ClasspathEntries generally point to a .jar of
+    the .class files generated for java_library targets.
+    These jars all have the same basename, z.jar, which
+    confuses the `jdeps` tool. Jdeps expects unique, and
+    descriptive, basenames for jars. When all basenames are
+    the same the deps collide in the jdeps output, some
+    .class files can't be found and the summary output
+    is not complete.
+    """
+    with temporary_dir() as tempdir:
+      aliases = {}
+      for i, cp in enumerate(classpaths):
+        alias = os.path.join(tempdir, f"{i}.jar" if not os.path.isdir(cp) else f"{i}")
+        os.symlink(cp, alias)
+        aliases[alias] = cp
+      yield aliases
 
   def execute(self):
     # If none of our computed products are necessary, return immediately.
+    deprecated_conditional(
+      lambda: self.context.products.is_required_data('classes_by_source'),
+      '1.20.0.dev2',
+      'The `classes_by_source` product depends on internal compiler details and is no longer produced.'
+    )
+    deprecated_conditional(
+      lambda: self.context.products.is_required_data('product_deps_by_src'),
+      '1.20.0.dev2',
+      'The `product_deps_by_src` product depends on internal compiler details and is no longer produced. '
+      'For similar functionality consume `product_deps_by_target`.'
+    )
+
     if not self._create_products_if_should_run():
       return
 
-    zinc_analysis = self.context.products.get_data('zinc_analysis')
     classpath_product = self.context.products.get_data('runtime_classpath')
-    classes_by_source = self.context.products.get_data('classes_by_source')
-    product_deps_by_src = self.context.products.get_data('product_deps_by_src')
+    product_deps_by_target = self.context.products.get_data('product_deps_by_target')
 
     fingerprint_strategy = DependencyContext.global_instance().create_fingerprint_strategy(
         classpath_product)
 
-    targets = list(zinc_analysis.keys())
+    # classpath fingerprint strategy only works on targets with a classpath.
+    targets = [target for target in self.context.targets() if hasattr(target, 'strict_deps')]
     with self.invalidated(targets,
                           fingerprint_strategy=fingerprint_strategy,
                           invalidate_dependents=True) as invalidation_check:
-      # Extract and parse products for any relevant targets.
       for vt in invalidation_check.all_vts:
-        summary_json_file = self._summary_json_file(vt)
-        cp_entry, _, analysis_file = zinc_analysis[vt.target]
+        # A list of class paths to the artifacts created by the target we are computing deps for.
+        target_artifact_classpaths = [path for _, path in classpath_product.get_for_target(vt.target)]
+        potential_deps_classpaths = self._zinc.compile_classpath('runtime_classpath', vt.target)
+
+        jdeps_output_json = self._jdeps_output_json(vt)
         if not vt.valid:
-          self._extract_analysis(vt.target, analysis_file, summary_json_file)
+          self._run_jdeps_analysis(
+            vt.target,
+            target_artifact_classpaths,
+            potential_deps_classpaths,
+            jdeps_output_json
+          )
         self._register_products(vt.target,
-                                cp_entry,
-                                summary_json_file,
-                                classes_by_source,
-                                product_deps_by_src)
-
-  def _extract_analysis(self, target, analysis_file, summary_json_file):
-    target_classpath = self._zinc.compile_classpath('runtime_classpath', target)
-    analysis_by_cp_entry = self._analysis_by_runtime_entry
-    upstream_analysis = list(self._upstream_analysis(target_classpath, analysis_by_cp_entry))
+                                jdeps_output_json,
+                                product_deps_by_target)
+
+  @memoized_property
+  def _jdeps_summary_line_regex(self):
+    return re.compile(r"^.+\s->\s(.+)$")
+
+  def _run_jdeps_analysis(self, target, target_artifact_classpaths, potential_deps_classpaths, jdeps_output_json):
+    with self.aliased_classpaths(potential_deps_classpaths) as classpaths_by_alias:
+      with open(jdeps_output_json, 'w') as f:
+        if len(target_artifact_classpaths):
+          jdeps_stdout, jdeps_stderr = self._spawn_jdeps_command(
+            target, target_artifact_classpaths, classpaths_by_alias.keys()
+          ).communicate()
+          deps_classpaths = set()
+          for line in io.StringIO(jdeps_stdout.decode('utf-8')):
+            match = self._jdeps_summary_line_regex.fullmatch(line.strip()).group(1)
+            deps_classpaths.add(classpaths_by_alias.get(match, match))
+
+        else:
+          deps_classpaths = []
+        json.dump(list(deps_classpaths), f)
+
+  def _spawn_jdeps_command(self, target, target_artifact_classpaths, potential_deps_classpaths):
+    jdk = DistributionLocator.cached(jdk=True)
+    tool_classpath = jdk.find_libs(['tools.jar'])
+
     args = [
-        '-summary-json', summary_json_file,
-        '-analysis-cache', analysis_file,
-        '-classpath', ':'.join(target_classpath),
-        '-analysis-map', ','.join('{}:{}'.format(k, v) for k, v in upstream_analysis),
-      ]
-
-    result = self.runjava(classpath=self._zinc.extractor,
-                          main=Zinc.ZINC_EXTRACT_MAIN,
-                          args=args,
-                          workunit_name=Zinc.ZINC_EXTRACTOR_TOOL_NAME,
-                          workunit_labels=[WorkUnitLabel.MULTITOOL])
-    if result != 0:
-      raise TaskError('Failed to parse analysis for {}'.format(target.address.spec),
-                      exit_code=result)
-
-  def _upstream_analysis(self, target_classpath, analysis_by_cp_entry):
-    for entry in target_classpath:
-      analysis_file = analysis_by_cp_entry.get(entry)
-      if analysis_file is not None:
-        yield entry, analysis_file
+      "-summary",
+      '-classpath', ":".join(cp for cp in potential_deps_classpaths),
+    ] + target_artifact_classpaths
+
+    java_executor = SubprocessExecutor(jdk)
+    return java_executor.spawn(classpath=tool_classpath,
+                               main='com.sun.tools.jdeps.Main',
+                               jvm_options=self.get_options().jvm_options,
+                               args=args,
+                               stdout=subprocess.PIPE)
 
   def _register_products(self,
                          target,
-                         target_cp_entry,
-                         summary_json_file,
-                         classes_by_source,
-                         product_deps_by_src):
-    summary_json = self._parse_summary_json(summary_json_file)
-
-    # Register a mapping between sources and classfiles (if requested).
-    if classes_by_source is not None:
-      buildroot = get_buildroot()
-      for abs_src, classes in summary_json['products'].items():
-        source = os.path.relpath(os.path.normpath(abs_src), buildroot)
-        classes = [os.path.normpath(c) for c in classes]
-        classes_by_source[source].add_abs_paths(target_cp_entry, classes)
-
-    # Register classfile product dependencies (if requested).
-    if product_deps_by_src is not None:
-      product_deps_by_src[target] = {
-          os.path.normpath(src): [os.path.normpath(dep) for dep in deps]
-          for src, deps in summary_json['dependencies'].items()
-        }
-
-  def _parse_summary_json(self, summary_json_file):
-    with open(summary_json_file, 'r') as f:
-      return json.load(f, encoding='utf-8')
+                         jdeps_output_json,
+                         product_deps_by_target):
+    with open(jdeps_output_json) as f:
+      product_deps_by_target[target] = json.load(f)
diff --git a/src/python/pants/backend/jvm/tasks/jvm_dependency_check.py b/src/python/pants/backend/jvm/tasks/jvm_dependency_check.py
--- a/src/python/pants/backend/jvm/tasks/jvm_dependency_check.py
+++ b/src/python/pants/backend/jvm/tasks/jvm_dependency_check.py
@@ -64,9 +64,8 @@ def _skip(options):
   def prepare(cls, options, round_manager):
     super().prepare(options, round_manager)
     if not cls._skip(options):
-      round_manager.require_data('product_deps_by_src')
+      round_manager.require_data('product_deps_by_target')
       round_manager.require_data('runtime_classpath')
-      round_manager.require_data('zinc_analysis')
 
   def __init__(self, *args, **kwargs):
     super().__init__(*args, **kwargs)
@@ -102,15 +101,15 @@ def execute(self):
     fingerprint_strategy = DependencyContext.global_instance().create_fingerprint_strategy(
         classpath_product)
 
-    targets = list(self.context.products.get_data('zinc_analysis').keys())
+    targets = [target for target in self.context.targets() if hasattr(target, 'strict_deps')]
 
     with self.invalidated(targets,
                           fingerprint_strategy=fingerprint_strategy,
                           invalidate_dependents=True) as invalidation_check:
       for vt in invalidation_check.invalid_vts:
-        product_deps_by_src = self.context.products.get_data('product_deps_by_src').get(vt.target)
-        if product_deps_by_src is not None:
-          self.check(vt.target, product_deps_by_src)
+        product_deps_for_target = self.context.products.get_data('product_deps_by_target').get(vt.target)
+        if product_deps_for_target is not None:
+          self.check(vt.target, product_deps_for_target)
 
   def check(self, src_tgt, actual_deps):
     """Check for missing deps.
@@ -130,8 +129,11 @@ def shorten(path):  # Make the output easier to read.
 
       def filter_whitelisted(missing_deps):
         # Removing any targets that exist in the whitelist from the list of dependency issues.
-        return [(tgt_pair, evidence) for (tgt_pair, evidence) in missing_deps
-                            if tgt_pair[0].address not in self._target_whitelist]
+        return [
+          (tgt_pair, evidence)
+          for (tgt_pair, evidence) in missing_deps
+          if tgt_pair[0].address not in self._target_whitelist
+        ]
 
       missing_direct_tgt_deps = filter_whitelisted(missing_direct_tgt_deps)
 
@@ -139,7 +141,7 @@ def filter_whitelisted(missing_deps):
         log_fn = (self.context.log.error if self._check_missing_direct_deps == 'fatal'
                   else self.context.log.warn)
         for (tgt_pair, evidence) in missing_direct_tgt_deps:
-          evidence_str = '\n'.join(['  {} uses {}'.format(shorten(e[0]), shorten(e[1]))
+          evidence_str = '\n'.join(['  {} uses {}'.format(e[0].address.spec, shorten(e[1]))
                                     for e in evidence])
           log_fn('Missing direct BUILD dependency {} -> {} because:\n{}'
                  .format(tgt_pair[0].address.spec, tgt_pair[1].address.spec, evidence_str))
@@ -180,6 +182,7 @@ def _compute_missing_deps(self, src_tgt, actual_deps):
     All paths in the input and output are absolute.
     """
     analyzer = self._analyzer
+
     def must_be_explicit_dep(dep):
       # We don't require explicit deps on the java runtime, so we shouldn't consider that
       # a missing dep.
@@ -204,22 +207,23 @@ def target_or_java_dep_in_targets(target, targets):
     missing_direct_tgt_deps_map = defaultdict(list)  # The same, but for direct deps.
 
     targets_by_file = analyzer.targets_by_file(self.context.targets())
-    buildroot = get_buildroot()
-    abs_srcs = [os.path.join(buildroot, src) for src in src_tgt.sources_relative_to_buildroot()]
-    for src in abs_srcs:
-      for actual_dep in filter(must_be_explicit_dep, actual_deps.get(src, [])):
-        actual_dep_tgts = targets_by_file.get(actual_dep)
-        # actual_dep_tgts is usually a singleton. If it's not, we only need one of these
-        # to be in our declared deps to be OK.
-        if actual_dep_tgts is None:
-          missing_file_deps.add((src_tgt, actual_dep))
-        elif not target_or_java_dep_in_targets(src_tgt, actual_dep_tgts):
-          # Obviously intra-target deps are fine.
-          canonical_actual_dep_tgt = next(iter(actual_dep_tgts))
-          if canonical_actual_dep_tgt not in src_tgt.dependencies:
-            # The canonical dep is the only one a direct dependency makes sense on.
-            missing_direct_tgt_deps_map[(src_tgt, canonical_actual_dep_tgt)].append(
-                (src, actual_dep))
+    for actual_dep in filter(must_be_explicit_dep, actual_deps):
+      actual_dep_tgts = targets_by_file.get(actual_dep)
+      # actual_dep_tgts is usually a singleton. If it's not, we only need one of these
+      # to be in our declared deps to be OK.
+      if actual_dep_tgts is None:
+        missing_file_deps.add((src_tgt, actual_dep))
+      elif not target_or_java_dep_in_targets(src_tgt, actual_dep_tgts):
+        # Obviously intra-target deps are fine.
+        canonical_actual_dep_tgt = next(iter(actual_dep_tgts))
+        if canonical_actual_dep_tgt not in src_tgt.dependencies:
+          # The canonical dep is the only one a direct dependency makes sense on.
+          # TODO get rid of src usage here. we dont have a way to map class
+          # files back to source files when using jdeps. I think we can get away without
+          # listing the src file directly and just list the target which has the transient
+          # dep
+          missing_direct_tgt_deps_map[(src_tgt, canonical_actual_dep_tgt)].append(
+              (src_tgt, actual_dep))
 
     return (list(missing_file_deps),
             list(missing_direct_tgt_deps_map.items()))
@@ -254,6 +258,8 @@ def _compute_unnecessary_deps(self, target, actual_deps):
     """
     # Flatten the product deps of this target.
     product_deps = set()
+    # TODO update actual deps will just be a list, not a dict when switching to
+    # product_deps_by_target_product.
     for dep_entries in actual_deps.values():
       product_deps.update(dep_entries)
 
diff --git a/src/python/pants/backend/jvm/tasks/jvm_dependency_usage.py b/src/python/pants/backend/jvm/tasks/jvm_dependency_usage.py
--- a/src/python/pants/backend/jvm/tasks/jvm_dependency_usage.py
+++ b/src/python/pants/backend/jvm/tasks/jvm_dependency_usage.py
@@ -70,9 +70,8 @@ def subsystem_dependencies(cls):
   def prepare(cls, options, round_manager):
     super().prepare(options, round_manager)
     if not options.use_cached:
-      round_manager.require_data('classes_by_source')
       round_manager.require_data('runtime_classpath')
-      round_manager.require_data('product_deps_by_src')
+      round_manager.require_data('product_deps_by_target')
     else:
       # We want to have synthetic targets in build graph to deserialize nodes properly.
       round_manager.optional_data('java')
@@ -163,6 +162,7 @@ def calculating_node_creator(self, target_to_vts):
     targets = self.context.targets()
     targets_by_file = self._analyzer.targets_by_file(targets)
     transitive_deps_by_target = self._analyzer.compute_transitive_deps_by_target(targets)
+
     def creator(target):
       transitive_deps = set(transitive_deps_by_target.get(target))
       node = self.create_dep_usage_node(target, targets_by_file, transitive_deps)
@@ -247,20 +247,20 @@ def _construct_edge(dep_tgt, products_used):
 
     # Record the used products and undeclared Edges for this target. Note that some of
     # these may be self edges, which are considered later.
-    product_deps_by_src = self.context.products.get_data('product_deps_by_src')
-    target_product_deps_by_src = product_deps_by_src.get(target, {})
-    for product_deps in target_product_deps_by_src.values():
-      for product_dep in product_deps:
-        for dep_tgt in targets_by_file.get(product_dep, []):
-          derived_from = dep_tgt.concrete_derived_from
-          if not self._select(derived_from):
-            continue
-          # Create edge only for those direct or transitive dependencies in order to
-          # disqualify irrelevant targets that happen to share some file in sources,
-          # not uncommon when globs especially rglobs is used.
-          if not derived_from in transitive_deps:
-            continue
-          node.add_edge(_construct_edge(dep_tgt, products_used={product_dep}), derived_from)
+    product_deps_by_target = self.context.products.get_data('product_deps_by_target')
+    product_deps_for_target = product_deps_by_target.get(target, [])
+    # product_deps is a list of class files / jars
+    for product_dep in product_deps_for_target:
+      for dep_tgt in targets_by_file.get(product_dep, []):
+        derived_from = dep_tgt.concrete_derived_from
+        if not self._select(derived_from):
+          continue
+        # Create edge only for those direct or transitive dependencies in order to
+        # disqualify irrelevant targets that happen to share some file in sources,
+        # not uncommon when globs especially rglobs is used.
+        if derived_from not in transitive_deps:
+          continue
+        node.add_edge(_construct_edge(dep_tgt, products_used={product_dep}), derived_from)
 
     return node
 


Test patch:
diff --git a/tests/python/pants_test/backend/jvm/tasks/test_jvm_dependency_usage.py b/tests/python/pants_test/backend/jvm/tasks/test_jvm_dependency_usage.py
--- a/tests/python/pants_test/backend/jvm/tasks/test_jvm_dependency_usage.py
+++ b/tests/python/pants_test/backend/jvm/tasks/test_jvm_dependency_usage.py
@@ -30,8 +30,8 @@ def _setup(self, target_classfiles):
         touch(os.path.join(target_dir, classfile))
       classpath_products.add_for_target(target, [('default', target_dir)])
 
-    product_deps_by_src = context.products.get_data('product_deps_by_src', dict)
-    return self.create_task(context), product_deps_by_src
+    product_deps_by_target = context.products.get_data('product_deps_by_target', dict)
+    return self.create_task(context), product_deps_by_target
 
   def make_java_target(self, *args, **kwargs):
     assert 'target_type' not in kwargs
@@ -48,15 +48,14 @@ def test_simple_dep_usage_graph(self):
     t2 = self.make_java_target(spec=':t2', sources=['c.java'], dependencies=[t1])
     t3 = self.make_java_target(spec=':t3', sources=['d.java', 'e.java'], dependencies=[t1])
     self.set_options(size_estimator='filecount')
-    dep_usage, product_deps_by_src = self._setup({
+    dep_usage, product_deps_by_target = self._setup({
         t1: ['a.class', 'b.class'],
         t2: ['c.class'],
         t3: ['d.class', 'e.class'],
       })
-    product_deps_by_src[t1] = {}
-    product_deps_by_src[t2] = {'c.java': ['a.class']}
-    product_deps_by_src[t3] = {'d.java': ['a.class', 'b.class'],
-                               'e.java': ['a.class', 'b.class']}
+    product_deps_by_target[t1] = []
+    product_deps_by_target[t2] = ['a.class']
+    product_deps_by_target[t3] = ['a.class', 'b.class']
 
     graph = self.create_graph(dep_usage, [t1, t2, t3])
 
@@ -83,18 +82,17 @@ def test_dep_usage_graph_with_synthetic_targets(self):
                                sources=['a.java', 'b.java'],
                                dependencies=[t1, t1_x, t1_y, t1_z])
     self.set_options(size_estimator='nosize')
-    dep_usage, product_deps_by_src = self._setup({
+    dep_usage, product_deps_by_target = self._setup({
         t1_x: ['x1.class'],
         t1_y: ['y1.class'],
         t1_z: ['z1.class', 'z2.class', 'z3.class'],
         t2: ['a.class', 'b.class'],
       })
-    product_deps_by_src[t1] = {}
-    product_deps_by_src[t1_x] = {}
-    product_deps_by_src[t1_y] = {}
-    product_deps_by_src[t1_z] = {}
-    product_deps_by_src[t2] = {'a.java': ['x1.class'],
-                               'b.java': ['z1.class', 'z2.class']}
+    product_deps_by_target[t1] = []
+    product_deps_by_target[t1_x] = []
+    product_deps_by_target[t1_y] = []
+    product_deps_by_target[t1_z] = []
+    product_deps_by_target[t2] = ['x1.class', 'z1.class', 'z2.class']
     graph = self.create_graph(dep_usage, [t1, t1_x, t1_y, t1_z, t2])
 
     self.assertEqual(graph._nodes[t1].products_total, 5)
@@ -110,13 +108,13 @@ def test_target_alias(self):
     nested_alias_b = self.make_target(spec=':nest_alias_b', dependencies=[alias_b])
     c = self.make_java_target(spec=':c', sources=['c.java'], dependencies=[alias_a_b, nested_alias_b])
     self.set_options(strict_deps=False)
-    dep_usage, product_deps_by_src = self._setup({
+    dep_usage, product_deps_by_target = self._setup({
       a: ['a.class'],
       b: ['b.class'],
       c: ['c.class'],
     })
 
-    product_deps_by_src[c] = {'c.java': ['a.class']}
+    product_deps_by_target[c] = ['a.class']
     graph = self.create_graph(dep_usage, [a, b, c, alias_a_b, alias_b, nested_alias_b])
     # both `:a` and `:b` are resolved from target aliases, one is used the other is not.
     self.assertTrue(graph._nodes[c].dep_edges[a].is_declared)
@@ -136,14 +134,14 @@ def test_overlapping_globs(self):
     t3 = self.make_java_target(spec=':t3', sources=['c.java'], dependencies=[t1])
     t4 = self.make_java_target(spec=':t4', sources=['d.java'], dependencies=[t3])
     self.set_options(strict_deps=False)
-    dep_usage, product_deps_by_src = self._setup({
+    dep_usage, product_deps_by_target = self._setup({
         t1: ['a.class'],
         t2: ['a.class', 'b.class'],
         t3: ['c.class'],
         t4: ['d.class'],
     })
-    product_deps_by_src[t3] = {'c.java': ['a.class']}
-    product_deps_by_src[t4] = {'d.java': ['a.class']}
+    product_deps_by_target[t3] = ['a.class']
+    product_deps_by_target[t4] = ['a.class']
     graph = self.create_graph(dep_usage, [t1, t2, t3, t4])
 
     # Not creating edge for t2 even it provides a.class that t4 depends on.
@@ -172,11 +170,11 @@ def test_cache_write(self):
     t2 = self.make_java_target(spec=':t2', sources=['b.java'], dependencies=[t1])
     self.create_file('b.java')
     self.set_options(size_estimator='filecount')
-    dep_usage, product_deps_by_src = self._setup({
+    dep_usage, product_deps_by_target = self._setup({
         t1: ['a.class'],
         t2: ['b.class'],
       })
-    product_deps_by_src[t1] = {}
-    product_deps_by_src[t2] = {'b.java': ['a.class']}
+    product_deps_by_target[t1] = []
+    product_deps_by_target[t2] = ['a.class']
 
     dep_usage.create_dep_usage_graph([t1, t2])