Completed
Push — master ( 4a1931...e7cf30 )
by P.R.
02:14
created

SDoc1Visitor.visitCmd_error()   A

Complexity

Conditions 1

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.512

Importance

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