1 /*
<lambda>null2  * Copyright (C) 2018 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
18 
19 import com.android.tools.metalava.cli.common.MetalavaCliException
20 import com.android.tools.metalava.model.AnnotationItem
21 import com.android.tools.metalava.model.ArrayTypeItem
22 import com.android.tools.metalava.model.Codebase
23 import com.android.tools.metalava.model.Item
24 import com.android.tools.metalava.model.MethodItem
25 import com.android.tools.metalava.model.ParameterItem
26 import com.android.tools.metalava.model.PrimitiveTypeItem
27 import com.android.tools.metalava.model.SUPPORT_TYPE_USE_ANNOTATIONS
28 import com.android.tools.metalava.model.TypeItem
29 import com.android.tools.metalava.model.VariableTypeItem
30 import com.android.tools.metalava.model.visitors.ApiVisitor
31 import com.android.tools.metalava.reporter.Issues
32 import com.android.tools.metalava.reporter.Reporter
33 import java.io.File
34 import java.io.PrintWriter
35 
36 private const val RETURN_LABEL = "return value"
37 
38 /** Class that validates nullability annotations in the codebase. */
39 class NullabilityAnnotationsValidator(
40     private val reporter: Reporter,
41     private val nullabilityErrorsFatal: Boolean,
42     private val nullabilityWarningsTxt: File?,
43 ) {
44 
45     private enum class ErrorType {
46         MULTIPLE,
47         ON_PRIMITIVE,
48         BAD_TYPE_PARAM,
49     }
50 
51     private interface Issue {
52         val method: MethodItem
53     }
54 
55     private data class Error(
56         override val method: MethodItem,
57         val label: String,
58         val type: ErrorType
59     ) : Issue {
60         override fun toString(): String {
61             return "ERROR: $method, $label, $type"
62         }
63     }
64 
65     private enum class WarningType {
66         MISSING,
67     }
68 
69     private data class Warning(
70         override val method: MethodItem,
71         val label: String,
72         val type: WarningType
73     ) : Issue {
74         override fun toString(): String {
75             return "WARNING: $method, $label, $type"
76         }
77     }
78 
79     private val errors: MutableList<Error> = mutableListOf()
80     private val warnings: MutableList<Warning> = mutableListOf()
81 
82     /**
83      * Validate all of the methods in the classes named in [topLevelClassNames] and in all their
84      * nested classes. Violations are stored by the validator and will be reported by [report].
85      */
86     fun validateAll(codebase: Codebase, topLevelClassNames: List<String>) {
87         for (topLevelClassName in topLevelClassNames) {
88             val topLevelClass =
89                 codebase.findClass(topLevelClassName)
90                     ?: throw MetalavaCliException(
91                         "Trying to validate nullability annotations for class $topLevelClassName which could not be found in main codebase"
92                     )
93             // Visit methods to check their return type, and parameters to check them. Don't visit
94             // constructors as we don't want to check their return types. This visits members of
95             // inner classes as well.
96             topLevelClass.accept(
97                 object :
98                     ApiVisitor(
99                         visitConstructorsAsMethods = false,
100                         config = @Suppress("DEPRECATION") options.apiVisitorConfig,
101                     ) {
102 
103                     override fun visitMethod(method: MethodItem) {
104                         checkItem(method, RETURN_LABEL, method.returnType(), method)
105                     }
106 
107                     override fun visitParameter(parameter: ParameterItem) {
108                         checkItem(
109                             parameter.containingMethod(),
110                             parameter.toString(),
111                             parameter.type(),
112                             parameter
113                         )
114                     }
115                 }
116             )
117         }
118     }
119 
120     /**
121      * As [validateAll], reading the list of class names from [topLevelClassesList]. The file names
122      * one top-level class per line, and lines starting with # are skipped. Does nothing if
123      * [topLevelClassesList] is null.
124      */
125     fun validateAllFrom(codebase: Codebase, topLevelClassesList: File?) {
126         if (topLevelClassesList != null) {
127             val classes =
128                 topLevelClassesList
129                     .readLines()
130                     .filterNot { it.isBlank() }
131                     .map { it.trim() }
132                     .filterNot { it.startsWith("#") }
133             validateAll(codebase, classes)
134         }
135     }
136 
137     private fun checkItem(method: MethodItem, label: String, type: TypeItem?, item: Item) {
138         if (type == null) {
139             throw MetalavaCliException("Missing type on $method item $label")
140         }
141         val annotations = item.modifiers.annotations()
142         val nullabilityAnnotations = annotations.filter(this::isAnyNullabilityAnnotation)
143         if (nullabilityAnnotations.size > 1) {
144             errors.add(Error(method, label, ErrorType.MULTIPLE))
145             return
146         }
147         checkItemNullability(type, nullabilityAnnotations.firstOrNull(), method, label)
148         // TODO: When type annotations are supported, we should check all the type parameters too.
149         // We can do invoke this method recursively, using a suitably descriptive label.
150         assert(!SUPPORT_TYPE_USE_ANNOTATIONS)
151     }
152 
153     private fun isNullFromTypeParam(it: AnnotationItem) =
154         it.qualifiedName?.endsWith("NullFromTypeParam") == true
155 
156     private fun isAnyNullabilityAnnotation(it: AnnotationItem) =
157         it.isNullnessAnnotation() || isNullFromTypeParam(it)
158 
159     private fun checkItemNullability(
160         type: TypeItem,
161         nullability: AnnotationItem?,
162         method: MethodItem,
163         label: String
164     ) {
165         when {
166             // Primitive (may not have nullability):
167             type is PrimitiveTypeItem -> {
168                 if (nullability != null) {
169                     errors.add(Error(method, label, ErrorType.ON_PRIMITIVE))
170                 }
171             }
172             // Array (see comment):
173             type is ArrayTypeItem -> {
174                 // TODO: When type annotations are supported, we should check the annotation on both
175                 // the array itself and the component type. Until then, there's nothing we can
176                 // safely do, because e.g. a method parameter declared as '@NonNull Object[]' means
177                 // a non-null array of unspecified-nullability Objects if that is a PARAMETER
178                 // annotation, but an unspecified-nullability array of non-null Objects if that is a
179                 // TYPE_USE annotation.
180                 assert(!SUPPORT_TYPE_USE_ANNOTATIONS)
181             }
182             // Type parameter reference (should have nullability):
183             type is VariableTypeItem -> {
184                 if (nullability == null) {
185                     warnings.add(Warning(method, label, WarningType.MISSING))
186                 }
187             }
188             // Anything else (should have nullability, may not be null-from-type-param):
189             else -> {
190                 when {
191                     nullability == null -> warnings.add(Warning(method, label, WarningType.MISSING))
192                     isNullFromTypeParam(nullability) ->
193                         errors.add(Error(method, label, ErrorType.BAD_TYPE_PARAM))
194                 }
195             }
196         }
197     }
198 
199     /** Report on any violations found during earlier validation calls. */
200     fun report() {
201         errors.sortBy { it.toString() }
202         warnings.sortBy { it.toString() }
203         val warningsTxtFile = nullabilityWarningsTxt
204         val fatalIssues = mutableListOf<Issue>()
205         val nonFatalIssues = mutableListOf<Issue>()
206 
207         // Errors are fatal iff nullabilityErrorsFatal is set.
208         if (nullabilityErrorsFatal) {
209             fatalIssues.addAll(errors)
210         } else {
211             nonFatalIssues.addAll(errors)
212         }
213 
214         // Warnings go to the configured .txt file if present, which means they're not fatal.
215         // Else they're fatal iff nullabilityErrorsFatal is set.
216         if (warningsTxtFile == null && nullabilityErrorsFatal) {
217             fatalIssues.addAll(warnings)
218         } else {
219             nonFatalIssues.addAll(warnings)
220         }
221 
222         // Fatal issues are thrown.
223         if (fatalIssues.isNotEmpty()) {
224             fatalIssues.forEach {
225                 reporter.report(Issues.INVALID_NULLABILITY_ANNOTATION, it.method, it.toString())
226             }
227         }
228 
229         // Non-fatal issues are written to the warnings .txt file if present, else logged.
230         if (warningsTxtFile != null) {
231             PrintWriter(warningsTxtFile.bufferedWriter()).use { w ->
232                 nonFatalIssues.forEach { w.println(it) }
233             }
234         } else {
235             nonFatalIssues.forEach {
236                 reporter.report(
237                     Issues.INVALID_NULLABILITY_ANNOTATION_WARNING,
238                     it.method,
239                     "Nullability issue: $it"
240                 )
241             }
242         }
243     }
244 }
245