Failed Conditions
Pull Request — master (#1098)
by Mischa
02:01
created

coalib.bearlib.languages.documentation.extract_documentation_with_docstyle()   F

Complexity

Conditions 18

Size

Total Lines 153

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 18
dl 0
loc 153
rs 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like coalib.bearlib.languages.documentation.extract_documentation_with_docstyle() 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
import re
2
3
from coalib.bearlib.languages.documentation.DocstyleDefinition import (
4
    DocstyleDefinition)
5
from coalib.bearlib.languages.documentation.DocumentationComment import (
6
    DocumentationComment)
7
from coalib.results.TextRange import TextRange
8
9
10
#TODO - Implement Match also for `split` and `search_for`? File an issue
11
#TODO - document currently existing docstyles from coala side?
12
#TODO - Add ''' ''' as markers for python 2/3 inside doc-definition files.
13
14
15
def extract_documentation_with_docstyle(content, docstyle_definition):
16
    """
17
    Extracts all documentation texts inside the given source-code-string.
18
19
    :param content:             The source-code-string where to extract
20
                                documentation from or an iterable with strings
21
                                where each string is a single line (including
22
                                ending whitespaces like `\\n`).
23
    :param docstyle_definition: The DocstyleDefinition that identifies the
24
                                documentation comments.
25
    :return:                    An iterator returning each documentation text
26
                                found in the content.
27
    """
28
    if isinstance(content, str):
29
        content_len = len(content)
30
        content = content.splitlines(keepends=True)
31
    else:
32
        content = list(content)
33
        content_len = sum(len(line) for line in content)
34
35
    # Used to break out of outer loops via exception raise.
36
    class BreakOut(Exception):
37
        pass
38
39
    # Prepare marker-tuple dict that maps a begin pattern to the corresponding
40
    # marker_set(s). This makes it faster to retrieve a marker-set from a
41
    # begin sequence we initially want to search for in source code. Then
42
    # the possible found documentation match is processed further with the
43
    # rest markers.
44
    begin_sequence_dict = {}
45
    for marker_set in docstyle_definition.markers:
46
        if marker_set[0] not in begin_sequence_dict:
47
            begin_sequence_dict[marker_set[0]] = [marker_set]
48
        else:
49
            begin_sequence_dict[marker_set[0]].append(marker_set)
50
51
    # Using regexes to perform a variable match is faster than finding each
52
    # substring with `str.find()` choosing the lowest match.
53
    begin_regex = re.compile("|".join(
54
        re.escape(marker_set[0])
55
        for marker_set in docstyle_definition.markers))
56
57
    line = 0
58
    line_pos = 0
59
    while line < len(content):
60
        begin_match = begin_regex.search(content[line], line_pos)
61
62
        if begin_match:
63
            begin_match_line = line
64
65
            # begin_sequence_dict[begin_match.group()] returns the marker set
66
            # the begin sequence from before matched.
67
            for marker_set in begin_sequence_dict[begin_match.group()]:
68
                try:
69
                    end_marker_pos = content[line].find(marker_set[2],
70
                                                        begin_match.end())
71
                    # If the each-line marker and the end marker do equal,
72
                    # search for the each-line marker until it runs out.
73
                    if marker_set[1] == marker_set[2]:
74
                        docstring = content[line][begin_match.end():]
75
76
                        line2 = line + 1
77
                        stripped_content = content[line2].lstrip()
78
79
                        # Now the each-line marker is no requirement for a
80
                        # docstring any more, just extract as long as there are
81
                        # no each-line markers any more.
82
                        while (stripped_content[:len(marker_set[1])] ==
83
                               marker_set[1]):
84
                            docstring += stripped_content[len(marker_set[1]):]
85
86
                            line2 += 1
87
                            if line2 >= len(content):
88
                                # End of content reached, done with
89
                                # doc-extraction.
90
                                break
91
92
                            stripped_content = content[line2].lstrip()
93
94
                        line = line2 - 1
95
                        line_pos = len(content[line])
96
                    else:
97
                        if end_marker_pos == -1:
98
                            docstring = content[line][begin_match.end():]
99
100
                            line2 = line + 1
101
                            end_marker_pos = content[line2].find(marker_set[2])
102
103
                            while end_marker_pos == -1:
104
                                if marker_set[1] == "":
105
                                    # When no each-line marker is set (i.e. for
106
                                    # Python docstrings), then align the comment
107
                                    # to the start-marker.
108
                                    stripped_content = (
109
                                        content[line2][begin_match.start():])
110
                                else:
111
                                    # Check whether we violate the each-line
112
                                    # marker "rule".
113
                                    current_each_line_marker = (content[line2]
114
                                        [begin_match.start():
115
                                         begin_match.start()
116
                                             + len(marker_set[1])])
117
                                    if (current_each_line_marker !=
118
                                            marker_set[1]):
119
                                        # Effectively a 'continue' for the outer
120
                                        # for-loop.
121
                                        raise BreakOut
122
123
                                    stripped_content = (
124
                                        content[line2][begin_match.start()
125
                                                       + len(marker_set[1]):])
126
127
                                # TODO Test also other C style doccomments
128
129
                                docstring += stripped_content
130
                                line2 += 1
131
132
                                if line2 >= len(content):
133
                                    # End of content reached, so there's no
134
                                    # closing marker and that's a mismatch.
135
                                    raise BreakOut
136
137
                                end_marker_pos = content[line2].find(
138
                                    marker_set[2])
139
140
                            docstring += (content[line2]
141
                                [begin_match.start():end_marker_pos])
142
                            line = line2
143
                        else:
144
                            docstring = (content[line]
145
                                [begin_match.start():end_marker_pos])
146
147
                        line_pos = end_marker_pos + len(marker_set[2])
148
149
                    rng = TextRange.from_values(begin_match_line + 1,
150
                                                begin_match.start() + 1,
151
                                                line + 1,
152
                                                line_pos + 1)
153
154
                    yield DocumentationComment(docstring,
155
                                               docstyle_definition,
156
                                               marker_set,
157
                                               rng)
158
159
                    break
160
161
                except BreakOut:
162
                    # Continues the marker_set loop.
163
                    pass
164
165
        else:
166
            line += 1
167
            line_pos = 0
168
169
170
def extract_documentation(content, language, docstyle):
171
    """
172
    Extracts all documentation texts inside the given source-code-string using
173
    the coala docstyle definition files.
174
175
    The documentation texts are sorted by their order appearing in `content`.
176
177
    For more information about how documentation comments are identified and
178
    extracted, see DocstyleDefinition.doctypes enumeration.
179
180
    :param content:            The source-code-string where to extract
181
                               documentation from.
182
    :param language:           The programming language used.
183
    :param docstyle:           The documentation style/tool used
184
                               (i.e. doxygen).
185
    :raises FileNotFoundError: Raised when the docstyle definition file was not
186
                               found. This is a compatability exception from
187
                               `coalib.misc.Compatability` module.
188
    :raises KeyError:          Raised when the given language is not defined in
189
                               given docstyle.
190
    :raises ValueError:        Raised when a docstyle definition setting has an
191
                               invalid format.
192
    :return:                   An iterator returning each DocumentationComment
193
                               found in the content.
194
    """
195
    docstyle_definition = DocstyleDefinition.load(language, docstyle)
196
    return extract_documentation_with_docstyle(content, docstyle_definition)
197