Skip to content

Commit 227eee5

Browse files
committed
Add RawTypeDataSetDetector for datasets
1 parent ac08271 commit 227eee5

2 files changed

Lines changed: 156 additions & 1 deletion

File tree

lint/src/main/kotlin/info/appdev/charting/lint/LintRegistry.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import com.android.tools.lint.detector.api.Issue
88
class LintRegistry : IssueRegistry() {
99

1010
override val issues: List<Issue> = listOf(
11-
EntryUsageDetector.ISSUE
11+
EntryUsageDetector.ISSUE,
12+
RawTypeDataSetDetector.ISSUE
1213
)
1314

1415
/** Must match the Lint API version used at compile time. */
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package info.appdev.charting.lint
2+
3+
import com.android.tools.lint.client.api.UElementHandler
4+
import com.android.tools.lint.detector.api.Category
5+
import com.android.tools.lint.detector.api.Detector
6+
import com.android.tools.lint.detector.api.Implementation
7+
import com.android.tools.lint.detector.api.Issue
8+
import com.android.tools.lint.detector.api.JavaContext
9+
import com.android.tools.lint.detector.api.Scope
10+
import com.android.tools.lint.detector.api.Severity
11+
import com.android.tools.lint.detector.api.SourceCodeScanner
12+
import com.intellij.psi.PsiClassType
13+
import org.jetbrains.uast.UElement
14+
import org.jetbrains.uast.UField
15+
import org.jetbrains.uast.UImportStatement
16+
import org.jetbrains.uast.ULocalVariable
17+
import org.jetbrains.uast.UMethod
18+
import org.jetbrains.uast.UParameter
19+
import org.jetbrains.uast.UTypeReferenceExpression
20+
import org.jetbrains.uast.getParentOfType
21+
22+
class RawTypeDataSetDetector : Detector(), SourceCodeScanner {
23+
24+
companion object {
25+
private const val DATA_PKG = "info.appdev.charting.data"
26+
private const val IFACE_PKG = "info.appdev.charting.interfaces.datasets"
27+
private const val FLOAT_DEFAULT = "EntryFloat"
28+
private const val DOUBLE_DEFAULT = "EntryDouble"
29+
30+
private val GENERIC_TYPES: Map<String, Pair<String, String>> = mapOf(
31+
"$DATA_PKG.DataSet" to (FLOAT_DEFAULT to DOUBLE_DEFAULT),
32+
"$DATA_PKG.LineDataSet" to (FLOAT_DEFAULT to DOUBLE_DEFAULT),
33+
"$IFACE_PKG.IDataSet" to (FLOAT_DEFAULT to DOUBLE_DEFAULT),
34+
"$IFACE_PKG.ILineDataSet" to (FLOAT_DEFAULT to DOUBLE_DEFAULT),
35+
"$IFACE_PKG.ILineRadarDataSet" to (FLOAT_DEFAULT to DOUBLE_DEFAULT),
36+
"$IFACE_PKG.ILineScatterCandleRadarDataSet" to (FLOAT_DEFAULT to DOUBLE_DEFAULT),
37+
"$IFACE_PKG.IBarLineScatterCandleBubbleDataSet" to (FLOAT_DEFAULT to DOUBLE_DEFAULT),
38+
)
39+
40+
@JvmField
41+
val ISSUE: Issue = Issue.create(
42+
id = "RawTypeDataSet",
43+
briefDescription = "Specify an explicit entry type parameter instead of `<*>` or no argument",
44+
explanation = """
45+
Using a star projection `Type<*>` or omitting the type argument entirely loses \
46+
compile-time type-safety. Replace with `<EntryFloat>` (same precision as the \
47+
legacy API) or `<EntryDouble>` (higher precision). \
48+
Example: `LineDataSet` or `LineDataSet<*>` → `LineDataSet<EntryFloat>`.
49+
""",
50+
category = Category.CORRECTNESS,
51+
priority = 5,
52+
severity = Severity.ERROR,
53+
implementation = Implementation(
54+
RawTypeDataSetDetector::class.java,
55+
Scope.JAVA_FILE_SCOPE
56+
)
57+
)
58+
}
59+
60+
// -----------------------------------------------------------------------
61+
// Why multiple node types?
62+
//
63+
// UTypeReferenceExpression in getApplicableUastTypes() is only traversed
64+
// for INLINE expressions (e.g. `is LineDataSet` checks).
65+
//
66+
// For type ANNOTATIONS on declarations the UTypeReferenceExpression is
67+
// NOT a traversed tree node — it is only accessible as a property:
68+
// ULocalVariable.typeReference → val x: LineDataSet<*>
69+
// UField.typeReference → class Foo { val x: LineDataSet<*> }
70+
// UParameter.typeReference → fun f(x: LineDataSet<*>)
71+
// UMethod.returnTypeReference → fun f(): LineDataSet<*>
72+
//
73+
// We therefore visit all container node types and pull the typeReference
74+
// from each one explicitly.
75+
// -----------------------------------------------------------------------
76+
override fun getApplicableUastTypes(): List<Class<out UElement>> = listOf(
77+
ULocalVariable::class.java, // val x: LineDataSet<*>
78+
UField::class.java, // class Foo { val x: LineDataSet<*> }
79+
UParameter::class.java, // fun f(x: LineDataSet<*>)
80+
UMethod::class.java, // fun f(): LineDataSet<*>
81+
UTypeReferenceExpression::class.java, // is LineDataSet, cast as LineDataSet<*>, …
82+
)
83+
84+
override fun createUastHandler(context: JavaContext): UElementHandler =
85+
object : UElementHandler() {
86+
87+
override fun visitLocalVariable(node: ULocalVariable) =
88+
checkTypeRef(context, node.typeReference)
89+
90+
override fun visitField(node: UField) =
91+
checkTypeRef(context, node.typeReference)
92+
93+
override fun visitParameter(node: UParameter) =
94+
checkTypeRef(context, node.typeReference)
95+
96+
override fun visitMethod(node: UMethod) =
97+
checkTypeRef(context, node.returnTypeReference)
98+
99+
// Catches inline type positions: `is LineDataSet`, `as LineDataSet<*>`, …
100+
override fun visitTypeReferenceExpression(node: UTypeReferenceExpression) {
101+
if (node.getParentOfType<UImportStatement>() != null) return
102+
checkTypeRefNode(context, node)
103+
}
104+
105+
// ---- shared implementation ----------------------------------------
106+
107+
private fun checkTypeRef(context: JavaContext, typeRef: UTypeReferenceExpression?) {
108+
if (typeRef == null) return
109+
if (typeRef.getParentOfType<UImportStatement>() != null) return
110+
checkTypeRefNode(context, typeRef)
111+
}
112+
113+
private fun checkTypeRefNode(context: JavaContext, node: UTypeReferenceExpression) {
114+
val sourcePsi = node.sourcePsi ?: return
115+
val nodeText = sourcePsi.text.trim()
116+
117+
for ((fqn, typeArgs) in GENERIC_TYPES) {
118+
val simpleName = fqn.substringAfterLast('.')
119+
val isStarProjection = nodeText == "$simpleName<*>"
120+
val isMissingTypeArg = nodeText == simpleName
121+
if (!isStarProjection && !isMissingTypeArg) continue
122+
123+
// Verify FQN when the type resolves (guards against same-named classes)
124+
val resolved = (node.type as? PsiClassType)?.resolve()
125+
if (resolved != null && resolved.qualifiedName != fqn) continue
126+
127+
val (floatArg, doubleArg) = typeArgs
128+
val (oldText, newFloat, newDouble) = if (isStarProjection)
129+
Triple("$simpleName<*>", "$simpleName<$floatArg>", "$simpleName<$doubleArg>")
130+
else
131+
Triple(simpleName, "$simpleName<$floatArg>", "$simpleName<$doubleArg>")
132+
133+
context.report(
134+
ISSUE,
135+
node,
136+
context.getLocation(sourcePsi),
137+
if (isStarProjection) "Replace `$simpleName<*>` with an explicit entry type for type-safety"
138+
else "Add missing type argument to `$simpleName`",
139+
fix().alternatives(
140+
fix().replace()
141+
.name("Replace with $newFloat")
142+
.text(oldText).with(newFloat)
143+
.autoFix().build(),
144+
fix().replace()
145+
.name("Replace with $newDouble (high precision)")
146+
.text(oldText).with(newDouble)
147+
.autoFix().build()
148+
)
149+
)
150+
break
151+
}
152+
}
153+
}
154+
}

0 commit comments

Comments
 (0)