mirror of
https://github.com/bitwarden/android.git
synced 2026-11-09 10:34:18 -06:00
[PM-34837] Add Detekt rule requiring named FontWeight constants
Add a detekt-rules module with a NamedFontWeight rule that reports numeric weights such as FontWeight.W600 or FontWeight(600) and suggests the named constant used in the Figma designs, such as FontWeight.SemiBold. The module is added as a non-transitive detektPlugins dependency so Detekt keeps its own Kotlin standard library and the root lockfile is unchanged.
This commit is contained in:
@@ -35,6 +35,8 @@ configurations.configureEach {
|
||||
dependencies {
|
||||
detektPlugins(libs.detekt.detekt.formatting)
|
||||
detektPlugins(libs.detekt.detekt.rules)
|
||||
// Bitwarden custom rules. Not transitive, so Detekt keeps its own Kotlin standard library.
|
||||
detektPlugins(project(":detekt-rules")) { isTransitive = false }
|
||||
|
||||
kover(project(":annotation"))
|
||||
kover(project(":app"))
|
||||
@@ -59,6 +61,7 @@ detekt {
|
||||
"core/src",
|
||||
"cxf/src",
|
||||
"data/src",
|
||||
"detekt-rules/src",
|
||||
"network/src",
|
||||
"testharness/src",
|
||||
"ui/src",
|
||||
|
||||
@@ -667,3 +667,8 @@ style:
|
||||
active: true
|
||||
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
|
||||
excludeImports: [ 'java.util.*', 'kotlinx.android.synthetic.*' ]
|
||||
|
||||
bitwarden:
|
||||
active: true
|
||||
NamedFontWeight:
|
||||
active: true
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,27 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
// The Kotlin Gradle plugin is already on the root build classpath, so no version is needed.
|
||||
id("org.jetbrains.kotlin.jvm")
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.toVersion(libs.versions.jvmTarget.get())
|
||||
targetCompatibility = JavaVersion.toVersion(libs.versions.jvmTarget.get())
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvmTarget.get()))
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.detekt.detekt.api)
|
||||
|
||||
testImplementation(libs.detekt.detekt.api)
|
||||
testImplementation(libs.detekt.detekt.test)
|
||||
testImplementation(platform(libs.junit.bom))
|
||||
testImplementation(libs.junit.jupiter)
|
||||
testRuntimeOnly(libs.junit.platform.launcher)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.bitwarden.detekt
|
||||
|
||||
import io.gitlab.arturbosch.detekt.api.Config
|
||||
import io.gitlab.arturbosch.detekt.api.RuleSet
|
||||
import io.gitlab.arturbosch.detekt.api.RuleSetProvider
|
||||
|
||||
/**
|
||||
* Provides Bitwarden's custom Detekt rules.
|
||||
*/
|
||||
class BitwardenRuleSetProvider : RuleSetProvider {
|
||||
override val ruleSetId: String = "bitwarden"
|
||||
|
||||
override fun instance(config: Config): RuleSet =
|
||||
RuleSet(
|
||||
id = ruleSetId,
|
||||
rules = listOf(
|
||||
NamedFontWeight(config),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.bitwarden.detekt
|
||||
|
||||
import io.gitlab.arturbosch.detekt.api.CodeSmell
|
||||
import io.gitlab.arturbosch.detekt.api.Config
|
||||
import io.gitlab.arturbosch.detekt.api.Debt
|
||||
import io.gitlab.arturbosch.detekt.api.Entity
|
||||
import io.gitlab.arturbosch.detekt.api.Issue
|
||||
import io.gitlab.arturbosch.detekt.api.Rule
|
||||
import io.gitlab.arturbosch.detekt.api.Severity
|
||||
import org.jetbrains.kotlin.psi.KtCallExpression
|
||||
import org.jetbrains.kotlin.psi.KtConstantExpression
|
||||
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
|
||||
import org.jetbrains.kotlin.psi.KtExpression
|
||||
|
||||
/**
|
||||
* Reports numeric `FontWeight` values, such as `FontWeight.W600` or `FontWeight(600)`, and
|
||||
* suggests the named constant used in the Figma designs, such as `FontWeight.SemiBold`.
|
||||
*/
|
||||
class NamedFontWeight(config: Config) : Rule(config) {
|
||||
|
||||
override val issue: Issue = Issue(
|
||||
id = javaClass.simpleName,
|
||||
severity = Severity.Style,
|
||||
description = "Use named FontWeight constants to match the Figma designs.",
|
||||
debt = Debt.FIVE_MINS,
|
||||
)
|
||||
|
||||
override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) {
|
||||
super.visitDotQualifiedExpression(expression)
|
||||
if (!expression.receiverExpression.text.isFontWeight()) return
|
||||
val selector = expression.selectorExpression?.text ?: return
|
||||
if (!selector.startsWith(prefix = "W")) return
|
||||
val weight = selector.removePrefix(prefix = "W").toIntOrNull() ?: return
|
||||
reportIfNamed(expression = expression, current = "FontWeight.$selector", weight = weight)
|
||||
}
|
||||
|
||||
override fun visitCallExpression(expression: KtCallExpression) {
|
||||
super.visitCallExpression(expression)
|
||||
if (expression.calleeExpression?.text?.isFontWeight() != true) return
|
||||
val argument = expression.valueArguments.singleOrNull()?.getArgumentExpression()
|
||||
val weight = (argument as? KtConstantExpression)?.text?.toIntOrNull() ?: return
|
||||
reportIfNamed(expression = expression, current = "FontWeight($weight)", weight = weight)
|
||||
}
|
||||
|
||||
private fun reportIfNamed(expression: KtExpression, current: String, weight: Int) {
|
||||
val name = NAMED_WEIGHTS[weight] ?: return
|
||||
report(
|
||||
CodeSmell(
|
||||
issue = issue,
|
||||
entity = Entity.from(expression),
|
||||
message = "Use FontWeight.$name instead of $current.",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.isFontWeight(): Boolean =
|
||||
this == FONT_WEIGHT || endsWith(suffix = ".$FONT_WEIGHT")
|
||||
|
||||
private companion object {
|
||||
const val FONT_WEIGHT: String = "FontWeight"
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
val NAMED_WEIGHTS: Map<Int, String> = mapOf(
|
||||
100 to "Thin",
|
||||
200 to "ExtraLight",
|
||||
300 to "Light",
|
||||
400 to "Normal",
|
||||
500 to "Medium",
|
||||
600 to "SemiBold",
|
||||
700 to "Bold",
|
||||
800 to "ExtraBold",
|
||||
900 to "Black",
|
||||
)
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
com.bitwarden.detekt.BitwardenRuleSetProvider
|
||||
@@ -0,0 +1,4 @@
|
||||
bitwarden:
|
||||
active: true
|
||||
NamedFontWeight:
|
||||
active: true
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.bitwarden.detekt
|
||||
|
||||
import io.gitlab.arturbosch.detekt.api.Config
|
||||
import io.gitlab.arturbosch.detekt.test.lint
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class NamedFontWeightTest {
|
||||
|
||||
private val rule = NamedFontWeight(Config.empty)
|
||||
|
||||
@Test
|
||||
fun `numeric constant should be reported with the named replacement`() {
|
||||
val findings = rule.lint("val weight = FontWeight.W600")
|
||||
|
||||
assertEquals(1, findings.size)
|
||||
assertEquals(
|
||||
"Use FontWeight.SemiBold instead of FontWeight.W600.",
|
||||
findings.single().message,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fully qualified numeric constant should be reported`() {
|
||||
val findings = rule.lint("val weight = androidx.compose.ui.text.font.FontWeight.W400")
|
||||
|
||||
assertEquals(1, findings.size)
|
||||
assertEquals(
|
||||
"Use FontWeight.Normal instead of FontWeight.W400.",
|
||||
findings.single().message,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `numeric constructor should be reported with the named replacement`() {
|
||||
val findings = rule.lint("val weight = FontWeight(700)")
|
||||
|
||||
assertEquals(1, findings.size)
|
||||
assertEquals("Use FontWeight.Bold instead of FontWeight(700).", findings.single().message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `named constant should not be reported`() {
|
||||
assertEquals(0, rule.lint("val weight = FontWeight.SemiBold").size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `weight without a named constant should not be reported`() {
|
||||
assertEquals(0, rule.lint("val weight = FontWeight(650)").size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `weight property should not be reported`() {
|
||||
assertEquals(0, rule.lint("val value = FontWeight.Bold.weight").size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `numeric constants in a text style should each be reported`() {
|
||||
val code = """
|
||||
val style = TextStyle(fontWeight = FontWeight.W500)
|
||||
val family = FontFamily(Font(1, weight = FontWeight.W900))
|
||||
""".trimIndent()
|
||||
|
||||
assertEquals(2, rule.lint(code).size)
|
||||
}
|
||||
}
|
||||
@@ -101,8 +101,10 @@ coil-coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
|
||||
coil-coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
|
||||
coil-coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" }
|
||||
coil-coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
|
||||
detekt-detekt-api = { module = "io.gitlab.arturbosch.detekt:detekt-api", version.ref = "detekt" }
|
||||
detekt-detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
|
||||
detekt-detekt-rules = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libraries", version.ref = "detekt" }
|
||||
detekt-detekt-test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version.ref = "detekt" }
|
||||
google-billing = { module = "com.android.billingclient:billing", version.ref = "googleBilling" }
|
||||
google-firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
|
||||
google-firebase-cloud-messaging = { module = "com.google.firebase:firebase-messaging" }
|
||||
|
||||
@@ -48,6 +48,7 @@ include(
|
||||
":core",
|
||||
":cxf",
|
||||
":data",
|
||||
":detekt-rules",
|
||||
":network",
|
||||
":testharness",
|
||||
":ui",
|
||||
|
||||
Reference in New Issue
Block a user