diff options
32 files changed, 1904 insertions, 0 deletions
diff --git a/tools/dependency_mapper/Android.bp b/tools/dependency_mapper/Android.bp new file mode 100644 index 0000000000..6763c0e106 --- /dev/null +++ b/tools/dependency_mapper/Android.bp @@ -0,0 +1,45 @@ +package { + default_applicable_licenses: ["Android-Apache-2.0"], + default_team: "trendy_team_android_crumpet", +} + +java_binary_host { + name: "dependency-mapper", + main_class: "com.android.dependencymapper.Main", + static_libs: [ + "dependency-mapper-host-lib", + ], + visibility: ["//visibility:public"], +} + +java_library_host { + name: "dependency-mapper-host-lib", + srcs: [ + "src/**/*.java", + "proto/**/*.proto", + ], + static_libs: [ + "gson", + "ow2-asm", + ], +} + +java_test_host { + name: "dependency-mapper-tests", + srcs: ["tests/src/**/*.java"], + static_libs: [ + "junit", + "dependency-mapper-host-lib", + ], + data: [ + "tests/res/**/*", + ], + test_options: { + unit_test: true, + }, +} + +java_library { + name: "dependency-mapper-test-data", + srcs: ["tests/res/**/*.java"], +} diff --git a/tools/dependency_mapper/OWNERS b/tools/dependency_mapper/OWNERS new file mode 100644 index 0000000000..44772698c4 --- /dev/null +++ b/tools/dependency_mapper/OWNERS @@ -0,0 +1 @@ +himanshuz@google.com
\ No newline at end of file diff --git a/tools/dependency_mapper/README.md b/tools/dependency_mapper/README.md new file mode 100644 index 0000000000..475aef24fe --- /dev/null +++ b/tools/dependency_mapper/README.md @@ -0,0 +1,26 @@ +# Dependency Mapper + +[dependency-mapper] command line tool. This tool finds the usage based dependencies between java +files by utilizing byte-code and java file analysis. + +# Getting Started + +## Inputs +* rsp file, containing list of java files separated by whitespace. +* jar file, containing class files generated after compiling the contents of rsp file. + +## Output +* proto file, representing the list of dependencies for each java file present in input rsp file, +represented by [proto/usage.proto] + +## Usage +``` +dependency-mapper --src-path [src-list.rsp] --jar-path [classes.jar] --usage-map-path [usage-map.proto]" +``` + +# Notes +## Dependencies enlisted are only within the java files present in input. +## Ensure that [SourceFile] is present in the classes present in the jar. +## To ensure dependencies are listed correctly +* Classes jar should only contain class files generated from the source rsp files. +* Classes jar should not exclude any class file that was generated from source rsp files.
\ No newline at end of file diff --git a/tools/dependency_mapper/proto/dependency.proto b/tools/dependency_mapper/proto/dependency.proto new file mode 100644 index 0000000000..60a88f8f40 --- /dev/null +++ b/tools/dependency_mapper/proto/dependency.proto @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2019 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. + */ + +syntax = "proto2"; + +package com.android.dependencymapper; +option java_package = "com.android.dependencymapper"; +option java_outer_classname = "DependencyProto"; + +/** + * A com.android.dependencymapper.DependencyProto.FileDependency object. + */ + +message FileDependency { + + // java file path on disk + optional string file_path = 1; + // if a change in this file warrants recompiling all files + optional bool is_dependency_to_all = 2; + // class files generated when this java file is compiled + repeated string generated_classes = 3; + // dependencies of this file. + repeated string file_dependencies = 4; +} + +/** + * A com.android.dependencymapper.DependencyProto.FileDependencyList object. + */ +message FileDependencyList { + + // List of java file usages + repeated FileDependency fileDependency = 1; +}
\ No newline at end of file diff --git a/tools/dependency_mapper/src/com/android/dependencymapper/ClassDependenciesVisitor.java b/tools/dependency_mapper/src/com/android/dependencymapper/ClassDependenciesVisitor.java new file mode 100644 index 0000000000..ba6514586e --- /dev/null +++ b/tools/dependency_mapper/src/com/android/dependencymapper/ClassDependenciesVisitor.java @@ -0,0 +1,316 @@ +/* + * 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 org.objectweb.asm.signature.SignatureReader; +import org.objectweb.asm.signature.SignatureVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.TypePath; + +import java.lang.annotation.RetentionPolicy; +import java.util.HashSet; +import java.util.Set; + +/** + * An ASM based class visitor to analyze and club all dependencies of a java file. + * Most of the logic of this class is inspired from + * <a href="https://github.com/gradle/gradle/blob/master/platforms/jvm/language-java/src/main/java/org/gradle/api/internal/tasks/compile/incremental/asm/ClassDependenciesVisitor.java">gradle incremental compilation</a> + */ +public class ClassDependenciesVisitor extends ClassVisitor { + + private final static int API = Opcodes.ASM9; + + private final Set<String> mClassTypes; + private final Set<Object> mConstantsDefined; + private final Set<Object> mInlinedUsages; + private String mSource; + private boolean isAnnotationType; + private boolean mIsDependencyToAll; + private final RetentionPolicyVisitor retentionPolicyVisitor; + + private final ClassRelevancyFilter mClassFilter; + + private ClassDependenciesVisitor(ClassReader reader, ClassRelevancyFilter filter) { + super(API); + this.mClassTypes = new HashSet<>(); + this.mConstantsDefined = new HashSet<>(); + this.mInlinedUsages = new HashSet<>(); + this.retentionPolicyVisitor = new RetentionPolicyVisitor(); + this.mClassFilter = filter; + collectRemainingClassDependencies(reader); + } + + public static ClassDependencyData analyze( + String className, ClassReader reader, ClassRelevancyFilter filter) { + ClassDependenciesVisitor visitor = new ClassDependenciesVisitor(reader, filter); + reader.accept(visitor, ClassReader.SKIP_FRAMES); + // Sometimes a class may contain references to the same class, we remove such cases to + // prevent circular dependency. + visitor.getClassTypes().remove(className); + return new ClassDependencyData(Utils.buildPackagePrependedClassSource( + className, visitor.getSource()), className, visitor.getClassTypes(), + visitor.isDependencyToAll(), visitor.getConstantsDefined(), + visitor.getInlinedUsages()); + } + + @Override + public void visitSource(String source, String debug) { + mSource = source; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, + String[] interfaces) { + isAnnotationType = isAnnotationType(interfaces); + maybeAddClassTypesFromSignature(signature, mClassTypes); + if (superName != null) { + // superName can be null if what we are analyzing is `java.lang.Object` + // which can happen when a custom Java SDK is on classpath (typically, android.jar) + Type type = Type.getObjectType(superName); + maybeAddClassType(mClassTypes, type); + } + for (String s : interfaces) { + Type interfaceType = Type.getObjectType(s); + maybeAddClassType(mClassTypes, interfaceType); + } + } + + // performs a fast analysis of classes referenced in bytecode (method bodies) + // avoiding us to implement a costly visitor and potentially missing edge cases + private void collectRemainingClassDependencies(ClassReader reader) { + char[] charBuffer = new char[reader.getMaxStringLength()]; + for (int i = 1; i < reader.getItemCount(); i++) { + int itemOffset = reader.getItem(i); + // see https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.4 + if (itemOffset > 0 && reader.readByte(itemOffset - 1) == 7) { + // A CONSTANT_Class entry, read the class descriptor + String classDescriptor = reader.readUTF8(itemOffset, charBuffer); + Type type = Type.getObjectType(classDescriptor); + maybeAddClassType(mClassTypes, type); + } + } + } + + private void maybeAddClassTypesFromSignature(String signature, Set<String> types) { + if (signature != null) { + SignatureReader signatureReader = new SignatureReader(signature); + signatureReader.accept(new SignatureVisitor(API) { + @Override + public void visitClassType(String className) { + Type type = Type.getObjectType(className); + maybeAddClassType(types, type); + } + }); + } + } + + protected void maybeAddClassType(Set<String> types, Type type) { + while (type.getSort() == Type.ARRAY) { + type = type.getElementType(); + } + if (type.getSort() != Type.OBJECT) { + return; + } + //String name = Utils.classPackageToFilePath(type.getClassName()); + String name = type.getClassName(); + if (mClassFilter.test(name)) { + types.add(name); + } + } + + public String getSource() { + return mSource; + } + + public Set<String> getClassTypes() { + return mClassTypes; + } + + public Set<Object> getConstantsDefined() { + return mConstantsDefined; + } + + public Set<Object> getInlinedUsages() { + return mInlinedUsages; + } + + private boolean isAnnotationType(String[] interfaces) { + return interfaces.length == 1 && interfaces[0].equals("java/lang/annotation/Annotation"); + } + + @Override + public FieldVisitor visitField( + int access, String name, String desc, String signature, Object value) { + maybeAddClassTypesFromSignature(signature, mClassTypes); + maybeAddClassType(mClassTypes, Type.getType(desc)); + if (isAccessibleConstant(access, value)) { + mConstantsDefined.add(value); + } + return new FieldVisitor(mClassTypes); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + maybeAddClassTypesFromSignature(signature, mClassTypes); + Type methodType = Type.getMethodType(desc); + maybeAddClassType(mClassTypes, methodType.getReturnType()); + for (Type argType : methodType.getArgumentTypes()) { + maybeAddClassType(mClassTypes, argType); + } + return new MethodVisitor(mClassTypes); + } + + @Override + public org.objectweb.asm.AnnotationVisitor visitAnnotation(String desc, boolean visible) { + if (isAnnotationType && "Ljava/lang/annotation/Retention;".equals(desc)) { + return retentionPolicyVisitor; + } else { + maybeAddClassType(mClassTypes, Type.getType(desc)); + return new AnnotationVisitor(mClassTypes); + } + } + + private static boolean isAccessible(int access) { + return (access & Opcodes.ACC_PRIVATE) == 0; + } + + private static boolean isAccessibleConstant(int access, Object value) { + return isConstant(access) && isAccessible(access) && value != null; + } + + private static boolean isConstant(int access) { + return (access & Opcodes.ACC_FINAL) != 0 && (access & Opcodes.ACC_STATIC) != 0; + } + + public boolean isDependencyToAll() { + return mIsDependencyToAll; + } + + private class FieldVisitor extends org.objectweb.asm.FieldVisitor { + private final Set<String> types; + + public FieldVisitor(Set<String> types) { + super(API); + this.types = types; + } + + @Override + public org.objectweb.asm.AnnotationVisitor visitAnnotation( + String descriptor, boolean visible) { + maybeAddClassType(types, Type.getType(descriptor)); + return new AnnotationVisitor(types); + } + + @Override + public org.objectweb.asm.AnnotationVisitor visitTypeAnnotation(int typeRef, + TypePath typePath, String descriptor, boolean visible) { + maybeAddClassType(types, Type.getType(descriptor)); + return new AnnotationVisitor(types); + } + } + + private class MethodVisitor extends org.objectweb.asm.MethodVisitor { + private final Set<String> types; + + protected MethodVisitor(Set<String> types) { + super(API); + this.types = types; + } + + @Override + public void visitLdcInsn(Object value) { + mInlinedUsages.add(value); + super.visitLdcInsn(value); + } + + @Override + public void visitLocalVariable( + String name, String desc, String signature, Label start, Label end, int index) { + maybeAddClassTypesFromSignature(signature, mClassTypes); + maybeAddClassType(mClassTypes, Type.getType(desc)); + super.visitLocalVariable(name, desc, signature, start, end, index); + } + + @Override + public org.objectweb.asm.AnnotationVisitor visitAnnotation( + String descriptor, boolean visible) { + maybeAddClassType(types, Type.getType(descriptor)); + return new AnnotationVisitor(types); + } + + @Override + public org.objectweb.asm.AnnotationVisitor visitParameterAnnotation( + int parameter, String descriptor, boolean visible) { + maybeAddClassType(types, Type.getType(descriptor)); + return new AnnotationVisitor(types); + } + + @Override + public org.objectweb.asm.AnnotationVisitor visitTypeAnnotation( + int typeRef, TypePath typePath, String descriptor, boolean visible) { + maybeAddClassType(types, Type.getType(descriptor)); + return new AnnotationVisitor(types); + } + } + + private class RetentionPolicyVisitor extends org.objectweb.asm.AnnotationVisitor { + public RetentionPolicyVisitor() { + super(ClassDependenciesVisitor.API); + } + + @Override + public void visitEnum(String name, String desc, String value) { + if ("Ljava/lang/annotation/RetentionPolicy;".equals(desc)) { + RetentionPolicy policy = RetentionPolicy.valueOf(value); + if (policy == RetentionPolicy.SOURCE) { + mIsDependencyToAll = true; + } + } + } + } + + private class AnnotationVisitor extends org.objectweb.asm.AnnotationVisitor { + private final Set<String> types; + + public AnnotationVisitor(Set<String> types) { + super(ClassDependenciesVisitor.API); + this.types = types; + } + + @Override + public void visit(String name, Object value) { + if (value instanceof Type) { + maybeAddClassType(types, (Type) value); + } + } + + @Override + public org.objectweb.asm.AnnotationVisitor visitArray(String name) { + return this; + } + + @Override + public org.objectweb.asm.AnnotationVisitor visitAnnotation(String name, String descriptor) { + maybeAddClassType(types, Type.getType(descriptor)); + return this; + } + } +}
\ No newline at end of file diff --git a/tools/dependency_mapper/src/com/android/dependencymapper/ClassDependencyAnalyzer.java b/tools/dependency_mapper/src/com/android/dependencymapper/ClassDependencyAnalyzer.java new file mode 100644 index 0000000000..4a37b41ffe --- /dev/null +++ b/tools/dependency_mapper/src/com/android/dependencymapper/ClassDependencyAnalyzer.java @@ -0,0 +1,56 @@ +/* + * 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 org.objectweb.asm.ClassReader; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * An utility class that reads each class file present in the classes jar, then analyzes the same, + * collecting the dependencies in {@link List<ClassDependencyData>} + */ +public class ClassDependencyAnalyzer { + + public static List<ClassDependencyData> analyze(Path classJar, ClassRelevancyFilter classFilter) { + List<ClassDependencyData> classAnalysisList = new ArrayList<>(); + try (JarFile jarFile = new JarFile(classJar.toFile())) { + Enumeration<JarEntry> entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (entry.getName().endsWith(".class")) { + try (InputStream inputStream = jarFile.getInputStream(entry)) { + String name = Utils.trimAndConvertToPackageBasedPath(entry.getName()); + ClassDependencyData classAnalysis = ClassDependenciesVisitor.analyze(name, + new ClassReader(inputStream), classFilter); + classAnalysisList.add(classAnalysis); + } + } + } + } catch (IOException e) { + System.err.println("Error reading the jar file at: " + classJar); + throw new RuntimeException(e); + } + return classAnalysisList; + } +} diff --git a/tools/dependency_mapper/src/com/android/dependencymapper/ClassDependencyData.java b/tools/dependency_mapper/src/com/android/dependencymapper/ClassDependencyData.java new file mode 100644 index 0000000000..58e388faa0 --- /dev/null +++ b/tools/dependency_mapper/src/com/android/dependencymapper/ClassDependencyData.java @@ -0,0 +1,65 @@ +/* + * 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 java.util.Set; + +/** + * Represents the Class Dependency Data collected via ASM analysis. + */ +public class ClassDependencyData { + private final String mPackagePrependedClassSource; + private final String mQualifiedName; + private final Set<String> mClassDependencies; + private final boolean mIsDependencyToAll; + private final Set<Object> mConstantsDefined; + private final Set<Object> mInlinedUsages; + + public ClassDependencyData(String packagePrependedClassSource, String className, + Set<String> classDependencies, boolean isDependencyToAll, Set<Object> constantsDefined, + Set<Object> inlinedUsages) { + this.mPackagePrependedClassSource = packagePrependedClassSource; + this.mQualifiedName = className; + this.mClassDependencies = classDependencies; + this.mIsDependencyToAll = isDependencyToAll; + this.mConstantsDefined = constantsDefined; + this.mInlinedUsages = inlinedUsages; + } + + public String getPackagePrependedClassSource() { + return mPackagePrependedClassSource; + } + + public String getQualifiedName() { + return mQualifiedName; + } + + public Set<String> getClassDependencies() { + return mClassDependencies; + } + + public Set<Object> getConstantsDefined() { + return mConstantsDefined; + } + + public Set<Object> inlinedUsages() { + return mInlinedUsages; + } + + public boolean isDependencyToAll() { + return mIsDependencyToAll; + } +} diff --git a/tools/dependency_mapper/src/com/android/dependencymapper/ClassRelevancyFilter.java b/tools/dependency_mapper/src/com/android/dependencymapper/ClassRelevancyFilter.java new file mode 100644 index 0000000000..c46b53f6d1 --- /dev/null +++ b/tools/dependency_mapper/src/com/android/dependencymapper/ClassRelevancyFilter.java @@ -0,0 +1,36 @@ +/* + * 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 java.util.Set; +import java.util.function.Predicate; + +/** + * A filter representing the list of class files which are relevant for dependency analysis. + */ +public class ClassRelevancyFilter implements Predicate<String> { + + private final Set<String> mAllowlistedClassNames; + + public ClassRelevancyFilter(Set<String> allowlistedClassNames) { + this.mAllowlistedClassNames = allowlistedClassNames; + } + + @Override + public boolean test(String className) { + return mAllowlistedClassNames.contains(className); + } +} diff --git a/tools/dependency_mapper/src/com/android/dependencymapper/DependencyMapper.java b/tools/dependency_mapper/src/com/android/dependencymapper/DependencyMapper.java new file mode 100644 index 0000000000..ecf520c7d8 --- /dev/null +++ b/tools/dependency_mapper/src/com/android/dependencymapper/DependencyMapper.java @@ -0,0 +1,165 @@ +/* + * 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(); + } +} diff --git a/tools/dependency_mapper/src/com/android/dependencymapper/JavaSourceAnalyzer.java b/tools/dependency_mapper/src/com/android/dependencymapper/JavaSourceAnalyzer.java new file mode 100644 index 0000000000..3a4efadd77 --- /dev/null +++ b/tools/dependency_mapper/src/com/android/dependencymapper/JavaSourceAnalyzer.java @@ -0,0 +1,81 @@ +/* + * 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 java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * An utility class that reads each java file present in the rsp content then analyzes the same, + * collecting the analysis in {@link List<JavaSourceData>} + */ +public class JavaSourceAnalyzer { + + // Regex that matches against "package abc.xyz.lmn;" declarations in a java file. + private static final String PACKAGE_REGEX = "^package\\s+([a-zA-Z_][a-zA-Z0-9_.]*);"; + + public static List<JavaSourceData> analyze(Path srcRspFile) { + List<JavaSourceData> javaSourceDataList = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new FileReader(srcRspFile.toFile()))) { + String line; + while ((line = reader.readLine()) != null) { + // Split the line by spaces, tabs, multiple java files can be on a single line. + String[] files = line.trim().split("\\s+"); + for (String file : files) { + Path p = Paths.get("", file); + System.out.println(p.toAbsolutePath().toString()); + javaSourceDataList + .add(new JavaSourceData(file, constructPackagePrependedFileName(file))); + } + } + } catch (IOException e) { + System.err.println("Error reading rsp file at: " + srcRspFile); + throw new RuntimeException(e); + } + return javaSourceDataList; + } + + private static String constructPackagePrependedFileName(String filePath) { + String packageAppendedFileName = null; + // if the file path is abc/def/ghi/JavaFile.java we extract JavaFile.java + String javaFileName = filePath.substring(filePath.lastIndexOf("/") + 1); + try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { + String line; + // Process each line and match against the package regex pattern. + while ((line = reader.readLine()) != null) { + Pattern pattern = Pattern.compile(PACKAGE_REGEX); + Matcher matcher = pattern.matcher(line); + if (matcher.find()) { + packageAppendedFileName = matcher.group(1) + "." + javaFileName; + break; + } + } + } catch (IOException e) { + System.err.println("Error reading java file at: " + filePath); + throw new RuntimeException(e); + } + // Should not be null + assert packageAppendedFileName != null; + return packageAppendedFileName; + } +} diff --git a/tools/dependency_mapper/src/com/android/dependencymapper/JavaSourceData.java b/tools/dependency_mapper/src/com/android/dependencymapper/JavaSourceData.java new file mode 100644 index 0000000000..89453d0abe --- /dev/null +++ b/tools/dependency_mapper/src/com/android/dependencymapper/JavaSourceData.java @@ -0,0 +1,38 @@ +/* + * 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; + +/** + * POJO representing the data collected from Java Source file analysis. + */ +public class JavaSourceData { + + private final String mFilePath; + private final String mPackagePrependedFileName; + + public JavaSourceData(String filePath, String packagePrependedFileName) { + mFilePath = filePath; + mPackagePrependedFileName = packagePrependedFileName; + } + + public String getFilePath() { + return mFilePath; + } + + public String getPackagePrependedFileName() { + return mPackagePrependedFileName; + } +} diff --git a/tools/dependency_mapper/src/com/android/dependencymapper/Main.java b/tools/dependency_mapper/src/com/android/dependencymapper/Main.java new file mode 100644 index 0000000000..131c931098 --- /dev/null +++ b/tools/dependency_mapper/src/com/android/dependencymapper/Main.java @@ -0,0 +1,123 @@ +/* + * 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 static com.android.dependencymapper.Utils.listClassesInJar; + +import com.android.dependencymapper.DependencyProto; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; + +public class Main { + + public static void main(String[] args) throws IOException, InterruptedException { + try { + InputData input = parseAndValidateInput(args); + generateDependencyMap(input); + } catch (IllegalArgumentException e) { + System.err.println("Error: " + e.getMessage()); + showUsage(); + } + } + + private static class InputData { + public Path srcList; + public Path classesJar; + public Path dependencyMapProto; + + public InputData(Path srcList, Path classesJar, Path dependencyMapProto) { + this.srcList = srcList; + this.classesJar = classesJar; + this.dependencyMapProto = dependencyMapProto; + } + } + + private static InputData parseAndValidateInput(String[] args) { + for (String arg : args) { + if ("--help".equals(arg)) { + showUsage(); + System.exit(0); // Indicate successful exit after showing help + } + } + + if (args.length != 6) { // Explicitly check for the correct number of arguments + throw new IllegalArgumentException("Incorrect number of arguments"); + } + + Path srcList = null; + Path classesJar = null; + Path dependencyMapProto = null; + + for (int i = 0; i < args.length; i += 2) { + String arg = args[i].trim(); + String argValue = args[i + 1].trim(); + + switch (arg) { + case "--src-path" -> srcList = Path.of(argValue); + case "--jar-path" -> classesJar = Path.of(argValue); + case "--dependency-map-path" -> dependencyMapProto = Path.of(argValue); + default -> throw new IllegalArgumentException("Unknown argument: " + arg); + } + } + + // Validate file existence and readability + validateFile(srcList, "--src-path"); + validateFile(classesJar, "--jar-path"); + + return new InputData(srcList, classesJar, dependencyMapProto); + } + + private static void validateFile(Path path, String argName) { + if (path == null) { + throw new IllegalArgumentException(argName + " is required"); + } + if (!Files.exists(path)) { + throw new IllegalArgumentException(argName + " does not exist: " + path); + } + if (!Files.isReadable(path)) { + throw new IllegalArgumentException(argName + " is not readable: " + path); + } + } + + private static void generateDependencyMap(InputData input) { + // First collect all classes in the jar. + Set<String> classesInJar = listClassesInJar(input.classesJar); + // Perform dependency analysis. + List<ClassDependencyData> classDependencyDataList = ClassDependencyAnalyzer + .analyze(input.classesJar, new ClassRelevancyFilter(classesInJar)); + // Perform java source analysis. + List<JavaSourceData> javaSourceDataList = JavaSourceAnalyzer.analyze(input.srcList); + // Collect all dependencies and map them as DependencyProto.FileDependencyList + DependencyMapper dp = new DependencyMapper(classDependencyDataList, javaSourceDataList); + DependencyProto.FileDependencyList dependencyList = dp.buildDependencyMaps(); + + // Write the proto to output file + Utils.writeContentsToProto(dependencyList, input.dependencyMapProto); + } + + private static void showUsage() { + System.err.println( + "Usage: dependency-mapper " + + "--src-path [src-list.rsp] " + + "--jar-path [classes.jar] " + + "--dependency-map-path [dependency-map.proto]"); + } + +}
\ No newline at end of file diff --git a/tools/dependency_mapper/src/com/android/dependencymapper/Utils.java b/tools/dependency_mapper/src/com/android/dependencymapper/Utils.java new file mode 100644 index 0000000000..5dd5f35bb9 --- /dev/null +++ b/tools/dependency_mapper/src/com/android/dependencymapper/Utils.java @@ -0,0 +1,94 @@ +/* + * 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 com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public class Utils { + + public static String trimAndConvertToPackageBasedPath(String fileBasedPath) { + // Remove ".class" from the fileBasedPath, then replace "/" with "." + return fileBasedPath.replaceAll("\\..*", "").replaceAll("/", "."); + } + + public static String buildPackagePrependedClassSource(String qualifiedClassPath, + String classSource) { + // Find the location of the start of classname in the qualifiedClassPath + int classNameSt = qualifiedClassPath.lastIndexOf(".") + 1; + // Replace the classname in qualifiedClassPath with classSource + return qualifiedClassPath.substring(0, classNameSt) + classSource; + } + + public static void writeContentsToJson(DependencyProto.FileDependencyList contents, Path jsonOut) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + Map<String, Set<String>> jsonMap = new HashMap<>(); + for (DependencyProto.FileDependency fileDependency : contents.getFileDependencyList()) { + jsonMap.putIfAbsent(fileDependency.getFilePath(), + Set.copyOf(fileDependency.getFileDependenciesList())); + } + String json = gson.toJson(jsonMap); + try (FileWriter file = new FileWriter(jsonOut.toFile())) { + file.write(json); + } catch (IOException e) { + System.err.println("Error writing json output to: " + jsonOut); + throw new RuntimeException(e); + } + } + + public static void writeContentsToProto(DependencyProto.FileDependencyList usages, Path protoOut) { + try { + OutputStream outputStream = Files.newOutputStream(protoOut); + usages.writeDelimitedTo(outputStream); + } catch (IOException e) { + System.err.println("Error writing proto output to: " + protoOut); + throw new RuntimeException(e); + } + } + + public static Set<String> listClassesInJar(Path classesJarPath) { + Set<String> classes = new HashSet<>(); + try (JarFile jarFile = new JarFile(classesJarPath.toFile())) { + Enumeration<JarEntry> entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (entry.getName().endsWith(".class")) { + String name = Utils.trimAndConvertToPackageBasedPath(entry.getName()); + classes.add(name); + } + } + } catch (IOException e) { + System.err.println("Error reading the jar file at: " + classesJarPath); + throw new RuntimeException(e); + } + return classes; + } +} diff --git a/tools/dependency_mapper/tests/res/testdata/annotation/AnnotationUsage.java b/tools/dependency_mapper/tests/res/testdata/annotation/AnnotationUsage.java new file mode 100644 index 0000000000..bb40776966 --- /dev/null +++ b/tools/dependency_mapper/tests/res/testdata/annotation/AnnotationUsage.java @@ -0,0 +1,30 @@ +/* + * 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 res.testdata.annotation; + +@res.testdata.annotation.RuntimeAnnotation +public class AnnotationUsage { + + private final int mSourceAnnField; + + public AnnotationUsage(@res.testdata.annotation.SourceAnnotation int sourceAnnField) { + mSourceAnnField = sourceAnnField; + } + + public @res.testdata.annotation.SourceAnnotation int getSourceAnnField() { + return mSourceAnnField; + } +} diff --git a/tools/dependency_mapper/tests/res/testdata/annotation/RuntimeAnnotation.java b/tools/dependency_mapper/tests/res/testdata/annotation/RuntimeAnnotation.java new file mode 100644 index 0000000000..99a60745a4 --- /dev/null +++ b/tools/dependency_mapper/tests/res/testdata/annotation/RuntimeAnnotation.java @@ -0,0 +1,23 @@ +/* + * 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 res.testdata.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface RuntimeAnnotation { +} diff --git a/tools/dependency_mapper/tests/res/testdata/annotation/SourceAnnotation.java b/tools/dependency_mapper/tests/res/testdata/annotation/SourceAnnotation.java new file mode 100644 index 0000000000..dec3e834de --- /dev/null +++ b/tools/dependency_mapper/tests/res/testdata/annotation/SourceAnnotation.java @@ -0,0 +1,23 @@ +/* + * 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 res.testdata.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.SOURCE) +public @interface SourceAnnotation { +} diff --git a/tools/dependency_mapper/tests/res/testdata/constants/ConstantDefinition.java b/tools/dependency_mapper/tests/res/testdata/constants/ConstantDefinition.java new file mode 100644 index 0000000000..3f0a7898d2 --- /dev/null +++ b/tools/dependency_mapper/tests/res/testdata/constants/ConstantDefinition.java @@ -0,0 +1,20 @@ +/* + * 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 res.testdata.constants; + +public class ConstantDefinition { + public static final String TEST_CONSTANT = "test_constant"; +} diff --git a/tools/dependency_mapper/tests/res/testdata/constants/ConstantUsage.java b/tools/dependency_mapper/tests/res/testdata/constants/ConstantUsage.java new file mode 100644 index 0000000000..852e4d5c7b --- /dev/null +++ b/tools/dependency_mapper/tests/res/testdata/constants/ConstantUsage.java @@ -0,0 +1,25 @@ +/* + * 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 res.testdata.constants; + +public class ConstantUsage { + + public ConstantUsage(){} + + public String useConstantInMethodBody() { + return res.testdata.constants.ConstantDefinition.TEST_CONSTANT; + } +} diff --git a/tools/dependency_mapper/tests/res/testdata/inheritance/BaseClass.java b/tools/dependency_mapper/tests/res/testdata/inheritance/BaseClass.java new file mode 100644 index 0000000000..3b11eb1be8 --- /dev/null +++ b/tools/dependency_mapper/tests/res/testdata/inheritance/BaseClass.java @@ -0,0 +1,19 @@ +/* + * 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 res.testdata.inheritance; + +public class BaseClass { +} diff --git a/tools/dependency_mapper/tests/res/testdata/inheritance/BaseImpl.java b/tools/dependency_mapper/tests/res/testdata/inheritance/BaseImpl.java new file mode 100644 index 0000000000..7c2698bb2e --- /dev/null +++ b/tools/dependency_mapper/tests/res/testdata/inheritance/BaseImpl.java @@ -0,0 +1,21 @@ +/* + * 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 res.testdata.inheritance; + +public interface BaseImpl { + + void baseImpl(); +} diff --git a/tools/dependency_mapper/tests/res/testdata/inheritance/InheritanceUsage.java b/tools/dependency_mapper/tests/res/testdata/inheritance/InheritanceUsage.java new file mode 100644 index 0000000000..f8924791a1 --- /dev/null +++ b/tools/dependency_mapper/tests/res/testdata/inheritance/InheritanceUsage.java @@ -0,0 +1,24 @@ +/* + * 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 res.testdata.inheritance; + +public class InheritanceUsage extends res.testdata.inheritance.BaseClass implements + res.testdata.inheritance.BaseImpl { + @Override + public void baseImpl() { + + } +} diff --git a/tools/dependency_mapper/tests/res/testdata/methods/FieldUsage.java b/tools/dependency_mapper/tests/res/testdata/methods/FieldUsage.java new file mode 100644 index 0000000000..0d97312f69 --- /dev/null +++ b/tools/dependency_mapper/tests/res/testdata/methods/FieldUsage.java @@ -0,0 +1,21 @@ +/* + * 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 res.testdata.methods; + +public class FieldUsage { + + private res.testdata.methods.ReferenceClass1 mReferenceClass1; +} diff --git a/tools/dependency_mapper/tests/res/testdata/methods/MethodUsage.java b/tools/dependency_mapper/tests/res/testdata/methods/MethodUsage.java new file mode 100644 index 0000000000..9dd0223e69 --- /dev/null +++ b/tools/dependency_mapper/tests/res/testdata/methods/MethodUsage.java @@ -0,0 +1,24 @@ +/* + * 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 res.testdata.methods; + +public class MethodUsage { + + public void methodReferences(res.testdata.methods.ReferenceClass1 mReferenceClass1) { + res.testdata.methods.ReferenceClass2 referenceClass2 = + new res.testdata.methods.ReferenceClass2(); + } +} diff --git a/tools/dependency_mapper/tests/res/testdata/methods/ReferenceClass1.java b/tools/dependency_mapper/tests/res/testdata/methods/ReferenceClass1.java new file mode 100644 index 0000000000..f56c0a9fa6 --- /dev/null +++ b/tools/dependency_mapper/tests/res/testdata/methods/ReferenceClass1.java @@ -0,0 +1,21 @@ +/* + * 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 res.testdata.methods; + +public class ReferenceClass1 { + + public ReferenceClass1(){} +} diff --git a/tools/dependency_mapper/tests/res/testdata/methods/ReferenceClass2.java b/tools/dependency_mapper/tests/res/testdata/methods/ReferenceClass2.java new file mode 100644 index 0000000000..09e742248e --- /dev/null +++ b/tools/dependency_mapper/tests/res/testdata/methods/ReferenceClass2.java @@ -0,0 +1,20 @@ +/* + * 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 res.testdata.methods; + +public class ReferenceClass2 { + public ReferenceClass2(){} +} diff --git a/tools/dependency_mapper/tests/res/testfiles/dependency-mapper-test-data.jar b/tools/dependency_mapper/tests/res/testfiles/dependency-mapper-test-data.jar Binary files differnew file mode 100644 index 0000000000..98f5893d68 --- /dev/null +++ b/tools/dependency_mapper/tests/res/testfiles/dependency-mapper-test-data.jar diff --git a/tools/dependency_mapper/tests/res/testfiles/sources.rsp b/tools/dependency_mapper/tests/res/testfiles/sources.rsp new file mode 100644 index 0000000000..d895033c06 --- /dev/null +++ b/tools/dependency_mapper/tests/res/testfiles/sources.rsp @@ -0,0 +1,12 @@ +tests/res/testdata/annotation/AnnotationUsage.java +tests/res/testdata/annotation/SourceAnnotation.java +tests/res/testdata/annotation/RuntimeAnnotation.java +tests/res/testdata/constants/ConstantDefinition.java +tests/res/testdata/constants/ConstantUsage.java +tests/res/testdata/inheritance/InheritanceUsage.java +tests/res/testdata/inheritance/BaseClass.java +tests/res/testdata/inheritance/BaseImpl.java +tests/res/testdata/methods/FieldUsage.java +tests/res/testdata/methods/MethodUsage.java +tests/res/testdata/methods/ReferenceClass1.java +tests/res/testdata/methods/ReferenceClass2.java
\ No newline at end of file diff --git a/tools/dependency_mapper/tests/src/com/android/dependencymapper/ClassDependencyAnalyzerTest.java b/tools/dependency_mapper/tests/src/com/android/dependencymapper/ClassDependencyAnalyzerTest.java new file mode 100644 index 0000000000..95492c8501 --- /dev/null +++ b/tools/dependency_mapper/tests/src/com/android/dependencymapper/ClassDependencyAnalyzerTest.java @@ -0,0 +1,133 @@ +/* + * 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 static com.android.dependencymapper.Utils.listClassesInJar; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.BeforeClass; +import org.junit.Test; + +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class ClassDependencyAnalyzerTest { + + private static List<ClassDependencyData> mClassDependencyDataList; + + private static final String CLASSES_JAR_PATH = + "tests/res/testfiles/dependency-mapper-test-data.jar"; + + @BeforeClass + public static void beforeClass() throws URISyntaxException { + Path path = Paths.get(CLASSES_JAR_PATH); + Set<String> classesInJar = listClassesInJar(path); + // Perform dependency analysis. + mClassDependencyDataList = ClassDependencyAnalyzer.analyze(path, + new ClassRelevancyFilter(classesInJar)); + } + + @Test + public void testAnnotationDeps(){ + String annoClass = "res.testdata.annotation.AnnotationUsage"; + String sourceAnno = "res.testdata.annotation.SourceAnnotation"; + String runTimeAnno = "res.testdata.annotation.RuntimeAnnotation"; + + dependencyVerifier(annoClass, + new HashSet<>(List.of(runTimeAnno)), new HashSet<>(List.of(sourceAnno))); + + for (ClassDependencyData dep : mClassDependencyDataList) { + if (dep.getQualifiedName().equals(sourceAnno)) { + assertTrue(sourceAnno + " is not dependencyToAll ", dep.isDependencyToAll()); + } + if (dep.getQualifiedName().equals(runTimeAnno)) { + assertFalse(runTimeAnno + " is dependencyToAll ", dep.isDependencyToAll()); + } + } + } + + @Test + public void testConstantsDeps(){ + String constDefined = "test_constant"; + String constDefClass = "res.testdata.constants.ConstantDefinition"; + String constUsageClass = "res.testdata.constants.ConstantUsage"; + + boolean constUsageClassFound = false; + boolean constDefClassFound = false; + for (ClassDependencyData dep : mClassDependencyDataList) { + if (dep.getQualifiedName().equals(constUsageClass)) { + constUsageClassFound = true; + assertTrue("InlinedUsage of : " + constDefined + " not found", + dep.inlinedUsages().contains(constDefined)); + } + if (dep.getQualifiedName().equals(constDefClass)) { + constDefClassFound = true; + assertTrue("Constant " + constDefined + " not defined", + dep.getConstantsDefined().contains(constDefined)); + } + } + assertTrue("Class " + constUsageClass + " not found", constUsageClassFound); + assertTrue("Class " + constDefClass + " not found", constDefClassFound); + } + + @Test + public void testInheritanceDeps(){ + String sourceClass = "res.testdata.inheritance.InheritanceUsage"; + String baseClass = "res.testdata.inheritance.BaseClass"; + String baseImpl = "res.testdata.inheritance.BaseImpl"; + + dependencyVerifier(sourceClass, + new HashSet<>(List.of(baseClass, baseImpl)), new HashSet<>()); + } + + + @Test + public void testMethodDeps(){ + String fieldUsage = "res.testdata.methods.FieldUsage"; + String methodUsage = "res.testdata.methods.MethodUsage"; + String ref1 = "res.testdata.methods.ReferenceClass1"; + String ref2 = "res.testdata.methods.ReferenceClass2"; + + dependencyVerifier(fieldUsage, + new HashSet<>(List.of(ref1)), new HashSet<>(List.of(ref2))); + dependencyVerifier(methodUsage, + new HashSet<>(List.of(ref1, ref2)), new HashSet<>()); + } + + private void dependencyVerifier(String qualifiedName, Set<String> deps, Set<String> nonDeps) { + boolean depFound = false; + for (ClassDependencyData classDependencyData : mClassDependencyDataList) { + if (classDependencyData.getQualifiedName().equals(qualifiedName)) { + depFound = true; + for (String dep : deps) { + assertTrue(qualifiedName + " does not depends on " + dep, + classDependencyData.getClassDependencies().contains(dep)); + } + for (String nonDep : nonDeps) { + assertFalse(qualifiedName + " depends on " + nonDep, + classDependencyData.getClassDependencies().contains(nonDep)); + } + } + } + assertTrue("Class " + qualifiedName + " not found", depFound); + } +} diff --git a/tools/dependency_mapper/tests/src/com/android/dependencymapper/ClassRelevancyFilterTest.java b/tools/dependency_mapper/tests/src/com/android/dependencymapper/ClassRelevancyFilterTest.java new file mode 100644 index 0000000000..9a80c4bd80 --- /dev/null +++ b/tools/dependency_mapper/tests/src/com/android/dependencymapper/ClassRelevancyFilterTest.java @@ -0,0 +1,60 @@ +/* + * 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 static com.android.dependencymapper.Utils.listClassesInJar; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; + +import com.android.dependencymapper.ClassDependencyAnalyzer; +import com.android.dependencymapper.ClassDependencyData; +import com.android.dependencymapper.ClassRelevancyFilter; + +import org.junit.Test; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Set; + +public class ClassRelevancyFilterTest { + + private static final String CLASSES_JAR_PATH = + "tests/res/testfiles/dependency-mapper-test-data.jar"; + + @Test + public void testClassRelevancyFilter() { + Path path = Paths.get(CLASSES_JAR_PATH); + Set<String> classesInJar = listClassesInJar(path); + + // Add a relevancy filter that skips a class. + String skippedClass = "res.testdata.BaseClass"; + classesInJar.remove(skippedClass); + + // Perform dependency analysis. + List<ClassDependencyData> classDependencyDataList = + ClassDependencyAnalyzer.analyze(path, new ClassRelevancyFilter(classesInJar)); + + // check that the skipped class is not present in classDepsList + for (ClassDependencyData dep : classDependencyDataList) { + assertNotEquals("SkippedClass " + skippedClass + " is present", + skippedClass, dep.getQualifiedName()); + assertFalse("SkippedClass " + skippedClass + " is present as dependency of " + dep, + dep.getClassDependencies().contains(skippedClass)); + } + } +} diff --git a/tools/dependency_mapper/tests/src/com/android/dependencymapper/DependencyMapperTest.java b/tools/dependency_mapper/tests/src/com/android/dependencymapper/DependencyMapperTest.java new file mode 100644 index 0000000000..9c08e796c3 --- /dev/null +++ b/tools/dependency_mapper/tests/src/com/android/dependencymapper/DependencyMapperTest.java @@ -0,0 +1,201 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +public class DependencyMapperTest { + + private static final List<JavaSourceData> mJavaSourceData = new ArrayList<>(); + private static final List<ClassDependencyData> mClassDependencyData = new ArrayList<>(); + + private static Map<String, DependencyProto.FileDependency> mFileDependencyMap; + + public static String AUDIO_CONS = "AUDIO_CONS"; + public static String AUDIO_CONS_PATH = "frameworks/base/audio/AudioPermission.java"; + public static String AUDIO_CONS_PACKAGE = "com.android.audio.AudioPermission"; + + public static String AUDIO_TONE_CONS_1 = "AUDIO_TONE_CONS_1"; + public static String AUDIO_TONE_CONS_2 = "AUDIO_TONE_CONS_2"; + public static String AUDIO_TONE_CONS_PATH = "frameworks/base/audio/Audio$Tones.java"; + public static String AUDIO_TONE_CONS_PACKAGE = "com.android.audio.Audio$Tones"; + + public static String ST_MANAGER_PATH = "frameworks/base/core/storage/StorageManager.java"; + public static String ST_MANAGER_PACKAGE = "com.android.storage.StorageManager"; + + public static String CONST_OUTSIDE_SCOPE = "CONST_OUTSIDE_SCOPE"; + public static String PERM_MANAGER_PATH = "frameworks/base/core/permission/PermissionManager.java"; + public static String PERM_MANAGER_PACKAGE = "com.android.permission.PermissionManager"; + + public static String SOURCE_ANNO_PATH = "frameworks/base/anno/SourceAnno.java"; + public static String SOURCE_ANNO_PACKAGE = "com.android.anno.SourceAnno"; + + public static String PERM_SOURCE_PATH = "frameworks/base/core/permission/PermissionSources.java"; + public static String PERM_SOURCE_PACKAGE = "com.android.permission.PermissionSources"; + + public static String PERM_DATA_PATH = "frameworks/base/core/permission/PermissionSources$Data.java"; + public static String PERM_DATA_PACKAGE = "com.android.permission.PermissionSources$Data"; + + static { + JavaSourceData audioConstants = new JavaSourceData(AUDIO_CONS_PATH, AUDIO_CONS_PACKAGE + ".java"); + JavaSourceData audioToneConstants = + new JavaSourceData(AUDIO_TONE_CONS_PATH, AUDIO_TONE_CONS_PACKAGE + ".java"); //f2 + JavaSourceData stManager = new JavaSourceData( ST_MANAGER_PATH, ST_MANAGER_PACKAGE + ".java"); + JavaSourceData permManager = new JavaSourceData(PERM_MANAGER_PATH, PERM_MANAGER_PACKAGE + ".java"); + JavaSourceData permSource = new JavaSourceData(PERM_SOURCE_PATH, PERM_SOURCE_PACKAGE + ".java"); + JavaSourceData permSourceData = new JavaSourceData(PERM_DATA_PATH, PERM_DATA_PACKAGE + ".java"); + + JavaSourceData sourceNotPresentInClass = + new JavaSourceData(SOURCE_ANNO_PATH, SOURCE_ANNO_PACKAGE); + + mJavaSourceData.addAll(List.of(audioConstants, audioToneConstants, stManager, + permManager, permSource, permSourceData, sourceNotPresentInClass)); + + ClassDependencyData audioConstantsDeps = + new ClassDependencyData(AUDIO_CONS_PACKAGE + ".java", + AUDIO_CONS_PACKAGE, new HashSet<>(), false, + new HashSet<>(List.of(AUDIO_CONS)), new HashSet<>()); + + ClassDependencyData audioToneConstantsDeps = + new ClassDependencyData(AUDIO_TONE_CONS_PACKAGE + ".java", + AUDIO_TONE_CONS_PACKAGE, new HashSet<>(), false, + new HashSet<>(List.of(AUDIO_TONE_CONS_1, AUDIO_TONE_CONS_2)), + new HashSet<>()); + + ClassDependencyData stManagerDeps = + new ClassDependencyData(ST_MANAGER_PACKAGE + ".java", + ST_MANAGER_PACKAGE, new HashSet<>(List.of(PERM_SOURCE_PACKAGE)), false, + new HashSet<>(), new HashSet<>(List.of(AUDIO_CONS, AUDIO_TONE_CONS_1))); + + ClassDependencyData permManagerDeps = + new ClassDependencyData(PERM_MANAGER_PACKAGE + ".java", PERM_MANAGER_PACKAGE, + new HashSet<>(List.of(PERM_SOURCE_PACKAGE, PERM_DATA_PACKAGE)), false, + new HashSet<>(), new HashSet<>(List.of(CONST_OUTSIDE_SCOPE))); + + ClassDependencyData permSourceDeps = + new ClassDependencyData(PERM_SOURCE_PACKAGE + ".java", + PERM_SOURCE_PACKAGE, new HashSet<>(), false, + new HashSet<>(), new HashSet<>()); + + ClassDependencyData permSourceDataDeps = + new ClassDependencyData(PERM_DATA_PACKAGE + ".java", + PERM_DATA_PACKAGE, new HashSet<>(), false, + new HashSet<>(), new HashSet<>()); + + mClassDependencyData.addAll(List.of(audioConstantsDeps, audioToneConstantsDeps, + stManagerDeps, permManagerDeps, permSourceDeps, permSourceDataDeps)); + } + + @BeforeClass + public static void beforeAll(){ + mFileDependencyMap = buildActualDepsMap( + new DependencyMapper(mClassDependencyData, mJavaSourceData).buildDependencyMaps()); + } + + @Test + public void testFileDependencies() { + // Test for AUDIO_CONS_PATH + DependencyProto.FileDependency audioDepsActual = mFileDependencyMap.get(AUDIO_CONS_PATH); + assertNotNull(AUDIO_CONS_PATH + " not found in dependencyList", audioDepsActual); + // This file should have 0 dependencies. + validateDependencies(audioDepsActual, AUDIO_CONS_PATH, 0, new ArrayList<>()); + + // Test for AUDIO_TONE_CONS_PATH + DependencyProto.FileDependency audioToneDepsActual = + mFileDependencyMap.get(AUDIO_TONE_CONS_PATH); + assertNotNull(AUDIO_TONE_CONS_PATH + " not found in dependencyList", audioDepsActual); + // This file should have 0 dependencies. + validateDependencies(audioToneDepsActual, AUDIO_TONE_CONS_PATH, 0, new ArrayList<>()); + + // Test for ST_MANAGER_PATH + DependencyProto.FileDependency stManagerDepsActual = + mFileDependencyMap.get(ST_MANAGER_PATH); + assertNotNull(ST_MANAGER_PATH + " not found in dependencyList", audioDepsActual); + // This file should have 3 dependencies. + validateDependencies(stManagerDepsActual, ST_MANAGER_PATH, 3, + new ArrayList<>(List.of(AUDIO_CONS_PATH, AUDIO_TONE_CONS_PATH, PERM_SOURCE_PATH))); + + // Test for PERM_MANAGER_PATH + DependencyProto.FileDependency permManagerDepsActual = + mFileDependencyMap.get(PERM_MANAGER_PATH); + assertNotNull(PERM_MANAGER_PATH + " not found in dependencyList", audioDepsActual); + // This file should have 2 dependencies. + validateDependencies(permManagerDepsActual, PERM_MANAGER_PATH, 2, + new ArrayList<>(List.of(PERM_SOURCE_PATH, PERM_DATA_PATH))); + + // Test for PERM_SOURCE_PATH + DependencyProto.FileDependency permSourceDepsActual = + mFileDependencyMap.get(PERM_SOURCE_PATH); + assertNotNull(PERM_SOURCE_PATH + " not found in dependencyList", audioDepsActual); + // This file should have 0 dependencies. + validateDependencies(permSourceDepsActual, PERM_SOURCE_PATH, 0, new ArrayList<>()); + + // Test for PERM_DATA_PATH + DependencyProto.FileDependency permDataDepsActual = + mFileDependencyMap.get(PERM_DATA_PATH); + assertNotNull(PERM_DATA_PATH + " not found in dependencyList", audioDepsActual); + // This file should have 0 dependencies. + validateDependencies(permDataDepsActual, PERM_DATA_PATH, 0, new ArrayList<>()); + } + + private void validateDependencies(DependencyProto.FileDependency dependency, String fileName, int fileDepsCount, List<String> fileDeps) { + assertEquals(fileName + " does not have expected dependencies", fileDepsCount, dependency.getFileDependenciesCount()); + assertTrue(fileName + " does not have expected dependencies", dependency.getFileDependenciesList().containsAll(fileDeps)); + } + + private static Map<String, DependencyProto.FileDependency> buildActualDepsMap( + DependencyProto.FileDependencyList fileDependencyList) { + Map<String, DependencyProto.FileDependency> dependencyMap = new HashMap<>(); + for (DependencyProto.FileDependency fileDependency : fileDependencyList.getFileDependencyList()) { + if (fileDependency.getFilePath().equals(AUDIO_CONS_PATH)) { + dependencyMap.put(AUDIO_CONS_PATH, fileDependency); + } + if (fileDependency.getFilePath().equals(AUDIO_TONE_CONS_PATH)) { + dependencyMap.put(AUDIO_TONE_CONS_PATH, fileDependency); + } + if (fileDependency.getFilePath().equals(ST_MANAGER_PATH)) { + dependencyMap.put(ST_MANAGER_PATH, fileDependency); + } + if (fileDependency.getFilePath().equals(PERM_MANAGER_PATH)) { + dependencyMap.put(PERM_MANAGER_PATH, fileDependency); + } + if (fileDependency.getFilePath().equals(PERM_SOURCE_PATH)) { + dependencyMap.put(PERM_SOURCE_PATH, fileDependency); + } + if (fileDependency.getFilePath().equals(PERM_DATA_PATH)) { + dependencyMap.put(PERM_DATA_PATH, fileDependency); + } + if (fileDependency.getFilePath().equals(SOURCE_ANNO_PATH)) { + dependencyMap.put(SOURCE_ANNO_PATH, fileDependency); + } + } + assertFalse(SOURCE_ANNO_PATH + " found in dependencyList", + dependencyMap.containsKey(SOURCE_ANNO_PATH)); + return dependencyMap; + } +} diff --git a/tools/dependency_mapper/tests/src/com/android/dependencymapper/JavaSourceAnalyzerTest.java b/tools/dependency_mapper/tests/src/com/android/dependencymapper/JavaSourceAnalyzerTest.java new file mode 100644 index 0000000000..1ca2b2a899 --- /dev/null +++ b/tools/dependency_mapper/tests/src/com/android/dependencymapper/JavaSourceAnalyzerTest.java @@ -0,0 +1,71 @@ +/* + * 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 static org.junit.Assert.assertEquals; + +import org.junit.BeforeClass; +import org.junit.Test; + +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class JavaSourceAnalyzerTest { + private static List<JavaSourceData> mJavaSourceDataList; + + private static final String SOURCES_RSP_PATH = + "tests/res/testfiles/sources.rsp"; + + @BeforeClass + public static void beforeClass() throws URISyntaxException { + Path path = Paths.get(SOURCES_RSP_PATH); + // Perform source analysis. + mJavaSourceDataList = JavaSourceAnalyzer.analyze(path); + } + + @Test + public void validateSourceData() { + Map<String, String> expectedSourceData = expectedSourceData(); + int expectedFileCount = expectedSourceData.size(); + int actualFileCount = 0; + for (JavaSourceData javaSourceData : mJavaSourceDataList) { + String file = javaSourceData.getFilePath(); + if (expectedSourceData.containsKey(file)) { + actualFileCount++; + assertEquals("Source Data not generated correctly for " + file, + expectedSourceData.get(file), javaSourceData.getPackagePrependedFileName()); + } + } + assertEquals("Not all source files processed", expectedFileCount, actualFileCount); + } + + private Map<String, String> expectedSourceData() { + Map<String, String> expectedSourceData = new HashMap<>(); + expectedSourceData.put("tests/res/testdata/annotation/AnnotationUsage.java", + "res.testdata.annotation.AnnotationUsage.java"); + expectedSourceData.put("tests/res/testdata/constants/ConstantUsage.java", + "res.testdata.constants.ConstantUsage.java"); + expectedSourceData.put("tests/res/testdata/inheritance/BaseClass.java", + "res.testdata.inheritance.BaseClass.java"); + expectedSourceData.put("tests/res/testdata/methods/FieldUsage.java", + "res.testdata.methods.FieldUsage.java"); + return expectedSourceData; + } +} diff --git a/tools/dependency_mapper/tests/src/com/android/dependencymapper/UtilsTest.java b/tools/dependency_mapper/tests/src/com/android/dependencymapper/UtilsTest.java new file mode 100644 index 0000000000..39c5190b97 --- /dev/null +++ b/tools/dependency_mapper/tests/src/com/android/dependencymapper/UtilsTest.java @@ -0,0 +1,64 @@ +/* + * 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 org.junit.Test; + +import static org.junit.Assert.assertEquals; + +import com.android.dependencymapper.Utils; + +public class UtilsTest { + + @Test + public void testTrimAndConvertToPackageBasedPath() { + String testPath1 = "com/android/storage/StorageManager.class"; + String testPath2 = "com/android/package/PackageManager$Package.class"; + + String expectedPackageBasedPath1 = "com.android.storage.StorageManager"; + String expectedPackageBasedPath2 = "com.android.package.PackageManager$Package"; + + assertEquals("Package Based Path not constructed correctly", + expectedPackageBasedPath1, Utils.trimAndConvertToPackageBasedPath(testPath1)); + assertEquals("Package Based Path not constructed correctly", + expectedPackageBasedPath2, Utils.trimAndConvertToPackageBasedPath(testPath2)); + } + + @Test + public void testBuildPackagePrependedClassSource() { + String qualifiedClassPath1 = "com.android.storage.StorageManager"; + String sourcePath1 = "StorageManager.java"; + String qualifiedClassPath2 = "com.android.package.PackageManager$Package"; + String sourcePath2 = "PackageManager.java"; + String qualifiedClassPath3 = "com.android.storage.StorageManager$Storage"; + String sourcePath3 = "StorageManager$Storage.java"; + + + String expectedPackagePrependedPath1 = "com.android.storage.StorageManager.java"; + String expectedPackagePrependedPath2 = "com.android.package.PackageManager.java"; + String expectedPackagePrependedPath3 = "com.android.storage.StorageManager$Storage.java"; + + assertEquals("Package Prepended Class Source not constructed correctly", + expectedPackagePrependedPath1, + Utils.buildPackagePrependedClassSource(qualifiedClassPath1, sourcePath1)); + assertEquals("Package Prepended Class Source not constructed correctly", + expectedPackagePrependedPath2, + Utils.buildPackagePrependedClassSource(qualifiedClassPath2, sourcePath2)); + assertEquals("Package Prepended Class Source not constructed correctly", + expectedPackagePrependedPath3, + Utils.buildPackagePrependedClassSource(qualifiedClassPath3, sourcePath3)); + } +} |