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