summaryrefslogtreecommitdiff
path: root/codegen/vulkan/scripts/comment_convert.py
blob: f1fa938ce11b3bc610e4eb1f3923b2243d8dcb24 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
#!/usr/bin/env python3
#
# Copyright (c) 2019 Collabora, Ltd.
#
# SPDX-License-Identifier: Apache-2.0
#
# Author(s):    Ryan Pavlik <ryan.pavlik@collabora.com>
#
# Purpose:      This script converts leading comments on some Python
#               classes and functions into docstrings.
#               It doesn't attempt to deal with line continuations, etc.
#               so you may want to "join line" on your def statements
#               temporarily before running.

import re

from spec_tools.file_process import LinewiseFileProcessor

COMMENT_RE = re.compile(r" *#(!.*| (?P<content>.*))?")
CONVERTIBLE_DEF_RE = re.compile(r"(?P<indentation> *)(def|class) .*:")


class CommentConverter(LinewiseFileProcessor):
    def __init__(self, single_line_quotes=False, allow_blank_lines=False):
        super().__init__()
        self.comment_lines = []
        "Temporary storage for contiguous comment lines."

        self.trailing_empty_lines = []
        "Temporary storage for empty lines following a comment."

        self.output_lines = []
        "Fully-processed output lines."

        self.single_line_quotes = single_line_quotes
        "Whether we generate simple, single-line quotes for single line comments."

        self.allow_blank_lines = allow_blank_lines
        "Whether we allow blank lines between a comment and the thing it's considered to document."

        self.done_with_initial_comment = False
        "Have we read our first non-comment line yet?"

    def output_line(self, line=None):
        if line:
            self.output_lines.append(line)
        else:
            self.output_lines.append("")

    def output_normal_line(self, line):
        # flush any comment lines we had stored and output this line.
        self.dump_comment_lines()
        self.output_line(line)

    def dump_comment_lines(self):
        # Early out for empty
        if not self.comment_lines:
            return

        for line in self.comment_lines:
            self.output_line(line)
        self.comment_lines = []

        for line in self.trailing_empty_lines:
            self.output_line(line)
        self.trailing_empty_lines = []

    def dump_converted_comment_lines(self, indent):
        # Early out for empty
        if not self.comment_lines:
            return

        for line in self.trailing_empty_lines:
            self.output_line(line)
        self.trailing_empty_lines = []

        indent = indent + '    '

        def extract(line):
            match = COMMENT_RE.match(line)
            content = match.group('content')
            if content:
                return content
            return ""

        # Extract comment content
        lines = [extract(line) for line in self.comment_lines]

        # Drop leading empty comments.
        while lines and not lines[0].strip():
            lines.pop(0)

        # Drop trailing empty comments.
        while lines and not lines[-1].strip():
            lines.pop()

        # Add single- or multi-line-string quote
        if self.single_line_quotes \
            and len(lines) == 1 \
                and '"' not in lines[0]:
            quote = '"'
        else:
            quote = '"""'
        lines[0] = quote + lines[0]
        lines[-1] = lines[-1] + quote

        # Output lines, indenting content as required.
        for line in lines:
            if line:
                self.output_line(indent + line)
            else:
                # Don't indent empty comment lines
                self.output_line()

        # Clear stored comment lines since we processed them
        self.comment_lines = []

    def queue_comment_line(self, line):
        if self.trailing_empty_lines:
            # If we had blank lines between comment lines, they are separate blocks
            self.dump_comment_lines()
        self.comment_lines.append(line)

    def handle_empty_line(self, line):
        """Handle an empty line.

        Contiguous empty lines between a comment and something documentable do not
        disassociate the comment from the documentable thing.
        We have someplace else to store these lines in case there isn't something
        documentable coming up."""
        if self.comment_lines and self.allow_blank_lines:
            self.trailing_empty_lines.append(line)
        else:
            self.output_normal_line(line)

    def is_next_line_doc_comment(self):
        next_line = self.next_line_rstripped
        if next_line is None:
            return False

        return next_line.strip().startswith('"')

    def process_line(self, line_num, line):
        line = line.rstrip()
        comment_match = COMMENT_RE.match(line)
        def_match = CONVERTIBLE_DEF_RE.match(line)

        # First check if this is a comment line.
        if comment_match:
            if self.done_with_initial_comment:
                self.queue_comment_line(line)
            else:
                self.output_line(line)
        else:
            # If not a comment line, then by definition we're done with the comment header.
            self.done_with_initial_comment = True
            if not line.strip():
                self.handle_empty_line(line)
            elif def_match and not self.is_next_line_doc_comment():
                # We got something we can make a docstring for:
                # print the thing the docstring is for first,
                # then the converted comment.

                indent = def_match.group('indentation')
                self.output_line(line)
                self.dump_converted_comment_lines(indent)
            else:
                # Can't make a docstring for this line:
                self.output_normal_line(line)

    def process(self, fn, write=False):
        self.process_file(fn)

        if write:
            with open(fn, 'w', encoding='utf-8') as fp:
                for line in self.output_lines:
                    fp.write(line)
                    fp.write('\n')

        # Reset state
        self.__init__(self.single_line_quotes, self.allow_blank_lines)


def main():
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument('filenames', metavar='filename',
                        type=str, nargs='+',
                        help='A Python file to transform.')
    parser.add_argument('-b', '--blanklines', action='store_true',
                        help='Allow blank lines between a comment and a define and still convert that comment.')

    args = parser.parse_args()

    converter = CommentConverter(allow_blank_lines=args.blanklines)
    for fn in args.filenames:
        print("Processing", fn)
        converter.process(fn, write=True)


if __name__ == "__main__":
    main()