Skip to content

Commit de5d0bc

Browse files
committed
Added task for formatting Doxygen/Javadoc comments
1 parent 7eb9e7c commit de5d0bc

File tree

3 files changed

+499
-0
lines changed

3 files changed

+499
-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.includeguard import IncludeGuard
@@ -475,6 +476,7 @@ def main():
475476
task_pipeline = [
476477
BraceComment(),
477478
CIdentList(),
479+
CommentFormat(),
478480
EofNewline(),
479481
IncludeGuard(),
480482
LicenseUpdate(),

wpiformat/wpiformat/commentformat.py

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

0 commit comments

Comments
 (0)