Completed
Pull Request — master (#1098)
by Mischa
02:06
created

coalib.bearlib.languages.documentation.extract_documentation()   B

Complexity

Conditions 1

Size

Total Lines 27

Duplication

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