1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tools.metalava.model.source 18 19 import com.android.tools.metalava.model.source.utils.DOT_JAVA 20 import com.android.tools.metalava.model.source.utils.DOT_KT 21 import com.android.tools.metalava.model.source.utils.OVERVIEW_HTML 22 import com.android.tools.metalava.model.source.utils.PACKAGE_HTML 23 import com.android.tools.metalava.model.source.utils.findPackage 24 import com.android.tools.metalava.reporter.Issues 25 import com.android.tools.metalava.reporter.Reporter 26 import java.io.File 27 import java.nio.file.Files 28 29 /** 30 * An abstraction of source files and root directories. 31 * 32 * Those are always paired together or computed from one another. 33 * 34 * @param sources the list of source files 35 * @param sourcePath a possibly empty list of root directories within which source files may be 36 * found. 37 */ 38 class SourceSet(val sources: List<File>, val sourcePath: List<File>) { 39 40 val absoluteSources: List<File> 41 get() { <lambda>null42 return sources.map { it.absoluteFile } 43 } 44 45 val absoluteSourcePaths: List<File> 46 get() { <lambda>null47 return sourcePath.filter { it.path.isNotBlank() }.map { it.absoluteFile } 48 } 49 50 /** Creates a copy of [SourceSet], but with elements mapped with [File.getAbsoluteFile] */ absoluteCopynull51 fun absoluteCopy(): SourceSet { 52 return SourceSet(absoluteSources, absoluteSourcePaths) 53 } 54 55 /** 56 * Creates a new instance of [SourceSet], adding in source roots implied by the source files in 57 * the current [SourceSet] 58 */ extractRootsnull59 fun extractRoots(reporter: Reporter): SourceSet { 60 val sourceRoots = extractRoots(reporter, sources, sourcePath.toMutableList()) 61 return SourceSet(sources, sourceRoots) 62 } 63 64 companion object { emptynull65 fun empty(): SourceSet = SourceSet(emptyList(), emptyList()) 66 67 /** * Creates [SourceSet] from the given [sourcePath] */ 68 fun createFromSourcePath( 69 reporter: Reporter, 70 sourcePath: List<File>, 71 fileTester: (File) -> Boolean = ::isSupportedSource, 72 ): SourceSet { 73 val sources = gatherSources(reporter, sourcePath, fileTester) 74 return SourceSet(sources, sourcePath) 75 } 76 skippableDirectorynull77 private fun skippableDirectory(file: File): Boolean = 78 file.path.endsWith(".git") && file.name == ".git" 79 80 private fun isSupportedSource(file: File): Boolean = 81 file.name.endsWith(DOT_JAVA) || 82 file.name.endsWith(DOT_KT) || 83 file.name.equals(PACKAGE_HTML) || 84 file.name.equals(OVERVIEW_HTML) 85 86 private fun addSourceFiles( 87 reporter: Reporter, 88 list: MutableList<File>, 89 file: File, 90 fileTester: (File) -> Boolean = ::isSupportedSource, 91 ) { 92 if (file.isDirectory) { 93 if (skippableDirectory(file)) { 94 return 95 } 96 if (Files.isSymbolicLink(file.toPath())) { 97 reporter.report( 98 Issues.IGNORING_SYMLINK, 99 file, 100 "Ignoring symlink during source file discovery directory traversal" 101 ) 102 return 103 } 104 val files = file.listFiles() 105 if (files != null) { 106 for (child in files) { 107 addSourceFiles(reporter, list, child) 108 } 109 } 110 } else if (file.isFile) { 111 if (fileTester.invoke(file)) { 112 list.add(file) 113 } 114 } 115 } 116 gatherSourcesnull117 private fun gatherSources( 118 reporter: Reporter, 119 sourcePath: List<File>, 120 fileTester: (File) -> Boolean = ::isSupportedSource, 121 ): List<File> { 122 val sources = mutableListOf<File>() 123 for (file in sourcePath) { 124 if (file.path.isBlank()) { 125 // --source-path "" means don't search source path; use "." for pwd 126 continue 127 } 128 addSourceFiles(reporter, sources, file.absoluteFile, fileTester) 129 } 130 return sources.sortedWith(compareBy { it.name }) 131 } 132 extractRootsnull133 private fun extractRoots( 134 reporter: Reporter, 135 sources: List<File>, 136 sourceRoots: MutableList<File> = mutableListOf() 137 ): List<File> { 138 // Cache for each directory since computing root for a source file is expensive 139 val dirToRootCache = mutableMapOf<String, File>() 140 for (file in sources) { 141 val parent = file.parentFile ?: continue 142 val found = dirToRootCache[parent.path] 143 if (found != null) { 144 continue 145 } 146 147 val root = findRoot(reporter, file) ?: continue 148 dirToRootCache[parent.path] = root 149 150 if (!sourceRoots.contains(root)) { 151 sourceRoots.add(root) 152 } 153 } 154 return sourceRoots 155 } 156 157 /** 158 * If given a full path to a Java or Kotlin source file, produces the path to the source 159 * root if possible. 160 */ findRootnull161 private fun findRoot(reporter: Reporter, file: File): File? { 162 val path = file.path 163 if (path.endsWith(DOT_JAVA) || path.endsWith(DOT_KT)) { 164 val pkg = findPackage(file) ?: return null 165 val parent = file.parentFile ?: return null 166 val endIndex = parent.path.length - pkg.length 167 val before = path[endIndex - 1] 168 if (before == '/' || before == '\\') { 169 return File(path.substring(0, endIndex)) 170 } else { 171 reporter.report( 172 Issues.IO_ERROR, 173 file, 174 "Unable to determine the package name. " + 175 "This usually means that a source file was where the directory does not seem to match the package " + 176 "declaration; we expected the path $path to end with /${pkg.replace('.', '/') + '/' + file.name}" 177 ) 178 } 179 } 180 return null 181 } 182 } 183 } 184