plugin and formatter improvements

This commit is contained in:
sergeych committed 2026-01-17 07:33:30 +03:00
1 parent f848d79d39
commit 0759346e4b
5 files changed
+150 -33

No files matched your search

@@ -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(
@@ -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
@@ -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,26 +67,38 @@ 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()
@@ -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" })
}
} }
@@ -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)