diff --git a/wpiformat/wpiformat/__init__.py b/wpiformat/wpiformat/__init__.py index 1ce8d8c..e6f5058 100644 --- a/wpiformat/wpiformat/__init__.py +++ b/wpiformat/wpiformat/__init__.py @@ -11,6 +11,7 @@ from wpiformat.cidentlist import CIdentList from wpiformat.clangformat import ClangFormat from wpiformat.clangtidy import ClangTidy +from wpiformat.commentformat import CommentFormat from wpiformat.config import Config from wpiformat.eofnewline import EofNewline from wpiformat.gtestname import GTestName @@ -470,6 +471,7 @@ def main(): task_pipeline = [ BraceComment(), CIdentList(), + CommentFormat(), EofNewline(), GTestName(), IncludeGuard(), diff --git a/wpiformat/wpiformat/commentformat.py b/wpiformat/wpiformat/commentformat.py new file mode 100644 index 0000000..3b93a01 --- /dev/null +++ b/wpiformat/wpiformat/commentformat.py @@ -0,0 +1,207 @@ +"""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 + +from wpiformat.task import Task + + +class CommentFormat(Task): + @staticmethod + def should_process_file(config_file, name): + return ( + config_file.is_c_file(name) + or config_file.is_cpp_file(name) + ) + + 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 = "" + 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() + if output_str: + output.append(output_str) + return output + + def run_pipeline(self, config_file, name, lines): + linesep = Task.get_linesep(lines) + + 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 = [] + 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) + ) + 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 = f"{spaces} * @{tag[0]} {tag[1]} " + else: + tagline = f"{spaces} * @{tag[0]} " + + # Capitalize first letter of description and wrap description + contents = self.textwrap( + tag[2][:1].upper() + tag[2][1:], + COLUMN_LIMIT - len(" ") - len(spaces), + ) + + # Write out tags + output += tagline + for i, line in enumerate(contents): + if i == 0: + output += line + else: + output += f"{spaces} * {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) + else: + return (lines, True) diff --git a/wpiformat/wpiformat/test/test_commentformat.py b/wpiformat/wpiformat/test/test_commentformat.py new file mode 100644 index 0000000..6811d15 --- /dev/null +++ b/wpiformat/wpiformat/test/test_commentformat.py @@ -0,0 +1,392 @@ +import os + +from .test_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) + + # Put /** on separate line + test.add_input("./Test.h", "/** asdf */" + os.linesep) + test.add_output( + "/**" + os.linesep + " * Asdf." + os.linesep + " */" + os.linesep, 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, + ) + + # 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, + ) + + # @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, + ) + + # 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, + ) + + # 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. The default" + + os.linesep + + " * 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 effects" + + os.linesep + + " * calculations of the integral and differental terms. 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, + ) + + # 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) + + test.run(OutputType.FILE)