idea plugin 0.0.2-SNAPSHOT, improced, added reformat code. Formatting tools improved in lynglib. Site information added
This commit is contained in:
1 parent
06e8e1579d
commit
ec49bbbf52
19 files changed
+530
-54
No files matched your search
@@ -198,6 +198,58 @@ object Binder {
|
||||
}
|
||||
}
|
||||
|
||||
// Extra pass: detect local val/var declarations that are not present in mini.declarations
|
||||
// (e.g., locals inside blocks and top-level statements). Use token stream heuristics.
|
||||
run {
|
||||
val spans = highlighter.highlight(text)
|
||||
var iTok = 0
|
||||
while (iTok < spans.size) {
|
||||
val t = spans[iTok]
|
||||
if (t.kind == HighlightKind.Keyword) {
|
||||
val kw = try { text.substring(t.range.start, t.range.endExclusive) } catch (_: Throwable) { "" }
|
||||
if (kw.equals("val", ignoreCase = true) || kw.equals("var", ignoreCase = true)) {
|
||||
// Find the next Identifier span which should be the declared name
|
||||
var j = iTok + 1
|
||||
var nameStart = -1
|
||||
var nameEnd = -1
|
||||
while (j < spans.size) {
|
||||
val s = spans[j]
|
||||
if (s.kind == HighlightKind.Identifier) {
|
||||
nameStart = s.range.start
|
||||
nameEnd = s.range.endExclusive
|
||||
break
|
||||
}
|
||||
// Stop if we hit newline/comment too far ahead (avoid wide scans)
|
||||
if (s.kind == HighlightKind.Comment) break
|
||||
j++
|
||||
}
|
||||
if (nameStart >= 0 && nameEnd > nameStart) {
|
||||
// Skip if a symbol with exactly this decl range already exists
|
||||
val exists = symbols.any { it.declStart == nameStart && it.declEnd == nameEnd }
|
||||
if (!exists) {
|
||||
// Determine enclosing function body (if any) to attach as local; else treat as top-level
|
||||
val inFn = functions.asSequence()
|
||||
.filter { it.rangeEnd > it.rangeStart && nameStart >= it.rangeStart && nameStart <= it.rangeEnd }
|
||||
.maxByOrNull { it.rangeEnd - it.rangeStart }
|
||||
val kind = if (kw.equals("var", true)) SymbolKind.Var else SymbolKind.Val
|
||||
if (inFn != null) {
|
||||
val localSym = Symbol(nextId++, text.substring(nameStart, nameEnd), kind, nameStart, nameEnd, containerId = inFn.id)
|
||||
symbols += localSym
|
||||
inFn.locals += localSym.id
|
||||
} else {
|
||||
val localSym = Symbol(nextId++, text.substring(nameStart, nameEnd), kind, nameStart, nameEnd, containerId = null)
|
||||
symbols += localSym
|
||||
topLevelByName.getOrPut(localSym.name) { mutableListOf() }.add(localSym.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
iTok = j // continue from the name or comment
|
||||
}
|
||||
}
|
||||
iTok++
|
||||
}
|
||||
}
|
||||
|
||||
// Build name -> symbol ids index per function (locals+params) and per class (fields) for faster resolution
|
||||
data class Idx(val byName: Map<String, List<Int>>)
|
||||
fun buildIndex(ids: List<Int>): Idx {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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 net.sergeych.lyng
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.binding.Binder
|
||||
import net.sergeych.lyng.binding.SymbolKind
|
||||
import net.sergeych.lyng.miniast.MiniAstBuilder
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class BindingHighlightTest {
|
||||
|
||||
private suspend fun compileWithMini(code: String): Pair<Script, MiniAstBuilder> {
|
||||
val sink = MiniAstBuilder()
|
||||
val script = Compiler.compileWithMini(code.trimIndent(), sink)
|
||||
return script to sink
|
||||
}
|
||||
|
||||
@Test
|
||||
fun binder_registers_top_level_var_and_binds_usages() = runTest {
|
||||
val code = """
|
||||
var counter = 0
|
||||
counter = counter + 1
|
||||
println(counter)
|
||||
"""
|
||||
|
||||
val text = code.trimIndent()
|
||||
val (_, sink) = compileWithMini(text)
|
||||
val mini = sink.build()
|
||||
assertNotNull(mini, "Mini-AST must be built")
|
||||
|
||||
val binding = Binder.bind(text, mini!!)
|
||||
|
||||
// Find the top-level symbol for counter and ensure it is mutable (Var)
|
||||
val sym = binding.symbols.firstOrNull { it.name == "counter" }
|
||||
assertNotNull(sym, "Top-level var 'counter' must be registered as a symbol")
|
||||
assertEquals(SymbolKind.Var, sym.kind, "'counter' declared with var should be SymbolKind.Var")
|
||||
|
||||
// Declaration position
|
||||
val declRange = sym.declStart to sym.declEnd
|
||||
|
||||
// Collect all references to counter (excluding the declaration itself)
|
||||
val refs = binding.references.filter { it.symbolId == sym.id && (it.start to it.end) != declRange }
|
||||
assertTrue(refs.isNotEmpty(), "Usages of top-level var 'counter' should be bound")
|
||||
|
||||
// Expect at least two usages: assignment LHS and println argument
|
||||
assertTrue(refs.size >= 2, "Expected at least two usages of 'counter'")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun binder_registers_top_level_val_and_binds_usages() = runTest {
|
||||
val code = """
|
||||
val answer = 41
|
||||
val next = answer + 1
|
||||
println(answer)
|
||||
"""
|
||||
|
||||
val text = code.trimIndent()
|
||||
val (_, sink) = compileWithMini(text)
|
||||
val mini = sink.build()
|
||||
assertNotNull(mini, "Mini-AST must be built")
|
||||
|
||||
val binding = Binder.bind(text, mini!!)
|
||||
|
||||
val sym = binding.symbols.firstOrNull { it.name == "answer" }
|
||||
assertNotNull(sym, "Top-level val 'answer' must be registered as a symbol")
|
||||
assertEquals(SymbolKind.Val, sym.kind, "'answer' declared with val should be SymbolKind.Val")
|
||||
|
||||
val declRange = sym.declStart to sym.declEnd
|
||||
val refs = binding.references.filter { it.symbolId == sym.id && (it.start to it.end) != declRange }
|
||||
assertTrue(refs.isNotEmpty(), "Usages of top-level val 'answer' should be bound")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun binder_binds_locals_in_top_level_block_and_function_call() = runTest {
|
||||
val code = """
|
||||
fun test21() {
|
||||
21
|
||||
}
|
||||
|
||||
val format = "%" + "s"
|
||||
|
||||
for( f in files ) {
|
||||
var name = f.name
|
||||
if( f.isDirectory() )
|
||||
println("is directory")
|
||||
name += "/"
|
||||
println( format(name, f.size()) )
|
||||
}
|
||||
|
||||
test21()
|
||||
"""
|
||||
|
||||
val text = code.trimIndent()
|
||||
val (_, sink) = compileWithMini(text)
|
||||
val mini = sink.build()
|
||||
assertNotNull(mini, "Mini-AST must be built")
|
||||
|
||||
val binding = Binder.bind(text, mini!!)
|
||||
|
||||
// Ensure we registered the local var/val symbol for `name`
|
||||
val nameSym = binding.symbols.firstOrNull { it.name == "name" }
|
||||
assertNotNull(nameSym, "Local variable 'name' should be registered as a symbol")
|
||||
assertEquals(SymbolKind.Var, nameSym.kind, "'name' is declared with var and must be SymbolKind.Var")
|
||||
|
||||
// Ensure there is at least one usage reference to `name` (not just the declaration)
|
||||
val nameRefs = binding.references.filter { it.symbolId == nameSym.id }
|
||||
println("[DEBUG_LOG] name decl at ${nameSym.declStart}..${nameSym.declEnd}")
|
||||
println("[DEBUG_LOG] name refs: ${nameRefs.map { it.start to it.end }}")
|
||||
assertTrue(nameRefs.isNotEmpty(), "Usages of 'name' should be bound to its declaration")
|
||||
|
||||
// We expect at least two usages of `name`: in "+=" and in the call argument.
|
||||
assertTrue(nameRefs.size >= 2, "Binder should bind multiple usages of 'name'")
|
||||
|
||||
// Ensure function call at top-level is bound to the function symbol
|
||||
val fnSym = binding.symbols.firstOrNull { it.name == "test21" && it.kind == SymbolKind.Function }
|
||||
assertNotNull(fnSym, "Function 'test21' symbol must be present")
|
||||
val callIdx = text.lastIndexOf("test21()")
|
||||
assertTrue(callIdx > 0, "Test snippet must contain a 'test21()' call")
|
||||
val callRef = binding.references.firstOrNull { it.symbolId == fnSym.id && it.start == callIdx && it.end == callIdx + "test21".length }
|
||||
assertNotNull(callRef, "Binder should bind the top-level call 'test21()' to its declaration")
|
||||
|
||||
// Sanity: no references point exactly to the declaration range of test21
|
||||
val declStart = fnSym.declStart
|
||||
val declEnd = fnSym.declEnd
|
||||
assertTrue(binding.references.none { it.start == declStart && it.end == declEnd }, "Declaration should not be duplicated as a reference")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun binder_binds_name_used_in_string_literal_invoke() = runTest {
|
||||
val code = """
|
||||
val format = "%" + "s"
|
||||
|
||||
for( f in files ) {
|
||||
var name = f.name
|
||||
if( f.isDirectory() )
|
||||
println("%s is directory"(name))
|
||||
name += "/"
|
||||
println( format(name, f.size()) )
|
||||
}
|
||||
"""
|
||||
|
||||
val text = code.trimIndent()
|
||||
val (_, sink) = compileWithMini(text)
|
||||
val mini = sink.build()
|
||||
assertNotNull(mini, "Mini-AST must be built")
|
||||
|
||||
val binding = Binder.bind(text, mini!!)
|
||||
|
||||
val nameSym = binding.symbols.firstOrNull { it.name == "name" && (it.kind == SymbolKind.Var || it.kind == SymbolKind.Val) }
|
||||
assertNotNull(nameSym, "Local variable 'name' should be registered as a symbol")
|
||||
|
||||
// Find the specific usage inside string-literal invocation: "%s is directory"(name)
|
||||
val pattern = "\"%s is directory\"(name)"
|
||||
val lineIdx = text.indexOf(pattern)
|
||||
assertTrue(lineIdx >= 0, "Pattern with string invoke should be present in the snippet")
|
||||
val nameStart = lineIdx + pattern.indexOf("name")
|
||||
val nameEnd = nameStart + "name".length
|
||||
|
||||
val hasRefAtInvoke = binding.references.any { it.symbolId == nameSym.id && it.start == nameStart && it.end == nameEnd }
|
||||
println("[DEBUG_LOG] refs for 'name': ${binding.references.filter { it.symbolId == nameSym.id }.map { it.start to it.end }}")
|
||||
assertTrue(hasRefAtInvoke, "Binder should bind 'name' used as an argument to a string-literal invocation")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user