Skip to content

Commit 7626540

Browse files
committed
Follow symbolic links when building KaModule for AA standalone session (#4264)
(cherry picked from commit e572fc9)
1 parent e4c7474 commit 7626540

File tree

2 files changed

+194
-5
lines changed

2 files changed

+194
-5
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
package org.jetbrains.dokka.gradle
5+
6+
import io.kotest.assertions.withClue
7+
import io.kotest.core.spec.style.FunSpec
8+
import io.kotest.inspectors.shouldForAll
9+
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
10+
import io.kotest.matchers.paths.shouldBeAFile
11+
import io.kotest.matchers.sequences.shouldNotBeEmpty
12+
import io.kotest.matchers.string.shouldContain
13+
import io.kotest.matchers.string.shouldNotContainIgnoringCase
14+
import org.jetbrains.dokka.gradle.internal.DokkaConstants.DOKKA_VERSION
15+
import org.jetbrains.dokka.gradle.utils.*
16+
import java.nio.file.Files
17+
import kotlin.io.path.extension
18+
import kotlin.io.path.name
19+
import kotlin.io.path.readText
20+
21+
class KotlinAnalysisSymlinksTest : FunSpec({
22+
23+
context("when DGP generates HTML") {
24+
val project = initProject()
25+
26+
project.runner
27+
.addArguments(
28+
":dokkaGeneratePublicationHtml",
29+
"--rerun-tasks",
30+
)
31+
.forwardOutput()
32+
.build {
33+
test("expect build is successful") {
34+
output shouldContain "BUILD SUCCESSFUL"
35+
}
36+
}
37+
38+
test("expect all DGP workers are successful") {
39+
project
40+
.findFiles { it.name == "dokka-worker.log" }
41+
.shouldBeSingleton { dokkaWorkerLog ->
42+
dokkaWorkerLog.shouldBeAFile()
43+
dokkaWorkerLog.readText().shouldNotContainAnyOf(
44+
"[ERROR]",
45+
"[WARN]",
46+
)
47+
}
48+
}
49+
50+
context("expect HTML site is generated") {
51+
val projectName = "kotlin-analysis-symlinks-project"
52+
53+
test("with expected HTML files") {
54+
project.projectDir
55+
.resolve("build/dokka/")
56+
.listRelativePathsMatching { it.extension == "html" }
57+
.shouldContainExactlyInAnyOrder(
58+
"html/index.html",
59+
"html/navigation.html",
60+
"html/$projectName/[root]/-foo/-foo.html",
61+
"html/$projectName/[root]/-foo/foo.html",
62+
"html/$projectName/[root]/-foo/index.html",
63+
"html/$projectName/[root]/index.html",
64+
"html/$projectName/[root]/use-foo.html",
65+
)
66+
}
67+
68+
test("expect no 'unknown class' message in HTML files") {
69+
val htmlFiles = project.projectDir.toFile()
70+
.resolve("build/dokka/html")
71+
.walk()
72+
.filter { it.isFile && it.extension == "html" }
73+
74+
htmlFiles.shouldNotBeEmpty()
75+
76+
htmlFiles.forEach { htmlFile ->
77+
val relativePath = htmlFile.relativeTo(project.projectDir.toFile())
78+
withClue("$relativePath should not contain 'Error class: unknown class' or 'ERROR CLASS'") {
79+
htmlFile.useLines { lines ->
80+
lines.shouldForAll { line -> line.shouldNotContainIgnoringCase("ERROR CLASS") }
81+
}
82+
}
83+
}
84+
}
85+
}
86+
}
87+
})
88+
89+
90+
private fun initProject(): GradleProjectTest = gradleKtsProjectTest("kotlin-analysis-symlinks-project") {
91+
buildGradleKts = """
92+
|plugins {
93+
| kotlin("jvm") version embeddedKotlinVersion
94+
| id("org.jetbrains.dokka") version "$DOKKA_VERSION"
95+
|}
96+
""".trimMargin()
97+
98+
createKotlinFile(
99+
filePath = "src/symlinked/kotlin/foo/Foo.kt",
100+
contents = """
101+
|package foo
102+
|
103+
|class Foo {
104+
| fun foo() = Unit
105+
|}
106+
""".trimIndent()
107+
)
108+
109+
// should be able to access `Foo`
110+
createKotlinFile(
111+
filePath = "src/main/kotlin/project/UseFoo.kt",
112+
contents = """
113+
|package project
114+
|
115+
|import foo.Foo
116+
|
117+
|/** Uses [Foo] */
118+
|fun useFoo(foo: Foo) = Unit
119+
""".trimIndent()
120+
)
121+
122+
// could exist because of caching in tests...
123+
Files.deleteIfExists(file("src/main/kotlin/foo"))
124+
Files.createSymbolicLink(
125+
file("src/main/kotlin/foo"),
126+
file("src/symlinked/kotlin/foo")
127+
)
128+
}

dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/KotlinAnalysis.kt

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,20 @@ import org.jetbrains.kotlin.analysis.project.structure.builder.KtModuleBuilder
1616
import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtLibraryModule
1717
import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtSdkModule
1818
import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtSourceModule
19-
import org.jetbrains.kotlin.config.*
19+
import org.jetbrains.kotlin.config.AnalysisFlags
20+
import org.jetbrains.kotlin.config.ApiVersion
21+
import org.jetbrains.kotlin.config.LanguageVersion
22+
import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl
2023
import org.jetbrains.kotlin.platform.CommonPlatforms
2124
import org.jetbrains.kotlin.platform.js.JsPlatforms
2225
import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
2326
import org.jetbrains.kotlin.platform.konan.NativePlatforms
2427
import org.jetbrains.kotlin.platform.wasm.WasmPlatforms
2528
import java.io.File
29+
import java.io.IOException
30+
import java.nio.file.*
31+
import java.nio.file.attribute.BasicFileAttributes
32+
import kotlin.io.path.extension
2633

2734
internal fun Platform.toTargetPlatform() = when (this) {
2835
Platform.wasm -> WasmPlatforms.unspecifiedWasmPlatform
@@ -113,10 +120,19 @@ internal fun createAnalysisSession(
113120
getLanguageVersionSettings(sourceSet.languageVersion, sourceSet.apiVersion)
114121
platform = targetPlatform
115122
moduleName = "<module ${sourceSet.displayName}>"
116-
if (isSampleProject)
117-
addSourceRoots(sourceSet.samples.map { it.toPath() })
118-
else
119-
addSourceRoots(sourceSet.sourceRoots.map { it.toPath() })
123+
124+
// can be removed after https://youtrack.jetbrains.com/issue/KT-81107 is implemented (see #4266)
125+
// here we mimic the logic, which happens inside AA during building KaModule, but we follow symlinks
126+
// https://github.com/JetBrains/kotlin/blob/dcd24449718cba21bd86428e5cddb9b25e5612af/analysis/analysis-api-standalone/src/org/jetbrains/kotlin/analysis/project/structure/builder/KaSourceModuleBuilder.kt#L80
127+
if (isSampleProject) {
128+
sourceSet.samples.forEach { root ->
129+
addSourceRoots(collectSourceFilePaths(root.toPath()))
130+
}
131+
} else {
132+
sourceSet.sourceRoots.forEach { root ->
133+
addSourceRoots(collectSourceFilePaths(root.toPath()))
134+
}
135+
}
120136
addModuleDependencies(
121137
sourceSet,
122138
)
@@ -171,3 +187,48 @@ internal fun topologicalSortByDependantSourceSets(
171187
sourceSets.forEach(::dfs)
172188
return result
173189
}
190+
191+
// copied from AA: https://github.com/JetBrains/kotlin/blob/dcd24449718cba21bd86428e5cddb9b25e5612af/analysis/analysis-api-standalone/src/org/jetbrains/kotlin/analysis/project/structure/impl/KaModuleUtils.kt#L60-L110
192+
// with a fix for following symlinks
193+
194+
private fun collectSourceFilePaths(root: Path): List<Path> {
195+
// NB: [Files#walk] throws an exception if there is an issue during IO.
196+
// With [Files#walkFileTree] with a custom visitor, we can take control of exception handling.
197+
val result = mutableListOf<Path>()
198+
Files.walkFileTree(
199+
/* start = */ root,
200+
/* options = */ setOf(FileVisitOption.FOLLOW_LINKS), // <-- THIS IS THE FIX
201+
/* maxDepth = */ Int.MAX_VALUE,
202+
/* visitor = */ object : SimpleFileVisitor<Path>() {
203+
override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult {
204+
return if (Files.isReadable(dir))
205+
FileVisitResult.CONTINUE
206+
else
207+
FileVisitResult.SKIP_SUBTREE
208+
}
209+
210+
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
211+
if (!Files.isRegularFile(file) || !Files.isReadable(file))
212+
return FileVisitResult.CONTINUE
213+
if (file.hasSuitableExtensionToAnalyse()) {
214+
result.add(file)
215+
}
216+
return FileVisitResult.CONTINUE
217+
}
218+
219+
override fun visitFileFailed(file: Path, exc: IOException): FileVisitResult {
220+
// TODO: report or log [IOException]?
221+
// NB: this intentionally swallows the exception, hence fail-safe.
222+
// Skipping subtree doesn't make any sense, since this is not a directory.
223+
// Skipping sibling may drop valid file paths afterward, so we just continue.
224+
return FileVisitResult.CONTINUE
225+
}
226+
}
227+
)
228+
return result
229+
}
230+
231+
private fun Path.hasSuitableExtensionToAnalyse(): Boolean {
232+
val extension = extension
233+
return extension == "kt" || extension == "kts" || extension == "java"
234+
}

0 commit comments

Comments
 (0)