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 Nodes 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.


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


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"))),

    @Test fun failBadPosition() = testCompile(
            "constant id = 5",
            NafiFile(ConstantDefinition("id", IntegerLiteral("5", Position(10, 14)))),

    @Test fun passGoodPosition() = testCompile(
            "constant id = 5",
            NafiFile(ConstantDefinition("id", IntegerLiteral("5", Position(1, 14, 1, 15)), Position(1, 0))),

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 be inline, 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.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.