Skip to content

Commit e11638d

Browse files
authored
Merge pull request #4 from mohdaquib/feature/scanner-engine
Feature/scanner engine
2 parents 0514b41 + b65ab66 commit e11638d

17 files changed

Lines changed: 951 additions & 27 deletions

File tree

.idea/gradle.xml

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/studiobot.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/build.gradle.kts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,12 @@ plugins {
33
alias(libs.plugins.kotlin.compose)
44
alias(libs.plugins.hilt)
55
alias(libs.plugins.ksp)
6+
alias(libs.plugins.kotlin.android)
67
}
78

89
android {
910
namespace = "com.composea11yscanner"
10-
compileSdk {
11-
version = release(36) {
12-
minorApiLevel = 1
13-
}
14-
}
11+
compileSdk = 36
1512

1613
defaultConfig {
1714
applicationId = "com.composea11yscanner"
@@ -63,4 +60,4 @@ dependencies {
6360
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
6461
debugImplementation(libs.androidx.compose.ui.tooling)
6562
debugImplementation(libs.androidx.compose.ui.test.manifest)
66-
}
63+
}

gradle.properties

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
# Project-wide Gradle settings.
2-
# IDE (e.g. Android Studio) users:
3-
# Gradle settings configured through the IDE *will override*
4-
# any settings specified in this file.
5-
# For more details on how to configure your build environment visit
1+
## For more details on how to configure your build environment visit
62
# http://www.gradle.org/docs/current/userguide/build_environment.html
3+
#
74
# Specifies the JVM arguments used for the daemon process.
85
# The setting is particularly useful for tweaking memory settings.
9-
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
6+
# Default value: -Xmx1024m -XX:MaxPermSize=256m
7+
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
8+
#
109
# When configured, Gradle will run in incubating parallel mode.
1110
# This option should only be used with decoupled projects. For more details, visit
1211
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
1312
# org.gradle.parallel=true
14-
# Kotlin code style for this project: "official" or "obsolete":
15-
kotlin.code.style=official
13+
#Wed May 06 12:20:37 SGT 2026
14+
android.enableJetifier=true
15+
android.useAndroidX=true
16+
kotlin.code.style=official
17+
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding\=UTF-8

gradle/libs.versions.toml

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
[versions]
2-
agp = "9.1.1"
3-
coreKtx = "1.18.0"
2+
agp = "8.13.2"
3+
coreKtx = "1.15.0"
44
junit = "4.13.2"
5-
junitVersion = "1.3.0"
6-
espressoCore = "3.7.0"
7-
lifecycleRuntimeKtx = "2.10.0"
8-
activityCompose = "1.13.0"
9-
kotlin = "2.2.10"
10-
composeBom = "2024.09.00"
11-
hilt = "2.59.2"
12-
ksp = "2.3.7"
5+
junitVersion = "1.2.1"
6+
espressoCore = "3.6.1"
7+
lifecycleRuntimeKtx = "2.8.7"
8+
activityCompose = "1.9.3"
9+
kotlin = "2.3.21"
10+
composeBom = "2024.12.01"
11+
hilt = "2.54"
12+
ksp = "2.1.0-1.0.29"
1313
coroutines = "1.10.1"
14+
mockk = "1.13.12"
15+
turbine = "1.2.0"
1416
detekt = "1.23.7"
1517

1618
[libraries]
@@ -20,9 +22,11 @@ androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "j
2022
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
2123
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
2224
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
23-
androidx-compose-bom = { group = "androidx.compose.bom", name = "compose-bom", version.ref = "composeBom" }
25+
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
2426
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
2527
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
28+
androidx-compose-ui-unit = { group = "androidx.compose.ui", name = "ui-unit" }
29+
androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util" }
2630
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
2731
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
2832
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
@@ -32,6 +36,9 @@ hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref
3236
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
3337
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
3438
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
39+
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
40+
turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" }
41+
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }
3542
detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt" }
3643

3744
[plugins]

sample/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ plugins {
33
alias(libs.plugins.kotlin.compose)
44
alias(libs.plugins.hilt)
55
alias(libs.plugins.ksp)
6+
alias(libs.plugins.kotlin.android)
67
}
78

89
android {

scanner-core/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ plugins {
22
alias(libs.plugins.android.library)
33
alias(libs.plugins.hilt)
44
alias(libs.plugins.ksp)
5+
alias(libs.plugins.kotlin.android)
56
}
67

78
android {
@@ -35,7 +36,13 @@ dependencies {
3536
ksp(libs.hilt.android.compiler)
3637
implementation(libs.kotlinx.coroutines.core)
3738
implementation(libs.kotlinx.coroutines.android)
39+
implementation(platform(libs.androidx.compose.bom))
40+
implementation(libs.androidx.compose.ui)
41+
implementation(libs.androidx.compose.ui.graphics)
3842
testImplementation(libs.junit)
43+
testImplementation(libs.mockk)
44+
testImplementation(libs.turbine)
45+
testImplementation(libs.kotlinx.coroutines.test)
3946
androidTestImplementation(libs.androidx.junit)
4047
androidTestImplementation(libs.androidx.espresso.core)
4148
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.composea11yscanner.core
2+
3+
import com.composea11yscanner.core.model.A11yIssue
4+
import com.composea11yscanner.core.model.A11yNode
5+
import com.composea11yscanner.core.model.ScanResult
6+
import com.composea11yscanner.core.model.ScannerConfig
7+
import com.composea11yscanner.core.model.ScannerState
8+
import com.composea11yscanner.core.rule.A11yRule
9+
import kotlinx.coroutines.CancellationException
10+
import kotlinx.coroutines.Dispatchers
11+
import kotlinx.coroutines.flow.Flow
12+
import kotlinx.coroutines.flow.flow
13+
import kotlinx.coroutines.flow.flowOn
14+
import java.util.UUID
15+
16+
/**
17+
* Core orchestrator: runs a fixed set of accessibility rules over a list of nodes and
18+
* streams scan progress as a [Flow] of [ScannerState].
19+
*
20+
* Emission sequence:
21+
* Scanning(0f) → Scanning(1/n) → … → Scanning(1f) → Complete(result)
22+
* or Complete(emptyResult) when no enabled rules / no nodes.
23+
* or Error(message) if a rule throws an unexpected exception.
24+
*
25+
* The entire flow body runs on [Dispatchers.Default] via [flowOn]; collectors receive
26+
* emissions on their own dispatcher.
27+
*
28+
* @param rules All candidate rules. Only those whose [A11yRule.ruleId] appears in
29+
* [config.enabledRules] will be evaluated — defensive against callers that pass the
30+
* full rule set regardless of config.
31+
*/
32+
class A11yScanEngine(
33+
rules: List<A11yRule>,
34+
private val config: ScannerConfig,
35+
) {
36+
private val enabledRules: List<A11yRule> = rules.filter { it.ruleId in config.enabledRules }
37+
38+
fun scan(nodes: List<A11yNode>): Flow<ScannerState> = flow {
39+
// Fast path: nothing to evaluate.
40+
if (enabledRules.isEmpty() || nodes.isEmpty()) {
41+
emit(ScannerState.Complete(buildResult(nodes.size, emptyList(), emptySet())))
42+
return@flow
43+
}
44+
45+
emit(ScannerState.Scanning(0f))
46+
47+
val allIssues = mutableListOf<A11yIssue>()
48+
val failedRuleIds = mutableSetOf<String>()
49+
50+
try {
51+
enabledRules.forEachIndexed { index, rule ->
52+
val issues = rule.evaluateAll(nodes)
53+
allIssues += issues
54+
if (issues.isNotEmpty()) failedRuleIds += rule.ruleId
55+
// Progress advances to 1f after the last rule.
56+
emit(ScannerState.Scanning((index + 1f) / enabledRules.size))
57+
}
58+
emit(ScannerState.Complete(buildResult(nodes.size, allIssues, failedRuleIds)))
59+
} catch (e: CancellationException) {
60+
throw e // never swallow cancellation
61+
} catch (e: Exception) {
62+
emit(ScannerState.Error(e.message ?: "Scan failed"))
63+
}
64+
}.flowOn(Dispatchers.Default)
65+
66+
// --- helpers ---
67+
68+
private fun buildResult(
69+
totalNodes: Int,
70+
issues: List<A11yIssue>,
71+
failedRuleIds: Set<String>,
72+
): ScanResult = ScanResult(
73+
scanId = UUID.randomUUID().toString(),
74+
timestamp = System.currentTimeMillis(),
75+
totalNodes = totalNodes,
76+
issues = issues.sortedWith(compareBy({ it.severity.sortOrder }, { it.ruleId })),
77+
passedRules = enabledRules.size - failedRuleIds.size,
78+
failedRules = failedRuleIds.size,
79+
)
80+
}

0 commit comments

Comments
 (0)