Skip to content

Commit b8e95f2

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

File tree

3 files changed

+601
-0
lines changed

3 files changed

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

0 commit comments

Comments
 (0)