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