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.MetalavaCliException 20 import com.android.tools.metalava.model.AnnotationItem 21 import com.android.tools.metalava.model.ArrayTypeItem 22 import com.android.tools.metalava.model.Codebase 23 import com.android.tools.metalava.model.Item 24 import com.android.tools.metalava.model.MethodItem 25 import com.android.tools.metalava.model.ParameterItem 26 import com.android.tools.metalava.model.PrimitiveTypeItem 27 import com.android.tools.metalava.model.SUPPORT_TYPE_USE_ANNOTATIONS 28 import com.android.tools.metalava.model.TypeItem 29 import com.android.tools.metalava.model.VariableTypeItem 30 import com.android.tools.metalava.model.visitors.ApiVisitor 31 import com.android.tools.metalava.reporter.Issues 32 import com.android.tools.metalava.reporter.Reporter 33 import java.io.File 34 import java.io.PrintWriter 35 36 private const val RETURN_LABEL = "return value" 37 38 /** Class that validates nullability annotations in the codebase. */ 39 class NullabilityAnnotationsValidator( 40 private val reporter: Reporter, 41 private val nullabilityErrorsFatal: Boolean, 42 private val nullabilityWarningsTxt: File?, 43 ) { 44 45 private enum class ErrorType { 46 MULTIPLE, 47 ON_PRIMITIVE, 48 BAD_TYPE_PARAM, 49 } 50 51 private interface Issue { 52 val method: MethodItem 53 } 54 55 private data class Error( 56 override val method: MethodItem, 57 val label: String, 58 val type: ErrorType 59 ) : Issue { 60 override fun toString(): String { 61 return "ERROR: $method, $label, $type" 62 } 63 } 64 65 private enum class WarningType { 66 MISSING, 67 } 68 69 private data class Warning( 70 override val method: MethodItem, 71 val label: String, 72 val type: WarningType 73 ) : Issue { 74 override fun toString(): String { 75 return "WARNING: $method, $label, $type" 76 } 77 } 78 79 private val errors: MutableList<Error> = mutableListOf() 80 private val warnings: MutableList<Warning> = mutableListOf() 81 82 /** 83 * Validate all of the methods in the classes named in [topLevelClassNames] and in all their 84 * nested classes. Violations are stored by the validator and will be reported by [report]. 85 */ 86 fun validateAll(codebase: Codebase, topLevelClassNames: List<String>) { 87 for (topLevelClassName in topLevelClassNames) { 88 val topLevelClass = 89 codebase.findClass(topLevelClassName) 90 ?: throw MetalavaCliException( 91 "Trying to validate nullability annotations for class $topLevelClassName which could not be found in main codebase" 92 ) 93 // Visit methods to check their return type, and parameters to check them. Don't visit 94 // constructors as we don't want to check their return types. This visits members of 95 // inner classes as well. 96 topLevelClass.accept( 97 object : 98 ApiVisitor( 99 visitConstructorsAsMethods = false, 100 config = @Suppress("DEPRECATION") options.apiVisitorConfig, 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.containingMethod(), 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(method: MethodItem, label: String, type: TypeItem?, item: Item) { 138 if (type == null) { 139 throw MetalavaCliException("Missing type on $method 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(method, label, ErrorType.MULTIPLE)) 145 return 146 } 147 checkItemNullability(type, nullabilityAnnotations.firstOrNull(), method, 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") == true 155 156 private fun isAnyNullabilityAnnotation(it: AnnotationItem) = 157 it.isNullnessAnnotation() || isNullFromTypeParam(it) 158 159 private fun checkItemNullability( 160 type: TypeItem, 161 nullability: AnnotationItem?, 162 method: MethodItem, 163 label: String 164 ) { 165 when { 166 // Primitive (may not have nullability): 167 type is PrimitiveTypeItem -> { 168 if (nullability != null) { 169 errors.add(Error(method, 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 method parameter declared as '@NonNull Object[]' means 177 // 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(method, 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 -> warnings.add(Warning(method, label, WarningType.MISSING)) 192 isNullFromTypeParam(nullability) -> 193 errors.add(Error(method, label, ErrorType.BAD_TYPE_PARAM)) 194 } 195 } 196 } 197 } 198 199 /** Report on any violations found during earlier validation calls. */ 200 fun report() { 201 errors.sortBy { it.toString() } 202 warnings.sortBy { it.toString() } 203 val warningsTxtFile = nullabilityWarningsTxt 204 val fatalIssues = mutableListOf<Issue>() 205 val nonFatalIssues = mutableListOf<Issue>() 206 207 // Errors are fatal iff nullabilityErrorsFatal is set. 208 if (nullabilityErrorsFatal) { 209 fatalIssues.addAll(errors) 210 } else { 211 nonFatalIssues.addAll(errors) 212 } 213 214 // Warnings go to the configured .txt file if present, which means they're not fatal. 215 // Else they're fatal iff nullabilityErrorsFatal is set. 216 if (warningsTxtFile == null && nullabilityErrorsFatal) { 217 fatalIssues.addAll(warnings) 218 } else { 219 nonFatalIssues.addAll(warnings) 220 } 221 222 // Fatal issues are thrown. 223 if (fatalIssues.isNotEmpty()) { 224 fatalIssues.forEach { 225 reporter.report(Issues.INVALID_NULLABILITY_ANNOTATION, it.method, it.toString()) 226 } 227 } 228 229 // Non-fatal issues are written to the warnings .txt file if present, else logged. 230 if (warningsTxtFile != null) { 231 PrintWriter(warningsTxtFile.bufferedWriter()).use { w -> 232 nonFatalIssues.forEach { w.println(it) } 233 } 234 } else { 235 nonFatalIssues.forEach { 236 reporter.report( 237 Issues.INVALID_NULLABILITY_ANNOTATION_WARNING, 238 it.method, 239 "Nullability issue: $it" 240 ) 241 } 242 } 243 } 244 } 245