diff --git a/.cfformat.json b/.cfformat.json index 7d5cc7f..91ff6c3 100644 --- a/.cfformat.json +++ b/.cfformat.json @@ -2,6 +2,7 @@ "alignment.consecutive.assignments": false, "alignment.consecutive.params": false, "alignment.consecutive.properties": false, + "alignment.doc_comments": false, "array.empty_padding": false, "array.multiline.comma_dangle": false, "array.multiline.element_count": 4, diff --git a/data/examples.json b/data/examples.json index c0ef06a..3c9fb27 100644 --- a/data/examples.json +++ b/data/examples.json @@ -11,6 +11,10 @@ "true":"// alignment.consecutive.properties: true\nproperty name=\"requestService\" inject=\"coldbox:requestService\";\nproperty name=\"log\" inject=\"logbox:logger:{this}\";", "false":"// alignment.consecutive.properties: false\nproperty name=\"requestService\" inject=\"coldbox:requestService\";\nproperty name=\"log\" inject=\"logbox:logger:{this}\";" }, + "alignment.doc_comments":{ + "true":"// alignment.doc_comments: true\n/**\n * @name test\n * @b another param\n */", + "false":"// alignment.doc_comments: false\n/**\n * @name test\n * @b another param\n */" + }, "array.empty_padding":{ "true":"// array.empty_padding: true\nmyArray = [ ];", "false":"// array.empty_padding: false\nmyArray = [];" diff --git a/data/reference.json b/data/reference.json index 69557e7..2fc0306 100644 --- a/data/reference.json +++ b/data/reference.json @@ -20,6 +20,13 @@ }, "type": "boolean" }, + "alignment.doc_comments": { + "description": "When true, cfformat will attempt to align the @param descriptions and @throws descriptions in doc comments.", + "example": { + "code": "/**\n * @name test\n * @b another param\n */" + }, + "type": "boolean" + }, "array.empty_padding": { "description": "When true, empty arrays are padded with a space.", "example": { diff --git a/models/Alignment.cfc b/models/Alignment.cfc index e98ac54..acae189 100644 --- a/models/Alignment.cfc +++ b/models/Alignment.cfc @@ -36,6 +36,17 @@ component accessors="true" { '(?:"[^"]*"|''[^'']*''|#identifier#)', // attribute value ')?' // attribute value is optional ]; + variables.docParamRegex = [ + '^([ \t]*\*\s*)', // leading indentation and * + '(?!(?i:@throws|@return))', // not @throws or @return + '(@#identifier#)', // param name + '([^\r\n]*)\r?\n' // param description (rest of the line) + ]; + variables.docThrowsRegex = [ + '^([ \t]*\*\s*)', // leading indentation and * + '(?i:(@throws\s+#identifier#))', + '([^\r\n]*)\r?\n' // throws description (rest of the line) + ]; function init() { var patternClass = createObject('java', 'java.util.regex.Pattern'); @@ -45,6 +56,9 @@ component accessors="true" { variables.propertiesPattern = patternClass.compile(propertiesRegex.toList(''), 8); variables.paramsPattern = patternClass.compile(propertiesRegex.toList('').replace('property', 'param'), 8); variables.attributePattern = patternClass.compile(attributeRegex.toList(''), 8); + variables.docParamPattern = patternClass.compile(docParamRegex.toList(''), 8); + variables.docThrowsPattern = patternClass.compile(docThrowsRegex.toList(''), 8); + return this; } @@ -151,6 +165,59 @@ component accessors="true" { return src; } + string function alignDocComments(required string src) { + var replacements = []; + var ranges = stringRanges.walk(src); + + for (var matcher in [docParamPattern.matcher(src), docThrowsPattern.matcher(src)]) { + var index = 0; + var strRanges = {index: 1, ranges: ranges}; + + while (matcher.find(index)) { + index = matcher.end(); + + if (!inDocRange(matcher.start(2), strRanges)) { + continue; + } + + var group = [matcher.toMatchResult()]; + var indent = matcher.group(1); + + while (true) { + matcher.region(index, len(src)); + + if (matcher.lookingAt()) { + if ( + inDocRange(matcher.start(2), strRanges) && + len(indent) == len(matcher.group(1)) + ) { + group.append(matcher.toMatchResult()); + index = matcher.end(); + continue; + } + } + + if (arrayLen(group) > 1) { + replacements.append(parseDocParamGroup(group), true); + } + break; + } + } + } + + replacements.sort(function(a, b) { + if (a.start > b.start) return 1; + if (a.start < b.start) return -1; + return 0; + }); + + for (var replacement in replacements.reverse()) { + src = src.substring(0, replacement.start) & replacement.line & src.substring(replacement.end); + } + + return src; + } + private function parseAssignmentGroup(group) { var longestKey = getLongestAssignmentKey(group); var output = []; @@ -214,6 +281,26 @@ component accessors="true" { return longestValues; } + private function parseDocParamGroup(group) { + var longestName = getLongestDocParamName(group); + var output = []; + for (var match in group) { + var line = match.group(2); + line &= repeatString(' ', longestName - line.len()); + line &= ' ' & match.group(3).ltrim(); + output.append({start: match.start(2), end: match.end(3), line: line.trim()}); + } + return output; + } + + private function getLongestDocParamName(group) { + var longest = 0; + for (var m in group) { + longest = max(longest, m.end(2) - m.start(2)); + } + return longest; + } + private function inStringRange(idx, strRanges) { while ( strRanges.ranges.len() >= strRanges.index && @@ -227,4 +314,21 @@ component accessors="true" { ); } + private function inDocRange(idx, strRanges) { + while ( + strRanges.ranges.len() >= strRanges.index && + ( + strRanges.ranges[strRanges.index].end - 1 < idx || + strRanges.ranges[strRanges.index].name != 'doc_comment' + ) + ) { + strRanges.index++; + } + return ( + strRanges.ranges.len() >= strRanges.index && + strRanges.ranges[strRanges.index].start <= idx && + strRanges.ranges[strRanges.index].name == 'doc_comment' + ); + } + } diff --git a/models/CFFormat.cfc b/models/CFFormat.cfc index 95e23e4..bf0f88d 100644 --- a/models/CFFormat.cfc +++ b/models/CFFormat.cfc @@ -207,6 +207,9 @@ component accessors="true" { if (settings['alignment.consecutive.params']) { formatted = this.alignment.alignAttributes(formatted, 'params'); } + if (settings['alignment.doc_comments']) { + formatted = this.alignment.alignDocComments(formatted); + } return bom & formatted; } diff --git a/models/StringRanges.cfc b/models/StringRanges.cfc index 0358854..d2f5dfe 100644 --- a/models/StringRanges.cfc +++ b/models/StringRanges.cfc @@ -2,6 +2,7 @@ component { variables.CFSCRIPT = [ 'line_comment', + 'doc_comment', 'multiline_comment', 'string_single', 'string_double', @@ -52,6 +53,7 @@ component { escaped_single_quote: ['''''', '(?=.)', [], 'first'], hash: ['##', '##', CFSCRIPT, 'first'], line_comment: ['//', '\n', [], 'first'], + doc_comment: ['/\*\*', '\*/', [], 'first'], multiline_comment: ['/\*', '\*/', [], 'first'], string_double: [ '"', @@ -110,6 +112,7 @@ component { 'string_single', 'string_double', 'line_comment', + 'doc_comment', 'multiline_comment', 'tag_comment', 'cfquery_tag' diff --git a/reference.md b/reference.md index f49698c..e9f9446 100644 --- a/reference.md +++ b/reference.md @@ -54,6 +54,28 @@ property name="requestService" inject="coldbox:requestService"; property name="log" inject="logbox:logger:{this}"; ``` +## alignment.doc_comments + +Type: _boolean_ + +Default: **false** + +When true, cfformat will attempt to align the @param descriptions and @throws descriptions in doc comments. + +```cfc +// alignment.doc_comments: true +/** + * @name test + * @b another param + */ + +// alignment.doc_comments: false +/** + * @name test + * @b another param + */ +``` + ## array.empty_padding Type: _boolean_ diff --git a/tests/data/alignDocComments/formatted.txt b/tests/data/alignDocComments/formatted.txt new file mode 100644 index 0000000..2f48d0c --- /dev/null +++ b/tests/data/alignDocComments/formatted.txt @@ -0,0 +1,26 @@ +component { + + /** + * Try's to get a jwt token from the authorization header or the custom header + * defined in the configuration or passed in by you. If it is a valid token and it decodes we will then + * continue to validate the subject it represents. Once those are satisfied, then it will + * store it in the `prc` as `prc.jwt_token` and the payload as `prc.jwt_payload`. + * + * @token The token to parse and validate, if not passed we call the discoverToken() method for you. + * @storeInContext By default, the token will be stored in the request context + * @authenticate By default, the token will be authenticated, you can disable it and do manual authentication. + * + * @throws TokenExpiredException If the token has expired or no longer in the storage (invalidated) + * @throws TokenInvalidException If the token doesn't verify decoding + * @throws TokenNotFoundException If the token cannot be found in the headers + * + * @returns The payload for convenience + */ + struct function parseToken( + string token = discoverToken(), + boolean storeInContext = true, + boolean authenticate = true + ) { + } + +} diff --git a/tests/data/alignDocComments/settings.json b/tests/data/alignDocComments/settings.json new file mode 100644 index 0000000..6f59e16 --- /dev/null +++ b/tests/data/alignDocComments/settings.json @@ -0,0 +1,5 @@ +[ + { + "alignment.doc_comments": true + } +] diff --git a/tests/data/alignDocComments/source.cfc b/tests/data/alignDocComments/source.cfc new file mode 100644 index 0000000..a37e089 --- /dev/null +++ b/tests/data/alignDocComments/source.cfc @@ -0,0 +1,27 @@ +// +component { + + /** + * Try's to get a jwt token from the authorization header or the custom header + * defined in the configuration or passed in by you. If it is a valid token and it decodes we will then + * continue to validate the subject it represents. Once those are satisfied, then it will + * store it in the `prc` as `prc.jwt_token` and the payload as `prc.jwt_payload`. + * + * @token The token to parse and validate, if not passed we call the discoverToken() method for you. + * @storeInContext By default, the token will be stored in the request context + * @authenticate By default, the token will be authenticated, you can disable it and do manual authentication. + * + * @throws TokenExpiredException If the token has expired or no longer in the storage (invalidated) + * @throws TokenInvalidException If the token doesn't verify decoding + * @throws TokenNotFoundException If the token cannot be found in the headers + * + * @returns The payload for convenience + */ + struct function parseToken( + string token = discoverToken(), + boolean storeInContext = true, + boolean authenticate = true + ) { + } + +} diff --git a/tests/specs/alignmentSpec.cfc b/tests/specs/alignmentSpec.cfc index 2ce5235..fa1c433 100644 --- a/tests/specs/alignmentSpec.cfc +++ b/tests/specs/alignmentSpec.cfc @@ -29,6 +29,9 @@ component extends=tests.FormatBaseSpec { it('aligns param attributes', function() { runTests(loadData('alignParamAttributes')); }); + it('aligns doc comment param and throws descriptions', function() { + runTests(loadData('alignDocComments')); + }); }); }