plugin and formatter improvements

This commit is contained in:
Sergey Chernov 2026-01-17 07:33:30 +03:00
parent f848d79d39
commit 0759346e4b
5 changed files with 152 additions and 35 deletions

View File

@ -122,6 +122,24 @@ class Compiler(
return doc 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 // Set just before entering a declaration parse, taken from keyword token position
private var pendingDeclStart: Pos? = null private var pendingDeclStart: Pos? = null
private var pendingDeclDoc: MiniDoc? = null private var pendingDeclDoc: MiniDoc? = null
@ -320,9 +338,15 @@ class Compiler(
} }
Token.Type.LABEL -> continue 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 Token.Type.SEMICOLON -> continue
@ -1345,7 +1369,7 @@ class Compiler(
modifiers.add(currentToken.value) modifiers.add(currentToken.value)
val next = cc.peekNextNonWhitespace() val next = cc.peekNextNonWhitespace()
if (next.type == Token.Type.ID || next.type == Token.Type.OBJECT) { if (next.type == Token.Type.ID || next.type == Token.Type.OBJECT) {
currentToken = cc.next() currentToken = nextNonWhitespace()
} else { } else {
break break
} }
@ -1373,7 +1397,9 @@ class Compiler(
throw ScriptError(currentToken.pos, "abstract members cannot be private") throw ScriptError(currentToken.pos, "abstract members cannot be private")
pendingDeclStart = firstId.pos 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) val isMember = (codeContexts.lastOrNull() is CodeContext.ClassBody)
@ -1900,27 +1926,31 @@ class Compiler(
// skip '{' // skip '{'
cc.skipTokenOfType(Token.Type.LBRACE) cc.skipTokenOfType(Token.Type.LBRACE)
do { if (cc.peekNextNonWhitespace().type != Token.Type.RBRACE) {
val t = cc.nextNonWhitespace() do {
when (t.type) { val t = cc.nextNonWhitespace()
Token.Type.ID -> { when (t.type) {
names += t.value Token.Type.ID -> {
positions += t.pos names += t.value
val t1 = cc.nextNonWhitespace() positions += t.pos
when (t1.type) { val t1 = cc.nextNonWhitespace()
Token.Type.COMMA -> when (t1.type) {
continue Token.Type.COMMA ->
continue
Token.Type.RBRACE -> break Token.Type.RBRACE -> break
else -> { else -> {
t1.raiseSyntax("unexpected token") t1.raiseSyntax("unexpected token")
}
} }
} }
}
else -> t.raiseSyntax("expected enum entry name") else -> t.raiseSyntax("expected enum entry name")
} }
} while (true) } while (true)
} else {
cc.nextNonWhitespace()
}
miniSink?.onEnumDecl( miniSink?.onEnumDecl(
MiniEnumDecl( MiniEnumDecl(

View File

@ -108,8 +108,9 @@ object LyngFormatter {
if (t == "finally") return true if (t == "finally") return true
// Short definition form: fun x() = or val x = // Short definition form: fun x() = or val x =
if (Regex("^(override\\s+)?(fun|fn)\\b.*=\\s*$").matches(t)) return true // We allow optional 'static' as well
if (Regex("^(private\\s+|protected\\s+|public\\s+|override\\s+)?(val|var)\\b.*=\\s*$").matches(t)) return true 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 = // property accessors ending with ) or =
if (isAccessorRelated(t)) { if (isAccessorRelated(t)) {
@ -122,7 +123,8 @@ object LyngFormatter {
val t = s.trim() val t = s.trim()
if (!t.endsWith("=")) return false if (!t.endsWith("=")) return false
// Is it a function or property definition? // 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? // Is it an accessor?
if (isPropertyAccessor(t)) return true if (isPropertyAccessor(t)) return true
return false return false

View File

@ -44,13 +44,22 @@ data class MiniDoc(
fun parse(range: MiniRange, lines: Iterable<String>, extraTags: Map<String, List<String>> = emptyMap()): MiniDoc { fun parse(range: MiniRange, lines: Iterable<String>, extraTags: Map<String, List<String>> = emptyMap()): MiniDoc {
val parsedTags = mutableMapOf<String, MutableList<String>>() val parsedTags = mutableMapOf<String, MutableList<String>>()
var currentTag: String? = null var currentTag: String? = null
val currentContent = StringBuilder() val currentContent = mutableListOf<String>()
fun flush() { fun flush() {
currentTag?.let { tag -> 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<String>() val descriptionLines = mutableListOf<String>()
@ -58,33 +67,45 @@ data class MiniDoc(
for (rawLine in lines) { for (rawLine in lines) {
for (line in rawLine.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("@")) { if (trimmed.startsWith("@")) {
inTags = true inTags = true
flush() flush()
val parts = trimmed.substring(1).split(Regex("\\s+"), 2) val parts = trimmed.substring(1).split(Regex("\\s+"), 2)
currentTag = parts[0] 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 { } else {
if (inTags) { if (inTags) {
if (currentContent.isNotEmpty()) currentContent.append("\n") currentContent.add(cleanedLine)
currentContent.append(line)
} else { } else {
descriptionLines.add(line) descriptionLines.add(cleanedLine)
} }
} }
} }
} }
flush() flush()
val raw = descriptionLines.joinToString("\n").trimEnd() val raw = descriptionLines.joinToString("\n").trimIndent().trim()
val summary = raw.lines().firstOrNull { it.isNotBlank() }?.trim() val summary = raw.lines().firstOrNull { it.isNotBlank() }?.trim()
val finalTags = parsedTags.toMutableMap() val finalTags = parsedTags.toMutableMap()
extraTags.forEach { (k, v) -> extraTags.forEach { (k, v) ->
finalTags.getOrPut(k) { mutableListOf() }.addAll(v) finalTags.getOrPut(k) { mutableListOf() }.addAll(v)
} }
return MiniDoc(range, raw, summary, finalTags) return MiniDoc(range, raw, summary, finalTags)
} }
} }

View File

@ -528,4 +528,52 @@ class MiniAstTest {
assertTrue(xParam.contains("first line of x"), "should contain first line") assertTrue(xParam.contains("first line of x"), "should contain first line")
assertTrue(xParam.contains("second line of x"), "should contain second 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<MiniEnumDecl>().firstOrNull { it.name == "E" }
assertNotNull(en)
assertNotNull(en.doc)
assertEquals("Enum doc", en.doc.summary)
val cl = mini.declarations.filterIsInstance<MiniClassDecl>().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<MiniClassDecl>().firstOrNull { it.name == "X" }
assertNotNull(cls)
assertTrue(cls.members.any { it.name == "f" })
}
} }

View File

@ -882,6 +882,22 @@ class LyngFormatterTest {
} }
""".trimIndent() """.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 cfg = LyngFormatConfig(indentSize = 4, continuationIndentSize = 4)
val out = LyngFormatter.reindent(src, cfg) val out = LyngFormatter.reindent(src, cfg)
assertEquals(expected, out) assertEquals(expected, out)