Skip to content

Commit a7aa6a2

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

File tree

5 files changed

+1314
-0
lines changed

5 files changed

+1314
-0
lines changed

lint-rules/src/main/java/com/duckduckgo/lint/registry/DuckDuckGoIssueRegistry.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import com.duckduckgo.lint.ui.ColorAttributeInXmlDetector.Companion.INVALID_COLO
3939
import com.duckduckgo.lint.ui.DaxButtonStylingDetector.Companion.INVALID_DAX_BUTTON_PROPERTY
4040
import com.duckduckgo.lint.ui.DaxTextColorUsageDetector.Companion.INVALID_DAX_TEXT_COLOR_USAGE
4141
import com.duckduckgo.lint.ui.DaxTextFieldTrailingIconDetector.Companion.INVALID_DAX_TEXT_FIELD_TRAILING_ICON_USAGE
42+
import com.duckduckgo.lint.ui.DaxSecureTextFieldTrailingIconDetector.Companion.INVALID_DAX_SECURE_TEXT_FIELD_TRAILING_ICON_USAGE
4243
import com.duckduckgo.lint.ui.DaxTextViewStylingDetector.Companion.INVALID_DAX_TEXT_VIEW_PROPERTY
4344
import com.duckduckgo.lint.ui.DeprecatedAndroidWidgetsUsedInXmlDetector.Companion.DEPRECATED_WIDGET_IN_XML
4445
import com.duckduckgo.lint.ui.MissingDividerDetector.Companion.MISSING_HORIZONTAL_DIVIDER
@@ -88,6 +89,7 @@ class DuckDuckGoIssueRegistry : IssueRegistry() {
8889
NO_SET_CONTENT_USAGE,
8990
INVALID_DAX_TEXT_COLOR_USAGE,
9091
INVALID_DAX_TEXT_FIELD_TRAILING_ICON_USAGE,
92+
INVALID_DAX_SECURE_TEXT_FIELD_TRAILING_ICON_USAGE,
9193

9294
).plus(WebViewCompatApisUsageDetector.issues)
9395

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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 DaxSecureTextFieldTrailingIconDetector : Detector(), SourceCodeScanner {
35+
36+
override fun getApplicableUastTypes() = listOf(UCallExpression::class.java)
37+
38+
override fun createUastHandler(context: JavaContext): UElementHandler = DaxSecureTextFieldCallHandler(context)
39+
40+
internal class DaxSecureTextFieldCallHandler(private val context: JavaContext) : UElementHandler() {
41+
override fun visitCallExpression(node: UCallExpression) {
42+
val methodName = node.methodName
43+
44+
if (methodName == "DaxSecureTextField") {
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+
// Only DaxTextFieldTrailingIcon should be used in the trailingIcon parameter
66+
// If the source doesn't contain it, then it's invalid
67+
return !source.contains("DaxTextFieldTrailingIcon")
68+
}
69+
70+
private fun reportInvalidComposableUsage(arg: org.jetbrains.uast.UExpression) {
71+
context.report(
72+
issue = INVALID_DAX_SECURE_TEXT_FIELD_TRAILING_ICON_USAGE,
73+
location = context.getLocation(arg),
74+
message = INVALID_DAX_SECURE_TEXT_FIELD_TRAILING_ICON_USAGE.getExplanation(TextFormat.RAW),
75+
)
76+
}
77+
}
78+
79+
companion object {
80+
val INVALID_DAX_SECURE_TEXT_FIELD_TRAILING_ICON_USAGE = Issue
81+
.create(
82+
id = "InvalidDaxSecureTextFieldTrailingIconUsage",
83+
briefDescription = "DaxSecureTextField trailingIcon parameter should use DaxTextFieldTrailingIcon",
84+
explanation = """
85+
Use DaxTextFieldTrailingIcon instead of arbitrary composables for the trailingIcon parameter to maintain design system consistency.
86+
87+
Example:
88+
DaxSecureTextField(
89+
state = state,
90+
isPasswordVisible = isPasswordVisible,
91+
onShowHidePasswordIconClick = { /* toggle visibility */ },
92+
trailingIcon = {
93+
DaxTextFieldTrailingIcon(
94+
painter = painterResource(R.drawable.ic_copy_24),
95+
contentDescription = stringResource(R.string.icon_description)
96+
)
97+
}
98+
)
99+
100+
This ensures consistent styling, spacing, and behavior across all text field icons in the app.
101+
""".trimIndent(),
102+
moreInfo = "",
103+
category = CUSTOM_LINT_CHECKS,
104+
priority = 6,
105+
severity = Severity.WARNING,
106+
androidSpecific = true,
107+
implementation = Implementation(
108+
DaxSecureTextFieldTrailingIconDetector::class.java,
109+
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES),
110+
),
111+
)
112+
}
113+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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+
// Only DaxTextFieldTrailingIcon should be used in the trailingIcon parameter
66+
// If the source doesn't contain it, then it's invalid
67+
return !source.contains("DaxTextFieldTrailingIcon")
68+
}
69+
70+
private fun reportInvalidComposableUsage(arg: org.jetbrains.uast.UExpression) {
71+
context.report(
72+
issue = INVALID_DAX_TEXT_FIELD_TRAILING_ICON_USAGE,
73+
location = context.getLocation(arg),
74+
message = INVALID_DAX_TEXT_FIELD_TRAILING_ICON_USAGE.getExplanation(TextFormat.RAW),
75+
)
76+
}
77+
}
78+
79+
companion object {
80+
val INVALID_DAX_TEXT_FIELD_TRAILING_ICON_USAGE = Issue
81+
.create(
82+
id = "InvalidDaxTextFieldTrailingIconUsage",
83+
briefDescription = "DaxTextField trailingIcon parameter should use DaxTextFieldTrailingIcon",
84+
explanation = """
85+
Use DaxTextFieldTrailingIcon instead of arbitrary composables for the trailingIcon parameter to maintain design system consistency.
86+
87+
Example:
88+
DaxTextField(
89+
state = state,
90+
trailingIcon = {
91+
DaxTextFieldTrailingIcon(
92+
painter = painterResource(R.drawable.ic_copy_24),
93+
contentDescription = stringResource(R.string.icon_description)
94+
)
95+
}
96+
)
97+
98+
This ensures consistent styling, spacing, and behavior across all text field icons in the app.
99+
""".trimIndent(),
100+
moreInfo = "",
101+
category = CUSTOM_LINT_CHECKS,
102+
priority = 6,
103+
severity = Severity.WARNING,
104+
androidSpecific = true,
105+
implementation = Implementation(
106+
DaxTextFieldTrailingIconDetector::class.java,
107+
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES),
108+
),
109+
)
110+
}
111+
}

0 commit comments

Comments
 (0)