| /* | |
| * Copyright (C) 2025 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| package com.android.dependencymapper; | |
| import com.android.dependencymapper.DependencyProto; | |
| import java.util.ArrayList; | |
| import java.util.HashMap; | |
| import java.util.HashSet; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Set; | |
| /** | |
| * This class binds {@link List<ClassDependencyData>} and {@link List<JavaSourceData>} together as a | |
| * flat map, which represents dependency related attributes of a java file. | |
| */ | |
| public class DependencyMapper { | |
| private final List<ClassDependencyData> mClassAnalysisList; | |
| private final List<JavaSourceData> mJavaSourceDataList; | |
| private final Map<String, String> mClassToSourceMap = new HashMap<>(); | |
| private final Map<String, Set<String>> mFileDependencies = new HashMap<>(); | |
| private final Set<String> mDependencyToAll = new HashSet<>(); | |
| private final Map<String, Set<String>> mSourceToClasses = new HashMap<>(); | |
| public DependencyMapper(List<ClassDependencyData> classAnalysisList, List<JavaSourceData> javaSourceDataList) { | |
| this.mClassAnalysisList = classAnalysisList; | |
| this.mJavaSourceDataList = javaSourceDataList; | |
| } | |
| public DependencyProto.FileDependencyList buildDependencyMaps() { | |
| buildClassDependencyMaps(); | |
| buildSourceToClassMap(); | |
| return createFileDependencies(); | |
| } | |
| private void buildClassDependencyMaps() { | |
| // Create a map between package appended file names and file paths. | |
| Map<String, String> sourcePaths = generateSourcePaths(); | |
| // A map between qualified className and its dependencies | |
| Map<String, Set<String>> classDependencies = new HashMap<>(); | |
| // A map between constant values and the their declarations. | |
| Map<Object, Set<String>> constantRegistry = new HashMap<>(); | |
| // A map between constant values and the their inlined usages. | |
| Map<Object, Set<String>> inlinedUsages = new HashMap<>(); | |
| for (ClassDependencyData analysis : mClassAnalysisList) { | |
| String className = analysis.getQualifiedName(); | |
| // Compute qualified class name to source path map. | |
| String sourceKey = analysis.getPackagePrependedClassSource(); | |
| String sourcePath = sourcePaths.get(sourceKey); | |
| mClassToSourceMap.put(className, sourcePath); | |
| // compute classDependencies | |
| classDependencies.computeIfAbsent(className, k -> | |
| new HashSet<>()).addAll(analysis.getClassDependencies()); | |
| // Compute constantRegistry | |
| analysis.getConstantsDefined().forEach(c -> | |
| constantRegistry.computeIfAbsent(c, k -> new HashSet<>()).add(className)); | |
| // Compute inlinedUsages map. | |
| analysis.inlinedUsages().forEach(u -> | |
| inlinedUsages.computeIfAbsent(u, k -> new HashSet<>()).add(className)); | |
| if (analysis.isDependencyToAll()) { | |
| mDependencyToAll.add(sourcePath); | |
| } | |
| } | |
| // Finally build file dependencies | |
| buildFileDependencies( | |
| combineDependencies(classDependencies, inlinedUsages, constantRegistry)); | |
| } | |
| private Map<String, String> generateSourcePaths() { | |
| Map<String, String> sourcePaths = new HashMap<>(); | |
| mJavaSourceDataList.forEach(data -> | |
| sourcePaths.put(data.getPackagePrependedFileName(), data.getFilePath())); | |
| return sourcePaths; | |
| } | |
| private Map<String, Set<String>> combineDependencies(Map<String, Set<String>> classDependencies, | |
| Map<Object, Set<String>> inlinedUsages, | |
| Map<Object, Set<String>> constantRegistry) { | |
| Map<String, Set<String>> combined = new HashMap<>( | |
| buildConstantDependencies(inlinedUsages, constantRegistry)); | |
| classDependencies.forEach((k, v) -> | |
| combined.computeIfAbsent(k, key -> new HashSet<>()).addAll(v)); | |
| return combined; | |
| } | |
| private Map<String, Set<String>> buildConstantDependencies( | |
| Map<Object, Set<String>> inlinedUsages, Map<Object, Set<String>> constantRegistry) { | |
| Map<String, Set<String>> constantDependencies = new HashMap<>(); | |
| for (Map.Entry<Object, Set<String>> usageEntry : inlinedUsages.entrySet()) { | |
| Object usage = usageEntry.getKey(); | |
| Set<String> usageClasses = usageEntry.getValue(); | |
| if (constantRegistry.containsKey(usage)) { | |
| Set<String> declarationClasses = constantRegistry.get(usage); | |
| for (String usageClass : usageClasses) { | |
| // Sometimes Usage and Declarations are in the same file, we remove such cases | |
| // to prevent circular dependency. | |
| declarationClasses.remove(usageClass); | |
| constantDependencies.computeIfAbsent(usageClass, k -> | |
| new HashSet<>()).addAll(declarationClasses); | |
| } | |
| } | |
| } | |
| return constantDependencies; | |
| } | |
| private void buildFileDependencies(Map<String, Set<String>> combinedClassDependencies) { | |
| combinedClassDependencies.forEach((className, dependencies) -> { | |
| String sourceFile = mClassToSourceMap.get(className); | |
| if (sourceFile == null) { | |
| throw new IllegalArgumentException("Class '" + className | |
| + "' does not have a corresponding source file."); | |
| } | |
| mFileDependencies.computeIfAbsent(sourceFile, k -> new HashSet<>()); | |
| dependencies.forEach(dependency -> { | |
| String dependencySource = mClassToSourceMap.get(dependency); | |
| if (dependencySource == null) { | |
| throw new IllegalArgumentException("Dependency '" + dependency | |
| + "' does not have a corresponding source file."); | |
| } | |
| mFileDependencies.get(sourceFile).add(dependencySource); | |
| }); | |
| }); | |
| } | |
| private void buildSourceToClassMap() { | |
| mClassToSourceMap.forEach((className, sourceFile) -> | |
| mSourceToClasses.computeIfAbsent(sourceFile, k -> | |
| new HashSet<>()).add(className)); | |
| } | |
| private DependencyProto.FileDependencyList createFileDependencies() { | |
| List<DependencyProto.FileDependency> fileDependencies = new ArrayList<>(); | |
| mFileDependencies.forEach((file, dependencies) -> { | |
| DependencyProto.FileDependency dependency = DependencyProto.FileDependency.newBuilder() | |
| .setFilePath(file) | |
| .setIsDependencyToAll(mDependencyToAll.contains(file)) | |
| .addAllGeneratedClasses(mSourceToClasses.get(file)) | |
| .addAllFileDependencies(dependencies) | |
| .build(); | |
| fileDependencies.add(dependency); | |
| }); | |
| return DependencyProto.FileDependencyList.newBuilder() | |
| .addAllFileDependency(fileDependencies).build(); | |
| } | |
| } |