Commit 30229480 authored by Maxime Perrotin's avatar Maxime Perrotin
Browse files

Introduce syntax checking for antlr3/python3

In Python2 the syntax errors were reported as printf during the parsing,
but it is different in Python3. To detect syntax errors it is now necessary
to go recursively in the tree of nodes and look for errors after the antlr parser has completed.
parent 03248704
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
During the build of the AST this library makes a number of semantic During the build of the AST this library makes a number of semantic
checks on the SDL input mode. checks on the SDL input mode.
Copyright (c) 2012-2019 European Space Agency Copyright (c) 2012-2020 European Space Agency
Designed and implemented by Maxime Perrotin Designed and implemented by Maxime Perrotin
...@@ -179,6 +179,7 @@ lineno = lambda : currentframe().f_back.f_lineno ...@@ -179,6 +179,7 @@ lineno = lambda : currentframe().f_back.f_lineno
# as the ASN1SCC generated python AST # as the ASN1SCC generated python AST
USER_DEFINED_TYPES = dict() USER_DEFINED_TYPES = dict()
def types(): def types():
''' Return all ASN.1 and user defined types ''' ''' Return all ASN.1 and user defined types '''
ret = getattr(DV, 'types', {}).copy() ret = getattr(DV, 'types', {}).copy()
...@@ -444,8 +445,13 @@ def get_interfaces(ast, process_name): ...@@ -444,8 +445,13 @@ def get_interfaces(ast, process_name):
def get_input_string(root): def get_input_string(root):
''' Return the input string of a tree node ''' ''' Return the input string of a tree node '''
return token_stream(root).toString(root.getTokenStartIndex(), try:
res = token_stream(root).toString(root.getTokenStartIndex(),
root.getTokenStopIndex()) root.getTokenStopIndex())
return res
except AttributeError as err:
# in case there is no token_strem(root)
return ""
def error(root, msg: str) -> str: def error(root, msg: str) -> str:
...@@ -458,6 +464,32 @@ def warning(root, msg: str) -> str: ...@@ -458,6 +464,32 @@ def warning(root, msg: str) -> str:
return '{} - "{}"'.format(msg, get_input_string(root)) return '{} - "{}"'.format(msg, get_input_string(root))
def check_syntax(node: antlr3.tree.CommonTree,
recursive:bool = False) -> None:
''' Check if the ANTLR node is valid, otherwise raise an excption,
meaning there is a syntax error, and report the string that could not be
parsed '''
def check(root: antlr3.tree.CommonTree,
parent: antlr3.tree.CommonTree,
rec: bool) -> None:
if rec:
for child in root.getChildren():
check(child, parent, rec)
if isinstance(root, antlr3.tree.CommonErrorNode):
token = root.trappedException.token
token_str = token.text
line = token.line
pos = token.charPositionInLine + 1
if parent != root:
text = get_input_string(parent)
else:
text = parent.getText() # take full node to get correct line/pos
syntax_error = f'In this code:\n{text}\n' \
f'Unexpected "{token_str}" at line {line}, position {pos}'
raise SyntaxError(syntax_error)
check(node, parent=node, rec=recursive)
def tmp() -> int: def tmp() -> int:
''' Return a temporary variable name ''' ''' Return a temporary variable name '''
global TMPVAR global TMPVAR
...@@ -3735,10 +3767,7 @@ def state(root, parent, context): ...@@ -3735,10 +3767,7 @@ def state(root, parent, context):
st_x, st_y = 0, 0 st_x, st_y = 0, 0
via_stop = None via_stop = None
for child in root.getChildren(): for child in root.getChildren():
if isinstance(child, antlr3.tree.CommonErrorNode): if child.type == lexer.CIF:
# There was a parsing error
sterr.append(f"Error parsing state: {child.getText()}")
elif child.type == lexer.CIF:
# Get symbol coordinates # Get symbol coordinates
(state_def.pos_x, state_def.pos_y, (state_def.pos_x, state_def.pos_y,
state_def.width, state_def.height) = cif(child) state_def.width, state_def.height) = cif(child)
...@@ -5062,13 +5091,11 @@ def pr_file(root): ...@@ -5062,13 +5091,11 @@ def pr_file(root):
# are parsed before process definition - to get signal definitions # are parsed before process definition - to get signal definitions
# and data typess references. # and data typess references.
processes, uses, systems = [], [], [] processes, uses, systems = [], [], []
for child in root.getChildren(): for child in root.getChildren():
if node_filename(child) is not None: if node_filename(child) is not None:
ast.pr_files.add(node_filename(child)) ast.pr_files.add(node_filename(child))
if isinstance(child, antlr3.tree.CommonErrorNode): if child.type == lexer.PROCESS:
# There was a parsing error
errors.append(f"Error parsing PR file: {child.getText()}")
elif child.type == lexer.PROCESS:
processes.append(child) processes.append(child)
elif child.type == lexer.USE: elif child.type == lexer.USE:
uses.append(child) uses.append(child)
...@@ -5226,6 +5253,11 @@ def add_to_ast(ast, filename=None, string=None): ...@@ -5226,6 +5253,11 @@ def add_to_ast(ast, filename=None, string=None):
# Root of the AST is of type antlr3.tree.CommonTree # Root of the AST is of type antlr3.tree.CommonTree
# Add it as a child of the common tree # Add it as a child of the common tree
subtree = tree_rule_return_scope.tree subtree = tree_rule_return_scope.tree
try:
check_syntax(node=subtree, recursive=True)
except SyntaxError as err:
LOG.error(str(err))
raise
token_str = parser.getTokenStream() token_str = parser.getTokenStream()
children_before = set(ast.children) children_before = set(ast.children)
# addChild does not simply add the subtree - it flattens it if necessary # addChild does not simply add the subtree - it flattens it if necessary
...@@ -5246,6 +5278,7 @@ def parse_pr(files=None, string=None): ...@@ -5246,6 +5278,7 @@ def parse_pr(files=None, string=None):
common_tree = antlr3.tree.CommonTree(None) common_tree = antlr3.tree.CommonTree(None)
for filename in files: for filename in files:
sys.path.insert(0, os.path.dirname(filename)) sys.path.insert(0, os.path.dirname(filename))
try:
for filename in files: for filename in files:
err, warn = add_to_ast(common_tree, filename=filename) err, warn = add_to_ast(common_tree, filename=filename)
errors.extend(err) errors.extend(err)
...@@ -5254,6 +5287,10 @@ def parse_pr(files=None, string=None): ...@@ -5254,6 +5287,10 @@ def parse_pr(files=None, string=None):
err, warn = add_to_ast(common_tree, string=string) err, warn = add_to_ast(common_tree, string=string)
errors.extend(err) errors.extend(err)
warnings.extend(warn) warnings.extend(warn)
except SyntaxError as err:
errors.append([f"Parser error: syntax error!\n{str(err)}",
[0, 0],
['- -']])
# If syntax errors were found, raise an alarm and try to continue anyway # If syntax errors were found, raise an alarm and try to continue anyway
if errors: if errors:
...@@ -5326,6 +5363,7 @@ def parseSingleElement(elem='', string='', context=None): ...@@ -5326,6 +5363,7 @@ def parseSingleElement(elem='', string='', context=None):
root.token_stream = parser.getTokenStream() root.token_stream = parser.getTokenStream()
backend_ptr = eval(elem) backend_ptr = eval(elem)
try: try:
check_syntax(node=root, recursive=True)
t, semantic_errors, warnings = backend_ptr( t, semantic_errors, warnings = backend_ptr(
root=root, parent=None, context=context) root=root, parent=None, context=context)
except AttributeError as err: except AttributeError as err:
...@@ -5336,6 +5374,8 @@ def parseSingleElement(elem='', string='', context=None): ...@@ -5336,6 +5374,8 @@ def parseSingleElement(elem='', string='', context=None):
pass pass
except NotImplementedError as err: except NotImplementedError as err:
syntax_errors.append('Syntax error in expression - Fix it.') syntax_errors.append('Syntax error in expression - Fix it.')
except SyntaxError as err:
syntax_errors.append(str(err))
return(t, syntax_errors, semantic_errors, warnings, return(t, syntax_errors, semantic_errors, warnings,
context.terminators) context.terminators)
......
...@@ -3132,7 +3132,7 @@ def cli(options): ...@@ -3132,7 +3132,7 @@ def cli(options):
LOG.error('Too many errors, cannot generate code') LOG.error('Too many errors, cannot generate code')
else: else:
if len(ast.processes) != 1: if len(ast.processes) != 1:
LOG.error('Only one process at a time is supported') LOG.error(f'Found {len(ast.processes)} process(es) instead of one')
return 1 return 1
if options.png or options.pdf or options.svg: if options.png or options.pdf or options.svg:
......
...@@ -550,10 +550,13 @@ class Join(VerticalSymbol): ...@@ -550,10 +550,13 @@ class Join(VerticalSymbol):
''' Set auto-completion list - list of labels ''' ''' Set auto-completion list - list of labels '''
return (label.inputString for label in CONTEXT.labels) return (label.inputString for label in CONTEXT.labels)
def update_completion_list(self, pr_text): def update_completion_list(self, pr_text: str) -> None:
''' When text was entered, update list of join terminators ''' ''' When text was entered, update list of join terminators '''
ast, _, _, _, _ = self.parser.parseSingleElement(self.common_name, ast, _, _, _, _ = self.parser.parseSingleElement(self.common_name,
pr_text) pr_text)
if not ast:
# in case of syntax error in the symbol text
return
for each in (t for t in CONTEXT.terminators if t.kind == 'join'): for each in (t for t in CONTEXT.terminators if t.kind == 'join'):
if each.inputString == str(self): if each.inputString == str(self):
# Ignore if already defined # Ignore if already defined
...@@ -609,6 +612,9 @@ class ProcedureStop(Join): ...@@ -609,6 +612,9 @@ class ProcedureStop(Join):
''' When text was entered, if in a nested state update exit points ''' ''' When text was entered, if in a nested state update exit points '''
ast, _, _, _, _ = self.parser.parseSingleElement(self.common_name, ast, _, _, _, _ = self.parser.parseSingleElement(self.common_name,
pr_text) pr_text)
if not ast:
# in case of syntax error in the symbol text
return
try: try:
CONTEXT.state_exitpoints = \ CONTEXT.state_exitpoints = \
set(CONTEXT.state_exitpoints) | set(str(self)) set(CONTEXT.state_exitpoints) | set(str(self))
...@@ -678,6 +684,9 @@ class Label(VerticalSymbol): ...@@ -678,6 +684,9 @@ class Label(VerticalSymbol):
''' When text was entered, update list of labels in current context ''' ''' When text was entered, update list of labels in current context '''
ast, _, _, _, _ = self.parser.parseSingleElement(self.common_name, ast, _, _, _, _ = self.parser.parseSingleElement(self.common_name,
pr_text) pr_text)
if not ast:
# in case of syntax error in the symbol text
return
for each in CONTEXT.labels: for each in CONTEXT.labels:
if each.inputString == str(self): if each.inputString == str(self):
# Ignore if already defined # Ignore if already defined
...@@ -863,6 +872,9 @@ class TextSymbol(HorizontalSymbol): ...@@ -863,6 +872,9 @@ class TextSymbol(HorizontalSymbol):
# it it is not! - no need to investigate performance issues here # it it is not! - no need to investigate performance issues here
# Get AST for the symbol # Get AST for the symbol
ast, _, _, _, _ = self.parser.parseSingleElement('text_area', pr_text) ast, _, _, _, _ = self.parser.parseSingleElement('text_area', pr_text)
if not ast:
# in case of syntax error in the symbol text
return
try: try:
CONTEXT.variables.update(ast.variables) CONTEXT.variables.update(ast.variables)
CONTEXT.timers = list(set(CONTEXT.timers + ast.timers)) CONTEXT.timers = list(set(CONTEXT.timers + ast.timers))
...@@ -989,6 +1001,8 @@ class State(VerticalSymbol): ...@@ -989,6 +1001,8 @@ class State(VerticalSymbol):
''' When text was entered, update state completion list ''' ''' When text was entered, update state completion list '''
# Get AST for the symbol and update the context dictionnary # Get AST for the symbol and update the context dictionnary
ast, _, _, _, _ = self.parser.parseSingleElement('state', pr_text) ast, _, _, _, _ = self.parser.parseSingleElement('state', pr_text)
if ast:
# None if there were syntax errors in the symbol
for each in ast.statelist: for each in ast.statelist:
CONTEXT.mapping[each.lower()] = None CONTEXT.mapping[each.lower()] = None
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment