diff --git a/core/src/main/kotlin/com/strumenta/kolasu/ids/SourceIdProvider.kt b/core/src/main/kotlin/com/strumenta/kolasu/ids/SourceIdProvider.kt index 69e9b06b8..0f2cb6105 100644 --- a/core/src/main/kotlin/com/strumenta/kolasu/ids/SourceIdProvider.kt +++ b/core/src/main/kotlin/com/strumenta/kolasu/ids/SourceIdProvider.kt @@ -9,6 +9,14 @@ import java.io.File const val UNKNOWN_SOURCE_ID = "UNKNOWN_SOURCE" +fun String.removeCharactersInvalidInLionWebIDs(): String { + return this.filter { + it in setOf('-', '_') || it in CharRange('0', '9') || + it in CharRange('a', 'z') || + it in CharRange('A', 'Z') + }.toString() +} + /** * Given a Source (even null), it generates a corresponding identifier. */ @@ -22,7 +30,7 @@ abstract class AbstractSourceIdProvider : SourceIdProvider { .replace('/', '-') .replace('#', '-') .replace(' ', '_') - .replace("@", "_at_") + .replace("@", "_at_").removeCharactersInvalidInLionWebIDs() } class SimpleSourceIdProvider(var acceptNullSource: Boolean = false) : AbstractSourceIdProvider() { diff --git a/core/src/main/kotlin/com/strumenta/kolasu/model/Errors.kt b/core/src/main/kotlin/com/strumenta/kolasu/model/Errors.kt index 71db4ba23..2baa9a42d 100644 --- a/core/src/main/kotlin/com/strumenta/kolasu/model/Errors.kt +++ b/core/src/main/kotlin/com/strumenta/kolasu/model/Errors.kt @@ -19,8 +19,8 @@ class GenericErrorNode(error: Exception? = null, message: String? = null) : Node private fun Throwable.message(): String { val cause = this.cause?.message()?.let { " -> $it" } ?: "" - val msg = if (this.message != null) ": " + this.message else "" - return "${this.javaClass.simpleName}$msg$cause" + val explanation = if (this.message != null) ": " + this.message else "" + return "${this.javaClass.simpleName}$explanation$cause" } } diff --git a/core/src/main/kotlin/com/strumenta/kolasu/transformation/DummyNodes.kt b/core/src/main/kotlin/com/strumenta/kolasu/transformation/DummyNodes.kt index acab0636f..f0dca94ab 100644 --- a/core/src/main/kotlin/com/strumenta/kolasu/transformation/DummyNodes.kt +++ b/core/src/main/kotlin/com/strumenta/kolasu/transformation/DummyNodes.kt @@ -12,6 +12,12 @@ import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.primaryConstructor import kotlin.reflect.jvm.javaType +/** + * This logic instantiate a node of the given class with dummy values. + * This is useful because it permits to add an element that "fit" and make the typesystem happy. + * Typically, the only goal of the element would be to hold some annotation that indicates that the element + * is representing an error or a missing transformation or something of that sort. + */ fun KClass.dummyInstance(): T { val kClassToInstantiate = this.toInstantiableType() val emptyConstructor = kClassToInstantiate.constructors.find { it.parameters.isEmpty() } @@ -77,6 +83,10 @@ private fun KClass.toInstantiableType(): KClass { } } +/** + * We can only instantiate concrete classes or sealed classes, if one its subclasses is directly or + * indirectly instantiable. For interfaces this return false. + */ fun KClass.isDirectlyOrIndirectlyInstantiable(): Boolean { return if (this.isSealed) { this.sealedSubclasses.any { it.isDirectlyOrIndirectlyInstantiable() } diff --git a/core/src/main/kotlin/com/strumenta/kolasu/transformation/PlaceholderASTTransformation.kt b/core/src/main/kotlin/com/strumenta/kolasu/transformation/PlaceholderASTTransformation.kt new file mode 100644 index 000000000..cff213a00 --- /dev/null +++ b/core/src/main/kotlin/com/strumenta/kolasu/transformation/PlaceholderASTTransformation.kt @@ -0,0 +1,42 @@ +package com.strumenta.kolasu.transformation + +import com.strumenta.kolasu.model.Node +import com.strumenta.kolasu.model.Origin +import com.strumenta.kolasu.model.Position +import kotlin.reflect.KClass + +/** + * This is used to indicate that a Node represents some form of placeholders to be used in transformation. + */ +sealed class PlaceholderASTTransformation(val origin: Origin?, val message: String) : Origin { + override val position: Position? + get() = origin?.position + override val sourceText: String? + get() = origin?.sourceText +} + +/** + * This is used to indicate that we do not know how to transform a certain node. + */ +class MissingASTTransformation( + origin: Origin?, + val transformationSource: Any?, + val expectedType: KClass? = null, + message: String = "Translation of a node is not yet implemented: " + + "${if (transformationSource is Node) transformationSource.simpleNodeType else transformationSource}" + + if (expectedType != null) " into $expectedType" else "" +) : + PlaceholderASTTransformation(origin, message) { + constructor(transformationSource: Node, expectedType: KClass? = null) : this( + transformationSource, + transformationSource, + expectedType + ) +} + +/** + * This is used to indicate that, while we had a transformation for a given node, that failed. + * This is typically the case because the transformation covers only certain case and we encountered + * one that was not covered. + */ +class FailingASTTransformation(origin: Origin?, message: String) : PlaceholderASTTransformation(origin, message) diff --git a/core/src/main/kotlin/com/strumenta/kolasu/transformation/Transformation.kt b/core/src/main/kotlin/com/strumenta/kolasu/transformation/Transformation.kt index 46d2c440a..ea38ae8f3 100644 --- a/core/src/main/kotlin/com/strumenta/kolasu/transformation/Transformation.kt +++ b/core/src/main/kotlin/com/strumenta/kolasu/transformation/Transformation.kt @@ -238,31 +238,6 @@ data class ChildNodeFactory( */ private val NO_CHILD_NODE = ChildNodeFactory("", { x -> x }, { _, _ -> }, Node::class) -sealed class PlaceholderASTTransformation(val origin: Origin?, val message: String) : Origin { - override val position: Position? - get() = origin?.position - override val sourceText: String? - get() = origin?.sourceText -} - -class MissingASTTransformation( - origin: Origin?, - val transformationSource: Any?, - val expectedType: KClass? = null, - message: String = "Translation of a node is not yet implemented: " + - "${if (transformationSource is Node) transformationSource.simpleNodeType else transformationSource}" + - if (expectedType != null) " into $expectedType" else "" -) : - PlaceholderASTTransformation(origin, message) { - constructor(transformationSource: Node, expectedType: KClass? = null) : this( - transformationSource, - transformationSource, - expectedType - ) -} - -class FailingASTTransformation(origin: Origin?, message: String) : PlaceholderASTTransformation(origin, message) - /** * Implementation of a tree-to-tree transformation. For each source node type, we can register a factory that knows how * to create a transformed node. Then, this transformer can read metadata in the transformed node to recursively @@ -278,6 +253,11 @@ open class ASTTransformer( @Deprecated("To be removed in Kolasu 1.6") val allowGenericNode: Boolean = true, val throwOnUnmappedNode: Boolean = false, + /** + * When the fault tollerant flag is set, in case a transformation fails we will add a node + * with the origin FailingASTTransformation. If the flag is not set, then the transformation will just + * fail. + */ val faultTollerant: Boolean = !throwOnUnmappedNode ) { /** diff --git a/core/src/test/kotlin/com/strumenta/kolasu/codegen/KotlinPrinter.kt b/core/src/test/kotlin/com/strumenta/kolasu/codegen/KotlinPrinter.kt index e6c2dfc85..8911d9c30 100644 --- a/core/src/test/kotlin/com/strumenta/kolasu/codegen/KotlinPrinter.kt +++ b/core/src/test/kotlin/com/strumenta/kolasu/codegen/KotlinPrinter.kt @@ -8,7 +8,6 @@ class KotlinPrinter : ASTCodeGenerator() { override val placeholderNodePrinter: NodePrinter get() = NodePrinter { output: PrinterOutput, ast: Node -> val placeholder = ast.origin as PlaceholderASTTransformation - val origin = placeholder.origin output.print("/* ${placeholder.message} */") } diff --git a/core/src/test/kotlin/com/strumenta/kolasu/ids/SourceIdProviderTest.kt b/core/src/test/kotlin/com/strumenta/kolasu/ids/SourceIdProviderTest.kt new file mode 100644 index 000000000..d16b14911 --- /dev/null +++ b/core/src/test/kotlin/com/strumenta/kolasu/ids/SourceIdProviderTest.kt @@ -0,0 +1,15 @@ +package com.strumenta.kolasu.ids + +import kotlin.test.Test +import kotlin.test.assertEquals + +class SourceIdProviderTest { + + @Test + fun testRemoveCharactersInvalidInLionWebIDs() { + assertEquals( + "funny933--_12aaAAAAZz", + "funny%à, è, é, ì, ò, ù =+933--_12aaAAAAZz".removeCharactersInvalidInLionWebIDs() + ) + } +} diff --git a/lionweb/src/main/kotlin/com/strumenta/kolasu/lionweb/LionWebModelConverter.kt b/lionweb/src/main/kotlin/com/strumenta/kolasu/lionweb/LionWebModelConverter.kt index 2981996e5..92dbbce91 100644 --- a/lionweb/src/main/kotlin/com/strumenta/kolasu/lionweb/LionWebModelConverter.kt +++ b/lionweb/src/main/kotlin/com/strumenta/kolasu/lionweb/LionWebModelConverter.kt @@ -22,6 +22,7 @@ import com.strumenta.kolasu.model.nodeOriginalProperties import com.strumenta.kolasu.parsing.FirstStageParsingResult import com.strumenta.kolasu.parsing.KolasuToken import com.strumenta.kolasu.parsing.ParsingResult +import com.strumenta.kolasu.transformation.FailingASTTransformation import com.strumenta.kolasu.transformation.MissingASTTransformation import com.strumenta.kolasu.transformation.PlaceholderASTTransformation import com.strumenta.kolasu.traversing.walk @@ -32,11 +33,13 @@ import io.lionweb.lioncore.java.language.Classifier import io.lionweb.lioncore.java.language.Concept import io.lionweb.lioncore.java.language.Containment import io.lionweb.lioncore.java.language.Enumeration +import io.lionweb.lioncore.java.language.EnumerationLiteral import io.lionweb.lioncore.java.language.Language import io.lionweb.lioncore.java.language.LionCoreBuiltins import io.lionweb.lioncore.java.language.PrimitiveType import io.lionweb.lioncore.java.language.Property import io.lionweb.lioncore.java.language.Reference +import io.lionweb.lioncore.java.model.AnnotationInstance import io.lionweb.lioncore.java.model.Node import io.lionweb.lioncore.java.model.ReferenceValue import io.lionweb.lioncore.java.model.impl.AbstractClassifierInstance @@ -243,6 +246,7 @@ class LionWebModelConverter( val targetID = myIDManager.nodeId(origin.origin as KNode) setOriginalNode(lwNode, targetID) } + setPlaceholderNodeType(instance, origin.javaClass.kotlin) lwNode.addAnnotation(instance) } else { throw Exception( @@ -357,6 +361,26 @@ class LionWebModelConverter( ) } + private fun setPlaceholderNodeType( + placeholderAnnotation: AnnotationInstance, + kClass: KClass + ) { + val enumerationLiteral: EnumerationLiteral = when (kClass) { + MissingASTTransformation::class -> StarLasuLWLanguage.PlaceholderNodeType.literals.find { + it.name == "MissingASTTransformation" + }!! + FailingASTTransformation::class -> StarLasuLWLanguage.PlaceholderNodeType.literals.find { + it.name == "FailingASTTransformation" + }!! + else -> TODO() + } + + placeholderAnnotation.setPropertyValue( + StarLasuLWLanguage.PlaceholderNodeTypeProperty, + EnumerationValueImpl(enumerationLiteral) + ) + } + fun importModelFromLionWeb(lwTree: LWNode): Any { val referencesPostponer = ReferencesPostponer() lwTree.thisAndAllDescendants().reversed().forEach { lwNode -> diff --git a/lionweb/src/main/kotlin/com/strumenta/kolasu/lionweb/StarLasuLWLanguage.kt b/lionweb/src/main/kotlin/com/strumenta/kolasu/lionweb/StarLasuLWLanguage.kt index 6708c4b44..30dbfcd51 100644 --- a/lionweb/src/main/kotlin/com/strumenta/kolasu/lionweb/StarLasuLWLanguage.kt +++ b/lionweb/src/main/kotlin/com/strumenta/kolasu/lionweb/StarLasuLWLanguage.kt @@ -10,6 +10,8 @@ import com.strumenta.kolasu.validation.IssueSeverity import com.strumenta.kolasu.validation.IssueType import io.lionweb.lioncore.java.language.Annotation import io.lionweb.lioncore.java.language.Concept +import io.lionweb.lioncore.java.language.Enumeration +import io.lionweb.lioncore.java.language.EnumerationLiteral import io.lionweb.lioncore.java.language.Interface import io.lionweb.lioncore.java.language.Language import io.lionweb.lioncore.java.language.LionCoreBuiltins @@ -101,6 +103,29 @@ object StarLasuLWLanguage : Language("com.strumenta.StarLasu") { keyForContainedElement(PLACEHOLDER_NODE) ) placeholderNodeAnnotation.annotates = LionCore.getConcept() + + val placeholderNodeAnnotationType = Enumeration( + this, + "PlaceholderNodeType" + ).apply { + this.id = "${placeholderNodeAnnotation.id!!.removeSuffix("-id")}-$name-id" + this.key = "${placeholderNodeAnnotation.key!!.removeSuffix("-key")}-$name-key" + val enumeration = this + addLiteral( + EnumerationLiteral(this, "MissingASTTransformation").apply { + this.id = "${enumeration.id!!.removeSuffix("-id")}-$name-id" + this.key = "${enumeration.id!!.removeSuffix("-key")}-$name-key" + } + ) + addLiteral( + EnumerationLiteral(this, "FailingASTTransformation").apply { + this.id = "${enumeration.id!!.removeSuffix("-id")}-$name-id" + this.key = "${enumeration.id!!.removeSuffix("-key")}-$name-key" + } + ) + } + addElement(placeholderNodeAnnotationType) + val reference = Reference().apply { this.name = "originalNode" @@ -111,6 +136,14 @@ object StarLasuLWLanguage : Language("com.strumenta.StarLasu") { this.setMultiple(false) } placeholderNodeAnnotation.addFeature(reference) + val type = Property().apply { + this.name = "type" + this.id = "${placeholderNodeAnnotation.id!!.removeSuffix("-id")}-$name-id" + this.key = "${placeholderNodeAnnotation.key!!.removeSuffix("-key")}-$name-key" + this.type = placeholderNodeAnnotationType + this.setOptional(false) + } + placeholderNodeAnnotation.addFeature(type) addElement(placeholderNodeAnnotation) } @@ -141,6 +174,12 @@ object StarLasuLWLanguage : Language("com.strumenta.StarLasu") { val PlaceholderNodeOriginalNode: Reference get() = PlaceholderNode.getReferenceByName("originalNode")!! + val PlaceholderNodeTypeProperty: Property + get() = PlaceholderNode.getPropertyByName("type")!! + + val PlaceholderNodeType: Enumeration + get() = StarLasuLWLanguage.getEnumerationByName("PlaceholderNodeType")!! + val BehaviorDeclaration: Interface get() = StarLasuLWLanguage.getInterfaceByName("BehaviorDeclaration")!! val Documentation: Interface