This is a pseudo-follow up to Kotlin Data Class Wildcard: taking in advice from that question I came upon a better but quite different solution to the same problem.
The problem: fuzzy matching AST nodes. All regular properties should be checked for equality normally (via ==
), but allowing any absent value to match a real value and recursing the fuzzy match down the Node
hierarchy. The second requirement is that this does not require anything extra within the Node
s themselves as this would be prohibitive towards adding new nodes and the fuzzy match is purely the domain of testing.
The reason for creating this is that the structure of a parsed AST can be very complex and in order to make writing tests easier, I want to be able to say "I don't care about this part of the parsed AST", and just test a portion of it.
NodeMatching.kt
package nafi.grammar
import nafi.grammar.ast.Node
import nafi.grammar.ast.Position
import kotlin.reflect.memberProperties
infix fun Node.matches(that: Node): Boolean {
if (this.javaClass != that.javaClass)
return false
else return this.javaClass.kotlin.memberProperties
.map { it.get(this) matches it.get(that) }
.fold(true, Boolean::and)
}
private infix fun Position.matches(that: Position) =
this.start == that.start &&
(this.end == that.end || this.end == null || that.end == null)
private infix fun <T : Collection<*>> T.matches(that: T): Boolean {
if (this.size != that.size) return false
else return this.indices
.map { this.elementAt(it) matches that.elementAt(it) }
.fold(true, Boolean::and)
}
private infix fun Any?.matches(that: Any?): Boolean = when {
this == null || that == null -> true
this is Node && that is Node -> this matches that
this is Position && that is Position -> this matches that
this is Collection<*> && that is Collection<*> -> this matches that
else -> this == that
}
ASTMatcherTest.kt
package nafi.grammar
import nafi.grammar.ast.ConstantDefinition
import nafi.grammar.ast.IntegerLiteral
import nafi.grammar.ast.NafiFile
import nafi.grammar.ast.Position
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class ASTMatcherTest {
fun testCompile(code: String, ast: NafiFile, expectedMatch: Boolean) {
val parsed = NafiParserFacade.parse(code)
assertEquals(expectedMatch, ast matches parsed,
"expected $ast, got $parsed, should ${if (!expectedMatch) "not " else ""}match"
)
}
@Test fun passNoPosition() = testCompile(
"constant id = 5",
NafiFile(ConstantDefinition("id", IntegerLiteral("5"))),
true
)
@Test fun failBadPosition() = testCompile(
"constant id = 5",
NafiFile(ConstantDefinition("id", IntegerLiteral("5", Position(10, 14)))),
false
)
@Test fun passGoodPosition() = testCompile(
"constant id = 5",
NafiFile(ConstantDefinition("id", IntegerLiteral("5", Position(1, 14, 1, 15)), Position(1, 0))),
true
)
}
Because Kotlin extensions are resolved statically, NodeMatching.kt includes a definition of matches
for two Any?
s and does a manual virtual dispatch to the appropriate matches
or ==
.
Key Points:
Position.matches
and<T : Collection<*>>.matches
can beinline
, but should they be?- This requires data which is non-nullable in application logic to be nullable to allow fuzzy matching. Is there a way around that?
- Any kotlin-isms that would make the code more readable or kotlin-abuses that are making the code less readable
The minimal AST Model that I'm using for testing can be found on GitHub along with the rest of the project. The project is a Maven project and you can run the tests with maven test
.