Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ Some of the concepts implemented here:
- [x] multi module setup (see `:common` & `:domain` & `:features`)
- [x] sharing build logic with [gradle convention plugin](https://docs.gradle.org/current/samples/sample_convention_plugins.html)
- [x] gradle version catalog, BOM & Bundles
- [ ] compose-navigation between feature modules
- [x] compose-navigation between feature modules
- [x] dependency injection with kotlin-inject(-anvil)
- [x] custom lint-rules
- [ ] USF architecture (much like [usf-movies-android](https://github.com/kaushikgopal/movies-usf-android))
- [x] logcat lib and injecting multiple loggers


# Getting started
- Download this repository and open the template folder on Android Studio
- in libs.versions.toml change app-namespace to your desired package name
Expand Down
5 changes: 2 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id("com.android.application")
id("template.feature")
id("template.feature") // comes packed with a lot of feature even at app level
}

android {
Expand All @@ -25,9 +25,8 @@ android {
}

dependencies {
// internal
implementation(project(":common:log"))

// internal
implementation(project(":domain:ui"))
implementation(project(":domain:shared"))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,25 @@ class TemplateFeatureConventionPlugin : Plugin<Project> {
override fun apply(project: Project) =
with(project) {
configAndroidAppAndLib(
androidApp = { project.applyAndroidConfig(this) },
androidLib = { project.applyAndroidConfig(this) },
androidApp = {
project.applyAndroidConfig(this)

// app level lint settings
lint {
quiet = true
// if true, stop the gradle build if errors are found
abortOnError = true
// if true, only report errors
ignoreWarnings = true
// Produce report for CI:
// https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/sarif-support-for-code-scanning
// sarifOutput = file("../lint-results.sarif")
textReport = true
}
},
androidLib = {
project.applyAndroidConfig(this)
},
)
}

Expand All @@ -40,6 +57,11 @@ class TemplateFeatureConventionPlugin : Plugin<Project> {
}

buildFeatures { compose = true } // enable compose functionality in Android Studio

lint {
// run lint on dependencies of this project
checkDependencies = true
}
}

plugins.apply(libs.plugins.kotlin.android.get().pluginId)
Expand All @@ -66,6 +88,16 @@ class TemplateFeatureConventionPlugin : Plugin<Project> {
// Navigation
implementation(libs.compose.navigation)
implementation(libs.kotlinx.serialization.json)

// internal dependencies
// be very judicious in adding more dependencies here
implementation(project(":common:log"))

// enable lint
val lintChecks by configurations
lintChecks(project(":common:lint-rules"))

// implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
}
}
}
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ plugins {
alias(libs.plugins.ksp) apply false

alias(libs.plugins.kotlin.serialization) apply false // needed for navigation

alias(libs.plugins.android.lint) apply false
}
18 changes: 18 additions & 0 deletions common/lint-rules/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
plugins {
id(libs.plugins.kotlin.jvm.get().pluginId)
id(libs.plugins.android.lint.get().pluginId)
}

lint {
htmlReport = true
htmlOutput = file("lint-report.html")
textReport = true
absolutePaths = false
ignoreTestSources = true
}


dependencies {
compileOnly(libs.bundles.lint.api)
// testImplementation(libs.bundles.lint.tests)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (C) 2024 The Android Open Source Project
// SPDX-License-Identifier: Apache-2.0
package sh.kau.playground.lint.checks

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.PsiMethod
import org.jetbrains.uast.UCallExpression

/**
* Adapted from:
* - https://github.com/googlesamples/android-custom-lint-rules/blob/main/checks/src/main/java/com/example/lint/checks/AvoidDateDetector.kt
*/
class AvoidDateDetector : Detector(), SourceCodeScanner {
companion object Issues {
private val IMPLEMENTATION =
Implementation(AvoidDateDetector::class.java, Scope.JAVA_FILE_SCOPE)

@JvmField
val ISSUE =
Issue.create(
id = "OldDate",
briefDescription = "Avoid Date and Calendar",
explanation =
"""
The `java.util.Date` and `java.util.Calendar` classes should not be used; instead \
use the `java.time` package, such as `LocalDate` and `LocalTime`.
""",
category = Category.CORRECTNESS,
priority = 6,
severity = Severity.ERROR,
androidSpecific = true,
implementation = IMPLEMENTATION,
)
}

// java.util.Date()
override fun getApplicableConstructorTypes(): List<String> = listOf("java.util.Date")

override fun visitConstructor(
context: JavaContext,
node: UCallExpression,
constructor: PsiMethod,
) {
context.report(
ISSUE,
node,
context.getLocation(node),
"Don't use `Date`; use `java.time.*` instead",
fix()
.alternatives(
fix().replace().all().with("java.time.LocalTime.now()").shortenNames().build(),
fix().replace().all().with("java.time.LocalDate.now()").shortenNames().build(),
fix().replace().all().with("java.time.LocalDateTime.now()").shortenNames()
.build(),
),
)
}

// java.util.Calendar.getInstance()
override fun getApplicableMethodNames(): List<String> = listOf("getInstance")

override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
val evaluator = context.evaluator
if (!evaluator.isMemberInClass(method, "java.util.Calendar")) {
return
}
context.report(
ISSUE,
node,
context.getLocation(node),
"Don't use `Calendar.getInstance`; use `java.time.*` instead",
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (C) 2024 Moxy Mouse, Inc.
// SPDX-License-Identifier: Apache-2.0
package sh.kau.playground.lint.checks

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 org.jetbrains.uast.UCallExpression

class CoerceAtLeastUsageDetector : Detector(), SourceCodeScanner {
companion object {
val ISSUE =
Issue.create(
id = "CoerceAtLeastUsage",
briefDescription = "Prefer maxOf over coerceAtLeast",
explanation =
"The `coerceAtLeast` method can be replaced with the simpler `maxOf` function.",
category = Category.CORRECTNESS,
priority = 6,
severity = Severity.ERROR,
implementation =
Implementation(CoerceAtLeastUsageDetector::class.java, Scope.JAVA_FILE_SCOPE)
)
}

override fun getApplicableUastTypes() = listOf(UCallExpression::class.java)


override fun createUastHandler(context: JavaContext) =
object : UElementHandler() {
override fun visitCallExpression(node: UCallExpression) {
if (node.methodName == "coerceAtLeast") {

// Get the receiver (the value before .coerceAtLeast)
val receiver = node.receiver?.asSourceString() ?: ""

// Get the argument (the value inside .coerceAtLeast)
val argument = node.valueArguments.firstOrNull()?.asSourceString() ?: ""

// Build the replacement string
val replacement = "maxOf($receiver, $argument)"

val fix =
fix()
.replace()
.all() // Replace the entire call expression, not just the method name
.with(replacement)
.build()

context.report(
ISSUE,
node,
context.getLocation(node),
"Prefer using `maxOf`",
fix
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (C) 2024 Moxy Mouse, Inc.
// SPDX-License-Identifier: Apache-2.0
package sh.kau.playground.lint.checks

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 org.jetbrains.uast.UCallExpression

class CoerceAtMostUsageDetector : Detector(), SourceCodeScanner {
companion object {
val ISSUE =
Issue.create(
id = "CoerceAtMostUsage",
briefDescription = "Prefer minOf over coerceAtMost",
explanation =
"The `coerceAtMost` method can be replaced with the simpler `minOf` function.",
category = Category.CORRECTNESS,
priority = 6,
severity = Severity.ERROR,
implementation =
Implementation(CoerceAtMostUsageDetector::class.java, Scope.JAVA_FILE_SCOPE)
)
}

override fun getApplicableUastTypes() = listOf(UCallExpression::class.java)

override fun createUastHandler(context: JavaContext) =
object : UElementHandler() {
override fun visitCallExpression(node: UCallExpression) {
if (node.methodName == "coerceAtMost") {

// Get the receiver (the value before .coerceAtMost)
val receiver = node.receiver?.asSourceString() ?: ""

// Get the argument (the value inside .coerceAtMost)
val argument = node.valueArguments.firstOrNull()?.asSourceString() ?: ""

// Build the replacement string
val replacement = "minOf($receiver, $argument)"

val fix =
fix()
.replace()
.all() // Replace the entire call expression, not just the method name
.with(replacement)
.build()

context.report(
CoerceAtLeastUsageDetector.ISSUE,
node,
context.getLocation(node),
"Prefer using `minOf`",
fix
)
}
}
}
}
Loading