diff --git a/examples/coffee-maker-glob/build.gradle.kts b/examples/coffee-maker-glob/build.gradle.kts new file mode 100644 index 00000000..afb84c2f --- /dev/null +++ b/examples/coffee-maker-glob/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + alias(libs.plugins.ksp) + kotlin("jvm") +} + +sourceSets.main { + java.srcDirs("build/generated/ksp/main/kotlin") +} + +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() + mavenLocal() + google() +} + +dependencies { + implementation(libs.koin.core) + implementation(libs.koin.annotations) + ksp(libs.koin.ksp) + implementation(project(":coffee-maker-module")) + + testImplementation(libs.koin.ksp) + testImplementation(libs.koin.test) + testImplementation(libs.junit) +} + +ksp { + arg("KOIN_CONFIG_CHECK", "true") +} diff --git a/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/CoffeeGlobApp.kt b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/CoffeeGlobApp.kt new file mode 100644 index 00000000..bc452acf --- /dev/null +++ b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/CoffeeGlobApp.kt @@ -0,0 +1,42 @@ +package org.koin.example + +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.koin.core.context.startKoin +import org.koin.core.logger.Level +import org.koin.example.coffee.CoffeeMaker +import org.koin.example.di.CoffeeAppAndTesterModule +import org.koin.example.tea.TeaModule +import org.koin.example.test.ext.ExternalModule +import org.koin.example.test.scope.ScopeModule +import org.koin.ksp.generated.* +import kotlin.time.measureTime + +class CoffeeApp : KoinComponent { + val maker: CoffeeMaker by inject() +} + +// be sure to import "import org.koin.ksp.generated.*" + +fun main() { + startKoin { + printLogger(Level.DEBUG) + // if no module +// defaultModule() + + // else let's use our modules + modules( + CoffeeAppAndTesterModule().module, + TeaModule().module, + ExternalModule().module, + org.koin.example.test.ext2.ExternalModule().module, + ScopeModule().module + ) + } + + val coffeeShop = CoffeeApp() + val t = measureTime { + coffeeShop.maker.brew() + } + println("Got Coffee in $t") +} \ No newline at end of file diff --git a/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/CoffeeMaker.kt b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/CoffeeMaker.kt new file mode 100644 index 00000000..1e1149f7 --- /dev/null +++ b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/CoffeeMaker.kt @@ -0,0 +1,15 @@ +package org.koin.example.coffee + +import org.koin.core.annotation.Singleton +import org.koin.example.coffee.pump.Pump + +@Singleton +class CoffeeMaker(private val pump: Pump, private val heater: Heater) { + + fun brew() { + heater.on() + pump.pump() + println(" [_]P coffee! [_]P ") + heater.off() + } +} \ No newline at end of file diff --git a/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/CoffeePumpList.kt b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/CoffeePumpList.kt new file mode 100644 index 00000000..f699c276 --- /dev/null +++ b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/CoffeePumpList.kt @@ -0,0 +1,7 @@ +package org.koin.example.coffee + +import org.koin.core.annotation.Single +import org.koin.example.coffee.pump.Pump + +@Single +class CoffeePumpList(val list : List) \ No newline at end of file diff --git a/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/ElectricHeater.kt b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/ElectricHeater.kt new file mode 100644 index 00000000..9fdae32f --- /dev/null +++ b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/ElectricHeater.kt @@ -0,0 +1,20 @@ +package org.koin.example.coffee + +import org.koin.core.annotation.Single + +@Single +class ElectricHeater : Heater { + + private var heating: Boolean = false + + override fun on() { + println("~ ~ ~ heating ~ ~ ~") + heating = true + } + + override fun off() { + heating = false + } + + override fun isHot(): Boolean = heating +} \ No newline at end of file diff --git a/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/Heater.kt b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/Heater.kt new file mode 100644 index 00000000..d12d14c5 --- /dev/null +++ b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/Heater.kt @@ -0,0 +1,7 @@ +package org.koin.example.coffee + +interface Heater { + fun on() + fun off() + fun isHot(): Boolean +} \ No newline at end of file diff --git a/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/pump/FakePump.kt b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/pump/FakePump.kt new file mode 100644 index 00000000..d43bfd9c --- /dev/null +++ b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/pump/FakePump.kt @@ -0,0 +1,10 @@ +package org.koin.example.coffee.pump + +import org.koin.core.annotation.Single + +@Single +class FakePump : Pump { + override fun pump() { + println("fake pump") + } +} \ No newline at end of file diff --git a/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/pump/Pump.kt b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/pump/Pump.kt new file mode 100644 index 00000000..a52d20d6 --- /dev/null +++ b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/pump/Pump.kt @@ -0,0 +1,5 @@ +package org.koin.example.coffee.pump + +interface Pump { + fun pump() +} \ No newline at end of file diff --git a/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/pump/PumpCounter.kt b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/pump/PumpCounter.kt new file mode 100644 index 00000000..e0e6372a --- /dev/null +++ b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/pump/PumpCounter.kt @@ -0,0 +1,5 @@ +package org.koin.example.coffee.pump + +class PumpCounter(val pump : List){ + val count = pump.size +} \ No newline at end of file diff --git a/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/pump/Thermosiphon.kt b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/pump/Thermosiphon.kt new file mode 100644 index 00000000..0afb5e5e --- /dev/null +++ b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/coffee/pump/Thermosiphon.kt @@ -0,0 +1,13 @@ +package org.koin.example.coffee.pump + +import org.koin.core.annotation.Single +import org.koin.example.coffee.Heater + +@Single +class Thermosiphon(private val heater: Heater) : Pump { + override fun pump() { + if (heater.isHot()) { + println("=> => pumping => =>") + } + } +} \ No newline at end of file diff --git a/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/di/CoffeeAppAndTesterModule.kt b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/di/CoffeeAppAndTesterModule.kt new file mode 100644 index 00000000..145d91c7 --- /dev/null +++ b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/di/CoffeeAppAndTesterModule.kt @@ -0,0 +1,36 @@ +package org.koin.example.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single +import org.koin.example.coffee.CoffeeMaker +import org.koin.example.coffee.pump.Pump +import org.koin.example.coffee.pump.PumpCounter +import org.koin.example.test.CoffeeMakerTesterTest +import org.koin.example.test.CoffeeMakerTesterTestImpl + +/** + * CoffeeAppAndTesterModule + * + * This module is responsible for configuring component scanning for both the Coffee (glob) application + * and its associated tester components. + * + * It scans the following package patterns: + * - "org.koin.example.coff*.**" + * - "org.koin.example.coff*" + * - "org.koin.example.tes*" + * + * Note: The two patterns for the coffee components ("org.koin.example.coff*.**" and "org.koin.example.coff*") + * can be consolidated into a single, more concise pattern "org.koin.example.coff**". + */ +@Module +@ComponentScan("org.koin.example.coff*.**", "org.koin.example.coff*", "org.koin.example.tes*") +class CoffeeAppAndTesterModule { + + @Single + fun pumpCounter(list: List) = PumpCounter(list) + + + @Single + fun CoffeeMakerTesterTest(coffeeMaker: CoffeeMaker): CoffeeMakerTesterTest = CoffeeMakerTesterTestImpl(coffeeMaker) +} diff --git a/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/tea/TeaModule.kt b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/tea/TeaModule.kt new file mode 100644 index 00000000..96161bcf --- /dev/null +++ b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/tea/TeaModule.kt @@ -0,0 +1,8 @@ +package org.koin.example.tea + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +class TeaModule \ No newline at end of file diff --git a/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/tea/TeaPot.kt b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/tea/TeaPot.kt new file mode 100644 index 00000000..794fef26 --- /dev/null +++ b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/tea/TeaPot.kt @@ -0,0 +1,7 @@ +package org.koin.example.tea + +import org.koin.core.annotation.Single +import org.koin.example.coffee.Heater + +@Single +class TeaPot(val heater: Heater) \ No newline at end of file diff --git a/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/test/CoffeeMakerTester.kt b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/test/CoffeeMakerTester.kt new file mode 100644 index 00000000..c67ab355 --- /dev/null +++ b/examples/coffee-maker-glob/src/main/kotlin/org/koin/example/test/CoffeeMakerTester.kt @@ -0,0 +1,20 @@ +package org.koin.example.test + +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single +import org.koin.example.coffee.CoffeeMaker + +@Factory +@Named("test") +class CoffeeMakerTester(val coffeeMaker: CoffeeMaker) + +interface CoffeeMakerTesterTest { + fun coffeeTest() +} + +class CoffeeMakerTesterTestImpl(val coffeeMaker: CoffeeMaker) : CoffeeMakerTesterTest { + override fun coffeeTest() { + coffeeMaker.brew() + } +} diff --git a/examples/coffee-maker-glob/src/test/java/CoffeeGlobAppTest.kt b/examples/coffee-maker-glob/src/test/java/CoffeeGlobAppTest.kt new file mode 100644 index 00000000..7e6e615a --- /dev/null +++ b/examples/coffee-maker-glob/src/test/java/CoffeeGlobAppTest.kt @@ -0,0 +1,182 @@ +import org.junit.Test +import org.koin.compiler.util.anyMatch +import org.koin.compiler.util.matchesGlob +import org.koin.compiler.util.toGlobRegex +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.core.logger.Level +import org.koin.core.parameter.parametersOf +import org.koin.core.qualifier.StringQualifier +import org.koin.core.qualifier.named +import org.koin.example.CoffeeApp +import org.koin.example.coffee.CoffeePumpList +import org.koin.example.coffee.MyDetachCoffeeComponent +import org.koin.example.coffee.pump.PumpCounter +import org.koin.example.di.CoffeeAppAndTesterModule +import org.koin.example.tea.TeaModule +import org.koin.example.tea.TeaPot +import org.koin.example.test.CoffeeMakerTester +import org.koin.example.test.CoffeeMakerTesterTest +import org.koin.example.test.ext.* +import org.koin.example.test.ext2.ExternalModule +import org.koin.example.test.include.IncludedComponent +import org.koin.example.test.scope.* +import org.koin.ksp.generated.module +import org.koin.mp.KoinPlatformTools +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.time.measureTime + +class CoffeeGlobAppTest { + + @Test + fun all_coffee_test() { + startKoin { + printLogger(Level.DEBUG) + // if no module +// defaultModule() + + // else let's use our modules + modules( + CoffeeAppAndTesterModule().module, + TeaModule().module, + org.koin.example.test.ext.ExternalModule().module, + ExternalModule().module, + ScopeModule().module + ) + } + + val coffeeShop = CoffeeApp() + val time = measureTime { + coffeeShop.maker.brew() + } + println("Got Coffee in $time") + + // Tests + val koin = KoinPlatformTools.defaultContext().get() + koin.get().heater + koin.get(StringQualifier("test")) + koin.get().coffeeTest() + koin.get(StringQualifier("tc")) + val id = "id" + assert(koin.get { parametersOf(id) }.id == id) + assert(koin.get { parametersOf(id) }.id == id) + koin.setProperty("prop_id", id) + assert(koin.get().id == id) + assert(koin.get().id == id) + + val myScope = MyScope() + val scopeS = koin.createScope("_ID1_", named(), myScope) + assert(myScope == scopeS.get().myScope) + assert(myScope == scopeS.get().myScope) + assert(myScope == scopeS.get().myScope) + assert(scopeS.get() != scopeS.get()) + assert(myScope == scopeS.get().myScope) + + assert(scopeS.getOrNull() != null) + + koin.createScope("_ID2_", named(MY_SCOPE_SESSION)) + .get() + + assert(koin.get().id == null) + assert(koin.get { parametersOf("42") }.id == "42") + + koin.get() + + assert(koin.get().list.size == 2) + assert(koin.get().count == 2) + + assert(koin.getOrNull() != null) + + stopKoin() + } + + + /// -- Internal glob tests -- + @Test + fun `toGlobRegex converts single star correctly`() { + val glob = "com.example.*" + val regex = glob.toGlobRegex() + // "com.example" doesn't contain any star, so ensureGlobPattern appends ".*", + // but the glob already has a star so it will be replaced as [^.]*. + // Expected regex: ^com\.example\.[^.]*$ + assertTrue(regex.matches("com.example.foo")) + assertFalse(regex.matches("com.examplefoo")) + } + + @Test + fun `toGlobRegex converts double star correctly`() { + val glob = "com.**.service" + val regex = glob.toGlobRegex() + // The "**" should become ".*", allowing for multiple levels in between. + assertTrue(regex.matches("com.foo.service")) + assertTrue(regex.matches("com.foo.bar.service")) + assertFalse(regex.matches("com.service")) + } + + @Test + fun `toGlobRegex converts double star correctly - no leading dot`() { + val glob = "com**.service" + val regex = glob.toGlobRegex() + // The "**" should become ".*", allowing for multiple levels in between. + assertTrue(regex.matches("com.foo.service")) + assertTrue(regex.matches("com.foo.bar.service")) + assertTrue(regex.matches("com.service")) + } + + @Test + fun `ensureGlobPattern appends recursive glob when no star is present`() { + val glob = "com.example" + val regex = glob.toGlobRegex() + // Since "com.example" doesn't contain a star, ensureGlobPattern appends ".*", + // resulting in a regex that matches any string starting with "com.example". + assertTrue(regex.matches("com.example.foo")) + // This may also match "com.exampleFoo" because no dot separator is enforced. + assertTrue(regex.matches("com.examplefoo")) + } + + @Test + fun `matchesGlob performs an exact match`() { + assertTrue("com.example.foo".matchesGlob("com.example.foo")) + assertFalse("com.example.foo".matchesGlob("com.example.bar")) + } + + @Test + fun `matchesGlob supports ignoreCase flag`() { + assertTrue("com.example.foo".matchesGlob("com.example.Foo", ignoreCase = true)) + assertFalse("com.example.foo".matchesGlob("com.example.Foo", ignoreCase = false)) + } + + @Test + fun `anyMatch finds a matching key in a map`() { + val testMap = mapOf( + "com.example" to 0, + "com.example.foo" to 1, + "com.test.bar" to 2, + "org.sample.baz" to 3 + ) + assertTrue(testMap.anyMatch("com.example*.*")) + assertTrue(testMap.anyMatch("com.**")) + assertTrue(testMap.anyMatch("co*.ex**")) + assertTrue(testMap.anyMatch("co*.ex*.**")) + assertTrue(testMap.anyMatch("co*.ex*")) + assertFalse(testMap.anyMatch("org.example*")) + } + + @Test + fun `test conversion of various glob patterns to regex`() { + val singleStarGlob = "com.exampl*" + val mixedGlob = "com.exampl*.**" + val doubleStarGlob = "com.exampl**" + + assertTrue(singleStarGlob.toGlobRegex().matches("com.example")) + assertFalse(singleStarGlob.toGlobRegex().matches("com.example.foo")) + + assertFalse(mixedGlob.toGlobRegex().matches("com.example")) + assertTrue(mixedGlob.toGlobRegex().matches("com.example.foo")) + + assertTrue(doubleStarGlob.toGlobRegex().matches("com.example")) + assertTrue(doubleStarGlob.toGlobRegex().matches("com.example.foo")) + } + +} \ No newline at end of file diff --git a/examples/settings.gradle.kts b/examples/settings.gradle.kts index 056e123f..f2aa2426 100644 --- a/examples/settings.gradle.kts +++ b/examples/settings.gradle.kts @@ -18,6 +18,7 @@ dependencyResolutionManagement { include( ":coffee-maker", + ":coffee-maker-glob", ":coffee-maker-module", ":other-ksp", ":compile-perf", diff --git a/examples/test.sh b/examples/test.sh index 820713b2..31cf5803 100755 --- a/examples/test.sh +++ b/examples/test.sh @@ -1,4 +1,4 @@ #!/bin/sh ./gradlew testDebug --no-build-cache -./gradlew :other-ksp:test :coffee-maker:test :compile-perf:test --no-build-cache +./gradlew :other-ksp:test :coffee-maker:test :coffee-maker-glob:test :compile-perf:test --no-build-cache diff --git a/projects/koin-annotations/src/commonMain/kotlin/org/koin/core/annotation/CoreAnnotations.kt b/projects/koin-annotations/src/commonMain/kotlin/org/koin/core/annotation/CoreAnnotations.kt index f0679f49..a26ee817 100644 --- a/projects/koin-annotations/src/commonMain/kotlin/org/koin/core/annotation/CoreAnnotations.kt +++ b/projects/koin-annotations/src/commonMain/kotlin/org/koin/core/annotation/CoreAnnotations.kt @@ -206,7 +206,27 @@ annotation class Module(val includes: Array> = [], val createdAtStart: * Will scan in current package or with the explicit packages names. * For scan current package use empty value array or empty string. * - * @param value: packages to scan + * The [value] parameter supports both exact package names and glob patterns: + * + * 1. Exact package: `"com.example.service"` + * - Scans only the `com.example.service` package. + * - Same as `com.example.service**` in glob pattern. + * + * 2. Single-level wildcard (`*`): `"com.example.*.service"` + * - Matches one level of package hierarchy. + * - E.g., `com.example.user.service`, `com.example.order.service`. + * - Does NOT match `com.example.service` or `com.example.user.impl.service`. + * + * 3. Multi-level wildcard (`**`): `"com.example.**"` + * - Matches any number of package levels. + * - E.g., `com.example`, `com.example.service`, `com.example.service.user`. + * + * Wildcards can be combined and used at any level: + * - `"com.**.service.*data"`: All packages that ends with "data" in any `service` subpackage. + * - `"com.*.service.**"`: All classes in `com.X.service` and its subpackages. + * + * @param value The packages to scan. Can be an exact package name or a glob pattern. + * Defaults to the package of the annotated element if empty. */ @Target(AnnotationTarget.CLASS, AnnotationTarget.FIELD) annotation class ComponentScan(vararg val value: String = []) diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/KoinMetaData.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/KoinMetaData.kt index ad70eece..775c3ebb 100644 --- a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/KoinMetaData.kt +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/metadata/KoinMetaData.kt @@ -18,6 +18,7 @@ package org.koin.compiler.metadata import com.google.devtools.ksp.symbol.KSDeclaration import com.google.devtools.ksp.symbol.KSType import com.google.devtools.ksp.symbol.Visibility +import org.koin.compiler.util.matchesGlob import java.util.* typealias PackageName = String @@ -54,7 +55,7 @@ sealed class KoinMetaData { fun acceptDefinition(defPackageName: String): Boolean { return componentsScan.any { componentScan -> when { - componentScan.packageName.isNotEmpty() -> defPackageName.contains( + componentScan.packageName.isNotEmpty() -> defPackageName.matchesGlob( componentScan.packageName, ignoreCase = true ) @@ -158,7 +159,7 @@ sealed class KoinMetaData { fun isNotScoped(): Boolean = !isScoped() fun isType(keyword: DefinitionAnnotation): Boolean = this.keyword == keyword - val packageNamePrefix : String = if (packageName.isEmpty()) "" else "${packageName}." + val packageNamePrefix: String = if (packageName.isEmpty()) "" else "${packageName}." override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/KoinMetaDataScanner.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/KoinMetaDataScanner.kt index 22f0d3d5..fb3ca5cb 100644 --- a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/KoinMetaDataScanner.kt +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/scanner/KoinMetaDataScanner.kt @@ -25,6 +25,7 @@ import com.google.devtools.ksp.symbol.KSFunctionDeclaration import com.google.devtools.ksp.validate import org.koin.compiler.metadata.DEFINITION_ANNOTATION_LIST_TYPES import org.koin.compiler.metadata.KoinMetaData +import org.koin.compiler.util.anyMatch import org.koin.core.annotation.Module import org.koin.core.annotation.PropertyValue import org.koin.meta.annotations.ExternalDefinition @@ -127,7 +128,7 @@ class KoinMetaDataScanner( module.componentsScan.forEach { scan -> when (scan.packageName) { "" -> emptyScanList.add(module) - else -> if (moduleList.contains(scan.packageName)) { + else -> if (moduleList.anyMatch(scan.packageName)) { val existing = moduleList[scan.packageName]!! error("@ComponentScan with '${scan.packageName}' from module ${module.name} is already declared in ${existing.name}. Please fix @ComponentScan value ") } else { diff --git a/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/util/GlobToRegex.kt b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/util/GlobToRegex.kt new file mode 100644 index 00000000..73fc2c89 --- /dev/null +++ b/projects/koin-ksp-compiler/src/jvmMain/kotlin/org/koin/compiler/util/GlobToRegex.kt @@ -0,0 +1,68 @@ +package org.koin.compiler.util + + +/** + * Converts this string, interpreted as a glob pattern, to a regular expression. + * + * @param ignoreCase if true, the resulting regex will be case-insensitive. + * Default is false, as package names are typically case-sensitive. + * @return a [Regex] object that matches strings according to this glob pattern. + * + * @throws IllegalArgumentException if the glob pattern contains `**` without a preceding dot. + * + * @author OffRange + */ +fun String.toGlobRegex(ignoreCase: Boolean = false): Regex { + // 1) Escape dots + // so we can safely do replacements for * and ** below. + val escaped = replace(".", "\\.") + + // 2) Replace '**' with a unique placeholder (so we don't conflict with single '*') + val doubleStarPlaceholder = "\u0000" + val afterDoubleStar = escaped.replace("**", doubleStarPlaceholder) + + // 3) Replace single '*' with [^.]* (zero-or-more non-dot characters) + val afterSingleStar = afterDoubleStar.replace("*", "[^.]*") + + // 4) Replace our placeholder with .* (zero-or-more of any character) + val afterPlaceholder = afterSingleStar.replace(doubleStarPlaceholder, ".*") + + // 5) Wrap with ^ and $ for a full-match regex + return with("^${afterPlaceholder.ensureGlobPattern()}$") { + if (ignoreCase) toRegex(RegexOption.IGNORE_CASE) else toRegex() + } +} + +/** + * Checks if this map contains a key that matches the given glob pattern. + * + * @param keyGlob the glob pattern to match against keys. + * @return true if any key matches the glob pattern, false otherwise. + * + * @author OffRange + */ +fun Map.anyMatch(keyGlob: String): Boolean = keys.any { keyGlob.toGlobRegex().matches(it) } + +/** + * Checks if this string matches the given glob pattern. + * + * @param glob the glob pattern to match. + * @param ignoreCase if true, the match will be case-insensitive. + * Default is false, as package and class names are typically case-sensitive. + * @return true if this string matches the glob pattern, false otherwise. + * + * @author OffRange + */ +fun String.matchesGlob(glob: String, ignoreCase: Boolean = false): Boolean = + glob.toGlobRegex(ignoreCase = ignoreCase).matches(this) + +/** + * Ensures that this string is a valid glob pattern. + * Implemented to ensure backwards compatibility, as `com.example` would ONLY match this package. + * + * @receiver the string to ensure as a glob pattern. + * @return this string if it already contains a glob pattern, otherwise this string with a recursive glob pattern appended. + * + * @author OffRange + */ +private fun String.ensureGlobPattern(): String = if (contains('*')) this else "$this.*" \ No newline at end of file