Skip to content

Commit 5d0a454

Browse files
committed
Added task for formatting Doxygen/Javadoc comments
1 parent cf1578a commit 5d0a454

File tree

3 files changed

+768
-0
lines changed

3 files changed

+768
-0
lines changed

wpiformat/wpiformat/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from wpiformat.cidentlist import CIdentList
1212
from wpiformat.clangformat import ClangFormat
1313
from wpiformat.clangtidy import ClangTidy
14+
from wpiformat.commentformat import CommentFormat
1415
from wpiformat.config import Config
1516
from wpiformat.eofnewline import EofNewline
1617
from wpiformat.gtestname import GTestName
@@ -470,6 +471,7 @@ def main():
470471
task_pipeline = [
471472
BraceComment(),
472473
CIdentList(),
474+
CommentFormat(),
473475
EofNewline(),
474476
GTestName(),
475477
IncludeGuard(),

wpiformat/wpiformat/commentformat.py

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
"""This task formats Doxygen and Javadoc comments.
2+
3+
Comments are rewrapped to 80 characters for C++ and 100 for Java. The @param
4+
tag has one space followed by the parameter name, at least one one space, then
5+
the description. All @param descriptions start on the same column.
6+
7+
The first letter of paragraphs and tag descriptions is capitalized and a "." is
8+
appended if one is not already. Descriptions past 80 (or 100) characters are
9+
wrapped to the next line at the same starting column.
10+
11+
The indentation of lists is left alone. Bulleted lists can use "-", "+", or "*"
12+
while numbered lists use numbers followed by ".".
13+
"""
14+
15+
import regex
16+
import sys
17+
18+
from wpiformat.task import Task
19+
20+
21+
class CommentFormat(Task):
22+
@staticmethod
23+
def should_process_file(config_file, name):
24+
return (
25+
config_file.is_c_file(name)
26+
or config_file.is_cpp_file(name)
27+
or name.endswith(".java")
28+
)
29+
30+
def textwrap(self, lines, column_limit):
31+
"""Wraps lines to the provided column limit and returns a list of lines.
32+
33+
Keyword Arguments:
34+
lines -- string to wrap
35+
column_limit -- maximum number of characters per line
36+
"""
37+
output = []
38+
output_str = ""
39+
pos = 0
40+
rgx = regex.compile(r"{@link(?>.*?})|\S+")
41+
for match in rgx.finditer(lines):
42+
if len(output_str) + len(" ") + len(match.group()) > column_limit:
43+
output.append(output_str)
44+
output_str = match.group()
45+
else:
46+
if output_str:
47+
output_str += " "
48+
output_str += match.group()
49+
pos = match.end()
50+
if output_str:
51+
output.append(output_str)
52+
return output
53+
54+
def run_pipeline(self, config_file, name, lines):
55+
linesep = Task.get_linesep(lines)
56+
57+
if name.endswith(".java"):
58+
column_limit = 100
59+
else:
60+
column_limit = 80
61+
62+
output = ""
63+
64+
# Construct regex for Doxygen comment
65+
indent = r"(?P<indent>[ \t]*)?"
66+
comment_rgx = regex.compile(indent + r"/\*\*(?>(.|" + linesep + r")*?\*/)")
67+
asterisk_rgx = regex.compile(r"^\s*(\*|\*/)")
68+
69+
# Comment parts
70+
brief = (
71+
r"(?P<brief>(.|"
72+
+ linesep
73+
+ r")*?("
74+
+ linesep
75+
+ linesep
76+
+ r"|"
77+
+ linesep
78+
+ r"$|"
79+
+ linesep
80+
+ r"(?=@)|$))"
81+
)
82+
brief_rgx = regex.compile(brief)
83+
84+
tag = r"@(?<tag_name>\w+)\s+(?<arg_name>\w+)\s+(?<description>[^@]*)"
85+
tag_rgx = regex.compile(tag)
86+
87+
pos = 0
88+
for comment_match in comment_rgx.finditer(lines):
89+
# Append lines before match
90+
output += lines[pos : comment_match.start()]
91+
92+
# If there is an indent, create a variable with that amount of
93+
# spaces in it
94+
if comment_match.group("indent"):
95+
spaces = " " * len(comment_match.group("indent"))
96+
else:
97+
spaces = ""
98+
99+
# Append start of comment
100+
output += spaces + "/**" + linesep
101+
102+
# Remove comment start/end and leading asterisks from comment lines
103+
comment = comment_match.group()
104+
comment = comment[
105+
len(comment_match.group("indent"))
106+
+ len("/**") : len(comment)
107+
- len("*/")
108+
]
109+
comment_list = [
110+
asterisk_rgx.sub("", line).strip() for line in comment.split(linesep)
111+
]
112+
comment = linesep.join(comment_list).strip(linesep)
113+
114+
# Parse comment paragraphs
115+
comment_pos = 0
116+
i = 0
117+
while comment_pos < len(comment) and comment[comment_pos] != "@":
118+
match = brief_rgx.search(comment[comment_pos:])
119+
120+
# If no paragraphs were found, bail out early
121+
if not match:
122+
break
123+
124+
# Start writing paragraph
125+
if comment_pos > 0:
126+
output += spaces + " *" + linesep
127+
output += spaces + " * "
128+
129+
# If comments are javadoc and it isn't the first paragraph
130+
if name.endswith(".java") and comment_pos > 0:
131+
if not match.group().startswith("<p>"):
132+
# Add paragraph tag before new paragraph
133+
output += "<p>"
134+
135+
# Strip newlines and extra spaces between words from paragraph
136+
contents = " ".join(match.group().split())
137+
138+
# Capitalize first letter of paragraph and wrap paragraph
139+
contents = self.textwrap(
140+
contents[:1].upper() + contents[1:],
141+
column_limit - len(" * ") - len(spaces),
142+
)
143+
144+
# Write out paragraphs
145+
for i, line in enumerate(contents):
146+
if i == 0:
147+
output += line
148+
else:
149+
output += spaces + " * " + line
150+
# Put period at end of paragraph
151+
if i == len(contents) - 1 and output[-1] != ".":
152+
output += "."
153+
output += linesep
154+
155+
comment_pos += match.end()
156+
157+
# Parse tags
158+
tag_list = []
159+
max_arglength = 0
160+
for match in tag_rgx.finditer(comment[comment_pos:]):
161+
contents = " ".join(match.group("description").split())
162+
if match.group("tag_name") == "param":
163+
tag_list.append(
164+
(match.group("tag_name"), match.group("arg_name"), contents)
165+
)
166+
167+
# Only param tags are lined up and thus count toward the
168+
# maximum amount indented
169+
max_arglength = max(max_arglength, len(match.group("arg_name")))
170+
else:
171+
tag_list.append(
172+
(
173+
match.group("tag_name"),
174+
"",
175+
match.group("arg_name") + " " + contents,
176+
)
177+
)
178+
179+
# Insert empty line before tags if there was a description before
180+
if tag_list and comment_pos > 0:
181+
output += spaces + " *" + linesep
182+
183+
for tag in tag_list:
184+
# Only line up param tags
185+
if tag[0] == "param":
186+
tagline = spaces + " * @" + tag[0] + " " + tag[1]
187+
tagline += " " * (max_arglength - len(tag[1]) + 1)
188+
else:
189+
tagline = spaces + " * @" + tag[0] + " "
190+
191+
# Capitalize first letter of description and wrap description
192+
contents = self.textwrap(
193+
tag[2][:1].upper() + tag[2][1:],
194+
column_limit - len(tagline) - len(spaces),
195+
)
196+
197+
# Write out tags
198+
output += tagline
199+
for i, line in enumerate(contents):
200+
if i == 0:
201+
output += line
202+
else:
203+
output += (
204+
spaces
205+
+ " * "
206+
+ " " * (len(tagline) - len(spaces) - len(" * "))
207+
+ line
208+
)
209+
# Put period at end of description
210+
if i == len(contents) - 1 and output[-1] != ".":
211+
output += "."
212+
output += linesep
213+
214+
# Append closing part of comment
215+
output += spaces + " */"
216+
pos = comment_match.end()
217+
218+
# Append leftover lines in file
219+
if pos < len(lines):
220+
output += lines[pos:]
221+
222+
if output != lines:
223+
return (output, True)
224+
else:
225+
return (lines, True)

0 commit comments

Comments
 (0)