diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java index a47d8f5790..3f27cc0f52 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java @@ -66,6 +66,7 @@ import org.eclipse.xtext.xbase.lib.Pair; import org.eclipse.xtext.xbase.lib.StringExtensions; import org.lflang.ASTUtils; +import org.lflang.AttributeUtils; import org.lflang.InferredType; import org.lflang.diagram.synthesis.action.CollapseAllReactorsAction; import org.lflang.diagram.synthesis.action.ExpandAllReactorsAction; @@ -1383,7 +1384,7 @@ private KNode addErrorComment(KNode node, String message) { private Iterable createUserComments(EObject element, KNode targetNode) { if (getBooleanValue(SHOW_USER_LABELS)) { - String commentText = ASTUtils.findAnnotationInComments(element, "@label"); + String commentText = AttributeUtils.label(element); if (!StringExtensions.isNullOrEmpty(commentText)) { KNode comment = _kNodeExtensions.createNode(); diff --git a/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaParsingTest.java b/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaParsingTest.java index f898b76c08..22c91ef021 100644 --- a/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaParsingTest.java +++ b/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaParsingTest.java @@ -30,6 +30,8 @@ import org.eclipse.xtext.testing.InjectWith; import org.eclipse.xtext.testing.extensions.InjectionExtension; import org.eclipse.xtext.testing.util.ParseHelper; + +import org.lflang.lf.LfPackage; import org.lflang.lf.Model; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Assertions; @@ -65,4 +67,50 @@ public void checkForTarget() throws Exception { Assertions.assertNotNull(result); Assertions.assertFalse(result.eResource().getErrors().isEmpty(), "Failed to catch misspelled target keyword."); } -} \ No newline at end of file + + @Test + public void testAttributes() throws Exception { + String testCase = """ + target C; + @label("somethign", "else") + @ohio() + @a + @bdebd(a="b") + @bd("abc") + @bd("abc",) + @a(a="a", b="b") + @a(a="a", b="b",) + main reactor { + + } + """; + parseWithoutError(testCase); + } + + @Test + public void testAttributeContexts() throws Exception { + String testCase = """ + target C; + @a + main reactor(@b parm: int) { + + @ohio reaction() {==} + @ohio logical action f; + @ohio timer t; + @ohio input q: int; + @ohio output q2: int; + } + """; + parseWithoutError(testCase); + } + + private Model parseWithoutError(String s) throws Exception { + Model model = parser.parse(s); + Assertions.assertNotNull(model); + Assertions.assertTrue(model.eResource().getErrors().isEmpty(), + "Encountered unexpected error while parsing: " + + model.eResource().getErrors()); + return model; + } + +} diff --git a/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaValidationTest.java b/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaValidationTest.java index 722617c779..1561788bf6 100644 --- a/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaValidationTest.java +++ b/org.lflang.tests/src/org/lflang/tests/compiler/LinguaFrancaValidationTest.java @@ -2222,6 +2222,40 @@ public void testUnrecognizedTarget() throws Exception { "Unrecognized target: Pjthon"); } + + @Test + public void testWrongStructureForLabelAttribute() throws Exception { + String testCase = """ + target C; + @label(name="something") + main reactor { } + """; + validator.assertError(parseWithoutError(testCase), LfPackage.eINSTANCE.getAttribute(), null, + "Unknown attribute parameter."); + } + + @Test + public void testMissingName() throws Exception { + String testCase = """ + target C; + @label("something", "else") + main reactor { } + """; + validator.assertError(parseWithoutError(testCase), LfPackage.eINSTANCE.getAttribute(), null, + "Missing name for attribute parameter."); + } + + @Test + public void testWrongParamType() throws Exception { + String testCase = """ + target C; + @label(value=1) + main reactor { } + """; + validator.assertError(parseWithoutError(testCase), LfPackage.eINSTANCE.getAttribute(), null, + "Incorrect type: \"value\" should have type String."); + } + @Test public void testInitialMode() throws Exception { String testCase = """ diff --git a/org.lflang/src/org/lflang/ASTUtils.java b/org.lflang/src/org/lflang/ASTUtils.java index 0f39e701a5..d93c61c901 100644 --- a/org.lflang/src/org/lflang/ASTUtils.java +++ b/org.lflang/src/org/lflang/ASTUtils.java @@ -62,6 +62,7 @@ import org.lflang.lf.Action; import org.lflang.lf.ActionOrigin; import org.lflang.lf.Assignment; +import org.lflang.lf.Attribute; import org.lflang.lf.Code; import org.lflang.lf.Connection; import org.lflang.lf.Element; @@ -1747,16 +1748,6 @@ public static String findAnnotationInComments(EObject object, String key) { return null; } - /** - * Search for an `@label` annotation for a given reaction. - * - * @param n the reaction for which the label should be searched - * @return The annotated string if an `@label` annotation was found. `null` otherwise. - */ - public static String label(Reaction n) { - return findAnnotationInComments(n, "@label"); - } - /** * Find the main reactor and set its name if none was defined. * @param resource The resource to find the main reactor in. diff --git a/org.lflang/src/org/lflang/AstExtensions.kt b/org.lflang/src/org/lflang/AstExtensions.kt index eb53d40f3d..25d09161c9 100644 --- a/org.lflang/src/org/lflang/AstExtensions.kt +++ b/org.lflang/src/org/lflang/AstExtensions.kt @@ -258,7 +258,7 @@ val Resource.model: Model get() = this.allContents.asSequence().filterIsInstance * If the reaction is annotated with a label, then the label is returned. Otherwise, a reaction name * is generated based on its priority. */ -val Reaction.label get(): String = ASTUtils.label(this) ?: "reaction_$priority" +val Reaction.label get(): String = AttributeUtils.label(this) ?: "reaction_$priority" /** Get the priority of a receiving reaction */ val Reaction.priority diff --git a/org.lflang/src/org/lflang/AttributeUtils.java b/org.lflang/src/org/lflang/AttributeUtils.java new file mode 100644 index 0000000000..9c67b06754 --- /dev/null +++ b/org.lflang/src/org/lflang/AttributeUtils.java @@ -0,0 +1,115 @@ +/* +Copyright (c) 2022, The University of California at Berkeley. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package org.lflang; + +import java.util.List; + +import org.eclipse.emf.ecore.EObject; + +import org.lflang.lf.Action; +import org.lflang.lf.Attribute; +import org.lflang.lf.Input; +import org.lflang.lf.Output; +import org.lflang.lf.Parameter; +import org.lflang.lf.Reaction; +import org.lflang.lf.Reactor; +import org.lflang.lf.StateVar; +import org.lflang.lf.Timer; + +/** + * A helper class for processing attributes in the AST. + * @author{Shaokai Lin } + * @author{Clément Fournier, TU Dresden, INSA Rennes} + */ +public class AttributeUtils { + + /** + * Return the attributes declared on the given node. Throws + * if the node does not support declaring attributes. + * + * @throws IllegalArgumentException If the node cannot have attributes + */ + public static List getAttributes(EObject node) { + if (node instanceof Reactor) { + return ((Reactor) node).getAttributes(); + } else if (node instanceof Reaction) { + return ((Reaction) node).getAttributes(); + } else if (node instanceof Action) { + return ((Action) node).getAttributes(); + } else if (node instanceof Timer) { + return ((Timer) node).getAttributes(); + } else if (node instanceof StateVar) { + return ((StateVar) node).getAttributes(); + } else if (node instanceof Parameter) { + return ((Parameter) node).getAttributes(); + } else if (node instanceof Input) { + return ((Input) node).getAttributes(); + } else if (node instanceof Output) { + return ((Output) node).getAttributes(); + } + throw new IllegalArgumentException("Not annotatable: " + node); + } + + /** + * Return the value of the {@code @label} attribute if + * present, otherwise return null. + * + * @throws IllegalArgumentException If the node cannot have attributes + */ + public static String findLabelAttribute(EObject node) { + List attrs = getAttributes(node); + return attrs.stream() + .filter(it -> it.getAttrName().equals("label")) + .map(it -> it.getAttrParms().get(0).getValue().getStr()) + .findFirst() + .orElse(null); + + } + + /** + * Return the declared label of the node, as given by the @label + * annotation (or an @label comment). + * + * @throws IllegalArgumentException If the node cannot have attributes + */ + public static String label(EObject n) { + String fromAttr = findLabelAttribute(n); + if (fromAttr == null) { + return ASTUtils.findAnnotationInComments(n, "@label"); + } + return fromAttr; + } + + /** + * Search for an `@label` annotation for a given reaction. + * + * @param n the reaction for which the label should be searched + * @return The annotated string if an `@label` annotation was found. `null` otherwise. + */ + public static String label(Reaction n) { + return label((EObject) n); + } +} diff --git a/org.lflang/src/org/lflang/LinguaFranca.xtext b/org.lflang/src/org/lflang/LinguaFranca.xtext index e5ece03e62..7746aa822a 100644 --- a/org.lflang/src/org/lflang/LinguaFranca.xtext +++ b/org.lflang/src/org/lflang/LinguaFranca.xtext @@ -74,7 +74,7 @@ ImportedReactor: reactorClass=[Reactor] ('as' name=ID)?; * Declaration of a reactor class. */ Reactor: - {Reactor} ((federated?='federated' | main?='main')? & realtime?='realtime'?) 'reactor' (name=ID)? + {Reactor} (attributes+=Attribute)* ((federated?='federated' | main?='main')? & realtime?='realtime'?) 'reactor' (name=ID)? ('<' typeParms+=TypeParm (',' typeParms+=TypeParm)* '>')? ('(' parameters+=Parameter (',' parameters+=Parameter)* ')')? ('at' host=Host)? @@ -129,6 +129,7 @@ TargetDecl: * must be given, or a literal or code that denotes zero. */ StateVar: + (attributes+=Attribute)* (reset?='reset')? 'state' name=ID ( (':' (type=Type))? ((parens+='(' (init+=Expression (',' init+=Expression)*)? parens+=')') @@ -150,24 +151,24 @@ MethodArgument: ; Input: - mutable?='mutable'? 'input' (widthSpec=WidthSpec)? name=ID (':' type=Type)? ';'?; + (attributes+=Attribute)* mutable?='mutable'? 'input' (widthSpec=WidthSpec)? name=ID (':' type=Type)? ';'?; Output: - 'output' (widthSpec=WidthSpec)? name=ID (':' type=Type)? ';'?; + (attributes+=Attribute)* 'output' (widthSpec=WidthSpec)? name=ID (':' type=Type)? ';'?; // Timing specification for a timer: (offset, period) // Can be empty, which means (0,0) = (NOW, ONCE). // E.g. (0) or (NOW) or (NOW, ONCE) or (100, 1000) // The latter means fire with period 1000, offset 100. Timer: - 'timer' name=ID ('(' offset=Expression (',' period=Expression)? ')')? ';'?; + (attributes+=Attribute)* 'timer' name=ID ('(' offset=Expression (',' period=Expression)? ')')? ';'?; Boolean: TRUE | FALSE ; Mode: - {Mode} (initial?='initial')? 'mode' (name=ID)? + {Mode} (initial?='initial')? 'mode' (name=ID)? '{' ( (stateVars+=StateVar) | (timers+=Timer) | @@ -188,11 +189,13 @@ Mode: // For all actions, minSpacing is the minimum difference between // the tags of two subsequently scheduled events. Action: - (origin=ActionOrigin)? 'action' name=ID + (attributes+=Attribute)* + (origin=ActionOrigin)? 'action' name=ID ('(' minDelay=Expression (',' minSpacing=Expression (',' policy=STRING)? )? ')')? (':' type=Type)? ';'?; Reaction: + (attributes+=Attribute)* ('reaction') ('(' (triggers+=TriggerRef (',' triggers+=TriggerRef)*)? ')')? (sources+=VarRef (',' sources+=VarRef)*)? @@ -227,7 +230,7 @@ Instantiation: name=ID '=' 'new' (widthSpec=WidthSpec)? reactorClass=[ReactorDecl] ('<' typeParms+=TypeParm (',' typeParms+=TypeParm)* '>')? '(' (parameters+=Assignment (',' parameters+=Assignment)*)? - ')' ('at' host=Host)? ';'?; + ')' (('at' host=Host ';') | ';'?); Connection: ((leftPorts += VarRef (',' leftPorts += VarRef)*) @@ -244,6 +247,21 @@ Serializer: 'serializer' type=STRING ; +/////////// Attributes +Attribute: + '@' attrName=ID ('(' (attrParms+=AttrParm (',' attrParms+=AttrParm)* ','?)? ')')? +; + +AttrParm: + (name=ID '=')? value=AttrParmValue; + +AttrParmValue: + str=STRING + | int=SignedInt + | bool=Boolean + | float=SignedFloat +; + /////////// For target parameters KeyValuePairs: @@ -289,6 +307,7 @@ Assignment: * Parameter declaration with optional type and mandatory initialization. */ Parameter: + (attributes+=Attribute)* name=ID (':' (type=Type))? ((parens+='(' (init+=Expression (',' init+=Expression)*)? parens+=')') | (braces+='{' (init+=Expression (',' init+=Expression)*)? braces+='}') @@ -510,4 +529,4 @@ Token: '@' | // Single quotes "'" -; \ No newline at end of file +; diff --git a/org.lflang/src/org/lflang/generator/rust/RustModel.kt b/org.lflang/src/org/lflang/generator/rust/RustModel.kt index 6d46e4f9cb..8fd0340ca7 100644 --- a/org.lflang/src/org/lflang/generator/rust/RustModel.kt +++ b/org.lflang/src/org/lflang/generator/rust/RustModel.kt @@ -552,7 +552,7 @@ object RustModelBuilder { body = n.code.toText(), isStartup = n.triggers.any { it is BuiltinTriggerRef && it.type == BuiltinTrigger.STARTUP }, isShutdown = n.triggers.any { it is BuiltinTriggerRef && it.type == BuiltinTrigger.SHUTDOWN }, - debugLabel = ASTUtils.label(n), + debugLabel = AttributeUtils.label(n), loc = n.locationInfo().let { // remove code block it.copy(lfText = it.lfText.replace(TARGET_BLOCK_R, "{= ... =}")) diff --git a/org.lflang/src/org/lflang/validation/AttributeSpec.java b/org.lflang/src/org/lflang/validation/AttributeSpec.java new file mode 100644 index 0000000000..07c70af72a --- /dev/null +++ b/org.lflang/src/org/lflang/validation/AttributeSpec.java @@ -0,0 +1,189 @@ +/************* + * Copyright (c) 2019-2020, The University of California at Berkeley. + + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ***************/ + +package org.lflang.validation; + +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 org.lflang.lf.AttrParm; +import org.lflang.lf.Attribute; +import org.lflang.lf.LfPackage.Literals; + +/** + * Specification of the structure of an annotation. + * + * @author{Clément Fournier, TU Dresden, INSA Rennes} + * @author{Shaokai Lin } + */ +class AttributeSpec { + + private final Map paramSpecByName; + + public static final String VALUE_ATTR = "value"; + + /** A map from a string to a supported AttributeSpec */ + public static final Map ATTRIBUTE_SPECS_BY_NAME = new HashMap<>(); + + public AttributeSpec(List params) { + paramSpecByName = params.stream().collect(Collectors.toMap(it -> it.name, it -> it)); + } + + /** + * Check that the attribute conforms to this spec and whether + * attr has the correct name. + */ + public void check(LFValidator validator, Attribute attr) { + Set seen; + // Check to see if there is one or multiple parameters. + if (attr.getAttrParms().size() == 1 && attr.getAttrParms().get(0).getName() == null) { + // If we are in this branch, + // then the user has provided @attr("value"), + // which is a shorthand for @attr(value="value"). + AttrParamSpec valueSpec = paramSpecByName.get(VALUE_ATTR); + if (valueSpec == null) { + validator.error("Attribute doesn't have a 'value' parameter.", Literals.ATTR_PARM__NAME); + return; + } + + valueSpec.check(validator, attr.getAttrParms().get(0)); + seen = Set.of(VALUE_ATTR); + } else { + // Process multiple attributes, each of which has to be named. + seen = processNamedAttrParms(validator, attr); + } + + // Check if there are any missing parameters required by this attribute. + Map missingParams = new HashMap<>(paramSpecByName); + missingParams.keySet().removeAll(seen); + missingParams.forEach((name, paramSpec) -> { + if (!paramSpec.isOptional()) { + validator.error("Missing required attribute parameter '" + name + "'.", Literals.ATTRIBUTE__ATTR_PARMS); + } + }); + } + + /** + * Check if the attribute parameters are named, whether + * these names are known, and whether the named parameters + * conform to the param spec (whether the param has the + * right type, etc.). + * + * @param validator The current validator in use + * @param attr The attribute being checked + * @return A set of named attribute parameters the user provides + */ + private Set processNamedAttrParms(LFValidator validator, Attribute attr) { + Set seen = new HashSet<>(); + for (AttrParm parm : attr.getAttrParms()) { + if (parm.getName() == null) { + validator.error("Missing name for attribute parameter.", Literals.ATTRIBUTE__ATTR_NAME); + continue; + } + + AttrParamSpec parmSpec = paramSpecByName.get(parm.getName()); + if (parmSpec == null) { + validator.error("\"" + parm.getName() + "\"" + " is an unknown attribute parameter.", + Literals.ATTRIBUTE__ATTR_NAME); + continue; + } + // Check whether a parameter conforms to its spec. + parmSpec.check(validator, parm); + seen.add(parm.getName()); + } + return seen; + } + + /** + * The specification of the attribute parameter. + * + * @param name The name of the attribute parameter + * @param type The type of the parameter + * @param defaultValue If non-null, parameter is optional. + */ + record AttrParamSpec(String name, AttrParamType type, Object defaultValue) { + + private boolean isOptional() { + return defaultValue == null; + } + + // Check if a parameter has the right type. + // Currently only String, Int, Boolean, and Float are supported. + public void check(LFValidator validator, AttrParm parm) { + switch(type) { + case STRING: + if (parm.getValue().getStr() == null) { + validator.error("Incorrect type: \"" + parm.getName() + "\"" + " should have type String.", + Literals.ATTRIBUTE__ATTR_NAME); + } + break; + case INT: + if (parm.getValue().getInt() == null) { + validator.error("Incorrect type: \"" + parm.getName() + "\"" + " should have type Int.", + Literals.ATTRIBUTE__ATTR_NAME); + } + break; + case BOOLEAN: + if (parm.getValue().getBool() == null) { + validator.error("Incorrect type: \"" + parm.getName() + "\"" + " should have type Boolean.", + Literals.ATTRIBUTE__ATTR_NAME); + } + break; + case FLOAT: + if (parm.getValue().getFloat() == null) { + validator.error("Incorrect type: \"" + parm.getName() + "\"" + " should have type Float.", + Literals.ATTRIBUTE__ATTR_NAME); + } + break; + } + + } + } + + /** + * The type of attribute parameters currently supported. + */ + enum AttrParamType { + STRING, + INT, + BOOLEAN, + FLOAT + } + + /** + * The specs of the known annotations are declared here. + * Note: If an attribute only has one parameter, the parameter name should be "value." + */ + static { + // @label("value") + ATTRIBUTE_SPECS_BY_NAME.put("label", new AttributeSpec( + List.of(new AttrParamSpec(AttributeSpec.VALUE_ATTR, AttrParamType.STRING, null)) + )); + } +} diff --git a/org.lflang/src/org/lflang/validation/LFValidator.java b/org.lflang/src/org/lflang/validation/LFValidator.java index aa5edb07c4..59e6f4e4d2 100644 --- a/org.lflang/src/org/lflang/validation/LFValidator.java +++ b/org.lflang/src/org/lflang/validation/LFValidator.java @@ -37,10 +37,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -66,6 +68,8 @@ import org.lflang.lf.Action; import org.lflang.lf.ActionOrigin; import org.lflang.lf.Assignment; +import org.lflang.lf.AttrParm; +import org.lflang.lf.Attribute; import org.lflang.lf.BuiltinTrigger; import org.lflang.lf.BuiltinTriggerRef; import org.lflang.lf.Connection; @@ -109,6 +113,8 @@ import org.lflang.lf.WidthSpec; import org.lflang.lf.WidthTerm; import org.lflang.util.FileUtil; +import org.lflang.validation.AttributeSpec.AttrParamSpec; +import org.lflang.validation.AttributeSpec.AttrParamType; import com.google.inject.Inject; @@ -128,11 +134,11 @@ public class LFValidator extends BaseLFValidator { ////////////////////////////////////////////////////////////// //// Public check methods. - + // These methods are automatically invoked on AST nodes matching // the types of their arguments. // CheckType.FAST ensures that these checks run whenever a file is modified. - // Alternatives are CheckType.NORMAL (when saving) and + // Alternatives are CheckType.NORMAL (when saving) and // CheckType.EXPENSIVE (only when right-click, validate). // FIXME: What is the default when nothing is specified? @@ -400,7 +406,7 @@ public void checkHost(Host host) { ); } } - + @Check public void checkImport(Import imp) { if (toDefinition(imp.getReactorClasses().get(0)).eResource().getErrors().size() > 0) { @@ -416,7 +422,7 @@ public void checkImport(Import imp) { } warning("Unused import.", Literals.IMPORT__IMPORT_URI); } - + @Check public void checkImportedReactor(ImportedReactor reactor) { if (isUnused(reactor)) { @@ -430,7 +436,7 @@ public void checkImportedReactor(ImportedReactor reactor) { cycleSet.addAll(cycle); } if (dependsOnCycle(toDefinition(reactor), cycleSet, new HashSet<>())) { - error("Imported reactor '" + toDefinition(reactor).getName() + + error("Imported reactor '" + toDefinition(reactor).getName() + "' has cyclic instantiation in it.", Literals.IMPORTED_REACTOR__REACTOR_CLASS); } } @@ -954,7 +960,7 @@ public void checkReactor(Reactor reactor) throws IOException { names.add(it.getName()); } error( - String.format("Cannot extend %s due to the following conflicts: %s.", + String.format("Cannot extend %s due to the following conflicts: %s.", superClass.getName(), String.join(",", names)), Literals.REACTOR__SUPER_CLASSES ); @@ -1209,6 +1215,24 @@ public void checkVarRef(VarRef varRef) { } } + /** + * Check whether an attribute is supported + * and the validity of the attribute. + * + * @param attr The attribute being checked + */ + @Check(CheckType.FAST) + public void checkAttributes(Attribute attr) { + String name = attr.getAttrName().toString(); + AttributeSpec spec = AttributeSpec.ATTRIBUTE_SPECS_BY_NAME.get(name); + if (spec == null) { + error("Unknown attribute.", Literals.ATTRIBUTE__ATTR_NAME); + return; + } + // Check the validity of the attribute. + spec.check(this, attr); + } + @Check(CheckType.FAST) public void checkWidthSpec(WidthSpec widthSpec) { if (!this.target.supportsMultiports()) { @@ -1243,7 +1267,7 @@ public void checkInitialMode(Reactor reactor) { error("Every modal reactor requires one initial mode.", Literals.REACTOR__MODES, 0); } else if (initialModesCount > 1) { reactor.getModes().stream().filter(m -> m.isInitial()).skip(1).forEach(m -> { - error("A modal reactor can only have one initial mode.", + error("A modal reactor can only have one initial mode.", Literals.REACTOR__MODES, reactor.getModes().indexOf(m)); }); } @@ -1394,13 +1418,13 @@ public void checkStateResetWithoutInitialValue(StateVar state) { ////////////////////////////////////////////////////////////// //// Public methods. - /** - * Return the error reporter for this validator. + /** + * Return the error reporter for this validator. */ public ValidatorErrorReporter getErrorReporter() { return this.errorReporter; } - + /** * Implementation required by xtext to report validation errors. */ @@ -1408,7 +1432,7 @@ public ValidatorErrorReporter getErrorReporter() { public ValidationMessageAcceptor getMessageAcceptor() { return messageAcceptor == null ? this : messageAcceptor; } - + /** * Return a list of error messages for the target declaration. */ @@ -1416,9 +1440,21 @@ public List getTargetPropertyErrors() { return this.targetPropertyErrors; } + ////////////////////////////////////////////////////////////// + //// Protected methods. + + /** + * Generate an error message for an AST node. + */ + @Override + protected void error(java.lang.String message, + org.eclipse.emf.ecore.EStructuralFeature feature) { + super.error(message, feature); + } + ////////////////////////////////////////////////////////////// //// Private methods. - + /** * For each input, report a conflict if: * 1) the input exists and the type doesn't match; or @@ -1581,7 +1617,7 @@ private boolean isCBasedTarget() { private boolean isUnused(ImportedReactor reactor) { TreeIterator instantiations = reactor.eResource().getAllContents(); TreeIterator subclasses = reactor.eResource().getAllContents(); - + boolean instantiationsCheck = true; while (instantiations.hasNext() && instantiationsCheck) { EObject obj = instantiations.next(); @@ -1662,14 +1698,14 @@ private boolean sameType(Type type1, Type type2) { ////////////////////////////////////////////////////////////// //// Private static constants. - private static String ACTIONS_MESSAGE + private static String ACTIONS_MESSAGE = "\"actions\" is a reserved word for the TypeScript target for objects " + "(inputs, outputs, actions, timers, parameters, state, reactor definitions, " + "and reactor instantiation): "; - private static String HOST_OR_FQN_REGEX + private static String HOST_OR_FQN_REGEX = "^([a-z0-9]+(-[a-z0-9]+)*)|(([a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,})$"; - + /** * Regular expression to check the validity of IPV4 addresses (due to David M. Syzdek). */ @@ -1698,11 +1734,12 @@ private boolean sameType(Type type1, Type type2) { private static String RESERVED_MESSAGE = "Reserved words in the target language are not allowed for objects " + "(inputs, outputs, actions, timers, parameters, state, reactor definitions, and reactor instantiation): "; - + private static List SPACING_VIOLATION_POLICIES = List.of("defer", "drop", "replace"); - + private static String UNDERSCORE_MESSAGE = "Names of objects (inputs, outputs, actions, timers, parameters, " + "state, reactor definitions, and reactor instantiation) may not start with \"__\": "; - + private static String USERNAME_REGEX = "^[a-z_]([a-z0-9_-]{0,31}|[a-z0-9_-]{0,30}\\$)$"; + }