diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c5864c808..959168d23 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -46,6 +46,9 @@ android { testOptions { animationsDisabled = true } + lint { + lintConfig = file("lint.xml") + } packaging { jniLibs { // androidx.graphics:graphics-path ships a .so that llvm-strip cannot process; @@ -56,6 +59,8 @@ android { } dependencies { + lintChecks(project(":lint")) + implementation("androidx.appcompat:appcompat:1.7.1") implementation("com.google.android.material:material:1.13.0") implementation(project(":chartLib")) diff --git a/app/lint.xml b/app/lint.xml new file mode 100644 index 000000000..bbd42cf46 --- /dev/null +++ b/app/lint.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/src/main/kotlin/info/appdev/chartexample/BubbleChartActivity.kt b/app/src/main/kotlin/info/appdev/chartexample/BubbleChartActivity.kt index 7207e7744..8b8e147d0 100644 --- a/app/src/main/kotlin/info/appdev/chartexample/BubbleChartActivity.kt +++ b/app/src/main/kotlin/info/appdev/chartexample/BubbleChartActivity.kt @@ -19,6 +19,7 @@ import info.appdev.charting.components.Legend import info.appdev.charting.components.XAxis import info.appdev.charting.data.BubbleData import info.appdev.charting.data.BubbleDataSet +import info.appdev.charting.data.BubbleEntry import info.appdev.charting.data.BubbleEntryFloat import info.appdev.charting.data.EntryFloat import info.appdev.charting.highlight.Highlight diff --git a/build.gradle.kts b/build.gradle.kts index 4fba12077..1ecd19fad 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,10 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:9.1.0") + // Lint API version tracks AGP: AGP 9.1.1 → Lint 32.1.1 + // in module lint: + // val lintVersion = "32.1.1" + classpath("com.android.tools.build:gradle:9.1.1") classpath("com.github.dcendents:android-maven-gradle-plugin:2.1") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.20") } diff --git a/chartLib/build.gradle.kts b/chartLib/build.gradle.kts index d8464bb59..fea1cd0a3 100644 --- a/chartLib/build.gradle.kts +++ b/chartLib/build.gradle.kts @@ -50,6 +50,9 @@ android { } dependencies { + lintChecks(project(":lint")) // applies locally + lintPublish(project(path = ":lint", configuration = "lintJar")) // embeds in published AAR + implementation("androidx.annotation:annotation:1.10.0") implementation("androidx.core:core:1.18.0") implementation("androidx.activity:activity-ktx:1.13.0") diff --git a/chartLib/src/main/kotlin/info/appdev/charting/data/BarEntry.kt b/chartLib/src/main/kotlin/info/appdev/charting/data/BarEntry.kt new file mode 100644 index 000000000..eedc9b38d --- /dev/null +++ b/chartLib/src/main/kotlin/info/appdev/charting/data/BarEntry.kt @@ -0,0 +1,45 @@ +package info.appdev.charting.data + +import android.graphics.drawable.Drawable + + +@Deprecated( + message = "The replacement is BarEntryFloat, or use BarEntryDouble for higher precision. BarEntry is retained for backward compatibility but will be removed in a future version.", + replaceWith = ReplaceWith("BarEntryFloat", "info.appdev.charting.data.BarEntryFloat") +) +open class BarEntry : BarEntryFloat { + + /** + * Constructor for normal bars (not stacked). + */ + constructor(x: Float, y: Float) : super(x, y) + + /** + * Constructor for normal bars (not stacked). + * + * @param x + * @param y + * @param data - Spot for additional data this Entry represents. + */ + constructor(x: Float, y: Float, data: Any?) : super(x, y, data) + + /** + * Constructor for normal bars (not stacked). + * + * @param x + * @param y + * @param icon - icon image + */ + constructor(x: Float, y: Float, icon: Drawable?) : super(x, y, icon) + + /** + * Constructor for normal bars (not stacked). + * + * @param x + * @param y + * @param icon - icon image + * @param data - Spot for additional data this Entry represents. + */ + constructor(x: Float, y: Float, icon: Drawable?, data: Any?) : super(x, y, icon, data) + +} diff --git a/chartLib/src/main/kotlin/info/appdev/charting/data/BubbleEntry.kt b/chartLib/src/main/kotlin/info/appdev/charting/data/BubbleEntry.kt new file mode 100644 index 000000000..af0986e7e --- /dev/null +++ b/chartLib/src/main/kotlin/info/appdev/charting/data/BubbleEntry.kt @@ -0,0 +1,46 @@ +package info.appdev.charting.data + +import android.annotation.SuppressLint +import android.graphics.drawable.Drawable + +@Deprecated( + message = "The replacement is BubbleEntryFloat, or use BubbleEntryDouble for higher precision. BubbleEntry is retained for backward compatibility but will be removed in a future version.", + replaceWith = ReplaceWith("BubbleEntryFloat", "info.appdev.charting.data.BubbleEntryFloat") +) +class BubbleEntry : BubbleEntryFloat { + + /** + * Constructor. + * + * @param x The value on the x-axis. + * @param y The value on the y-axis. + * @param size The size of the bubble. + */ + constructor(x: Float, y: Float, size: Float) : super(x, y, size) { + this.size = size + } + + /** + * Constructor. + * + * @param x The value on the x-axis. + * @param y The value on the y-axis. + * @param size The size of the bubble. + * @param data Spot for additional data this Entry represents. + */ + constructor(x: Float, y: Float, size: Float, data: Any?) : super(x, y, size, data) { + this.size = size + } + + /** + * Constructor. + * + * @param x The value on the x-axis. + * @param y The value on the y-axis. + * @param size The size of the bubble. + * @param icon Icon image + */ + constructor(x: Float, y: Float, size: Float, icon: Drawable?) : super(x, y, size, icon) { + this.size = size + } +} diff --git a/chartLib/src/main/kotlin/info/appdev/charting/data/CandleEntry.kt b/chartLib/src/main/kotlin/info/appdev/charting/data/CandleEntry.kt new file mode 100644 index 000000000..e7d9811bf --- /dev/null +++ b/chartLib/src/main/kotlin/info/appdev/charting/data/CandleEntry.kt @@ -0,0 +1,82 @@ +package info.appdev.charting.data + +import android.graphics.drawable.Drawable + +@Deprecated( + message = "The replacement is CandleEntryFloat, or use CandleEntryDouble for higher precision. CandleEntry is retained for backward compatibility but will be removed in a future version.", + replaceWith = ReplaceWith("CandleEntryFloat", "info.appdev.charting.data.CandleEntryFloat") +) +class CandleEntry : CandleEntryFloat { + + /** + * Constructor. + * + * @param x The value on the x-axis + * @param shadowH The (shadow) high value + * @param shadowL The (shadow) low value + * @param open The open value + * @param close The close value + */ + constructor(x: Float, shadowH: Float, shadowL: Float, open: Float, close: Float) : super(x, shadowH, shadowL, open, close) { + this.high = shadowH + this.low = shadowL + this.open = open + this.close = close + } + + /** + * Constructor. + * + * @param x The value on the x-axis + * @param shadowH The (shadow) high value + * @param shadowL The (shadow) low value + * @param data Spot for additional data this Entry represents + */ + constructor( + x: Float, shadowH: Float, shadowL: Float, open: Float, close: Float, + data: Any? + ) : super(x, shadowH, shadowL, open, close) { + this.high = shadowH + this.low = shadowL + this.open = open + this.close = close + } + + /** + * Constructor. + * + * @param x The value on the x-axis + * @param shadowH The (shadow) high value + * @param shadowL The (shadow) low value + * @param icon Icon image + */ + constructor( + x: Float, shadowH: Float, shadowL: Float, open: Float, close: Float, + icon: Drawable? + ) : super(x, shadowH, shadowL, open, close) { + this.high = shadowH + this.low = shadowL + this.open = open + this.close = close + } + + /** + * Constructor. + * + * @param x The value on the x-axis + * @param shadowH The (shadow) high value + * @param shadowL The (shadow) low value + * @param icon Icon image + * @param data Spot for additional data this Entry represents + */ + constructor( + x: Float, shadowH: Float, shadowL: Float, open: Float, close: Float, + icon: Drawable?, data: Any? + ) : super(x, shadowH, shadowL, open, close) { + this.high = shadowH + this.low = shadowL + this.open = open + this.close = close + } + +} diff --git a/chartLib/src/main/kotlin/info/appdev/charting/data/ChartData.kt b/chartLib/src/main/kotlin/info/appdev/charting/data/ChartData.kt index 84ee4ea1c..dc247286d 100644 --- a/chartLib/src/main/kotlin/info/appdev/charting/data/ChartData.kt +++ b/chartLib/src/main/kotlin/info/appdev/charting/data/ChartData.kt @@ -1,5 +1,6 @@ package info.appdev.charting.data +import android.annotation.SuppressLint import android.graphics.Typeface import info.appdev.charting.components.YAxis.AxisDependency import info.appdev.charting.formatter.IValueFormatter @@ -440,6 +441,7 @@ abstract class ChartData> : Serializable { * specified index. Returns true if an Entry was removed, false if no Entry * was found that meets the specified requirements. */ + @SuppressLint("RawTypeDataSet") open fun removeEntry(xValue: Float, dataSetIndex: Int): Boolean { if (dataSetIndex >= dataSets.size) { return false diff --git a/chartLib/src/main/kotlin/info/appdev/charting/data/DataSet.kt b/chartLib/src/main/kotlin/info/appdev/charting/data/DataSet.kt index 33b87d315..4073809f3 100644 --- a/chartLib/src/main/kotlin/info/appdev/charting/data/DataSet.kt +++ b/chartLib/src/main/kotlin/info/appdev/charting/data/DataSet.kt @@ -1,5 +1,6 @@ package info.appdev.charting.data +import android.annotation.SuppressLint import timber.log.Timber import java.io.Serializable import kotlin.math.abs @@ -130,7 +131,7 @@ abstract class DataSet>( */ abstract fun copy(): DataSet? - protected fun copy(dataSet: DataSet<*>) { + protected fun copy(@SuppressLint("RawTypeDataSet") dataSet: DataSet<*>) { super.copy(dataSet) } diff --git a/chartLib/src/main/kotlin/info/appdev/charting/data/Entry.kt b/chartLib/src/main/kotlin/info/appdev/charting/data/Entry.kt new file mode 100644 index 000000000..9bc6dd11b --- /dev/null +++ b/chartLib/src/main/kotlin/info/appdev/charting/data/Entry.kt @@ -0,0 +1,63 @@ +package info.appdev.charting.data + +import android.R.attr.data +import android.R.attr.x +import android.R.attr.y +import android.graphics.drawable.Drawable +import android.os.Build +import android.os.Parcel +import android.os.ParcelFormatException +import android.os.Parcelable +import info.appdev.charting.utils.Utils +import java.io.Serializable +import kotlin.math.abs + +/** + * Class representing one entry in the chart. Might contain multiple values. + * Might only contain a single value depending on the used constructor. + */ +@Deprecated( + message = "The replacement is EntryFloat, or use EntryDouble for higher precision. Entry is retained for backward compatibility but will be removed in a future version.", + replaceWith = ReplaceWith("EntryFloat", "info.appdev.charting.data.EntryFloat") +) +class Entry : EntryFloat { + + constructor() + + /** + * An EntryFloat represents one single entry in the chart. + * + * @param x the x value + * @param y the y value (the actual value of the entry) + */ + constructor(x: Float, y: Float) : super(x = x, y = y) + + /** + * An EntryFloat represents one single entry in the chart. + * + * @param x the x value + * @param y the y value (the actual value of the entry) + * @param data Spot for additional data this Entry represents. + */ + constructor(x: Float, y: Float, data: Any?) : super(x = x, y = y, data = data) + + /** + * An EntryFloat represents one single entry in the chart. + * + * @param x the x value + * @param y the y value (the actual value of the entry) + * @param icon icon image + */ + constructor(x: Float, y: Float, icon: Drawable?) : super(x = x, y = y, icon = icon) + + /** + * An EntryFloat represents one single entry in the chart. + * + * @param x the x value + * @param y the y value (the actual value of the entry) + * @param icon icon image + * @param data Spot for additional data this EntryFloat represents. + */ + constructor(x: Float, y: Float, icon: Drawable?, data: Any?) : super(x = x, y = y, icon = icon, data = data) + +} diff --git a/chartLib/src/main/kotlin/info/appdev/charting/data/LineDataSet.kt b/chartLib/src/main/kotlin/info/appdev/charting/data/LineDataSet.kt index 635c017d5..3c369112d 100644 --- a/chartLib/src/main/kotlin/info/appdev/charting/data/LineDataSet.kt +++ b/chartLib/src/main/kotlin/info/appdev/charting/data/LineDataSet.kt @@ -1,5 +1,6 @@ package info.appdev.charting.data +import android.annotation.SuppressLint import android.content.Context import android.graphics.Color import android.graphics.DashPathEffect @@ -84,7 +85,7 @@ open class LineDataSet>(yVals: MutableList = mutableList return copied as DataSet } - protected fun copy(lineDataSet: LineDataSet<*>) { + protected fun copy(@SuppressLint("RawTypeDataSet") lineDataSet: LineDataSet<*>) { super.copy((lineDataSet as BaseDataSet<*>?)!!) lineDataSet.circleColors = this.circleColors lineDataSet.mCircleHoleColor = mCircleHoleColor diff --git a/chartLib/src/main/kotlin/info/appdev/charting/data/PieEntry.kt b/chartLib/src/main/kotlin/info/appdev/charting/data/PieEntry.kt new file mode 100644 index 000000000..dafa6e378 --- /dev/null +++ b/chartLib/src/main/kotlin/info/appdev/charting/data/PieEntry.kt @@ -0,0 +1,35 @@ +package info.appdev.charting.data + +import android.graphics.drawable.Drawable + +@Deprecated( + message = "The replacement is PieEntryFloat, or use PieEntryDouble for higher precision. PieEntry is retained for backward compatibility but will be removed in a future version.", + replaceWith = ReplaceWith("PieEntryFloat", "info.appdev.charting.data.PieEntryFloat") +) +class PieEntry : PieEntryFloat { + + constructor(value: Float) : super(0f) + + constructor(value: Float, data: Any?) : super(value, data) + + constructor(value: Float, icon: Drawable?) : super(value, icon) + + constructor(value: Float, icon: Drawable?, data: Any?) : super(value, icon, data) + + constructor(value: Float, label: String?) : super(value) { + this.label = label + } + + constructor(value: Float, label: String?, data: Any?) : super(value, data) { + this.label = label + } + + constructor(value: Float, label: String?, icon: Drawable?) : super(value, icon) { + this.label = label + } + + constructor(value: Float, label: String?, icon: Drawable?, data: Any?) : super(value, icon, data) { + this.label = label + } + +} diff --git a/chartLib/src/main/kotlin/info/appdev/charting/data/PieEntryDouble.kt b/chartLib/src/main/kotlin/info/appdev/charting/data/PieEntryDouble.kt index a27a56a22..e69de29bb 100644 --- a/chartLib/src/main/kotlin/info/appdev/charting/data/PieEntryDouble.kt +++ b/chartLib/src/main/kotlin/info/appdev/charting/data/PieEntryDouble.kt @@ -1,59 +0,0 @@ -package info.appdev.charting.data - -import android.annotation.SuppressLint -import android.graphics.drawable.Drawable -import timber.log.Timber - -/** - * High-precision pie entry that stores the value as Double, extending [PieEntryFloat] - * so it works seamlessly in the existing pie chart rendering pipeline. - * Use [valueDouble] for full-precision access. - * Pie entries have no meaningful x-axis value. - */ -@SuppressLint("ParcelCreator") -open class PieEntryDouble : PieEntryFloat { - - var valueDouble: Double = 0.0 - - override var y: Float - get() = valueDouble.toFloat() - set(value) { valueDouble = value.toDouble() } - - @get:Deprecated("") - @set:Deprecated("") - @Suppress("DEPRECATION") - override var x: Float - get() { - Timber.i("Pie entries do not have x values") - return super.x - } - set(x) { - super.x = x - Timber.i("Pie entries do not have x values") - } - - constructor(value: Double) : super(value.toFloat()) { valueDouble = value } - - constructor(value: Double, data: Any?) : super(value.toFloat(), data) { valueDouble = value } - - constructor(value: Double, icon: Drawable?) : super(value.toFloat(), icon) { valueDouble = value } - - constructor(value: Double, icon: Drawable?, data: Any?) : super(value.toFloat(), icon, data) { valueDouble = value } - - constructor(value: Double, label: String?) : super(value.toFloat(), label) { valueDouble = value } - - constructor(value: Double, label: String?, data: Any?) : super(value.toFloat(), label, data) { valueDouble = value } - - constructor(value: Double, label: String?, icon: Drawable?) : super(value.toFloat(), label, icon) { valueDouble = value } - - constructor(value: Double, label: String?, icon: Drawable?, data: Any?) : super(value.toFloat(), label, icon, data) { valueDouble = value } - - /** Full-precision value (same as [valueDouble]). */ - val valuePrecise: Double get() = valueDouble - - override fun copy(): PieEntryDouble = PieEntryDouble(valueDouble, label, data) - - override fun toString(): String = "PieEntryDouble valueDouble=$valueDouble label=$label" -} - - diff --git a/chartLib/src/main/kotlin/info/appdev/charting/data/RadarEntry.kt b/chartLib/src/main/kotlin/info/appdev/charting/data/RadarEntry.kt new file mode 100644 index 000000000..49c5c3dd6 --- /dev/null +++ b/chartLib/src/main/kotlin/info/appdev/charting/data/RadarEntry.kt @@ -0,0 +1,12 @@ +package info.appdev.charting.data + +@Deprecated( + message = "The replacement is RadarEntryFloat, or use RadarEntryDouble for higher precision. RadarEntry is retained for backward compatibility but will be removed in a future version.", + replaceWith = ReplaceWith("RadarEntryFloat", "info.appdev.charting.data.RadarEntryFloat") +) +class RadarEntry : RadarEntryFloat { + constructor(value: Float) : super(value) + + constructor(value: Float, data: Any?) : super(value, data) + +} diff --git a/chartLib/src/main/kotlin/info/appdev/charting/data/RadarEntryDouble.kt b/chartLib/src/main/kotlin/info/appdev/charting/data/RadarEntryDouble.kt index 4fad54682..03080dfee 100644 --- a/chartLib/src/main/kotlin/info/appdev/charting/data/RadarEntryDouble.kt +++ b/chartLib/src/main/kotlin/info/appdev/charting/data/RadarEntryDouble.kt @@ -8,6 +8,7 @@ import android.annotation.SuppressLint * Use [valueDouble] for full-precision access. * Radar entries have no meaningful x-axis value. */ +@Suppress("DEPRECATION") @SuppressLint("ParcelCreator") open class RadarEntryDouble : RadarEntryFloat { @@ -19,7 +20,6 @@ open class RadarEntryDouble : RadarEntryFloat { @get:Deprecated("") @set:Deprecated("") - @Suppress("DEPRECATION") override var x: Float get() = super.x set(x) { super.x = x } @@ -36,4 +36,3 @@ open class RadarEntryDouble : RadarEntryFloat { override fun toString(): String = "RadarEntryDouble valueDouble=$valueDouble" } - diff --git a/chartLib/src/main/kotlin/info/appdev/charting/highlight/ChartHighlighter.kt b/chartLib/src/main/kotlin/info/appdev/charting/highlight/ChartHighlighter.kt index ddd760648..32be88ca6 100644 --- a/chartLib/src/main/kotlin/info/appdev/charting/highlight/ChartHighlighter.kt +++ b/chartLib/src/main/kotlin/info/appdev/charting/highlight/ChartHighlighter.kt @@ -1,5 +1,6 @@ package info.appdev.charting.highlight +import android.annotation.SuppressLint import info.appdev.charting.components.YAxis.AxisDependency import info.appdev.charting.data.ChartData import info.appdev.charting.data.DataSet @@ -115,7 +116,7 @@ open class ChartHighlighter>(prote */ @Suppress("SameParameterValue") protected open fun buildHighlights( - set: IDataSet<*>, + @SuppressLint("RawTypeDataSet") set: IDataSet<*>, dataSetIndex: Int, xVal: Float, rounding: DataSet.Rounding? diff --git a/chartLib/src/main/kotlin/info/appdev/charting/highlight/HorizontalBarHighlighter.kt b/chartLib/src/main/kotlin/info/appdev/charting/highlight/HorizontalBarHighlighter.kt index 8e61df89d..48cf47467 100644 --- a/chartLib/src/main/kotlin/info/appdev/charting/highlight/HorizontalBarHighlighter.kt +++ b/chartLib/src/main/kotlin/info/appdev/charting/highlight/HorizontalBarHighlighter.kt @@ -1,5 +1,6 @@ package info.appdev.charting.highlight +import android.annotation.SuppressLint import info.appdev.charting.data.DataSet import info.appdev.charting.data.EntryFloat import info.appdev.charting.interfaces.dataprovider.BarDataProvider @@ -32,7 +33,7 @@ class HorizontalBarHighlighter(dataProvider: BarDataProvider) : BarHighlighter(d return null } - override fun buildHighlights(set: IDataSet<*>, dataSetIndex: Int, xVal: Float, rounding: DataSet.Rounding?): MutableList { + override fun buildHighlights(@SuppressLint("RawTypeDataSet") set: IDataSet<*>, dataSetIndex: Int, xVal: Float, rounding: DataSet.Rounding?): MutableList { val highlights = ArrayList() var entries = set.getEntriesForXValue(xVal)?.map { it as EntryFloat }?.toMutableList() diff --git a/chartLib/src/main/kotlin/info/appdev/charting/listener/OnDrawListener.kt b/chartLib/src/main/kotlin/info/appdev/charting/listener/OnDrawListener.kt index 45b9af8ac..04690245a 100644 --- a/chartLib/src/main/kotlin/info/appdev/charting/listener/OnDrawListener.kt +++ b/chartLib/src/main/kotlin/info/appdev/charting/listener/OnDrawListener.kt @@ -1,5 +1,6 @@ package info.appdev.charting.listener +import android.annotation.SuppressLint import info.appdev.charting.data.DataSet import info.appdev.charting.data.EntryFloat @@ -25,5 +26,5 @@ interface OnDrawListener { * * @param dataSet the last drawn DataSet */ - fun onDrawFinished(dataSet: DataSet<*>) + fun onDrawFinished(@SuppressLint("RawTypeDataSet") dataSet: DataSet<*>) } diff --git a/chartLib/src/main/kotlin/info/appdev/charting/renderer/BarLineScatterCandleBubbleRenderer.kt b/chartLib/src/main/kotlin/info/appdev/charting/renderer/BarLineScatterCandleBubbleRenderer.kt index 8b256869f..6775ee280 100644 --- a/chartLib/src/main/kotlin/info/appdev/charting/renderer/BarLineScatterCandleBubbleRenderer.kt +++ b/chartLib/src/main/kotlin/info/appdev/charting/renderer/BarLineScatterCandleBubbleRenderer.kt @@ -1,5 +1,6 @@ package info.appdev.charting.renderer +import android.annotation.SuppressLint import info.appdev.charting.animation.ChartAnimator import info.appdev.charting.data.DataSet import info.appdev.charting.data.BaseEntry @@ -22,7 +23,7 @@ abstract class BarLineScatterCandleBubbleRenderer( /** * Returns true if the DataSet values should be drawn, false if not. */ - protected fun shouldDrawValues(set: IDataSet<*>): Boolean { + protected fun shouldDrawValues(@SuppressLint("RawTypeDataSet") set: IDataSet<*>): Boolean { return set.isVisible && (set.isDrawValues || set.isDrawIcons) } diff --git a/chartLib/src/main/kotlin/info/appdev/charting/renderer/DataRenderer.kt b/chartLib/src/main/kotlin/info/appdev/charting/renderer/DataRenderer.kt index ba5e970ad..f4ed9d404 100644 --- a/chartLib/src/main/kotlin/info/appdev/charting/renderer/DataRenderer.kt +++ b/chartLib/src/main/kotlin/info/appdev/charting/renderer/DataRenderer.kt @@ -1,5 +1,6 @@ package info.appdev.charting.renderer +import android.annotation.SuppressLint import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint @@ -70,7 +71,7 @@ abstract class DataRenderer( * Applies the required styling (provided by the DataSet) to the value-paint * object. */ - protected fun applyValueTextStyle(set: IDataSet<*>) { + protected fun applyValueTextStyle(@SuppressLint("RawTypeDataSet") set: IDataSet<*>) { paintValues.typeface = set.valueTypeface paintValues.textSize = set.valueTextSize } diff --git a/chartLib/src/main/kotlin/info/appdev/charting/renderer/LineChartRenderer.kt b/chartLib/src/main/kotlin/info/appdev/charting/renderer/LineChartRenderer.kt index 14a528102..0b0db2432 100644 --- a/chartLib/src/main/kotlin/info/appdev/charting/renderer/LineChartRenderer.kt +++ b/chartLib/src/main/kotlin/info/appdev/charting/renderer/LineChartRenderer.kt @@ -1,5 +1,6 @@ package info.appdev.charting.renderer +import android.annotation.SuppressLint import android.graphics.Bitmap import android.graphics.Bitmap.createBitmap import android.graphics.Canvas @@ -8,7 +9,6 @@ import android.graphics.Paint import android.graphics.Path import info.appdev.charting.animation.ChartAnimator import info.appdev.charting.data.BaseEntry -import info.appdev.charting.data.EntryFloat import info.appdev.charting.data.LineDataSet import info.appdev.charting.highlight.Highlight import info.appdev.charting.interfaces.dataprovider.LineDataProvider @@ -25,6 +25,7 @@ import java.lang.ref.WeakReference import kotlin.math.max import kotlin.math.min +@SuppressLint("RawTypeDataSet") open class LineChartRenderer( var dataProvider: LineDataProvider, animator: ChartAnimator, diff --git a/chartLib/src/main/kotlin/info/appdev/charting/renderer/LineScatterCandleRadarRenderer.kt b/chartLib/src/main/kotlin/info/appdev/charting/renderer/LineScatterCandleRadarRenderer.kt index 9cfe7bc7b..13f8ad48e 100644 --- a/chartLib/src/main/kotlin/info/appdev/charting/renderer/LineScatterCandleRadarRenderer.kt +++ b/chartLib/src/main/kotlin/info/appdev/charting/renderer/LineScatterCandleRadarRenderer.kt @@ -1,5 +1,6 @@ package info.appdev.charting.renderer +import android.annotation.SuppressLint import android.graphics.Canvas import android.graphics.Path import info.appdev.charting.animation.ChartAnimator @@ -23,7 +24,7 @@ abstract class LineScatterCandleRadarRenderer( * @param y y-position of the highlight line intersection * @param set the currently drawn dataset */ - protected fun drawHighlightLines(canvas: Canvas, x: Float, y: Float, set: ILineScatterCandleRadarDataSet<*>) { + protected fun drawHighlightLines(canvas: Canvas, x: Float, y: Float, @SuppressLint("RawTypeDataSet") set: ILineScatterCandleRadarDataSet<*>) { // set color and stroke-width paintHighlight.color = set.highLightColor diff --git a/chartLib/src/main/kotlin/info/appdev/charting/utils/Transformer.kt b/chartLib/src/main/kotlin/info/appdev/charting/utils/Transformer.kt index e26be48d7..9d5831ed2 100644 --- a/chartLib/src/main/kotlin/info/appdev/charting/utils/Transformer.kt +++ b/chartLib/src/main/kotlin/info/appdev/charting/utils/Transformer.kt @@ -1,5 +1,6 @@ package info.appdev.charting.utils +import android.annotation.SuppressLint import android.graphics.Matrix import android.graphics.Path import android.graphics.RectF @@ -142,7 +143,7 @@ open class Transformer(protected var viewPortHandler: ViewPortHandler) { * y values transformed with all matrices for the LINECHART. */ fun generateTransformedValuesLine( - data: ILineDataSet<*>, + @SuppressLint("RawTypeDataSet") data: ILineDataSet<*>, phaseX: Float, phaseY: Float, min: Int, max: Int ): FloatArray { diff --git a/lint/build.gradle.kts b/lint/build.gradle.kts new file mode 100644 index 000000000..9d1f47a35 --- /dev/null +++ b/lint/build.gradle.kts @@ -0,0 +1,46 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +tasks.withType().configureEach { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + +// Lint API version tracks AGP: AGP 9.1.1 → Lint 32.1.1 +val lintVersion = "32.1.1" + +dependencies { + compileOnly("com.android.tools.lint:lint-api:$lintVersion") + compileOnly("com.android.tools.lint:lint-checks:$lintVersion") + + testImplementation("com.android.tools.lint:lint:$lintVersion") + testImplementation("com.android.tools.lint:lint-tests:$lintVersion") + testImplementation("junit:junit:4.13.2") +} + +// The manifest attribute tells the Android Lint tooling which IssueRegistry to load. +tasks.jar { + manifest { + attributes["Lint-Registry-v2"] = "info.appdev.charting.lint.LintRegistry" + } +} + +// Expose a single-artifact configuration so library modules can use lintPublish. +// lintPublish requires exactly one JAR — the default project() dependency +// resolves to multiple artifacts and causes "Found more than one jar" errors. +configurations { + create("lintJar") +} +artifacts { + add("lintJar", tasks.named("jar")) +} diff --git a/lint/src/main/kotlin/info/appdev/charting/lint/EntryUsageDetector.kt b/lint/src/main/kotlin/info/appdev/charting/lint/EntryUsageDetector.kt new file mode 100644 index 000000000..15c63fac1 --- /dev/null +++ b/lint/src/main/kotlin/info/appdev/charting/lint/EntryUsageDetector.kt @@ -0,0 +1,129 @@ +package info.appdev.charting.lint + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.LintFix +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import org.jetbrains.uast.UReferenceExpression +import org.jetbrains.uast.UImportStatement +import org.jetbrains.uast.getParentOfType + +/** + * Lint detector that flags all usages of deprecated legacy entry classes and offers + * auto-fixes to replace them with either the `*Float` or `*Double` equivalent. + * + * Deprecated → Float replacement → Double replacement: + * Entry → EntryFloat → EntryDouble + * BarEntry → BarEntryFloat → BarEntryDouble + * BubbleEntry → BubbleEntryFloat → BubbleEntryDouble + * CandleEntry → CandleEntryFloat → CandleEntryDouble + * PieEntry → PieEntryFloat → PieEntryDouble + * RadarEntry → RadarEntryFloat → RadarEntryDouble + */ +class EntryUsageDetector : Detector(), SourceCodeScanner { + + companion object { + private const val PKG = "info.appdev.charting.data" + + /** Deprecated FQN → Float replacement FQN */ + private val FLOAT_REPLACEMENTS: Map = mapOf( + "$PKG.Entry" to "$PKG.EntryFloat", + "$PKG.BarEntry" to "$PKG.BarEntryFloat", + "$PKG.BubbleEntry" to "$PKG.BubbleEntryFloat", + "$PKG.CandleEntry" to "$PKG.CandleEntryFloat", + "$PKG.PieEntry" to "$PKG.PieEntryFloat", + "$PKG.RadarEntry" to "$PKG.RadarEntryFloat", + ) + + /** Deprecated FQN → Double replacement FQN */ + private val DOUBLE_REPLACEMENTS: Map = mapOf( + "$PKG.Entry" to "$PKG.EntryDouble", + "$PKG.BarEntry" to "$PKG.BarEntryDouble", + "$PKG.BubbleEntry" to "$PKG.BubbleEntryDouble", + "$PKG.CandleEntry" to "$PKG.CandleEntryDouble", + "$PKG.PieEntry" to "$PKG.PieEntryDouble", + "$PKG.RadarEntry" to "$PKG.RadarEntryDouble", + ) + + @JvmField + val ISSUE: Issue = Issue.create( + id = "LegacyEntryUsage", + briefDescription = "Replace deprecated legacy entry class with its `*Float` or `*Double` equivalent", + explanation = """ + Several entry classes (`Entry`, `BarEntry`, `BubbleEntry`, `CandleEntry`, \ + `PieEntry`, `RadarEntry`) are deprecated and will be removed in a future version. \ + Replace them with their `*Float` equivalents for identical precision, or \ + `*Double` equivalents for higher precision. + """, + category = Category.CORRECTNESS, + priority = 6, + severity = Severity.ERROR, + implementation = Implementation( + EntryUsageDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + private val DEPRECATED_SIMPLE_NAMES: Set = + FLOAT_REPLACEMENTS.keys.map { it.substringAfterLast('.') }.toSet() + } + + override fun getApplicableReferenceNames(): List = + DEPRECATED_SIMPLE_NAMES.toList() + + override fun visitReference( + context: JavaContext, + reference: UReferenceExpression, + referenced: PsiElement + ) { + if (referenced !is PsiClass) return + val deprecatedFqn = referenced.qualifiedName ?: return + val floatFqn = FLOAT_REPLACEMENTS[deprecatedFqn] ?: return + val doubleFqn = DOUBLE_REPLACEMENTS[deprecatedFqn] ?: return + + val deprecatedSimple = deprecatedFqn.substringAfterLast('.') + val floatSimple = floatFqn.substringAfterLast('.') + val doubleSimple = doubleFqn.substringAfterLast('.') + + val importNode = reference.getParentOfType() + val combinedFix: LintFix = if (importNode != null) { + // For import statements replace the full FQN + fix().alternatives( + fix().replace().name("Replace with $floatSimple") + .text(deprecatedFqn).with(floatFqn).autoFix().build(), + fix().replace().name("Replace with $doubleSimple (high precision)") + .text(deprecatedFqn).with(doubleFqn).autoFix().build() + ) + } else { + // For type refs / constructor calls replace only the simple name + fix().alternatives( + fix().replace().name("Replace with $floatSimple") + .text(deprecatedSimple).with(floatSimple).autoFix().build(), + fix().replace().name("Replace with $doubleSimple (high precision)") + .text(deprecatedSimple).with(doubleSimple).autoFix().build() + ) + } + + val location = if (importNode != null) + context.getLocation(importNode) + else + context.getLocation(reference) + + val element = importNode ?: reference + + context.report( + ISSUE, + element, + location, + "Replace deprecated `$deprecatedSimple` — use `$floatSimple` (same precision) or `$doubleSimple` (higher precision)", + combinedFix + ) + } +} diff --git a/lint/src/main/kotlin/info/appdev/charting/lint/LintRegistry.kt b/lint/src/main/kotlin/info/appdev/charting/lint/LintRegistry.kt new file mode 100644 index 000000000..7597608d7 --- /dev/null +++ b/lint/src/main/kotlin/info/appdev/charting/lint/LintRegistry.kt @@ -0,0 +1,26 @@ +package info.appdev.charting.lint + +import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.client.api.Vendor +import com.android.tools.lint.detector.api.CURRENT_API +import com.android.tools.lint.detector.api.Issue + +class LintRegistry : IssueRegistry() { + + override val issues: List = listOf( + EntryUsageDetector.ISSUE, + RawTypeDataSetDetector.ISSUE + ) + + /** Must match the Lint API version used at compile time. */ + override val api: Int = CURRENT_API + + /** Minimum Lint API version this registry works with. */ + override val minApi: Int = 14 + + override val vendor: Vendor = Vendor( + vendorName = "AndroidChart", + feedbackUrl = "https://github.com/AppDevNext/AndroidChart/issues" + ) +} + diff --git a/lint/src/main/kotlin/info/appdev/charting/lint/RawTypeDataSetDetector.kt b/lint/src/main/kotlin/info/appdev/charting/lint/RawTypeDataSetDetector.kt new file mode 100644 index 000000000..36b684385 --- /dev/null +++ b/lint/src/main/kotlin/info/appdev/charting/lint/RawTypeDataSetDetector.kt @@ -0,0 +1,154 @@ +package info.appdev.charting.lint + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiClassType +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UField +import org.jetbrains.uast.UImportStatement +import org.jetbrains.uast.ULocalVariable +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UParameter +import org.jetbrains.uast.UTypeReferenceExpression +import org.jetbrains.uast.getParentOfType + +class RawTypeDataSetDetector : Detector(), SourceCodeScanner { + + companion object { + private const val DATA_PKG = "info.appdev.charting.data" + private const val IFACE_PKG = "info.appdev.charting.interfaces.datasets" + private const val FLOAT_DEFAULT = "EntryFloat" + private const val DOUBLE_DEFAULT = "EntryDouble" + + private val GENERIC_TYPES: Map> = mapOf( + "$DATA_PKG.DataSet" to (FLOAT_DEFAULT to DOUBLE_DEFAULT), + "$DATA_PKG.LineDataSet" to (FLOAT_DEFAULT to DOUBLE_DEFAULT), + "$IFACE_PKG.IDataSet" to (FLOAT_DEFAULT to DOUBLE_DEFAULT), + "$IFACE_PKG.ILineDataSet" to (FLOAT_DEFAULT to DOUBLE_DEFAULT), + "$IFACE_PKG.ILineRadarDataSet" to (FLOAT_DEFAULT to DOUBLE_DEFAULT), + "$IFACE_PKG.ILineScatterCandleRadarDataSet" to (FLOAT_DEFAULT to DOUBLE_DEFAULT), + "$IFACE_PKG.IBarLineScatterCandleBubbleDataSet" to (FLOAT_DEFAULT to DOUBLE_DEFAULT), + ) + + @JvmField + val ISSUE: Issue = Issue.create( + id = "RawTypeDataSet", + briefDescription = "Specify an explicit entry type parameter instead of `<*>` or no argument", + explanation = """ + Using a star projection `Type<*>` or omitting the type argument entirely loses \ + compile-time type-safety. Replace with `` (same precision as the \ + legacy API) or `` (higher precision). \ + Example: `LineDataSet` or `LineDataSet<*>` → `LineDataSet`. + """, + category = Category.CORRECTNESS, + priority = 5, + severity = Severity.ERROR, + implementation = Implementation( + RawTypeDataSetDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } + + // ----------------------------------------------------------------------- + // Why multiple node types? + // + // UTypeReferenceExpression in getApplicableUastTypes() is only traversed + // for INLINE expressions (e.g. `is LineDataSet` checks). + // + // For type ANNOTATIONS on declarations the UTypeReferenceExpression is + // NOT a traversed tree node — it is only accessible as a property: + // ULocalVariable.typeReference → val x: LineDataSet<*> + // UField.typeReference → class Foo { val x: LineDataSet<*> } + // UParameter.typeReference → fun f(x: LineDataSet<*>) + // UMethod.returnTypeReference → fun f(): LineDataSet<*> + // + // We therefore visit all container node types and pull the typeReference + // from each one explicitly. + // ----------------------------------------------------------------------- + override fun getApplicableUastTypes(): List> = listOf( + ULocalVariable::class.java, // val x: LineDataSet<*> + UField::class.java, // class Foo { val x: LineDataSet<*> } + UParameter::class.java, // fun f(x: LineDataSet<*>) + UMethod::class.java, // fun f(): LineDataSet<*> + UTypeReferenceExpression::class.java, // is LineDataSet, cast as LineDataSet<*>, … + ) + + override fun createUastHandler(context: JavaContext): UElementHandler = + object : UElementHandler() { + + override fun visitLocalVariable(node: ULocalVariable) = + checkTypeRef(context, node.typeReference) + + override fun visitField(node: UField) = + checkTypeRef(context, node.typeReference) + + override fun visitParameter(node: UParameter) = + checkTypeRef(context, node.typeReference) + + override fun visitMethod(node: UMethod) = + checkTypeRef(context, node.returnTypeReference) + + // Catches inline type positions: `is LineDataSet`, `as LineDataSet<*>`, … + override fun visitTypeReferenceExpression(node: UTypeReferenceExpression) { + if (node.getParentOfType() != null) return + checkTypeRefNode(context, node) + } + + // ---- shared implementation ---------------------------------------- + + private fun checkTypeRef(context: JavaContext, typeRef: UTypeReferenceExpression?) { + if (typeRef == null) return + if (typeRef.getParentOfType() != null) return + checkTypeRefNode(context, typeRef) + } + + private fun checkTypeRefNode(context: JavaContext, node: UTypeReferenceExpression) { + val sourcePsi = node.sourcePsi ?: return + val nodeText = sourcePsi.text.trim() + + for ((fqn, typeArgs) in GENERIC_TYPES) { + val simpleName = fqn.substringAfterLast('.') + val isStarProjection = nodeText == "$simpleName<*>" + val isMissingTypeArg = nodeText == simpleName + if (!isStarProjection && !isMissingTypeArg) continue + + // Verify FQN when the type resolves (guards against same-named classes) + val resolved = (node.type as? PsiClassType)?.resolve() + if (resolved != null && resolved.qualifiedName != fqn) continue + + val (floatArg, doubleArg) = typeArgs + val (oldText, newFloat, newDouble) = if (isStarProjection) + Triple("$simpleName<*>", "$simpleName<$floatArg>", "$simpleName<$doubleArg>") + else + Triple(simpleName, "$simpleName<$floatArg>", "$simpleName<$doubleArg>") + + context.report( + ISSUE, + node, + context.getLocation(sourcePsi), + if (isStarProjection) "Replace `$simpleName<*>` with an explicit entry type for type-safety" + else "Add missing type argument to `$simpleName`", + fix().alternatives( + fix().replace() + .name("Replace with $newFloat") + .text(oldText).with(newFloat) + .autoFix().build(), + fix().replace() + .name("Replace with $newDouble (high precision)") + .text(oldText).with(newDouble) + .autoFix().build() + ) + ) + break + } + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 2edcf4e5b..2e108c247 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,4 @@ include("chartLib") include("chartLibCompose") include("app") +include("lint")