Skip to content

Commit 3a7c175

Browse files
committed
Added task for formatting Doxygen/Javadoc comments
1 parent b392ae5 commit 3a7c175

File tree

3 files changed

+498
-0
lines changed

3 files changed

+498
-0
lines changed

wpiformat/wpiformat/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from wpiformat.bracecomment import BraceComment
1111
from wpiformat.cidentlist import CIdentList
1212
from wpiformat.clangformat import ClangFormat
13+
from wpiformat.commentformat import CommentFormat
1314
from wpiformat.config import Config
1415
from wpiformat.eofnewline import EofNewline
1516
from wpiformat.includeguard import IncludeGuard
@@ -350,6 +351,7 @@ def main():
350351
task_pipeline = [
351352
BraceComment(),
352353
CIdentList(),
354+
CommentFormat(),
353355
EofNewline(),
354356
IncludeGuard(),
355357
LicenseUpdate(),

wpiformat/wpiformat/commentformat.py

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

0 commit comments

Comments
 (0)