Passed
Push — master ( 8ff2fc...2dab2b )
by P.R.
04:18 queued 10s
created

sdoc.sdoc1.SDoc1Visitor.SDoc1Visitor.visitCmd_if()   B

Complexity

Conditions 7

Size

Total Lines 49
Code Lines 29

Duplication

Lines 49
Ratio 100 %

Code Coverage

Tests 1
CRAP Score 51.1014

Importance

Changes 0
Metric Value
eloc 29
dl 49
loc 49
ccs 1
cts 29
cp 0.0345
rs 7.784
c 0
b 0
f 0
cc 7
nop 2
crap 51.1014
1 1
import os
2 1
from typing import Any, Dict, Optional
3
4 1
import antlr4
5 1
from antlr4 import ParserRuleContext
6 1
from antlr4.Token import CommonToken
7 1
from cleo.styles import OutputStyle
8
9 1
from sdoc.antlr.sdoc1Lexer import sdoc1Lexer
10 1
from sdoc.antlr.sdoc1Parser import sdoc1Parser
11 1
from sdoc.antlr.sdoc1ParserVisitor import sdoc1ParserVisitor
12 1
from sdoc.helper.SDoc import SDoc
13 1
from sdoc.sdoc.SDocVisitor import SDocVisitor
14 1
from sdoc.sdoc1.data_type.ArrayDataType import ArrayDataType
15 1
from sdoc.sdoc1.data_type.DataType import DataType
16 1
from sdoc.sdoc1.data_type.IdentifierDataType import IdentifierDataType
17 1
from sdoc.sdoc1.data_type.IntegerDataType import IntegerDataType
18 1
from sdoc.sdoc1.data_type.StringDataType import StringDataType
19 1
from sdoc.sdoc1.error import DataTypeError
20
21
22 1 View Code Duplication
class SDoc1Visitor(sdoc1ParserVisitor, SDocVisitor):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
23
    """
24
    Visitor for SDoc level 1.
25
    """
26
27
    # ------------------------------------------------------------------------------------------------------------------
28 1
    def __init__(self, io: OutputStyle, root_dir: str = os.getcwd()):
29
        """
30
        Object constructor.
31
32
        :param str root_dir: The root directory for including sub-documents.
33
        """
34 1
        SDocVisitor.__init__(self, io)
35
36 1
        self._io: OutputStyle = io
37
        """
38
        Styled output formatter.
39
        """
40
41 1
        self._output: Any = None
42
        """
43
        Object for streaming the generated output. This object MUST implement the write method.
44
        """
45
46 1
        self._global_scope: ArrayDataType = ArrayDataType()
47
        """
48
        All defined variables at global scope.
49
        """
50
51 1
        self._include_level: int = 0
52
        """
53
        The level of including other SDoc documents.
54
        """
55
56 1
        self._options: Dict[str, int] = {'max_include_level': 100}
57
        """
58
        The options.
59
        """
60
61 1
        self._root_dir: str = root_dir
62 1
        """
63
        The root directory for including sub-documents.
64
        """
65
66
    # ------------------------------------------------------------------------------------------------------------------
67 1
    @property
68 1
    def output(self) -> Any:
69
        """
70
        Getter for output.
71
        """
72
        return self._output
73
74
    # ------------------------------------------------------------------------------------------------------------------
75 1
    @output.setter
76 1
    def output(self, output: Any) -> None:
77
        """
78
        Setter for output.
79
80
        :param any output: This object MUST implement the write method.
81
        """
82 1
        self._output = output
83
84
    # ------------------------------------------------------------------------------------------------------------------
85 1
    @property
86 1
    def include_level(self) -> int:
87
        """
88
        Getter for include_level.
89
        """
90
        return self._include_level
91
92
    # ------------------------------------------------------------------------------------------------------------------
93 1
    @include_level.setter
94 1
    def include_level(self, include_level: int) -> None:
95
        """
96
        Setter for include_level.
97
98
        :param int include_level: The include level.
99
        """
100
        self._include_level = include_level
101
102
    # ------------------------------------------------------------------------------------------------------------------
103 1
    @property
104 1
    def global_scope(self) -> ArrayDataType:
105
        """
106
        Getter for global_scope.
107
        """
108
        return self._global_scope
109
110
    # ------------------------------------------------------------------------------------------------------------------
111 1
    @global_scope.setter
112 1
    def global_scope(self, scope: ArrayDataType) -> None:
113
        """
114
        Setter for global_scope.
115
116
        :param ArrayDataType scope: The global scope.
117
        """
118
        self._global_scope = scope
119
120
    # ------------------------------------------------------------------------------------------------------------------
121 1
    def stream(self, snippet: str) -> None:
122
        """
123
        Puts an output snippet on the output stream.
124
125
        :param str snippet: The snippet to be appended to the output stream of this parser.
126
        """
127 1
        if snippet is not None:
128 1
            self._output.write(snippet)
129
130
    # ------------------------------------------------------------------------------------------------------------------
131 1
    def put_position(self, ctx: ParserRuleContext, position: str) -> None:
132
        """
133
        Puts a position SDoc2 command on the output stream.
134
135
        :param ParserRuleContext ctx: The context tree.
136
        :param str position: Either start or stop.
137
        """
138 1
        if position == 'start':
139 1
            token = ctx.start
140
        else:
141 1
            token = ctx.stop
142
143 1
        stream = token.getInputStream()
144 1
        if hasattr(stream, 'fileName'):
145
            # antlr4.FileStream.FileStream
146 1
            filename = stream.fileName  # Replace fileName with get_source_name() when implemented in ANTLR.
147
        else:
148
            # Input stream is a antlr4.InputStream.InputStream.
149
            filename = ''
150
151 1
        line_number = token.line
152 1
        column = token.column
153
154 1
        if position == 'stop':
155 1
            column += len(token.text)
156
157 1
        self.stream('\\position{{{0!s}:{1:d}.{2:d}}}'.format(SDoc.escape(filename), line_number, column))
158
159
    # ------------------------------------------------------------------------------------------------------------------
160 1
    def _data_is_true(self, data: DataType, token: Optional[CommonToken] = None) -> Optional[bool]:
161
        """
162
        Returns True if a data type evaluates to True, False if a data type evaluates to False, and None if an error
163
        occurs.
164
165
        :param DataType data: The data.
166
        :param CommonToken token: The token where data type is been used.
167
        """
168
        try:
169
            return data.is_true()
170
171
        except DataTypeError as e:
172
            self._error(str(e), token)
173
            return None
174
175
    # ------------------------------------------------------------------------------------------------------------------
176 1
    def visit(self, tree: ParserRuleContext) -> Any:
177
        """
178
        Visits a parse tree produced by sdoc1
179
180
        :param ParserRuleContext tree: The context tree.
181
        """
182 1
        self.put_position(tree, 'start')
183
184 1
        return super().visit(tree)
185
186
    # ------------------------------------------------------------------------------------------------------------------
187 1
    def visitAssignmentExpressionAssignment(self, ctx: sdoc1Parser.AssignmentExpressionAssignmentContext) -> Any:
188
        """
189
        Visit a parse tree for expression like a = b.
190
191
        :param sdoc1Parser.AssignmentExpressionAssignmentContext ctx: The context tree.
192
        """
193 1
        right_hand_side = ctx.assignmentExpression().accept(self)
194 1
        left_hand_side = ctx.postfixExpression().accept(self)
195
196
        # Left hand side must be an identifier.
197
        # @todo implement array element.
198 1
        if not isinstance(left_hand_side, IdentifierDataType):
199
            message = "Left hand side '{0!s}' is not an identifier.".format(str(left_hand_side))
200
            self._error(message, ctx.postfixExpression().start)
201
            return None
202
203 1
        try:
204 1
            value = left_hand_side.set_value(right_hand_side)
205
        except DataTypeError as e:
206
            self._error(str(e), ctx.assignmentExpression().start)
207
            return None
208
209 1
        return value
210
211
    # ------------------------------------------------------------------------------------------------------------------
212 1
    def visitLogicalAndExpressionAnd(self, ctx: sdoc1Parser.LogicalAndExpressionAndContext) -> IntegerDataType:
213
        """
214
        Visits a parse tree for expressions like 'a && b'.
215
216
        :param sdoc1Parser.LogicalAndExpressionAndContext ctx: The context tree.
217
        """
218
        a_ctx = ctx.logicalAndExpression()
219
        b_ctx = ctx.equalityExpression()
220
221
        a = a_ctx.accept(self)
222
        b = b_ctx.accept(self)
223
224
        a_is_true = self._data_is_true(a, a_ctx.start)
225
        b_is_true = self._data_is_true(b, b_ctx.start)
226
227
        return IntegerDataType(1 if a_is_true and b_is_true else 0)
228
229
    # ------------------------------------------------------------------------------------------------------------------
230 1
    def visitLogicalOrExpressionLogicalOr(self,
231
                                          ctx: sdoc1Parser.LogicalOrExpressionLogicalOrContext) -> IntegerDataType:
232
        """
233
        Visits a parse tree for expressions like 'a || b'.
234
235
        :param sdoc1Parser.LogicalOrExpressionLogicalOrContext ctx: The context tree.
236
        """
237
        a_ctx = ctx.logicalOrExpression()
238
        b_ctx = ctx.logicalAndExpression()
239
240
        a = a_ctx.accept(self)
241
        b = b_ctx.accept(self)
242
243
        a_is_true = self._data_is_true(a, a_ctx.start)
244
        b_is_true = self._data_is_true(b, b_ctx.start)
245
246
        return IntegerDataType(1 if a_is_true or b_is_true else 0)
247
248
    # ------------------------------------------------------------------------------------------------------------------
249 1
    def visitPostfixExpressionExpression(self, ctx: sdoc1Parser.PostfixExpressionExpressionContext) \
250
            -> Optional[DataType]:
251
        """
252
        Visits a parse tree for expressions like 'a[1]'.
253
254
        :param sdoc1Parser.PostfixExpressionExpressionContext ctx: The context tree.
255
        """
256
        # First get the value of key.
257 1
        expression = ctx.expression().accept(self)
258 1
        if not expression.is_defined():
259
            message = '{0!s} is not defined.'.format(ctx.expression().getSymbol())
260
            self._error(message, ctx.expression().start)
261
            return None
262
263 1
        postfix_expression = ctx.postfixExpression().accept(self)
264 1
        if not isinstance(postfix_expression, IdentifierDataType):
265
            message = "'{0!s}' is not an identifier.".format(ctx.postfixExpression().getSymbol())
266
            self._error(message, ctx.postfixExpression().start)
267
            return None
268
269 1
        return postfix_expression.get_array_element(expression)
270
271
    # ------------------------------------------------------------------------------------------------------------------
272 1
    def visitPrimaryExpressionIdentifier(self, ctx: sdoc1Parser.PrimaryExpressionIdentifierContext) \
273
            -> IdentifierDataType:
274
        """
275
        Visits a parse tree produced by sdoc1Parser#primaryExpressionIdentifier.
276
277
        :param sdoc1Parser.PrimaryExpressionIdentifierContext ctx: The context tree.
278
        """
279 1
        return IdentifierDataType(self._global_scope, ctx.EXPR_IDENTIFIER().getText())
280
281
    # ------------------------------------------------------------------------------------------------------------------
282 1
    def visitPrimaryExpressionIntegerConstant(self, ctx: sdoc1Parser.PrimaryExpressionIntegerConstantContext) \
283
            -> IntegerDataType:
284
        """
285
        Visits a parse tree produced by sdoc1Parser#PrimaryExpressionIntegerConstantContext.
286
287
        :param sdoc1Parser.PrimaryExpressionIntegerConstantContext ctx: The context tree.
288
        """
289 1
        return IntegerDataType(ctx.EXPR_INTEGER_CONSTANT().getText())
290
291
    # ------------------------------------------------------------------------------------------------------------------
292 1
    def visitPrimaryExpressionStringConstant(self, ctx: sdoc1Parser.PrimaryExpressionStringConstantContext) \
293
            -> StringDataType:
294
        """
295
        Visits a parse tree produced by sdoc1Parser#PrimaryExpressionStringConstantContext.
296
297
        :param sdoc1Parser.PrimaryExpressionStringConstantContext ctx: The context tree.
298
        """
299 1
        return StringDataType(ctx.EXPR_STRING_CONSTANT().getText()[1:-1].replace('\\\\', '\\').replace('\\\'', '\''))
300
301
    # ------------------------------------------------------------------------------------------------------------------
302 1
    def visitPrimaryExpressionSubExpression(self, ctx: sdoc1Parser.PrimaryExpressionSubExpressionContext) -> Any:
303
        """
304
        Visits a parse tree for sub-expressions like (a && b).
305
306
        :param sdoc1Parser.PrimaryExpressionSubExpressionContext ctx: The context tree.
307
        """
308
        return ctx.expression().accept(self)
309
310
    # ------------------------------------------------------------------------------------------------------------------
311 1
    def visitCmd_comment(self, ctx: sdoc1Parser.Cmd_commentContext) -> None:
312
        """
313
        Visits a parse tree produced by sdoc1Parser#cmd_comment.
314
315
        :param sdoc1Parser.Cmd_commentContext ctx: The context tree.
316
        """
317
        self.put_position(ctx, 'stop')
318
319
    # ------------------------------------------------------------------------------------------------------------------
320 1
    def visitCmd_debug(self, ctx: sdoc1Parser.Cmd_debugContext) -> None:
321
        """
322
        Visits a parse tree produced by sdoc1Parser#cmd_debug.
323
324
        :param sdoc1Parser.Cmd_debugContext ctx: The context tree.
325
        """
326 1
        expression = ctx.expression()
327
328 1
        if expression is not None:
329 1
            self._io.writeln(expression.accept(self).debug())
330
        else:
331
            self._io.writeln(self._global_scope.debug())
332
333 1
        self.put_position(ctx, 'stop')
334
335
    # ------------------------------------------------------------------------------------------------------------------
336 1
    def visitCmd_expression(self, ctx: sdoc1Parser.Cmd_expressionContext) -> None:
337
        """
338
        Visits a parse tree produced by sdoc1Parser#cmd_expression.
339
340
        :param sdoc1Parser.Cmd_expressionContext ctx: The context tree.
341
        """
342 1
        self.visitExpression(ctx.expression())
343
344 1
        self.put_position(ctx, 'stop')
345
346
    # ------------------------------------------------------------------------------------------------------------------
347 1
    def visitCmd_error(self, ctx: sdoc1Parser.Cmd_errorContext) -> None:
348
        """
349
        Visits a parse tree produced by sdoc1Parser#cmd_error.
350
351
        :param sdoc1Parser.Cmd_errorContext ctx: The parse tree.
352
        """
353
        token = ctx.ERROR().getSymbol()
354
        message = SDoc.unescape(ctx.SIMPLE_ARG().getText())
355
356
        self._error(message, token)
357
358
        self.put_position(ctx, 'stop')
359
360
    # ------------------------------------------------------------------------------------------------------------------
361 1
    def visitCmd_if(self, ctx: sdoc1Parser.Cmd_ifContext) -> None:
362
        """
363
        Visits a parse tree produced by sdoc1Parser#cmd_if.
364
365
        :param sdoc1Parser.Cmd_ifContext ctx: The parse tree.
366
        """
367
        n = ctx.getChildCount()
368
        fired = False
369
        i = 0
370
        while i < n and not fired:
371
            child = ctx.getChild(i)
372
            token_text = child.getText()
373
            i += 1
374
            if token_text in ['\\if', '\\elif']:
375
                # Skip {
376
                i += 1
377
378
                # Child is the expression to be evaluated.
379
                child = ctx.getChild(i)
380
                i += 1
381
                data = child.accept(self)
382
383
                # Skip }
384
                i += 1
385
386
                if self._data_is_true(data, child.start):
387
                    # Child is the code inside the if or elif clause.
388
                    child = ctx.getChild(i)
389
                    self.put_position(child, 'start')
390
                    i += 1
391
                    child.accept(self)
392
                    fired = True
393
394
                else:
395
                    # Skip the code inside the if or elif clause.
396
                    i += 1
397
398
            elif token_text == '\\else':
399
                # Child is the code inside the else clause.
400
                child = ctx.getChild(i)
401
                i += 1
402
403
                child.accept(self)
404
                fired = True
405
406
            elif token_text == '\\endif':
407
                pass
408
409
        self.put_position(ctx, 'stop')
410
411
    # ------------------------------------------------------------------------------------------------------------------
412 1
    def visitCmd_include(self, ctx: sdoc1Parser.Cmd_includeContext) -> None:
413
        """
414
        Includes another SDoc into this SDoc.
415
416
        :param sdoc1Parser.Cmd_includeContext ctx: The parse tree.
417
        """
418
        # Test the maximum include level.
419
        if self._include_level >= self._options['max_include_level']:
420
            message = 'Maximum include level exceeded'
421
            self._error(message, ctx.INCLUDE().getSymbol())
422
            return
423
424
        # Open a stream for the sub-document.
425
        file_name = SDoc.unescape(ctx.SIMPLE_ARG().getText())
426
        if not os.path.isabs(file_name):
427
            file_name = os.path.join(self._root_dir, file_name + '.sdoc')
428
        real_path = os.path.relpath(file_name)
429
        self._io.writeln("Including <fso>{0!s}</fso>".format(real_path))
430
        try:
431
            stream = antlr4.FileStream(file_name, 'utf-8')
432
433
            # root_dir
434
435
            # Create a new lexer and parser for the sub-document.
436
            lexer = sdoc1Lexer(stream)
437
            tokens = antlr4.CommonTokenStream(lexer)
438
            parser = sdoc1Parser(tokens)
439
            tree = parser.sdoc()
440
441
            # Create a visitor.
442
            visitor = SDoc1Visitor(self._io, root_dir=os.path.dirname(os.path.realpath(file_name)))
443
444
            # Set or inherit properties from the parser of the parent document.
445
            visitor.include_level = self._include_level + 1
446
            visitor.output = self._output
447
            visitor.global_scope = self._global_scope
448
449
            # Run the visitor on the parse tree.
450
            visitor.visit(tree)
451
452
            # Copy  properties from the child document.
453
            self._errors += visitor.errors
454
455
            self.put_position(ctx, 'stop')
456
        except FileNotFoundError as e:
457
            message = 'Unable to open file {0!s}.\nCause: {1!s}'.format(real_path, e)
458
            self._error(message, ctx.INCLUDE().getSymbol())
459
460
    # ------------------------------------------------------------------------------------------------------------------
461 1
    def visitCmd_notice(self, ctx: sdoc1Parser.Cmd_noticeContext) -> None:
462
        """
463
        Visits a parse tree produced by sdoc1Parser#cmd_notice.
464
465
        :param sdoc1Parser.Cmd_noticeContext ctx: The parse tree.
466
        """
467
        token = ctx.NOTICE().getSymbol()
468
        filename = token.getInputStream().fileName  # Replace fileName with get_source_name() when implemented in ANTLR.
469
        line_number = token.line
470
        message = SDoc.unescape(ctx.SIMPLE_ARG().getText())
471
472
        self._io.writeln(
473
                '<notice>Notice: {0!s} at {1!s}:{2:d}</notice>'.format(message, os.path.relpath(filename), line_number))
474
475
        self.put_position(ctx, 'stop')
476
477
    # ------------------------------------------------------------------------------------------------------------------
478 1
    def visitCmd_substitute(self, ctx: sdoc1Parser.Cmd_substituteContext) -> None:
479
        """
480
        Visit a parse tree produced by sdoc1Parser#cmd_substitute.
481
482
        :param sdoc1Parser.Cmd_substituteContext ctx:  The parse tree.
483
        """
484 1
        expression = ctx.expression()
485 1
        self.stream(expression.accept(self).get_value())
486
487 1
        self.put_position(ctx, 'stop')
488
489
    # ------------------------------------------------------------------------------------------------------------------
490 1
    def visitCmd_sdoc2(self, ctx: sdoc1Parser.Cmd_sdoc2Context) -> None:
491
        """
492
        Visits a parse tree produced by sdoc1Parser#sdoc2_cmd.
493
494
        :param sdoc1Parser.Cmd_sdoc2Context ctx: The parse tree.
495
        """
496 1
        self.stream(ctx.SDOC2_COMMAND().getText())
497
498
    # ------------------------------------------------------------------------------------------------------------------
499 1
    def visitText(self, ctx: sdoc1Parser.TextContext) -> None:
500
        """
501
        Visits a parse tree produced by sdoc1Parser#text.
502
503
        :param sdoc1Parser.TextContext ctx: The parse tree.
504
        """
505 1
        self.stream(ctx.TEXT().getText())
506
507
# ----------------------------------------------------------------------------------------------------------------------
508