diff --git a/src/main/kotlin/org/pkl/lsp/PklBaseModule.kt b/src/main/kotlin/org/pkl/lsp/PklBaseModule.kt index e9703a21..7c72917d 100644 --- a/src/main/kotlin/org/pkl/lsp/PklBaseModule.kt +++ b/src/main/kotlin/org/pkl/lsp/PklBaseModule.kt @@ -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 @@ -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) } @@ -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) } diff --git a/src/main/kotlin/org/pkl/lsp/PklRefModule.kt b/src/main/kotlin/org/pkl/lsp/PklRefModule.kt new file mode 100644 index 00000000..6633e7c7 --- /dev/null +++ b/src/main/kotlin/org/pkl/lsp/PklRefModule.kt @@ -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 = 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 } +} diff --git a/src/main/kotlin/org/pkl/lsp/Project.kt b/src/main/kotlin/org/pkl/lsp/Project.kt index 4c03dc71..bf141739 100644 --- a/src/main/kotlin/org/pkl/lsp/Project.kt +++ b/src/main/kotlin/org/pkl/lsp/Project.kt @@ -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) } diff --git a/src/main/kotlin/org/pkl/lsp/Stdlib.kt b/src/main/kotlin/org/pkl/lsp/Stdlib.kt index 331a0c49..fb49c9f5 100644 --- a/src/main/kotlin/org/pkl/lsp/Stdlib.kt +++ b/src/main/kotlin/org/pkl/lsp/Stdlib.kt @@ -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() diff --git a/src/main/kotlin/org/pkl/lsp/analyzers/TypeAnalyzer.kt b/src/main/kotlin/org/pkl/lsp/analyzers/TypeAnalyzer.kt index 13de3782..7bfb17e0 100644 --- a/src/main/kotlin/org/pkl/lsp/analyzers/TypeAnalyzer.kt +++ b/src/main/kotlin/org/pkl/lsp/analyzers/TypeAnalyzer.kt @@ -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 @@ -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 @@ -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 + } } diff --git a/src/main/kotlin/org/pkl/lsp/ast/Expr.kt b/src/main/kotlin/org/pkl/lsp/ast/Expr.kt index 1c6522d3..7231202d 100644 --- a/src/main/kotlin/org/pkl/lsp/ast/Expr.kt +++ b/src/main/kotlin/org/pkl/lsp/ast/Expr.kt @@ -321,6 +321,8 @@ class PklSuperSubscriptExprImpl( override fun accept(visitor: PklVisitor): R? { return visitor.visitSuperSubscriptExpr(this) } + + override val expr: PklExpr by lazy { children.firstInstanceOf()!! } } class PklQualifiedAccessExprImpl( diff --git a/src/main/kotlin/org/pkl/lsp/ast/Extensions.kt b/src/main/kotlin/org/pkl/lsp/ast/Extensions.kt index ea02877f..4e0fdb51 100644 --- a/src/main/kotlin/org/pkl/lsp/ast/Extensions.kt +++ b/src/main/kotlin/org/pkl/lsp/ast/Extensions.kt @@ -273,6 +273,9 @@ private fun PklType?.isRecursive(seen: Set, 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) diff --git a/src/main/kotlin/org/pkl/lsp/ast/ModuleOrClass.kt b/src/main/kotlin/org/pkl/lsp/ast/ModuleOrClass.kt index 43f04d3d..040a754f 100644 --- a/src/main/kotlin/org/pkl/lsp/ast/ModuleOrClass.kt +++ b/src/main/kotlin/org/pkl/lsp/ast/ModuleOrClass.kt @@ -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? = @@ -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", "") } diff --git a/src/main/kotlin/org/pkl/lsp/ast/PklNode.kt b/src/main/kotlin/org/pkl/lsp/ast/PklNode.kt index f7167924..b046fdac 100644 --- a/src/main/kotlin/org/pkl/lsp/ast/PklNode.kt +++ b/src/main/kotlin/org/pkl/lsp/ast/PklNode.kt @@ -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? @@ -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 @@ -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 = emptyList() + override val containingFile: VirtualFile = EphemeralFile("", project) + override val enclosingModule: PklModule? = null + override val terminals: List = 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?, diff --git a/src/main/kotlin/org/pkl/lsp/ast/Reference.kt b/src/main/kotlin/org/pkl/lsp/ast/Reference.kt new file mode 100644 index 00000000..acc950aa --- /dev/null +++ b/src/main/kotlin/org/pkl/lsp/ast/Reference.kt @@ -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 accept(visitor: PklVisitor): R? = null + + override val type: PklType = + DeclaredType( + project, + project.pklRefModule.referenceType!!, + listOf(DeclaredType(project, domain), referent), + ) + + class DeclaredType(project: Project, type: Type, typeArguments: List? = null) : + FakePklNode(project), PklDeclaredType { + override fun accept(visitor: PklVisitor): R? = visitor.visitDeclaredType(this) + + override val name: PklTypeName = TypeName(project, type) + override val typeArgumentList: PklTypeArgumentList? = + typeArguments?.let { + object : FakePklNode(project), PklTypeArgumentList { + override fun accept(visitor: PklVisitor): R? = visitor.visitTypeArgumentList(this) + + override val types: List = it + } + } + } + + class TypeName(project: Project, type: Type) : FakePklNode(project), PklTypeName { + override fun accept(visitor: PklVisitor): 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 accept(visitor: PklVisitor): 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 + } ?: "", + ) + } + + class SimpleTypeName(project: Project, type: Type) : FakePklNode(project), PklSimpleTypeName { + override fun accept(visitor: PklVisitor): 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 -> "" + }, + ) + } + + class Identifier(project: Project, name: String) : FakePklNode(project), Terminal { + override fun accept(visitor: PklVisitor): R? = visitor.visitTerminal(this) + + override val text: String = name + override val type: TokenType = TokenType.Identifier + } +} diff --git a/src/main/kotlin/org/pkl/lsp/ast/Type.kt b/src/main/kotlin/org/pkl/lsp/ast/Type.kt index 93bc5aab..06fd500f 100644 --- a/src/main/kotlin/org/pkl/lsp/ast/Type.kt +++ b/src/main/kotlin/org/pkl/lsp/ast/Type.kt @@ -37,6 +37,10 @@ class PklUnknownTypeImpl(project: Project, override val parent: PklNode, ctx: No } } +class PklFakeUnknownTypeImpl(project: Project) : FakePklNode(project), PklUnknownType { + override fun accept(visitor: PklVisitor): R? = visitor.visitUnknownType(this) +} + class PklNothingTypeImpl(project: Project, override val parent: PklNode, ctx: Node) : AbstractPklNode(project, parent, ctx), PklNothingType { override fun accept(visitor: PklVisitor): R? { @@ -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 by lazy { listOf(leftType, rightType) } + + override fun accept(visitor: PklVisitor): R? = visitor.visitUnionType(this) + + companion object { + fun create(project: Project, types: List): 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 by lazy { diff --git a/src/main/kotlin/org/pkl/lsp/documentation/toMarkdown.kt b/src/main/kotlin/org/pkl/lsp/documentation/toMarkdown.kt index 4074dc5d..9c3618be 100644 --- a/src/main/kotlin/org/pkl/lsp/documentation/toMarkdown.kt +++ b/src/main/kotlin/org/pkl/lsp/documentation/toMarkdown.kt @@ -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 } diff --git a/src/main/kotlin/org/pkl/lsp/features/HoverFeature.kt b/src/main/kotlin/org/pkl/lsp/features/HoverFeature.kt index f2e16844..d03b9903 100644 --- a/src/main/kotlin/org/pkl/lsp/features/HoverFeature.kt +++ b/src/main/kotlin/org/pkl/lsp/features/HoverFeature.kt @@ -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. @@ -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 } } diff --git a/src/main/kotlin/org/pkl/lsp/resolvers/ResolveVisitors.kt b/src/main/kotlin/org/pkl/lsp/resolvers/ResolveVisitors.kt index 5994698f..17a294b9 100644 --- a/src/main/kotlin/org/pkl/lsp/resolvers/ResolveVisitors.kt +++ b/src/main/kotlin/org/pkl/lsp/resolvers/ResolveVisitors.kt @@ -168,13 +168,18 @@ object ResolveVisitors { val type = when (element) { + is PklReferenceQualifiedAccessProxy -> + element.project.pklRefModule.referenceType!!.withTypeArguments( + element.domain, + element.referent.toType(base, bindings, context, preserveUnboundTypeVars), + ) is PklImport -> element .resolve(context) .computeResolvedImportType(base, bindings, preserveUnboundTypeVars, context) is PklTypeParameter -> bindings[element] ?: Type.Unknown is PklMethod -> computeMethodReturnType(element, bindings, context, receiverType) - is PklClass -> base.classType.withTypeArguments(Type.Class(element)) + is PklClass -> base.classType.withTypeArguments(Type.Class.create(element)) is PklTypeAlias -> base.typeAliasType.withTypeArguments(Type.alias(element, context)) is PklNavigableElement -> element.computeResolvedImportType(base, bindings, context, preserveUnboundTypeVars) @@ -238,7 +243,7 @@ object ResolveVisitors { base.mapConstructor -> { val arguments = argumentList?.elements if (arguments == null || arguments.size < 2) { - Type.Class(base.mapType.ctx) + Type.Class.create(base.mapType.ctx) } else { var keyType = arguments[0].computeExprType(base, bindings, context) var valueType = arguments[1].computeExprType(base, bindings, context) @@ -261,7 +266,7 @@ object ResolveVisitors { ) } } - Type.Class(base.mapType.ctx, listOf(keyType, valueType)) + Type.Class.create(base.mapType.ctx, listOf(keyType, valueType)) } } base.anyGetClassMethod -> @@ -363,6 +368,7 @@ object ResolveVisitors { if (name != expectedName) return true when { + element is PklReferenceQualifiedAccessProxy -> result = element element is PklImport -> { result = if (resolveImports && !element.isGlob) { @@ -418,6 +424,7 @@ object ResolveVisitors { } } element is PklNavigableElement -> result.add(element) + element is PklReferenceQualifiedAccessProxy -> result.add(element) element is PklExpr -> return true else -> unexpectedType(element) } @@ -459,6 +466,7 @@ object ResolveVisitors { result.add(element.complete()) } is PklExpr -> {} + is PklReferenceQualifiedAccessProxy -> result.add(element.toCompletionItem()) else -> throw AssertionError("Unexpected type: ${element::class.java.typeName ?: "null"}") } return true diff --git a/src/main/kotlin/org/pkl/lsp/type/ComputeDefinitionType.kt b/src/main/kotlin/org/pkl/lsp/type/ComputeDefinitionType.kt index 8d69f9a3..81a30e0e 100644 --- a/src/main/kotlin/org/pkl/lsp/type/ComputeDefinitionType.kt +++ b/src/main/kotlin/org/pkl/lsp/type/ComputeDefinitionType.kt @@ -34,7 +34,7 @@ fun PklNode?.computeResolvedImportType( return RecursionManager.doPreventingRecursion(this, "computeResolvedImportType", Type.Unknown) { when (this) { is PklModule -> Type.module(this, shortDisplayName, context) - is PklClass -> Type.Class(this) + is PklClass -> Type.Class.create(this) is PklTypeAlias -> Type.alias(this, context) is PklMethod -> when { diff --git a/src/main/kotlin/org/pkl/lsp/type/ComputeExprType.kt b/src/main/kotlin/org/pkl/lsp/type/ComputeExprType.kt index c4b359ad..0058b384 100644 --- a/src/main/kotlin/org/pkl/lsp/type/ComputeExprType.kt +++ b/src/main/kotlin/org/pkl/lsp/type/ComputeExprType.kt @@ -119,11 +119,13 @@ private fun PklNode.doComputeExprType( is PklOuterExpr -> Type.Unknown // TODO is PklSubscriptExpr -> { val receiverType = leftExpr.computeExprType(base, bindings, context) - doComputeSubscriptExprType(receiverType, base, context) + val getKeyType = { rightExpr.computeExprType(base, bindings, context) } + doComputeSubscriptExprType(receiverType, getKeyType, base, context) } is PklSuperSubscriptExpr -> { val receiverType = computeThisType(base, bindings, context) - doComputeSubscriptExprType(receiverType, base, context) + val getKeyType = { expr.computeExprType(base, bindings, context) } + doComputeSubscriptExprType(receiverType, getKeyType, base, context) } is PklEqualityExpr -> base.booleanType is PklComparisonExpr -> base.booleanType @@ -401,11 +403,13 @@ private fun PklNode.doComputeExprType( private fun doComputeSubscriptExprType( receiverType: Type, + getKeyType: () -> Type, base: PklBaseModule, context: PklProject?, ) = when (receiverType) { is Type.StringLiteral -> base.stringType + is Type.Reference -> receiverType.valueTypeForSubscriptKeyType(getKeyType(), base, context) else -> { val receiverClassType = receiverType.toClassType(base, context) when { diff --git a/src/main/kotlin/org/pkl/lsp/type/ComputeThisType.kt b/src/main/kotlin/org/pkl/lsp/type/ComputeThisType.kt index ec461c8c..e6a51e5b 100644 --- a/src/main/kotlin/org/pkl/lsp/type/ComputeThisType.kt +++ b/src/main/kotlin/org/pkl/lsp/type/ComputeThisType.kt @@ -78,7 +78,7 @@ fun PklNode.computeThisType( -> val visitor = ResolveVisitors.firstElementNamed(keyExpr.memberNameText, base, true) (keyExpr.resolve(base, null, bindings, visitor, context) as? PklClass)?.let { - return Type.Class(it) + return Type.Class.create(it) } } } diff --git a/src/main/kotlin/org/pkl/lsp/type/InferExprTypeFromContext.kt b/src/main/kotlin/org/pkl/lsp/type/InferExprTypeFromContext.kt index a6a8226e..9c212d96 100644 --- a/src/main/kotlin/org/pkl/lsp/type/InferExprTypeFromContext.kt +++ b/src/main/kotlin/org/pkl/lsp/type/InferExprTypeFromContext.kt @@ -97,11 +97,14 @@ private fun PklExpr?.doInferExprTypeFromContext( when (expr.parentOfType()?.name) { "converters" -> resolvedKeyClass?.let { - base.function1Type.withTypeArguments(Type.Class(it), base.anyType) + base.function1Type.withTypeArguments(Type.Class.create(it), base.anyType) } ?: defaultExpectedType "convertPropertyTransformers" -> resolvedKeyClass?.let { - base.function1Type.withTypeArguments(Type.Class(it), Type.Class(it)) + base.function1Type.withTypeArguments( + Type.Class.create(it), + Type.Class.create(it), + ) } ?: defaultExpectedType else -> defaultExpectedType } @@ -187,6 +190,7 @@ private fun PklExpr?.doInferExprTypeFromContext( return when (val unaliasedType = subscriptableType.unaliased(base, context)) { base.stringType -> base.intType base.dynamicType -> Type.Unknown + is Type.Reference -> unaliasedType.validSubscriptKeyType(base, context) is Type.Class -> { when { unaliasedType.classEquals(base.listType) -> base.intType diff --git a/src/main/kotlin/org/pkl/lsp/type/Types.kt b/src/main/kotlin/org/pkl/lsp/type/Types.kt index 53008249..efe352d2 100644 --- a/src/main/kotlin/org/pkl/lsp/type/Types.kt +++ b/src/main/kotlin/org/pkl/lsp/type/Types.kt @@ -128,7 +128,8 @@ sealed class Type(val constraints: List = listOf()) { ) fun union(types: List, base: PklBaseModule, context: PklProject?): Type = - types.reduce { t1, t2 -> Union.create(t1, t2, base, context) } + if (types.size == 1) types.single() + else types.reduce { t1, t2 -> Union.create(t1, t2, base, context) } fun function1(param1Type: Type, returnType: Type, base: PklBaseModule): Type = base.function1Type.withTypeArguments(param1Type, returnType) @@ -535,7 +536,211 @@ sealed class Type(val constraints: List = listOf()) { } } - class Class( + class Reference( + ctx: PklClass, + specifiedTypeArguments: List = listOf(), + constraints: List = listOf(), + ) : Class(ctx, specifiedTypeArguments, constraints) { + + private val domain: Type = typeArguments[0] + + private val referent: Type = typeArguments[1] + + private val referencesUnknown = referent is Unknown + + override fun withConstraints(constraints: List): Type = + Reference(ctx, typeArguments, constraints) + + override fun withTypeArguments(argument1: Type): Class = + Reference(ctx, listOf(argument1), constraints) + + override fun withTypeArguments(argument1: Type, argument2: Type): Class = + Reference(ctx, listOf(argument1, argument2), constraints) + + override fun withTypeArguments(arguments: List): Class = + Reference(ctx, arguments, constraints) + + // enforce restrictions! cannot reference: + // - external or local properties + // - properties of Listing or Mapping + // - Dynamic.default + // - properties of any external class + // - any Module.output + private fun isViable( + prop: PklClassProperty, + type: Type, + base: PklBaseModule, + context: PklProject?, + ): Boolean = + !(prop.isExternal || + prop.isLocal || + (type as? Class)?.let { + it == base.listingType || + it == base.mappingType || + (it == base.dynamicType && prop.name == "default") || + it.ctx.isExternal + } ?: false || + (type.isSubtypeOf(base.moduleType, base, context) && prop.name == "output")) + + override fun visitMembers( + isProperty: Boolean, + allowClasses: Boolean, + base: PklBaseModule, + visitor: ResolveVisitor<*>, + context: PklProject?, + ): Boolean { + fun visit(name: String, type: PklType): Boolean = + visitor.visit( + name, + PklReferenceQualifiedAccessProxyImpl(ctx.project, name, domain, type), + bindings, + context, + ) + + return when { + !isProperty -> super.visitMembers(false, allowClasses, base, visitor, context) + referencesUnknown -> + visit(visitor.exactName ?: "UNKNOWN", PklFakeUnknownTypeImpl(ctx.project)) + visitor.exactName != null -> { + var isUnknown = false + val candidates = mutableSetOf() + + walkCandidates(referent, base, context) { type, properties -> + for (prop in properties) { + if (isViable(prop, type, base, context) && prop.name == visitor.exactName) { + val propType = prop.type ?: PklFakeUnknownTypeImpl(ctx.project) + if (propType is PklUnknownType) { + isUnknown = true + return@walkCandidates false + } + candidates.add(propType) + return@walkCandidates true + } + } + return@walkCandidates true + } + + if (isUnknown) visit(visitor.exactName!!, PklFakeUnknownTypeImpl(ctx.project)) + else if (!candidates.isEmpty()) + visit( + visitor.exactName!!, + PklFakeUnionTypeImpl.create(ctx.project, candidates.toList()), + ) + else true + } + else -> { + val propertyCandidates = mutableMapOf>() + walkCandidates(referent, base, context) { type, properties -> + for (prop in properties) { + if (isViable(prop, type, base, context)) { + propertyCandidates + .getOrPut(prop.name) { mutableSetOf() } + .add(prop.type ?: PklFakeUnknownTypeImpl(ctx.project)) + } + } + return@walkCandidates true + } + for ((propName, candidates) in propertyCandidates) { + when { + candidates.any { it is PklUnknownType } -> + visit(propName, PklFakeUnknownTypeImpl(ctx.project)) + else -> visit(propName, PklFakeUnionTypeImpl.create(ctx.project, candidates.toList())) + } + } + true + } + } + } + + fun valueTypeForSubscriptKeyType( + keyType: Type, + base: PklBaseModule, + context: PklProject?, + ): Type { + if (referencesUnknown) return this + val keyClass = keyType.toClassType(base, context) + var isUnknown = false + val candidates = mutableSetOf() + walkCandidates(referent, base, context) { type, _ -> + if (type !is Class) return@walkCandidates true + when { + type.classEquals(base.dynamicType) -> { + isUnknown = true + return@walkCandidates false + } + (type.classEquals(base.listingType) || type.classEquals(base.listType)) && + (keyType is Unknown || keyClass?.classEquals(base.intType) == true) -> + if (type.typeArguments.single() is Unknown) { + isUnknown = true + return@walkCandidates false + } else candidates.add(type.typeArguments.single()) + (type.classEquals(base.mappingType) || type.classEquals(base.mapType)) && + (type.typeArguments.first() is Unknown || + keyClass?.isSubtypeOf(type.typeArguments.first(), base, context) == true) -> + if (type.typeArguments.last() is Unknown) { + isUnknown = true + return@walkCandidates false + } else candidates.add(type.typeArguments.last()) + } + return@walkCandidates true + } + + if (isUnknown) return withTypeArguments(domain, Unknown) + if (candidates.isEmpty()) return Nothing + return withTypeArguments(domain, union(candidates.toList(), base, context)) + } + + fun validSubscriptKeyType(base: PklBaseModule, context: PklProject?): Type { + if (referencesUnknown) return Unknown + var isUnknown = false + val keyCandidates = mutableSetOf() + walkCandidates(referent, base, context) { type, _ -> + if (type !is Class) return@walkCandidates true + when { + type.classEquals(base.dynamicType) -> { + isUnknown = true + return@walkCandidates false + } + (type.classEquals(base.listingType) || type.classEquals(base.listType)) -> + keyCandidates.add(base.intType) + (type.classEquals(base.mappingType) || type.classEquals(base.mapType)) -> + keyCandidates.add(type.typeArguments.first()) + } + return@walkCandidates true + } + + if (isUnknown) return Unknown + if (keyCandidates.isEmpty()) return Nothing + return union(keyCandidates.toList(), base, context) + } + + companion object { + fun walkCandidates( + root: Type, + base: PklBaseModule, + context: PklProject?, + visit: (Type, List) -> Boolean, + ): Boolean = + when (root) { + is Alias -> { + val aliased = root.aliasedType(base, context) + if (root.ctx.isInPklBaseModule && (aliased as? Class)?.ctx?.name == "Int") + walkCandidates(root, base, context, visit) + else walkCandidates(aliased, base, context, visit) + } + is Class -> visit(root, root.ctx.properties) + is Module -> visit(root, root.ctx.properties) + is Union -> { + walkCandidates(root.leftType, base, context, visit) && + walkCandidates(root.rightType, base, context, visit) + } + else -> true + } + } + } + + open class Class + protected constructor( val ctx: PklClass, specifiedTypeArguments: List = listOf(), constraints: List = listOf(), @@ -544,6 +749,20 @@ sealed class Type(val constraints: List = listOf()) { private val typeParameters: List = ctx.typeParameterList?.typeParameters ?: listOf(), ) : Type(constraints) { + companion object { + fun create( + ctx: PklClass, + specifiedTypeArguments: List = listOf(), + constraints: List = listOf(), + // enables the illusion that pkl.base#Class and pkl.base#TypeAlias + // have a type parameter even though they currently don't + typeParameters: List = ctx.typeParameterList?.typeParameters ?: listOf(), + ): Class = + if (ctx.name == "Reference" && ctx.isInPklRefModule) + Reference(ctx, specifiedTypeArguments, constraints) + else Class(ctx, specifiedTypeArguments, constraints, typeParameters) + } + val typeArguments: List = when { typeParameters.size <= specifiedTypeArguments.size -> @@ -558,13 +777,13 @@ sealed class Type(val constraints: List = listOf()) { override fun withConstraints(constraints: List): Type = Class(ctx, typeArguments, constraints, typeParameters) - fun withTypeArguments(argument1: Type) = + open fun withTypeArguments(argument1: Type) = Class(ctx, listOf(argument1), constraints, typeParameters) - fun withTypeArguments(argument1: Type, argument2: Type) = + open fun withTypeArguments(argument1: Type, argument2: Type) = Class(ctx, listOf(argument1, argument2), constraints, typeParameters) - fun withTypeArguments(arguments: List) = + open fun withTypeArguments(arguments: List) = Class(ctx, arguments, constraints, typeParameters) override fun visitMembers( @@ -838,8 +1057,7 @@ sealed class Type(val constraints: List = listOf()) { visitor: ResolveVisitor<*>, context: PklProject?, ): Boolean { - return ctx.type - .toType(base, bindings, context) + return aliasedType(base, context) .visitMembers(isProperty, allowClasses, base, visitor, context) } @@ -987,6 +1205,8 @@ sealed class Type(val constraints: List = listOf()) { fun render(startDelimiter: String) = buildString { render(this, startDelimiter) } override fun toString(): String = "\"$value\"" + + override fun toClassType(base: PklBaseModule, context: PklProject?): Class = base.stringType } class Union @@ -1139,7 +1359,7 @@ sealed class Type(val constraints: List = listOf()) { } leftType.render(builder, nameRenderer) - builder.append('|') + builder.append(" | ") rightType.render(builder, nameRenderer) } @@ -1175,7 +1395,7 @@ fun PklType?.toType( is PklModule -> Type.module(resolved, simpleName.identifierName!!, context) is PklClass -> { val typeArguments = this.typeArgumentList?.types ?: listOf() - Type.Class( + Type.Class.create( resolved, typeArguments.toTypes(base, bindings, context, preserveUnboundTypeVars), ) diff --git a/src/main/resources/org/pkl/lsp/errorMessages.properties b/src/main/resources/org/pkl/lsp/errorMessages.properties index 4b9bfd2c..d062f789 100644 --- a/src/main/resources/org/pkl/lsp/errorMessages.properties +++ b/src/main/resources/org/pkl/lsp/errorMessages.properties @@ -211,3 +211,6 @@ incorrectTypeArgumentCount=\ Incorrect type argument count.\n\ Required: 0 or {0}\n\ Actual: {1} + +invalidReferenceTypeWithConstraint=\ +`pkl.ref#Reference` type annotations may not contain type constraints. diff --git a/src/test/files/DiagnosticsSnippetTests/inputs/type/referenceTypeConstraint.pkl b/src/test/files/DiagnosticsSnippetTests/inputs/type/referenceTypeConstraint.pkl new file mode 100644 index 00000000..ca9f7954 --- /dev/null +++ b/src/test/files/DiagnosticsSnippetTests/inputs/type/referenceTypeConstraint.pkl @@ -0,0 +1,10 @@ +hidden a: String +foo = Reference(a) as Reference +bar = Reference(a) as Reference +baz = Reference(a) as Reference> +qux = Reference(a) as Reference + +typealias Alias1 = Int | Alias2? +typealias Alias2 = String(length < 5) + +xxx: Reference = Reference(a) diff --git a/src/test/files/DiagnosticsSnippetTests/outputs/type/referenceTypeConstraint.txt b/src/test/files/DiagnosticsSnippetTests/outputs/type/referenceTypeConstraint.txt new file mode 100644 index 00000000..3b27abad --- /dev/null +++ b/src/test/files/DiagnosticsSnippetTests/outputs/type/referenceTypeConstraint.txt @@ -0,0 +1,31 @@ + hidden a: String + foo = Reference(a) as Reference + ^^^^^^^^^ +| Error: Unresolved reference `Reference` + ^^^^^^^^^ +| Error: Unresolved reference `Reference` + bar = Reference(a) as Reference + ^^^^^^^^^ +| Error: Unresolved reference `Reference` + ^^^^^^^^^ +| Error: Unresolved reference `Reference` + baz = Reference(a) as Reference> + ^^^^^^^^^ +| Error: Unresolved reference `Reference` + ^^^^^^^^^ +| Error: Unresolved reference `Reference` + qux = Reference(a) as Reference + ^^^^^^^^^ +| Error: Unresolved reference `Reference` + ^^^^^^^^^ +| Error: Unresolved reference `Reference` + + typealias Alias1 = Int | Alias2? + typealias Alias2 = String(length < 5) + + xxx: Reference = Reference(a) + ^^^^^^^^^ +| Error: Unresolved reference `Reference` + ^^^^^^^^^ +| Error: Unresolved reference `Reference` + \ No newline at end of file diff --git a/src/test/files/DiagnosticsSnippetTests/outputs/typecheck/nullabilityMismatch.txt b/src/test/files/DiagnosticsSnippetTests/outputs/typecheck/nullabilityMismatch.txt index ceadb460..6cdeb73c 100644 --- a/src/test/files/DiagnosticsSnippetTests/outputs/typecheck/nullabilityMismatch.txt +++ b/src/test/files/DiagnosticsSnippetTests/outputs/typecheck/nullabilityMismatch.txt @@ -4,7 +4,7 @@ ...foo ^^^ | Warning: Nullability mismatch. -| Required: Collection|Map|Dynamic|Listing|Mapping|IntSeq|Bytes +| Required: Collection | Map | Dynamic | Listing | Mapping | IntSeq | Bytes | Actual: Listing? } @@ -12,7 +12,7 @@ for (elem in foo) { ^^^ | Warning: Nullability mismatch. -| Required: Collection|Map|Dynamic|Listing|Mapping|IntSeq|Bytes +| Required: Collection | Map | Dynamic | Listing | Mapping | IntSeq | Bytes | Actual: Listing? "hi \(elem)" } diff --git a/src/test/files/DiagnosticsSnippetTests/outputs/typecheck/spreadSyntax.txt b/src/test/files/DiagnosticsSnippetTests/outputs/typecheck/spreadSyntax.txt index 00022b28..55ecf1c3 100644 --- a/src/test/files/DiagnosticsSnippetTests/outputs/typecheck/spreadSyntax.txt +++ b/src/test/files/DiagnosticsSnippetTests/outputs/typecheck/spreadSyntax.txt @@ -10,7 +10,7 @@ ...bar ^^^ | Warning: Nullability mismatch. -| Required: Collection|Map|Dynamic|Listing|Mapping|IntSeq|Bytes +| Required: Collection | Map | Dynamic | Listing | Mapping | IntSeq | Bytes | Actual: Mapping? } @@ -18,7 +18,7 @@ ...myNum ^^^^^ | Error: Type mismatch. -| Required: Collection|Map|Dynamic|Listing|Mapping|IntSeq|Bytes +| Required: Collection | Map | Dynamic | Listing | Mapping | IntSeq | Bytes | Actual: Int } @@ -30,6 +30,6 @@ ...Bytes(1, 2, 3) ^^^^^^^^^^^^^^ | Error: Type mismatch. -| Required: Collection|Listing|Dynamic +| Required: Collection | Listing | Dynamic | Actual: Bytes } \ No newline at end of file diff --git a/src/test/files/DiagnosticsSnippetTests/outputs/typecheck/unions.txt b/src/test/files/DiagnosticsSnippetTests/outputs/typecheck/unions.txt index e40ca805..64853e50 100644 --- a/src/test/files/DiagnosticsSnippetTests/outputs/typecheck/unions.txt +++ b/src/test/files/DiagnosticsSnippetTests/outputs/typecheck/unions.txt @@ -1,7 +1,7 @@ foo: Int|String = true ^^^^ | Error: Type mismatch. -| Required: Int|String +| Required: Int | String | Actual: Boolean bar: Int = null