Skip to content
Open
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
9 changes: 5 additions & 4 deletions src/main/kotlin/org/pkl/lsp/PklBaseModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ class PklBaseModule(project: Project) : Component(project) {
val typeParameters =
member.typeParameterList?.typeParameters
?: listOf(PklNodeFactory.createTypeParameter(project, "Type"))
types[className] = Type.Class(member, listOf(), listOf(), typeParameters)
types[className] = Type.Class.create(member, listOf(), listOf(), typeParameters)
}
else -> types[className] = Type.Class(member)
else -> types[className] = Type.Class.create(member)
}
is PklTypeAlias -> types[member.name] = Type.Alias.unchecked(member, listOf(), listOf())
is PklClassMethod -> methods[member.name] = member
Expand Down Expand Up @@ -159,7 +159,7 @@ class PklBaseModule(project: Project) : Component(project) {
val additiveOperandType: Type by lazy {
val types =
mutableListOf(stringType, numberType, durationType, dataSizeType, collectionType, mapType)
if (bytesType != null) types += bytesType
bytesType?.let(types::add)
Type.union(types, this, null)
}

Expand All @@ -170,7 +170,8 @@ class PklBaseModule(project: Project) : Component(project) {
val subscriptableType: Type by lazy {
val types =
mutableListOf(stringType, collectionType, mapType, listingType, mappingType, dynamicType)
if (bytesType != null) types += bytesType
bytesType?.let(types::add)
project.pklRefModule.referenceType?.let(types::add)
Type.union(types, this, null)
}

Expand Down
36 changes: 36 additions & 0 deletions src/main/kotlin/org/pkl/lsp/PklRefModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.lsp

import org.pkl.lsp.ast.PklClass
import org.pkl.lsp.ast.PklModule
import org.pkl.lsp.type.Type

class PklRefModule(project: Project) : Component(project) {
val module: PklModule?
get() = project.stdlib.ref?.getModule()?.get()

val types: Map<String, Type> = buildMap {
for (member in module?.members ?: emptyList()) {
if (member is PklClass) {
put(member.name, Type.Class.create(member))
}
}
}

// Will be `null` for versions < 0.32
val referenceType: Type.Reference? by lazy { types["Reference"] as? Type.Reference }
}
2 changes: 2 additions & 0 deletions src/main/kotlin/org/pkl/lsp/Project.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class Project(private val server: PklLspServer) {

val pklBaseModule: PklBaseModule by lazy { PklBaseModule(this) }

val pklRefModule: PklRefModule by lazy { PklRefModule(this) }

val packageManager: PackageManager by lazy { PackageManager(this) }

val pklProjectManager: PklProjectManager by lazy { PklProjectManager(this) }
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/org/pkl/lsp/Stdlib.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ class Stdlib(project: Project) : Component(project) {
val base: VirtualFile
get() = files["base"]!!

val ref: VirtualFile?
get() = files["ref"]

val version: Version
get() = loadVersion()

Expand Down
28 changes: 28 additions & 0 deletions src/main/kotlin/org/pkl/lsp/analyzers/TypeAnalyzer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
package org.pkl.lsp.analyzers

import org.pkl.lsp.ErrorMessages
import org.pkl.lsp.PklBaseModule
import org.pkl.lsp.Project
import org.pkl.lsp.ast.PklDeclaredType
import org.pkl.lsp.ast.PklNode
import org.pkl.lsp.packages.dto.PklProject
import org.pkl.lsp.type.Type
import org.pkl.lsp.type.toType

Expand All @@ -27,6 +29,7 @@ class TypeAnalyzer(project: Project) : Analyzer(project) {
when {
node is PklDeclaredType && !node.typeArgumentList?.types.isNullOrEmpty() -> {
val type = node.toType(project.pklBaseModule, emptyMap(), node.containingFile.pklProject)

val argCount = node.typeArgumentList!!.types.size
val paramCount =
if (type is Type.Class) type.typeArguments.size
Expand All @@ -37,8 +40,33 @@ class TypeAnalyzer(project: Project) : Analyzer(project) {
ErrorMessages.create("incorrectTypeArgumentCount", paramCount, argCount),
)
}

val unaliased = type.unaliased(project.pklBaseModule, node.containingFile.pklProject)
if (
unaliased is Type.Reference &&
type.containsConstrainedType(project.pklBaseModule, node.containingFile.pklProject)
) {
diagnosticsHolder.addError(
node,
ErrorMessages.create("invalidReferenceTypeWithConstraint"),
)
}

false
}
else -> true
}

private fun Type.containsConstrainedType(base: PklBaseModule, context: PklProject?): Boolean =
!constraints.isEmpty() ||
when (this) {
is Type.Class -> typeArguments.any { it.containsConstrainedType(base, context) }
is Type.Alias ->
typeArguments.any { it.containsConstrainedType(base, context) } ||
aliasedType(base, context).containsConstrainedType(base, context)
is Type.Union ->
leftType.containsConstrainedType(base, context) ||
rightType.containsConstrainedType(base, context)
else -> false
}
}
2 changes: 2 additions & 0 deletions src/main/kotlin/org/pkl/lsp/ast/Expr.kt
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ class PklSuperSubscriptExprImpl(
override fun <R> accept(visitor: PklVisitor<R>): R? {
return visitor.visitSuperSubscriptExpr(this)
}

override val expr: PklExpr by lazy { children.firstInstanceOf<PklExpr>()!! }
}

class PklQualifiedAccessExprImpl(
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/org/pkl/lsp/ast/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@ private fun PklType?.isRecursive(seen: Set<PklTypeAlias>, context: PklProject?):
val PklNode.isInPklBaseModule: Boolean
get() = containingFile === project.stdlib.base

val PklNode.isInPklRefModule: Boolean
get() = project.stdlib.ref != null && containingFile === project.stdlib.ref

val PklModuleMember.owner: PklTypeDefOrModule?
get() = parentOfTypes(PklClass::class, PklModule::class)

Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/org/pkl/lsp/ast/ModuleOrClass.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class PklModuleImpl(override val ctx: Node, override val virtualFile: VirtualFil
header?.moduleExtendsAmendsClause?.moduleUri
}

private val lock = Object()
private val lock = Any()

// This is cached at the VirtualFile level
override fun supermodule(context: PklProject?): PklModule? =
Expand Down Expand Up @@ -88,7 +88,7 @@ class PklModuleImpl(override val ctx: Node, override val virtualFile: VirtualFil
?: uri.toString().substringAfterLast('/').replace(".pkl", "")
}

override val moduleName: String? by lazy {
override val moduleName: String by lazy {
header?.moduleClause?.moduleName ?: uri.toString().substringAfterLast('/').replace(".pkl", "")
}

Expand Down
19 changes: 17 additions & 2 deletions src/main/kotlin/org/pkl/lsp/ast/PklNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ interface PklModule : PklTypeDefOrModule {
fun cache(context: PklProject?): ModuleMemberCache

val shortDisplayName: String
val moduleName: String?
val moduleName: String

/** The package dependencies of this module. */
fun dependencies(context: PklProject?): Map<String, Dependency>?
Expand Down Expand Up @@ -512,7 +512,9 @@ interface PklAmendExpr : PklExpr, PklObjectBodyOwner {
override val objectBody: PklObjectBody
}

interface PklSuperSubscriptExpr : PklExpr
interface PklSuperSubscriptExpr : PklExpr {
val expr: PklExpr
}

interface PklAccessExpr : PklExpr, PklReference, IdentifierOwner {
val memberNameText: String
Expand Down Expand Up @@ -838,6 +840,19 @@ abstract class AbstractPklNode(
}
}

abstract class FakePklNode(override val project: Project, override val parent: PklNode? = null) :
PklNode {
override val span: Span = Span(0, 0, 0, 0)
override val children: List<PklNode> = emptyList()
override val containingFile: VirtualFile = EphemeralFile("", project)
override val enclosingModule: PklModule? = null
override val terminals: List<Terminal> = emptyList()
override val text: String = ""
override val isMissing: Boolean = false
override val source: String = ""
override var index: Int = 0
}

class PklErrorImpl(
override val project: Project,
override val parent: PklNode?,
Expand Down
114 changes: 114 additions & 0 deletions src/main/kotlin/org/pkl/lsp/ast/Reference.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.lsp.ast

import org.eclipse.lsp4j.CompletionItem
import org.eclipse.lsp4j.CompletionItemKind
import org.pkl.lsp.PklVisitor
import org.pkl.lsp.Project
import org.pkl.lsp.type.Type

interface PklReferenceQualifiedAccessProxy : PklNode {
val name: String
val domain: Type
val referent: PklType
val type: PklType

fun toCompletionItem(): CompletionItem {
return CompletionItem(name).apply {
kind = CompletionItemKind.Field
detail = type.render()
}
}
}

class PklReferenceQualifiedAccessProxyImpl(
project: Project,
override val name: String,
override val domain: Type,
override val referent: PklType,
) : FakePklNode(project), PklReferenceQualifiedAccessProxy {
override fun <R> accept(visitor: PklVisitor<R>): R? = null

override val type: PklType =
DeclaredType(
project,
project.pklRefModule.referenceType!!,
listOf(DeclaredType(project, domain), referent),
)

class DeclaredType(project: Project, type: Type, typeArguments: List<PklType>? = null) :
FakePklNode(project), PklDeclaredType {
override fun <R> accept(visitor: PklVisitor<R>): R? = visitor.visitDeclaredType(this)

override val name: PklTypeName = TypeName(project, type)
override val typeArgumentList: PklTypeArgumentList? =
typeArguments?.let {
object : FakePklNode(project), PklTypeArgumentList {
override fun <R> accept(visitor: PklVisitor<R>): R? = visitor.visitTypeArgumentList(this)

override val types: List<PklType> = it
}
}
}

class TypeName(project: Project, type: Type) : FakePklNode(project), PklTypeName {
override fun <R> accept(visitor: PklVisitor<R>): R? = visitor.visitTypeName(this)

override val moduleName: PklModuleName = ModuleName(project, type)
override val simpleTypeName: PklSimpleTypeName = SimpleTypeName(project, type)
override val text: String =
"${moduleName.identifier!!.text}#${simpleTypeName.identifier!!.text}"
}

class ModuleName(project: Project, type: Type) : FakePklNode(project), PklModuleName {
override fun <R> accept(visitor: PklVisitor<R>): R? = visitor.visitModuleName(this)

override val identifier: Terminal =
Identifier(
project,
when (type) {
is Type.Class -> type.ctx.enclosingModule?.moduleName
is Type.Alias -> type.ctx.enclosingModule?.moduleName
is Type.Module -> ""
else -> null
} ?: "<module>",
)
}

class SimpleTypeName(project: Project, type: Type) : FakePklNode(project), PklSimpleTypeName {
override fun <R> accept(visitor: PklVisitor<R>): R? = visitor.visitSimpleTypeName(this)

override val identifier: Terminal =
Identifier(
project,
when (type) {
is Type.Class -> type.ctx.name
is Type.Alias -> type.ctx.name
is Type.Module -> type.ctx.moduleName
is Type.Variable -> type.ctx.text
else -> "<type>"
},
)
}

class Identifier(project: Project, name: String) : FakePklNode(project), Terminal {
override fun <R> accept(visitor: PklVisitor<R>): R? = visitor.visitTerminal(this)

override val text: String = name
override val type: TokenType = TokenType.Identifier
}
}
23 changes: 23 additions & 0 deletions src/main/kotlin/org/pkl/lsp/ast/Type.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ class PklUnknownTypeImpl(project: Project, override val parent: PklNode, ctx: No
}
}

class PklFakeUnknownTypeImpl(project: Project) : FakePklNode(project), PklUnknownType {
override fun <R> accept(visitor: PklVisitor<R>): R? = visitor.visitUnknownType(this)
}

class PklNothingTypeImpl(project: Project, override val parent: PklNode, ctx: Node) :
AbstractPklNode(project, parent, ctx), PklNothingType {
override fun <R> accept(visitor: PklVisitor<R>): R? {
Expand Down Expand Up @@ -156,6 +160,25 @@ class PklUnionTypeImpl(project: Project, override val parent: PklNode, ctx: Node
}
}

class PklFakeUnionTypeImpl(
project: Project,
override val leftType: PklType,
override val rightType: PklType,
) : FakePklNode(project), PklUnionType {
override val typeList: List<PklType> by lazy { listOf(leftType, rightType) }

override fun <R> accept(visitor: PklVisitor<R>): R? = visitor.visitUnionType(this)

companion object {
fun create(project: Project, types: List<PklType>): PklType =
when {
types.isEmpty() -> PklFakeUnknownTypeImpl(project)
types.size == 1 -> types.single()
else -> types.reduce { t1, t2 -> PklFakeUnionTypeImpl(project, t1, t2) }
}
}
}

class PklFunctionTypeImpl(project: Project, override val parent: PklNode, override val ctx: Node) :
AbstractPklNode(project, parent, ctx), PklFunctionType {
override val parameterList: List<PklType> by lazy {
Expand Down
6 changes: 6 additions & 0 deletions src/main/kotlin/org/pkl/lsp/documentation/toMarkdown.kt
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ private fun PklNode.doRenderMarkdown(originalNode: PklNode?, context: PklProject
typeParameterList?.let { append(it.doRenderMarkdown(originalNode, context)) }
}
is PklType -> render()
is PklReferenceQualifiedAccessProxy ->
buildString {
append(name)
append(": ")
append(type.doRenderMarkdown(originalNode, context))
}
else -> text
}

Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/org/pkl/lsp/features/HoverFeature.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -89,6 +89,7 @@ class HoverFeature(project: Project) : Component(project) {
is PklTypedIdentifier -> node.toMarkdown(originalNode, context)
is PklThisExpr -> node.computeThisType(base, mapOf(), context).toMarkdown(project, context)
is PklModuleExpr -> node.enclosingModule?.toMarkdown(originalNode, context)
is PklReferenceQualifiedAccessProxy -> node.toMarkdown(originalNode, context)
else -> null
}
}
Expand Down
Loading
Loading