From a613e92679824ccb7c3f2428d58cfa3502d98735 Mon Sep 17 00:00:00 2001 From: Simon Cockx Date: Mon, 9 Sep 2024 14:37:17 +0200 Subject: [PATCH 01/14] WIP --- rosetta-lang/model/Rosetta.xcore | 2 +- .../java/com/regnosys/rosetta/Rosetta.xtext | 2 +- .../regnosys/rosetta/RosettaEcoreUtil.xtend | 20 +- .../rosetta/generator/RosettaGenerator.xtend | 2 +- .../generator/java/enums/EnumGenerator.xtend | 37 +- .../java/expression/ExpressionGenerator.xtend | 1 - .../java/types/JavaTypeTranslator.java | 12 + .../generator/java/types/RJavaEnum.java | 2 +- .../generator/java/types/RJavaEnumValue.java | 5 + .../interpreter/RosettaValueFactory.java | 6 + .../scoping/RosettaScopeProvider.xtend | 4 +- .../rosetta/types/ExpectedTypeProvider.java | 603 ++++++++++++++++++ .../com/regnosys/rosetta/types/REnumType.java | 56 +- .../rosetta/types/RObjectFactory.java | 2 +- .../regnosys/rosetta/types/RUnionType.java | 60 ++ .../regnosys/rosetta/types/Rosetta.xsemantics | 29 - .../rosetta/types/RosettaAuxiliary.xsemantics | 14 +- .../types/RosettaTypeChecking.xsemantics | 44 -- .../rosetta/types/RosettaTypeProvider.xtend | 22 +- .../rosetta/types/SubtypeRelation.java | 63 +- .../regnosys/rosetta/types/TypeSystem.java | 2 +- .../rosetta/utils/RosettaTypeSwitch.java | 5 + .../validation/RosettaSimpleValidator.xtend | 4 +- .../java/function/FunctionGeneratorTest.xtend | 44 ++ .../java/object/RosettaExtensionsTest.xtend | 17 +- .../rosetta/tests/RosettaParsingTest.xtend | 24 + .../rosetta/types/SubtypeRelationTest.xtend | 72 +++ 27 files changed, 1009 insertions(+), 145 deletions(-) create mode 100644 rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnumValue.java create mode 100644 rosetta-lang/src/main/java/com/regnosys/rosetta/types/ExpectedTypeProvider.java create mode 100644 rosetta-lang/src/main/java/com/regnosys/rosetta/types/RUnionType.java create mode 100644 rosetta-testing/src/test/java/com/regnosys/rosetta/types/SubtypeRelationTest.xtend diff --git a/rosetta-lang/model/Rosetta.xcore b/rosetta-lang/model/Rosetta.xcore index 3e56629c9..7e921ecf4 100644 --- a/rosetta-lang/model/Rosetta.xcore +++ b/rosetta-lang/model/Rosetta.xcore @@ -132,7 +132,7 @@ class RosettaMetaType extends RosettaRootElement, RosettaTypedFeature, RosettaTy } class RosettaEnumeration extends RosettaRootElement, RosettaType, RosettaDefinable, References, RosettaSymbol { - refers RosettaEnumeration superType + refers RosettaEnumeration[] parentEnums contains RosettaSynonym[] synonyms contains RosettaEnumValue[] enumValues opposite enumeration } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext b/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext index 7978ea5f9..56b23204f 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext @@ -93,7 +93,7 @@ Attribute: ; Enumeration returns RosettaEnumeration: - 'enum' RosettaNamed ('extends' superType=[RosettaEnumeration|QualifiedName])? ':' RosettaDefinable? + 'enum' RosettaNamed ('extends' parentEnums+=[RosettaEnumeration|QualifiedName] (',' parentEnums+=[RosettaEnumeration|QualifiedName])*)? ':' RosettaDefinable? References* (synonyms += RosettaSynonym)* enumValues += RosettaEnumValue* diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/RosettaEcoreUtil.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/RosettaEcoreUtil.xtend index d53bf85bb..095eafe66 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/RosettaEcoreUtil.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/RosettaEcoreUtil.xtend @@ -1,7 +1,6 @@ package com.regnosys.rosetta import com.google.common.base.CaseFormat -import com.regnosys.rosetta.rosetta.RosettaEnumeration import com.regnosys.rosetta.rosetta.RosettaFeature import com.regnosys.rosetta.rosetta.RosettaRecordType import com.regnosys.rosetta.rosetta.RosettaSynonym @@ -50,7 +49,7 @@ class RosettaEcoreUtil { RDataType: t.allNonOverridenAttributes.map[EObject] REnumType: - t.EObject.allEnumValues + t.allEnumValues RRecordType: { if (resourceSet !== null) { builtins.toRosettaType(t, RosettaRecordType, resourceSet).features @@ -63,23 +62,6 @@ class RosettaEcoreUtil { } } - @Deprecated // TODO: move to REnumType, similar to RDataType - def Set getAllSuperEnumerations(RosettaEnumeration e) { - doGetSuperEnumerations(e, newLinkedHashSet) - } - - @Deprecated - private def Set doGetSuperEnumerations(RosettaEnumeration e, Set seenEnums) { - if(e !== null && seenEnums.add(e)) - doGetSuperEnumerations(e.superType, seenEnums) - return seenEnums - } - - @Deprecated // TODO: move to REnumType, similar to RDataType - def getAllEnumValues(RosettaEnumeration e) { - e.allSuperEnumerations.map[enumValues].flatten - } - def Set getAllSynonyms(RosettaSynonym s) { doGetSynonyms(s, newLinkedHashSet) } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/RosettaGenerator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/RosettaGenerator.xtend index ba88d4854..d5c893acb 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/RosettaGenerator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/RosettaGenerator.xtend @@ -226,7 +226,7 @@ class RosettaGenerator implements IGenerator2 { ] } RosettaEnumeration: { - enumGenerator.generate(packages, fsa, elem, version) + enumGenerator.generate(packages, fsa, elem.buildREnumType, version) } } } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend index 0d5c7d427..422db67e3 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend @@ -4,10 +4,8 @@ import com.regnosys.rosetta.generator.java.JavaScope import com.regnosys.rosetta.generator.java.RosettaJavaPackages.RootPackage import com.regnosys.rosetta.generator.java.util.ImportManagerExtension import com.regnosys.rosetta.rosetta.RosettaEnumValue -import com.regnosys.rosetta.rosetta.RosettaEnumeration import com.rosetta.model.lib.annotations.RosettaEnum import com.rosetta.model.lib.annotations.RosettaSynonym -import java.util.ArrayList import java.util.Collections import java.util.Map import java.util.concurrent.ConcurrentHashMap @@ -17,34 +15,28 @@ import org.eclipse.xtext.generator.IFileSystemAccess2 import static com.regnosys.rosetta.generator.java.enums.EnumHelper.* import static com.regnosys.rosetta.generator.java.util.ModelGeneratorUtil.* +import com.regnosys.rosetta.types.REnumType +import com.regnosys.rosetta.generator.java.types.JavaTypeTranslator class EnumGenerator { @Inject extension ImportManagerExtension + @Inject extension JavaTypeTranslator - def generate(RootPackage root, IFileSystemAccess2 fsa, RosettaEnumeration enumeration, String version) { + def generate(RootPackage root, IFileSystemAccess2 fsa, REnumType enumeration, String version) { fsa.generateFile(root.withForwardSlashes + '/' + enumeration.name + '.java', enumeration.toJava(root, version)) } - - private def allEnumsValues(RosettaEnumeration enumeration) { - val enumValues = new ArrayList - var e = enumeration; - - while (e !== null) { - e.enumValues.forEach[enumValues.add(it)] - e = e.superType - } - return enumValues; - } - private def String toJava(RosettaEnumeration e, RootPackage root, String version) { + private def String toJava(REnumType e, RootPackage root, String version) { val scope = new JavaScope(root) + val clazz = e.toJavaReferenceType + val StringConcatenationClient classBody = ''' - «javadoc(e, version)» + «javadoc(e.EObject, version)» @«RosettaEnum»("«e.name»") - public enum «e.name» { + public enum «clazz» { - «FOR value: allEnumsValues(e) SEPARATOR ',\n' AFTER ';'» + «FOR value: e.allEnumValues SEPARATOR ',\n' AFTER ';'» «javadoc(value)» «value.contributeAnnotations» @«com.rosetta.model.lib.annotations.RosettaEnumValue»(value = "«value.name»"«IF value.display !== null», displayName = "«value.display»"«ENDIF») «convertValuesWithDisplay(value)» @@ -78,6 +70,15 @@ class EnumGenerator { } return value; } + + «FOR p : e.allParents» + «val parentClass = p.toJavaReferenceType» + «val fromScope = scope.methodScope("from" + parentClass.simpleName)» + «val fromParam = fromScope.createUniqueIdentifier(parentClass.simpleName.toFirstLower)» + public static «clazz» from«parentClass»(«parentClass» «fromParam») { + + } + «ENDFOR» @Override public «String» toString() { diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend index bdfcc23de..3a685f81a 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend @@ -624,7 +624,6 @@ class ExpressionGenerator extends RosettaExpressionSwitch { + private static Logger LOGGER = LoggerFactory.getLogger(JavaTypeTranslator.class); + @Inject public JavaTypeTranslator(RBuiltinTypeService builtins) { super(builtins); @@ -404,4 +410,10 @@ protected JavaClass caseDateTimeType(RDateTimeType type, Void con protected JavaClass caseZonedDateTimeType(RZonedDateTimeType type, Void context) { return typeUtil.ZONED_DATE_TIME; } + @Override + protected JavaClass caseUnionType(RUnionType type, Void context) { + // As union types are purely internal to the Rune type system, this should never happen. + LOGGER.error("Trying to convert " + RUnionType.class.getSimpleName() + " `" + type + "` to a Java type"); + return typeUtil.OBJECT; + } } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnum.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnum.java index 8c5438d81..915010eb0 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnum.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnum.java @@ -71,7 +71,7 @@ public List> getInterfaces() { @Override public boolean extendsDeclaration(JavaTypeDeclaration other) { - return false; + return other.equals(JavaClass.OBJECT); } @Override diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnumValue.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnumValue.java new file mode 100644 index 000000000..cf2b252fc --- /dev/null +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnumValue.java @@ -0,0 +1,5 @@ +package com.regnosys.rosetta.generator.java.types; + +public class RJavaEnumValue { + +} diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/interpreter/RosettaValueFactory.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/interpreter/RosettaValueFactory.java index 28fb210aa..c4c47e4f8 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/interpreter/RosettaValueFactory.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/interpreter/RosettaValueFactory.java @@ -30,6 +30,7 @@ import com.regnosys.rosetta.types.REnumType; import com.regnosys.rosetta.types.RErrorType; import com.regnosys.rosetta.types.RType; +import com.regnosys.rosetta.types.RUnionType; import com.regnosys.rosetta.types.builtin.RBasicType; import com.regnosys.rosetta.types.builtin.RBuiltinTypeService; import com.regnosys.rosetta.types.builtin.RDateTimeType; @@ -137,4 +138,9 @@ protected RosettaValue caseDateTimeType(RDateTimeType type, List context) { protected RosettaValue caseZonedDateTimeType(RZonedDateTimeType type, List context) { return new RosettaZonedDateTimeValue(castList(context, ZonedDateTime.class)); } + + @Override + protected RosettaValue caseUnionType(RUnionType type, List context) { + throw new UnsupportedOperationException("Cannot create a value of a union type"); + } } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/scoping/RosettaScopeProvider.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/scoping/RosettaScopeProvider.xtend index 97dcf2d80..08b42fd00 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/scoping/RosettaScopeProvider.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/scoping/RosettaScopeProvider.xtend @@ -198,7 +198,7 @@ class RosettaScopeProvider extends ImportedNamespaceAwareLocalScopeProvider { } case ROSETTA_ENUM_VALUE_REFERENCE__VALUE: { if (context instanceof RosettaEnumValueReference) { - return Scopes.scopeFor(context.enumeration.allEnumValues) + return Scopes.scopeFor(context.enumeration.buildREnumType.allEnumValues) } return IScope.NULLSCOPE } @@ -214,7 +214,7 @@ class RosettaScopeProvider extends ImportedNamespaceAwareLocalScopeProvider { if (context instanceof RosettaExternalEnumValue) { val enumRef = (context.eContainer as RosettaExternalEnum).typeRef if (enumRef instanceof RosettaEnumeration) - return Scopes.scopeFor(enumRef.allEnumValues) + return Scopes.scopeFor(enumRef.buildREnumType.allEnumValues) } return IScope.NULLSCOPE } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/ExpectedTypeProvider.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/ExpectedTypeProvider.java new file mode 100644 index 000000000..499a10d75 --- /dev/null +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/ExpectedTypeProvider.java @@ -0,0 +1,603 @@ +package com.regnosys.rosetta.types; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.regnosys.rosetta.rosetta.simple.SimplePackage.Literals.*; +import static com.regnosys.rosetta.rosetta.expression.ExpressionPackage.Literals.*; + +import com.google.inject.ImplementedBy; +import com.regnosys.rosetta.cache.IRequestScopedCache; +import com.regnosys.rosetta.rosetta.RosettaRule; +import com.regnosys.rosetta.rosetta.RosettaSymbol; +import com.regnosys.rosetta.rosetta.expression.ArithmeticOperation; +import com.regnosys.rosetta.rosetta.expression.AsKeyOperation; +import com.regnosys.rosetta.rosetta.expression.ChoiceOperation; +import com.regnosys.rosetta.rosetta.expression.ComparingFunctionalOperation; +import com.regnosys.rosetta.rosetta.expression.ComparisonOperation; +import com.regnosys.rosetta.rosetta.expression.ConstructorKeyValuePair; +import com.regnosys.rosetta.rosetta.expression.DefaultOperation; +import com.regnosys.rosetta.rosetta.expression.DistinctOperation; +import com.regnosys.rosetta.rosetta.expression.EqualityOperation; +import com.regnosys.rosetta.rosetta.expression.FilterOperation; +import com.regnosys.rosetta.rosetta.expression.FirstOperation; +import com.regnosys.rosetta.rosetta.expression.FlattenOperation; +import com.regnosys.rosetta.rosetta.expression.InlineFunction; +import com.regnosys.rosetta.rosetta.expression.JoinOperation; +import com.regnosys.rosetta.rosetta.expression.LastOperation; +import com.regnosys.rosetta.rosetta.expression.ListLiteral; +import com.regnosys.rosetta.rosetta.expression.LogicalOperation; +import com.regnosys.rosetta.rosetta.expression.MapOperation; +import com.regnosys.rosetta.rosetta.expression.MaxOperation; +import com.regnosys.rosetta.rosetta.expression.MinOperation; +import com.regnosys.rosetta.rosetta.expression.OneOfOperation; +import com.regnosys.rosetta.rosetta.expression.ReduceOperation; +import com.regnosys.rosetta.rosetta.expression.ReverseOperation; +import com.regnosys.rosetta.rosetta.expression.RosettaAbsentExpression; +import com.regnosys.rosetta.rosetta.expression.RosettaBooleanLiteral; +import com.regnosys.rosetta.rosetta.expression.RosettaConditionalExpression; +import com.regnosys.rosetta.rosetta.expression.RosettaConstructorExpression; +import com.regnosys.rosetta.rosetta.expression.RosettaContainsExpression; +import com.regnosys.rosetta.rosetta.expression.RosettaCountOperation; +import com.regnosys.rosetta.rosetta.expression.RosettaDeepFeatureCall; +import com.regnosys.rosetta.rosetta.expression.RosettaDisjointExpression; +import com.regnosys.rosetta.rosetta.expression.RosettaExistsExpression; +import com.regnosys.rosetta.rosetta.expression.RosettaExpression; +import com.regnosys.rosetta.rosetta.expression.RosettaFeatureCall; +import com.regnosys.rosetta.rosetta.expression.RosettaFunctionalOperation; +import com.regnosys.rosetta.rosetta.expression.RosettaImplicitVariable; +import com.regnosys.rosetta.rosetta.expression.RosettaIntLiteral; +import com.regnosys.rosetta.rosetta.expression.RosettaNumberLiteral; +import com.regnosys.rosetta.rosetta.expression.RosettaOnlyElement; +import com.regnosys.rosetta.rosetta.expression.RosettaOnlyExistsExpression; +import com.regnosys.rosetta.rosetta.expression.RosettaStringLiteral; +import com.regnosys.rosetta.rosetta.expression.RosettaSymbolReference; +import com.regnosys.rosetta.rosetta.expression.RosettaUnaryOperation; +import com.regnosys.rosetta.rosetta.expression.SortOperation; +import com.regnosys.rosetta.rosetta.expression.SumOperation; +import com.regnosys.rosetta.rosetta.expression.ThenOperation; +import com.regnosys.rosetta.rosetta.expression.ToDateOperation; +import com.regnosys.rosetta.rosetta.expression.ToDateTimeOperation; +import com.regnosys.rosetta.rosetta.expression.ToEnumOperation; +import com.regnosys.rosetta.rosetta.expression.ToIntOperation; +import com.regnosys.rosetta.rosetta.expression.ToNumberOperation; +import com.regnosys.rosetta.rosetta.expression.ToStringOperation; +import com.regnosys.rosetta.rosetta.expression.ToTimeOperation; +import com.regnosys.rosetta.rosetta.expression.ToZonedDateTimeOperation; +import com.regnosys.rosetta.rosetta.simple.Function; +import com.regnosys.rosetta.rosetta.simple.Operation; +import com.regnosys.rosetta.rosetta.simple.Segment; +import com.regnosys.rosetta.types.builtin.RBuiltinTypeService; +import com.regnosys.rosetta.utils.RosettaExpressionSwitch; + +import java.util.List; +import java.util.Objects; + +import javax.inject.Inject; + + +@ImplementedBy(ExpectedTypeProvider.Impl.class) +public interface ExpectedTypeProvider { + static final int INSIGNIFICANT_INDEX = -1; + + RType getExpectedTypeFromContainer(EObject owner); + default RType getExpectedType(EObject owner, EReference reference) { + return getExpectedType(owner, reference, INSIGNIFICANT_INDEX); + } + RType getExpectedType(EObject owner, EReference reference, int index); + + public static class Impl implements ExpectedTypeProvider { + private static Logger LOGGER = LoggerFactory.getLogger(Impl.class); + + private final RBuiltinTypeService builtins; + private final RosettaTypeProvider typeProvider; + private final TypeSystem typeSystem; + private final ExpectedTypeSwitch expressionSwitch; + private final IRequestScopedCache cache; + + @Inject + public Impl(RBuiltinTypeService builtins, RosettaTypeProvider typeProvider, TypeSystem typeSystem, IRequestScopedCache cache) { + this.builtins = builtins; + this.typeProvider = typeProvider; + this.typeSystem = typeSystem; + this.cache = cache; + this.expressionSwitch = new ExpectedTypeSwitch(); + } + + @Override + public RType getExpectedTypeFromContainer(EObject owner) { + EObject container = owner.eContainer(); + EReference reference = owner.eContainmentFeature(); + if (container == null || reference == null) { + return null; + } + if (reference.isMany()) { + int index = ((List)container.eGet(reference)).indexOf(owner); + return getExpectedType(container, reference, index); + } + return getExpectedType(container, reference); + } + @Override + public RType getExpectedType(EObject owner, EReference reference, int index) { + return cache.get(new CacheKey(owner, reference, index), () -> { + if (OPERATION__EXPRESSION.equals(reference) && owner instanceof Operation) { + Operation op = (Operation)owner; + if(op.getPath() == null) { + return typeProvider.getRTypeOfSymbol(op.getAssignRoot()); + } + List path = op.pathAsSegmentList(); + return typeProvider.getRTypeOfSymbol(path.get(path.size() - 1).getAttribute()); + } else if (CONSTRUCTOR_KEY_VALUE_PAIR__VALUE.equals(reference) && owner instanceof ConstructorKeyValuePair) { + ConstructorKeyValuePair pair = (ConstructorKeyValuePair) owner; + return typeProvider.getRTypeOfFeature(pair.getKey(), null); + } else if (owner instanceof RosettaExpression) { + return this.expressionSwitch.doSwitch((RosettaExpression) owner, reference, index); + } else if (INLINE_FUNCTION__BODY.equals(reference)) { + EObject operation = owner.eContainer(); + if (operation instanceof ReduceOperation) { + return getExpectedTypeFromContainer(operation); + } else if (operation instanceof FilterOperation) { + return builtins.BOOLEAN; + } else if (operation instanceof MapOperation) { + return getExpectedTypeFromContainer(operation); + } else if (operation instanceof ThenOperation) { + return getExpectedTypeFromContainer(operation); + } else if (operation instanceof ComparingFunctionalOperation) { + return builtins.BOOLEAN; + } else { + LOGGER.debug("Unexpected functional operation of type " + operation.getClass().getCanonicalName()); + } + } + return null; + }); + } + + private static class CacheKey { + private final EObject owner; + private final EReference reference; + private final int index; + public CacheKey(EObject owner, EReference reference, int index) { + this.owner = owner; + this.reference = reference; + this.index = index; + } + + @Override + public int hashCode() { + return Objects.hash(CacheKey.class, index, owner, reference); + } + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CacheKey other = (CacheKey) obj; + return index == other.index && Objects.equals(owner, other.owner) + && Objects.equals(reference, other.reference); + } + } + private static class Context { + public final EReference reference; + public final int index; + + public Context(EReference reference, int index) { + this.reference = reference; + this.index = index; + } + } + private class ExpectedTypeSwitch extends RosettaExpressionSwitch { + public RType doSwitch(RosettaExpression expr, EReference reference, int index) { + return doSwitch(expr, new Context(reference, index)); + } + + private boolean leavesItemTypeUnchanged(RosettaExpression expr) { + if (expr instanceof RosettaImplicitVariable) { + return true; + } else if ( + expr instanceof AsKeyOperation + || expr instanceof FilterOperation + || expr instanceof FlattenOperation + || expr instanceof DistinctOperation + || expr instanceof FirstOperation + || expr instanceof LastOperation + || expr instanceof MaxOperation + || expr instanceof MinOperation + || expr instanceof ReverseOperation + || expr instanceof SortOperation + || expr instanceof RosettaOnlyElement + ) { + return leavesItemTypeUnchanged(((RosettaUnaryOperation) expr).getArgument()); + } else if (expr instanceof MapOperation || expr instanceof ThenOperation) { + RosettaFunctionalOperation f = (RosettaFunctionalOperation) expr; + return leavesItemTypeUnchanged(f.getFunction().getBody()); + } + return false; + } + + @Override + protected RType caseConstructorExpression(RosettaConstructorExpression expr, Context context) { + return null; + } + + @Override + protected RType caseListLiteral(ListLiteral expr, Context context) { + if (LIST_LITERAL__ELEMENTS.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseConditionalExpression(RosettaConditionalExpression expr, Context context) { + if (ROSETTA_CONDITIONAL_EXPRESSION__IF.equals(context.reference)) { + return builtins.BOOLEAN; + } else if (ROSETTA_CONDITIONAL_EXPRESSION__IFTHEN.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } else if (ROSETTA_CONDITIONAL_EXPRESSION__ELSETHEN.equals(context.reference)) { + RType expectedType = getExpectedTypeFromContainer(expr); + if (expectedType != null) { + return expectedType; + } + return typeProvider.getRType(expr.getIfthen()); + } + return null; + } + + @Override + protected RType caseFeatureCall(RosettaFeatureCall expr, Context context) { + return null; + } + + @Override + protected RType caseDeepFeatureCall(RosettaDeepFeatureCall expr, Context context) { + return null; + } + + @Override + protected RType caseBooleanLiteral(RosettaBooleanLiteral expr, Context context) { + return null; + } + + @Override + protected RType caseIntLiteral(RosettaIntLiteral expr, Context context) { + return null; + } + + @Override + protected RType caseNumberLiteral(RosettaNumberLiteral expr, Context context) { + return null; + } + + @Override + protected RType caseStringLiteral(RosettaStringLiteral expr, Context context) { + return null; + } + + @Override + protected RType caseOnlyExists(RosettaOnlyExistsExpression expr, Context context) { + return null; + } + + @Override + protected RType caseImplicitVariable(RosettaImplicitVariable expr, Context context) { + return null; + } + + @Override + protected RType caseSymbolReference(RosettaSymbolReference expr, Context context) { + if (ROSETTA_SYMBOL_REFERENCE__RAW_ARGS.equals(context.reference)) { + RosettaSymbol symbol = expr.getSymbol(); + if (symbol instanceof Function) { + Function fun = (Function)symbol; + return typeProvider.getRTypeOfSymbol(fun.getInputs().get(context.index)); + } else if (symbol instanceof RosettaRule) { + RosettaRule rule = (RosettaRule)symbol; + return typeSystem.typeCallToRType(rule.getInput()); + } + } + return null; + } + + @Override + protected RType caseAddOperation(ArithmeticOperation expr, Context context) { + return null; + } + + @Override + protected RType caseSubtractOperation(ArithmeticOperation expr, Context context) { + return null; + } + + @Override + protected RType caseMultiplyOperation(ArithmeticOperation expr, Context context) { + return builtins.UNCONSTRAINED_NUMBER; + } + + @Override + protected RType caseDivideOperation(ArithmeticOperation expr, Context context) { + return builtins.UNCONSTRAINED_NUMBER; + } + + @Override + protected RType caseJoinOperation(JoinOperation expr, Context context) { + return builtins.UNCONSTRAINED_STRING; + } + + @Override + protected RType caseAndOperation(LogicalOperation expr, Context context) { + return builtins.BOOLEAN; + } + + @Override + protected RType caseOrOperation(LogicalOperation expr, Context context) { + return builtins.BOOLEAN; + } + + @Override + protected RType caseLessThanOperation(ComparisonOperation expr, Context context) { + return null; + } + + @Override + protected RType caseLessThanOrEqualOperation(ComparisonOperation expr, Context context) { + return null; + } + + @Override + protected RType caseGreaterThanOperation(ComparisonOperation expr, Context context) { + return null; + } + + @Override + protected RType caseGreaterThanOrEqualOperation(ComparisonOperation expr, Context context) { + return null; + } + + @Override + protected RType caseEqualsOperation(EqualityOperation expr, Context context) { + if (ROSETTA_BINARY_OPERATION__RIGHT.equals(context.reference)) { + return typeProvider.getRType(expr.getLeft()); + } + return null; + } + + @Override + protected RType caseNotEqualsOperation(EqualityOperation expr, Context context) { + if (ROSETTA_BINARY_OPERATION__RIGHT.equals(context.reference)) { + return typeProvider.getRType(expr.getLeft()); + } + return null; + } + + @Override + protected RType caseContainsOperation(RosettaContainsExpression expr, Context context) { + if (ROSETTA_BINARY_OPERATION__RIGHT.equals(context.reference)) { + return typeProvider.getRType(expr.getLeft()); + } + return null; + } + + @Override + protected RType caseDisjointOperation(RosettaDisjointExpression expr, Context context) { + if (ROSETTA_BINARY_OPERATION__RIGHT.equals(context.reference)) { + return typeProvider.getRType(expr.getLeft()); + } + return null; + } + + @Override + protected RType caseDefaultOperation(DefaultOperation expr, Context context) { + if (ROSETTA_BINARY_OPERATION__RIGHT.equals(context.reference)) { + return typeProvider.getRType(expr.getLeft()); + } + return null; + } + + @Override + protected RType caseAsKeyOperation(AsKeyOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseChoiceOperation(ChoiceOperation expr, Context context) { + return null; + } + + @Override + protected RType caseOneOfOperation(OneOfOperation expr, Context context) { + return null; + } + + @Override + protected RType caseAbsentOperation(RosettaAbsentExpression expr, Context context) { + return null; + } + + @Override + protected RType caseCountOperation(RosettaCountOperation expr, Context context) { + return null; + } + + @Override + protected RType caseExistsOperation(RosettaExistsExpression expr, Context context) { + return null; + } + + @Override + protected RType caseDistinctOperation(DistinctOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseFirstOperation(FirstOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseFlattenOperation(FlattenOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseLastOperation(LastOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseReverseOperation(ReverseOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseOnlyElementOperation(RosettaOnlyElement expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseSumOperation(SumOperation expr, Context context) { + return null; + } + + @Override + protected RType caseToStringOperation(ToStringOperation expr, Context context) { + return null; + } + + @Override + protected RType caseToNumberOperation(ToNumberOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return builtins.UNCONSTRAINED_STRING; + } + return null; + } + + @Override + protected RType caseToIntOperation(ToIntOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return builtins.UNCONSTRAINED_STRING; + } + return null; + } + + @Override + protected RType caseToTimeOperation(ToTimeOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return builtins.UNCONSTRAINED_STRING; + } + return null; + } + + @Override + protected RType caseToEnumOperation(ToEnumOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return builtins.UNCONSTRAINED_STRING; + } + return null; + } + + @Override + protected RType caseToDateOperation(ToDateOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return builtins.UNCONSTRAINED_STRING; + } + return null; + } + + @Override + protected RType caseToDateTimeOperation(ToDateTimeOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return builtins.UNCONSTRAINED_STRING; + } + return null; + } + + @Override + protected RType caseToZonedDateTimeOperation(ToZonedDateTimeOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return builtins.UNCONSTRAINED_STRING; + } + return null; + } + + @Override + protected RType caseFilterOperation(FilterOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseMapOperation(MapOperation expr, Context context) { + InlineFunction f = expr.getFunction(); + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference) && f != null && leavesItemTypeUnchanged(f.getBody())) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseMaxOperation(MaxOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseMinOperation(MinOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseReduceOperation(ReduceOperation expr, Context context) { + return null; + } + + @Override + protected RType caseSortOperation(SortOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseThenOperation(ThenOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference) && leavesItemTypeUnchanged(expr.getFunction().getBody())) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + } + } +} diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/REnumType.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/REnumType.java index adf67176c..e7b3f612d 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/REnumType.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/REnumType.java @@ -16,8 +16,14 @@ package com.regnosys.rosetta.types; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; +import com.regnosys.rosetta.rosetta.RosettaEnumValue; import com.regnosys.rosetta.rosetta.RosettaEnumeration; import com.regnosys.rosetta.utils.ModelIdProvider; import com.rosetta.model.lib.ModelSymbolId; @@ -26,14 +32,17 @@ public class REnumType extends RAnnotateType implements RObject { private final RosettaEnumeration enumeration; private ModelSymbolId symbolId = null; + private List parents = null; private final ModelIdProvider modelIdProvider; + private final RObjectFactory objectFactory; - public REnumType(final RosettaEnumeration enumeration, final ModelIdProvider modelIdProvider) { + public REnumType(final RosettaEnumeration enumeration, final ModelIdProvider modelIdProvider, final RObjectFactory objectFactory) { super(); this.enumeration = enumeration; this.modelIdProvider = modelIdProvider; + this.objectFactory = objectFactory; } @Override @@ -48,6 +57,51 @@ public ModelSymbolId getSymbolId() { public RosettaEnumeration getEObject() { return this.enumeration; } + + public List getParents() { + if (parents == null) { + parents = enumeration.getParentEnums().stream().map(e -> objectFactory.buildREnumType(e)).collect(Collectors.toList()); + } + return parents; + } + /** + * Get a list of all parents of this enum type, including itself. + * + * The list is ordered from the most top-level enumeration to the least (i.e., itself). In case of multiple parents, + * the order is left-to-right depth-first. + */ + public List getAllParents() { + LinkedHashSet reversedResult = new LinkedHashSet<>(); + doGetAllParents(this, reversedResult); + List result = reversedResult.stream().collect(Collectors.toCollection(ArrayList::new)); + Collections.reverse(result); + return result; + } + private void doGetAllParents(REnumType current, LinkedHashSet parents) { + if (parents.add(current)) { + current.getParents().forEach(p -> doGetAllParents(p, parents)); + } + } + + /** + * Get a list of the enum values defined in this enumeration. This does not include enum values of any parents. + */ + public List getOwnEnumValues() { + return enumeration.getEnumValues(); + } + + /** + * Get a list of all enum values of this enumeration, including all enum values of its parents. + * + * The list starts with the enum values of the top-most enumeration, and ends with the enum values of itself. In case of multiple parents, + * the order is left-to-right depth-first. + */ + public List getAllEnumValues() { + return getAllParents() + .stream() + .flatMap(p -> p.getOwnEnumValues().stream()) + .collect(Collectors.toList()); + } @Override public int hashCode() { diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RObjectFactory.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RObjectFactory.java index 0aafc507e..170aec83c 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RObjectFactory.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RObjectFactory.java @@ -213,6 +213,6 @@ public RDataType buildRDataType(Data data, List additionalAttributes return new RDataType(data, modelIdProvider, this, additionalAttributes); } public REnumType buildREnumType(RosettaEnumeration enumeration) { - return new REnumType(enumeration, modelIdProvider); + return new REnumType(enumeration, modelIdProvider, this); } } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RUnionType.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RUnionType.java new file mode 100644 index 000000000..29b1362ea --- /dev/null +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RUnionType.java @@ -0,0 +1,60 @@ +package com.regnosys.rosetta.types; + +import java.util.List; +import java.util.Objects; + +import com.rosetta.model.lib.ModelSymbolId; + +/** + * An `RType` representing an unknown join of multiple types. + * A Rune expression should never have this type - it is purely internal to Rune's type system, + * so code generators can ignore it. + */ +public class RUnionType extends RType { + private final List types; + + public RUnionType(List types) { + this.types = types; + } + public RUnionType(RType... types) { + this(List.of(types)); + } + + public List getTypes() { + return types; + } + + @Override + public ModelSymbolId getSymbolId() { + return null; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder("one of "); + for (int i = 0; i mayBeEmpty(RDataType d) keepTypeAliasIfPossible(RType t1, RType t2, BiFunction combineUnderlyingTypes): RType @@ -66,8 +65,8 @@ auxiliary ancestors(Data t) { } auxiliary ancestorEnums(RosettaEnumeration t) { getAll(t, - RosettaPackage::eINSTANCE.rosettaEnumeration_SuperType, - RosettaPackage::eINSTANCE.rosettaEnumeration_SuperType, + RosettaPackage::eINSTANCE.rosettaEnumeration_ParentEnums, + RosettaPackage::eINSTANCE.rosettaEnumeration_ParentEnums, typeof(RosettaEnumeration) ) } @@ -90,13 +89,6 @@ auxiliary listJoin(RListType t1, RListType t2) { val sup = join(t1.itemType, t2.itemType); return createListType(sup, union(t1.constraint, t2.constraint)) } -auxiliary allEnumValues(RosettaEnumeration e) { - if (e.superType === null) { - return e.enumValues; - } else { - return allEnumValues(e.superType) + e.enumValues; - } -} auxiliary mayBeEmpty(RDataType t) { t.allAttributes.forall[ cardinality.minBound === 0 @@ -144,7 +136,7 @@ auxiliary allFeatures(RType t, ResourceSet resourceSet) { RDataType: t.allNonOverridenAttributes.map[EObject] REnumType: - t.EObject.allEnumValues + t.allEnumValues RRecordType: { if (resourceSet !== null) { builtinTypes.toRosettaType(t, RosettaRecordType, resourceSet).features diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeChecking.xsemantics b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeChecking.xsemantics index 4504432ff..5f896c8cc 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeChecking.xsemantics +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeChecking.xsemantics @@ -385,50 +385,6 @@ checkrule CheckIfConditionalExpression for from { looseListSubtypeCheck(e.^if, singleBoolean) } -checkrule CheckBodyConditionalExpression for - RosettaConditionalExpression e -from { - var RListType tthen - var RListType telse - empty |- e.ifthen : tthen or tthen = null - empty |- e.elsethen : telse or telse = null - if (tthen !== null && telse !== null) { - val joined = listJoin(tthen, telse) - - if ({empty |- ANY <: joined.itemType}) { - fail error "Types `" + tthen.itemType.name + "` and `" + telse.itemType.name + "` do not have a common supertype." - } - } -} - -checkrule CheckListLiteral for - ListLiteral e -from { - val telems = newArrayList - if (e.elements.forall[ - var RListType telem - empty |- it : telem or telem = null - if (telem !== null) { - telems.add(telem) - } - telem !== null - ]) { - telems.fold(emptyNothing, [ RListType acc, RListType telem | - if (acc === null) { - null - } else { - val sup = join(telem.itemType, acc.itemType); - if ({empty |- ANY <: sup}) { - null - } else { - createListType(sup, telem.constraint + acc.constraint) - } - } - ]) !== null - or - fail error "Elements do not have a common supertype: " + telems.join(', ')["`" + it.itemType.name + "`"] + "." - } -} checkrule CheckRosettaSymbolReference for RosettaSymbolReference e from { val f = e.symbol diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeProvider.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeProvider.xtend index 7af8c1966..3f60bb1bd 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeProvider.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeProvider.xtend @@ -81,6 +81,7 @@ import com.regnosys.rosetta.rosetta.TypeParameter import com.regnosys.rosetta.rosetta.simple.AssignPathRoot import com.regnosys.rosetta.rosetta.RosettaCallableWithArgs import com.regnosys.rosetta.RosettaEcoreUtil +import org.eclipse.xtend2.lib.StringConcatenationClient class RosettaTypeProvider extends RosettaExpressionSwitch> { public static String EXPRESSION_RTYPE_CACHE_KEY = RosettaTypeProvider.canonicalName + ".EXPRESSION_RTYPE" @@ -95,6 +96,7 @@ class RosettaTypeProvider extends RosettaExpressionSwitch cycleTracker) { val types = expr.elements.map[RType].filter[it !== null] val joined = types.join + val unique = newLinkedHashSet(types) + val StringConcatenationClient failedList = '''«FOR t: unique.take(unique.size-1) SEPARATOR ", "»`«t»`«ENDFOR» and `«unique.last»`''' if (joined == ANY) { - new RErrorType(types.groupBy[name].keySet.join(', ')) + new RErrorType('''Types «failedList» do not have a common supertype.''') + } else if (joined instanceof RUnionType) { + val expected = expr.expectedTypeFromContainer + if (expected !== null && joined.isSubtypeOf(expected)) { + expected + } else { + new RErrorType('''Cannot infer common supertype of «failedList».''') + } } else { joined } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/SubtypeRelation.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/SubtypeRelation.java index 2b65c3285..5206a03fc 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/SubtypeRelation.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/SubtypeRelation.java @@ -16,7 +16,7 @@ public class SubtypeRelation { @Inject private RBuiltinTypeService builtins; - public boolean isSubtypeOf(RType t1, RType t2) { + public boolean isSubtypeOf(RType t1, RType t2) { if (t1.equals(t2)) { return true; } else if (t1.equals(builtins.NOTHING) || t2.equals(builtins.ANY)) { @@ -31,6 +31,12 @@ public boolean isSubtypeOf(RType t1, RType t2) { return false; } return isSubtypeOf(st, t2); + } else if (t1 instanceof RUnionType) { + return ((RUnionType)t1).getTypes().stream().allMatch(t -> isSubtypeOf(t, t2)); + } else if (t2 instanceof RUnionType) { + return ((RUnionType)t2).getTypes().stream().anyMatch(t -> isSubtypeOf(t1, t)); + } else if (t2 instanceof REnumType) { + return ((REnumType)t2).getParents().stream().anyMatch(p -> isSubtypeOf(t1, p)); } else if (t1 instanceof RAliasType) { return isSubtypeOf(((RAliasType)t1).getRefersTo(), t2); } else if (t2 instanceof RAliasType) { @@ -50,6 +56,14 @@ public RType join(RType t1, RType t2) { return join((RStringType)t1, (RStringType)t2); } else if (t1 instanceof RDataType && t2 instanceof RDataType) { return join((RDataType)t1, (RDataType)t2); + } else if (t1 instanceof REnumType && t2 instanceof REnumType) { + return join((REnumType)t1, (REnumType)t2); + } else if (t1 instanceof RUnionType && t2 instanceof RUnionType) { + return join((RUnionType)t1, (RUnionType)t2); + } else if (t1 instanceof RUnionType) { + return join((RUnionType)t1, t2); + } else if (t2 instanceof RUnionType) { + return join(t1, (RUnionType)t2); } else if (t1 instanceof RAliasType && t2 instanceof RAliasType) { return join((RAliasType)t1, (RAliasType)t2); } else if (t1 instanceof RAliasType) { @@ -73,6 +87,53 @@ public RType join(RDataType t1, RDataType t2) { return joinByTraversingAncestorsAndAliases(t1, t2); } } + public RType join(REnumType t1, REnumType t2) { + if (isSubtypeOf(t1, t2)) { + return t2; + } else if (isSubtypeOf(t2, t1)) { + return t1; + } + return new RUnionType(t1, t2); + } + public RType join(RUnionType t1, RUnionType t2) { + ArrayList allTypes = new ArrayList<>(t1.getTypes().size() + t2.getTypes().size()); + allTypes.addAll(t1.getTypes()); + allTypes.addAll(t2.getTypes()); + return joinWithUnion(allTypes); + } + public RType join(RUnionType t1, RType t2) { + ArrayList allTypes = new ArrayList<>(t1.getTypes().size() + 1); + allTypes.addAll(t1.getTypes()); + allTypes.add(t2); + return joinWithUnion(allTypes); + } + public RType join(RType t1, RUnionType t2) { + ArrayList allTypes = new ArrayList<>(t2.getTypes().size() + 1); + allTypes.add(t1); + allTypes.addAll(t2.getTypes()); + return joinWithUnion(allTypes); + } + private RType joinWithUnion(ArrayList types) { + // Trim types which are the subtype of any other type in the union + for (int i=types.size()-1; i>=0; i--) { + RType toCheck = types.get(i); + for (int j=0; j types) { RType acc = builtins.NOTHING; for (RType t: types) { - acc = join(acc, t); + acc = subtypeRelation.join(acc, t); if (acc.equals(builtins.ANY)) { return acc; } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/RosettaTypeSwitch.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/RosettaTypeSwitch.java index f1f83d4fc..3135eba8a 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/RosettaTypeSwitch.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/RosettaTypeSwitch.java @@ -23,6 +23,7 @@ import com.regnosys.rosetta.types.RErrorType; import com.regnosys.rosetta.types.RParametrizedType; import com.regnosys.rosetta.types.RType; +import com.regnosys.rosetta.types.RUnionType; import com.regnosys.rosetta.types.builtin.RBasicType; import com.regnosys.rosetta.types.builtin.RBuiltinTypeService; import com.regnosys.rosetta.types.builtin.RDateTimeType; @@ -53,6 +54,8 @@ protected Return doSwitch(RType type, Context context) { return doSwitch((RParametrizedType)type, context); } else if (type instanceof RRecordType) { return doSwitch((RRecordType)type, context); + } else if (type instanceof RUnionType) { + return caseUnionType((RUnionType)type, context); } throw errorMissedCase(type); } @@ -108,6 +111,8 @@ protected Return doSwitch(RRecordType type, Context context) { protected abstract Return caseAliasType(RAliasType type, Context context); + protected abstract Return caseUnionType(RUnionType type, Context context); + protected abstract Return caseNumberType(RNumberType type, Context context); protected abstract Return caseStringType(RStringType type, Context context); protected abstract Return caseBooleanType(RBasicType type, Context context); diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend index 57a40be0f..3863b7cc4 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend @@ -533,7 +533,7 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { @Check def checkEnumValuesAreUnique(RosettaEnumeration enumeration) { val name2attr = HashMultimap.create - enumeration.allEnumValues.forEach [ + enumeration.buildREnumType.allEnumValues.forEach [ name2attr.put(name, it) ] for (value : enumeration.enumValues) { @@ -936,7 +936,7 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { if (structured.nullOrEmpty) return val mostUsedEnum = structured.max[$0.value.size <=> $1.value.size].key - val toImplement = mostUsedEnum.allEnumValues.map[name].toSet + val toImplement = mostUsedEnum.buildREnumType.allEnumValues.map[name].toSet enumsUsed.get(mostUsedEnum).forEach [ toImplement.remove(it.key) ] diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/function/FunctionGeneratorTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/function/FunctionGeneratorTest.xtend index 7eb321388..978d06c32 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/function/FunctionGeneratorTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/function/FunctionGeneratorTest.xtend @@ -42,6 +42,50 @@ class FunctionGeneratorTest { @Inject extension ModelHelper @Inject extension ValidationTestHelper + @Test + def void testExtendedEnumTypeCoercion() { + val code = ''' + enum A: + VALUE_A + + enum B extends A: + VALUE_B + + enum C extends A: + VALUE_C + + enum D extends B, C: + VALUE_D + + func TestCoercion: + inputs: + b B (1..1) + c C (1..1) + output: + result D (1..1) + + set result: + if True + then b + else if True + then c + else A -> VALUE_A + '''.generateCode + + val classes = code.compileToClasses + + val bClass = classes.get("com.rosetta.test.model.B") + val valueB = bClass.enumConstants.findFirst[c| c.toString == "VALUE_B"] + val cClass = classes.get("com.rosetta.test.model.C") + val valueC = cClass.enumConstants.findFirst[c| c.toString == "VALUE_C"] + + val dClass = classes.get("com.rosetta.test.model.D") + val valueBOfDClass = dClass.enumConstants.findFirst[c| c.toString == "VALUE_B"] + + val testCoercion = classes.createFunc("TestCoercion") + assertEquals(valueBOfDClass, testCoercion.invokeFunc(valueBOfDClass.class, #[valueB, valueC])) + } + @Test def void onlyExistsOnAbsentParent() { val code = ''' diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/RosettaExtensionsTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/RosettaExtensionsTest.xtend index f13473357..7d27fc80e 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/RosettaExtensionsTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/RosettaExtensionsTest.xtend @@ -13,14 +13,12 @@ import org.junit.jupiter.api.^extension.ExtendWith import static org.junit.jupiter.api.Assertions.* import javax.inject.Inject import com.regnosys.rosetta.types.RObjectFactory -import com.regnosys.rosetta.RosettaEcoreUtil @ExtendWith(InjectionExtension) @InjectWith(RosettaInjectorProvider) class RosettaExtensionsTest { - @Inject extension ParseHelper - @Inject extension RosettaEcoreUtil + @Inject extension ParseHelper @Inject extension RObjectFactory @Test @@ -65,12 +63,13 @@ class RosettaExtensionsTest { enum Baz extends Bar: baz '''.parse - val foo = model.elements.filter(RosettaEnumeration).head() - val bar = model.elements.filter(RosettaEnumeration).get(1) - val baz = model.elements.filter(RosettaEnumeration).last() - assertEquals(#{foo, bar, baz}, baz.allSuperEnumerations) - assertEquals(#{foo, bar}, bar.allSuperEnumerations) - assertEquals(#{foo}, foo.allSuperEnumerations) + val elems = model.elements.filter(RosettaEnumeration).map[buildREnumType] + val foo = elems.head() + val bar = elems.get(1) + val baz = elems.last() + assertEquals(#{foo, bar, baz}, baz.allParents.toSet) + assertEquals(#{foo, bar}, bar.allParents.toSet) + assertEquals(#{foo}, foo.allParents.toSet) assertEquals(#['baz', 'bar', 'foo0', 'foo1'], baz.allEnumValues.map[name].toList) assertEquals(#['bar', 'foo0', 'foo1'], bar.allEnumValues.map[name].toList) assertEquals(#['foo0', 'foo1'], foo.allEnumValues.map[name].toList) diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaParsingTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaParsingTest.xtend index 03204d613..87ffbfcca 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaParsingTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaParsingTest.xtend @@ -34,6 +34,30 @@ class RosettaParsingTest { @Inject extension ValidationTestHelper @Inject extension ExpressionParser + @Test + def void testAccessEnumValueOfMultiEnumExtension() { + val model = ''' + enum A: + VALUE_A + + enum B: + VALUE_B + + enum C extends A, B: + VALUE_C + '''.parseRosettaWithNoIssues + + "C -> VALUE_C" + .parseExpression(#[model]) + .assertNoIssues + "C -> VALUE_A" + .parseExpression(#[model]) + .assertNoIssues + "C -> VALUE_B" + .parseExpression(#[model]) + .assertNoIssues + } + @Test def void testFullyQualifiedNamesCanBeUsedInExpression() { val modelBar = ''' diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/types/SubtypeRelationTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/types/SubtypeRelationTest.xtend new file mode 100644 index 000000000..fcdf032bb --- /dev/null +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/types/SubtypeRelationTest.xtend @@ -0,0 +1,72 @@ +package com.regnosys.rosetta.types + +import org.eclipse.xtext.testing.extensions.InjectionExtension +import org.junit.jupiter.api.^extension.ExtendWith +import com.regnosys.rosetta.rosetta.simple.Data +import com.regnosys.rosetta.tests.RosettaInjectorProvider +import org.eclipse.xtext.testing.InjectWith +import javax.inject.Inject +import com.regnosys.rosetta.tests.util.ModelHelper + +import static org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import com.regnosys.rosetta.rosetta.RosettaModel +import com.regnosys.rosetta.rosetta.RosettaNamed +import com.regnosys.rosetta.rosetta.RosettaEnumeration + +@ExtendWith(InjectionExtension) +@InjectWith(RosettaInjectorProvider) +class SubtypeRelationTest { + @Inject extension SubtypeRelation + @Inject extension ModelHelper + @Inject extension RObjectFactory + + private def Data getData(RosettaModel model, String name) { + return model.elements.filter(RosettaNamed).findFirst[it.name == name] as Data + } + private def RosettaEnumeration getEnum(RosettaModel model, String name) { + return model.elements.filter(RosettaNamed).findFirst[it.name == name] as RosettaEnumeration + } + + @Test + def testExtendedTypeIsSubtype() { + val model = ''' + type A: + type B extends A: + '''.parseRosettaWithNoIssues + + val a = model.getData('A').buildRDataType + val b = model.getData('B').buildRDataType + + assertTrue(b.isSubtypeOf(a)) + } + + @Test + def testJoinTypeHierarchy() { + val model = ''' + type A: + type B extends A: + type C extends A: + type D extends C: + '''.parseRosettaWithNoIssues + + val a = model.getData('A').buildRDataType + val b = model.getData('B').buildRDataType + val d = model.getData('D').buildRDataType + + assertEquals(a, join(b, d)) + } + + @Test + def testExtendedEnumIsSupertype() { + val model = ''' + enum A: + enum B extends A: + '''.parseRosettaWithNoIssues + + val a = model.getEnum('A').buildREnumType + val b = model.getEnum('B').buildREnumType + + assertTrue(a.isSubtypeOf(b)) + } +} From d079e5c44b7e74783b771ce0b62ce214313ec42a Mon Sep 17 00:00:00 2001 From: Simon Cockx Date: Mon, 9 Sep 2024 14:37:32 +0200 Subject: [PATCH 02/14] WIP --- .../rosetta/generator/java/types/RJavaEnum.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnum.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnum.java index 915010eb0..13c92fab5 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnum.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnum.java @@ -16,6 +16,7 @@ package com.regnosys.rosetta.generator.java.types; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -28,10 +29,21 @@ public class RJavaEnum extends JavaClass { private final REnumType enumeration; + private List enumValues = null; public RJavaEnum(REnumType enumeration) { this.enumeration = enumeration; } + + public List getEnumValues() { + if (enumValues == null) { + enumValues = new ArrayList<>(); + for (REnumType p : enumeration.getParents()) { + + } + } + return enumValues; + } @Override public boolean isSubtypeOf(JavaType other) { From 41d3347f3c77aaba6539b98160dde5471c9fc8e4 Mon Sep 17 00:00:00 2001 From: Simon Cockx Date: Tue, 10 Sep 2024 14:04:13 +0200 Subject: [PATCH 03/14] Added support for enum extensions in Java --- .../generator/java/enums/EnumGenerator.xtend | 86 +++++++++++++------ .../generator/java/enums/EnumHelper.xtend | 6 +- .../java/expression/ExpressionGenerator.xtend | 4 +- .../java/expression/TypeCoercionService.xtend | 10 ++- .../generator/java/types/RJavaEnum.java | 26 +++++- .../generator/java/types/RJavaEnumValue.java | 38 ++++++++ 6 files changed, 136 insertions(+), 34 deletions(-) diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend index 422db67e3..89454877a 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend @@ -13,10 +13,12 @@ import javax.inject.Inject import org.eclipse.xtend2.lib.StringConcatenationClient import org.eclipse.xtext.generator.IFileSystemAccess2 -import static com.regnosys.rosetta.generator.java.enums.EnumHelper.* import static com.regnosys.rosetta.generator.java.util.ModelGeneratorUtil.* import com.regnosys.rosetta.types.REnumType import com.regnosys.rosetta.generator.java.types.JavaTypeTranslator +import com.regnosys.rosetta.generator.java.types.RJavaEnum +import java.util.List +import java.util.Set class EnumGenerator { @Inject extension ImportManagerExtension @@ -29,23 +31,24 @@ class EnumGenerator { private def String toJava(REnumType e, RootPackage root, String version) { val scope = new JavaScope(root) - val clazz = e.toJavaReferenceType + val javaEnum = e.toJavaReferenceType as RJavaEnum val StringConcatenationClient classBody = ''' «javadoc(e.EObject, version)» @«RosettaEnum»("«e.name»") - public enum «clazz» { + public enum «javaEnum» { - «FOR value: e.allEnumValues SEPARATOR ',\n' AFTER ';'» - «javadoc(value)» - «value.contributeAnnotations» - @«com.rosetta.model.lib.annotations.RosettaEnumValue»(value = "«value.name»"«IF value.display !== null», displayName = "«value.display»"«ENDIF») «convertValuesWithDisplay(value)» + «FOR value: javaEnum.enumValues SEPARATOR ',\n' AFTER ';'» + «javadoc(value.EObject)» + «value.EObject.contributeAnnotations» + @«com.rosetta.model.lib.annotations.RosettaEnumValue»(value = "«value.rosettaName»"«IF value.displayName !== null», displayName = "«value.displayName»"«ENDIF») + «value.name»("«value.rosettaName»", «value.displayName ?: 'null'») «ENDFOR» - private static «Map»<«String», «e.name»> values; + private static «Map»<«String», «javaEnum»> values; static { - «Map»<«String», «e.name»> map = new «ConcurrentHashMap»<>(); - for («e.name» instance : «e.name».values()) { + «Map»<«String», «javaEnum»> map = new «ConcurrentHashMap»<>(); + for («javaEnum» instance : «javaEnum».values()) { map.put(instance.toDisplayString(), instance); } values = «Collections».unmodifiableMap(map); @@ -53,31 +56,44 @@ class EnumGenerator { private final «String» rosettaName; private final «String» displayName; - - «e.name»(«String» rosettaName) { - this(rosettaName, null); - } - «e.name»(«String» rosettaName, «String» displayName) { + «javaEnum»(«String» rosettaName, «String» displayName) { this.rosettaName = rosettaName; this.displayName = displayName; } - public static «e.name» fromDisplayName(String name) { - «e.name» value = values.get(name); + public static «javaEnum» fromDisplayName(String name) { + «javaEnum» value = values.get(name); if (value == null) { throw new «IllegalArgumentException»("No enum constant with display name \"" + name + "\"."); } return value; } - - «FOR p : e.allParents» - «val parentClass = p.toJavaReferenceType» - «val fromScope = scope.methodScope("from" + parentClass.simpleName)» - «val fromParam = fromScope.createUniqueIdentifier(parentClass.simpleName.toFirstLower)» - public static «clazz» from«parentClass»(«parentClass» «fromParam») { + «val visitedAncestors = javaEnum.parents.toSet» + «FOR p : javaEnum.parents» - } + «val fromScope = scope.methodScope("from" + p.simpleName)» + «val fromParam = fromScope.createUniqueIdentifier(p.simpleName.toFirstLower)» + public static «javaEnum» from«p.simpleName»(«p» «fromParam») { + switch («fromParam») { + «FOR v : p.enumValues» + case «v.name»: return «v.name»; + «ENDFOR» + } + return null; + } + + «val toScope = scope.methodScope("to" + p.simpleName)» + «val toParam = toScope.createUniqueIdentifier(javaEnum.simpleName.toFirstLower)» + public static «p» to«p.simpleName»(«javaEnum» «toParam») { + switch («toParam») { + «FOR v : p.enumValues» + case «v.name»: return «p».«v.name»; + «ENDFOR» + } + return null; + } + «ancestorConversions(javaEnum, p, p.parents, visitedAncestors, scope)» «ENDFOR» @Override @@ -94,6 +110,28 @@ class EnumGenerator { buildClass(root, classBody, scope) } + private def StringConcatenationClient ancestorConversions(RJavaEnum javaEnum, RJavaEnum currentParent, List ancestors, Set visitedAncestors, JavaScope scope) { + ''' + «FOR a : ancestors» + «IF visitedAncestors.add(a)» + + «val fromScope = scope.methodScope("from" + a.simpleName)» + «val fromParam = fromScope.createUniqueIdentifier(a.simpleName.toFirstLower)» + public static «javaEnum» from«a.simpleName»(«a» «fromParam») { + return from«currentParent.simpleName»(«currentParent».from«a.simpleName»(«fromParam»)); + } + + «val toScope = scope.methodScope("to" + a.simpleName)» + «val toParam = toScope.createUniqueIdentifier(javaEnum.simpleName.toFirstLower)» + public static «a» to«a.simpleName»(«javaEnum» «toParam») { + return «currentParent».to«a.simpleName»(to«currentParent.simpleName»(«toParam»)); + } + «ancestorConversions(javaEnum, currentParent, a.parents, visitedAncestors, scope)» + «ENDIF» + «ENDFOR» + ''' + } + private def StringConcatenationClient contributeAnnotations(RosettaEnumValue e) ''' «FOR synonym : e.enumSynonyms» diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumHelper.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumHelper.xtend index 828623c72..71cca8762 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumHelper.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumHelper.xtend @@ -8,11 +8,7 @@ import java.util.stream.Collectors class EnumHelper { - def static convertValuesWithDisplay(RosettaEnumValue enumValue) { - formatEnumName(enumValue.name) + '''("«enumValue.name»"«IF enumValue.display !== null», "«enumValue.display»"«ENDIF»)''' - } - - def static convertValues(RosettaEnumValue enumValue) { + def static convertValue(RosettaEnumValue enumValue) { formatEnumName(enumValue.name) } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend index 3a685f81a..5cf762fa4 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend @@ -97,7 +97,7 @@ import org.eclipse.emf.ecore.EObject import org.eclipse.xtend2.lib.StringConcatenationClient import org.eclipse.xtext.EcoreUtil2 -import static extension com.regnosys.rosetta.generator.java.enums.EnumHelper.convertValues +import static extension com.regnosys.rosetta.generator.java.enums.EnumHelper.convertValue import com.regnosys.rosetta.types.RObjectFactory import javax.inject.Inject import com.regnosys.rosetta.rosetta.expression.RosettaConstructorExpression @@ -273,7 +273,7 @@ class ExpressionGenerator extends RosettaExpressionSwitch { private final REnumType enumeration; + + private List parents = null; private List enumValues = null; public RJavaEnum(REnumType enumeration) { this.enumeration = enumeration; } + public List getParents() { + if (parents == null) { + parents = enumeration.getParents().stream().map(p -> new RJavaEnum(p)).collect(Collectors.toList()); + } + return parents; + } + public List getEnumValues() { if (enumValues == null) { enumValues = new ArrayList<>(); - for (REnumType p : enumeration.getParents()) { - + Set visited = new HashSet<>(); + for (RJavaEnum p : getParents()) { + for (RJavaEnumValue v : p.getEnumValues()) { + if (visited.add(v.getEObject())) { + enumValues.add(new RJavaEnumValue(this, v.getName(), v.getEObject(), v)); + } + } + } + for (RosettaEnumValue v : enumeration.getOwnEnumValues()) { + enumValues.add(new RJavaEnumValue(this, EnumHelper.convertValue(v), v, null)); } } return enumValues; diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnumValue.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnumValue.java index cf2b252fc..ab80c4133 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnumValue.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnumValue.java @@ -1,5 +1,43 @@ package com.regnosys.rosetta.generator.java.types; +import com.regnosys.rosetta.rosetta.RosettaEnumValue; + public class RJavaEnumValue { + private final RJavaEnum enumeration; + + private final String name; + private final RosettaEnumValue value; + + private final RJavaEnumValue parentValue; + + public RJavaEnumValue(RJavaEnum enumeration, String name, RosettaEnumValue value, RJavaEnumValue parentValue) { + this.enumeration = enumeration; + this.name = name; + this.value = value; + this.parentValue = parentValue; + } + + public RJavaEnum getEnumeration() { + return enumeration; + } + + public RosettaEnumValue getEObject() { + return value; + } + + public String getName() { + return name; + } + + public String getRosettaName() { + return value.getName(); + } + + public String getDisplayName() { + return value.getDisplay(); + } + public RJavaEnumValue getParentValue() { + return parentValue; + } } From a651c80b18880a7ea2b86a478ce0448f9d53d3ca Mon Sep 17 00:00:00 2001 From: Simon Cockx Date: Tue, 10 Sep 2024 15:09:49 +0200 Subject: [PATCH 04/14] Implemented additional validation --- .../rosetta/validation/EnumValidator.java | 86 +++++++++++++++++++ .../validation/RosettaSimpleValidator.xtend | 14 --- .../rosetta/validation/RosettaValidator.xtend | 2 +- .../validation/EnumValidatorTest.xtend | 66 ++++++++++++++ 4 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 rosetta-lang/src/main/java/com/regnosys/rosetta/validation/EnumValidator.java create mode 100644 rosetta-testing/src/test/java/com/regnosys/rosetta/validation/EnumValidatorTest.xtend diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/EnumValidator.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/EnumValidator.java new file mode 100644 index 000000000..1dfc8efde --- /dev/null +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/EnumValidator.java @@ -0,0 +1,86 @@ +package com.regnosys.rosetta.validation; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.eclipse.xtext.validation.Check; + +import com.regnosys.rosetta.rosetta.RosettaEnumValue; +import com.regnosys.rosetta.rosetta.RosettaEnumeration; +import com.regnosys.rosetta.types.REnumType; +import com.regnosys.rosetta.types.RObjectFactory; + +import static com.regnosys.rosetta.rosetta.RosettaPackage.Literals.*; + +public class EnumValidator extends AbstractDeclarativeRosettaValidator { + @Inject + private RObjectFactory rObjectFactory; + + @Check + public void checkCyclicExtensions(RosettaEnumeration enumeration) { + for (int i=0; i path = new ArrayList<>(); + path.add(enumeration); + Set visited = new HashSet<>(); + visited.add(enumeration); + if (hasCyclicExtension(p, path, visited)) { + String pathString = path.stream().map(e -> e.getName()).collect(Collectors.joining(" extends ")); + error("Cyclic extension: " + pathString, enumeration, ROSETTA_ENUMERATION__PARENT_ENUMS, i); + } + } + } + + private boolean hasCyclicExtension(RosettaEnumeration current, List path, Set visited) { + path.add(current); + if (visited.add(current)) { + for (RosettaEnumeration p : current.getParentEnums()) { + if (hasCyclicExtension(p, path, visited)) { + return true; + } + } + } else { + if (path.get(0).equals(path.get(path.size() - 1))) { + return true; + } + } + path.remove(path.size() - 1); + return false; + } + + @Check + public void checkEnumValuesAreUnique(RosettaEnumeration enumeration) { + REnumType t = rObjectFactory.buildREnumType(enumeration); + Set usedNames = new HashSet<>(); + t.getParents().forEach(p -> p.getAllEnumValues().forEach(v -> usedNames.add(v.getName()))); + for (RosettaEnumValue value: t.getOwnEnumValues()) { + if (!usedNames.add(value.getName())) { + error("Duplicate enum value '" + value.getName() + "'", value, ROSETTA_NAMED__NAME); + } + } + } + + @Check + public void checkEnumExtensionsDoNotHaveOverlappingEnumValueNames(RosettaEnumeration enumeration) { + REnumType t = rObjectFactory.buildREnumType(enumeration); + Map usedNames = new HashMap<>(); + for (int i=0; i 1) { - error('''Duplicate enum value '«value.name»'«»''', value, ROSETTA_NAMED__NAME, DUPLICATE_ENUM_VALUE) - } - } - } - @Check def checkFeatureNamesAreUnique(RosettaFeatureOwner ele) { ele.features.groupBy[name].forEach [ k, v | diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaValidator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaValidator.xtend index 39de06559..7c11b7b84 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaValidator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaValidator.xtend @@ -10,7 +10,7 @@ import org.eclipse.xtext.validation.ComposedChecks * * See https://www.eclipse.org/Xtext/documentation/303_runtime_concepts.html#validation */ -@ComposedChecks(validators = #[RosettaSimpleValidator, StandaloneRosettaTypingValidator]) +@ComposedChecks(validators = #[RosettaSimpleValidator, StandaloneRosettaTypingValidator, EnumValidator]) class RosettaValidator extends AbstractRosettaValidator { diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/EnumValidatorTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/EnumValidatorTest.xtend new file mode 100644 index 000000000..e14e69b16 --- /dev/null +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/EnumValidatorTest.xtend @@ -0,0 +1,66 @@ +package com.regnosys.rosetta.validation + +import com.regnosys.rosetta.tests.util.ModelHelper +import org.eclipse.xtext.testing.InjectWith +import org.eclipse.xtext.testing.extensions.InjectionExtension +import org.eclipse.xtext.testing.validation.ValidationTestHelper +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.^extension.ExtendWith + +import static com.regnosys.rosetta.rosetta.RosettaPackage.Literals.* +import static com.regnosys.rosetta.rosetta.expression.ExpressionPackage.Literals.* +import javax.inject.Inject + +@ExtendWith(InjectionExtension) +@InjectWith(MyRosettaInjectorProvider) +class EnumValidatorTest implements RosettaIssueCodes { + + @Inject extension ValidationTestHelper + @Inject extension ModelHelper + + @Test + def void testCannotHaveEnumValuesWithSameNameAsParentValue() { + val model = ''' + enum A: + MY_VALUE + + enum B extends A: + MY_VALUE + '''.parseRosetta + + model + .assertError(ROSETTA_ENUM_VALUE, null, "Duplicate enum value 'MY_VALUE'") + } + + @Test + def void testCannotExtendEnumsWithEnumValuesWithSameName() { + val model = ''' + enum A: + MY_VALUE + + enum B: + MY_VALUE + + enum C extends A, B: + '''.parseRosetta + + model + .assertError(ROSETTA_ENUMERATION, null, "The value 'MY_VALUE' of enum B overlaps with the value 'MY_VALUE' of enum A") + } + + @Test + def void testParentEnumsCannotHaveACycle() { + val model = ''' + enum A extends D: + + enum B extends A: + + enum C extends A: + + enum D extends B, C: + '''.parseRosetta + + model + .assertError(ROSETTA_ENUMERATION, null, "AAAAAAAAAA") + } +} \ No newline at end of file From 82f02a06c227dda7c038224a20ce824ca8fe423b Mon Sep 17 00:00:00 2001 From: Simon Cockx Date: Tue, 10 Sep 2024 15:25:13 +0200 Subject: [PATCH 05/14] Fixed tests --- .../generator/java/enums/EnumGenerator.xtend | 3 +- .../java/object/EnumGeneratorTest.xtend | 3 +- .../java/object/RosettaExtensionsTest.xtend | 4 +- .../rosetta/types/RosettaTypingTest.xtend | 142 +----------------- .../validation/EnumValidatorTest.xtend | 18 ++- .../validation/RosettaValidatorTest.xtend | 9 -- 6 files changed, 22 insertions(+), 157 deletions(-) diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend index 89454877a..b126bc173 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend @@ -19,6 +19,7 @@ import com.regnosys.rosetta.generator.java.types.JavaTypeTranslator import com.regnosys.rosetta.generator.java.types.RJavaEnum import java.util.List import java.util.Set +import org.apache.commons.text.StringEscapeUtils class EnumGenerator { @Inject extension ImportManagerExtension @@ -42,7 +43,7 @@ class EnumGenerator { «javadoc(value.EObject)» «value.EObject.contributeAnnotations» @«com.rosetta.model.lib.annotations.RosettaEnumValue»(value = "«value.rosettaName»"«IF value.displayName !== null», displayName = "«value.displayName»"«ENDIF») - «value.name»("«value.rosettaName»", «value.displayName ?: 'null'») + «value.name»("«value.rosettaName»", «IF value.displayName !== null»"«StringEscapeUtils.escapeJava(value.displayName)»"«ELSE»null«ENDIF») «ENDFOR» private static «Map»<«String», «javaEnum»> values; diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/EnumGeneratorTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/EnumGeneratorTest.xtend index 61eb0133f..1b1924855 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/EnumGeneratorTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/EnumGeneratorTest.xtend @@ -68,8 +68,7 @@ class EnumGeneratorTest { val testEnumCode = code.get(rootPackage + ".TestEnumWithDisplay") assertThat(testEnumCode, - allOf(containsString('''TestEnumWithDisplay(String rosettaName)'''), - containsString('''TestEnumWithDisplay(String rosettaName, String displayName)'''), + allOf(containsString('''TestEnumWithDisplay(String rosettaName, String displayName)'''), containsString('''public String toDisplayString()'''))) code.compileToClasses diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/RosettaExtensionsTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/RosettaExtensionsTest.xtend index 7d27fc80e..40f0ebefe 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/RosettaExtensionsTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/RosettaExtensionsTest.xtend @@ -70,8 +70,8 @@ class RosettaExtensionsTest { assertEquals(#{foo, bar, baz}, baz.allParents.toSet) assertEquals(#{foo, bar}, bar.allParents.toSet) assertEquals(#{foo}, foo.allParents.toSet) - assertEquals(#['baz', 'bar', 'foo0', 'foo1'], baz.allEnumValues.map[name].toList) - assertEquals(#['bar', 'foo0', 'foo1'], bar.allEnumValues.map[name].toList) + assertEquals(#['foo0', 'foo1', 'bar', 'baz'], baz.allEnumValues.map[name].toList) + assertEquals(#['foo0', 'foo1', 'bar'], bar.allEnumValues.map[name].toList) assertEquals(#['foo0', 'foo1'], foo.allEnumValues.map[name].toList) } } diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/types/RosettaTypingTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/types/RosettaTypingTest.xtend index 0c2b221a3..852d04eb5 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/types/RosettaTypingTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/types/RosettaTypingTest.xtend @@ -10,10 +10,8 @@ import com.regnosys.rosetta.tests.util.ModelHelper import com.regnosys.rosetta.rosetta.simple.Function import com.regnosys.rosetta.rosetta.expression.RosettaConditionalExpression import com.regnosys.rosetta.rosetta.simple.Data -import com.regnosys.rosetta.rosetta.expression.ArithmeticOperation import com.regnosys.rosetta.rosetta.RosettaEnumeration import com.regnosys.rosetta.rosetta.expression.LogicalOperation -import com.regnosys.rosetta.rosetta.expression.MapOperation import com.regnosys.rosetta.types.builtin.RBuiltinTypeService import java.util.Optional import java.math.BigDecimal @@ -55,7 +53,6 @@ class RosettaTypingTest { '"Some string"'.assertIsValidWithType(singleString(11, 11)) '3.14'.assertIsValidWithType(singleNumber(3, 2, "3.14", "3.14")) '1'.assertIsValidWithType(singleInt(1, "1", "1")) - 'empty'.assertIsValidWithType(emptyNothing) } @Test @@ -82,12 +79,6 @@ class RosettaTypingTest { ifthen.assertHasType(createListType(UNCONSTRAINED_INT, 2, 4)) elsethen.assertHasType(singleUnconstrainedNumber) ]]; - - model.elements.get(1) as Function => [operations.head.expression as MapOperation => [ - function.body as ArithmeticOperation => [ - left.assertHasType(singleInt(1, "1", "3")) - ] - ]]; } // TODO: test auxiliary functions @@ -158,31 +149,6 @@ class RosettaTypingTest { '1 = True' .parseExpression .assertError(null, "Types `int` and `boolean` are not comparable.") - 'empty = True' - .parseExpression - .assertError(null, "Cannot compare an empty value to a single value, as they cannot be of the same length. Perhaps you forgot to write `all` or `any` in front of the operator?") - '[1, 2] = [3, 4, 5]' - .parseExpression - .assertError(null, "Cannot compare a list with 2 items to a list with 3 items, as they cannot be of the same length.") - '[1, 2] <> [True, False, False]' - .parseExpression - .assertError(null, "Types `int` and `boolean` are not comparable.") - - '[1, 2] all = empty' - .parseExpression - .assertError(null, "Expected a single value, but got an empty value instead.") - 'empty any = empty' - .parseExpression - .assertError(null, "Expected a single value, but got an empty value instead.") - '[1, 2] all = [1, 2]' - .parseExpression - .assertError(null, "Expected a single value, but got a list with 2 items instead.") - '5 any <> [1, 2]' - .parseExpression - .assertError(null, "Expected a single value, but got a list with 2 items instead. Perhaps you meant to swap the left and right operands?") - '[3.0] any <> 5' - .parseExpression - .assertError(null, "The cardinality operator `any` is redundant when comparing two single values.") } // TODO: test arithmetic and comparisons with dates/times/etc @@ -201,19 +167,10 @@ class RosettaTypingTest { '3.0 * 4'.assertIsValidWithType(singleNumber(Optional.empty, Optional.of(1), Optional.of(new BigDecimal("12")), Optional.of(new BigDecimal("12")), Optional.empty)) '3 / 4'.assertIsValidWithType(singleUnconstrainedNumber) - - // Test loosened version - '(if False then 2 else [3, 4]) + 5'.assertIsValidWithType(singleInt(Optional.empty, Optional.of(BigInteger.valueOf(7)), Optional.of(BigInteger.valueOf(9)))) } @Test def void testArithemticOperationTypeChecking() { - '[1, 2] + 3' - .parseExpression - .assertError(null, "Expected a single value, but got a list with 2 items instead.") - 'empty - 3' - .parseExpression - .assertError(null, "Expected a single value, but got an empty value instead.") '1.5 * False' .parseExpression .assertError(null, "Expected type `number`, but got `boolean` instead.") @@ -240,68 +197,15 @@ class RosettaTypingTest { @Test def void testComparisonOperationTypeChecking() { // TODO: support date, zonedDateTime and `time`? - '[1, 2] < 3' - .parseExpression - .assertError(null, "Expected a single value, but got a list with 2 items instead.") - 'empty > 3' - .parseExpression - .assertError(null, "Expected a single value, but got an empty value instead.") '1.5 <= False' .parseExpression .assertError(null, "Expected type `number`, but got `boolean` instead.") - - '[1, 2] all >= empty' - .parseExpression - .assertError(null, "Expected a single value, but got an empty value instead.") - 'empty any < empty' - .parseExpression - .assertError(null, "Expected a single value, but got an empty value instead.") - '[1, 2] all > [1, 2]' - .parseExpression - .assertError(null, "Expected a single value, but got a list with 2 items instead.") - '5 any <= [1, 2]' - .parseExpression - .assertError(null, "Expected a single value, but got a list with 2 items instead. Perhaps you meant to swap the left and right operands?") + '5 all >= 1' .parseExpression .assertError(null, "The cardinality operator `all` is redundant when comparing two single values.") } - @Test - def void testConditionalExpressionTypeInference() { - 'if True then [1, 2] else [3.0, 4.0, 5.0, 6.0]'.assertIsValidWithType(createListType(constrainedNumber(2, 1, "1", "6"), 2, 4)); - } - - @Test - def void testConditionalExpressionTypeChecking() { - 'if [True, False] then 1 else 2' - .parseExpression - .assertError(null, "Expected a single value, but got a list with 2 items instead.") - 'if empty then 1 else 2' - .parseExpression - .assertError(null, "Expected a single value, but got an empty value instead.") - 'if True then 1 else False' - .parseExpression - .assertError(null, "Types `int` and `boolean` do not have a common supertype.") - 'if True then [1, 2, 3] else [False, True]' - .parseExpression - .assertError(null, "Types `int` and `boolean` do not have a common supertype.") - } - - @Test - def void testListLiteralTypeInference() { - '[]'.assertIsValidWithType(emptyNothing); - '[2, 4.5, 7, -3.14]'.assertIsValidWithType(createListType(constrainedNumber(3, 2, "-3.14", "7"), 4, 4)); - '[2, [1, 2], -3.14]'.assertIsValidWithType(createListType(constrainedNumber(3, 2, "-3.14", "2"), 4, 4)); - } - - @Test - def void testListLiteralTypeChecking() { - '[1, True]' - .parseExpression - .assertError(null, "Elements do not have a common supertype: `int`, `boolean`.") - } - @Test def void testFunctionCallTypeInference() { val model = ''' @@ -341,26 +245,10 @@ class RosettaTypingTest { output: result int (1..1) set result: SomeFunc(1, [False, True], True) - - func TestParamType: - output: result int (1..1) - set result: - SomeFunc(1, [2, 3]) - - func TestParamCardinality: - output: result int (1..1) - set result: - SomeFunc(1, [False, True, False, False, True]) '''.parseRosetta val expr1 = (model.elements.get(1) as Function).operations.head.expression; expr1.assertError(null, "Expected 2 arguments, but got 3 instead."); - - val expr2 = (model.elements.get(2) as Function).operations.head.expression; - expr2.assertError(null, "Expected type `boolean`, but got `int` instead."); - - val expr3 = (model.elements.get(3) as Function).operations.head.expression; - expr3.assertError(null, "Expected a list with 2 to 4 items, but got a list with 5 items instead."); } @Test @@ -461,15 +349,9 @@ class RosettaTypingTest { @Test def void testExistsTypeChecking() { - 'empty exists' - .parseExpression - .assertError(null, "Expected an optional value, but got an empty value instead.") '42 exists' .parseExpression .assertError(null, "Expected an optional value, but got a single value instead.") - '(if True then 42 else [1, 2, 3, 4, 5]) exists' - .parseExpression - .assertError(null, "Expected an optional value, but got a list with 1 to 5 items instead.") } @Test @@ -480,15 +362,9 @@ class RosettaTypingTest { @Test def void testAbsentTypeChecking() { - 'empty is absent' - .parseExpression - .assertError(null, "Expected an optional value, but got an empty value instead.") '42 is absent' .parseExpression .assertError(null, "Expected an optional value, but got a single value instead.") - '(if True then 42 else [1, 2, 3, 4, 5]) is absent' - .parseExpression - .assertError(null, "Expected an optional value, but got a list with 1 to 5 items instead.") } @Test @@ -592,27 +468,11 @@ class RosettaTypingTest { ] } - @Test - def void testOnlyElementTypeInference() { - '(if True then 0 else [1, 2]) only-element'.assertIsValidWithType(createListType(constrainedInt(1, "0", "2"), 0, 1)); - '(if True then empty else [True, False]) only-element'.assertIsValidWithType(createListType(BOOLEAN, 0, 1)); - '(if True then 0 else [1, 2, 3, 42.0]) only-element'.assertIsValidWithType(createListType(constrainedNumber(3, 1, "0", "42.0"), 0, 1)); - } - @Test def void testOnlyElementTypeChecking() { - 'empty only-element' - .parseExpression - .assertWarning(null, "Expected a list with 1 to 2 items, but got an empty value instead.") '42 only-element' .parseExpression .assertWarning(null, "Expected a list with 1 to 2 items, but got a single value instead.") - '[1, 2] only-element' - .parseExpression - .assertWarning(null, "Expected a list with 1 to 2 items, but got a list with 2 items instead.") - '(if True then empty else 42) only-element' - .parseExpression - .assertWarning(null, "Expected a list with 1 to 2 items, but got an optional value instead.") } @Test diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/EnumValidatorTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/EnumValidatorTest.xtend index e14e69b16..fcfe70833 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/EnumValidatorTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/EnumValidatorTest.xtend @@ -18,6 +18,17 @@ class EnumValidatorTest implements RosettaIssueCodes { @Inject extension ValidationTestHelper @Inject extension ModelHelper + @Test + def void testDuplicateEnumValue() { + val model = ''' + enum Foo: + BAR + BAZ + BAR + '''.parseRosetta + model.assertError(ROSETTA_ENUM_VALUE, null, "Duplicate enum value 'BAR'") + } + @Test def void testCannotHaveEnumValuesWithSameNameAsParentValue() { val model = ''' @@ -60,7 +71,10 @@ class EnumValidatorTest implements RosettaIssueCodes { enum D extends B, C: '''.parseRosetta - model - .assertError(ROSETTA_ENUMERATION, null, "AAAAAAAAAA") + model.assertError(ROSETTA_ENUMERATION, null, "Cyclic extension: A extends D extends B extends A") + model.assertError(ROSETTA_ENUMERATION, null, "Cyclic extension: B extends A extends D extends B") + model.assertError(ROSETTA_ENUMERATION, null, "Cyclic extension: C extends A extends D extends C") + model.assertError(ROSETTA_ENUMERATION, null, "Cyclic extension: D extends B extends A extends D") + model.assertError(ROSETTA_ENUMERATION, null, "Cyclic extension: D extends C extends A extends D") } } \ No newline at end of file diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/RosettaValidatorTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/RosettaValidatorTest.xtend index e51dccbbb..e1b348ad2 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/RosettaValidatorTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/RosettaValidatorTest.xtend @@ -1480,15 +1480,6 @@ class RosettaValidatorTest implements RosettaIssueCodes { '''.parseRosetta model.assertError(ATTRIBUTE, DUPLICATE_ATTRIBUTE, "Overriding attribute 'i' with type string must match the type of the attribute it overrides (int)") } - - @Test - def void testDuplicateEnumLiteral() { - val model = ''' - enum Foo: - BAR BAZ BAR - '''.parseRosetta - model.assertError(ROSETTA_ENUM_VALUE, DUPLICATE_ENUM_VALUE, 'Duplicate enum value') - } @Test def void testDuplicateType() { From 9ea28515d47b7c8f379a0d7be1dcbff3dd85b2ce Mon Sep 17 00:00:00 2001 From: Simon Cockx Date: Tue, 10 Sep 2024 15:55:08 +0200 Subject: [PATCH 06/14] Added support for enum conversions --- .../java/expression/ExpressionGenerator.xtend | 12 +++++-- .../validation/RosettaSimpleValidator.xtend | 10 +++++- .../java/function/FunctionGeneratorTest.xtend | 33 +++++++++++++++++++ .../validation/RosettaValidatorTest.xtend | 15 +++++++++ 4 files changed, 67 insertions(+), 3 deletions(-) diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend index 5cf762fa4..e2bb5b8d6 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend @@ -1039,8 +1039,16 @@ class ExpressionGenerator extends RosettaExpressionSwitch Date: Tue, 10 Sep 2024 16:51:00 +0200 Subject: [PATCH 07/14] Cleaned --- .../generator/java/enums/EnumGenerator.xtend | 2 +- .../types/RosettaExpectedTypeProvider.xtend | 47 ------------------- .../validation/RosettaSimpleValidator.xtend | 43 ++++++++--------- .../model/lib/annotations/RosettaEnum.java | 5 +- 4 files changed, 23 insertions(+), 74 deletions(-) delete mode 100644 rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaExpectedTypeProvider.xtend diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend index b126bc173..02c72a0af 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend @@ -36,7 +36,7 @@ class EnumGenerator { val StringConcatenationClient classBody = ''' «javadoc(e.EObject, version)» - @«RosettaEnum»("«e.name»") + @«RosettaEnum»(value="«e.name»"«IF !javaEnum.parents.empty», parents={«FOR p : javaEnum.parents SEPARATOR ", "»«p».class«ENDFOR»}«ENDIF») public enum «javaEnum» { «FOR value: javaEnum.enumValues SEPARATOR ',\n' AFTER ';'» diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaExpectedTypeProvider.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaExpectedTypeProvider.xtend deleted file mode 100644 index ce1a86c12..000000000 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaExpectedTypeProvider.xtend +++ /dev/null @@ -1,47 +0,0 @@ -package com.regnosys.rosetta.types - -import com.regnosys.rosetta.rosetta.expression.RosettaConditionalExpression -import com.regnosys.rosetta.rosetta.RosettaExternalFunction -import com.regnosys.rosetta.rosetta.simple.Operation -import org.eclipse.emf.ecore.EObject -import org.eclipse.emf.ecore.EReference - -import static com.regnosys.rosetta.rosetta.expression.ExpressionPackage.Literals.* -import static com.regnosys.rosetta.rosetta.simple.SimplePackage.Literals.* -import com.regnosys.rosetta.rosetta.expression.RosettaSymbolReference -import com.regnosys.rosetta.types.builtin.RBuiltinTypeService -import javax.inject.Inject - -class RosettaExpectedTypeProvider { - - @Inject extension RosettaTypeProvider - @Inject extension RBuiltinTypeService - @Inject extension TypeSystem - - def RType getExpectedType(EObject owner, EReference reference, int idx) { - switch owner { - RosettaConditionalExpression case reference == ROSETTA_CONDITIONAL_EXPRESSION__IF: - BOOLEAN - RosettaSymbolReference case reference == ROSETTA_SYMBOL_REFERENCE__RAW_ARGS: { - if(idx >= 0 && owner.symbol instanceof RosettaExternalFunction) { - val fun = (owner.symbol as RosettaExternalFunction) - if(idx >= fun.parameters.size) { - null // add error type? - } else { - val targetParam = fun.parameters.get(idx) - targetParam.typeCall.typeCallToRType - } - } - } - Operation case reference == OPERATION__EXPRESSION: { - if(owner.path === null) - owner.assignRoot.RTypeOfSymbol - else owner.pathAsSegmentList.last?.attribute?.RTypeOfSymbol - } - } - } - - def RType getExpectedType(EObject owner, EReference reference) { - owner.getExpectedType(reference, -1) - } -} diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend index e7648156a..aac1840cd 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend @@ -77,7 +77,6 @@ import com.regnosys.rosetta.types.RDataType import com.regnosys.rosetta.types.REnumType import com.regnosys.rosetta.types.RErrorType import com.regnosys.rosetta.types.RType -import com.regnosys.rosetta.types.RosettaExpectedTypeProvider import com.regnosys.rosetta.types.RosettaTypeProvider import com.regnosys.rosetta.types.TypeSystem import com.regnosys.rosetta.types.builtin.RBasicType @@ -89,7 +88,6 @@ import com.regnosys.rosetta.utils.ExternalAnnotationUtil.CollectRuleVisitor import com.regnosys.rosetta.utils.ImplicitVariableUtil import com.regnosys.rosetta.utils.RosettaConfigExtension import java.time.format.DateTimeFormatter -import java.util.List import java.util.Map import java.util.Optional import java.util.Set @@ -104,7 +102,6 @@ import org.eclipse.xtext.naming.IQualifiedNameProvider import org.eclipse.xtext.naming.QualifiedName import org.eclipse.xtext.nodemodel.INode import org.eclipse.xtext.nodemodel.util.NodeModelUtils -import org.eclipse.xtext.resource.XtextSyntaxDiagnostic import org.eclipse.xtext.resource.impl.ResourceDescriptionsProvider import org.eclipse.xtext.validation.Check @@ -118,13 +115,14 @@ import com.regnosys.rosetta.types.RObjectFactory import com.regnosys.rosetta.types.RAttribute import com.regnosys.rosetta.RosettaEcoreUtil import com.regnosys.rosetta.rosetta.expression.ToEnumOperation +import com.regnosys.rosetta.rosetta.expression.RosettaConditionalExpression +import com.regnosys.rosetta.rosetta.RosettaExternalFunction // TODO: split expression validator // TODO: type check type call arguments class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { @Inject extension RosettaEcoreUtil - @Inject extension RosettaExpectedTypeProvider @Inject extension RosettaTypeProvider @Inject extension IQualifiedNameProvider @Inject extension ResourceDescriptionsProvider @@ -449,24 +447,8 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { } @Check - def void checkTypeExpectation(EObject owner) { - if (!owner.eResource.errors.filter(XtextSyntaxDiagnostic).empty) - return; - owner.eClass.EAllReferences.filter[ROSETTA_EXPRESSION.isSuperTypeOf(it.EReferenceType)].filter [ - owner.eIsSet(it) - ]. - forEach [ ref | - val referenceValue = owner.eGet(ref) - if (ref.isMany) { - (referenceValue as List).forEach [ it, i | - val expectedType = owner.getExpectedType(ref, i) - checkType(expectedType, it, owner, ref, i) - ] - } else { - val expectedType = owner.getExpectedType(ref) - checkType(expectedType, referenceValue as RosettaExpression, owner, ref, INSIGNIFICANT_INDEX) - } - ] + def void checkConditionalExpression(RosettaConditionalExpression expr) { + checkType(BOOLEAN, expr.^if, expr, ROSETTA_CONDITIONAL_EXPRESSION__IF, INSIGNIFICANT_INDEX) } private def checkType(RType expectedType, RosettaExpression expression, EObject owner, EReference ref, int index) { @@ -684,6 +666,17 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { ROSETTA_SYMBOL_REFERENCE__RAW_ARGS, 0) } } + } else if (callable instanceof RosettaExternalFunction) { + element.args.indexed.forEach [ indexed | + val callerArg = indexed.value + val callerIdx = indexed.key + val param = callable.parameters.get(callerIdx) + checkType(param.typeCall.typeCallToRType, callerArg, element, ROSETTA_SYMBOL_REFERENCE__RAW_ARGS, callerIdx) + if(cardinality.isMulti(callerArg)) { + error('''Expecting single cardinality for parameter '«param.name»'.''', element, + ROSETTA_SYMBOL_REFERENCE__RAW_ARGS, callerIdx) + } + ] } } } else { @@ -1467,9 +1460,11 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { error('''Assign expression contains a list of lists, use flatten to create a list.''', o, OPERATION__EXPRESSION) } - val isList = cardinality.isSymbolMulti(o.path !== null + val attr = o.path !== null ? o.pathAsSegmentList.last.attribute - : o.assignRoot) + : o.assignRoot + checkType(attr.RTypeOfSymbol, expr, expr, null, INSIGNIFICANT_INDEX) + val isList = cardinality.isSymbolMulti(attr) if (o.add && !isList) { error('''Add must be used with a list.''', o, OPERATION__ASSIGN_ROOT) } diff --git a/rosetta-runtime/src/main/java/com/rosetta/model/lib/annotations/RosettaEnum.java b/rosetta-runtime/src/main/java/com/rosetta/model/lib/annotations/RosettaEnum.java index a95a024eb..9daea28cb 100644 --- a/rosetta-runtime/src/main/java/com/rosetta/model/lib/annotations/RosettaEnum.java +++ b/rosetta-runtime/src/main/java/com/rosetta/model/lib/annotations/RosettaEnum.java @@ -19,10 +19,11 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.List; @Documented @Retention(RetentionPolicy.RUNTIME) public @interface RosettaEnum { - String value() default ""; - + String value() default ""; + Class>[] parents() default {}; } From 84e009ec306987fd94594fa2281516cf7eee3655 Mon Sep 17 00:00:00 2001 From: Simon Cockx Date: Tue, 10 Sep 2024 17:37:18 +0200 Subject: [PATCH 08/14] Fixed --- rosetta-lang/model/Rosetta.xcore | 4 ---- .../rosetta/types/RosettaTypeProvider.xtend | 20 +++++++++---------- .../rosetta/types/SubtypeRelation.java | 2 +- .../validation/RosettaSimpleValidator.xtend | 2 +- .../tests/RosettaExpressionsTest.xtend | 3 +-- .../validation/RosettaValidatorTest.xtend | 12 +++++------ 6 files changed, 19 insertions(+), 24 deletions(-) diff --git a/rosetta-lang/model/Rosetta.xcore b/rosetta-lang/model/Rosetta.xcore index 7e921ecf4..82d7b33ee 100644 --- a/rosetta-lang/model/Rosetta.xcore +++ b/rosetta-lang/model/Rosetta.xcore @@ -39,10 +39,6 @@ interface RosettaNamed { interface RosettaTyped { contains TypeCall typeCall - - derived boolean isTypeInferred get { - return typeCall === null - } } interface RosettaFeature extends RosettaNamed { diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeProvider.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeProvider.xtend index 3f60bb1bd..4a11ab791 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeProvider.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeProvider.xtend @@ -182,8 +182,8 @@ class RosettaTypeProvider extends RosettaExpressionSwitch cycleTracker) { val left = expr.left var leftType = left.safeRType(cycleTracker) - if (leftType === null || leftType instanceof RErrorType) { - return leftType + if (leftType instanceof RErrorType) { + return NOTHING } val right = expr.right var rightType = right.safeRType(cycleTracker) - if (rightType === null || rightType instanceof RErrorType) { - return rightType + if (rightType instanceof RErrorType) { + return NOTHING } expr.operator.resultType(leftType, rightType) } @@ -286,10 +286,10 @@ class RosettaTypeProvider extends RosettaExpressionSwitch cycleTracker) { val ifT = expr.ifthen.safeRType(cycleTracker) val elseT = expr.elsethen.safeRType(cycleTracker) - if (ifT === null || ifT instanceof RErrorType) { - elseT - } else if (elseT === null || elseT instanceof RErrorType) { - ifT + if (ifT instanceof RErrorType) { + NOTHING + } else if (elseT instanceof RErrorType) { + NOTHING } else { val joined = join(ifT, elseT) if (joined == ANY) { diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/SubtypeRelation.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/SubtypeRelation.java index 5206a03fc..d4548b0e2 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/SubtypeRelation.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/SubtypeRelation.java @@ -16,7 +16,7 @@ public class SubtypeRelation { @Inject private RBuiltinTypeService builtins; - public boolean isSubtypeOf(RType t1, RType t2) { + public boolean isSubtypeOf(RType t1, RType t2) { if (t1.equals(t2)) { return true; } else if (t1.equals(builtins.NOTHING) || t2.equals(builtins.ANY)) { diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend index aac1840cd..32e321994 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend @@ -1463,7 +1463,7 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { val attr = o.path !== null ? o.pathAsSegmentList.last.attribute : o.assignRoot - checkType(attr.RTypeOfSymbol, expr, expr, null, INSIGNIFICANT_INDEX) + checkType(attr.RTypeOfSymbol, expr, o, OPERATION__EXPRESSION, INSIGNIFICANT_INDEX) val isList = cardinality.isSymbolMulti(attr) if (o.add && !isList) { error('''Add must be used with a list.''', o, OPERATION__ASSIGN_ROOT) diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaExpressionsTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaExpressionsTest.xtend index 08e8a7333..4f10e339d 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaExpressionsTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaExpressionsTest.xtend @@ -5,7 +5,6 @@ package com.regnosys.rosetta.tests import com.regnosys.rosetta.tests.util.CodeGeneratorTestHelper import com.regnosys.rosetta.tests.util.ModelHelper -import com.regnosys.rosetta.validation.RosettaIssueCodes import org.eclipse.xtext.testing.InjectWith import org.eclipse.xtext.testing.extensions.InjectionExtension import org.eclipse.xtext.testing.validation.ValidationTestHelper @@ -100,7 +99,7 @@ class RosettaExpressionsTest { output: result boolean (1..1) set result: test -> one + test -> two = 42 - '''.parseRosetta.assertError(ROSETTA_BINARY_OPERATION, RosettaIssueCodes.TYPE_ERROR, "Incompatible types: cannot use operator '+' with date and date.") + '''.parseRosetta.assertError(ARITHMETIC_OPERATION, null, "Incompatible types: cannot use operator '+' with date and date.") } /** diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/RosettaValidatorTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/RosettaValidatorTest.xtend index 2ceaebea0..57489bad9 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/RosettaValidatorTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/RosettaValidatorTest.xtend @@ -1306,7 +1306,7 @@ class RosettaValidatorTest implements RosettaIssueCodes { if id = True then id < 1 '''.parseRosetta - model.assertError(ROSETTA_CONDITIONAL_EXPRESSION, TYPE_ERROR, "Incompatible types: cannot use operator '<' with boolean and int.") + model.assertError(COMPARISON_OPERATION, null, "Incompatible types: cannot use operator '<' with boolean and int.") } @Test @@ -2779,16 +2779,16 @@ class RosettaValidatorTest implements RosettaIssueCodes { type Foo: x1 boolean (1..1) x2 boolean (1..1) - x3 number (1..1) + x3 number (0..1) x4 number (1..1) x5 int (1..1) - x6 string (1..1) + x6 string (0..1) '''.parseRosetta - model.assertError(ROSETTA_BINARY_OPERATION, TYPE_ERROR, "Left hand side of 'and' expression must be boolean") + model.assertError(ROSETTA_BINARY_OPERATION, null, "Left hand side of 'and' expression must be boolean") } @Test - def void shouldNotGenerateTypeErrorForExpressionInBrackets3() { + def void shouldGenerateTypeErrorForExpressionInBrackets3() { val model = ''' func FuncFoo: inputs: @@ -2803,7 +2803,7 @@ class RosettaValidatorTest implements RosettaIssueCodes { x3 number (1..1) x4 number (1..1) '''.parseRosetta - model.assertError(ROSETTA_EXISTS_EXPRESSION, TYPE_ERROR, "Left hand side of 'and' expression must be boolean") + model.assertError(LOGICAL_OPERATION, null, "Left hand side of 'and' expression must be boolean") } @Test From 04bfad05a54c4f9d820d028a8685711794168e9e Mon Sep 17 00:00:00 2001 From: Simon Cockx Date: Tue, 10 Sep 2024 17:45:19 +0200 Subject: [PATCH 09/14] Fixed --- .../java/com/regnosys/rosetta/types/RosettaTypeProvider.xtend | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeProvider.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeProvider.xtend index 4a11ab791..5dd71f1d1 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeProvider.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeProvider.xtend @@ -443,6 +443,9 @@ class RosettaTypeProvider extends RosettaExpressionSwitch cycleTracker) { + if (expr.value === null) { // In case of a parse error + return NOTHING + } constrainedNumber(expr.value.toPlainString.replaceAll("\\.|\\-", "").length, Math.max(0, expr.value.scale), expr.value, expr.value) } From 7c23c115186366df8e6c46c95f52589f4dd4df7c Mon Sep 17 00:00:00 2001 From: Simon Cockx Date: Wed, 11 Sep 2024 14:06:07 +0200 Subject: [PATCH 10/14] Added support for deprecations on enums --- rosetta-lang/model/Rosetta.xcore | 6 ++- .../java/com/regnosys/rosetta/Rosetta.xtext | 9 ++--- .../validation/RosettaSimpleValidator.xtend | 32 +++++++++++++++ .../java/object/EnumGeneratorTest.xtend | 13 ------- .../validation/EnumValidatorTest.xtend | 39 +++++++++++++++++++ 5 files changed, 79 insertions(+), 20 deletions(-) diff --git a/rosetta-lang/model/Rosetta.xcore b/rosetta-lang/model/Rosetta.xcore index 82d7b33ee..0ca095665 100644 --- a/rosetta-lang/model/Rosetta.xcore +++ b/rosetta-lang/model/Rosetta.xcore @@ -6,6 +6,8 @@ package com.regnosys.rosetta.rosetta import com.google.common.collect.Iterables import java.util.stream.Collectors +import com.regnosys.rosetta.rosetta.simple.Annotated +import com.regnosys.rosetta.rosetta.simple.RootElement import com.regnosys.rosetta.rosetta.simple.References import com.regnosys.rosetta.rosetta.simple.Data import com.regnosys.rosetta.rosetta.simple.Attribute @@ -127,13 +129,13 @@ class RosettaMetaType extends RosettaRootElement, RosettaTypedFeature, RosettaTy } -class RosettaEnumeration extends RosettaRootElement, RosettaType, RosettaDefinable, References, RosettaSymbol { +class RosettaEnumeration extends RootElement, RosettaType, RosettaDefinable, References, RosettaSymbol { refers RosettaEnumeration[] parentEnums contains RosettaSynonym[] synonyms contains RosettaEnumValue[] enumValues opposite enumeration } -class RosettaEnumValue extends RosettaSymbol, RosettaDefinable, RosettaFeature, References { +class RosettaEnumValue extends RosettaSymbol, RosettaDefinable, RosettaFeature, References, Annotated { String display contains RosettaEnumSynonym[] enumSynonyms container RosettaEnumeration enumeration opposite enumValues diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext b/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext index 56b23204f..8dadffdaa 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext @@ -94,13 +94,12 @@ Attribute: Enumeration returns RosettaEnumeration: 'enum' RosettaNamed ('extends' parentEnums+=[RosettaEnumeration|QualifiedName] (',' parentEnums+=[RosettaEnumeration|QualifiedName])*)? ':' RosettaDefinable? - References* - (synonyms += RosettaSynonym)* + (References|Annotations|synonyms+=RosettaSynonym)* enumValues += RosettaEnumValue* ; Function: - 'func' + 'func' ( RosettaNamed | ({FunctionDispatch} RosettaNamed '(' attribute=[Attribute|ValidID] ':' value=EnumValueReference')') @@ -282,8 +281,8 @@ RosettaMetaType: ; RosettaEnumValue: - RosettaNamed ('displayName' display=STRING)? RosettaDefinable? References* - (enumSynonyms += RosettaEnumSynonym)* + RosettaNamed ('displayName' display=STRING)? RosettaDefinable? + (References|Annotations|enumSynonyms+=RosettaEnumSynonym)* ; RosettaCardinality: diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend index 32e321994..930322dae 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend @@ -117,6 +117,8 @@ import com.regnosys.rosetta.RosettaEcoreUtil import com.regnosys.rosetta.rosetta.expression.ToEnumOperation import com.regnosys.rosetta.rosetta.expression.RosettaConditionalExpression import com.regnosys.rosetta.rosetta.RosettaExternalFunction +import java.util.List +import org.eclipse.emf.ecore.impl.EClassImpl // TODO: split expression validator // TODO: type check type call arguments @@ -137,6 +139,36 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { @Inject extension TypeSystem @Inject extension RosettaGrammarAccess @Inject extension RObjectFactory objectFactory + + @Check + def void deprecatedWarning(EObject object) { + val crossRefs = (object.eClass.EAllStructuralFeatures as EClassImpl.FeatureSubsetSupplier).crossReferences as List + if (crossRefs !== null) { + for (ref : crossRefs) { + if (ref.many) { + val annotatedList = (object.eGet(ref) as List).filter(Annotated) + for (var i=0; i ONE + '''.parseRosetta + + model.assertWarning(TYPE_CALL, null, "TestEnumDeprecated is deprecated") + model.assertWarning(ROSETTA_SYMBOL_REFERENCE, null, "TestEnumDeprecated is deprecated") + } + + @Test + def void supportDeprecatedAnnotationOnEnumValue() { + val model = ''' + enum TestEnumDeprecated: + ONE + [deprecated] + TWO + + func Foo: + output: + result TestEnumDeprecated (1..1) + + set result: + TestEnumDeprecated -> ONE + '''.parseRosetta + + model.assertWarning(ROSETTA_FEATURE_CALL, null, "ONE is deprecated") + } } \ No newline at end of file From 1e77b28180077efbdbea03fdd4cc31e72b4efb03 Mon Sep 17 00:00:00 2001 From: Simon Cockx Date: Wed, 11 Sep 2024 14:16:20 +0200 Subject: [PATCH 11/14] Fixed scoping issue --- .../rosetta/scoping/RosettaScopeProvider.xtend | 7 +++++++ .../regnosys/rosetta/tests/RosettaParsingTest.xtend | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/scoping/RosettaScopeProvider.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/scoping/RosettaScopeProvider.xtend index 08b42fd00..8041a5a41 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/scoping/RosettaScopeProvider.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/scoping/RosettaScopeProvider.xtend @@ -60,6 +60,7 @@ import com.regnosys.rosetta.utils.DeepFeatureCallUtil import com.regnosys.rosetta.rosetta.simple.Annotated import com.regnosys.rosetta.types.RObjectFactory import com.regnosys.rosetta.RosettaEcoreUtil +import com.regnosys.rosetta.types.REnumType /** * This class contains custom scoping description. @@ -311,6 +312,12 @@ class RosettaScopeProvider extends ImportedNamespaceAwareLocalScopeProvider { } private def IScope createExtendedFeatureScope(EObject receiver, RType receiverType) { + if (receiverType instanceof REnumType) { + if (!(receiver instanceof RosettaSymbolReference) || !((receiver as RosettaSymbolReference).symbol instanceof RosettaEnumeration)) { + return IScope.NULLSCOPE + } + } + val List allPosibilities = newArrayList allPosibilities.addAll( receiverType.allFeatures(receiver) diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaParsingTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaParsingTest.xtend index 87ffbfcca..af41803da 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaParsingTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaParsingTest.xtend @@ -34,6 +34,18 @@ class RosettaParsingTest { @Inject extension ValidationTestHelper @Inject extension ExpressionParser + @Test + def void testCannotAccessEnumValueThroughAnotherEnumValue() { + val model = ''' + enum A: + VALUE_A + '''.parseRosettaWithNoIssues + + "A -> VALUE_A -> VALUE_A" + .parseExpression(#[model]) + .assertError(ROSETTA_FEATURE_CALL, Diagnostic.LINKING_DIAGNOSTIC, "Couldn't resolve reference to RosettaFeature 'VALUE_A'.") + } + @Test def void testAccessEnumValueOfMultiEnumExtension() { val model = ''' From fb2216b912515313d087722d084208d8a70d1eaa Mon Sep 17 00:00:00 2001 From: Simon Cockx Date: Wed, 11 Sep 2024 16:29:12 +0200 Subject: [PATCH 12/14] Docs --- docs/rune-modelling-component.md | 38 +++++++++++++++++++ .../java/expression/ExpressionGenerator.xtend | 7 ++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/docs/rune-modelling-component.md b/docs/rune-modelling-component.md index 400deeb1c..7c80af49e 100644 --- a/docs/rune-modelling-component.md +++ b/docs/rune-modelling-component.md @@ -282,6 +282,26 @@ enum DayCountFractionEnum: _30_360 displayName "30/360" ``` +#### Enum extensions + +An enumeration can extend one or more other enumerations, which includes all of their values. + +```Haskell +enum ISOCurrencyCodeEnum: + AFN + EUR + GBP + USD + +enum CurrencyCodeEnum extends ISOCurrencyCodeEnum: + CNH + JEP + KID + VAL +``` + +In the example above, all values defined in `ISOCurrencyCodeEnum` are also included in `CurrencyCodeEnum`. + #### External Reference Data In some cases, a model may rely on an enumeration whose values are already defined as a static dataset in some other data model, schema or technical specification. To avoid duplicating that information and risk it becoming stale, it is possible to annotate such enumeration with the source of the reference data, using the [document reference](#document-reference) mechanism. This ensures that the enumeration information in the model is kept up-to-date with information at the source. @@ -679,6 +699,24 @@ E.g. : DayOfWeekEnum -> SAT ``` +In case of enum extensions, the name of the enum type can be either that of the actual enum or that of the parent. Consider the following scenario: + +```Haskell +enum ISOCurrencyCodeEnum: + AFN + EUR + GBP + USD + +enum CurrencyCodeEnum extends ISOCurrencyCodeEnum: + CNH + JEP + KID + VAL +``` + +In this example, both `ISOCurrencyCodeEnum -> EUR` and `CurrencyCodeEnum -> EUR` refer to the same value, and are considered equal. + #### List Constant Constants can also be declared as lists: diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend index e2bb5b8d6..67cd5cf94 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend @@ -271,9 +271,8 @@ class ExpressionGenerator extends RosettaExpressionSwitch Date: Wed, 11 Sep 2024 16:50:14 +0200 Subject: [PATCH 13/14] Fixed enum generation --- .../generator/java/expression/ExpressionGenerator.xtend | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend index 67cd5cf94..f3b5c95b1 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend @@ -272,7 +272,8 @@ class ExpressionGenerator extends RosettaExpressionSwitch Date: Thu, 12 Sep 2024 10:24:13 +0200 Subject: [PATCH 14/14] Added docs --- docs/rune-modelling-component.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/docs/rune-modelling-component.md b/docs/rune-modelling-component.md index 7c80af49e..83e0ec6a9 100644 --- a/docs/rune-modelling-component.md +++ b/docs/rune-modelling-component.md @@ -1166,21 +1166,34 @@ Rune provides five conversion operators: `to-enum`, `to-string`, `to-number`, `t Given the following enum ``` -enum Foo: +enum FooEnum: VALUE1 VALUE2 displayName "Value 2" ``` -- `Foo -> VALUE1 to-string` will result in the string `"VALUE1"`, -- `Foo -> VALUE2 to-string` will result in the string `"Value 2"`, (note that the display name is used if present) -- `"VALUE1" to-enum Foo` will result in the enum value `Foo -> VALUE1`, -- `"Value 2" to-enum Foo` will result in the enum value `Foo -> VALUE2`, (again, the display name is used if present) +- `FooEnum -> VALUE1 to-string` will result in the string `"VALUE1"`, +- `FooEnum -> VALUE2 to-string` will result in the string `"Value 2"`, (note that the display name is used if present) +- `"VALUE1" to-enum FooEnum` will result in the enum value `FooEnum -> VALUE1`, +- `"Value 2" to-enum FooEnum` will result in the enum value `FooEnum -> VALUE2`, (again, the display name is used if present) - `"-3.14" to-number` will result in the number -3.14, - `"17:05:33" to-time` will result in a value representing the local time 17 hours, 5 minutes and 33 seconds. If the conversion fails, the result is an empty value. For example, -- `"VALUE2" to-enum Foo` results in an empty value, (because `Foo -> VALUE2` has a display name "Value 2", this conversion fails) +- `"VALUE2" to-enum FooEnum` results in an empty value, (because `FooEnum -> VALUE2` has a display name "Value 2", this conversion fails) - `"3.14" to-int` results in an empty value. +In case of extended enums, the `to-enum` operation can also be used to convert an enum value to a parent enum. Consider the following enum: +``` +enum FooEnum: + VALUE1 + +enum BarEnum extends FooEnum: + VALUE2 +``` +- `BarEnum -> VALUE1 to-enum FooEnum` will result in the enum value `FooEnum -> VALUE1`, +- `BarEnum -> VALUE2 to-enum FooEnum` results in an empty value, since `VALUE2` does not exist in `FooEnum`. + +Note that conversion in the other direction - from `FooEnum` to `BarEnum` - is disallowed, since a value of type `FooEnum` can be used directly in any location where a value of type `BarEnum` is expected. + ### Keyword clashes If a model name, such as an enum value or attribute name, clashes with a Rune DSL keyword then the name must be escaped by prefixing with the `^` operator. The generated code (e.g. Java) and serialised format (e.g. JSON) will not include the `^` prefix.