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