Skip to content

Commit f46a4c3

Browse files
authored
Polish context output and auto-discover dep scopes (#5)
* Polish context output and auto-discover dependency scopes - Skip empty root project row in projects table - Overview counts symbols from included builds - Proper singular/plural: "1 project", "2 dependents" - Rename "Dashboard" column to "Context" - Deduplicate findings across source sets - Auto-discover all dependency configurations instead of hardcoded whitelist - New excludeDepScopes extension property (defaults exclude Kotlin internals) * Address CodeRabbit: exclude-based PSI dep parsing, task-level property wiring - PSI build file parser now uses exclude-based model matching Gradle API path - PSI_SKIP_CALLS filters known non-dependency call expressions - projectDeps wired from task.excludeDepScopes (not extension directly)
1 parent 52e5c1a commit f46a4c3

8 files changed

Lines changed: 99 additions & 40 deletions

File tree

src/main/kotlin/zone/clanker/gradle/srcx/Srcx.kt

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import org.gradle.api.Project
66
import org.gradle.api.initialization.Settings
77
import org.gradle.api.logging.Logging
88
import org.gradle.api.provider.Property
9+
import org.gradle.api.provider.SetProperty
910
import zone.clanker.gradle.srcx.scan.ProjectScanner
1011
import zone.clanker.gradle.srcx.scan.SymbolExtractor
1112
import zone.clanker.gradle.srcx.task.CleanTask
@@ -43,6 +44,22 @@ data object Srcx {
4344
/** Task: delete the .srcx output directory. */
4445
const val TASK_CLEAN = "srcx-clean"
4546

47+
/** Dependency scopes excluded from scanning by default. */
48+
val DEFAULT_EXCLUDED_DEP_SCOPES: Set<String> =
49+
setOf(
50+
"archives",
51+
"default",
52+
"kotlinBuildToolsApiClasspath",
53+
"kotlinCompilerClasspath",
54+
"kotlinCompilerPluginClasspath",
55+
"kotlinCompilerPluginClasspathMain",
56+
"kotlinCompilerPluginClasspathTest",
57+
"kotlinKlibCommonizerClasspath",
58+
"kotlinNativeCompilerPluginClasspath",
59+
"kotlinScriptDef",
60+
"kotlinScriptDefExtensions",
61+
)
62+
4663
/** Delete an output directory, printing what was removed. */
4764
fun cleanOutputDir(dir: File) {
4865
if (dir.exists()) {
@@ -77,6 +94,9 @@ data object Srcx {
7794

7895
/** When true, compileKotlin/compileJava tasks will finalize with srcx-context. */
7996
abstract val autoGenerate: Property<Boolean>
97+
98+
/** Dependency scopes to exclude from scanning. All others are discovered automatically. */
99+
abstract val excludeDepScopes: SetProperty<String>
80100
}
81101

82102
/**
@@ -99,6 +119,7 @@ data object Srcx {
99119
val extension = settings.extensions.create(EXTENSION_NAME, SettingsExtension::class.java)
100120
extension.outputDir.convention(OUTPUT_DIR)
101121
extension.autoGenerate.convention(false)
122+
extension.excludeDepScopes.convention(DEFAULT_EXCLUDED_DEP_SCOPES)
102123

103124
settings.gradle.rootProject(
104125
Action { rootProject ->
@@ -131,10 +152,11 @@ data object Srcx {
131152
task.subprojectPaths.set(
132153
rootProject.provider { rootProject.subprojects.map { it.path } },
133154
)
155+
task.excludeDepScopes.convention(extension.excludeDepScopes)
134156
task.projectDeps.set(
135-
rootProject.provider {
157+
task.excludeDepScopes.map { excludes ->
136158
rootProject.allprojects.associate { proj ->
137-
proj.path to SymbolExtractor.extractDependenciesFromProject(proj)
159+
proj.path to SymbolExtractor.extractDependenciesFromProject(proj, excludes)
138160
}
139161
},
140162
)

src/main/kotlin/zone/clanker/gradle/srcx/report/DashboardRenderer.kt

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ internal class DashboardRenderer(
4646
}
4747

4848
private fun StringBuilder.appendOverview() {
49-
val totalSymbols = summaries.sumOf { it.symbols.size }
49+
val totalSymbols =
50+
summaries.sumOf { it.symbols.size } +
51+
includedBuildSummaries.values.sumOf { projects -> projects.sumOf { it.symbols.size } }
5052
val totalWarnings =
5153
summaries.sumOf { s ->
5254
s.analysis?.findings?.count { it.severity == FindingSeverity.WARNING } ?: 0
@@ -56,9 +58,16 @@ internal class DashboardRenderer(
5658

5759
appendLine("## Overview")
5860
appendLine()
59-
appendLine("- $totalSymbols symbols across ${summaries.size} project(s)")
60-
if (totalWarnings > 0) appendLine("- $totalWarnings warning(s)")
61-
if (includedBuilds.isNotEmpty()) appendLine("- ${includedBuilds.size} included build(s)")
61+
val projectLabel = if (summaries.size == 1) "project" else "projects"
62+
appendLine("- $totalSymbols symbols across ${summaries.size} $projectLabel")
63+
if (totalWarnings > 0) {
64+
val warningLabel = if (totalWarnings == 1) "warning" else "warnings"
65+
appendLine("- $totalWarnings $warningLabel")
66+
}
67+
if (includedBuilds.isNotEmpty()) {
68+
val buildLabel = if (includedBuilds.size == 1) "included build" else "included builds"
69+
appendLine("- ${includedBuilds.size} $buildLabel")
70+
}
6271
if (subprojects.isNotEmpty()) {
6372
appendLine("- subprojects: ${subprojects.joinToString(", ")}")
6473
}
@@ -76,6 +85,7 @@ internal class DashboardRenderer(
7685
appendLine("| Project | Symbols | Source Sets | Dependencies | Warnings |")
7786
appendLine("|---------|---------|------------|-------------|----------|")
7887
for (s in summaries) {
88+
if (s.symbols.isEmpty() && s.dependencies.isEmpty() && s.sourceSets.isEmpty()) continue
7989
val sets = s.sourceSets.joinToString(", ") { it.name.value }.ifEmpty { "-" }
8090
val warnings = s.analysis?.findings?.count { it.severity == FindingSeverity.WARNING } ?: 0
8191
appendLine("| ${s.projectPath} | ${s.symbols.size} | $sets | ${s.dependencies.size} | $warnings |")
@@ -105,8 +115,8 @@ internal class DashboardRenderer(
105115
if (includedBuilds.isEmpty()) return
106116
appendLine("## Included Builds")
107117
appendLine()
108-
appendLine("| Build | Projects | Symbols | Warnings | Dashboard |")
109-
appendLine("|-------|----------|---------|----------|-----------|")
118+
appendLine("| Build | Projects | Symbols | Warnings | Context |")
119+
appendLine("|-------|----------|---------|----------|---------|")
110120
for (ref in includedBuilds) {
111121
val buildSummaries = includedBuildSummaries[ref.name]
112122
val projectCount = buildSummaries?.size ?: 0
@@ -154,7 +164,8 @@ internal class DashboardRenderer(
154164
val roleTag = if (hub != null && hub.role.isNotEmpty()) " [${hub.role}]" else ""
155165
val depTag =
156166
if (hub != null && hub.dependentCount > 0) {
157-
" (${hub.dependentCount} dependents)"
167+
val depLabel = if (hub.dependentCount == 1) "dependent" else "dependents"
168+
" (${hub.dependentCount} $depLabel)"
158169
} else {
159170
""
160171
}
@@ -206,8 +217,9 @@ internal class DashboardRenderer(
206217
}
207218
}
208219

209-
val warnings = (allFindings + buildFindings).filter { it.second == FindingSeverity.WARNING }
210-
val notes = (allFindings + buildFindings).filter { it.second == FindingSeverity.INFO }
220+
val combined = (allFindings + buildFindings).distinctBy { it.third.message }
221+
val warnings = combined.filter { it.second == FindingSeverity.WARNING }
222+
val notes = combined.filter { it.second == FindingSeverity.INFO }
211223

212224
if (warnings.isEmpty() && notes.isEmpty()) return
213225

src/main/kotlin/zone/clanker/gradle/srcx/report/IncludedBuildRenderer.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ internal class IncludedBuildRenderer(
9393
if (hub != null && hub.role.isNotEmpty()) " [${hub.role}]" else ""
9494
val depTag =
9595
if (hub != null && hub.dependentCount > 0) {
96-
" (${hub.dependentCount} dependents)"
96+
val depLabel = if (hub.dependentCount == 1) "dependent" else "dependents"
97+
" (${hub.dependentCount} $depLabel)"
9798
} else {
9899
""
99100
}
@@ -124,12 +125,13 @@ internal class IncludedBuildRenderer(
124125
val roleTag = if (hub.role.isNotEmpty()) " [${hub.role}]" else ""
125126
val loc =
126127
if (hub.filePath.isNotEmpty()) "${hub.filePath}:${hub.line}" else ""
128+
val depLabel = if (hub.dependentCount == 1) "dependent" else "dependents"
127129
if (hub.dependentCount >= ProjectReportRenderer.SUPER_NODE_THRESHOLD) {
128130
appendLine(
129-
"- **${hub.name}**$roleTag$loc — super node (${hub.dependentCount} dependents)",
131+
"- **${hub.name}**$roleTag$loc — super node (${hub.dependentCount} $depLabel)",
130132
)
131133
} else {
132-
appendLine("- **${hub.name}**$roleTag$loc (${hub.dependentCount} dependents)")
134+
appendLine("- **${hub.name}**$roleTag$loc (${hub.dependentCount} $depLabel)")
133135
for (dep in hub.dependents) {
134136
appendLine(" - ${dep.name}${dep.filePath}:${dep.line}")
135137
}
@@ -155,9 +157,9 @@ internal class IncludedBuildRenderer(
155157

156158
private fun StringBuilder.appendProblems() {
157159
val allFindings =
158-
summaries.flatMap { s ->
159-
s.analysis?.findings ?: emptyList()
160-
}
160+
summaries
161+
.flatMap { s -> s.analysis?.findings ?: emptyList() }
162+
.distinctBy { it.message }
161163
val warnings = allFindings.filter { it.severity == FindingSeverity.WARNING }
162164
val notes = allFindings.filter { it.severity == FindingSeverity.INFO }
163165
val allCycles =

src/main/kotlin/zone/clanker/gradle/srcx/report/ProjectReportRenderer.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,11 @@ internal class ProjectReportRenderer(
116116
private fun StringBuilder.appendHubTree(hub: HubClass) {
117117
val roleTag = if (hub.role.isNotEmpty()) " [${hub.role}]" else ""
118118
val loc = if (hub.filePath.isNotEmpty()) "${hub.filePath}:${hub.line}" else ""
119+
val depLabel = if (hub.dependentCount == 1) "dependent" else "dependents"
119120
if (hub.dependentCount >= SUPER_NODE_THRESHOLD) {
120-
appendLine("- **${hub.name}**$roleTag$loc — super node (${hub.dependentCount} dependents)")
121+
appendLine("- **${hub.name}**$roleTag$loc — super node (${hub.dependentCount} $depLabel)")
121122
} else {
122-
appendLine("- **${hub.name}**$roleTag$loc (${hub.dependentCount} dependents)")
123+
appendLine("- **${hub.name}**$roleTag$loc (${hub.dependentCount} $depLabel)")
123124
for (dep in hub.dependents) {
124125
appendLine(" - ${dep.name}${dep.filePath}:${dep.line}")
125126
}

src/main/kotlin/zone/clanker/gradle/srcx/scan/SymbolExtractor.kt

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import org.jetbrains.kotlin.psi.KtCallExpression
77
import org.jetbrains.kotlin.psi.KtFile
88
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
99
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
10+
import zone.clanker.gradle.srcx.Srcx
1011
import zone.clanker.gradle.srcx.analysis.analyzeProject
1112
import zone.clanker.gradle.srcx.model.ArtifactGroup
1213
import zone.clanker.gradle.srcx.model.ArtifactName
@@ -33,11 +34,9 @@ import java.io.File
3334
* and dependencies from build files or the Gradle configuration API.
3435
*/
3536
object SymbolExtractor {
36-
/** Dependency scope names recognised in build files. */
37-
private val DEP_SCOPES =
38-
setOf(
39-
"api", "implementation", "compileOnly", "runtimeOnly", "testImplementation",
40-
)
37+
/** Dependency scopes excluded from scanning by default. */
38+
internal val DEFAULT_EXCLUDED_DEP_SCOPES =
39+
Srcx.DEFAULT_EXCLUDED_DEP_SCOPES
4140

4241
/** Minimum number of colon-separated parts in a Maven coordinate (group:artifact:version). */
4342
private const val MIN_COORDINATE_PARTS = 3
@@ -110,7 +109,7 @@ object SymbolExtractor {
110109
.map { it.relativeTo(project.projectDir).path }
111110
}
112111

113-
val dependencies = extractDependencies(project)
112+
val dependencies = extractDependenciesFromProject(project)
114113
val buildFileName = ProjectScanner.buildFileName(project)
115114
val subprojectPaths =
116115
if (project == rootProject) {
@@ -244,8 +243,25 @@ object SymbolExtractor {
244243
)
245244
}
246245

246+
/** Call expressions in build files that are not dependency declarations. */
247+
private val PSI_SKIP_CALLS =
248+
setOf(
249+
"plugins", "kotlin", "id", "version", "apply",
250+
"repositories", "mavenCentral", "google", "gradlePluginPortal", "mavenLocal",
251+
"java", "tasks", "register", "named", "configure",
252+
"sourceSets", "dependencies", "buildscript", "allprojects", "subprojects",
253+
"project", "files", "fileTree", "exclude", "include",
254+
"create", "getting", "creating", "withType", "matching",
255+
"println", "print", "error", "require", "check",
256+
"listOf", "setOf", "mapOf", "mutableListOf", "mutableSetOf",
257+
"buildList", "buildString", "buildMap", "run", "let", "also", "apply", "with",
258+
)
259+
247260
/** Extract dependencies from a build file by parsing dependency declarations with PSI. */
248-
internal fun extractDependenciesFromBuildFile(projectDir: File): List<DependencyEntry> {
261+
internal fun extractDependenciesFromBuildFile(
262+
projectDir: File,
263+
excludeScopes: Set<String> = DEFAULT_EXCLUDED_DEP_SCOPES,
264+
): List<DependencyEntry> {
249265
val buildFile =
250266
File(projectDir, "build.gradle.kts").takeIf { it.exists() }
251267
?: File(projectDir, "build.gradle").takeIf { it.exists() }
@@ -257,8 +273,10 @@ object SymbolExtractor {
257273

258274
ktFile
259275
.collectDescendantsOfType<KtCallExpression>()
260-
.filter { call -> call.calleeExpression?.text in DEP_SCOPES }
261-
.mapNotNull { call ->
276+
.filter { call ->
277+
val name = call.calleeExpression?.text ?: return@filter false
278+
name !in excludeScopes && name !in PSI_SKIP_CALLS
279+
}.mapNotNull { call ->
262280
val scope = call.calleeExpression?.text ?: return@mapNotNull null
263281
val arg =
264282
call.valueArguments
@@ -278,28 +296,27 @@ object SymbolExtractor {
278296
}
279297
}
280298

281-
/** Extract dependencies from a Gradle project's configurations. Call at configuration time. */
282-
internal fun extractDependenciesFromProject(project: Project): List<DependencyEntry> =
283-
extractDependencies(project)
284-
285-
private fun extractDependencies(project: Project): List<DependencyEntry> {
299+
/** Extract dependencies from all project configurations, excluding specified scopes. */
300+
internal fun extractDependenciesFromProject(
301+
project: Project,
302+
excludeScopes: Set<String> = DEFAULT_EXCLUDED_DEP_SCOPES,
303+
): List<DependencyEntry> {
286304
val results = mutableListOf<DependencyEntry>()
287-
val scopes = listOf("api", "implementation", "compileOnly", "runtimeOnly", "testImplementation")
288-
for (scope in scopes) {
289-
val config = project.configurations.findByName(scope) ?: continue
305+
for (config in project.configurations) {
306+
if (config.name in excludeScopes) continue
290307
config.dependencies.forEach { dep ->
291308
if (dep.group != null) {
292309
results.add(
293310
DependencyEntry(
294311
group = ArtifactGroup(dep.group.orEmpty()),
295312
artifact = ArtifactName(dep.name),
296313
version = ArtifactVersion(dep.version ?: "unspecified"),
297-
scope = scope,
314+
scope = config.name,
298315
),
299316
)
300317
}
301318
}
302319
}
303-
return results
320+
return results.distinctBy { "${it.scope}:${it.group}:${it.artifact}" }
304321
}
305322
}

src/main/kotlin/zone/clanker/gradle/srcx/task/ContextTask.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import org.gradle.api.file.DirectoryProperty
66
import org.gradle.api.provider.ListProperty
77
import org.gradle.api.provider.MapProperty
88
import org.gradle.api.provider.Property
9+
import org.gradle.api.provider.SetProperty
910
import org.gradle.api.tasks.Input
1011
import org.gradle.api.tasks.InputFiles
1112
import org.gradle.api.tasks.Internal
@@ -77,6 +78,10 @@ abstract class ContextTask : DefaultTask() {
7778
@get:Internal
7879
abstract val projectDeps: MapProperty<String, List<DependencyEntry>>
7980

81+
/** Dependency scopes to exclude from scanning. */
82+
@get:Input
83+
abstract val excludeDepScopes: SetProperty<String>
84+
8085
/** Included build info: name → (dir, relPath, projects). */
8186
@get:Internal
8287
abstract val includedBuildInfos: ListProperty<IncludedBuildInfo>

src/test/kotlin/zone/clanker/gradle/srcx/SrcxIncludedBuildPluginTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ class SrcxIncludedBuildPluginTest :
141141
val content = index.readText()
142142
content shouldContain "## Included Builds"
143143
content shouldContain "lib-build"
144-
content shouldContain "| Build | Projects | Symbols | Warnings | Dashboard |"
144+
content shouldContain "| Build | Projects | Symbols | Warnings | Context |"
145145
}
146146
}
147147
}

src/test/kotlin/zone/clanker/gradle/srcx/report/DashboardIncludedBuildTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class DashboardIncludedBuildTest :
8484
}
8585

8686
then("it has the correct table headers") {
87-
output shouldContain "| Build | Projects | Symbols | Warnings | Dashboard |"
87+
output shouldContain "| Build | Projects | Symbols | Warnings | Context |"
8888
}
8989
}
9090
}

0 commit comments

Comments
 (0)