diff --git a/wpiformat/wpiformat/__init__.py b/wpiformat/wpiformat/__init__.py index c98a85b4..71c2d2d3 100644 --- a/wpiformat/wpiformat/__init__.py +++ b/wpiformat/wpiformat/__init__.py @@ -10,6 +10,7 @@ from wpiformat.bracecomment import BraceComment from wpiformat.cidentlist import CIdentList from wpiformat.clangformat import ClangFormat +from wpiformat.commentformat import CommentFormat from wpiformat.config import Config from wpiformat.eofnewline import EofNewline from wpiformat.includeguard import IncludeGuard @@ -345,6 +346,7 @@ def main(): task_pipeline = [ BraceComment(), CIdentList(), + CommentFormat(), EofNewline(), IncludeGuard(), LicenseUpdate(), diff --git a/wpiformat/wpiformat/commentformat.py b/wpiformat/wpiformat/commentformat.py new file mode 100644 index 00000000..7dab4373 --- /dev/null +++ b/wpiformat/wpiformat/commentformat.py @@ -0,0 +1,200 @@ +"""This task formats Doxygen and Javadoc comments. + +Comments are rewrapped to 80 characters for C++ and 100 for Java. The @param +tag has one space followed by the parameter name, at least one one space, then +the description. All @param descriptions start on the same column. + +The first letter of paragraphs and tag descriptions is capitalized and a "." is +appended if one is not already. Descriptions past 80 (or 100) characters are +wrapped to the next line at the same starting column. + +The indentation of lists is left alone. Bulleted lists can use "-", "+", or "*" +while numbered lists use numbers followed by ".". +""" + +import regex +import sys + +from wpiformat.task import Task + + +class CommentFormat(Task): + + def should_process_file(self, config_file, name): + return config_file.is_c_file(name) or config_file.is_cpp_file(name) or \ + name.endswith(".java") + + def textwrap(self, lines, column_limit): + """Wraps lines to the provided column limit and returns a list of lines. + + Keyword Arguments: + lines -- string to wrap + column_limit -- maximum number of characters per line + """ + output = [] + output_str = "" + pos = 0 + rgx = regex.compile(r"{@link(?>.*?})|\S+") + for match in rgx.finditer(lines): + if len(output_str) + len(" ") + len(match.group()) > column_limit: + output.append(output_str) + output_str = match.group() + else: + if output_str: + output_str += " " + output_str += match.group() + pos = match.end() + if output_str: + output.append(output_str) + return output + + def run_pipeline(self, config_file, name, lines): + linesep = Task.get_linesep(lines) + + if name.endswith(".java"): + column_limit = 100 + else: + column_limit = 80 + + output = "" + + # Construct regex for Doxygen comment + indent = r"(?P[ \t]*)?" + comment_rgx = regex.compile(indent + r"/\*\*(?>(.|" + linesep + + r")*?\*/)") + asterisk_rgx = regex.compile(r"^\s*(\*|\*/)") + + # Comment parts + brief = r"(?P(.|" + linesep + r")*?(" + \ + linesep + linesep + r"|" + linesep + r"$|" + linesep + r"(?=@)|$))" + brief_rgx = regex.compile(brief) + + tag = r"@(?\w+)\s+(?\w+)\s+(?[^@]*)" + tag_rgx = regex.compile(tag) + + pos = 0 + for comment_match in comment_rgx.finditer(lines): + # Append lines before match + output += lines[pos:comment_match.start()] + + # If there is an indent, create a variable with that amount of + # spaces in it + if comment_match.group("indent"): + spaces = " " * len(comment_match.group("indent")) + else: + spaces = "" + + # Append start of comment + output += spaces + "/**" + linesep + + # Remove comment start/end and leading asterisks from comment lines + comment = comment_match.group() + comment = comment[len(comment_match.group("indent")) + + len("/**"):len(comment) - len("*/")] + comment_list = [ + asterisk_rgx.sub("", line).strip() + for line in comment.split(linesep) + ] + comment = linesep.join(comment_list).strip(linesep) + + # Parse comment paragraphs + comment_pos = 0 + i = 0 + while comment_pos < len(comment) and comment[comment_pos] != "@": + match = brief_rgx.search(comment[comment_pos:]) + + # If no paragraphs were found, bail out early + if not match: + break + + # Start writing paragraph + if comment_pos > 0: + output += spaces + " *" + linesep + output += spaces + " * " + + # If comments are javadoc and it isn't the first paragraph + if name.endswith(".java") and comment_pos > 0: + if not match.group().startswith("

"): + # Add paragraph tag before new paragraph + output += "

" + + # Strip newlines and extra spaces between words from paragraph + contents = " ".join(match.group().split()) + + # Capitalize first letter of paragraph and wrap paragraph + contents = self.textwrap( + contents[:1].upper() + contents[1:], + column_limit - len(" * ") - len(spaces)) + + # Write out paragraphs + for i, line in enumerate(contents): + if i == 0: + output += line + else: + output += spaces + " * " + line + # Put period at end of paragraph + if i == len(contents) - 1 and output[-1] != ".": + output += "." + output += linesep + + comment_pos += match.end() + + # Parse tags + tag_list = [] + max_arglength = 0 + for match in tag_rgx.finditer(comment[comment_pos:]): + contents = " ".join(match.group("description").split()) + if match.group("tag_name") == "param": + tag_list.append((match.group("tag_name"), + match.group("arg_name"), contents)) + + # Only param tags are lined up and thus count toward the + # maximum amount indented + max_arglength = max(max_arglength, + len(match.group("arg_name"))) + else: + tag_list.append((match.group("tag_name"), "", + match.group("arg_name") + " " + contents)) + + # Insert empty line before tags if there was a description before + if tag_list and comment_pos > 0: + output += spaces + " *" + linesep + + for tag in tag_list: + # Only line up param tags + if tag[0] == "param": + tagline = spaces + " * @" + tag[0] + " " + tag[1] + tagline += " " * (max_arglength - len(tag[1]) + 1) + else: + tagline = spaces + " * @" + tag[0] + " " + + # Capitalize first letter of description and wrap description + contents = self.textwrap( + tag[2][:1].upper() + tag[2][1:], + column_limit - len(tagline) - len(spaces)) + + # Write out tags + output += tagline + for i, line in enumerate(contents): + if i == 0: + output += line + else: + output += spaces + " * " + " " * ( + len(tagline) - len(spaces) - len(" * ")) + line + # Put period at end of description + if i == len(contents) - 1 and output[-1] != ".": + output += "." + output += linesep + + # Append closing part of comment + output += spaces + " */" + pos = comment_match.end() + + # Append leftover lines in file + if pos < len(lines): + output += lines[pos:] + + if output != lines: + return (output, True, True) + else: + return (lines, False, True) diff --git a/wpiformat/wpiformat/test/test_commentformat.py b/wpiformat/wpiformat/test/test_commentformat.py new file mode 100644 index 00000000..39be813d --- /dev/null +++ b/wpiformat/wpiformat/test/test_commentformat.py @@ -0,0 +1,296 @@ +import os + +from .tasktest import * +from wpiformat.commentformat import CommentFormat + + +def test_commentformat(): + test = TaskTest(CommentFormat()) + + # Empty comment + test.add_input("./Test.h", + "/**" + os.linesep + \ + " */" + os.linesep) + test.add_latest_input_as_output(True) + + # Adds space before asterisks + test.add_input("./Test.h", + "/**" + os.linesep + \ + "*" + os.linesep + \ + "*/" + os.linesep) + test.add_output( + "/**" + os.linesep + \ + " */" + os.linesep, True, True) + + # Put /** on separate line + test.add_input("./Test.h", "/** asdf */" + os.linesep) + test.add_output( + "/**" + os.linesep + \ + " * Asdf." + os.linesep + \ + " */" + os.linesep, True, True) + + # Paragraphs but no tags + test.add_input("./Accelerometer.cpp", + "/**" + os.linesep + \ + " * Get the x-axis acceleration" + os.linesep + \ + " *" + os.linesep + \ + " * This is a floating point value in units of 1 g-force" + os.linesep + \ + " */" + os.linesep) + test.add_output( + "/**" + os.linesep + \ + " * Get the x-axis acceleration." + os.linesep + \ + " *" + os.linesep + \ + " * This is a floating point value in units of 1 g-force." + os.linesep + \ + " */" + os.linesep, True, True) + + # Paragraphs but no tags + test.add_input("./Accelerometer.cpp", + "/**" + os.linesep + \ + " * Convert a 12-bit raw acceleration value into a scaled double in units of" + os.linesep + \ + " * 1 g-force, taking into account the accelerometer range." + os.linesep + \ + " */" + os.linesep) + test.add_output( + "/**" + os.linesep + \ + " * Convert a 12-bit raw acceleration value into a scaled double in units of 1" + os.linesep + \ + " * g-force, taking into account the accelerometer range." + os.linesep + \ + " */" + os.linesep, True, True) + + # @param tag but with blank line before it and no description + test.add_input("./AnalogInput.cpp", + "/**" + os.linesep + \ + " *" + os.linesep + \ + " * @param analogPortHandle Handle to the analog port." + os.linesep + \ + " */" + os.linesep) + test.add_output( + "/**" + os.linesep + \ + " * @param analogPortHandle Handle to the analog port." + os.linesep + \ + " */" + os.linesep, True, True) + + # Paragraph with @param and @return tags + test.add_input("./AnalogAccumulator.cpp", + "/**" + os.linesep + \ + " * Is the channel attached to an accumulator." + os.linesep + \ + " *" + os.linesep + \ + " * @param analogPortHandle Handle to the analog port." + os.linesep + \ + " * @return The analog channel is attached to an accumulator." + os.linesep + \ + " */" + os.linesep) + test.add_latest_input_as_output(True) + + # Paragraph and @return with no empty line between them + test.add_input("./AnalogTrigger.cpp", + "/**" + os.linesep + \ + " * Return the InWindow output of the analog trigger." + os.linesep + \ + " *" + os.linesep + \ + " * True if the analog input is between the upper and lower limits." + os.linesep + \ + " * @return The InWindow output of the analog trigger." + os.linesep + \ + " */" + os.linesep) + test.add_output( + "/**" + os.linesep + \ + " * Return the InWindow output of the analog trigger." + os.linesep + \ + " *" + os.linesep + \ + " * True if the analog input is between the upper and lower limits." + os.linesep + \ + " *" + os.linesep + \ + " * @return The InWindow output of the analog trigger." + os.linesep + \ + " */" + os.linesep, True, True) + + # List ("-" bullets) + test.add_input("./DigitalInternal.h", + "/**" + os.linesep + \ + " * The default PWM period is in ms." + os.linesep + \ + " *" + os.linesep + \ + " * - 20ms periods (50 Hz) are the \"safest\" setting in that this works for all" + os.linesep + \ + " * devices" + os.linesep + \ + " * - 20ms periods seem to be desirable for Vex Motors" + os.linesep + \ + " * - 20ms periods are the specified period for HS-322HD servos, but work" + os.linesep + \ + " * reliably down to 10.0 ms; starting at about 8.5ms, the servo sometimes hums" + os.linesep + \ + " * and get hot; by 5.0ms the hum is nearly continuous" + os.linesep + \ + " * - 10ms periods work well for Victor 884" + os.linesep + \ + " * - 5ms periods allows higher update rates for Luminary Micro Jaguar speed" + os.linesep + \ + " * controllers. Due to the shipping firmware on the Jaguar, we can't run the" + os.linesep + \ + " * update period less than 5.05 ms." + os.linesep + \ + " *" + os.linesep + \ + " * kDefaultPwmPeriod is the 1x period (5.05 ms). In hardware, the period" + os.linesep + \ + " * scaling is implemented as an output squelch to get longer periods for old" + os.linesep + \ + " * devices." + os.linesep + \ + " */" + os.linesep) + test.add_latest_input_as_output(True) + + # List ("+" bullets) + test.add_input("./DigitalInternal.h", + "/**" + os.linesep + \ + " * The default PWM period is in ms." + os.linesep + \ + " *" + os.linesep + \ + " * + 20ms periods (50 Hz) are the \"safest\" setting in that this works for all" + os.linesep + \ + " * devices" + os.linesep + \ + " * + 20ms periods seem to be desirable for Vex Motors" + os.linesep + \ + " * + 20ms periods are the specified period for HS-322HD servos, but work" + os.linesep + \ + " * reliably down to 10.0 ms; starting at about 8.5ms, the servo sometimes hums" + os.linesep + \ + " * and get hot; by 5.0ms the hum is nearly continuous" + os.linesep + \ + " * + 10ms periods work well for Victor 884" + os.linesep + \ + " * + 5ms periods allows higher update rates for Luminary Micro Jaguar speed" + os.linesep + \ + " * controllers. Due to the shipping firmware on the Jaguar, we can't run the" + os.linesep + \ + " * update period less than 5.05 ms." + os.linesep + \ + " *" + os.linesep + \ + " * kDefaultPwmPeriod is the 1x period (5.05 ms). In hardware, the period" + os.linesep + \ + " * scaling is implemented as an output squelch to get longer periods for old" + os.linesep + \ + " * devices." + os.linesep + \ + " */" + os.linesep) + test.add_latest_input_as_output(True) + + # List ("*" bullets) + test.add_input("./DigitalInternal.h", + "/**" + os.linesep + \ + " * The default PWM period is in ms." + os.linesep + \ + " *" + os.linesep + \ + " * * 20ms periods (50 Hz) are the \"safest\" setting in that this works for all" + os.linesep + \ + " * devices" + os.linesep + \ + " * * 20ms periods seem to be desirable for Vex Motors" + os.linesep + \ + " * * 20ms periods are the specified period for HS-322HD servos, but work" + os.linesep + \ + " * reliably down to 10.0 ms; starting at about 8.5ms, the servo sometimes hums" + os.linesep + \ + " * and get hot; by 5.0ms the hum is nearly continuous" + os.linesep + \ + " * * 10ms periods work well for Victor 884" + os.linesep + \ + " * * 5ms periods allows higher update rates for Luminary Micro Jaguar speed" + os.linesep + \ + " * controllers. Due to the shipping firmware on the Jaguar, we can't run the" + os.linesep + \ + " * update period less than 5.05 ms." + os.linesep + \ + " *" + os.linesep + \ + " * kDefaultPwmPeriod is the 1x period (5.05 ms). In hardware, the period" + os.linesep + \ + " * scaling is implemented as an output squelch to get longer periods for old" + os.linesep + \ + " * devices." + os.linesep + \ + " */" + os.linesep) + test.add_latest_input_as_output(True) + + # List (numbered items) + test.add_input("./DigitalInternal.h", + "/**" + os.linesep + \ + " * The default PWM period is in ms." + os.linesep + \ + " *" + os.linesep + \ + " * 1. 20ms periods (50 Hz) are the \"safest\" setting in that this works for all" + os.linesep + \ + " * devices" + os.linesep + \ + " * 2. 20ms periods seem to be desirable for Vex Motors" + os.linesep + \ + " * 3. 20ms periods are the specified period for HS-322HD servos, but work" + os.linesep + \ + " * reliably down to 10.0 ms; starting at about 8.5ms, the servo sometimes hums" + os.linesep + \ + " * and get hot; by 5.0ms the hum is nearly continuous" + os.linesep + \ + " * 4. 10ms periods work well for Victor 884" + os.linesep + \ + " * 5. 5ms periods allows higher update rates for Luminary Micro Jaguar speed" + os.linesep + \ + " * controllers. Due to the shipping firmware on the Jaguar, we can't run the" + os.linesep + \ + " * update period less than 5.05 ms." + os.linesep + \ + " *" + os.linesep + \ + " * kDefaultPwmPeriod is the 1x period (5.05 ms). In hardware, the period" + os.linesep + \ + " * scaling is implemented as an output squelch to get longer periods for old" + os.linesep + \ + " * devices." + os.linesep + \ + " */" + os.linesep) + test.add_latest_input_as_output(True) + + # C++: paragraphs with @param tags + test.add_input("./PIDController.cpp", + " /**" + os.linesep + \ + " * Allocate a PID object with the given constants for P, I, D." + os.linesep + \ + " *" + os.linesep + \ + " * More summary." + os.linesep + \ + " * Even more summary." + os.linesep + \ + " *" + os.linesep + \ + " * @param Kp the proportional coefficient" + os.linesep + \ + " * @param Ki the integral coefficient" + os.linesep + \ + " * @param Kd the derivative coefficient" + os.linesep + \ + " * @param source The PIDSource object that is used to get values" + os.linesep + \ + " * @param output The PIDOutput object that is set to the output value" + os.linesep + \ + " * @param period the loop time for doing calculations. This particularly" + os.linesep + \ + " * effects calculations of the integral and differental terms." + os.linesep + \ + " * The default is 50ms." + os.linesep + \ + " */" + os.linesep + \ + " PIDController::PIDController(double Kp, double Ki, double Kd, PIDSource* source," + os.linesep + \ + " PIDOutput* output, double period)" + os.linesep) + test.add_output( + " /**" + os.linesep + \ + " * Allocate a PID object with the given constants for P, I, D." + os.linesep + \ + " *" + os.linesep + \ + " * More summary. Even more summary." + os.linesep + \ + " *" + os.linesep + \ + " * @param Kp The proportional coefficient." + os.linesep + \ + " * @param Ki The integral coefficient." + os.linesep + \ + " * @param Kd The derivative coefficient." + os.linesep + \ + " * @param source The PIDSource object that is used to get values." + os.linesep + \ + " * @param output The PIDOutput object that is set to the output value." + os.linesep + \ + " * @param period The loop time for doing calculations. This particularly" + os.linesep + \ + " * effects calculations of the integral and differental terms." + os.linesep + \ + " * The default is 50ms." + os.linesep + \ + " */" + os.linesep + \ + " PIDController::PIDController(double Kp, double Ki, double Kd, PIDSource* source," + os.linesep + \ + " PIDOutput* output, double period)" + os.linesep, True, True) + + # Java: paragraphs with @param tags idempotence + test.add_input("./PIDController.java", + " /**" + os.linesep + \ + " * Allocate a PID object with the given constants for P, I, D." + os.linesep + \ + " *" + os.linesep + \ + " * More summary." + os.linesep + \ + " * Even more summary." + os.linesep + \ + " *" + os.linesep + \ + " * @param Kp the proportional coefficient" + os.linesep + \ + " * @param Ki the integral coefficient" + os.linesep + \ + " * @param Kd the derivative coefficient" + os.linesep + \ + " * @param source The PIDSource object that is used to get values" + os.linesep + \ + " * @param output The PIDOutput object that is set to the output value" + os.linesep + \ + " * @param period the loop time for doing calculations. This particularly" + os.linesep + \ + " * effects calculations of the integral and differental terms." + os.linesep + \ + " * The default is 50ms." + os.linesep + \ + " */" + os.linesep + \ + " PIDController(double Kp, double Ki, double Kd, PIDSource source, PIDOutput output, double period)" + os.linesep) + test.add_output( + " /**" + os.linesep + \ + " * Allocate a PID object with the given constants for P, I, D." + os.linesep + \ + " *" + os.linesep + \ + " *

More summary. Even more summary." + os.linesep + \ + " *" + os.linesep + \ + " * @param Kp The proportional coefficient." + os.linesep + \ + " * @param Ki The integral coefficient." + os.linesep + \ + " * @param Kd The derivative coefficient." + os.linesep + \ + " * @param source The PIDSource object that is used to get values." + os.linesep + \ + " * @param output The PIDOutput object that is set to the output value." + os.linesep + \ + " * @param period The loop time for doing calculations. This particularly effects calculations of" + os.linesep + \ + " * the integral and differental terms. The default is 50ms." + os.linesep + \ + " */" + os.linesep + \ + " PIDController(double Kp, double Ki, double Kd, PIDSource source, PIDOutput output, double period)" + os.linesep, True, True) + + test.add_input("./PIDController.java", + " /**" + os.linesep + \ + " * Allocate a PID object with the given constants for P, I, D." + os.linesep + \ + " *" + os.linesep + \ + " *

More summary. Even more summary." + os.linesep + \ + " *" + os.linesep + \ + " * @param Kp The proportional coefficient." + os.linesep + \ + " * @param Ki The integral coefficient." + os.linesep + \ + " * @param Kd The derivative coefficient." + os.linesep + \ + " * @param source The PIDSource object that is used to get values." + os.linesep + \ + " * @param output The PIDOutput object that is set to the output value." + os.linesep + \ + " * @param period The loop time for doing calculations. This particularly effects calculations of" + os.linesep + \ + " * the integral and differental terms. The default is 50ms." + os.linesep + \ + " */" + os.linesep + \ + " PIDController(double Kp, double Ki, double Kd, PIDSource source, PIDOutput output, double period)" + os.linesep) + test.add_latest_input_as_output(True) + + # Java: Don't count "{@" as tag (only "@" at beginning of line) + test.add_input("./Test.java", + "/**" + os.linesep + \ + " * This is a {@link test} description." + os.linesep + \ + " *" + os.linesep + \ + " * @param test Test parameter." + os.linesep + \ + " */" + os.linesep) + test.add_latest_input_as_output(True) + + # Java: Make sure {@link ...} is wrapped atomically + test.add_input("./Test.java", + "/**" + os.linesep + \ + " * This is a sentence. This is a really, really, really, really long sentence with a Javadoc {@link test} in it." + os.linesep + \ + " *" + os.linesep + \ + " * @param test Test parameter." + os.linesep + \ + " */" + os.linesep) + test.add_output( + "/**" + os.linesep + \ + " * This is a sentence. This is a really, really, really, really long sentence with a Javadoc" + os.linesep + \ + " * {@link test} in it." + os.linesep + \ + " *" + os.linesep + \ + " * @param test Test parameter." + os.linesep + \ + " */" + os.linesep, True, True) + + test.run(OutputType.FILE)