1 /* <lambda>null2 * Copyright 2020 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 @file:Suppress("UnstableApiUsage") 18 19 package androidx.compose.ui.lint 20 21 import androidx.compose.lint.Names 22 import androidx.compose.lint.inheritsFrom 23 import androidx.compose.lint.isComposable 24 import androidx.compose.lint.returnsUnit 25 import com.android.tools.lint.client.api.UElementHandler 26 import com.android.tools.lint.detector.api.Category 27 import com.android.tools.lint.detector.api.Detector 28 import com.android.tools.lint.detector.api.Implementation 29 import com.android.tools.lint.detector.api.Issue 30 import com.android.tools.lint.detector.api.JavaContext 31 import com.android.tools.lint.detector.api.LintFix 32 import com.android.tools.lint.detector.api.Scope 33 import com.android.tools.lint.detector.api.Severity 34 import com.android.tools.lint.detector.api.SourceCodeScanner 35 import java.util.EnumSet 36 import java.util.Locale 37 import org.jetbrains.kotlin.psi.KtNameReferenceExpression 38 import org.jetbrains.kotlin.psi.KtParameter 39 import org.jetbrains.uast.UElement 40 import org.jetbrains.uast.UMethod 41 42 /** 43 * [Detector] that checks Composable functions with Modifiers parameters for consistency with 44 * guidelines. 45 * 46 * For functions with one / more modifier parameters, the first modifier parameter must: 47 * - Be named `modifier` 48 * - Have a type of `Modifier` 49 * - Either have no default value, or have a default value of `Modifier` 50 * - If optional, be the first optional parameter in the parameter list 51 */ 52 class ModifierParameterDetector : Detector(), SourceCodeScanner { 53 override fun getApplicableUastTypes() = listOf(UMethod::class.java) 54 55 override fun createUastHandler(context: JavaContext) = 56 object : UElementHandler() { 57 override fun visitMethod(node: UMethod) { 58 // Ignore non-composable functions 59 if (!node.isComposable) return 60 61 // Ignore non-unit composable functions 62 if (!node.returnsUnit) return 63 64 val modifierParameter = 65 node.uastParameters.firstOrNull { parameter -> 66 parameter.sourcePsi is KtParameter && 67 parameter.type.inheritsFrom(Names.Ui.Modifier) 68 } ?: return 69 70 // Need to strongly type this or else Kotlinc cannot resolve overloads for 71 // getNameLocation 72 val modifierParameterElement: UElement = modifierParameter 73 74 val source = modifierParameter.sourcePsi as KtParameter 75 76 val modifierName = Names.Ui.Modifier.shortName 77 78 if (modifierParameter.name != ModifierParameterName) { 79 context.report( 80 ModifierParameter, 81 modifierParameterElement, 82 context.getNameLocation(modifierParameterElement), 83 "$modifierName parameter should be named $ModifierParameterName", 84 LintFix.create() 85 .replace() 86 .name("Change name to $ModifierParameterName") 87 .text(modifierParameter.name) 88 .with(ModifierParameterName) 89 .autoFix() 90 .build() 91 ) 92 } 93 94 if (modifierParameter.type.canonicalText != Names.Ui.Modifier.javaFqn) { 95 context.report( 96 ModifierParameter, 97 modifierParameterElement, 98 context.getNameLocation(modifierParameterElement), 99 "$modifierName parameter should have a type of $modifierName", 100 LintFix.create() 101 .replace() 102 .range(context.getLocation(modifierParameterElement)) 103 .name("Change type to $modifierName") 104 .text(source.typeReference!!.text) 105 .with(modifierName) 106 .autoFix() 107 .build() 108 ) 109 } 110 111 if (source.hasDefaultValue()) { 112 val defaultValue = source.defaultValue!! 113 // If the default value is not a reference expression, then it isn't `Modifier` 114 // anyway and we can just report an error 115 val referenceExpression = source.defaultValue as? KtNameReferenceExpression 116 if (referenceExpression?.getReferencedName() != modifierName) { 117 context.report( 118 ModifierParameter, 119 modifierParameterElement, 120 context.getNameLocation(modifierParameterElement), 121 "Optional $modifierName parameter should have a default value " + 122 "of `$modifierName`", 123 LintFix.create() 124 .replace() 125 .range(context.getLocation(modifierParameterElement)) 126 .name("Change default value to $modifierName") 127 .text(defaultValue.text) 128 .with(modifierName) 129 .autoFix() 130 .build() 131 ) 132 } 133 val index = node.uastParameters.indexOf(modifierParameter) 134 val optionalParameterIndex = 135 node.uastParameters.indexOfFirst { parameter -> 136 (parameter.sourcePsi as? KtParameter)?.hasDefaultValue() == true 137 } 138 if (index != optionalParameterIndex) { 139 context.report( 140 ModifierParameter, 141 modifierParameterElement, 142 context.getNameLocation(modifierParameterElement), 143 "$modifierName parameter should be the first optional parameter", 144 // Hard to make a lint fix for this and keep parameter formatting, so 145 // ignore it 146 ) 147 } 148 } 149 } 150 } 151 152 companion object { 153 val ModifierParameter = 154 Issue.create( 155 "ModifierParameter", 156 "Guidelines for Modifier parameters in a Composable function", 157 "The first (or only) Modifier parameter in a Composable function should follow the " + 158 "following rules:" + 159 "\n- Be named `$ModifierParameterName`" + 160 "\n- Have a type of `${Names.Ui.Modifier.shortName}`" + 161 "\n- Either have no default value, or have a default value of " + 162 "`${Names.Ui.Modifier.shortName}`" + 163 "\n- If optional, be the first optional parameter in the parameter list", 164 Category.CORRECTNESS, 165 3, 166 Severity.WARNING, 167 Implementation( 168 ModifierParameterDetector::class.java, 169 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) 170 ) 171 ) 172 } 173 } 174 175 private val ModifierParameterName = <lambda>null176 Names.Ui.Modifier.shortName.replaceFirstChar { it.lowercase(Locale.ROOT) } 177