diff --git a/native/swift/sir-passes/build.gradle.kts b/native/swift/sir-passes/build.gradle.kts index 5446c6a17f9..726a2f4eebd 100644 --- a/native/swift/sir-passes/build.gradle.kts +++ b/native/swift/sir-passes/build.gradle.kts @@ -27,6 +27,8 @@ sourceSets { "test" { projectDefault() } } +testsJar() + projectTest(jUnitMode = JUnitMode.JUnit5) { workingDir = rootDir useJUnitPlatform { } diff --git a/native/swift/sir-passes/src/org/jetbrains/sir/passes/utility/DeclarationParentVisitor.kt b/native/swift/sir-passes/src/org/jetbrains/sir/passes/utility/DeclarationParentVisitor.kt new file mode 100644 index 00000000000..18bc175b725 --- /dev/null +++ b/native/swift/sir-passes/src/org/jetbrains/sir/passes/utility/DeclarationParentVisitor.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.sir.passes.utility + +import org.jetbrains.kotlin.sir.SirDeclaration +import org.jetbrains.kotlin.sir.SirDeclarationParent +import org.jetbrains.kotlin.sir.SirElement +import org.jetbrains.kotlin.sir.SirModule +import org.jetbrains.kotlin.sir.visitors.SirVisitor +import java.util.* + + +// Copy of DeclarationParentVisitor from Kotlin Backend IR. +internal abstract class DeclarationParentVisitor : SirVisitor() { + + protected val declarationParentStack = ArrayDeque() + + protected abstract fun handleParent(declaration: SirDeclaration, parent: SirDeclarationParent) + + override fun visitElement(element: SirElement, data: Nothing?) { + element.acceptChildren(this, data) + } + + override fun visitModule(module: SirModule, data: Nothing?) { + declarationParentStack.push(module) + super.visitModule(module, data) + declarationParentStack.pop() + } + + override fun visitDeclaration(declaration: SirDeclaration, data: Nothing?) { + handleParent(declaration, declarationParentStack.peekFirst()) + + if (declaration is SirDeclarationParent) { + declarationParentStack.push(declaration) + } + + super.visitDeclaration(declaration, data) + + if (declaration is SirDeclarationParent) { + declarationParentStack.pop() + } + } +} + +/** + * Fixes parents of declarations. + * + * Copy of PatchDeclarationParentVisitor from Backend IR. + */ +internal class PatchDeclarationParentVisitor() : DeclarationParentVisitor() { + constructor(containingDeclaration: SirDeclarationParent) : this() { + declarationParentStack.push(containingDeclaration) + } + + override fun handleParent(declaration: SirDeclaration, parent: SirDeclarationParent) { + declaration.parent = parent + } +} diff --git a/native/swift/sir-passes/src/org/jetbrains/sir/passes/utility/SirValidator.kt b/native/swift/sir-passes/src/org/jetbrains/sir/passes/utility/SirValidator.kt new file mode 100644 index 00000000000..5ad3a84f62b --- /dev/null +++ b/native/swift/sir-passes/src/org/jetbrains/sir/passes/utility/SirValidator.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.sir.passes.utility + +import org.jetbrains.kotlin.sir.* +import org.jetbrains.kotlin.sir.visitors.SirVisitor +import org.jetbrains.sir.passes.SirPass +import org.jetbrains.sir.passes.utility.ValidationError.WrongForeignDeclarationOrigin +import org.jetbrains.sir.passes.utility.ValidationError.WrongParent +import kotlin.collections.plusAssign + +/** + * Collection of errors that might arise during SIR validation. + */ +public sealed interface ValidationError { + public class WrongParent(public val declaration: SirDeclaration, public val expectedParent: SirDeclarationParent) : ValidationError + public class WrongForeignDeclarationOrigin(public val declaration: SirForeignDeclaration) : ValidationError +} + +public class SirValidatorConfig( + public val checkParents: Boolean = true, + public val checkForeignDeclarations: Boolean = true +) + +/** + * Validate various invariants about Swift IR. + */ +public fun validate(sirElement: SirElement, config: SirValidatorConfig): List { + return SirValidator(config).run(sirElement, null) +} + +/** + * Run full set of SIR validators, and fail if any error encountered. + */ +public fun SirElement.assertValid() { + val errors = validate(this, SirValidatorConfig()) + if (errors.isNotEmpty()) { + val messages = errors.map { + when (it) { + // TODO: better rendering of SIR elements. + is WrongForeignDeclarationOrigin -> "Wrong foreign declaration origin: ${it.declaration}" + is WrongParent -> "Wrong declaration parent of ${it.declaration}. Expected: ${it.expectedParent}. Got: ${it.declaration.parent}" + } + } + error("SIR is invalid:\n" + messages.joinToString(separator = "\n")) + } +} + +private fun interface ErrorHandler { + fun handle(error: ValidationError) +} + +internal class SirValidator(private val config: SirValidatorConfig) : SirPass> { + override fun run(element: SirElement, data: Nothing?): List { + val errors = mutableListOf() + + val handler = ErrorHandler { + errors += it + } + if (config.checkParents) { + element.accept(ParentValidator(handler), data) + } + if (config.checkForeignDeclarations) { + element.accept(ForeignDeclarationValidator(handler), data) + } + return errors + } +} + +private class ParentValidator(private val errorHandler: ErrorHandler) : DeclarationParentVisitor() { + override fun handleParent(declaration: SirDeclaration, parent: SirDeclarationParent) { + if (declaration.parent != parent) { + errorHandler.handle(WrongParent(declaration, parent)) + } + } +} + +private class ForeignDeclarationValidator(private val errorHandler: ErrorHandler) : SirVisitor() { + override fun visitElement(element: SirElement, data: Nothing?) { + element.acceptChildren(this, data) + } + + override fun visitForeignFunction(foreignFunction: SirForeignFunction, data: Nothing?) { + super.visitForeignFunction(foreignFunction, data) + if (foreignFunction.origin !is SirOrigin.Foreign) { + errorHandler.handle(WrongForeignDeclarationOrigin(foreignFunction)) + } + } +} \ No newline at end of file diff --git a/native/swift/sir-passes/tests/org/jetbrains/kotlin/sir/passes/SirParentPatcherTests.kt b/native/swift/sir-passes/tests/org/jetbrains/kotlin/sir/passes/SirParentPatcherTests.kt new file mode 100644 index 00000000000..a0b9bae107e --- /dev/null +++ b/native/swift/sir-passes/tests/org/jetbrains/kotlin/sir/passes/SirParentPatcherTests.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.sir.passes + +import org.jetbrains.kotlin.sir.SirNominalType +import org.jetbrains.kotlin.sir.builder.buildEnum +import org.jetbrains.kotlin.sir.builder.buildFunction +import org.jetbrains.kotlin.sir.builder.buildModule +import org.jetbrains.kotlin.sir.util.SirSwiftModule +import org.jetbrains.sir.passes.utility.PatchDeclarationParentVisitor +import org.jetbrains.sir.passes.utility.SirValidatorConfig +import org.jetbrains.sir.passes.utility.ValidationError +import org.jetbrains.sir.passes.utility.validate +import kotlin.test.Test +import kotlin.test.assertTrue + +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +class SirParentPatcherTests { + @Test + fun `parent patcher should patch parents`() { + val wrongEnum = buildEnum { name = "wrongEnum" } + val function = buildFunction { + name = "foo" + returnType = SirNominalType(SirSwiftModule.bool) + } + function.parent = wrongEnum + val wrongModule = buildModule { name = "wrongModule" } + val enum = buildEnum { + name = "e" + declarations += function + } + enum.parent = wrongModule + val module = buildModule { + name = "MyModule" + declarations += enum + } + val validationErrorsBefore = validate(module, SirValidatorConfig()) + assertTrue(validationErrorsBefore.filterIsInstance().count() == 2) + module.accept(PatchDeclarationParentVisitor(), null) + val validationErrorsAfter = validate(module, SirValidatorConfig()) + assertTrue(validationErrorsAfter.filterIsInstance().isEmpty()) + } +} \ No newline at end of file diff --git a/native/swift/sir-passes/tests/org/jetbrains/kotlin/sir/passes/SirValidatorTests.kt b/native/swift/sir-passes/tests/org/jetbrains/kotlin/sir/passes/SirValidatorTests.kt new file mode 100644 index 00000000000..792f97b3002 --- /dev/null +++ b/native/swift/sir-passes/tests/org/jetbrains/kotlin/sir/passes/SirValidatorTests.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.sir.passes + +import org.jetbrains.kotlin.sir.SirOrigin +import org.jetbrains.kotlin.sir.builder.buildForeignFunction +import org.jetbrains.kotlin.sir.builder.buildModule +import org.jetbrains.kotlin.sir.mock.MockFunction +import org.jetbrains.kotlin.sir.mock.MockKotlinType +import org.jetbrains.sir.passes.utility.PatchDeclarationParentVisitor +import org.jetbrains.sir.passes.utility.SirValidatorConfig +import org.jetbrains.sir.passes.utility.ValidationError +import org.jetbrains.sir.passes.utility.validate +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class SirValidatorTests { + @Test + fun `validator should check foreign origins`() { + val module = buildModule { + name = "MyModule" + declarations += buildForeignFunction { + } + } + + val config = SirValidatorConfig(checkParents = false) + val error = validate(module, config).first() + assertTrue(error is ValidationError.WrongForeignDeclarationOrigin) + assertEquals(module.declarations.first(), error.declaration) + } + + @Test + fun `validatior should check declaration parents`() { + val wrongModule = buildModule { + name = "WrongModule" + } + val foreignFunction = buildForeignFunction { + val kotlinEntity = MockFunction( + fqName = listOf("foo"), + parameters = emptyList(), + returnType = MockKotlinType("kotlin/Byte") + ) + origin = kotlinEntity + } + foreignFunction.parent = wrongModule + val module = buildModule { + name = "MyModule" + declarations += foreignFunction + } + + val config = SirValidatorConfig( + checkForeignDeclarations = false + ) + val error = validate(module, config).first() + assertTrue(error is ValidationError.WrongParent) + assertEquals(foreignFunction, error.declaration) + assertEquals(module, error.expectedParent) + } +} \ No newline at end of file