/* * 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} and {@link List} together as a * flat map, which represents dependency related attributes of a java file. */ public class DependencyMapper { private final List mClassAnalysisList; private final List mJavaSourceDataList; private final Map mClassToSourceMap = new HashMap<>(); private final Map> mFileDependencies = new HashMap<>(); private final Set mDependencyToAll = new HashSet<>(); private final Map> mSourceToClasses = new HashMap<>(); public DependencyMapper(List classAnalysisList, List 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 sourcePaths = generateSourcePaths(); // A map between qualified className and its dependencies Map> classDependencies = new HashMap<>(); // A map between constant values and the their declarations. Map> constantRegistry = new HashMap<>(); // A map between constant values and the their inlined usages. Map> 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 generateSourcePaths() { Map sourcePaths = new HashMap<>(); mJavaSourceDataList.forEach(data -> sourcePaths.put(data.getPackagePrependedFileName(), data.getFilePath())); return sourcePaths; } private Map> combineDependencies(Map> classDependencies, Map> inlinedUsages, Map> constantRegistry) { Map> combined = new HashMap<>( buildConstantDependencies(inlinedUsages, constantRegistry)); classDependencies.forEach((k, v) -> combined.computeIfAbsent(k, key -> new HashSet<>()).addAll(v)); return combined; } private Map> buildConstantDependencies( Map> inlinedUsages, Map> constantRegistry) { Map> constantDependencies = new HashMap<>(); for (Map.Entry> usageEntry : inlinedUsages.entrySet()) { Object usage = usageEntry.getKey(); Set usageClasses = usageEntry.getValue(); if (constantRegistry.containsKey(usage)) { Set 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> 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 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(); } }