Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add settings.gradle.dcl code assistance #7

Merged
merged 3 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format

[versions]
gradle-tooling = "8.11-20240930100431+0000"
declarative-dsl = "8.11-20240930100431+0000"
gradle-tooling = "8.12-20241112084018+0000"
declarative-dsl = "8.12-20241112084018+0000"
detekt = "1.23.6"
lsp4j = "0.23.1"
logback = "1.5.6"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,9 @@ import org.eclipse.lsp4j.SignatureInformation
import org.eclipse.lsp4j.jsonrpc.messages.Either
import org.eclipse.lsp4j.services.LanguageClient
import org.eclipse.lsp4j.services.TextDocumentService
import org.gradle.declarative.dsl.schema.AnalysisSchema
import org.gradle.declarative.dsl.schema.DataClass
import org.gradle.declarative.dsl.schema.DataParameter
import org.gradle.declarative.dsl.schema.DataType
import org.gradle.declarative.dsl.schema.DataTypeRef
import org.gradle.declarative.dsl.schema.FunctionSemantics
import org.gradle.declarative.dsl.schema.SchemaFunction
import org.gradle.declarative.dsl.schema.*
import org.gradle.declarative.lsp.build.model.DeclarativeResourcesModel
import org.gradle.declarative.lsp.extension.indexBasedOverlayResultFromDocuments
import org.gradle.declarative.lsp.extension.toLspRange
import org.gradle.declarative.lsp.service.MutationRegistry
import org.gradle.declarative.lsp.service.VersionedDocumentStore
Expand All @@ -59,11 +54,9 @@ import org.gradle.internal.declarativedsl.analysis.SchemaTypeRefContext
import org.gradle.internal.declarativedsl.dom.DeclarativeDocument
import org.gradle.internal.declarativedsl.dom.DocumentResolution
import org.gradle.internal.declarativedsl.dom.mutation.MutationParameterKind
import org.gradle.internal.declarativedsl.dom.operations.overlay.DocumentOverlay
import org.gradle.internal.declarativedsl.dom.operations.overlay.DocumentOverlayResult
import org.gradle.internal.declarativedsl.dom.resolution.DocumentResolutionContainer
import org.gradle.internal.declarativedsl.evaluator.main.AnalysisDocumentUtils.resolvedDocument
import org.gradle.internal.declarativedsl.evaluator.main.AnalysisSequenceResult
import org.gradle.internal.declarativedsl.evaluator.main.SimpleAnalysisEvaluator
import org.gradle.internal.declarativedsl.evaluator.runner.stepResultOrPartialResult
import org.slf4j.LoggerFactory
Expand All @@ -78,7 +71,6 @@ class DeclarativeTextDocumentService : TextDocumentService {
private lateinit var documentStore: VersionedDocumentStore
private lateinit var mutationRegistry: MutationRegistry
private lateinit var declarativeResources: DeclarativeResourcesModel
private lateinit var analysisSchema: AnalysisSchema
private lateinit var schemaAnalysisEvaluator: SimpleAnalysisEvaluator

fun initialize(
Expand All @@ -91,8 +83,7 @@ class DeclarativeTextDocumentService : TextDocumentService {
this.documentStore = documentStore
this.mutationRegistry = mutationRegistry
this.declarativeResources = declarativeResources

this.analysisSchema = declarativeResources.analysisSchema

this.schemaAnalysisEvaluator = SimpleAnalysisEvaluator.withSchema(
declarativeResources.settingsInterpretationSequence,
declarativeResources.projectInterpretationSequence
Expand All @@ -106,9 +97,9 @@ class DeclarativeTextDocumentService : TextDocumentService {
params?.let {
val uri = URI(it.textDocument.uri)
val text = it.textDocument.text
val dom = parse(uri, text)
val parsed = parse(uri, text)
run {
documentStore.storeInitial(uri, text, dom)
documentStore.storeInitial(uri, text, parsed.documentOverlayResult, parsed.analysisSchemas)
processDocument(uri)
}
}
Expand All @@ -121,8 +112,8 @@ class DeclarativeTextDocumentService : TextDocumentService {
it.contentChanges.forEach { change ->
val version = it.textDocument.version
val text = change.text
val dom = parse(uri, change.text)
documentStore.storeVersioned(uri, version, text, dom)
val parsed = parse(uri, change.text)
documentStore.storeVersioned(uri, version, text, parsed.documentOverlayResult, parsed.analysisSchemas)
processDocument(uri)
}
}
Expand All @@ -144,7 +135,7 @@ class DeclarativeTextDocumentService : TextDocumentService {
LOGGER.trace("Hover requested for position: {}", params)
val hover = params?.let {
val uri = URI(it.textDocument.uri)
withDom(uri) { dom, _ ->
withDom(uri) { dom, _, _ ->
// LSPs are supplying 0-based line and column numbers, while the DSL model is 1-based
val visitor = BestFittingNodeVisitor(
params.position,
Expand All @@ -169,19 +160,20 @@ class DeclarativeTextDocumentService : TextDocumentService {

override fun completion(params: CompletionParams?): CompletableFuture<Either<MutableList<CompletionItem>, CompletionList>> {
LOGGER.trace("Completion requested for position: {}", params)
val completions = params?.let {
val uri = URI(it.textDocument.uri)
withDom(uri) { dom, _ ->
val completions = params?.let { param ->
val uri = URI(param.textDocument.uri)
withDom(uri) { dom, schema, _ ->
dom.document.visit(
BestFittingNodeVisitor(
params.position,
DeclarativeDocument.DocumentNode.ElementNode::class
)
).bestFittingNode
?.getDataClass(dom.overlayResolutionContainer)
?.let { dataClass ->
computePropertyCompletions(dataClass, analysisSchema) +
computeFunctionCompletions(dataClass, analysisSchema)
.let { it ?: schema.topLevelReceiverType }
.let { dataClass ->
computePropertyCompletions(dataClass, schema) +
computeFunctionCompletions(dataClass, schema)
}
}
}.orEmpty().toMutableList()
Expand All @@ -193,7 +185,7 @@ class DeclarativeTextDocumentService : TextDocumentService {

val signatureInformationList = params?.let {
val uri = URI(it.textDocument.uri)
withDom(uri) { dom, _ ->
withDom(uri) { dom, _, _ ->
val position = it.position
val matchingNodes = dom.document.visit(
BestFittingNodeVisitor(
Expand Down Expand Up @@ -258,10 +250,10 @@ class DeclarativeTextDocumentService : TextDocumentService {

// Utility and other member functions ------------------------------------------------------------------------------

private fun processDocument(uri: URI) = withDom(uri) { dom, _ ->
private fun processDocument(uri: URI) = withDom(uri) { dom, schema, _ ->
reportSyntaxErrors(uri, dom)
reportSemanticErrors(uri, dom)
mutationRegistry.registerDocument(uri, dom.result)
mutationRegistry.registerDocument(uri, schema, dom.result)
}

/**
Expand Down Expand Up @@ -300,30 +292,37 @@ class DeclarativeTextDocumentService : TextDocumentService {
)
)
}

data class ParsedDocument(
val documentOverlayResult: DocumentOverlayResult,
val analysisSchemas: List<AnalysisSchema>
)

private fun parse(uri: URI, text: String): DocumentOverlayResult {
fun AnalysisSequenceResult.lastStepDocument() =
stepResults.values.last().stepResultOrPartialResult.resolvedDocument()

private fun parse(uri: URI, text: String): ParsedDocument {
val fileName = uri.path.substringAfterLast('/')
val document = schemaAnalysisEvaluator.evaluate(fileName, text).lastStepDocument()
val analysisResult = schemaAnalysisEvaluator.evaluate(fileName, text)

// Workaround: for now, the mutation utilities cannot handle mutations that touch the underlay document content.
// To avoid that, use an empty document as an underlay instead of the real document produced from the
// settings file.
// To avoid that, use the utility that produces an overlay result with no real underlay content.
// This utility also takes care of multi-step resolution results and merges them, presenting .
// TODO: carry both the real overlay and the document produced from just the current file, run the mutations
// against the latter for now.
// TODO: once the mutation utilities start handling mutations across the overlay, pass them the right overlay.
val emptyUnderlay = schemaAnalysisEvaluator.evaluate("empty-underlay/build.gradle.dcl", "").lastStepDocument()

val overlay = indexBasedOverlayResultFromDocuments(
analysisResult.stepResults.map { it.value.stepResultOrPartialResult.resolvedDocument() }
)

LOGGER.trace("Parsed declarative model for document: {}", uri)

return DocumentOverlay.overlayResolvedDocuments(emptyUnderlay, document)
return ParsedDocument(
overlay,
analysisResult.stepResults.map { it.key.evaluationSchemaForStep.analysisSchema }
)
}

private fun <T> withDom(uri: URI, work: (DocumentOverlayResult, String) -> T): T? {
private fun <T> withDom(uri: URI, work: (DocumentOverlayResult, AnalysisSchema, String) -> T): T? {
return documentStore[uri]?.let { entry ->
work(entry.dom, entry.document)
work(entry.dom, entry.unionSchema, entry.document)
}
}

Expand Down Expand Up @@ -411,9 +410,9 @@ private fun computeTypedPlaceholder(
type: DataTypeRef,
analysisSchema: AnalysisSchema
): String {
val resolvedType = SchemaTypeRefContext(analysisSchema).resolveRef(type)
return when (resolvedType) {
return when (val resolvedType = SchemaTypeRefContext(analysisSchema).resolveRef(type)) {
is DataType.BooleanDataType -> "\${$index|true,false|}"
is EnumClass -> "\${$index|${resolvedType.entryNames.joinToString(",")}|}"
is DataType.IntDataType -> "\${$index:0}"
is DataType.LongDataType -> "\${$index:0L}"
is DataType.StringDataType -> "\"\${$index}\""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package org.gradle.declarative.lsp.extension

import org.gradle.declarative.dsl.schema.AnalysisSchema
import org.gradle.declarative.dsl.schema.*
import org.gradle.internal.declarativedsl.analysis.DefaultAnalysisSchema
import org.gradle.internal.declarativedsl.analysis.DefaultDataClass
import org.gradle.internal.declarativedsl.analysis.DefaultEnumClass
import org.gradle.internal.declarativedsl.analysis.DefaultFqName

/*
* Copyright 2024 the original author or authors.
Expand All @@ -24,4 +28,84 @@ inline fun <reified T> AnalysisSchema.findType(name: String): T? = dataClassType
dataClass.name.qualifiedName == name && dataClass is T
}?.let {
it as T
}
}


/**
* Produces an [AnalysisSchema] that approximates the [schemas] merged together.
* Namely, it has [AnalysisSchema.dataClassTypesByFqName] from all the schemas, and if a type appears in more than
* one of the schemas, its contents get merged, too.
*
* The top level receiver is either the merged type from the [AnalysisSchema.topLevelReceiverType]s from the schemas, if
* it has the same name in all of them, or a type with a synthetic name that has the content from the top level
* receiver types from [schemas].
*/
fun unionAnalysisSchema(schemas: List<AnalysisSchema>): AnalysisSchema = if (schemas.size == 1)
schemas.single()
else {
fun mergeDataClasses(newName: String?, dataClasses: List<DataClass>): DataClass {
// Can't have properties with the same name but different types anyway:
val properties = dataClasses.flatMap { it.properties }.distinctBy { it.name }

val functions = dataClasses.flatMap { it.memberFunctions }
.distinctBy { listOf(it.simpleName) + it.parameters.map { it.name to typeIdentityName(it.type) } }

val constructors =
dataClasses.flatMap { it.constructors }.distinctBy { it.parameters.map { typeIdentityName(it.type) } }

val supertypes = dataClasses.flatMap { it.supertypes }.toSet()

return DefaultDataClass(
newName?.let { DefaultFqName.parse(it) } ?: dataClasses.first().name,
dataClasses.first().javaTypeName,
dataClasses.first().javaTypeArgumentTypeNames,
supertypes,
properties,
functions,
constructors
)
}

val dataClassesByFqName = run {
fun mergeEnums(enumTypes: List<EnumClass>): EnumClass =
DefaultEnumClass(
enumTypes.first().name,
enumTypes.first().javaTypeName,
enumTypes.flatMap { it.entryNames }.distinct()
)

schemas.flatMap { it.dataClassTypesByFqName.values }.groupBy { it.name }
.mapValues { (_, dataClasses) ->
when {
dataClasses.all { it is DataClass } -> mergeDataClasses(null, dataClasses.map { it as DataClass })
dataClasses.all { it is EnumClass } -> mergeEnums(dataClasses.map { it as EnumClass })
else -> error("mixed enum and data classes")
}
}
}

val newTopLevelReceiver = run {
val topLevelReceivers = schemas.map { it.topLevelReceiverType }
if (topLevelReceivers.map { it.name.qualifiedName }.distinct().size == 1) {
dataClassesByFqName.getValue(topLevelReceivers.first().name) as DataClass
} else {
mergeDataClasses("\$top-level-receiver\$", topLevelReceivers)
}
}

DefaultAnalysisSchema(
newTopLevelReceiver,
dataClassesByFqName,
emptyMap(),
emptyMap(),
emptySet()
)
}

private fun typeIdentityName(typeRef: DataTypeRef) = when (typeRef) {
is DataTypeRef.Name -> typeRef.fqName.qualifiedName
is DataTypeRef.Type -> when (val type = typeRef.dataType) {
is DataType.ClassDataType -> type.name.qualifiedName
else -> type.toString()
}
}
Loading