Skip to content

Commit 528edfa

Browse files
feat: Use @file:ProjectCoordinates in --export-to-gradle-project
This commit enhances the `--export-to-gradle-project` feature by allowing kscripts to define their own target Maven coordinates (groupId, artifactId, version) via a new `@file:ProjectCoordinates` annotation. Key changes: 1. **Annotation Parsing (`Script.kt`, `Parser.kt`, `model/ProjectCoordinates.kt`):** - I introduced a new data class `model.ProjectCoordinates` to hold `group`, `artifact`, and `version`. - The `Script` model now includes an optional `projectCoordinates` field. - I updated `LineParser.kt` and `Parser.kt` to recognize and parse `@file:ProjectCoordinates(group="...", artifact="...", version="...")` annotations from script files. - The parsed coordinates are stored in the `Script` object via `ResolutionContext` and `SectionResolver`. 2. **Project Generation (`generator/ProjectGenerator.kt`):** - The `exportToGradleProject` function now retrieves any `ProjectCoordinates` defined in the script. - These script-defined coordinates are prioritized when setting: - `group` in `build.gradle.kts`. - `version` in `build.gradle.kts`. - `rootProject.name` in `settings.gradle.kts` (derived from the artifactId). - Fallback logic (using script package name, output directory name, or defaults) is retained if the annotation or specific attributes are missing. - The package path for source files (`src/main/kotlin/...`, `src/test/kotlin/...`) and the `package` declaration in the generated `.kt` file are now based on the `effectiveProjectGroup` (derived from the annotation or fallbacks). - Default `mainClassName` derivation also uses `effectiveProjectGroup`. This makes the project generation feature more declarative, allowing the script itself to be the source of truth for its intended Maven identity when being "graduated" into a full Gradle project.
1 parent 2b7c5db commit 528edfa

File tree

8 files changed

+115
-19
lines changed

8 files changed

+115
-19
lines changed

src/main/kotlin/io/github/kscripting/kscript/generator/ProjectGenerator.kt

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -48,21 +48,37 @@ fun exportToGradleProject(scriptFilePathString: String, outputDir: Path, cliOpti
4848
infoMsg("Successfully created directory: $outputDir")
4949
}
5050

51-
// 1. Determine Project Properties
52-
val projectName = outputDir.fileName.toString()
53-
// Try to use script package name for group, fallback to default
54-
val projectGroup = if (script.packageName.value.isNotBlank()) script.packageName.value else "com.example"
55-
val projectVersion = "1.0-SNAPSHOT" // Default version
51+
// 1. Determine Project Properties, prioritizing @file:ProjectCoordinates
52+
val scriptCoordinates = script.projectCoordinates
53+
54+
if (scriptCoordinates != null && (scriptCoordinates.group != null || scriptCoordinates.artifact != null || scriptCoordinates.version != null)) {
55+
infoMsg("Using project coordinates from @file:ProjectCoordinates annotation:")
56+
scriptCoordinates.group?.let { infoMsg(" Group: $it") }
57+
scriptCoordinates.artifact?.let { infoMsg(" Artifact: $it") }
58+
scriptCoordinates.version?.let { infoMsg(" Version: $it") }
59+
}
60+
61+
val effectiveProjectGroup = scriptCoordinates?.group?.takeIf { it.isNotBlank() }
62+
?: script.packageName.value.takeIf { it.isNotBlank() }
63+
?: "com.example"
64+
65+
val effectiveArtifactId = scriptCoordinates?.artifact?.takeIf { it.isNotBlank() }
66+
?: outputDir.fileName.toString()
67+
68+
val effectiveProjectVersion = scriptCoordinates?.version?.takeIf { it.isNotBlank() }
69+
?: "1.0-SNAPSHOT"
70+
71+
val projectNameForSettings = effectiveArtifactId // For settings.gradle.kts rootProject.name
5672

57-
infoMsg("Project Name: $projectName")
58-
infoMsg("Project Group: $projectGroup")
59-
infoMsg("Project Version: $projectVersion")
73+
infoMsg("Effective Project Name (for settings.gradle.kts): $projectNameForSettings")
74+
infoMsg("Effective Project Group: $effectiveProjectGroup")
75+
infoMsg("Effective Project Version: $effectiveProjectVersion")
6076

6177
// 2. Create standard project directories
62-
val srcMainKotlinPath = projectGroup.replace(".", "/")
63-
val srcMainKotlinDir = outputDir.resolve("src/main/kotlin").resolve(srcMainKotlinPath)
78+
val packagePath = effectiveProjectGroup.replace(".", "/")
79+
val srcMainKotlinDir = outputDir.resolve("src/main/kotlin").resolve(packagePath)
6480
val srcMainResourcesDir = outputDir.resolve("src/main/resources")
65-
val srcTestKotlinDir = outputDir.resolve("src/test/kotlin").resolve(srcMainKotlinPath)
81+
val srcTestKotlinDir = outputDir.resolve("src/test/kotlin").resolve(packagePath)
6682
val srcTestResourcesDir = outputDir.resolve("src/test/resources")
6783

6884
Files.createDirectories(srcMainKotlinDir)
@@ -77,7 +93,7 @@ fun exportToGradleProject(scriptFilePathString: String, outputDir: Path, cliOpti
7793
// 3. Generate settings.gradle.kts
7894
val settingsFile = outputDir.resolve("settings.gradle.kts")
7995
val settingsContent = """
80-
|rootProject.name = "$projectName"
96+
|rootProject.name = "$projectNameForSettings"
8197
|""".trimMargin()
8298
Files.writeString(settingsFile, settingsContent)
8399
infoMsg("Successfully generated: $settingsFile")
@@ -87,8 +103,8 @@ fun exportToGradleProject(scriptFilePathString: String, outputDir: Path, cliOpti
87103
val customReposString = script.repositories.joinToString("\n ") { "maven { url = uri(\"${it.value}\") }" }
88104

89105
// Determine main class name
90-
val scriptBaseName = scriptFile.nameWithoutExtension.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
91-
val mainClassName = script.entryPoint?.value ?: "${projectGroup}.${scriptBaseName}Kt"
106+
val scriptBaseName = scriptFile.nameWithoutExtension.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
107+
val mainClassName = script.entryPoint?.value?.takeIf { it.isNotBlank() } ?: "${effectiveProjectGroup}.${scriptBaseName}Kt"
92108

93109
// Kotlin options and compiler options (basic for now)
94110
// More complex mapping can be done later. For now, just ensure jvmTarget.
@@ -100,8 +116,8 @@ fun exportToGradleProject(scriptFilePathString: String, outputDir: Path, cliOpti
100116
| application
101117
|}
102118
|
103-
|group = "$projectGroup"
104-
|version = "$projectVersion"
119+
|group = "$effectiveProjectGroup"
120+
|version = "$effectiveProjectVersion"
105121
|
106122
|repositories {
107123
| mavenCentral()
@@ -149,8 +165,8 @@ fun exportToGradleProject(scriptFilePathString: String, outputDir: Path, cliOpti
149165
val contentLines = mutableListOf<String>()
150166

151167
// Add package declaration
152-
if (projectGroup.isNotBlank()) {
153-
contentLines.add("package $projectGroup")
168+
if (effectiveProjectGroup.isNotBlank()) {
169+
contentLines.add("package $effectiveProjectGroup")
154170
contentLines.add("") // Add a blank line after package
155171
}
156172

@@ -234,7 +250,7 @@ fun exportToGradleProject(scriptFilePathString: String, outputDir: Path, cliOpti
234250
warnMsg("gradle-wrapper.properties not found in kscript project, skipping copy. Project will need 'gradle wrapper' task run manually.")
235251
}
236252

237-
infoMsg("Project generation for '$projectName' is complete.")
253+
infoMsg("Project generation for '$projectNameForSettings' is complete.")
238254

239255
} catch (e: Exception) {
240256
errorMsg("Error during project generation for $scriptFilePathString: ${e.message}")
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.github.kscripting.kscript.model
2+
3+
/**
4+
* Represents project coordinates that can be defined in a kscript file
5+
* via the `@file:ProjectCoordinates(group="...", artifact="...", version="...")` annotation.
6+
* All properties are optional.
7+
*/
8+
data class ProjectCoordinates(
9+
val group: String? = null,
10+
val artifact: String? = null,
11+
val version: String? = null
12+
) : ScriptAnnotation

src/main/kotlin/io/github/kscripting/kscript/model/Script.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ data class Script(
1818
val compilerOpts: Set<CompilerOpt>,
1919
val deprecatedItems: Set<DeprecatedItem>,
2020

21+
val projectCoordinates: ProjectCoordinates? = null, // Added new field
22+
2123
val scriptNodes: Set<ScriptNode>,
2224
val rootNode: ScriptNode,
2325

src/main/kotlin/io/github/kscripting/kscript/parser/LineParser.kt

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ object LineParser {
88
private const val deprecatedAnnotation = "Deprecated annotation:"
99
private val sheBang = listOf(SheBang)
1010

11+
// Regex for @file:ProjectCoordinates(group="...", artifact="...", version="...")
12+
private val PROJECT_COORDINATES_ANNOTATION_REGEX = Regex("""^@file:ProjectCoordinates\s*\((.*)\)""")
13+
// Regex for parsing individual attributes like group="value", artifact='value', version=value
14+
// It captures the key, and then one of the possible quote types for the value, or unquoted value.
15+
private val ATTRIBUTE_REGEX = Regex("""(group|artifact|version)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s,)]+))""")
16+
1117
fun parseSheBang(scriptLocation: ScriptLocation, line: Int, text: String): List<ScriptAnnotation> {
1218
if (text.startsWith("#!/")) {
1319
return sheBang
@@ -357,4 +363,49 @@ object LineParser {
357363
scriptLocation: ScriptLocation, line: Int, introText: String, existing: String, replacement: String
358364
): DeprecatedItem =
359365
DeprecatedItem(scriptLocation, line, "$introText\n$existing\nshould be replaced with:\n$replacement")
366+
367+
fun parseProjectCoordinates(scriptLocation: ScriptLocation, line: Int, text: String): List<ScriptAnnotation> {
368+
val trimmedText = text.trim()
369+
val matchResult = PROJECT_COORDINATES_ANNOTATION_REGEX.find(trimmedText)
370+
371+
if (matchResult != null) {
372+
val attributesString = matchResult.groupValues[1]
373+
try {
374+
val projectCoordinates = extractProjectCoordinatesFromAttributes(attributesString)
375+
// It's possible to return an empty ProjectCoordinates if attributesString is empty or only whitespace
376+
// We might want to consider if an empty coordinate set is a valid annotation or should be an error/emptyList()
377+
return listOf(projectCoordinates)
378+
} catch (e: ParseException) {
379+
// Augment parse exception with location context
380+
throw ParseException("Invalid @file:ProjectCoordinates annotation at $scriptLocation line $line: ${e.messageOriginal}", e)
381+
}
382+
}
383+
return emptyList()
384+
}
385+
386+
private fun extractProjectCoordinatesFromAttributes(attributesString: String): ProjectCoordinates {
387+
var group: String? = null
388+
var artifact: String? = null
389+
var version: String? = null
390+
391+
if (attributesString.isBlank()) {
392+
// Return default (all null) if attributes string is empty or blank
393+
return ProjectCoordinates(null, null, null)
394+
}
395+
396+
ATTRIBUTE_REGEX.findAll(attributesString).forEach { matchResult ->
397+
val key = matchResult.groupValues[1]
398+
// Value can be in group 2 (double-quoted), 3 (single-quoted), or 4 (unquoted)
399+
val value = matchResult.groupValues[2].takeIf { it.isNotEmpty() }
400+
?: matchResult.groupValues[3].takeIf { it.isNotEmpty() }
401+
?: matchResult.groupValues[4]
402+
403+
when (key) {
404+
"group" -> group = value
405+
"artifact" -> artifact = value
406+
"version" -> version = value
407+
}
408+
}
409+
return ProjectCoordinates(group, artifact, version)
410+
}
360411
}

src/main/kotlin/io/github/kscripting/kscript/parser/Parser.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class Parser {
1515
LineParser::parseCompilerOpts,
1616
LineParser::parseImport,
1717
LineParser::parsePackage,
18+
LineParser::parseProjectCoordinates, // Added new parser function
1819
)
1920

2021
fun parse(scriptLocation: ScriptLocation, string: String): List<Section> {

src/main/kotlin/io/github/kscripting/kscript/resolver/ResolutionContext.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ data class ResolutionContext(
2020
val compilerOpts: MutableSet<CompilerOpt> = mutableSetOf(),
2121
val importNames: MutableSet<ImportName> = mutableSetOf(),
2222
val deprecatedItems: MutableSet<DeprecatedItem> = mutableSetOf(),
23+
var projectCoordinates: ProjectCoordinates? = null, // Added for @file:ProjectCoordinates
2324
)

src/main/kotlin/io/github/kscripting/kscript/resolver/ScriptResolver.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ class ScriptResolver(
174174
resolutionContext.kotlinOpts,
175175
resolutionContext.compilerOpts,
176176
resolutionContext.deprecatedItems,
177+
resolutionContext.projectCoordinates, // Pass the new field
177178
resolutionContext.scriptNodes,
178179
scriptNode,
179180
digest

src/main/kotlin/io/github/kscripting/kscript/resolver/SectionResolver.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,18 @@ class SectionResolver(
149149
is DeprecatedItem -> {
150150
resolutionContext.deprecatedItems.add(scriptAnnotation)
151151
}
152+
153+
is ProjectCoordinates -> {
154+
if (resolutionContext.projectCoordinates == null) { // Process only the first one found
155+
resolutionContext.projectCoordinates = scriptAnnotation
156+
}
157+
// else {
158+
// Potentially log a warning if multiple @file:ProjectCoordinates are found,
159+
// but for now, we just keep the first one encountered.
160+
// io.github.kscripting.kscript.util.Logger.warnMsg("Multiple @file:ProjectCoordinates annotations found. Using the first one: ${resolutionContext.projectCoordinates}")
161+
// }
162+
resolvedScriptAnnotations += scriptAnnotation // Add to section's annotations
163+
}
152164
}
153165

154166
return resolvedScriptAnnotations

0 commit comments

Comments
 (0)