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.Issues 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.synthetic) { 122 // Don't validate items which don't exist in source such as an enum's valueOf(String) 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(Issues.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(Issues.INVALID_NULLABILITY_ANNOTATION_WARNING, it.method, "Nullability issue: $it") 221 } 222 } 223 } 224 } 225