1 /*
2  * Copyright (C) 2017 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
18 
19 import java.util.TreeSet
20 import java.util.function.Predicate
21 
22 /** Represents a Kotlin/Java source file */
23 interface SourceFile {
24     /** Top level classes contained in this file */
classesnull25     fun classes(): Sequence<ClassItem>
26 
27     fun getHeaderComments(): String? = null
28 
29     fun getImports(predicate: Predicate<Item>): Collection<Import> = emptyList()
30 
31     /**
32      * Compute set of import statements that are actually referenced from the documentation (we do
33      * inexact matching here; we don't need to have an exact set of imports since it's okay to have
34      * some extras). This isn't a big problem since our code style forbids/discourages wildcards, so
35      * it shows up in fewer places, but we need to handle it when it does -- such as in ojluni.
36      */
37     fun filterImports(imports: TreeSet<Import>, predicate: Predicate<Item>): TreeSet<Import> {
38         // Create a map from the short name for the import to a list of the items imported. A
39         // list is needed because classes and members could be imported with the same short
40         // name.
41         val remainingImports = mutableMapOf<String, MutableList<Import>>()
42         imports.groupByTo(remainingImports) { it.name }
43 
44         val result = TreeSet<Import>(compareBy { it.pattern })
45 
46         // We keep the wildcard imports since we don't know which ones of those are relevant
47         imports.filter { it.name == "*" }.forEach { result.add(it) }
48 
49         for (cls in classes().filter { predicate.test(it) }) {
50             cls.accept(
51                 object : TraversingVisitor() {
52                     override fun visitItem(item: Item): TraversalAction {
53                         // Do not let documentation on hidden items affect the imports.
54                         if (!predicate.test(item)) {
55                             // Just because an item like a class is hidden does not mean
56                             // that its child items are so make sure to visit them.
57                             return TraversalAction.CONTINUE
58                         }
59                         val doc = item.documentation
60                         if (doc.isNotBlank()) {
61                             // Scan the documentation text to see if it contains any of the
62                             // short names imported. It does not check whether the names
63                             // are actually used as part of a link, so they could just be in
64                             // as text but having extra imports should not be an issue.
65                             var found: MutableList<String>? = null
66                             for (name in remainingImports.keys) {
67                                 if (docContainsWord(doc, name)) {
68                                     if (found == null) {
69                                         found = mutableListOf()
70                                     }
71                                     found.add(name)
72                                 }
73                             }
74 
75                             // For every imported name add all the matching imports and then
76                             // remove them from the available imports as there is no need to
77                             // check them again.
78                             found?.let {
79                                 for (name in found) {
80                                     val all = remainingImports.remove(name) ?: continue
81                                     result.addAll(all)
82                                 }
83 
84                                 if (remainingImports.isEmpty()) {
85                                     // There is nothing to do if the map of imports to add
86                                     // is empty.
87                                     return TraversalAction.SKIP_TRAVERSAL
88                                 }
89                             }
90                         }
91 
92                         return TraversalAction.CONTINUE
93                     }
94                 }
95             )
96         }
97         return result
98     }
99 
docContainsWordnull100     fun docContainsWord(doc: String, word: String): Boolean {
101         // Cache pattern compilation across source files
102         val regexMap = HashMap<String, Regex>()
103 
104         if (!doc.contains(word)) {
105             return false
106         }
107 
108         val regex =
109             regexMap[word]
110                 ?: run {
111                     val new = Regex("""\b$word\b""")
112                     regexMap[word] = new
113                     new
114                 }
115         return regex.find(doc) != null
116     }
117 }
118 
119 /** Encapsulates information about the imports used in a [SourceFile]. */
120 data class Import
121 internal constructor(
122     /**
123      * The import pattern, i.e. the whole part of the import statement after `import static? ` and
124      * before the optional `;`, excluding any whitespace.
125      */
126     val pattern: String,
127 
128     /**
129      * The name that is being imported, i.e. the part after the last `.`. Is `*` for wildcard
130      * imports.
131      */
132     val name: String,
133 
134     /**
135      * True if the item that is being imported is a member of a class. Corresponds to the `static`
136      * keyword in Java, has no effect on Kotlin import statements.
137      */
138     val isMember: Boolean,
139 ) {
140     /** Import a whole [PackageItem], i.e. uses a wildcard. */
141     constructor(pkgItem: PackageItem) : this("${pkgItem.qualifiedName()}.*", "*", false)
142 
143     /** Import a [ClassItem]. */
144     constructor(
145         classItem: ClassItem
146     ) : this(
147         classItem.qualifiedName(),
148         classItem.simpleName(),
149         false,
150     )
151 
152     /** Import a [MemberItem]. */
153     constructor(
154         memberItem: MemberItem
155     ) : this(
156         "${memberItem.containingClass().qualifiedName()}.${memberItem.name()}",
157         memberItem.name(),
158         true,
159     )
160 }
161