diff --git a/src/plcdoc/interpreter.py b/src/plcdoc/interpreter.py index 7786fdf..87b9fc4 100644 --- a/src/plcdoc/interpreter.py +++ b/src/plcdoc/interpreter.py @@ -132,6 +132,7 @@ def _parse_file(self, filepath) -> bool: plc_item = item.tag # I.e. "POU" if plc_item not in self.XML_TYPES: + logger.warning(f"Skipping file with XML tag {plc_item}") continue # Name is repeated inside the declaration, use it from there instead @@ -139,6 +140,7 @@ def _parse_file(self, filepath) -> bool: object_model = self._parse_declaration(item) if object_model is None: + # Log entry is made inside _parse_declaration() already continue obj = PlcDeclaration(object_model, filepath) @@ -287,6 +289,8 @@ def __init__(self, meta_model: TextXMetaClass, file=None): self._objtype = "struct" elif "Union" in type_str: self._objtype = "union" + elif "Alias" in type_str: + self._objtype = "alias" else: raise ValueError(f"Could not categorize type `{type_str}`") diff --git a/src/plcdoc/st_declaration.tx b/src/plcdoc/st_declaration.tx index 2e2727f..059761f 100644 --- a/src/plcdoc/st_declaration.tx +++ b/src/plcdoc/st_declaration.tx @@ -8,12 +8,13 @@ Note: `FUNCTION*` does not have a closing call for some reason! There are a lot of `comments*=CommentAny`. This will offer a list of comments, the relevant docblocks need to be extracted later. There seems no better way to do this in TextX. -Comment captures should be move down (= more basic elements) as much as possible to limit their usage. +Comment captures should be moved down (= more basic elements) as much as possible to limit their usage. There can be dynamic expressions in variable declarations, which are very tricky to parse. Therefore expected -expressions are parsed greedily as whole string. As a consequence, blocks like argument lists will result in a long +expressions are parsed greedily as whole strings. As a consequence, blocks like argument lists will result in a long string including the parentheses and commas. */ + Declaration: types*=TypeDef properties*=Property @@ -41,7 +42,7 @@ TypeDef: ; AnyType: - TypeStruct | TypeUnion | TypeEnum + TypeStruct | TypeUnion | TypeEnum | TypeAlias ; TypeStruct: @@ -66,7 +67,8 @@ TypeEnum: CommentAny* ')' (base_type=Fqn)? - ';' + (default=EnumDefault)? + SemiColon ; EnumOption: @@ -77,6 +79,19 @@ EnumOption: (comment=CommentLine)? ; +EnumDefault: + ':=' + option=ID + // Enum default must be a literal field, it cannot be e.g. an integer +; + +TypeAlias: + base=VariableType + CommentAny* + // Catch trailing comments here, not at the end of `Variable` + SemiColon +; + /* --------------------------------------------------- */ @@ -91,7 +106,7 @@ Function: ('EXTENDS' extends=Fqn)? ('IMPLEMENTS' implements=Fqn)? (':' return=VariableType (arglist=ArgList)?)? - (';')? + (SemiColon)? lists*=VariableList ; @@ -152,7 +167,7 @@ Variable: type=VariableType (arglist=ArgList)? (AssignmentSymbol value=AssignmentValue)? - ';' + SemiColon comment=CommentLine? ; @@ -220,6 +235,13 @@ Fqn[noskipws]: /\s/*- ; +/* +Semi-colons may be repeated in valid code +*/ +SemiColon: + ';'+ +; + /* Anything that is considered a value: a literal, a variable, or e.g. a sum */ diff --git a/tests/plc_code/E_Filter.txt b/tests/plc_code/E_Filter.txt index 1090563..80843db 100644 --- a/tests/plc_code/E_Filter.txt +++ b/tests/plc_code/E_Filter.txt @@ -14,3 +14,11 @@ TYPE E_Filter : Filter697nm := 6 // Note: non-consecutive number! ) USINT; // Specify base type so API is explicit END_TYPE + +TYPE E_FilterDefault : +( + NoFilter := 0, + Filter434nm, + Filter697nm +) USINT := NoFilter; +END_TYPE diff --git a/tests/plc_code/E_Options.txt b/tests/plc_code/E_Options.txt index cb9ff7f..5927810 100644 --- a/tests/plc_code/E_Options.txt +++ b/tests/plc_code/E_Options.txt @@ -1,7 +1,15 @@ TYPE E_Options : ( - Default := 0, + Default := 0, Option1, Option2 ); END_TYPE + +TYPE E_OptionsDefault : +( + Default := 0, + Option1, + Option2 +) := DEFAULT; +END_TYPE diff --git a/tests/plc_code/FB_Variables.txt b/tests/plc_code/FB_Variables.txt index 490c5eb..8028425 100644 --- a/tests/plc_code/FB_Variables.txt +++ b/tests/plc_code/FB_Variables.txt @@ -54,6 +54,8 @@ myfloat_no_ws:REAL; mypointer2 : REFERENCE TO UDINT; mypointer3 : REFERENCE TO FB_Motor REF= _motor; + extra_semicolons : INT := 7;;;;;;;;;; + timeout1 : TIME := T#2S; timeout2 : TIME := T#12m13s14ms; diff --git a/tests/plc_code/T_ALIAS.txt b/tests/plc_code/T_ALIAS.txt new file mode 100644 index 0000000..188edcf --- /dev/null +++ b/tests/plc_code/T_ALIAS.txt @@ -0,0 +1,4 @@ +TYPE T_INTERLOCK : WORD; END_TYPE + +TYPE T_Message : STRING[50]; +END_TYPE diff --git a/tests/roots/test-plc-project/src_plc/DUTs/E_Error.TcDUT b/tests/roots/test-plc-project/src_plc/DUTs/E_Error.TcDUT new file mode 100644 index 0000000..0abf316 --- /dev/null +++ b/tests/roots/test-plc-project/src_plc/DUTs/E_Error.TcDUT @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/tests/roots/test-plc-project/src_plc/DUTs/ST_MyStruct.TcDUT b/tests/roots/test-plc-project/src_plc/DUTs/ST_MyStruct.TcDUT new file mode 100644 index 0000000..1418ba1 --- /dev/null +++ b/tests/roots/test-plc-project/src_plc/DUTs/ST_MyStruct.TcDUT @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/tests/roots/test-plc-project/src_plc/DUTs/T_ALIAS.TcDUT b/tests/roots/test-plc-project/src_plc/DUTs/T_ALIAS.TcDUT new file mode 100644 index 0000000..a5a354b --- /dev/null +++ b/tests/roots/test-plc-project/src_plc/DUTs/T_ALIAS.TcDUT @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/tests/roots/test-plc-project/src_plc/MyPLC.plcproj b/tests/roots/test-plc-project/src_plc/MyPLC.plcproj index e9ab387..a5e6a0d 100644 --- a/tests/roots/test-plc-project/src_plc/MyPLC.plcproj +++ b/tests/roots/test-plc-project/src_plc/MyPLC.plcproj @@ -1,5 +1,17 @@ + + Code + + + Code + + + Code + + + Code + Code diff --git a/tests/roots/test-plc-project/src_plc/POUs/F_SyntaxError.TcPOU b/tests/roots/test-plc-project/src_plc/POUs/F_SyntaxError.TcPOU new file mode 100644 index 0000000..ff0f605 --- /dev/null +++ b/tests/roots/test-plc-project/src_plc/POUs/F_SyntaxError.TcPOU @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/tests/test_plc_project.py b/tests/test_plc_project.py index 8214f95..51490b4 100644 --- a/tests/test_plc_project.py +++ b/tests/test_plc_project.py @@ -17,6 +17,9 @@ def test_project_interpret(app, status, warning): "functionblock": ["FB_MyBlock", "FB_SecondBlock", "PlainFunctionBlock"], "function": ["PlainFunction", "RegularFunction"], "program": ["MAIN"], + "enum": ["E_Error"], + "struct": ["ST_MyStruct"], + "alias": ["T_ALIAS"], } for objtype, objects in expected.items(): @@ -29,6 +32,8 @@ def test_project_interpret(app, status, warning): @pytest.mark.sphinx("dummy", testroot="plc-project") -def test_project_build(app, status, warning): +def test_project_build(app, status, warning, caplog): """Test building a document loading a project.""" app.builder.build_all() + # Project contains a function with an outright syntax error, but the project + # completes nonetheless. diff --git a/tests/test_st_grammar.py b/tests/test_st_grammar.py index f6569f9..f829f9a 100644 --- a/tests/test_st_grammar.py +++ b/tests/test_st_grammar.py @@ -5,7 +5,7 @@ import pytest import os -from textx import metamodel_from_file +from textx import metamodel_from_file, TextXSyntaxError import re @@ -30,6 +30,7 @@ def meta_model(): "E_Filter.txt", "Properties.txt", "Unions.txt", + "T_ALIAS.txt", "GlobalVariableList.txt", "Main.txt", ] @@ -41,8 +42,8 @@ def test_grammar_on_files(meta_model, file): filepath = os.path.realpath(tests_dir + "/plc_code/" + file) try: model = meta_model.model_from_file(filepath) - except: - pytest.fail(f"Error when analyzing the file `{file}`") + except TextXSyntaxError as err: # noqa + pytest.fail(f"Error when analyzing the file `{file}`: {err}") else: assert model is not None assert ( @@ -79,8 +80,8 @@ def test_grammar_variables(meta_model): filepath = os.path.realpath(tests_dir + "/plc_code/" + filename) try: model = meta_model.model_from_file(filepath) - except: - pytest.fail(f"Error when analyzing the file `{filename}`") + except TextXSyntaxError as err: + pytest.fail(f"Error when analyzing the file `{filename}`: {err}") else: assert model.functions fb = model.functions[0] @@ -144,12 +145,13 @@ def test_grammar_variables(meta_model): ("mypointer1", "UDINT", None, None, None, "POINTER"), ("mypointer2", "UDINT", None, None, None, "REFERENCE"), ("mypointer3", "FB_Motor", "_motor", None, None, "REFERENCE"), + ("extra_semicolons", "INT", "7", None, None, None), ("timeout1", "TIME", "T#2S", None, None, None), ("timeout2", "TIME", "T#12m13s14ms", None, None, None), ("inline1", "INT", None, None, None, None), ] - assert len(variables) == 44 + assert len(variables) == 45 for i, expected in enumerate(expected_list): assert_variable(variables[i], expected)