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