Skip to content

Commit 464f7a6

Browse files
committed
Add new lint detector
1 parent 9c4d4a6 commit 464f7a6

File tree

2 files changed

+589
-0
lines changed

2 files changed

+589
-0
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
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.duckduckgo.lint.ui
18+
19+
import com.android.tools.lint.client.api.UElementHandler
20+
import com.android.tools.lint.detector.api.Category.Companion.CUSTOM_LINT_CHECKS
21+
import com.android.tools.lint.detector.api.Detector
22+
import com.android.tools.lint.detector.api.Implementation
23+
import com.android.tools.lint.detector.api.Issue
24+
import com.android.tools.lint.detector.api.JavaContext
25+
import com.android.tools.lint.detector.api.Scope
26+
import com.android.tools.lint.detector.api.Severity
27+
import com.android.tools.lint.detector.api.SourceCodeScanner
28+
import com.android.tools.lint.detector.api.TextFormat
29+
import org.jetbrains.uast.UCallExpression
30+
import org.jetbrains.uast.getParameterForArgument
31+
import java.util.EnumSet
32+
33+
@Suppress("UnstableApiUsage")
34+
class DaxTextFieldTrailingIconDetector : Detector(), SourceCodeScanner {
35+
36+
override fun getApplicableUastTypes() = listOf(UCallExpression::class.java)
37+
38+
override fun createUastHandler(context: JavaContext): UElementHandler = DaxTextFieldCallHandler(context)
39+
40+
internal class DaxTextFieldCallHandler(private val context: JavaContext) : UElementHandler() {
41+
override fun visitCallExpression(node: UCallExpression) {
42+
val methodName = node.methodName
43+
44+
if (methodName == "DaxTextField") {
45+
checkTrailingIconParameter(node)
46+
}
47+
}
48+
49+
private fun checkTrailingIconParameter(node: UCallExpression) {
50+
// Find the 'trailingIcon' parameter
51+
val trailingIconArgument = node.valueArguments.find { arg ->
52+
val parameterName = node.getParameterForArgument(arg)?.name
53+
parameterName == "trailingIcon"
54+
} ?: return // No 'trailingIcon' parameter provided which is fine
55+
56+
// Check if the trailingIcon uses an invalid composable
57+
if (isInvalidComposable(trailingIconArgument)) {
58+
reportInvalidComposableUsage(trailingIconArgument)
59+
}
60+
}
61+
62+
private fun isInvalidComposable(argument: org.jetbrains.uast.UExpression): Boolean {
63+
val source = argument.sourcePsi?.text ?: return false
64+
65+
if (source.contains("DaxTextFieldTrailingIcon")) {
66+
// using the correct 'DaxTextFieldTrailingIcon' composable
67+
return false
68+
}
69+
70+
// Check if the source contains invalid Composables (only DaxTextFieldTrailingIcon should be used)
71+
// This covers cases like:
72+
// - Icon
73+
// - IconButton
74+
val composablePattern = Regex("""\b(Icon|IconButton|Image|Text|Button|Surface)\s*\(""")
75+
return composablePattern.containsMatchIn(source)
76+
}
77+
78+
private fun reportInvalidComposableUsage(arg: org.jetbrains.uast.UExpression) {
79+
context.report(
80+
issue = INVALID_DAX_TEXT_FIELD_TRAILING_ICON_USAGE,
81+
location = context.getLocation(arg),
82+
message = INVALID_DAX_TEXT_FIELD_TRAILING_ICON_USAGE.getExplanation(TextFormat.RAW),
83+
)
84+
}
85+
}
86+
87+
companion object {
88+
val INVALID_DAX_TEXT_FIELD_TRAILING_ICON_USAGE = Issue
89+
.create(
90+
id = "InvalidDaxTextFieldTrailingIconUsage",
91+
briefDescription = "DaxTextField trailingIcon parameter should use DaxTextFieldTrailingIcon",
92+
explanation = """
93+
Use DaxTextFieldTrailingIcon instead of arbitrary composables for the trailingIcon parameter to maintain design system consistency.
94+
95+
Example:
96+
DaxTextField(
97+
state = state,
98+
trailingIcon = {
99+
DaxTextFieldTrailingIcon(
100+
painter = painterResource(R.drawable.ic_copy_24),
101+
contentDescription = stringResource(R.string.icon_description)
102+
)
103+
}
104+
)
105+
106+
This ensures consistent styling, spacing, and behavior across all text field icons in the app.
107+
""".trimIndent(),
108+
moreInfo = "",
109+
category = CUSTOM_LINT_CHECKS,
110+
priority = 6,
111+
severity = Severity.WARNING,
112+
androidSpecific = true,
113+
implementation = Implementation(
114+
DaxTextFieldTrailingIconDetector::class.java,
115+
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES),
116+
),
117+
)
118+
}
119+
}

0 commit comments

Comments
 (0)