Skip to content

Commit a3a2be1

Browse files
committed
Added task for formatting Doxygen/Javadoc comments
1 parent 6694a03 commit a3a2be1

File tree

3 files changed

+430
-0
lines changed

3 files changed

+430
-0
lines changed

wpiformat/wpiformat/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from wpiformat.clangformat import ClangFormat
1313
from wpiformat.clangtidy import ClangTidy
1414
from wpiformat.cmakeformat import CMakeFormat
15+
from wpiformat.commentformat import CommentFormat
1516
from wpiformat.config import Config
1617
from wpiformat.eofnewline import EofNewline
1718
from wpiformat.gtestname import GTestName
@@ -517,6 +518,7 @@ def main():
517518
task_pipeline = [
518519
BraceComment(),
519520
CIdentList(),
521+
CommentFormat(),
520522
EofNewline(),
521523
GTestName(),
522524
IncludeGuard(),

wpiformat/wpiformat/commentformat.py

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

0 commit comments

Comments
 (0)