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
6 changes: 5 additions & 1 deletion .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,15 @@ A CLI and Android Studio plugin for generating Clean Architecture boilerplate.
| Generate a data source | ✔️ | ✔️ |
| Automatic git staging | ✔️ | ✔️ |
| Configurable | ✔️ | ✔️ |
| Inline inspections | ✔️ | ❌️ |

**Android Studio Plugin** is available on the IDE Plugins Marketplace.

**Terminal command** is available via Homebrew.

## Android Studio plugin

Adds multiple time-saving code generation shortcuts to Android Studio.
Adds multiple time-saving code generation shortcuts to Android Studio.

### Usage

Expand All @@ -53,6 +54,9 @@ Settings are available under `Tools` > `Clean Architecture`.

For a working project example, visit [Clean Architecture For Android](https://github.com/EranBoudjnah/CleanArchitectureForAndroid).

#### Inspections
Work out of the box. You can disable any or all inspections via Android Studio's settings.

## CLI

Generates Clean Architecture Android code from your terminal.
Expand Down
1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

## [0.3.0]

### Added
- Added inspections to the plugin
- Added a reference project to README for easier onboarding (#29)
- Added Homebrew installation instructions to README (#28)
- Added backwards compatibility for Android Studio Meerkat (#25)
- Added settings for custom git path (#20)
- Added version output to the CLI (#26)
- Added git automation to the CLI for staging and commits (#19)

### Changed
- Renamed generated binary from cli to cag (#27)
- Migrated UI to use JetBrains UI DSL v2 (#24)
- Deduplicated version catalog entries (#23)
- Refreshed and updated the README content (#30)

### Fixed
- Removed dexmaker from presentation-test module (#22)
- Ensured new project minimum SDK version respects IDE-specified value (#21)

## [0.2.0]

### Added
Expand Down
2 changes: 1 addition & 1 deletion cli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ plugins {
}

group = "com.mitteloupe.cag"
version = "0.0.2"
version = "0.3.0"

repositories {
mavenCentral()
Expand Down
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ mockk = "1.13.11"
junit4 = "4.13.2"
androidStudio = "2025.1.4.7"
hamcrest = "3.0"
lint = "31.13.0"

[libraries]
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
junit4 = { module = "junit:junit", version.ref = "junit4" }
hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" }
lint-api = { module = "com.android.tools.lint:lint-api", version.ref = "lint" }
lint-checks = { module = "com.android.tools.lint:lint-checks", version.ref = "lint" }

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
Expand Down
32 changes: 32 additions & 0 deletions lint/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
id("java-library")
alias(libs.plugins.kotlin.jvm)
}

kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
}
}

repositories {
google()
mavenCentral()
}

dependencies {
implementation(kotlin("stdlib"))

compileOnly(libs.lint.api)
compileOnly(libs.lint.checks)

testImplementation(libs.junit4)
}

tasks.register<Jar>("lintJar") {
dependsOn(tasks.named("compileKotlin"))
from(sourceSets.main.get().output)
archiveBaseName.set("lint")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.mitteloupe.cag.cleanarchitecturegenerator.lint

import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.detector.api.CURRENT_API
import com.android.tools.lint.detector.api.Issue

class CagIssueRegistry : IssueRegistry() {
override val issues: List<Issue>
get() = listOf(ViewModelPublicFunctionShouldStartWithOnDetector.ISSUE)

override val api: Int = CURRENT_API
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.mitteloupe.cag.cleanarchitecturegenerator.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.mitteloupe.cag.cleanarchitecturegenerator.rule.ViewModelPublicFunctionShouldStartWithOnRule
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.UastVisibility

class ViewModelPublicFunctionShouldStartWithOnDetector : Detector(), Detector.UastScanner {

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

override fun createUastHandler(context: JavaContext) =
object : UElementHandler() {
override fun visitMethod(node: UMethod) {
val functionName = node.name
val isValid = ViewModelPublicFunctionShouldStartWithOnRule.isValid(
className = node.containingClass?.name,
functionName = functionName,
isPublic = node.visibility == UastVisibility.PUBLIC
)

if (!isValid) {
context.report(
ISSUE,
node,
context.getNameLocation(node),
ViewModelPublicFunctionShouldStartWithOnRule.violationMessage(functionName)
)
}
}
}

companion object {
val ISSUE: Issue = Issue.create(
id = "ViewModelPublicFunctionShouldStartWithOn",
briefDescription = "Public ViewModel functions should start with 'on'",
explanation = """
Public functions in ViewModel classes should start with 'on' to clearly indicate that they represent user or system events, \
following Clean Architecture naming conventions for better readability and maintainability.
""".trimIndent(),
category = Category.CORRECTNESS,
priority = 5,
severity = Severity.WARNING,
implementation = Implementation(
ViewModelPublicFunctionShouldStartWithOnDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.mitteloupe.cag.cleanarchitecturegenerator.rule

import com.intellij.codeInsight.intention.FileModifier.SafeTypeForPreview

private val uiTermsRegex =
"(${firstLetter("c")}lick(ed)?|${firstLetter("s")}croll(ed)?|${firstLetter("s")}wipe(d)?|${firstLetter("t")}ap(ped)?|${firstLetter("l")}ongPress(ed)?|${firstLetter("t")}ouch(ed)?)".toRegex()

private fun firstLetter(firstCharacter: String) =
"((?<=\\w)${firstCharacter.uppercase()}|^${firstCharacter.lowercase()})"

object ViewModelFunctionShouldNotContainUiTermsRule {
fun validate(
className: String?,
functionName: String
): Result {
if (className?.endsWith("ViewModel") != true) {
return Result.Valid
}

val uiTermsMatchResult = uiTermsRegex.find(functionName) ?: return Result.Valid

return Result.Invalid(functionName = functionName, offendingValue = uiTermsMatchResult.value)
}

fun violationMessage(offendingValue: String): String =
"Function name contains UI term '$offendingValue', consider replacing it with 'Action'"

sealed interface Result {
data object Valid : Result

@SafeTypeForPreview
data class Invalid(
private val functionName: String,
val offendingValue: String
) : Result {
fun fixFunctionName(): String {
val capitalizedA =
if (functionName.indexOf(offendingValue) == 0) {
"a"
} else {
"A"
}
return functionName.replaceFirst(offendingValue.toRegex(RegexOption.IGNORE_CASE), "${capitalizedA}ction")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.mitteloupe.cag.cleanarchitecturegenerator.rule

object ViewModelPublicFunctionShouldStartWithOnRule {
private val validFunctionNameRegex = Regex("^on[A-Z0-9].*$")

fun isValid(
className: String?,
functionName: String,
isPublic: Boolean
): Boolean =
!isPublic ||
className?.endsWith("ViewModel") != true ||
functionName.matches(validFunctionNameRegex)

fun violationMessage(functionName: String): String =
"ViewModel public function name '$functionName' should start with 'on'"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.mitteloupe.cag.cleanarchitecturegenerator.lint.CagIssueRegistry
Loading