diff --git a/agentic-tool/build.gradle.dcl b/agentic-tool/build.gradle.dcl new file mode 100644 index 0000000..3577627 --- /dev/null +++ b/agentic-tool/build.gradle.dcl @@ -0,0 +1,19 @@ +kotlinJvmApplication { + mainClass = "org.gradle.client.agentic.AgenticToolKt" + jvmArguments += listOf("--enable-native-access=ALL-UNNAMED") + + dependencies { + implementation(project(":build-action")) + implementation(project(":dcl-utils")) + + implementation("org.gradle:gradle-tooling-api:9.6.0-20260511195019+0000") + implementation("org.gradle:gradle-declarative-dsl-core:9.6.0-20260511195019+0000") + implementation("org.gradle:gradle-declarative-dsl-evaluator:9.6.0-20260511195019+0000") + implementation("org.gradle:gradle-declarative-dsl-tooling-models:9.6.0-20260511195019+0000") + + implementation("com.github.ajalt.clikt:clikt:5.0.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") + + runtimeOnly("org.slf4j:slf4j-simple:2.0.17") + } +} diff --git a/agentic-tool/src/main/kotlin/org/gradle/client/agentic/AgenticTool.kt b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/AgenticTool.kt new file mode 100644 index 0000000..edbb6d5 --- /dev/null +++ b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/AgenticTool.kt @@ -0,0 +1,66 @@ +package org.gradle.client.agentic + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.Context +import com.github.ajalt.clikt.core.NoOpCliktCommand +import com.github.ajalt.clikt.core.main +import com.github.ajalt.clikt.core.subcommands +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import org.gradle.client.build.action.GetResolvedDomAction +import org.gradle.client.build.model.ResolvedDomPrerequisites +import org.gradle.internal.declarativedsl.evaluator.main.SimpleAnalysisEvaluator +import org.gradle.tooling.BuildAction +import org.gradle.tooling.GradleConnector +import java.io.File + +fun main(args: Array) { + MainCommand() + .subcommands( + ProjectsCommand(), + EffectiveProjectDefinitionCommand(), + SettingsDefaultsCommand(), + SettingsDefinitionCommand(), + ProjectSchemaCommand(), + ProjectTypesAndFeaturesCommand() + ) + .main(args) +} + +class MainCommand : NoOpCliktCommand() { + override fun help(context: Context): String = + "An agent-friendly introspection tool for Declarative Gradle projects." + + "\nEach command outputs a JSON on the standard output and has its own ' --help'." +} + +abstract class DclCommand(name: String) : CliktCommand(name) { + protected val rootProjectDir: String by option(help = "Gradle build root project directory").default(".") + protected val documentationProvider: DocumentationProvider = SchemaEmbeddedDocumentationProvider + + private val domPrerequisites: BuildAction = GetResolvedDomAction() + + final override fun run() { + val connector = GradleConnector.newConnector().forProjectDirectory(File(rootProjectDir).absoluteFile) + val prerequisites = try { + connector.connect().use { connection -> + connection.action(domPrerequisites).addArguments("--no-scan", "--quiet").run() + } + } finally { + connector.disconnect() + } + + val analyzer = SimpleAnalysisEvaluator.withSchema( + prerequisites.settingsInterpretationSequence, + prerequisites.projectInterpretationSequence + ) + + val output = produceOutput(prerequisites, analyzer) + println(Json { prettyPrint = true }.encodeToString(output)) + } + + abstract fun produceOutput(prerequisites: ResolvedDomPrerequisites, analyzer: SimpleAnalysisEvaluator): JsonElement +} + + diff --git a/agentic-tool/src/main/kotlin/org/gradle/client/agentic/AnalysisUtils.kt b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/AnalysisUtils.kt new file mode 100644 index 0000000..8c6a037 --- /dev/null +++ b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/AnalysisUtils.kt @@ -0,0 +1,22 @@ +package org.gradle.client.agentic + +import org.gradle.client.build.model.ResolvedDomPrerequisites +import org.gradle.internal.declarativedsl.dom.operations.overlay.DocumentOverlayResult +import org.gradle.internal.declarativedsl.evaluator.main.AnalysisDocumentUtils +import org.gradle.internal.declarativedsl.evaluator.main.SimpleAnalysisEvaluator + +fun getDomForProject( + analyzer: SimpleAnalysisEvaluator, + prerequisites: ResolvedDomPrerequisites, + projectInfo: ResolvedDomPrerequisites.ProjectInfo +): DocumentOverlayResult? { + val settingsScript = prerequisites.settingsFile.takeIf { it.canRead() }?.readText() ?: "" + val projectScript = projectInfo.projectBuildFile.takeIf { it.canRead() }?.readText() ?: "" + + val settingsResult = + analyzer.evaluate(prerequisites.settingsFile.path, settingsScript) + val projectResult = + analyzer.evaluate(projectInfo.projectBuildFile.path, projectScript) + + return AnalysisDocumentUtils.documentWithModelDefaults(settingsResult, projectResult) +} diff --git a/agentic-tool/src/main/kotlin/org/gradle/client/agentic/DocumentationProvider.kt b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/DocumentationProvider.kt new file mode 100644 index 0000000..f2cd3c0 --- /dev/null +++ b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/DocumentationProvider.kt @@ -0,0 +1,36 @@ +package org.gradle.client.agentic + +import org.gradle.declarative.dsl.schema.DataProperty +import org.gradle.declarative.dsl.schema.DataType +import org.gradle.declarative.dsl.schema.SchemaDocumentation +import org.gradle.declarative.dsl.schema.SchemaItemMetadata +import org.gradle.declarative.dsl.schema.SchemaMemberFunction + +interface DocumentationProvider { + fun classDocumentation(classDataType: DataType.ClassDataType): String? + fun functionDocumentation(schemaMemberFunction: SchemaMemberFunction): String? + fun propertyDocumentation(property: DataProperty): String? +} + +object SchemaEmbeddedDocumentationProvider : DocumentationProvider { + override fun classDocumentation(classDataType: DataType.ClassDataType): String? = + extractDocsFromMetadata(classDataType.metadata) + + override fun functionDocumentation(schemaMemberFunction: SchemaMemberFunction): String? = + extractDocsFromMetadata(schemaMemberFunction.metadata) + + override fun propertyDocumentation(property: DataProperty): String? = + extractDocsFromMetadata(property.metadata) + + private fun extractDocsFromMetadata(metadata: Iterable) = + metadata + .filterIsInstance() + .flatMap { + listOf(it.text) + + it.parts.map { (key, value) -> + "* `$key`: $value" + } + } + .joinToString("\n\n") + .ifBlank { null } +} diff --git a/agentic-tool/src/main/kotlin/org/gradle/client/agentic/DomJsonRenderer.kt b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/DomJsonRenderer.kt new file mode 100644 index 0000000..7c05ff0 --- /dev/null +++ b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/DomJsonRenderer.kt @@ -0,0 +1,373 @@ +package org.gradle.client.agentic + +import kotlinx.serialization.json.* +import org.gradle.client.core.gradle.dcl.userFriendlyErrorMessages +import org.gradle.declarative.dsl.schema.* +import org.gradle.declarative.dsl.schema.DataType.ParameterizedTypeInstance.TypeArgument.ConcreteTypeArgument +import org.gradle.declarative.dsl.schema.DataType.ParameterizedTypeInstance.TypeArgument.StarProjection +import org.gradle.declarative.dsl.schema.FunctionSemantics.ConfigureSemantics +import org.gradle.internal.declarativedsl.analysis.isReadOnly +import org.gradle.internal.declarativedsl.analysis.ref +import org.gradle.internal.declarativedsl.dom.DeclarativeDocument +import org.gradle.internal.declarativedsl.dom.DeclarativeDocument.DocumentNode.* +import org.gradle.internal.declarativedsl.dom.DocumentResolution +import org.gradle.internal.declarativedsl.dom.DocumentResolution.ElementResolution.SuccessfulElementResolution +import org.gradle.internal.declarativedsl.dom.DocumentResolution.ValueNodeResolution.ValueFactoryResolution.ValueFactoryResolved +import org.gradle.internal.declarativedsl.dom.operations.overlay.DocumentOverlayResult +import org.gradle.internal.declarativedsl.dom.operations.overlay.OverlayNodeOrigin +import org.gradle.internal.declarativedsl.language.SourceData + +@Suppress("TooManyFunctions") +class DomJsonRenderer( + val withUnusedMembers: Boolean, + val dom: DocumentOverlayResult, + val documentationProvider: DocumentationProvider, +) { + @Suppress("ComplexMethod") + fun JsonArrayBuilder.visitNode(node: DeclarativeDocument.DocumentNode) { + when (node) { + is ElementNode -> addJsonObject { + val type = putElementHeaderAndGetType(node) as? DataClass + + val properties = node.content.filterIsInstance() + val unusedProperties = type?.properties?.toSet() + ?.minus(properties.mapNotNull { it.asResolved(dom)?.property }.toSet()) + .orEmpty() + + if (properties.isNotEmpty()) { + put("properties", buildJsonObject { + properties.forEach { put(it.name, propertyInfo(it)) } + }) + } + if (withUnusedMembers && unusedProperties.isNotEmpty()) { + put("unusedProperties", buildJsonObject { + unusedProperties.forEach { put(it.name, unusedPropertyInfo(type!!, it)) } + }) + } + + val (features, nestedObjects) = + node.content.filterIsInstance().partition { isFeatureUsageElement(it) } + + if (features.isNotEmpty()) { + put("features", buildJsonArray { features.forEach { visitNode(it) } }) + } + if (nestedObjects.isNotEmpty()) { + putJsonArray("nestedObjects") { nestedObjects.forEach { visitNode(it) } } + } + + val usedFunctions = node.content.asSequence() + .filterIsInstance() + .map(dom.overlayResolutionContainer::data) + .filterIsInstance() + .map { it.elementFactoryFunction } + .toSet() + + val (unusedFeatures, unusedNestedObjects) = type?.memberFunctions + ?.filter { it.semantics is ConfigureSemantics && it !in usedFunctions } + .orEmpty() + .partition { isFeatureUsageFunction(it) } + + if (withUnusedMembers && unusedFeatures.isNotEmpty()) { + putJsonArray("unusedFeatures") { + unusedFeatures.forEach { add(unusedNestedObject(it)) } + } + } + if (withUnusedMembers && unusedNestedObjects.isNotEmpty()) { + putJsonArray("unusedNestedObjects") { + unusedNestedObjects.forEach { add(unusedNestedObject(it)) } + } + } + + val errors = node.content.filterIsInstance() + if (errors.isNotEmpty()) { + putJsonArray("errors") { + errors.forEach { add(errorNode(dom, it)) } + } + } + + val resolved = dom.overlayResolutionContainer.data(node) + if (resolved is SuccessfulElementResolution) { + documentationProvider.functionDocumentation(resolved.elementFactoryFunction) + ?.let { put("documentation", it) } + } + } + + is PropertyNode -> add(propertyInfo(node)) + is ErrorNode -> add(errorNode(dom, node)) + } + } + + private fun isFeatureUsageElement(node: ElementNode): Boolean = + (dom.overlayResolutionContainer.data(node) as? SuccessfulElementResolution) + ?.elementFactoryFunction?.let(::isFeatureUsageFunction) == true + + private fun JsonObjectBuilder.putElementHeaderAndGetType( + node: ElementNode, + ): DataType? { + put("name", node.name) + putJsonArray("args") { + node.elementValues.forEach { putValueInObject(it, dom) } + } + + val overlayOrigin = dom.overlayNodeOriginContainer.data(node) + val resolution = dom.overlayResolutionContainer.data(node) + val function = when (resolution) { + is SuccessfulElementResolution -> { + resolution.elementFactoryFunction + } + + is DocumentResolution.UnsuccessfulResolution -> { + put("isUnresolved", true) + putJsonArray("causes") { + userFriendlyErrorMessages(node, resolution).forEach(::add) + } + null + } + } + val type = if (resolution is SuccessfulElementResolution) { + resolution.elementType + } else null + + if (type != null) { + put("type", type.ref.typeName) + val supertypeNames = (type as? DataClass)?.supertypes + ?.filter { it.qualifiedName != Any::class.qualifiedName }.orEmpty() + if (supertypeNames.isNotEmpty()) { + putJsonArray("supertypes") { + supertypeNames.forEach { add(it.qualifiedName) } + } + } + if (type is DataType.ClassDataType) { + documentationProvider.classDocumentation(type) + ?.let { put("typeDocumentation", it) } + } + } + + elementMetadata(function) + + sourceLocation(overlayOrigin, node) + + return type + } + + fun propertyInfo(property: PropertyNode): JsonObject { + val resolved = dom.overlayResolutionContainer.data(property) + return buildJsonObject { + if (property.augmentation == PropertyNode.PropertyAugmentation.Plus) { + put("augmentedWithPlusAssign", true) + } + when (resolved) { + is DocumentResolution.PropertyResolution.PropertyAssignmentResolved -> { + put("type", resolved.property.valueType.typeName) + if (resolved.property.isReadOnly) { + put("isReadOnly", true) + } + } + + is DocumentResolution.PropertyResolution.PropertyNotAssigned -> { + put("isUnresolved", true) + putJsonArray("causes") { + userFriendlyErrorMessages(property, resolved).forEach(::add) + } + } + } + + val origin = dom.overlayNodeOriginContainer.data(property) + + putValueInline(property.value, dom) + if (origin is OverlayNodeOrigin.MergedProperties) { + val effective = origin.effectivePropertiesFromUnderlay + + origin.effectivePropertiesFromOverlay + + if (effective.size > 1) { + putJsonArray("multipartEffectiveValue") { + addJsonObject { + effective.forEach { putValueInline(it.value, dom) } + } + } + } + + val shadowed = origin.shadowedPropertiesFromUnderlay + + origin.shadowedPropertiesFromOverlay + + if (shadowed.isNotEmpty()) { + putJsonArray("overridesValues") { + addJsonObject { + shadowed.forEach { putValueInline(it.value, dom, it.sourceData) } + } + } + } + } + + if (resolved is DocumentResolution.PropertyResolution.PropertyAssignmentResolved) { + documentationProvider.propertyDocumentation(resolved.property) + ?.let { put("documentation", it) } + } + } + } + + private fun unusedPropertyInfo(type: DataClass?, property: DataProperty): JsonObject = buildJsonObject { + put("type", property.valueType.typeName) + if (type != null) { + documentationProvider.propertyDocumentation(property) + ?.let { put("documentation", it) } + } + } + + private fun unusedNestedObject(function: SchemaMemberFunction): JsonObject = buildJsonObject { + put("name", function.simpleName) + put("type", (function.semantics as ConfigureSemantics).configuredType.typeName) + put( + "isBlock", + (function.semantics as ConfigureSemantics).configureBlockRequirement.requires + ) + if (function.parameters.isNotEmpty()) { + putJsonArray("parameters") { + function.parameters.forEach { param -> + addJsonObject { + put("name", param.name) + put("type", param.type.typeName) + } + } + } + } + documentationProvider.functionDocumentation(function) + ?.let { put("documentation", it) } + } + + private fun errorNode( + fullDom: DocumentOverlayResult, + node: ErrorNode + ) = buildJsonObject { + val errorResolution = fullDom.overlayResolutionContainer.data(node) + put("source", node.sourceData.text()) + sourceLocation(fullDom.overlayNodeOriginContainer.data(node), node) + putJsonArray("causes") { + userFriendlyErrorMessages(node, errorResolution).forEach(::add) + } + } + + private fun JsonArrayBuilder.putValueInObject( + node: DeclarativeDocument.ValueNode, + fullDom: DocumentOverlayResult + ) = addJsonObject { putValueInline(node, fullDom) } + + private fun JsonObjectBuilder.putValueInline( + node: DeclarativeDocument.ValueNode, + fullDom: DocumentOverlayResult, + sourceLocation: SourceData? = null + ) { + when (node) { + is DeclarativeDocument.ValueNode.LiteralValueNode -> { + put("value", node.sourceData.text()) + } + is DeclarativeDocument.ValueNode.NamedReferenceNode -> { + put("value", node.referenceName) + sourceLocation(fullDom.overlayNodeOriginContainer.data(node), node) + } + + is DeclarativeDocument.ValueNode.ValueFactoryNode -> { + sourceLocation(fullDom.overlayNodeOriginContainer.data(node), node) + val resolved = fullDom.overlayResolutionContainer.data(node) + if (resolved is ValueFactoryResolved) { + put("type", resolved.function.semantics.returnValueType.typeName) + } + put("valueFactory", node.factoryName + (if (node.isInfix) "(infix-notation)" else "")) + putJsonArray("args") { + node.values.forEach { + putValueInObject(it, fullDom) + } + } + put("source", node.sourceData.text()) + } + } + if (sourceLocation != null) { + put("sourceLocation", sourceLocation.locationString) + } else { + sourceLocation(fullDom.overlayNodeOriginContainer.data(node), node) + } + } + + private fun JsonObjectBuilder.sourceLocation( + overlayOrigin: OverlayNodeOrigin, + node: DeclarativeDocument.Node + ) { + when (overlayOrigin) { + is OverlayNodeOrigin.CopiedOrigin -> { + if (overlayOrigin is OverlayNodeOrigin.FromUnderlay) { + put("isDefault", true) + } + put("sourceLocation", node.sourceData.locationString) + } + + is OverlayNodeOrigin.MergedElements -> putJsonArray("sourceLocations") { + add(overlayOrigin.underlayElement.sourceData.locationString) + add(overlayOrigin.overlayElement.sourceData.locationString) + } + + is OverlayNodeOrigin.MergedProperties -> putJsonArray("sourceLocations") { + overlayOrigin.effectivePropertiesFromUnderlay.map { it.sourceData.locationString }.forEach(::add) + overlayOrigin.effectivePropertiesFromOverlay.map { it.sourceData.locationString }.forEach(::add) + } + } + } + + private fun PropertyNode.asResolved(inDom: DocumentOverlayResult) = + inDom.overlayResolutionContainer.data(this) as? DocumentResolution.PropertyResolution.PropertyAssignmentResolved + + + private val SourceData.locationString: String + get() = "${this.sourceIdentifier.fileIdentifier}:${this.lineRange.first}" +} + +internal fun JsonObjectBuilder.elementMetadata(function: SchemaMemberFunction?) = + function?.metadata?.let(::elementMetadata) + +internal fun JsonObjectBuilder.elementMetadata(metadata: Iterable) { + metadata.forEach { metadata -> + when (metadata) { + is ProjectFeatureOrigin -> { + val isProjectType = + metadata.targetDefinitionClassName == "org.gradle.api.Project" + putJsonObject(if (isProjectType) "projectType" else "feature") { + put("name", metadata.featureName) + put("ecosystemPluginId", metadata.ecosystemPluginId) + } + } + + is ConfigureFromGetterOrigin -> Unit + is ContainerElementFactory -> Unit + is UnsafeSchemaItem -> Unit + is SchemaDocumentation -> Unit + } + } +} + +internal val DataTypeRef.typeName: String + get() = when (this) { + is DataTypeRef.Name -> this.fqName.qualifiedName + is DataTypeRef.NameWithArgs -> this.fqName.qualifiedName + "<${ + typeArguments.joinToString { arg -> + when (arg) { + is ConcreteTypeArgument -> { + arg.type.typeName + } + + is StarProjection -> "*" + } + } + }>" + + is DataTypeRef.Type -> when (val t = dataType) { + is DataType.BooleanDataType -> "Boolean" + is DataType.IntDataType -> "Int" + is DataType.LongDataType -> "Long" + is DataType.StringDataType -> "String" + is DataType.NullType -> "Nothing?" + is DataType.TypeVariableUsage -> "TypeVariable#${t.variableId}" + is DataType.UnitType -> "Unit" + } + } + +internal fun isFeatureUsageFunction(function: SchemaMemberFunction): Boolean = + function.metadata.any { it is ProjectFeatureOrigin } diff --git a/agentic-tool/src/main/kotlin/org/gradle/client/agentic/EffectiveProjectDefinitionCommand.kt b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/EffectiveProjectDefinitionCommand.kt new file mode 100644 index 0000000..34ac7f7 --- /dev/null +++ b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/EffectiveProjectDefinitionCommand.kt @@ -0,0 +1,62 @@ +package org.gradle.client.agentic + +import com.github.ajalt.clikt.core.Context +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.boolean +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import org.gradle.client.build.model.ResolvedDomPrerequisites +import org.gradle.internal.declarativedsl.evaluator.main.SimpleAnalysisEvaluator +import java.io.File + +class EffectiveProjectDefinitionCommand : DclCommand("effective-project-definition") { + override fun help(context: Context): String = + "Prints the effective definition of a specified subproject. The effective definition is produced by applying " + + "the project's definition as written in the build file on top of the defaults for the project type. " + + "If a subproject path is not specified, prints the effective definition of the root project. " + + "By default, omits schema properties that have no values and nested objects that are not used." + + val subprojectPath: String by option( + help = "Gradle project path such as :foo:bar or filesystem path such as foo/bar" + ).default(":") + + val withUnusedMembers by option( + help = "Include members that are not present in the effective definition" + ).boolean().default(false) + + @Suppress("ReturnCount", "CyclomaticComplexMethod") + override fun produceOutput( + prerequisites: ResolvedDomPrerequisites, + analyzer: SimpleAnalysisEvaluator + ): JsonElement { + val projectInfo = matchingProjectInfo(prerequisites, subprojectPath) + ?: return buildJsonObject { put("error", "Project not found") } + + val fullDom = getDomForProject(analyzer, prerequisites, projectInfo) + ?: return buildJsonObject { put("error", "Could not analyze the definition") } + + return buildJsonObject { + put("gradlePath", projectInfo.projectPath) + put("settingsFile", prerequisites.settingsFile.path) + put("buildFile", projectInfo.projectBuildFile.path) + put("content", buildJsonArray { + with(DomJsonRenderer(withUnusedMembers, fullDom, documentationProvider)) { + fullDom.document.content.forEach { visitNode(it) } + } + }) + } + } +} + +internal fun matchingProjectInfo( + prerequisites: ResolvedDomPrerequisites, + projectPath: String +): ResolvedDomPrerequisites.ProjectInfo? = + prerequisites.projectInfos.find { + it.projectPath == projectPath + } ?: prerequisites.projectInfos.find { + it.projectDirectory.relativeToOrNull(File(".").absoluteFile)?.path?.equals(projectPath) == true + } ?: prerequisites.projectInfos.find { it.projectDirectory.absolutePath == projectPath } diff --git a/agentic-tool/src/main/kotlin/org/gradle/client/agentic/ProjectSchemaCommand.kt b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/ProjectSchemaCommand.kt new file mode 100644 index 0000000..0ce8fa5 --- /dev/null +++ b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/ProjectSchemaCommand.kt @@ -0,0 +1,257 @@ +package org.gradle.client.agentic + +import com.github.ajalt.clikt.core.Context +import com.github.ajalt.clikt.parameters.options.option +import kotlinx.serialization.json.* +import org.gradle.client.build.model.ResolvedDomPrerequisites +import org.gradle.declarative.dsl.schema.* +import org.gradle.declarative.dsl.schema.DataType.ClassDataType +import org.gradle.declarative.dsl.schema.DataType.ParameterizedTypeInstance.TypeArgument.ConcreteTypeArgument +import org.gradle.declarative.dsl.schema.FunctionSemantics.ConfigureSemantics +import org.gradle.internal.declarativedsl.analysis.SchemaTypeRefContext +import org.gradle.internal.declarativedsl.analysis.isReadOnly +import org.gradle.internal.declarativedsl.evaluator.main.SimpleAnalysisEvaluator + +class ProjectSchemaCommand : DclCommand("project-schema") { + override fun help(context: Context): String = + "Prints the schema for the project types and features available in the build. " + + "If project features are specified, only their schema will be returned. " + + "If a subproject path is specified, only the schema for that subproject's " + + "project type (and its features) will be returned." + + val projectFeatures by option( + help = "Comma-separated project type or feature names for which to query settings defaults; " + + "e.g. javaLibrary,javaApplication,spotless" + + "If not specified, returns the schema for all project types and features" + ) + + val subprojectPath by option( + help = "Gradle project path such as :foo:bar or filesystem path such as foo/bar" + ) + + @Suppress("CyclomaticComplexMethod", "ReturnCount") + override fun produceOutput( + prerequisites: ResolvedDomPrerequisites, + analyzer: SimpleAnalysisEvaluator + ): JsonElement { + if (projectFeatures != null && subprojectPath != null) { + return buildJsonObject { put("error", "Subproject path and project features cannot be specified together") } + } + + val schema = + prerequisites.projectInterpretationSequence.steps.last().evaluationSchemaForStep.analysisSchema + + val projectFeaturePredicate: ((ProjectFeatureOrigin) -> Boolean)? = when { + projectFeatures != null -> { + val featureNames = splitCommaSeparatedFeatureNames(projectFeatures); + { origin -> featureNames?.contains(origin.featureName) ?: true } + } + + subprojectPath != null -> { + val projectInfo = + matchingProjectInfo(prerequisites, subprojectPath!!) + ?: return buildJsonObject { put("error", "Project not found for path: $subprojectPath") } + + val dom = getDomForProject(analyzer, prerequisites, projectInfo) + ?: return buildJsonObject { put("error", "Failed to analyze project at path: $subprojectPath") } + + val used = collectProjectTypeAndFeatures(dom).run { projectFeatures + listOfNotNull(projectType) }; + { feature -> used.any(feature::matchesFeature) } + } + + else -> null + } + + val initialTypes = if (projectFeaturePredicate != null) + matchingInitialTypes(schema, projectFeaturePredicate) + else listOf(schema.topLevelReceiverType) + + val allUsedTypes = collectTypesToInclude(schema, initialTypes) + + return buildJsonObject { + if (projectFeaturePredicate == null) { + put("topLevelTypeName", schema.topLevelReceiverType.name.qualifiedName) + } + + put("types", buildJsonArray { + allUsedTypes.forEach { type -> + add(typeInfo(type)) + } + }) + } + } + + private fun matchingInitialTypes( + schema: AnalysisSchema, + projectFeaturePredicate: (ProjectFeatureOrigin) -> Boolean + ): List = schema.dataClassTypesByFqName.values.filterIsInstance().filter { type -> + type.metadata.any { metadata -> + metadata is ProjectFeatureOrigin && projectFeaturePredicate(metadata) + } + } + + private fun typeInfo(type: ClassDataType): JsonObject = buildJsonObject { + if (type is EnumClass) { + put("name", type.name.qualifiedName) + putJsonArray("enumEntries") { + type.entryNames.forEach { add(it) } + } + } + if (type is DataClass) { + put("name", type.name.qualifiedName) + val supertypes = type.supertypes.filter { it.qualifiedName != Any::class.qualifiedName } + if (supertypes.isNotEmpty()) { + putJsonArray("supertypes") { + type.supertypes.forEach { add(it.qualifiedName) } + } + } + if (type.properties.isNotEmpty()) { + put("properties", buildJsonArray { + type.properties.forEach { add(propertyInfo(it)) } + }) + } + + elementMetadata(type.metadata) + + val (features, nestedObjectFunctions) = type.memberFunctions + .filter { it.semantics is ConfigureSemantics } + .partition { it.metadata.filterIsInstance().any() } + + if (features.isNotEmpty()) { + put("features", buildJsonArray { + features.forEach { add(nestedObjectInfo(it)) } + }) + } + if (nestedObjectFunctions.isNotEmpty()) { + put("nestedObjects", buildJsonArray { + nestedObjectFunctions.forEach { add(nestedObjectInfo(it)) } + }) + } + val valueFactories = type.memberFunctions.filter { it.semantics is FunctionSemantics.Pure } + if (valueFactories.isNotEmpty()) { + put("valueFactories", buildJsonArray { + valueFactories.forEach { add(valueFactoryInfo(it)) } + }) + } + } + + documentationProvider.classDocumentation(type)?.let { docs -> + put("documentation", docs) + } + } + + private fun propertyInfo(property: DataProperty): JsonObject = buildJsonObject { + put("name", property.name) + put("type", property.valueType.typeName) + if (property.isReadOnly) { + put("isReadOnly", true) + } + documentationProvider.propertyDocumentation(property)?.let { docs -> + put("documentation", docs) + } + } + + + private fun nestedObjectInfo(function: SchemaMemberFunction): JsonObject = buildJsonObject { + put("name", function.simpleName) + elementMetadata(function) + putJsonArray("parameters") { + function.parameters.forEach { parameter -> + addJsonObject { + put("name", parameter.name) + put("type", parameter.type.typeName) + when (parameter.semantics) { + is ParameterSemantics.IdentityKey -> put("semantics", "identity-key") + else -> Unit + } + } + } + } + val semantics = function.semantics as ConfigureSemantics + put("type", (function.semantics as ConfigureSemantics).configuredType.typeName) + when (semantics) { + is FunctionSemantics.AccessAndConfigure -> { + put( + "semantics", + when { + isFeatureUsageFunction(function) -> "feature-application" + function.parameters.any { it.semantics is ParameterSemantics.IdentityKey } -> + "creates-or-configures-object-by-id" + else -> "pre-existing-nested-object" + } + ) + } + + is FunctionSemantics.AddAndConfigure -> { + put("semantics", "adds-new-object") + put("requiresConfiguringBlock", semantics.configureBlockRequirement.requires) + } + } + documentationProvider.functionDocumentation(function)?.let { docs -> + put("documentation", docs) + } + } + + private fun collectTypesToInclude( + schema: AnalysisSchema, + initial: Iterable + ): Set = with(SchemaTypeRefContext(schema)) { + + buildSet { + fun visitType(type: DataClass) { + fun maybeVisitRef(ref: DataTypeRef) { + when (val type = resolveRef(ref)) { + is DataClass -> { + visitType(type) + } + + is DataType.ParameterizedTypeInstance -> { + type.typeArguments.forEach { if (it is ConcreteTypeArgument) maybeVisitRef(it.type) } + } + + is EnumClass -> add(type) + is DataType.PrimitiveType -> Unit + } + } + + if (add(type)) { + type.properties.forEach { + maybeVisitRef(it.valueType) + } + type.memberFunctions.forEach { function -> + function.parameters.forEach { maybeVisitRef(it.type) } + val semantics = function.semantics + maybeVisitRef(semantics.returnValueType) + if (semantics is ConfigureSemantics) { + maybeVisitRef(semantics.configuredType) + } + } + } + } + + initial.forEach(::visitType) + } + } + + private fun valueFactoryInfo(function: SchemaMemberFunction): JsonObject = buildJsonObject { + put("name", function.simpleName) + put("returnType", (function.semantics as FunctionSemantics.Pure).returnValueType.typeName) + putJsonArray("parameters") { + function.parameters.forEach { parameter -> + addJsonObject { + put("name", parameter.name) + put("type", parameter.type.typeName) + } + } + } + documentationProvider.functionDocumentation(function)?.let { docs -> + put("documentation", docs) + } + } +} + +private fun ProjectFeatureOrigin.matchesFeature( + feature: ProjectFeatureOrigin +): Boolean = featureName == feature.featureName && + targetDefinitionClassName == feature.targetDefinitionClassName && + targetBuildModelClassName == feature.targetBuildModelClassName diff --git a/agentic-tool/src/main/kotlin/org/gradle/client/agentic/ProjectTypeAndFeaturesInfo.kt b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/ProjectTypeAndFeaturesInfo.kt new file mode 100644 index 0000000..c7e3d40 --- /dev/null +++ b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/ProjectTypeAndFeaturesInfo.kt @@ -0,0 +1,53 @@ +package org.gradle.client.agentic + +import org.gradle.declarative.dsl.schema.ProjectFeatureOrigin +import org.gradle.internal.declarativedsl.dom.DeclarativeDocument +import org.gradle.internal.declarativedsl.dom.DocumentResolution +import org.gradle.internal.declarativedsl.dom.operations.overlay.DocumentOverlayResult + +internal data class ProjectTypeAndFeaturesInfo( + val projectType: ProjectFeatureOrigin?, + val projectFeatures: List, +) { + val projectTypeName = projectType?.featureName + val projectFeatureNames = projectFeatures.map { it.featureName }.toSet() +} + +@Suppress("NestedBlockDepth") +internal fun collectProjectTypeAndFeatures( + fullDom: DocumentOverlayResult, +): ProjectTypeAndFeaturesInfo { + var projectType: ProjectFeatureOrigin? = null + val projectFeatures = mutableSetOf() + + fun visitNode(node: DeclarativeDocument.DocumentNode) { + when (node) { + is DeclarativeDocument.DocumentNode.ElementNode -> { + when (val resolution = fullDom.overlayResolutionContainer.data(node)) { + is DocumentResolution.ElementResolution.ElementNotResolved -> Unit + is DocumentResolution.ElementResolution.SuccessfulElementResolution -> { + val function = resolution.elementFactoryFunction + function.metadata.filterIsInstance().forEach { metadata -> + val isProjectType = + metadata.targetDefinitionClassName == "org.gradle.api.Project" + + if (isProjectType) { + projectType = metadata + } else { + projectFeatures += metadata + } + } + } + } + node.content.forEach(::visitNode) + } + + is DeclarativeDocument.DocumentNode.ErrorNode -> Unit + is DeclarativeDocument.DocumentNode.PropertyNode -> Unit + } + } + + fullDom.document.content.forEach(::visitNode) + + return ProjectTypeAndFeaturesInfo(projectType, projectFeatures.toList()) +} diff --git a/agentic-tool/src/main/kotlin/org/gradle/client/agentic/ProjectTypesAndFeaturesCommand.kt b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/ProjectTypesAndFeaturesCommand.kt new file mode 100644 index 0000000..aaf26a0 --- /dev/null +++ b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/ProjectTypesAndFeaturesCommand.kt @@ -0,0 +1,77 @@ +package org.gradle.client.agentic + +import com.github.ajalt.clikt.core.Context +import kotlinx.serialization.json.* +import org.gradle.client.build.model.ResolvedDomPrerequisites +import org.gradle.declarative.dsl.schema.DataClass +import org.gradle.declarative.dsl.schema.ProjectFeatureOrigin +import org.gradle.internal.declarativedsl.evaluator.main.SimpleAnalysisEvaluator + +class ProjectTypesAndFeaturesCommand : DclCommand("project-types-and-features") { + + override fun help(context: Context): String = + "Lists project types and features available in this build. For each project type and feature," + + "provides the ID of the ecosystem plugin that contributes the project type or feature. " + + "For project features, specifies which definition or build model type the feature can be applied to." + + override fun produceOutput( + prerequisites: ResolvedDomPrerequisites, + analyzer: SimpleAnalysisEvaluator + ): JsonElement { + return buildJsonObject { + val schema = prerequisites.projectInterpretationSequence.steps.last().evaluationSchemaForStep.analysisSchema + + val projectTypes = schema.topLevelReceiverType.memberFunctions.flatMap { fn -> + fn.metadata.filterIsInstance().map { fn to it } + } + + putJsonArray("projectTypes") { + projectTypes.forEach { (function, projectType) -> + addJsonObject { + put("name", projectType.featureName) + put("ecosystemPluginId", projectType.ecosystemPluginId) + + documentationProvider.functionDocumentation(function)?.let { + put("documentation", it) + } + } + } + } + + val features = schema.dataClassTypesByFqName.values.filterIsInstance() + .flatMap { + it.memberFunctions.flatMap { fn -> + fn.metadata.filterIsInstance() + .filter { it.targetDefinitionClassName != "org.gradle.api.Project" } + .map { fn to it } + } + }.distinctBy { (_, it) -> + listOf( + it.featureName, + it.ecosystemPluginId, + it.targetDefinitionClassName, + it.targetBuildModelClassName + ) + } + + putJsonArray("projectFeatures") { + features.forEach { (function, feature) -> + addJsonObject { + put("name", feature.featureName) + put("ecosystemPluginId", feature.ecosystemPluginId) + + if (feature.targetDefinitionClassName != null) { + put("targetDefinitionTypeName", feature.targetDefinitionClassName) + } + if (feature.targetBuildModelClassName != null) { + put("targetBuildModelTypeName", feature.targetBuildModelClassName) + } + + documentationProvider.functionDocumentation(function) + ?.let { put("documentation", it) } + } + } + } + } + } +} diff --git a/agentic-tool/src/main/kotlin/org/gradle/client/agentic/ProjectsCommand.kt b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/ProjectsCommand.kt new file mode 100644 index 0000000..acb7cb3 --- /dev/null +++ b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/ProjectsCommand.kt @@ -0,0 +1,54 @@ +package org.gradle.client.agentic + +import com.github.ajalt.clikt.core.Context +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.add +import kotlinx.serialization.json.addJsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonArray +import org.gradle.client.build.model.ResolvedDomPrerequisites +import org.gradle.internal.declarativedsl.evaluator.main.SimpleAnalysisEvaluator + +class ProjectsCommand : DclCommand("projects") { + + override fun help(context: Context): String = + "Lists projects in the Gradle build. For each project, provides the " + + "project path, project directory, project build file path. " + + "If a project is Declarative, detects its project type and all project features used in it." + + override fun produceOutput( + prerequisites: ResolvedDomPrerequisites, + analyzer: SimpleAnalysisEvaluator + ): JsonElement { + return buildJsonObject { + put("settingsFile", prerequisites.settingsFile.path) + + putJsonArray("projects") { + prerequisites.projectInfos.forEach { projectInfo -> + addJsonObject project@{ + put("gradlePath", projectInfo.projectPath) + put("projectDir", projectInfo.projectDirectory.path) + put("buildFile", projectInfo.projectBuildFile.path) + + if (!projectInfo.isDeclarative) { + put("buildFileIsDeclarative", false) + return@project + } + + val fullDom = getDomForProject(analyzer, prerequisites, projectInfo) + ?: return@project + + val projectTypeAndFeatures = collectProjectTypeAndFeatures(fullDom) + put("projectType", projectTypeAndFeatures.projectTypeName) + putJsonArray("features") { + projectTypeAndFeatures.projectFeatureNames.forEach { feature -> + add(feature) + } + } + } + } + } + } + } +} diff --git a/agentic-tool/src/main/kotlin/org/gradle/client/agentic/SettingsDefaultsCommand.kt b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/SettingsDefaultsCommand.kt new file mode 100644 index 0000000..4a4a934 --- /dev/null +++ b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/SettingsDefaultsCommand.kt @@ -0,0 +1,75 @@ +package org.gradle.client.agentic + +import com.github.ajalt.clikt.core.Context +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.boolean +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import org.gradle.client.build.model.ResolvedDomPrerequisites +import org.gradle.internal.declarativedsl.dom.DeclarativeDocument.DocumentNode.ElementNode +import org.gradle.internal.declarativedsl.dom.DocumentResolution.ElementResolution.SuccessfulElementResolution +import org.gradle.internal.declarativedsl.evaluator.main.SimpleAnalysisEvaluator +import settingsWithNoOverlayOrigin + +class SettingsDefaultsCommand : DclCommand("settings-defaults") { + + override fun help(context: Context): String = + "Lists settings defaults for project types. " + + "If a project type list is specified, prints the defaults only for them. " + + "By default, omits schema properties that have no values and nested objects that are not used." + + val projectTypes by option( + help = "Comma-separated project type names for which to query settings defaults; " + + "e.g. javaLibrary,javaApplication." + + "If not specified, returns defaults for all project types" + ) + + val withUnusedMembers by option( + help = "Include members that are not present in the defaults" + ).boolean().default(false) + + override fun produceOutput( + prerequisites: ResolvedDomPrerequisites, + analyzer: SimpleAnalysisEvaluator + ): JsonElement { + val settingsScript = prerequisites.settingsFile.takeIf { it.canRead() }?.readText() ?: "" + + val projectTypeNames = splitCommaSeparatedFeatureNames(projectTypes) + + val settingsResult = + analyzer.evaluate(prerequisites.settingsFile.path, settingsScript) + val dom = settingsWithNoOverlayOrigin(settingsResult) + ?: return buildJsonObject { put("error", "Model evaluation failed") } + + return buildJsonObject { + put("settingsFile", prerequisites.settingsFile.path) + put("content", buildJsonArray { + with(DomJsonRenderer(withUnusedMembers, dom, documentationProvider)) { + val defaultsBlocks = + dom.document.content.filterIsInstance() + .filter { + it.name == "defaults" && + dom.overlayResolutionContainer.data(it) is SuccessfulElementResolution + } + + defaultsBlocks.forEach { defaultsBlock -> + defaultsBlock.content.forEach { node -> + if (node is ElementNode && (projectTypeNames == null || node.name in projectTypeNames)) { + visitNode(node) + } + } + } + } + }) + } + } +} + +internal fun splitCommaSeparatedFeatureNames(projectTypes: String?): Set? = projectTypes + ?.split(",") + ?.map { it.trim() } + ?.filter { it.isNotBlank() } + ?.toSet() diff --git a/agentic-tool/src/main/kotlin/org/gradle/client/agentic/SettingsDefinitionCommand.kt b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/SettingsDefinitionCommand.kt new file mode 100644 index 0000000..a9679bc --- /dev/null +++ b/agentic-tool/src/main/kotlin/org/gradle/client/agentic/SettingsDefinitionCommand.kt @@ -0,0 +1,45 @@ +package org.gradle.client.agentic + +import com.github.ajalt.clikt.core.Context +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.boolean +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import org.gradle.client.build.model.ResolvedDomPrerequisites +import org.gradle.internal.declarativedsl.evaluator.main.SimpleAnalysisEvaluator +import settingsWithNoOverlayOrigin + +class SettingsDefinitionCommand : DclCommand("settings-definition") { + + override fun help(context: Context): String = + "Prints the effective settings definition (including defaults declared for project types). " + + "By default, omits schema properties that have no values and nested objects that are not used." + + val withUnusedMembers by option( + help = "Include members that are not present in the definition or defaults" + ).boolean().default(false) + + override fun produceOutput( + prerequisites: ResolvedDomPrerequisites, + analyzer: SimpleAnalysisEvaluator + ): JsonElement { + val settingsScript = prerequisites.settingsFile.takeIf { it.canRead() }?.readText() ?: "" + + val settingsResult = + analyzer.evaluate(prerequisites.settingsFile.path, settingsScript) + val dom = settingsWithNoOverlayOrigin(settingsResult) + ?: return buildJsonObject { put("error", "Model evaluation failed") } + + return buildJsonObject { + put("settingsFile", prerequisites.settingsFile.path) + put("content", buildJsonArray { + with(DomJsonRenderer(withUnusedMembers, dom, documentationProvider)) { + dom.document.content.forEach { visitNode(it) } + } + }) + } + } +} diff --git a/build-action/build.gradle.dcl b/build-action/build.gradle.dcl index b985433..967c52e 100644 --- a/build-action/build.gradle.dcl +++ b/build-action/build.gradle.dcl @@ -1,8 +1,6 @@ javaLibrary { - javaVersion = 8 - dependencies { - implementation("org.gradle:gradle-tooling-api:9.5.0-milestone-1") - implementation("org.gradle:gradle-declarative-dsl-tooling-models:9.5.0-milestone-1") + implementation("org.gradle:gradle-tooling-api:9.6.0-20260511195019+0000") + implementation("org.gradle:gradle-declarative-dsl-tooling-models:9.6.0-20260511195019+0000") } } diff --git a/build-action/src/main/java/org/gradle/client/build/action/GetResolvedDomAction.java b/build-action/src/main/java/org/gradle/client/build/action/GetResolvedDomAction.java index 6f10bb5..4453ddd 100644 --- a/build-action/src/main/java/org/gradle/client/build/action/GetResolvedDomAction.java +++ b/build-action/src/main/java/org/gradle/client/build/action/GetResolvedDomAction.java @@ -10,6 +10,7 @@ import org.gradle.tooling.model.gradle.GradleBuild; import java.io.File; +import java.io.Serializable; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -22,43 +23,86 @@ public ResolvedDomPrerequisites execute(BuildController controller) { DeclarativeSchemaModel declarativeSchemaModel = controller.getModel(DeclarativeSchemaModel.class); InterpretationSequence settingsSchema = declarativeSchemaModel.getSettingsSequence(); InterpretationSequence projectSchema = declarativeSchemaModel.getProjectSequence(); - Pair> buildFiles = getDeclarativeBuildFiles(controller); + Pair> buildFiles = getProjects(controller); return new ResolvedDomPrerequisitesImpl(settingsSchema, projectSchema, buildFiles.getLeft(), buildFiles.getRight()); } - private static Pair> getDeclarativeBuildFiles(BuildController controller) { + private static Pair> getProjects(BuildController controller) { GradleBuild gradleBuild = controller.getModel(GradleBuild.class); File rootProjectDirectory = gradleBuild.getRootProject().getProjectDirectory(); - List declarativeBuildFiles = gradleBuild - .getProjects() - .getAll() - .stream() - .map(p -> new File(p.getProjectDirectory(), "build.gradle.dcl")) - .filter(File::exists).collect(Collectors.toList()); + List declarativeBuildFiles = gradleBuild + .getProjects() + .getAll() + .stream() + .map(p -> new ProjectInfoImpl(p.getPath(), getBuildFileInProjectDirectory(p.getProjectDirectory()))) + .collect(Collectors.toList()); if (declarativeBuildFiles.isEmpty()) { throw new RuntimeException("No declarative project file found"); } return Pair.of(rootProjectDirectory, declarativeBuildFiles); } + private static final class ProjectInfoImpl implements ResolvedDomPrerequisites.ProjectInfo, Serializable { + private final String projectPath; + private final File projectBuildFile; + + @Override + public String getProjectPath() { + return projectPath; + } + + @Override + public File getProjectBuildFile() { + return projectBuildFile; + } + + @Override + public File getProjectDirectory() { + return projectBuildFile.getParentFile(); + } + + @Override + public boolean isDeclarative() { + return projectBuildFile.getName().endsWith(".dcl") && projectBuildFile.canRead(); + } + + public ProjectInfoImpl(String projectPath, File projectBuildFile) { + this.projectPath = projectPath; + this.projectBuildFile = projectBuildFile; + } + } + + private static File getBuildFileInProjectDirectory(File projectDir) { + File dcl = new File(projectDir, "build.gradle.dcl"); + if (dcl.exists()) + return dcl; + File kts = new File(projectDir, "build.gradle.kts"); + if (kts.exists()) + return kts; + File groovy = new File(projectDir, "build.gradle"); + if (groovy.exists()) + return groovy; + + return dcl; + } private static final class ResolvedDomPrerequisitesImpl implements ResolvedDomPrerequisites { private final InterpretationSequence settingsSequence; private final InterpretationSequence projectSequence; private final File rootDir; - private final List declarativeBuildFiles; + private final List projectInfos; public ResolvedDomPrerequisitesImpl( InterpretationSequence settingsSequence, InterpretationSequence projectSequence, File rootDir, - List declarativeBuildFiles + List projectInfos ) { this.settingsSequence = settingsSequence; this.projectSequence = projectSequence; this.rootDir = rootDir; - this.declarativeBuildFiles = declarativeBuildFiles; + this.projectInfos = projectInfos; } @Override @@ -94,9 +138,16 @@ public File getSettingsFile() { @Override public List getDeclarativeFiles() { return Stream.concat( - declarativeBuildFiles.stream(), + projectInfos.stream() + .filter(f -> f.isDeclarative() && f.getProjectBuildFile().exists()) + .map(ProjectInfo::getProjectBuildFile), getSettingsFile().canRead() ? Stream.of(getSettingsFile()) : Stream.empty() ).collect(Collectors.toList()); } + + @Override + public List getProjectInfos() { + return projectInfos; + } } -} \ No newline at end of file +} diff --git a/build-action/src/main/java/org/gradle/client/build/model/ResolvedDomPrerequisites.java b/build-action/src/main/java/org/gradle/client/build/model/ResolvedDomPrerequisites.java index 2e9980c..1aff391 100644 --- a/build-action/src/main/java/org/gradle/client/build/model/ResolvedDomPrerequisites.java +++ b/build-action/src/main/java/org/gradle/client/build/model/ResolvedDomPrerequisites.java @@ -8,16 +8,25 @@ import java.util.List; public interface ResolvedDomPrerequisites extends Serializable { - + InterpretationSequence getSettingsInterpretationSequence(); - + InterpretationSequence getProjectInterpretationSequence(); AnalysisSchema getAnalysisSchema(); File getRootDir(); - + File getSettingsFile(); List getDeclarativeFiles(); + + List getProjectInfos(); + + interface ProjectInfo { + String getProjectPath(); + File getProjectBuildFile(); + File getProjectDirectory(); + boolean isDeclarative(); + } } diff --git a/build-logic/plugins/build.gradle.dcl b/build-logic/plugins/build.gradle.dcl index 1172e5f..77d649a 100644 --- a/build-logic/plugins/build.gradle.dcl +++ b/build-logic/plugins/build.gradle.dcl @@ -2,7 +2,7 @@ javaGradlePlugin { description = "Declarative plugins containing custom project features for the gradle-client project." dependencies { - api("org.gradle.experimental.kmp-ecosystem:org.gradle.experimental.kmp-ecosystem.gradle.plugin:0.1.54") + api("org.gradle.experimental.kmp-ecosystem:org.gradle.experimental.kmp-ecosystem.gradle.plugin:0.1.61") api("org.jetbrains.kotlin.multiplatform:org.jetbrains.kotlin.multiplatform.gradle.plugin:2.3.0") api("org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin:2.3.0") api("org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:2.3.0") diff --git a/build-logic/plugins/src/main/java/org/gradle/client/projectfeatures/compose/ComposeProjectFeaturePlugin.java b/build-logic/plugins/src/main/java/org/gradle/client/projectfeatures/compose/ComposeProjectFeaturePlugin.java index e9dcf12..f8dc773 100644 --- a/build-logic/plugins/src/main/java/org/gradle/client/projectfeatures/compose/ComposeProjectFeaturePlugin.java +++ b/build-logic/plugins/src/main/java/org/gradle/client/projectfeatures/compose/ComposeProjectFeaturePlugin.java @@ -4,9 +4,9 @@ import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.experimental.kmp.KotlinMultiplatformBuildModel; +import org.gradle.api.model.ObjectFactory; import org.gradle.features.annotations.BindsProjectFeature; -import org.gradle.features.binding.ProjectFeatureBindingBuilder; -import org.gradle.features.binding.ProjectFeatureBinding; +import org.gradle.features.binding.*; import org.gradle.api.plugins.PluginManager; import org.gradle.api.provider.ProviderFactory; import org.jetbrains.compose.ComposeExtension; @@ -28,48 +28,68 @@ public void apply(Project project) { } static class Binding implements ProjectFeatureBinding { + @Override public void bind(ProjectFeatureBindingBuilder builder) { - builder.bindProjectFeatureToBuildModel("compose", Compose.class, KotlinMultiplatformBuildModel.class, - (context, definition, buildModel, parent) -> { - Services services = context.getObjectFactory().newInstance(Services.class); - services.getPluginManager().apply("org.jetbrains.kotlin.plugin.serialization"); - services.getPluginManager().apply("org.jetbrains.kotlin.plugin.compose"); - - buildModel.getPackageVersion().set( - definition.getNativeDistributions().getPackageVersion() - .orElse(services.getProviders().provider(() -> services.getProject().getVersion().toString())) - ); - - // Many of the underlying extension elements are not provider-aware, so we have to do - // this in an afterEvaluate block - services.getProject().afterEvaluate(p -> { - services.getPluginManager().apply("org.jetbrains.compose"); - ((DefaultComposeBuildModel)buildModel).setComposeExtension(services.getProject().getExtensions().getByType(ComposeExtension.class)); - DesktopExtension desktop = buildModel.getComposeExtension().getExtensions().getByType(DesktopExtension.class); - KotlinMultiplatformExtension kmpExtension = context.getBuildModel(parent).getKotlinMultiplatformExtension(); - JvmApplication application = desktop.getApplication(); - JvmApplicationDistributions nativeDistributions = application.getNativeDistributions(); - - wireNativeDistribution(definition.getNativeDistributions(), nativeDistributions, buildModel.getPackageVersion().get()); - application.setMainClass(definition.getMainClass().get()); - definition.getJvmArgs().forEach(jvmArg -> application.getJvmArgs().add(jvmArg.getName() + jvmArg.getValue().get())); - - wireBuildTypes(definition, application.getBuildTypes()); - - // Need to set the main class for the JVM run task in the KMP model from the one nested in the Compose model - kmpExtension.jvm("jvm" , jvm -> { - jvm.mainRun(kotlinJvmRunDsl -> { - kotlinJvmRunDsl.getMainClass().set(definition.getMainClass()); - return Unit.INSTANCE; - }); - }); - }); - }) + builder.bindProjectFeatureToBuildModel( + "compose", Compose.class, KotlinMultiplatformBuildModel.class, ApplyAction.class + ) .withUnsafeApplyAction() .withBuildModelImplementationType(DefaultComposeBuildModel.class); } + public static abstract class ApplyAction implements ProjectFeatureApplyAction< + Compose, + ComposeBuildModel, + Definition + > { + + @Inject + public ApplyAction() { + } + + @Inject + public abstract ObjectFactory getObjectFactory(); + + @Override + public void apply(ProjectFeatureApplicationContext context, Compose definition, ComposeBuildModel buildModel, Definition parent) { + Binding.Services services = getObjectFactory().newInstance(Binding.Services.class); + services.getPluginManager().apply("org.jetbrains.kotlin.plugin.serialization"); + services.getPluginManager().apply("org.jetbrains.kotlin.plugin.compose"); + + buildModel.getPackageVersion().set( + definition.getNativeDistributions().getPackageVersion() + .orElse(services.getProviders().provider(() -> services.getProject().getVersion().toString())) + ); + + // Many of the underlying extension elements are not provider-aware, so we have to do + // this in an afterEvaluate block + services.getProject().afterEvaluate(p -> { + services.getPluginManager().apply("org.jetbrains.compose"); + ((DefaultComposeBuildModel) buildModel).setComposeExtension(services.getProject().getExtensions().getByType(ComposeExtension.class)); + DesktopExtension desktop = buildModel.getComposeExtension().getExtensions().getByType(DesktopExtension.class); + KotlinMultiplatformExtension kmpExtension = context.getBuildModel(parent).getKotlinMultiplatformExtension(); + JvmApplication application = desktop.getApplication(); + JvmApplicationDistributions nativeDistributions = application.getNativeDistributions(); + + wireNativeDistribution(definition.getNativeDistributions(), nativeDistributions, buildModel.getPackageVersion().get()); + application.setMainClass(definition.getMainClass().get()); + definition.getJvmArgs().forEach(jvmArg -> application.getJvmArgs().add(jvmArg.getName() + jvmArg.getValue().get())); + + wireBuildTypes(definition, application.getBuildTypes()); + + // Need to set the main class for the JVM run task in the KMP model from the one nested in the Compose model + kmpExtension.jvm("jvm", jvm -> { + jvm.mainRun(kotlinJvmRunDsl -> { + kotlinJvmRunDsl.getMainClass().set(definition.getMainClass()); + return Unit.INSTANCE; + }); + }); + }); + } + + } + private static void wireBuildTypes(Compose composeBuildModel, JvmApplicationBuildTypes buildTypes) { wireBuildType(composeBuildModel.getBuildTypes().getRelease(), buildTypes.getRelease()); } diff --git a/build-logic/plugins/src/main/java/org/gradle/client/projectfeatures/detekt/DetektProjectFeaturePlugin.java b/build-logic/plugins/src/main/java/org/gradle/client/projectfeatures/detekt/DetektProjectFeaturePlugin.java index bdaf1de..932ebac 100644 --- a/build-logic/plugins/src/main/java/org/gradle/client/projectfeatures/detekt/DetektProjectFeaturePlugin.java +++ b/build-logic/plugins/src/main/java/org/gradle/client/projectfeatures/detekt/DetektProjectFeaturePlugin.java @@ -4,9 +4,9 @@ import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.experimental.kmp.KotlinMultiplatformBuildModel; +import org.gradle.api.model.ObjectFactory; import org.gradle.features.annotations.BindsProjectFeature; -import org.gradle.features.binding.ProjectFeatureBindingBuilder; -import org.gradle.features.binding.ProjectFeatureBinding; +import org.gradle.features.binding.*; import org.gradle.api.plugins.PluginManager; import javax.inject.Inject; @@ -22,24 +22,35 @@ public void apply(Project project) { static class Binding implements ProjectFeatureBinding { @Override public void bind(ProjectFeatureBindingBuilder builder) { - builder.bindProjectFeatureToBuildModel("detekt", Detekt.class, KotlinMultiplatformBuildModel.class, - (context, definition, buildModel, parent) -> { + builder.bindProjectFeatureToBuildModel("detekt", Detekt.class, KotlinMultiplatformBuildModel.class, ApplyAction.class) + .withUnsafeApplyAction() + .withBuildModelImplementationType(DefaultDetektBuildModel.class); + } + + static abstract class ApplyAction implements ProjectFeatureApplyAction> { + + @Inject + public ApplyAction() { + } - Services services = context.getObjectFactory().newInstance(Services.class); + @Inject + public abstract ObjectFactory getObjectFactory(); - services.getPluginManager().apply("io.gitlab.arturbosch.detekt"); - ((DefaultDetektBuildModel)buildModel).setDetektExtension(services.getProject().getExtensions().getByType(DetektExtension.class)); + @Override + public void apply(ProjectFeatureApplicationContext context, Detekt definition, DetektBuildModel buildModel, Definition kotlinMultiplatformBuildModelDefinition) { + Services services = getObjectFactory().newInstance(Services.class); - buildModel.getDetektExtension().getSource().from(definition.getSource()); - buildModel.getDetektExtension().getConfig().from(definition.getConfig()); + services.getPluginManager().apply("io.gitlab.arturbosch.detekt"); + ((DefaultDetektBuildModel)buildModel).setDetektExtension(services.getProject().getExtensions().getByType(DetektExtension.class)); - services.getProject().afterEvaluate(p -> { - // This is not a property, need to wire this in afterEvaluate - buildModel.getDetektExtension().setParallel(definition.getParallel().get()); - }); - }) - .withUnsafeApplyAction() - .withBuildModelImplementationType(DefaultDetektBuildModel.class); + buildModel.getDetektExtension().getSource().from(definition.getSource()); + buildModel.getDetektExtension().getConfig().from(definition.getConfig()); + + services.getProject().afterEvaluate(p -> { + // This is not a property, need to wire this in afterEvaluate + buildModel.getDetektExtension().setParallel(definition.getParallel().get()); + }); + } } interface Services { diff --git a/build-logic/plugins/src/main/java/org/gradle/client/projectfeatures/sqldelight/SqlDelightProjectFeaturePlugin.java b/build-logic/plugins/src/main/java/org/gradle/client/projectfeatures/sqldelight/SqlDelightProjectFeaturePlugin.java index 65d0e49..4fa3342 100644 --- a/build-logic/plugins/src/main/java/org/gradle/client/projectfeatures/sqldelight/SqlDelightProjectFeaturePlugin.java +++ b/build-logic/plugins/src/main/java/org/gradle/client/projectfeatures/sqldelight/SqlDelightProjectFeaturePlugin.java @@ -9,8 +9,7 @@ import org.gradle.api.artifacts.dsl.DependencyFactory; import org.gradle.api.experimental.kmp.KotlinMultiplatformBuildModel; import org.gradle.features.annotations.BindsProjectFeature; -import org.gradle.features.binding.ProjectFeatureBindingBuilder; -import org.gradle.features.binding.ProjectFeatureBinding; +import org.gradle.features.binding.*; import org.gradle.api.model.ObjectFactory; import org.gradle.api.plugins.PluginManager; import org.gradle.api.provider.Provider; @@ -39,47 +38,60 @@ public void apply(Project project) { } static class Binding implements ProjectFeatureBinding { - @Override - public void bind(ProjectFeatureBindingBuilder builder) { - builder.bindProjectFeatureToBuildModel("sqlDelight", SqlDelight.class, KotlinMultiplatformBuildModel.class, - (context, definition, buildModel, parent) -> { - Services services = context.getObjectFactory().newInstance(Services.class); - - buildModel.getVersion().set(definition.getVersion().orElse("2.1.0")); - - KotlinMultiplatformBuildModel parentBuildModel = context.getBuildModel(parent); - parentBuildModel.getKotlinMultiplatformExtension().jvm(jvmTarget -> { - NamedDomainObjectProvider sqlDelightConfiguration = services.getProject().getConfigurations().dependencyScope("sqlDelightTool", conf -> { - stream(SQLDELIGHT_DEPENDENCY_MODULES).forEach(module -> - conf.getDependencies().addLater(buildModel.getVersion().map(version -> services.getDependencyFactory().create(SQLDELIGHT_GROUP + ":" + module + ":" + version))) - ); - }); - - services.getProject().getConfigurations().named(jvmTarget.getCompilations().getByName("main").getDefaultSourceSet().getImplementationConfigurationName(), conf -> - conf.extendsFrom(sqlDelightConfiguration.get()) + + public static abstract class ApplyAction implements ProjectFeatureApplyAction> { + + @Inject + public ApplyAction() { + } + + @Inject + public abstract ObjectFactory getObjectFactory(); + + @Override + public void apply(ProjectFeatureApplicationContext context, SqlDelight definition, SqlDelightBuildModel buildModel, Definition parent) { + Services services = getObjectFactory().newInstance(Services.class); + + buildModel.getVersion().set(definition.getVersion().orElse("2.1.0")); + + KotlinMultiplatformBuildModel parentBuildModel = context.getBuildModel(parent); + parentBuildModel.getKotlinMultiplatformExtension().jvm(jvmTarget -> { + NamedDomainObjectProvider sqlDelightConfiguration = services.getProject().getConfigurations().dependencyScope("sqlDelightTool", conf -> { + stream(SQLDELIGHT_DEPENDENCY_MODULES).forEach(module -> + conf.getDependencies().addLater(buildModel.getVersion().map(version -> services.getDependencyFactory().create(SQLDELIGHT_GROUP + ":" + module + ":" + version))) ); }); - services.getPluginManager().apply("app.cash.sqldelight"); - ((DefaultSqlDelightBuildModel) buildModel).setSqlDelightExtension(services.getProject().getExtensions().getByType(SqlDelightExtension.class)); + services.getProject().getConfigurations().named(jvmTarget.getCompilations().getByName("main").getDefaultSourceSet().getImplementationConfigurationName(), conf -> + conf.extendsFrom(sqlDelightConfiguration.get()) + ); + }); - // https://github.com/gradle/gradle/issues/28043 - Provider> featureDatabasesProvider = services.getProviders().provider(() -> Collections.unmodifiableSet(definition.getDatabases())); + services.getPluginManager().apply("app.cash.sqldelight"); + ((DefaultSqlDelightBuildModel) buildModel).setSqlDelightExtension(services.getProject().getExtensions().getByType(SqlDelightExtension.class)); - Provider> sqlDelightDatabases = featureDatabasesProvider.map(featureDatabases -> featureDatabases.stream().map(featureDatabase -> { - SqlDelightDatabase database = services.getObjects().newInstance(SqlDelightDatabase.class, featureDatabase.getName()); + // https://github.com/gradle/gradle/issues/28043 + Provider> featureDatabasesProvider = services.getProviders().provider(() -> Collections.unmodifiableSet(definition.getDatabases())); - database.getPackageName().set(featureDatabase.getPackageName()); - database.getVerifyDefinitions().set(featureDatabase.getVerifyDefinitions()); - database.getVerifyMigrations().set(featureDatabase.getVerifyMigrations()); - database.getDeriveSchemaFromMigrations().set(featureDatabase.getDeriveSchemaFromMigrations()); - database.getGenerateAsync().set(featureDatabase.getGenerateAsync()); + Provider> sqlDelightDatabases = featureDatabasesProvider.map(featureDatabases -> featureDatabases.stream().map(featureDatabase -> { + SqlDelightDatabase database = services.getObjects().newInstance(SqlDelightDatabase.class, featureDatabase.getName()); - return database; - }).collect(Collectors.toUnmodifiableSet())); + database.getPackageName().set(featureDatabase.getPackageName()); + database.getVerifyDefinitions().set(featureDatabase.getVerifyDefinitions()); + database.getVerifyMigrations().set(featureDatabase.getVerifyMigrations()); + database.getDeriveSchemaFromMigrations().set(featureDatabase.getDeriveSchemaFromMigrations()); + database.getGenerateAsync().set(featureDatabase.getGenerateAsync()); - buildModel.getSqlDelightExtension().getDatabases().addAllLater(sqlDelightDatabases); - }) + return database; + }).collect(Collectors.toUnmodifiableSet())); + + buildModel.getSqlDelightExtension().getDatabases().addAllLater(sqlDelightDatabases); + } + } + + @Override + public void bind(ProjectFeatureBindingBuilder builder) { + builder.bindProjectFeatureToBuildModel("sqlDelight", SqlDelight.class, KotlinMultiplatformBuildModel.class, ApplyAction.class) .withUnsafeApplyAction() .withBuildModelImplementationType(DefaultSqlDelightBuildModel.class); } diff --git a/build-logic/plugins/src/main/resources/META-INF/declarative-dsl/documentation.json b/build-logic/plugins/src/main/resources/META-INF/declarative-dsl/documentation.json new file mode 100644 index 0000000..42a731c --- /dev/null +++ b/build-logic/plugins/src/main/resources/META-INF/declarative-dsl/documentation.json @@ -0,0 +1,10 @@ +{ + "types": { + "org.gradle.client.projectfeatures.compose.Compose": { + "documentation": "The top-level Compose definition.", + "properties": { + "mainClass": "Main class to execute in the packaged Compose app." + } + } + } +} diff --git a/build-logic/settings.gradle.dcl b/build-logic/settings.gradle.dcl index ef9a084..2730faa 100644 --- a/build-logic/settings.gradle.dcl +++ b/build-logic/settings.gradle.dcl @@ -1,5 +1,5 @@ plugins { - id("org.gradle.experimental.plugin-ecosystem").version("0.1.54") + id("org.gradle.experimental.plugin-ecosystem").version("0.1.61") } dependencyResolutionManagement { diff --git a/dcl-utils/build.gradle.dcl b/dcl-utils/build.gradle.dcl new file mode 100644 index 0000000..67a04f3 --- /dev/null +++ b/dcl-utils/build.gradle.dcl @@ -0,0 +1,9 @@ +kotlinJvmLibrary { + + dependencies { + implementation("org.gradle:gradle-tooling-api:9.6.0-20260511195019+0000") + implementation("org.gradle:gradle-declarative-dsl-core:9.6.0-20260511195019+0000") + implementation("org.gradle:gradle-declarative-dsl-evaluator:9.6.0-20260511195019+0000") + implementation("org.gradle:gradle-declarative-dsl-tooling-models:9.6.0-20260511195019+0000") + } +} diff --git a/gradle-client/src/jvmMain/kotlin/org/gradle/client/core/gradle/dcl/DocumentUtils.kt b/dcl-utils/src/main/kotlin/DocumentOverlayUtils.kt similarity index 92% rename from gradle-client/src/jvmMain/kotlin/org/gradle/client/core/gradle/dcl/DocumentUtils.kt rename to dcl-utils/src/main/kotlin/DocumentOverlayUtils.kt index b932868..e3ffc83 100644 --- a/gradle-client/src/jvmMain/kotlin/org/gradle/client/core/gradle/dcl/DocumentUtils.kt +++ b/dcl-utils/src/main/kotlin/DocumentOverlayUtils.kt @@ -1,5 +1,3 @@ -package org.gradle.client.core.gradle.dcl - import org.gradle.internal.declarativedsl.dom.DeclarativeDocument import org.gradle.internal.declarativedsl.dom.DocumentResolution import org.gradle.internal.declarativedsl.dom.UnresolvedBase @@ -10,6 +8,7 @@ import org.gradle.internal.declarativedsl.dom.resolution.DocumentResolutionConta import org.gradle.internal.declarativedsl.dom.resolution.DocumentWithResolution import org.gradle.internal.declarativedsl.evaluator.main.AnalysisDocumentUtils.resolvedDocument import org.gradle.internal.declarativedsl.evaluator.main.AnalysisSequenceResult +import org.gradle.internal.declarativedsl.evaluator.runner.AnalysisStepResult import org.gradle.internal.declarativedsl.evaluator.runner.stepResultOrPartialResult import org.gradle.internal.declarativedsl.language.SourceData import org.gradle.internal.declarativedsl.language.SyntheticallyProduced @@ -59,7 +58,8 @@ fun DeclarativeDocument.nodeAt(fileIdentifier: String, offset: Int): Declarative } fun settingsWithNoOverlayOrigin(analysisSequenceResult: AnalysisSequenceResult): DocumentOverlayResult? { - val docs = analysisSequenceResult.stepResults.map { it.value.stepResultOrPartialResult.resolvedDocument() } + val docs = analysisSequenceResult.stepResults.mapNotNull { + (it.value.stepResultOrPartialResult as? AnalysisStepResult.PassedAnalysisStepResult)?.resolvedDocument() } if (docs.isEmpty()) return null @@ -67,7 +67,7 @@ fun settingsWithNoOverlayOrigin(analysisSequenceResult: AnalysisSequenceResult): } -internal fun indexBasedOverlayResultFromDocuments(docs: List): DocumentOverlayResult { +fun indexBasedOverlayResultFromDocuments(docs: List): DocumentOverlayResult { val emptyDoc = DocumentWithResolution( object : DeclarativeDocument { override val content: List = emptyList() @@ -91,12 +91,12 @@ internal fun indexBasedOverlayResultFromDocuments(docs: List): DocumentResolutionContainer { +fun indexBasedMultiResolutionContainer(docs: List): DocumentResolutionContainer { val indicesMaps: Map> = docs.associateWith { buildMap { fun visitValue(valueNode: DeclarativeDocument.ValueNode) { @@ -156,7 +156,7 @@ internal fun indexBasedMultiResolutionContainer(docs: List data(node) is DeclarativeDocument.ValueNode -> data(node) diff --git a/gradle-client/src/jvmMain/kotlin/org/gradle/client/core/gradle/dcl/ErrorUtil.kt b/dcl-utils/src/main/kotlin/org/gradle/client/core/gradle/dcl/ErrorUtil.kt similarity index 99% rename from gradle-client/src/jvmMain/kotlin/org/gradle/client/core/gradle/dcl/ErrorUtil.kt rename to dcl-utils/src/main/kotlin/org/gradle/client/core/gradle/dcl/ErrorUtil.kt index e15b649..bdda2ec 100644 --- a/gradle-client/src/jvmMain/kotlin/org/gradle/client/core/gradle/dcl/ErrorUtil.kt +++ b/dcl-utils/src/main/kotlin/org/gradle/client/core/gradle/dcl/ErrorUtil.kt @@ -33,7 +33,7 @@ fun userFriendlyErrorMessages( } else listOf("Block mismatch") CrossScopeAccess -> listOf("Cross-scope access") - + IsError -> if (node is DeclarativeDocument.DocumentNode.ErrorNode) { node.errors.map { error -> when (error) { @@ -44,7 +44,7 @@ fun userFriendlyErrorMessages( } } } else listOf("Syntax error") - + OpaqueValueInIdentityKey -> listOf("Opaque value passed as identity key") UnresolvedName -> listOf("Unresolved name") UnresolvedSignature -> listOf("Unresolved function signature") @@ -53,7 +53,7 @@ fun userFriendlyErrorMessages( UnresolvedValueUsed -> listOf("Assigned value is not resolved") ValueTypeMismatch -> listOf("Value type mismatch") NonEnumValueNamedReference -> listOf("Illegal named reference. Only enum entries can be referenced by name") - + UnresolvedBase -> emptyList() } } diff --git a/gradle-client/build.gradle.dcl b/gradle-client/build.gradle.dcl index 6d3b21c..e11a257 100644 --- a/gradle-client/build.gradle.dcl +++ b/gradle-client/build.gradle.dcl @@ -18,8 +18,9 @@ kotlinApplication { dependencies { implementation(project(":build-action")) implementation(project(":mutations-demo")) + implementation(project(":dcl-utils")) - implementation("org.gradle:gradle-tooling-api:9.5.0-milestone-1") + implementation("org.gradle:gradle-tooling-api:9.6.0-20260511195019+0000") implementation("com.arkivanov.decompose:decompose:3.3.0") implementation("com.arkivanov.decompose:extensions-compose:3.3.0") @@ -36,9 +37,9 @@ kotlinApplication { implementation("org.slf4j:slf4j-api:2.0.17") implementation("ch.qos.logback:logback-classic:1.5.18") - implementation("org.gradle:gradle-declarative-dsl-core:9.5.0-milestone-1") - implementation("org.gradle:gradle-declarative-dsl-evaluator:9.5.0-milestone-1") - implementation("org.gradle:gradle-declarative-dsl-tooling-models:9.5.0-milestone-1") + implementation("org.gradle:gradle-declarative-dsl-core:9.6.0-20260511195019+0000") + implementation("org.gradle:gradle-declarative-dsl-evaluator:9.6.0-20260511195019+0000") + implementation("org.gradle:gradle-declarative-dsl-tooling-models:9.6.0-20260511195019+0000") implementation("org.jspecify:jspecify:1.0.0") runtimeOnly("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.10.2") @@ -78,12 +79,6 @@ kotlinApplication { } } - detekt { - source = listOf(layout.projectDirectory.dir("src/jvmMain/kotlin"), layout.projectDirectory.dir("src/jvmTest/kotlin")) - config = listOf(layout.settingsDirectory.file("gradle/detekt/detekt.conf")) - parallel = true - } - compose { mainClass = "org.gradle.client.GradleClientMainKt" diff --git a/gradle-client/proguard-desktop.pro b/gradle-client/proguard-desktop.pro index 19e8c57..6fbfc43 100644 --- a/gradle-client/proguard-desktop.pro +++ b/gradle-client/proguard-desktop.pro @@ -88,6 +88,14 @@ # Kotlin embedded compiler -keep class org.jetbrains.kotlin.** { *; } +-keep class org.jetbrains.org.objectweb.asm.** { *; } + +# Guava uses signature-polymorphic MethodHandle.invokeExact and VarHandle.{get,set,setRelease,compareAndSet,getAndSet}, +# which ProGuard cannot resolve against java.base. The call sites are valid at runtime. +-dontwarn com.google.common.hash.ChecksumHashFunction$ChecksumMethodHandles +-dontwarn com.google.common.hash.Hashing$Crc32cMethodHandles +-dontwarn com.google.common.hash.LittleEndianByteArray$VarHandleLittleEndianBytes$1 +-dontwarn com.google.common.util.concurrent.AbstractFutureState$VarHandleAtomicHelper # JNA classes -keep class com.sun.jna.** { *; } diff --git a/gradle-client/src/jvmMain/kotlin/org/gradle/client/core/gradle/dcl/AnalysisUtils.kt b/gradle-client/src/jvmMain/kotlin/org/gradle/client/core/gradle/dcl/AnalysisUtils.kt index 39623ac..3cd2875 100644 --- a/gradle-client/src/jvmMain/kotlin/org/gradle/client/core/gradle/dcl/AnalysisUtils.kt +++ b/gradle-client/src/jvmMain/kotlin/org/gradle/client/core/gradle/dcl/AnalysisUtils.kt @@ -9,6 +9,7 @@ import org.gradle.internal.declarativedsl.dom.DocumentResolution.ElementResoluti import org.gradle.internal.declarativedsl.dom.resolution.DocumentResolutionContainer import org.gradle.internal.declarativedsl.evaluator.main.AnalysisSequenceResult import org.gradle.internal.declarativedsl.evaluator.main.SimpleAnalysisEvaluator +import org.gradle.internal.declarativedsl.evaluator.runner.AnalysisStepResult import org.gradle.internal.declarativedsl.evaluator.runner.stepResultOrPartialResult import org.gradle.internal.declarativedsl.language.SourceIdentifier @@ -20,9 +21,6 @@ fun analyzer( prerequisites.projectInterpretationSequence ) -fun AnalysisSequenceResult.sourceIdentifier(): SourceIdentifier = - stepResults.values.last().stepResultOrPartialResult.languageTreeResult.topLevelBlock.sourceData.sourceIdentifier - fun DeclarativeDocument.DocumentNode.ElementNode.type(resolutionContainer: DocumentResolutionContainer): DataType? = when (val resolution = resolutionContainer.data(this)) { is ElementNotResolved -> null diff --git a/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/composables/SourcesColumn.kt b/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/composables/SourcesColumn.kt index c436384..d579511 100644 --- a/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/composables/SourcesColumn.kt +++ b/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/composables/SourcesColumn.kt @@ -2,6 +2,7 @@ package org.gradle.client.ui.composables +import ErrorData import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.fillMaxWidth @@ -18,7 +19,6 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration -import org.gradle.client.core.gradle.dcl.ErrorData import org.gradle.client.core.gradle.dcl.userFriendlyErrorMessages import org.gradle.client.ui.connected.actions.declarativedocuments.HighlightingEntry import org.gradle.client.ui.theme.spacing @@ -121,7 +121,7 @@ private fun SourceFileTitleAndText( val errorPopupText = selectedErrors?.takeIf { it.isNotEmpty() }?.let { it.flatMap { userFriendlyErrorMessages(it.documentNode, it.resolution) }.joinToString("\n") } - + if (trimmedSource != null) { var isTrimmed by remember { mutableStateOf(true) } diff --git a/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/connected/actions/declarativedocuments/GetDeclarativeDocuments.kt b/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/connected/actions/declarativedocuments/GetDeclarativeDocuments.kt index 7f7c27e..8ae5bf1 100644 --- a/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/connected/actions/declarativedocuments/GetDeclarativeDocuments.kt +++ b/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/connected/actions/declarativedocuments/GetDeclarativeDocuments.kt @@ -8,10 +8,14 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.pointerHoverIcon +import errorRanges +import indexBasedMultiResolutionContainer +import indexBasedOverlayResultFromDocuments import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import nodeAt import org.gradle.client.build.action.GetResolvedDomAction import org.gradle.client.build.model.ResolvedDomPrerequisites import org.gradle.client.core.gradle.dcl.* @@ -33,10 +37,13 @@ import org.gradle.internal.declarativedsl.dom.operations.overlay.OverlayNodeOrig import org.gradle.internal.declarativedsl.dom.operations.overlay.OverlayOriginContainer import org.gradle.internal.declarativedsl.dom.resolution.DocumentResolutionContainer import org.gradle.internal.declarativedsl.evaluator.main.AnalysisDocumentUtils.resolvedDocument +import org.gradle.internal.declarativedsl.evaluator.runner.AnalysisStepResult import org.gradle.internal.declarativedsl.evaluator.runner.stepResultOrPartialResult import org.gradle.tooling.BuildAction import org.jetbrains.skiko.Cursor +import relevantRange import java.io.File +import kotlin.collections.orEmpty import kotlin.time.Duration.Companion.seconds @@ -79,7 +86,9 @@ class GetDeclarativeDocuments : GetModelAction.GetCompositeModelAction - val document = result.stepResultOrPartialResult.resolvedDocument() + val document = + (result.stepResultOrPartialResult as? AnalysisStepResult.PassedAnalysisStepResult)?.resolvedDocument() + ?: return /** * For multistep settings, we want to avoid document content appearing as duplicates in @@ -102,7 +111,8 @@ class GetDeclarativeDocuments : GetModelAction.GetCompositeModelAction>()) private fun readDocumentFile() = _selectedDocument.value.takeIf { it.canRead() }?.readText().orEmpty() private fun readSettingsFile() = model.settingsFile.takeIf { it.canRead() }?.readText().orEmpty() - + val typeRefContext = JointTypeRefContext( listOf(model.settingsInterpretationSequence, model.projectInterpretationSequence) ) @@ -42,7 +42,7 @@ internal class GetDeclarativeDocumentsModel(private val model: ResolvedDomPrereq _highlightedSourceRangeByFileId.value = entries _reportedErrors.value = emptyMap() } - + fun reportErrors(errorsByFileId: Map>) { _reportedErrors.value = errorsByFileId } diff --git a/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/connected/actions/declarativedocuments/ModelTreeRendering.kt b/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/connected/actions/declarativedocuments/ModelTreeRendering.kt index f243f15..425b8d5 100644 --- a/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/connected/actions/declarativedocuments/ModelTreeRendering.kt +++ b/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/connected/actions/declarativedocuments/ModelTreeRendering.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.times -import org.gradle.client.core.gradle.dcl.isUnresolvedBase +import isUnresolvedBase import org.gradle.client.core.gradle.dcl.type import org.gradle.client.ui.composables.LabelMedium import org.gradle.client.ui.composables.TitleSmall diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0aa20c1..1d8ae0e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-milestone-7-bin.zip +distributionUrl=https://services.gradle.org/distributions-snapshots/gradle-9.6.0-branch-eskatosDocInDclSchema-20260511195019+0000-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/mutations-demo/build.gradle.dcl b/mutations-demo/build.gradle.dcl index 45546e3..2814a24 100644 --- a/mutations-demo/build.gradle.dcl +++ b/mutations-demo/build.gradle.dcl @@ -1,5 +1,5 @@ kotlinJvmLibrary { dependencies { - implementation("org.gradle:gradle-declarative-dsl-core:9.5.0-milestone-1") + implementation("org.gradle:gradle-declarative-dsl-core:9.6.0-20260511195019+0000") } } diff --git a/settings.gradle.dcl b/settings.gradle.dcl index 9f7b75d..6be3832 100644 --- a/settings.gradle.dcl +++ b/settings.gradle.dcl @@ -7,8 +7,8 @@ plugins { id("io.github.gradle.develocity-conventions-plugin").version("0.14.1") id("org.gradle.toolchains.foojay-resolver-convention").version("0.8.0") - id("org.gradle.experimental.jvm-ecosystem").version("0.1.54") - id("org.gradle.experimental.kmp-ecosystem").version("0.1.54") + id("org.gradle.experimental.jvm-ecosystem").version("0.1.61") + id("org.gradle.experimental.kmp-ecosystem").version("0.1.61") id("org.gradle.client.ecosystem.custom-ecosystem") } @@ -32,3 +32,24 @@ rootProject.name = "gradle-client-root" include(":gradle-client") include(":build-action") include(":mutations-demo") +include(":agentic-tool") +include(":dcl-utils") + +defaults { + kotlinApplication { + detekt { + parallel = true + source = listOf(layout.projectDirectory.dir("src/jvmMain/kotlin"), layout.projectDirectory.dir("src/jvmTest/kotlin")) + config = listOf(layout.settingsDirectory.file("gradle/detekt/detekt.conf")) + } + } + kotlinJvmApplication { + javaVersion = 8 + } + kotlinJvmLibrary { + javaVersion = 8 + } + javaLibrary { + javaVersion = 8 + } +}