From 0759346e4b29f5a64b7566e8d3a48ee806b71593 Mon Sep 17 00:00:00 2001 From: sergeych Date: Sat, 17 Jan 2026 07:33:30 +0300 Subject: [PATCH] plugin and formatter improvements --- .../kotlin/net/sergeych/lyng/Compiler.kt | 72 +++++++++++++------ .../net/sergeych/lyng/format/LyngFormatter.kt | 8 ++- .../net/sergeych/lyng/miniast/MiniAst.kt | 43 ++++++++--- lynglib/src/commonTest/kotlin/MiniAstTest.kt | 48 +++++++++++++ .../sergeych/lyng/format/LyngFormatterTest.kt | 16 +++++ 5 files changed, 152 insertions(+), 35 deletions(-) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 91c71af..d2a587a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -122,6 +122,24 @@ class Compiler( return doc } + private fun nextNonWhitespace(): Token { + while (true) { + val t = cc.next() + when (t.type) { + Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> { + pushPendingDocToken(t) + } + + Token.Type.NEWLINE -> { + if (!prevWasComment) clearPendingDoc() else prevWasComment = false + } + + Token.Type.EOF -> return t + else -> return t + } + } + } + // Set just before entering a declaration parse, taken from keyword token position private var pendingDeclStart: Pos? = null private var pendingDeclDoc: MiniDoc? = null @@ -320,9 +338,15 @@ class Compiler( } Token.Type.LABEL -> continue - Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> continue + Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> { + pushPendingDocToken(t) + continue + } - Token.Type.NEWLINE -> continue + Token.Type.NEWLINE -> { + if (!prevWasComment) clearPendingDoc() else prevWasComment = false + continue + } Token.Type.SEMICOLON -> continue @@ -1345,7 +1369,7 @@ class Compiler( modifiers.add(currentToken.value) val next = cc.peekNextNonWhitespace() if (next.type == Token.Type.ID || next.type == Token.Type.OBJECT) { - currentToken = cc.next() + currentToken = nextNonWhitespace() } else { break } @@ -1373,7 +1397,9 @@ class Compiler( throw ScriptError(currentToken.pos, "abstract members cannot be private") pendingDeclStart = firstId.pos - pendingDeclDoc = consumePendingDoc() + // pendingDeclDoc might be already set by an annotation + if (pendingDeclDoc == null) + pendingDeclDoc = consumePendingDoc() val isMember = (codeContexts.lastOrNull() is CodeContext.ClassBody) @@ -1900,27 +1926,31 @@ class Compiler( // skip '{' cc.skipTokenOfType(Token.Type.LBRACE) - do { - val t = cc.nextNonWhitespace() - when (t.type) { - Token.Type.ID -> { - names += t.value - positions += t.pos - val t1 = cc.nextNonWhitespace() - when (t1.type) { - Token.Type.COMMA -> - continue + if (cc.peekNextNonWhitespace().type != Token.Type.RBRACE) { + do { + val t = cc.nextNonWhitespace() + when (t.type) { + Token.Type.ID -> { + names += t.value + positions += t.pos + val t1 = cc.nextNonWhitespace() + when (t1.type) { + Token.Type.COMMA -> + continue - Token.Type.RBRACE -> break - else -> { - t1.raiseSyntax("unexpected token") + Token.Type.RBRACE -> break + else -> { + t1.raiseSyntax("unexpected token") + } } } - } - else -> t.raiseSyntax("expected enum entry name") - } - } while (true) + else -> t.raiseSyntax("expected enum entry name") + } + } while (true) + } else { + cc.nextNonWhitespace() + } miniSink?.onEnumDecl( MiniEnumDecl( diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/format/LyngFormatter.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/format/LyngFormatter.kt index 9023b26..95bae67 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/format/LyngFormatter.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/format/LyngFormatter.kt @@ -108,8 +108,9 @@ object LyngFormatter { if (t == "finally") return true // Short definition form: fun x() = or val x = - if (Regex("^(override\\s+)?(fun|fn)\\b.*=\\s*$").matches(t)) return true - if (Regex("^(private\\s+|protected\\s+|public\\s+|override\\s+)?(val|var)\\b.*=\\s*$").matches(t)) return true + // We allow optional 'static' as well + if (Regex("^(static\\s+)?(override\\s+)?(fun|fn)\\b.*=\\s*$").matches(t)) return true + if (Regex("^(static\\s+)?(private\\s+|protected\\s+|public\\s+|override\\s+)?(val|var)\\b.*=\\s*$").matches(t)) return true // property accessors ending with ) or = if (isAccessorRelated(t)) { @@ -122,7 +123,8 @@ object LyngFormatter { val t = s.trim() if (!t.endsWith("=")) return false // Is it a function or property definition? - if (Regex("\\b(fun|fn|val|var)\\b").containsMatchIn(t)) return true + // (Note: we exclude 'static' here to avoid double indent if it's already handled) + if (Regex("^(override\\s+)?(fun|fn|val|var)\\b").containsMatchIn(t)) return true // Is it an accessor? if (isPropertyAccessor(t)) return true return false diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt index 8280651..4f67ab2 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt @@ -44,13 +44,22 @@ data class MiniDoc( fun parse(range: MiniRange, lines: Iterable, extraTags: Map> = emptyMap()): MiniDoc { val parsedTags = mutableMapOf>() var currentTag: String? = null - val currentContent = StringBuilder() + val currentContent = mutableListOf() fun flush() { currentTag?.let { tag -> - parsedTags.getOrPut(tag) { mutableListOf() }.add(currentContent.toString().trim()) + val trimmedContent = if (currentContent.size > 1) { + // First line is common content, we want to trim indent from subsequent lines + // based on THEIR common indent. + val firstLine = currentContent[0] + val remaining = currentContent.drop(1).joinToString("\n").trimIndent() + if (remaining.isEmpty()) firstLine else firstLine + "\n" + remaining + } else { + currentContent.joinToString("\n") + } + parsedTags.getOrPut(tag) { mutableListOf() }.add(trimmedContent.trim()) } - currentContent.setLength(0) + currentContent.clear() } val descriptionLines = mutableListOf() @@ -58,33 +67,45 @@ data class MiniDoc( for (rawLine in lines) { for (line in rawLine.lines()) { - val trimmed = line.trim() + // Strip leading '*' and space if present (standard doc comment style) + val cleanedLine = line.trim().let { + if (it.startsWith("*")) it.substring(1).removePrefix(" ") else line + } + val trimmed = cleanedLine.trim() if (trimmed.startsWith("@")) { inTags = true flush() val parts = trimmed.substring(1).split(Regex("\\s+"), 2) currentTag = parts[0] - currentContent.append(parts.getOrNull(1) ?: "") + val tagFirstLine = parts.getOrNull(1) ?: "" + if (tagFirstLine.isNotEmpty()) { + // Find where the content starts in the cleaned line to preserve relative indentation + val contentIndex = cleanedLine.indexOf(tagFirstLine) + if (contentIndex >= 0) { + currentContent.add(cleanedLine.substring(contentIndex)) + } else { + currentContent.add(tagFirstLine) + } + } } else { if (inTags) { - if (currentContent.isNotEmpty()) currentContent.append("\n") - currentContent.append(line) + currentContent.add(cleanedLine) } else { - descriptionLines.add(line) + descriptionLines.add(cleanedLine) } } } } flush() - val raw = descriptionLines.joinToString("\n").trimEnd() + val raw = descriptionLines.joinToString("\n").trimIndent().trim() val summary = raw.lines().firstOrNull { it.isNotBlank() }?.trim() - + val finalTags = parsedTags.toMutableMap() extraTags.forEach { (k, v) -> finalTags.getOrPut(k) { mutableListOf() }.addAll(v) } - + return MiniDoc(range, raw, summary, finalTags) } } diff --git a/lynglib/src/commonTest/kotlin/MiniAstTest.kt b/lynglib/src/commonTest/kotlin/MiniAstTest.kt index f5af2b7..ca9b9d5 100644 --- a/lynglib/src/commonTest/kotlin/MiniAstTest.kt +++ b/lynglib/src/commonTest/kotlin/MiniAstTest.kt @@ -528,4 +528,52 @@ class MiniAstTest { assertTrue(xParam.contains("first line of x"), "should contain first line") assertTrue(xParam.contains("second line of x"), "should contain second line") } + + @Test + fun enum_minidocs_and_semicolon_robustness() = runTest { + val code = """ + /** Enum doc */ + enum E { A }; + + /** Next doc */ + class C {} + """.trimIndent() + val (_, sink) = compileWithMini(code) + val mini = sink.build() + assertNotNull(mini) + + val en = mini.declarations.filterIsInstance().firstOrNull { it.name == "E" } + assertNotNull(en) + assertNotNull(en.doc) + assertEquals("Enum doc", en.doc.summary) + + val cl = mini.declarations.filterIsInstance().firstOrNull { it.name == "C" } + assertNotNull(cl) + assertNotNull(cl.doc) + assertEquals("Next doc", cl.doc.summary) + } + + @Test + fun empty_enum_support() = runTest { + val code = "enum E {}" + val (_, sink) = compileWithMini(code) + val mini = sink.build() + assertNotNull(mini) + assertTrue(mini.declarations.any { it.name == "E" && it is MiniEnumDecl }) + } + + @Test + fun modifiers_with_comment_robustness() = runTest { + val code = """ + class X { + static /** doc */ fun f() {} + } + """.trimIndent() + val (_, sink) = compileWithMini(code) + val mini = sink.build() + assertNotNull(mini) + val cls = mini.declarations.filterIsInstance().firstOrNull { it.name == "X" } + assertNotNull(cls) + assertTrue(cls.members.any { it.name == "f" }) + } } diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/format/LyngFormatterTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/format/LyngFormatterTest.kt index 213a5d9..e447520 100644 --- a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/format/LyngFormatterTest.kt +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/format/LyngFormatterTest.kt @@ -882,6 +882,22 @@ class LyngFormatterTest { } """.trimIndent() + val cfg = LyngFormatConfig(indentSize = 4, continuationIndentSize = 4) + val out = LyngFormatter.reindent(src, cfg) + assertEquals(expected, out) + } + @Test + fun multlinedShortFunIndentation() { + val src = """ + static fun create(walletId, trId, amount) = + Cells.createNew(Commission(amount) => ["commission", "w:" + walletId, "tr:" + trId, createdTag(), "pending"]) + """.trimIndent() + + val expected = """ + static fun create(walletId, trId, amount) = + Cells.createNew(Commission(amount) => ["commission", "w:" + walletId, "tr:" + trId, createdTag(), "pending"]) + """.trimIndent() + val cfg = LyngFormatConfig(indentSize = 4, continuationIndentSize = 4) val out = LyngFormatter.reindent(src, cfg) assertEquals(expected, out)