Passed
Push — master ( 1ac98c...ed1e15 )
by P.R.
01:49
created

SDoc1Visitor   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 511
Duplicated Lines 0 %

Test Coverage

Coverage 43.41%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 50
c 4
b 0
f 0
dl 0
loc 511
ccs 79
cts 182
cp 0.4341
rs 8.6206

26 Methods

Rating   Name   Duplication   Size   Complexity  
A _data_is_true() 0 16 2
A visitCmd_substitute() 0 10 1
A visitCmd_error() 0 12 1
A visitCmd_expression() 0 9 1
A visitCmd_notice() 0 15 1
A visitText() 0 7 1
A visitPrimaryExpressionSubExpression() 0 7 1
A visitLogicalOrExpressionLogicalOr() 0 18 2
A __init__() 0 47 1
A visitPrimaryExpressionIdentifier() 0 9 1
A include_level() 0 8 1
B put_position() 0 27 4
A visitPrimaryExpressionIntegerConstant() 0 9 1
A visitPrimaryExpressionStringConstant() 0 9 1
B visitAssignmentExpressionAssignment() 0 25 3
B visitCmd_include() 0 47 4
A stream() 0 8 2
A visit() 0 9 1
A visitLogicalAndExpressionAnd() 0 18 2
C visitCmd_if() 0 49 7
A global_scope() 0 8 1
A visitCmd_sdoc2() 0 7 1
A output() 0 8 1
A visitPostfixExpressionExpression() 0 22 3
A visitCmd_comment() 0 7 1
A visitCmd_debug() 0 14 2

How to fix   Complexity   

Complex Class

Complex classes like SDoc1Visitor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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):
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[str,int]
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: int
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 antlr4.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
        :rtype: bool|None
188
        """
189
        try:
190
            return data.is_true()
191
192
        except DataTypeError as e:
193
            self._error(str(e), token)
194
            return None
195
196
    # ------------------------------------------------------------------------------------------------------------------
197 1
    def visit(self, tree):
198
        """
199
        Visits a parse tree produced by sdoc1
200
201
        :param antlr4.ParserRuleContext tree: The context tree.
202
        """
203 1
        self.put_position(tree, 'start')
204
205 1
        return super().visit(tree)
206
207
    # ------------------------------------------------------------------------------------------------------------------
208 1
    def visitAssignmentExpressionAssignment(self, ctx):
209
        """
210
        Visit a parse tree for expression like a = b.
211
212
        :param sdoc1Parser.AssignmentExpressionAssignmentContext ctx: The context tree.
213
214
        :rtype: mixed
215
        """
216 1
        right_hand_side = ctx.assignmentExpression().accept(self)
217 1
        left_hand_side = ctx.postfixExpression().accept(self)
218
219
        # Left hand side must be an identifier.
220
        # @todo implement array element.
221 1
        if not isinstance(left_hand_side, IdentifierDataType):
222
            message = "Left hand side '{0!s}' is not an identifier.".format(str(left_hand_side))
223
            self._error(message, ctx.postfixExpression().start)
224
            return
225
226 1
        try:
227 1
            value = left_hand_side.set_value(right_hand_side)
228
        except DataTypeError as e:
229
            self._error(str(e), ctx.assignmentExpression().start)
230
            return None
231
232 1
        return value
233
234
    # ------------------------------------------------------------------------------------------------------------------
235 1
    def visitLogicalAndExpressionAnd(self, ctx):
236
        """
237
        Visits a parse tree for expressions like 'a && b'.
238
239
        :param sdoc1Parser.LogicalAndExpressionAndContext ctx: The context tree.
240
241
        :rtype: sdoc.sdoc1.data_type.IntegerDataType.IntegerDataType
242
        """
243
        a_ctx = ctx.logicalAndExpression()
244
        b_ctx = ctx.equalityExpression()
245
246
        a = a_ctx.accept(self)
247
        b = b_ctx.accept(self)
248
249
        a_is_true = self._data_is_true(a, a_ctx.start)
250
        b_is_true = self._data_is_true(b, b_ctx.start)
251
252
        return IntegerDataType(1 if a_is_true and b_is_true else 0)
253
254
    # ------------------------------------------------------------------------------------------------------------------
255 1
    def visitLogicalOrExpressionLogicalOr(self, ctx):
256
        """
257
        Visits a parse tree for expressions like 'a || b'.
258
259
        :param sdoc1Parser.LogicalOrExpressionLogicalOrContext ctx: The context tree.
260
261
        :rtype: sdoc.sdoc1.data_type.IntegerDataType.IntegerDataType
262
        """
263
        a_ctx = ctx.logicalOrExpression()
264
        b_ctx = ctx.logicalAndExpression()
265
266
        a = a_ctx.accept(self)
267
        b = b_ctx.accept(self)
268
269
        a_is_true = self._data_is_true(a, a_ctx.start)
270
        b_is_true = self._data_is_true(b, b_ctx.start)
271
272
        return IntegerDataType(1 if a_is_true or b_is_true else 0)
273
274
    # ------------------------------------------------------------------------------------------------------------------
275 1
    def visitPostfixExpressionExpression(self, ctx):
276
        """
277
        Visits a parse tree for expressions like 'a[1]'.
278
279
        :param sdoc1Parser.PostfixExpressionExpressionContext ctx: The context tree.
280
281
        :rtype: sdoc.sdoc1.data_type.DataType.DataType
282
        """
283
        # First get the value of key.
284 1
        expression = ctx.expression().accept(self)
285 1
        if not expression.is_defined():
286
            message = '{0!s} is not defined.'.format(ctx.expression().getSymbol())
287
            self._error(message, ctx.expression().start)
288
            return
289
290 1
        postfix_expression = ctx.postfixExpression().accept(self)
291 1
        if not isinstance(postfix_expression, IdentifierDataType):
292
            message = "'{0!s}' is not an identifier.".format(ctx.postfixExpression().getSymbol())
293
            self._error(message, ctx.postfixExpression().start)
294
            return
295
296 1
        return postfix_expression.get_array_element(expression)
297
298
    # ------------------------------------------------------------------------------------------------------------------
299 1
    def visitPrimaryExpressionIdentifier(self, ctx):
300
        """
301
        Visits a parse tree produced by sdoc1Parser#primaryExpressionIdentifier.
302
303
        :param sdoc1Parser.PrimaryExpressionIdentifierContext ctx: The context tree.
304
305
        :rtype: sdoc.sdoc1.data_type.IdentifierDataType.IdentifierDataType
306
        """
307 1
        return IdentifierDataType(self._global_scope, ctx.EXPR_IDENTIFIER().getText())
308
309
    # ------------------------------------------------------------------------------------------------------------------
310 1
    def visitPrimaryExpressionIntegerConstant(self, ctx):
311
        """
312
        Visits a parse tree produced by sdoc1Parser#PrimaryExpressionIntegerConstantContext.
313
314
        :param sdoc1Parser.PrimaryExpressionIntegerConstantContext ctx: The context tree.
315
316
        :rtype: sdoc.sdoc1.data_type.IntegerDataType.IntegerDataType
317
        """
318 1
        return IntegerDataType(ctx.EXPR_INTEGER_CONSTANT().getText())
319
320
    # ------------------------------------------------------------------------------------------------------------------
321 1
    def visitPrimaryExpressionStringConstant(self, ctx):
322
        """
323
        Visits a parse tree produced by sdoc1Parser#PrimaryExpressionStringConstantContext.
324
325
        :param sdoc1Parser.PrimaryExpressionStringConstantContext ctx: The context tree.
326
327
        :rtype sdoc.sdoc1.data_type.StringDataType.StringDataType
328
        """
329 1
        return StringDataType(ctx.EXPR_STRING_CONSTANT().getText()[1:-1].replace('\\\\', '\\').replace('\\\'', '\''))
330
331
    # ------------------------------------------------------------------------------------------------------------------
332 1
    def visitPrimaryExpressionSubExpression(self, ctx):
333
        """
334
        Visits a parse tree for sub-expressions like (a && b).
335
336
        :param sdoc1Parser.primaryExpressionSubExpression ctx: The context tree.
337
        """
338
        return ctx.expression().accept(self)
339
340
    # ------------------------------------------------------------------------------------------------------------------
341 1
    def visitCmd_comment(self, ctx):
342
        """
343
        Visits a parse tree produced by sdoc1Parser#cmd_comment.
344
345
        :param sdoc1Parser.Cmd_commentContext ctx: The context tree.
346
        """
347
        self.put_position(ctx, 'stop')
348
349
    # ------------------------------------------------------------------------------------------------------------------
350 1
    def visitCmd_debug(self, ctx):
351
        """
352
        Visits a parse tree produced by sdoc1Parser#cmd_debug.
353
354
        :param sdoc1Parser.Cmd_debugContext ctx: The context tree.
355
        """
356 1
        expression = ctx.expression()
357
358 1
        if expression is not None:
359 1
            self._io.writeln(expression.accept(self).debug())
360
        else:
361
            self._io.writeln(self._global_scope.debug())
362
363 1
        self.put_position(ctx, 'stop')
364
365
    # ------------------------------------------------------------------------------------------------------------------
366 1
    def visitCmd_expression(self, ctx):
367
        """
368
        Visits a parse tree produced by sdoc1Parser#cmd_expression.
369
370
        :param sdoc1Parser.Cmd_expressionContext ctx: The context tree.
371
        """
372 1
        self.visitExpression(ctx.expression())
373
374 1
        self.put_position(ctx, 'stop')
375
376
    # ------------------------------------------------------------------------------------------------------------------
377 1
    def visitCmd_error(self, ctx):
378
        """
379
        Visits a parse tree produced by sdoc1Parser#cmd_error.
380
381
        :param sdoc1Parser.Cmd_errorContext ctx: The parse tree.
382
        """
383
        token = ctx.ERROR().getSymbol()
384
        message = SDoc.unescape(ctx.SIMPLE_ARG().getText())
385
386
        self._error(message, token)
387
388
        self.put_position(ctx, 'stop')
389
390
    # ------------------------------------------------------------------------------------------------------------------
391 1
    def visitCmd_if(self, ctx):
392
        """
393
        Visits a parse tree produced by sdoc1Parser#cmd_if.
394
395
        :param sdoc1Parser.Cmd_ifContext ctx: The parse tree.
396
        """
397
        n = ctx.getChildCount()
398
        fired = False
399
        i = 0
400
        while i < n and not fired:
401
            child = ctx.getChild(i)
402
            token_text = child.getText()
403
            i += 1
404
            if token_text in ['\\if', '\\elif']:
405
                # Skip {
406
                i += 1
407
408
                # Child is the expression to be evaluated.
409
                child = ctx.getChild(i)
410
                i += 1
411
                data = child.accept(self)
412
413
                # Skip }
414
                i += 1
415
416
                if self._data_is_true(data, child.start):
417
                    # Child is the code inside the if or elif clause.
418
                    child = ctx.getChild(i)
419
                    self.put_position(child, 'start')
420
                    i += 1
421
                    child.accept(self)
422
                    fired = True
423
424
                else:
425
                    # Skip the code inside the if or elif clause.
426
                    i += 1
427
428
            elif token_text == '\\else':
429
                # Child is the code inside the else clause.
430
                child = ctx.getChild(i)
431
                i += 1
432
433
                child.accept(self)
434
                fired = True
435
436
            elif token_text == '\\endif':
437
                pass
438
439
        self.put_position(ctx, 'stop')
440
441
    # ------------------------------------------------------------------------------------------------------------------
442 1
    def visitCmd_include(self, ctx):
443
        """
444
        Includes another SDoc into this SDoc.
445
446
        :param sdoc1Parser.Cmd_includeContext ctx: The parse tree.
447
        """
448
        # Test the maximum include level.
449
        if self._include_level >= self._options['max_include_level']:
450
            message = 'Maximum include level exceeded'
451
            self._error(message, ctx.INCLUDE().getSymbol())
452
            return
453
454
        # Open a stream for the sub-document.
455
        file_name = SDoc.unescape(ctx.SIMPLE_ARG().getText())
456
        if not os.path.isabs(file_name):
457
            file_name = os.path.join(self._root_dir, file_name + '.sdoc')
458
        real_path = os.path.relpath(file_name)
459
        self._io.writeln("Including <fso>{0!s}</fso>".format(real_path))
460
        try:
461
            stream = antlr4.FileStream(file_name, 'utf-8')
462
463
            # root_dir
464
465
            # Create a new lexer and parser for the sub-document.
466
            lexer = sdoc1Lexer(stream)
467
            tokens = antlr4.CommonTokenStream(lexer)
468
            parser = sdoc1Parser(tokens)
469
            tree = parser.sdoc()
470
471
            # Create a visitor.
472
            visitor = SDoc1Visitor(self._io, root_dir=os.path.dirname(os.path.realpath(file_name)))
473
474
            # Set or inherit properties from the parser of the parent document.
475
            visitor.include_level = self._include_level + 1
476
            visitor.output = self._output
477
            visitor.global_scope = self._global_scope
478
479
            # Run the visitor on the parse tree.
480
            visitor.visit(tree)
481
482
            # Copy  properties from the child document.
483
            self._errors += visitor.errors
484
485
            self.put_position(ctx, 'stop')
486
        except FileNotFoundError as e:
487
            message = 'Unable to open file {0!s}.\nCause: {1!s}'.format(real_path, e)
488
            self._error(message, ctx.INCLUDE().getSymbol())
489
490
    # ------------------------------------------------------------------------------------------------------------------
491 1
    def visitCmd_notice(self, ctx):
492
        """
493
        Visits a parse tree produced by sdoc1Parser#cmd_notice.
494
495
        :param sdoc1Parser.Cmd_noticeContext ctx: The parse tree.
496
        """
497
        token = ctx.NOTICE().getSymbol()
498
        filename = token.getInputStream().fileName  # Replace fileName with get_source_name() when implemented in ANTLR.
499
        line_number = token.line
500
        message = SDoc.unescape(ctx.SIMPLE_ARG().getText())
501
502
        self._io.writeln(
503
            '<notice>Notice: {0!s} at {1!s}:{2:d}</notice>'.format(message, os.path.relpath(filename), line_number))
504
505
        self.put_position(ctx, 'stop')
506
507
    # ------------------------------------------------------------------------------------------------------------------
508 1
    def visitCmd_substitute(self, ctx):
509
        """
510
        Visit a parse tree produced by sdoc1Parser#cmd_substitute.
511
512
        :param sdoc1Parser.Cmd_substituteContext ctx:  The parse tree.
513
        """
514 1
        expression = ctx.expression()
515 1
        self.stream(expression.accept(self).get_value())
516
517 1
        self.put_position(ctx, 'stop')
518
519
    # ------------------------------------------------------------------------------------------------------------------
520 1
    def visitCmd_sdoc2(self, ctx):
521
        """
522
        Visits a parse tree produced by sdoc1Parser#sdoc2_cmd.
523
524
        :param sdoc1Parser.Cmd_sdoc2Context ctx: The parse tree.
525
        """
526 1
        self.stream(ctx.SDOC2_COMMAND().getText())
527
528
    # ------------------------------------------------------------------------------------------------------------------
529 1
    def visitText(self, ctx):
530
        """
531
        Visits a parse tree produced by sdoc1Parser#text.
532
533
        :param sdoc1Parser.TextContext ctx: The parse tree.
534
        """
535 1
        self.stream(ctx.TEXT().getText())
536
537
# ----------------------------------------------------------------------------------------------------------------------
538