• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.cliError
20 import com.android.tools.metalava.model.AnnotationItem
21 import com.android.tools.metalava.model.ArrayTypeItem
22 import com.android.tools.metalava.model.CallableItem
23 import com.android.tools.metalava.model.Codebase
24 import com.android.tools.metalava.model.Item
25 import com.android.tools.metalava.model.MethodItem
26 import com.android.tools.metalava.model.ParameterItem
27 import com.android.tools.metalava.model.PrimitiveTypeItem
28 import com.android.tools.metalava.model.SUPPORT_TYPE_USE_ANNOTATIONS
29 import com.android.tools.metalava.model.TypeItem
30 import com.android.tools.metalava.model.VariableTypeItem
31 import com.android.tools.metalava.model.visitors.ApiVisitor
32 import com.android.tools.metalava.reporter.Issues
33 import com.android.tools.metalava.reporter.Reporter
34 import java.io.File
35 import java.io.PrintWriter
36 
37 private const val RETURN_LABEL = "return value"
38 
39 /** Class that validates nullability annotations in the codebase. */
40 class NullabilityAnnotationsValidator(
41     private val reporter: Reporter,
42     private val nullabilityErrorsFatal: Boolean,
43     private val nullabilityWarningsTxt: File?,
44 ) {
45 
46     private enum class ErrorType {
47         MULTIPLE,
48         ON_PRIMITIVE,
49         BAD_TYPE_PARAM,
50     }
51 
52     private interface Issue {
53         val callable: CallableItem
54     }
55 
56     private data class Error(
57         override val callable: CallableItem,
58         val label: String,
59         val type: ErrorType
60     ) : Issue {
61         override fun toString(): String {
62             return "ERROR: $callable, $label, $type"
63         }
64     }
65 
66     private enum class WarningType {
67         MISSING,
68     }
69 
70     private data class Warning(
71         override val callable: CallableItem,
72         val label: String,
73         val type: WarningType
74     ) : Issue {
75         override fun toString(): String {
76             return "WARNING: $callable, $label, $type"
77         }
78     }
79 
80     private val errors: MutableList<Error> = mutableListOf()
81     private val warnings: MutableList<Warning> = mutableListOf()
82 
83     /**
84      * Validate all the methods in the classes named in [topLevelClassNames] and in all their nested
85      * classes. Violations are stored by the validator and will be reported by [report].
86      */
87     fun validateAll(codebase: Codebase, topLevelClassNames: List<String>) {
88         for (topLevelClassName in topLevelClassNames) {
89             val topLevelClass =
90                 codebase.findClass(topLevelClassName)
91                     ?: cliError(
92                         "Trying to validate nullability annotations for class $topLevelClassName which could not be found in main codebase"
93                     )
94             // Visit methods to check their return type, and parameters to check them. Don't visit
95             // constructors as we don't want to check their return types. This visits members of
96             // nested classes as well.
97             topLevelClass.accept(
98                 object :
99                     ApiVisitor(
100                         apiPredicateConfig = @Suppress("DEPRECATION") options.apiPredicateConfig,
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.containingCallable(),
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(callable: CallableItem, label: String, type: TypeItem?, item: Item) {
138         if (type == null) {
139             cliError("Missing type on $callable 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(callable, label, ErrorType.MULTIPLE))
145             return
146         }
147         checkItemNullability(type, nullabilityAnnotations.firstOrNull(), callable, 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")
155 
156     private fun isAnyNullabilityAnnotation(it: AnnotationItem) =
157         it.isNullnessAnnotation() || isNullFromTypeParam(it)
158 
159     private fun checkItemNullability(
160         type: TypeItem,
161         nullability: AnnotationItem?,
162         callable: CallableItem,
163         label: String
164     ) {
165         when {
166             // Primitive (may not have nullability):
167             type is PrimitiveTypeItem -> {
168                 if (nullability != null) {
169                     errors.add(Error(callable, 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 callable parameter declared as '@NonNull Object[]'
177                 // means 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(callable, 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 ->
192                         warnings.add(Warning(callable, label, WarningType.MISSING))
193                     isNullFromTypeParam(nullability) ->
194                         errors.add(Error(callable, label, ErrorType.BAD_TYPE_PARAM))
195                 }
196             }
197         }
198     }
199 
200     /** Report on any violations found during earlier validation calls. */
201     fun report() {
202         errors.sortBy { it.toString() }
203         warnings.sortBy { it.toString() }
204         val warningsTxtFile = nullabilityWarningsTxt
205         val fatalIssues = mutableListOf<Issue>()
206         val nonFatalIssues = mutableListOf<Issue>()
207 
208         // Errors are fatal iff nullabilityErrorsFatal is set.
209         if (nullabilityErrorsFatal) {
210             fatalIssues.addAll(errors)
211         } else {
212             nonFatalIssues.addAll(errors)
213         }
214 
215         // Warnings go to the configured .txt file if present, which means they're not fatal.
216         // Else they're fatal iff nullabilityErrorsFatal is set.
217         if (warningsTxtFile == null && nullabilityErrorsFatal) {
218             fatalIssues.addAll(warnings)
219         } else {
220             nonFatalIssues.addAll(warnings)
221         }
222 
223         // Fatal issues are thrown.
224         if (fatalIssues.isNotEmpty()) {
225             fatalIssues.forEach {
226                 reporter.report(Issues.INVALID_NULLABILITY_ANNOTATION, it.callable, it.toString())
227             }
228         }
229 
230         // Non-fatal issues are written to the warnings .txt file if present, else logged.
231         if (warningsTxtFile != null) {
232             PrintWriter(warningsTxtFile.bufferedWriter()).use { w ->
233                 nonFatalIssues.forEach { w.println(it) }
234             }
235         } else {
236             nonFatalIssues.forEach {
237                 reporter.report(
238                     Issues.INVALID_NULLABILITY_ANNOTATION_WARNING,
239                     it.callable,
240                     "Nullability issue: $it"
241                 )
242             }
243         }
244     }
245 }
246