lynglib: added MiniAST support
lyngweb: better syntax highlighting and code editor
This commit is contained in:
1 parent
28b961d339
commit
ea0ecb1db3
24 files changed
+3095
-218
No files matched your search
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* Mini binding tests for highlighting/editor features
|
||||
*/
|
||||
package net.sergeych.lyng
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.binding.Binder
|
||||
import net.sergeych.lyng.miniast.MiniAstBuilder
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class BindingTest {
|
||||
|
||||
private suspend fun bind(code: String): net.sergeych.lyng.binding.BindingSnapshot {
|
||||
val src = code.trimIndent()
|
||||
val sink = MiniAstBuilder()
|
||||
Compiler.compileWithMini(src, sink)
|
||||
val mini = sink.build()
|
||||
assertNotNull(mini, "MiniScript should be built")
|
||||
return Binder.bind(src, mini!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun binds_params_and_locals() = runTest {
|
||||
val snap = bind(
|
||||
"""
|
||||
fun f(a:Int){
|
||||
val x = 1
|
||||
a + x
|
||||
}
|
||||
"""
|
||||
)
|
||||
// Expect at least one Param symbol "a" and one Val symbol "x"
|
||||
val aIds = snap.symbols.filter { it.name == "a" }.map { it.id }
|
||||
val xIds = snap.symbols.filter { it.name == "x" }.map { it.id }
|
||||
assertTrue(aIds.isNotEmpty())
|
||||
assertTrue(xIds.isNotEmpty())
|
||||
// Both should have at least one reference across any symbol with that name
|
||||
val aRefs = snap.references.count { it.symbolId in aIds }
|
||||
val xRefs = snap.references.count { it.symbolId in xIds }
|
||||
assertEquals(1, aRefs)
|
||||
assertEquals(1, xRefs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun binds_top_level_val_usage() = runTest {
|
||||
val snap = bind(
|
||||
"""
|
||||
val x = 1
|
||||
x + 1
|
||||
"""
|
||||
)
|
||||
val xSym = snap.symbols.firstOrNull { it.name == "x" }
|
||||
assertNotNull(xSym)
|
||||
// One reference usage to top-level x
|
||||
val refs = snap.references.filter { it.symbolId == xSym!!.id }
|
||||
assertEquals(1, refs.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shadowing_scopes() = runTest {
|
||||
val snap = bind(
|
||||
"""
|
||||
val x = 1
|
||||
fun f(){
|
||||
val x = 2
|
||||
x
|
||||
}
|
||||
"""
|
||||
)
|
||||
val allX = snap.symbols.filter { it.name == "x" }
|
||||
// Expect at least two x symbols (top-level and local)
|
||||
assertEquals(true, allX.size >= 2)
|
||||
// The single reference inside f body should bind to the inner x (containerId != null)
|
||||
val localXs = allX.filter { it.containerId != null }
|
||||
assertEquals(true, localXs.isNotEmpty())
|
||||
val localX = localXs.maxBy { it.declStart }
|
||||
val refsToLocal = snap.references.count { it.symbolId == localX.id }
|
||||
assertEquals(1, refsToLocal)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun class_fields_basic() = runTest {
|
||||
val snap = bind(
|
||||
"""
|
||||
class C {
|
||||
val foo = 1
|
||||
fun bar(){ foo }
|
||||
}
|
||||
"""
|
||||
)
|
||||
val fooField = snap.symbols.firstOrNull { it.name == "foo" }
|
||||
assertNotNull(fooField)
|
||||
// Should have at least one reference (usage in bar)
|
||||
val refs = snap.references.count { it.symbolId == fooField!!.id }
|
||||
assertEquals(1, refs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ctor_params_as_fields() = runTest {
|
||||
val snap = bind(
|
||||
"""
|
||||
class C(val x:Int){
|
||||
fun f(){ x }
|
||||
}
|
||||
"""
|
||||
)
|
||||
val xField = snap.symbols.firstOrNull { it.name == "x" }
|
||||
assertNotNull(xField)
|
||||
val refs = snap.references.count { it.symbolId == xField!!.id }
|
||||
assertEquals(1, refs)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* Mini-AST capture tests
|
||||
*/
|
||||
package net.sergeych.lyng
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.miniast.*
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class MiniAstTest {
|
||||
|
||||
private suspend fun compileWithMini(code: String): Pair<Script, net.sergeych.lyng.miniast.MiniAstBuilder> {
|
||||
val sink = MiniAstBuilder()
|
||||
val script = Compiler.compileWithMini(code.trimIndent(), sink)
|
||||
return script to sink
|
||||
}
|
||||
|
||||
@Test
|
||||
fun miniAst_captures_import_segments() = runTest {
|
||||
val code = """
|
||||
import lyng.stdlib
|
||||
val x = 1
|
||||
"""
|
||||
val (_, sink) = compileWithMini(code)
|
||||
val mini = sink.build()
|
||||
assertNotNull(mini)
|
||||
val imps = mini!!.imports
|
||||
assertTrue(imps.isNotEmpty(), "imports should be captured")
|
||||
val first = imps.first()
|
||||
val segNames = first.segments.map { it.name }
|
||||
assertEquals(listOf("lyng", "stdlib"), segNames)
|
||||
// Ensure ranges are valid and ordered
|
||||
for (seg in first.segments) {
|
||||
assertTrue(seg.range.start.line == first.range.start.line)
|
||||
assertTrue(seg.range.start.column <= seg.range.end.column)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun miniAst_captures_fun_docs_and_types() = runTest {
|
||||
val code = """
|
||||
// Summary: does foo
|
||||
// details can be here
|
||||
fun foo(a: Int, b: pkg.String?): Boolean {
|
||||
true
|
||||
}
|
||||
"""
|
||||
val (_, sink) = compileWithMini(code)
|
||||
val mini = sink.build()
|
||||
assertNotNull(mini)
|
||||
val fn = mini!!.declarations.filterIsInstance<MiniFunDecl>().firstOrNull { it.name == "foo" }
|
||||
assertNotNull(fn, "function decl should be captured")
|
||||
// Doc
|
||||
assertNotNull(fn.doc)
|
||||
assertEquals("Summary: does foo", fn.doc!!.summary)
|
||||
assertTrue(fn.doc!!.raw.contains("details"))
|
||||
// Params
|
||||
assertEquals(2, fn.params.size)
|
||||
val p1 = fn.params[0]
|
||||
val p2 = fn.params[1]
|
||||
val t1 = p1.type as MiniTypeName
|
||||
assertEquals(listOf("Int"), t1.segments.map { it.name })
|
||||
assertEquals(false, t1.nullable)
|
||||
val t2 = p2.type as MiniTypeName
|
||||
assertEquals(listOf("pkg", "String"), t2.segments.map { it.name })
|
||||
assertEquals(true, t2.nullable)
|
||||
// Return type
|
||||
val rt = fn.returnType as MiniTypeName
|
||||
assertEquals(listOf("Boolean"), rt.segments.map { it.name })
|
||||
assertEquals(false, rt.nullable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun miniAst_captures_val_type_and_doc() = runTest {
|
||||
val code = """
|
||||
// docs for x
|
||||
val x: List<String> = ["a", "b"]
|
||||
"""
|
||||
val (_, sink) = compileWithMini(code)
|
||||
val mini = sink.build()
|
||||
assertNotNull(mini)
|
||||
val vd = mini!!.declarations.filterIsInstance<net.sergeych.lyng.miniast.MiniValDecl>().firstOrNull { it.name == "x" }
|
||||
assertNotNull(vd)
|
||||
assertNotNull(vd.doc)
|
||||
assertEquals("docs for x", vd.doc!!.summary)
|
||||
val ty = vd.type
|
||||
assertNotNull(ty)
|
||||
val gen = ty as MiniGenericType
|
||||
val base = gen.base as MiniTypeName
|
||||
assertEquals(listOf("List"), base.segments.map { it.name })
|
||||
assertEquals(1, gen.args.size)
|
||||
val arg0 = gen.args[0] as MiniTypeName
|
||||
assertEquals(listOf("String"), arg0.segments.map { it.name })
|
||||
assertEquals(false, gen.nullable)
|
||||
assertNotNull(vd.initRange)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun miniAst_captures_class_bases_and_doc() = runTest {
|
||||
val code = """
|
||||
/** Class C docs */
|
||||
class C: Base1, Base2 {}
|
||||
"""
|
||||
val (_, sink) = compileWithMini(code)
|
||||
val mini = sink.build()
|
||||
assertNotNull(mini)
|
||||
val cd = mini!!.declarations.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == "C" }
|
||||
assertNotNull(cd)
|
||||
assertNotNull(cd.doc)
|
||||
assertTrue(cd.doc!!.raw.contains("Class C docs"))
|
||||
// Bases captured as plain names for now
|
||||
assertEquals(listOf("Base1", "Base2"), cd.bases)
|
||||
}
|
||||
}
|
||||
@@ -3544,4 +3544,25 @@ class ScriptTest {
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
|
||||
// @Ignore
|
||||
// @Test
|
||||
// fun interpolationTest() = runTest {
|
||||
// eval($$$"""
|
||||
//
|
||||
// val foo = "bar"
|
||||
// val buzz = ["foo", "bar"]
|
||||
//
|
||||
// // 1. simple interpolation
|
||||
// assertEquals( "bar", "$foo" )
|
||||
// assertEquals( "bar", "${foo}" )
|
||||
//
|
||||
// // 2. escaping the dollar sign
|
||||
// assertEquals( "$", "\$foo"[0] )
|
||||
// assertEquals( "foo, "\$foo"[1..] )
|
||||
//
|
||||
// // 3. interpolation with expression
|
||||
// assertEquals( "foo!. bar?", "${buzz[0]+"!"}. ${buzz[1]+"?"}" )
|
||||
// """.trimIndent())
|
||||
// }
|
||||
}
|
||||
Reference in new issue
Block a user