1 /* 2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.dependencymapper; 17 18 import com.android.dependencymapper.DependencyProto; 19 20 import java.util.ArrayList; 21 import java.util.HashMap; 22 import java.util.HashSet; 23 import java.util.List; 24 import java.util.Map; 25 import java.util.Set; 26 27 /** 28 * This class binds {@link List<ClassDependencyData>} and {@link List<JavaSourceData>} together as a 29 * flat map, which represents dependency related attributes of a java file. 30 */ 31 public class DependencyMapper { 32 private final List<ClassDependencyData> mClassAnalysisList; 33 private final List<JavaSourceData> mJavaSourceDataList; 34 private final Map<String, String> mClassToSourceMap = new HashMap<>(); 35 private final Map<String, Set<String>> mFileDependencies = new HashMap<>(); 36 private final Set<String> mDependencyToAll = new HashSet<>(); 37 private final Map<String, Set<String>> mSourceToClasses = new HashMap<>(); 38 DependencyMapper(List<ClassDependencyData> classAnalysisList, List<JavaSourceData> javaSourceDataList)39 public DependencyMapper(List<ClassDependencyData> classAnalysisList, List<JavaSourceData> javaSourceDataList) { 40 this.mClassAnalysisList = classAnalysisList; 41 this.mJavaSourceDataList = javaSourceDataList; 42 } 43 buildDependencyMaps()44 public DependencyProto.FileDependencyList buildDependencyMaps() { 45 buildClassDependencyMaps(); 46 buildSourceToClassMap(); 47 return createFileDependencies(); 48 } 49 buildClassDependencyMaps()50 private void buildClassDependencyMaps() { 51 // Create a map between package appended file names and file paths. 52 Map<String, String> sourcePaths = generateSourcePaths(); 53 // A map between qualified className and its dependencies 54 Map<String, Set<String>> classDependencies = new HashMap<>(); 55 // A map between constant values and the their declarations. 56 Map<Object, Set<String>> constantRegistry = new HashMap<>(); 57 // A map between constant values and the their inlined usages. 58 Map<Object, Set<String>> inlinedUsages = new HashMap<>(); 59 60 for (ClassDependencyData analysis : mClassAnalysisList) { 61 String className = analysis.getQualifiedName(); 62 63 // Compute qualified class name to source path map. 64 String sourceKey = analysis.getPackagePrependedClassSource(); 65 String sourcePath = sourcePaths.get(sourceKey); 66 mClassToSourceMap.put(className, sourcePath); 67 68 // compute classDependencies 69 classDependencies.computeIfAbsent(className, k -> 70 new HashSet<>()).addAll(analysis.getClassDependencies()); 71 72 // Compute constantRegistry 73 analysis.getConstantsDefined().forEach(c -> 74 constantRegistry.computeIfAbsent(c, k -> new HashSet<>()).add(className)); 75 // Compute inlinedUsages map. 76 analysis.inlinedUsages().forEach(u -> 77 inlinedUsages.computeIfAbsent(u, k -> new HashSet<>()).add(className)); 78 79 if (analysis.isDependencyToAll()) { 80 mDependencyToAll.add(sourcePath); 81 } 82 } 83 // Finally build file dependencies 84 buildFileDependencies( 85 combineDependencies(classDependencies, inlinedUsages, constantRegistry)); 86 } 87 generateSourcePaths()88 private Map<String, String> generateSourcePaths() { 89 Map<String, String> sourcePaths = new HashMap<>(); 90 mJavaSourceDataList.forEach(data -> 91 sourcePaths.put(data.getPackagePrependedFileName(), data.getFilePath())); 92 return sourcePaths; 93 } 94 combineDependencies(Map<String, Set<String>> classDependencies, Map<Object, Set<String>> inlinedUsages, Map<Object, Set<String>> constantRegistry)95 private Map<String, Set<String>> combineDependencies(Map<String, Set<String>> classDependencies, 96 Map<Object, Set<String>> inlinedUsages, 97 Map<Object, Set<String>> constantRegistry) { 98 Map<String, Set<String>> combined = new HashMap<>( 99 buildConstantDependencies(inlinedUsages, constantRegistry)); 100 classDependencies.forEach((k, v) -> 101 combined.computeIfAbsent(k, key -> new HashSet<>()).addAll(v)); 102 return combined; 103 } 104 buildConstantDependencies( Map<Object, Set<String>> inlinedUsages, Map<Object, Set<String>> constantRegistry)105 private Map<String, Set<String>> buildConstantDependencies( 106 Map<Object, Set<String>> inlinedUsages, Map<Object, Set<String>> constantRegistry) { 107 Map<String, Set<String>> constantDependencies = new HashMap<>(); 108 for (Map.Entry<Object, Set<String>> usageEntry : inlinedUsages.entrySet()) { 109 Object usage = usageEntry.getKey(); 110 Set<String> usageClasses = usageEntry.getValue(); 111 if (constantRegistry.containsKey(usage)) { 112 Set<String> declarationClasses = constantRegistry.get(usage); 113 for (String usageClass : usageClasses) { 114 // Sometimes Usage and Declarations are in the same file, we remove such cases 115 // to prevent circular dependency. 116 declarationClasses.remove(usageClass); 117 constantDependencies.computeIfAbsent(usageClass, k -> 118 new HashSet<>()).addAll(declarationClasses); 119 } 120 } 121 } 122 123 return constantDependencies; 124 } 125 buildFileDependencies(Map<String, Set<String>> combinedClassDependencies)126 private void buildFileDependencies(Map<String, Set<String>> combinedClassDependencies) { 127 combinedClassDependencies.forEach((className, dependencies) -> { 128 String sourceFile = mClassToSourceMap.get(className); 129 if (sourceFile == null) { 130 throw new IllegalArgumentException("Class '" + className 131 + "' does not have a corresponding source file."); 132 } 133 mFileDependencies.computeIfAbsent(sourceFile, k -> new HashSet<>()); 134 dependencies.forEach(dependency -> { 135 String dependencySource = mClassToSourceMap.get(dependency); 136 if (dependencySource == null) { 137 throw new IllegalArgumentException("Dependency '" + dependency 138 + "' does not have a corresponding source file."); 139 } 140 mFileDependencies.get(sourceFile).add(dependencySource); 141 }); 142 }); 143 } 144 buildSourceToClassMap()145 private void buildSourceToClassMap() { 146 mClassToSourceMap.forEach((className, sourceFile) -> 147 mSourceToClasses.computeIfAbsent(sourceFile, k -> 148 new HashSet<>()).add(className)); 149 } 150 createFileDependencies()151 private DependencyProto.FileDependencyList createFileDependencies() { 152 List<DependencyProto.FileDependency> fileDependencies = new ArrayList<>(); 153 mFileDependencies.forEach((file, dependencies) -> { 154 DependencyProto.FileDependency dependency = DependencyProto.FileDependency.newBuilder() 155 .setFilePath(file) 156 .setIsDependencyToAll(mDependencyToAll.contains(file)) 157 .addAllGeneratedClasses(mSourceToClasses.get(file)) 158 .addAllFileDependencies(dependencies) 159 .build(); 160 fileDependencies.add(dependency); 161 }); 162 return DependencyProto.FileDependencyList.newBuilder() 163 .addAllFileDependency(fileDependencies).build(); 164 } 165 } 166