Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KON-371 Initialize All KoFiles At Start #878

Merged
merged 7 commits into from
Mar 4, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ import org.amshove.kluent.shouldBeEqualTo
import org.junit.jupiter.api.Test

class Architecture2Test {
private val rootPath = PathProvider.getInstance().rootProjectPath
private val rootPath = PathProvider.rootProjectPath
private val domain =
Layer("Domain", "com.lemonappdev.konsist.architecture.assertarchitecture.architecture2.project.domain..")
Layer(
"Domain",
"com.lemonappdev.konsist.architecture.assertarchitecture.architecture2.project.domain..",
)
private val presentation =
Layer(
"Presentation",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import org.amshove.kluent.shouldBeEqualTo
import org.junit.jupiter.api.Test

class OptionalParametersTest {
private val rootPath = PathProvider.getInstance().rootProjectPath
private val rootPath = PathProvider.rootProjectPath
private val domain =
Layer("Domain", "com.lemonappdev.konsist.architecture.optionalparameters.project.domain..")
private val presentation =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import org.amshove.kluent.assertSoftly
import org.amshove.kluent.shouldBeEqualTo
import org.junit.jupiter.api.Test

class KoClassDeclarationForKoDeclarationProviderTest {
class KoClassDeclarationForKoFileDeclarationProviderTest {
@Test
fun `class-has-no-declarations`() {
// given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments.arguments
import org.junit.jupiter.params.provider.MethodSource

class KoFileDeclarationForKoDeclarationProviderTest {
class KoFileDeclarationForKoFileDeclarationProviderTest {
@Test
fun `file-has-two-declarations`() {
// given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import org.amshove.kluent.assertSoftly
import org.amshove.kluent.shouldBeEqualTo
import org.junit.jupiter.api.Test

class KoInterfaceDeclarationForKoDeclarationProviderTest {
class KoInterfaceDeclarationForKoFileDeclarationProviderTest {
@Test
fun `interface-has-no-declarations`() {
// given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import org.amshove.kluent.assertSoftly
import org.amshove.kluent.shouldBeEqualTo
import org.junit.jupiter.api.Test

class KoObjectDeclarationForKoDeclarationProviderTest {
class KoObjectDeclarationForKoFileDeclarationProviderTest {
@Test
fun `object-has-no-declarations`() {
// given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,50 +9,56 @@ import com.lemonappdev.konsist.core.ext.sep
import com.lemonappdev.konsist.core.ext.toKoFile
import com.lemonappdev.konsist.core.ext.toMacOsSeparator
import com.lemonappdev.konsist.core.filesystem.PathProvider
import com.lemonappdev.konsist.core.provider.util.KoFileDeclarationProvider
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import java.io.File

@Suppress("detekt.TooManyFunctions")
internal class KoScopeCreatorCore : KoScopeCreator {
private val pathProvider: PathProvider by lazy { PathProvider.getInstance() }
override val projectRootPath: String by lazy { PathProvider.rootProjectPath }

private val projectKotlinFiles: List<KoFileDeclaration> by lazy { File(pathProvider.rootProjectPath).toKoFiles() }

override val projectRootPath: String by lazy { pathProvider.rootProjectPath }

override fun scopeFromProject(moduleName: String?, sourceSetName: String?, ignoreBuildConfig: Boolean): KoScope {
val koFiles = getFiles(moduleName, sourceSetName, ignoreBuildConfig)
return KoScopeCore(koFiles)
}
override fun scopeFromProject(moduleName: String?, sourceSetName: String?, ignoreBuildConfig: Boolean): KoScope =
runBlocking {
val koFiles = getFiles(moduleName, sourceSetName, ignoreBuildConfig)
KoScopeCore(koFiles)
}

override fun scopeFromModule(moduleName: String, vararg moduleNames: String): KoScope =
scopeFromModules(setOf(moduleName) + moduleNames)

override fun scopeFromModules(moduleNames: Set<String>): KoScope =
moduleNames
.flatMap { getFiles(it) }
.let { KoScopeCore(it) }
override fun scopeFromModules(moduleNames: Set<String>): KoScopeCore =
runBlocking {
moduleNames
.flatMap { getFiles(it) }
.let { KoScopeCore(it) }
}

override fun scopeFromPackage(packagee: String, moduleName: String?, sourceSetName: String?): KoScope {
val koFiles = getFiles(moduleName, sourceSetName)
.withPackage(packagee)
override fun scopeFromPackage(packagee: String, moduleName: String?, sourceSetName: String?): KoScope =
runBlocking {
val koFiles = getFiles(moduleName, sourceSetName)
.withPackage(packagee)

return KoScopeCore(koFiles)
}
KoScopeCore(koFiles)
}

override fun scopeFromSourceSet(sourceSetName: String, vararg sourceSetNames: String): KoScope =
scopeFromSourceSets(setOf(sourceSetName) + sourceSetNames)

override fun scopeFromSourceSets(sourceSetNames: Set<String>): KoScope =
sourceSetNames
.flatMap { getFiles(sourceSetName = it) }
.let { KoScopeCore(it) }
runBlocking {
sourceSetNames
.flatMap { getFiles(sourceSetName = it) }
.let { KoScopeCore(it) }
}

private fun getFiles(
private suspend fun getFiles(
moduleName: String? = null,
sourceSetName: String? = null,
ignoreBuildConfig: Boolean = true,
): List<KoFileDeclaration> {
val localProjectKotlinFiles = projectKotlinFiles
): List<KoFileDeclaration> = coroutineScope {
val localProjectKotlinFiles = KoFileDeclarationProvider
.getKoFileDeclarations()
.filterNot { isBuildToolPath(it.path.toMacOsSeparator()) }
.let {
if (ignoreBuildConfig) {
Expand All @@ -63,7 +69,7 @@ internal class KoScopeCreatorCore : KoScopeCreator {
}

if (moduleName == null && sourceSetName == null) {
return localProjectKotlinFiles
return@coroutineScope localProjectKotlinFiles
}

var pathPrefix = if (moduleName == ROOT_MODULE_NAME) {
Expand All @@ -80,31 +86,33 @@ internal class KoScopeCreatorCore : KoScopeCreator {
"$pathPrefix/src/.*"
}.toMacOsSeparator()

return localProjectKotlinFiles
return@coroutineScope localProjectKotlinFiles
.filter { it.path.toMacOsSeparator().matches(Regex(pathPrefix)) }
}

override fun scopeFromProduction(moduleName: String?, sourceSetName: String?): KoScope {
sourceSetName?.let {
require(!isTestSourceSet(it)) { "Source set '$it' is a test source set, but it should be production source set." }
}

val koFiles = getFiles(moduleName, sourceSetName)
.filterNot { isTestSourceSet(it.sourceSetName) }
override fun scopeFromProduction(moduleName: String?, sourceSetName: String?): KoScope =
runBlocking {
sourceSetName?.let {
require(!isTestSourceSet(it)) { "Source set '$it' is a test source set, but it should be production source set." }
}

return KoScopeCore(koFiles)
}
val koFiles = getFiles(moduleName, sourceSetName)
.filterNot { isTestSourceSet(it.sourceSetName) }

override fun scopeFromTest(moduleName: String?, sourceSetName: String?): KoScope {
sourceSetName?.let {
require(isTestSourceSet(it)) { "Source set '$it' is a production source set, but it should be test source set." }
KoScopeCore(koFiles)
}

val koFiles = getFiles(moduleName, sourceSetName)
.filter { isTestSourceSet(it.sourceSetName) }
override fun scopeFromTest(moduleName: String?, sourceSetName: String?): KoScope =
runBlocking {
sourceSetName?.let {
require(isTestSourceSet(it)) { "Source set '$it' is a production source set, but it should be test source set." }
}

return KoScopeCore(koFiles)
}
val koFiles = getFiles(moduleName, sourceSetName)
.filter { isTestSourceSet(it.sourceSetName) }

KoScopeCore(koFiles)
}

/**
* Get the scope of the paths obtaining the absolute path of it and, getting the files from that directory
Expand Down Expand Up @@ -192,7 +200,8 @@ internal class KoScopeCreatorCore : KoScopeCreator {
private fun isBuildOrTargetPath(path: String): Boolean {
val gradleBuildDirectoryName = "build"
val gradleRootBuildDirectoryRegex = Regex("$projectRootPath/$gradleBuildDirectoryName/.*".toMacOsSeparator())
val gradleModuleBuildDirectoryRegex = Regex("$projectRootPath/.+/$gradleBuildDirectoryName/.*".toMacOsSeparator())
val gradleModuleBuildDirectoryRegex =
Regex("$projectRootPath/.+/$gradleBuildDirectoryName/.*".toMacOsSeparator())

val mavenBuildDirectoryName = "target"
val mavenRootBuildDirectoryRegex = Regex("$projectRootPath/$mavenBuildDirectoryName/.*".toMacOsSeparator())
Expand Down Expand Up @@ -236,10 +245,14 @@ internal class KoScopeCreatorCore : KoScopeCreator {
.map { it.toKoFile() }
.toList()

private fun getKoFiles(files: List<File>) = projectKotlinFiles.filter {
files.any { file ->
file.path == it.path
}
private fun getKoFiles(files: List<File>) = runBlocking {
KoFileDeclarationProvider
.getKoFileDeclarations()
.filter {
files.any { file ->
file.path == it.path
}
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package com.lemonappdev.konsist.core.filesystem
import com.lemonappdev.konsist.core.exception.KoInternalException
import java.io.File

class PathProvider(
private val koFileFactory: KoFileFactory,
private val projectRootDirProviderFactory: ProjectRootDirProviderFactory,
) {
object PathProvider {
private val koFileFactory = KoFileFactory()
private val pathVerifier = PathVerifier()
private val projectRootDirProviderFactory = ProjectRootDirProviderFactory(pathVerifier)

val rootProjectPath: String by lazy {
val file = koFileFactory.create("")

Expand All @@ -30,10 +31,4 @@ class PathProvider(
.firstOrNull { it != null }
?: getProjectRootDirectory(file.absoluteFile.parentFile)
}

companion object {
private val pathVerifier = PathVerifier()

fun getInstance() = PathProvider(KoFileFactory(), ProjectRootDirProviderFactory(pathVerifier))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ internal interface KoModuleProviderCore : KoModuleProvider, KoPathProviderCore,
override val moduleName: String
get() {
val projectName = PathProvider
.getInstance()
.rootProjectPath
.substringAfterLast(sep)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ internal interface KoPathProviderCore : KoPathProvider, KoBaseProviderCore {
override val projectPath: String
get() {
val rootPathProvider = PathProvider
.getInstance()
.rootProjectPath
.toOsSeparator()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,18 +169,34 @@ internal object KoDeclarationProviderCoreUtil {
containingDeclaration: KoBaseDeclaration,
): KoBaseDeclaration? = when {
ktDeclaration is KtEnumEntry -> KoEnumConstantDeclarationCore.getInstance(ktDeclaration, containingDeclaration)
ktDeclaration is KtSecondaryConstructor -> KoSecondaryConstructorDeclarationCore.getInstance(ktDeclaration, containingDeclaration)
ktDeclaration is KtClass && !ktDeclaration.isInterface() -> KoClassDeclarationCore.getInstance(ktDeclaration, containingDeclaration)
ktDeclaration is KtSecondaryConstructor -> KoSecondaryConstructorDeclarationCore.getInstance(
ktDeclaration,
containingDeclaration,
)

ktDeclaration is KtClass && !ktDeclaration.isInterface() -> KoClassDeclarationCore.getInstance(
ktDeclaration,
containingDeclaration,
)

ktDeclaration is KtClass && ktDeclaration.isInterface() -> KoInterfaceDeclarationCore.getInstance(
ktDeclaration,
containingDeclaration,
)

ktDeclaration is KtObjectDeclaration -> KoObjectDeclarationCore.getInstance(ktDeclaration, containingDeclaration)
ktDeclaration is KtObjectDeclaration -> KoObjectDeclarationCore.getInstance(
ktDeclaration,
containingDeclaration,
)

ktDeclaration is KtProperty -> KoPropertyDeclarationCore.getInstance(ktDeclaration, containingDeclaration)
ktDeclaration is KtFunction -> KoFunctionDeclarationCore.getInstance(ktDeclaration, containingDeclaration)
ktDeclaration is KtTypeAlias -> KoTypeAliasDeclarationCore.getInstance(ktDeclaration, containingDeclaration)
ktDeclaration is KtAnonymousInitializer -> KoInitBlockDeclarationCore.getInstance(ktDeclaration, containingDeclaration)
ktDeclaration is KtAnonymousInitializer -> KoInitBlockDeclarationCore.getInstance(
ktDeclaration,
containingDeclaration,
)

else -> null
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.lemonappdev.konsist.core.provider.util

import com.lemonappdev.konsist.api.declaration.KoFileDeclaration
import com.lemonappdev.konsist.core.ext.isKotlinFile
import com.lemonappdev.konsist.core.ext.toKoFile
import com.lemonappdev.konsist.core.filesystem.PathProvider
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.io.File

internal object KoFileDeclarationProvider {
private val projectRootDir: File = File(PathProvider.rootProjectPath)

private val mutex: Mutex = Mutex()

@Volatile
private var createKoFilesDeclarationDeferred: Deferred<List<KoFileDeclaration>>? = null

init {
check(projectRootDir.exists()) { "Directory does not exist: ${projectRootDir.absolutePath}" }
check(projectRootDir.isDirectory) { "Project root directory is a File ${projectRootDir.absolutePath}" }

@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch {
withContext(Dispatchers.IO) {
getKoFileDeclarations()
}
}
}

/**
* Retrieves a list of [KoFileDeclaration]s asynchronously from the project's root directory.
* This function scans the directory for Kotlin files and parses them to obtain list of KoFileDeclaration.
*
* The parsing operations are performed concurrently.
*
* Threading Strategy:
* Ensures thread-safe initialization of a single deferred operation to parse all Kotlin files, using a mutex to
* guard against concurrent initializations.
* File walking and parsing operations, optimizing for I/O operations are performed concurrently across
* multiple threads.
*
* e.g.
* 1. getKoFileDeclarations started at Thread 1 - start file parsing
* 2. getKoFileDeclarations started at Thread 2 - file parsing in progress, wait for it to complete
* 3. Parse Kotlin files in parallel
* 4. getKoFileDeclarations started at Thread 1 completes
* 5. getKoFileDeclarations started at Thread 2 completes
*
* @return A list of [KoFileDeclaration]s representing the parsed Kotlin files.
* @throws Exception if there's an issue accessing the file system or parsing the files.
*/
suspend fun getKoFileDeclarations(): List<KoFileDeclaration> = coroutineScope {
val currentDeferred: Deferred<List<KoFileDeclaration>> = mutex.withLock {
createKoFilesDeclarationDeferred ?: async(Dispatchers.IO) {
projectRootDir.walk()
.filter { it.isKotlinFile }
.map { async { parseKotlinFile(it) } }
.toList()
.awaitAll()
}.also { createKoFilesDeclarationDeferred = it }
}

currentDeferred.await()
}

private suspend fun parseKotlinFile(it: File): KoFileDeclaration = coroutineScope {
async { it.toKoFile() }.await()
}
}
Loading
Loading