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