diff --git a/src/main/kotlin/org/pkl/lsp/analyzers/MemberAnalyzer.kt b/src/main/kotlin/org/pkl/lsp/analyzers/MemberAnalyzer.kt index f21e867a..fcdef0eb 100644 --- a/src/main/kotlin/org/pkl/lsp/analyzers/MemberAnalyzer.kt +++ b/src/main/kotlin/org/pkl/lsp/analyzers/MemberAnalyzer.kt @@ -19,10 +19,16 @@ import org.eclipse.lsp4j.DiagnosticSeverity import org.pkl.lsp.ErrorMessages import org.pkl.lsp.PklBaseModule import org.pkl.lsp.Project +import org.pkl.lsp.ast.PklClassMember +import org.pkl.lsp.ast.PklClassMethod import org.pkl.lsp.ast.PklClassProperty +import org.pkl.lsp.ast.PklExpr import org.pkl.lsp.ast.PklNode import org.pkl.lsp.ast.PklObjectProperty import org.pkl.lsp.ast.PklProperty +import org.pkl.lsp.ast.Span +import org.pkl.lsp.ast.Terminal +import org.pkl.lsp.ast.TokenType import org.pkl.lsp.packages.dto.PklProject import org.pkl.lsp.resolvers.ResolveVisitors import org.pkl.lsp.resolvers.Resolvers @@ -56,6 +62,10 @@ class MemberAnalyzer(project: Project) : Analyzer(project) { } is PklClassProperty -> { checkUnresolvedProperty(node, memberType, base, diagnosticsHolder, context) + checkAbstractMemberWithBody(node, diagnosticsHolder) + } + is PklClassMethod -> { + checkAbstractMemberWithBody(node, diagnosticsHolder) } } return true @@ -100,4 +110,33 @@ class MemberAnalyzer(project: Project) : Analyzer(project) { } } } + + private fun checkAbstractMemberWithBody( + node: PklClassMember, + diagnosticsHolder: DiagnosticsHolder, + ) { + if (!node.isAbstract) return + val bodySpan = + if (node is PklClassProperty) { + node.expr?.spanWithAssign ?: node.objectBody?.span + } else { + node as PklClassMethod + node.body?.spanWithAssign + } + if (bodySpan != null) { + diagnosticsHolder.addDiagnostic( + node, + "Abstract member cannot have a body", + bodySpan, + DiagnosticSeverity.Error, + ) + } + } + + private val PklExpr.spanWithAssign: Span? + get() { + val assignNode = + this.prevSiblingMatching { it is Terminal && it.type == TokenType.ASSIGN } ?: return null + return assignNode.span.endAt(span) + } } diff --git a/src/main/kotlin/org/pkl/lsp/ast/PklNode.kt b/src/main/kotlin/org/pkl/lsp/ast/PklNode.kt index f7167924..9ac7ea06 100644 --- a/src/main/kotlin/org/pkl/lsp/ast/PklNode.kt +++ b/src/main/kotlin/org/pkl/lsp/ast/PklNode.kt @@ -82,6 +82,14 @@ interface PklNode { val parentNode = parent ?: return null return parentNode.children[index - 1] } + + fun prevSiblingMatching(filter: (PklNode) -> Boolean): PklNode? { + var node = prevSibling() + while (node != null && !filter(node)) { + node = node.prevSibling() + } + return node + } } sealed interface PklSuppressWarningsTarget : PklNode { diff --git a/src/test/files/DiagnosticsSnippetTests/inputs/member/abstractMemberWithBody.pkl b/src/test/files/DiagnosticsSnippetTests/inputs/member/abstractMemberWithBody.pkl new file mode 100644 index 00000000..1e3b8e95 --- /dev/null +++ b/src/test/files/DiagnosticsSnippetTests/inputs/member/abstractMemberWithBody.pkl @@ -0,0 +1,7 @@ +abstract module MyMod + +abstract foo = 1 + +abstract bar {} + +abstract function foo() = 1 diff --git a/src/test/files/DiagnosticsSnippetTests/outputs/member/abstractMemberWithBody.txt b/src/test/files/DiagnosticsSnippetTests/outputs/member/abstractMemberWithBody.txt new file mode 100644 index 00000000..ee538c17 --- /dev/null +++ b/src/test/files/DiagnosticsSnippetTests/outputs/member/abstractMemberWithBody.txt @@ -0,0 +1,14 @@ + abstract module MyMod + + abstract foo = 1 + ^^^ +| Error: Abstract member cannot have a body + + abstract bar {} + ^^ +| Error: Abstract member cannot have a body + + abstract function foo() = 1 + ^^^ +| Error: Abstract member cannot have a body + \ No newline at end of file diff --git a/src/test/kotlin/org/pkl/lsp/ModifierQuickfixTest.kt b/src/test/kotlin/org/pkl/lsp/ModifierQuickfixTest.kt index b3b76f8d..44115824 100644 --- a/src/test/kotlin/org/pkl/lsp/ModifierQuickfixTest.kt +++ b/src/test/kotlin/org/pkl/lsp/ModifierQuickfixTest.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2025-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. @@ -53,7 +53,7 @@ class ModifierQuickfixTest : LspTestBase() { """ amends "pkl:test" - abstract external foo: String = "Hello" + const foo: String = "Hello" """ .trimIndent() ) @@ -62,13 +62,12 @@ class ModifierQuickfixTest : LspTestBase() { val action = diagnostic.actions.find { it.title == "Add modifier 'local'" } assertThat(action).isNotNull runAction(action!!.toMessage(diagnostic)) - // modifier gets inserted in between 'abstract' and 'external' assertThat(file.contents) .isEqualTo( """ amends "pkl:test" - abstract local external foo: String = "Hello" + local const foo: String = "Hello" """ .trimIndent() )