1 /* 2 * Copyright 2021 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.graphics.lint 20 21 import androidx.compose.lint.Names 22 import androidx.compose.lint.isInPackageName 23 import com.android.tools.lint.detector.api.Category 24 import com.android.tools.lint.detector.api.Detector 25 import com.android.tools.lint.detector.api.Implementation 26 import com.android.tools.lint.detector.api.Issue 27 import com.android.tools.lint.detector.api.JavaContext 28 import com.android.tools.lint.detector.api.LintFix 29 import com.android.tools.lint.detector.api.Scope 30 import com.android.tools.lint.detector.api.Severity 31 import com.android.tools.lint.detector.api.SourceCodeScanner 32 import com.intellij.psi.PsiMethod 33 import java.util.EnumSet 34 import org.jetbrains.kotlin.psi.KtConstantExpression 35 import org.jetbrains.uast.UCallExpression 36 import org.jetbrains.uast.ULiteralExpression 37 38 /** 39 * [Detector] that checks hex Color definitions to ensure that they provide values for all four 40 * (ARGB) channels. Providing only three channels (such as 0xFF0000) will result in an empty alpha 41 * channel, which is rarely intended - in cases where it is, it is typically more readable to just 42 * explicitly define the alpha channel anyway. 43 */ 44 class ColorDetector : Detector(), SourceCodeScanner { getApplicableMethodNamesnull45 override fun getApplicableMethodNames(): List<String> = listOf(Names.UiGraphics.Color.shortName) 46 47 override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { 48 if (method.isInPackageName(Names.UiGraphics.PackageName)) { 49 // Ignore other Color functions that have separate parameters for separate channels 50 if (node.valueArgumentCount == 1) { 51 val argument = node.valueArguments.first() 52 // Ignore non-literal expressions 53 if (argument !is ULiteralExpression) return 54 val argumentText = (argument.sourcePsi as? KtConstantExpression)?.text ?: return 55 val hexPrefix = "0x" 56 val hexIndex = argumentText.indexOf(hexPrefix, ignoreCase = true) 57 // Ignore if this isn't a hex value 58 if (hexIndex != 0) return 59 val hexArgument = argumentText.substring(hexIndex + hexPrefix.length) 60 // The length of the actual hex value (without separators and suffix) should be 8 61 val hexLength = 62 hexArgument 63 // Trim any underscores that might be used to separate values 64 .replace("_", "") 65 // Remove the suffix `L` if present 66 .replace("L", "") 67 .length 68 when (hexLength) { 69 // Expected length is 8: four 8-bit channels, e.g FF000000 70 8 -> return 71 // In the specific (and common) case of missing the alpha channel, i.e 72 // FF0000, we can suggest a quick fix to add a value of `FF` for the alpha, 73 // and a more specific lint warning. 74 6 -> { 75 // Try to be consistent with how the hex value is currently defined - if 76 // there are any lower case characters, suggest to add a lower case 77 // channel. Otherwise use upper case as the default. 78 val isHexValueLowerCase = 79 hexArgument.firstOrNull { !it.isDigit() }?.isLowerCase() == true 80 81 val alphaChannel = if (isHexValueLowerCase) "ff" else "FF" 82 val replacement = hexPrefix + alphaChannel + hexArgument 83 84 context.report( 85 MissingColorAlphaChannel, 86 node, 87 context.getLocation(argument), 88 "Missing Color alpha channel", 89 LintFix.create() 90 .replace() 91 .name("Add `$alphaChannel` alpha channel") 92 .text(argumentText) 93 .with(replacement) 94 .autoFix() 95 .build() 96 ) 97 } 98 // Otherwise report a generic warning for an valid value - there is no quick fix 99 // we can really provide here. 100 else -> { 101 context.report( 102 InvalidColorHexValue, 103 node, 104 context.getLocation(argument), 105 "Invalid Color hex value", 106 ) 107 } 108 } 109 } 110 } 111 } 112 113 companion object { 114 val MissingColorAlphaChannel = 115 Issue.create( 116 "MissingColorAlphaChannel", 117 "Missing Color alpha channel", 118 "Creating a Color with a hex value requires a 32 bit value " + 119 "(such as 0xFF000000), with 8 bits being used per channel (ARGB). Not passing a " + 120 "full 32 bit value will result in channels being undefined. For example, passing " + 121 "0xFF0000 will result in a missing alpha channel, so the color will not appear " + 122 "visible.", 123 Category.CORRECTNESS, 124 3, 125 Severity.WARNING, 126 Implementation( 127 ColorDetector::class.java, 128 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) 129 ) 130 ) 131 132 val InvalidColorHexValue = 133 Issue.create( 134 "InvalidColorHexValue", 135 "Invalid Color hex value", 136 "Creating a Color with a hex value requires a 32 bit value " + 137 "(such as 0xFF000000), with 8 bits being used per channel (ARGB). Not passing a " + 138 "full 32 bit value will result in channels being undefined / incorrect.", 139 Category.CORRECTNESS, 140 3, 141 Severity.WARNING, 142 Implementation( 143 ColorDetector::class.java, 144 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) 145 ) 146 ) 147 } 148 } 149