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