diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cc76b6..468411f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,42 +9,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Enum and const values containing BrightScript hex literals (`&hFF0000FF`) are now normalized to valid JS syntax (`0xFF0000FF`) before being emitted, instead of breaking jsdoc's parser ([#14](https://github.com/markwpearce/brighterscript-jsdocs-plugin/issues/14)) -- Class fields with an object literal default value (`foo = {}`) no longer crash with `this.getLookupTable is not a function` (fixed upstream in `brighterscript@1.0.0-alpha.56`) ([#17](https://github.com/markwpearce/brighterscript-jsdocs-plugin/issues/17)) -- Constants with a non-literal value (eg. `1.0 / 30.0`, `-1`, or a reference to another constant) no longer emit `[object Object]` and break jsdoc's parser; the value's source text is used as the `@default` instead ([#18](https://github.com/markwpearce/brighterscript-jsdocs-plugin/issues/18)) +- Enum and const values containing BrightScript hex literals (`&hFF0000FF`) are now normalized to valid JS syntax (`0xFF0000FF`) before being emitted, instead of breaking jsdoc's parser ([#14](https://github.com/markwpearce/brighterscript-jsdocs-plugin/issues/14)) +- Class fields with an object literal default value (`foo = {}`) no longer crash with `this.getLookupTable is not a function` (fixed upstream in `brighterscript@1.0.0-alpha.56`) ([#17](https://github.com/markwpearce/brighterscript-jsdocs-plugin/issues/17)) +- Constants with a non-literal value (eg. `1.0 / 30.0`, `-1`, or a reference to another constant) no longer emit `[object Object]` and break jsdoc's parser; the value's source text is used as the `@default` instead ([#18](https://github.com/markwpearce/brighterscript-jsdocs-plugin/issues/18)) +- Multi-line `@param` and `@returns` descriptions now stay with their tag instead of being appended to the function's description. Lines following one of those tags (up to the next tag or a blank comment line) are treated as part of its description ([#11](https://github.com/markwpearce/brighterscript-jsdocs-plugin/issues/11)) ### Added -- Support for BrighterScript v1 type syntax in doc comments: union types (`string or integer` → `{(string|integer)}`), typed arrays (`string[]` → `{Array.}`, including arrays of custom class/interface types), and intersection types (`A and B`, which fall back to `{dynamic}` since JSDoc's type grammar has no intersection operator) -- `AGENTS.md` with instructions for AI coding agents; `CLAUDE.md` now points to it +- Support for BrighterScript v1 type syntax in doc comments: union types (`string or integer` → `{(string|integer)}`), typed arrays (`string[]` → `{Array.}`, including arrays of custom class/interface types), and intersection types (`A and B`, which fall back to `{dynamic}` since JSDoc's type grammar has no intersection operator) +- `AGENTS.md` with instructions for AI coding agents; `CLAUDE.md` now points to it ### Changed -- Updated to `brighterscript@^1.0.0-alpha.56` (BrighterScript v1 is still in alpha). This is a `dependency`, not a `peerDependency`, matching the convention used on other RokuCommunity projects' `v1` branches (eg. bslint) -- Releases from this branch are alpha prereleases: `npm run release:alpha` bumps a `-alpha.N` version and publishes to npm under the `next` dist-tag (not `latest`), so this doesn't become the default install -- Ported the plugin's AST traversal to BrighterScript v1's restructured parser API: `CommentStatement` no longer exists in v1 (comments are lexer trivia, not AST statements), and many statement properties moved under a `.tokens` object +- Updated to `brighterscript@^1.0.0-alpha.56` (BrighterScript v1 is still in alpha). This is a `dependency`, not a `peerDependency`, matching the convention used on other RokuCommunity projects' `v1` branches (eg. bslint) +- Releases from this branch are alpha prereleases: `npm run release:alpha` bumps a `-alpha.N` version and publishes to npm under the `next` dist-tag (not `latest`), so this doesn't become the default install +- Ported the plugin's AST traversal to BrighterScript v1's restructured parser API: `CommentStatement` no longer exists in v1 (comments are lexer trivia, not AST statements), and many statement properties moved under a `.tokens` object ## [0.7.3] ### Fixed -- Wrong file in package.json +- Wrong file in package.json ## [0.7.2] ### Added -- Support for Brighterscript Interfaces, Consts, Enums +- Support for Brighterscript Interfaces, Consts, Enums ### Changed -- Brighterscript Namespaces are not placed under modules -- Much better and consistent JSdoc output -- Added unit tests -- Migrated to TypeScript +- Brighterscript Namespaces are not placed under modules +- Much better and consistent JSdoc output +- Added unit tests +- Migrated to TypeScript ## [0.6.0] ### Added -- Previous Release +- Previous Release diff --git a/examples/source/MultiLineDescriptions.bs b/examples/source/MultiLineDescriptions.bs new file mode 100644 index 0000000..c678e0a --- /dev/null +++ b/examples/source/MultiLineDescriptions.bs @@ -0,0 +1,10 @@ +' Moves an entity towards a target position +' @param {object} entity the entity to move. It must have +' x and y properties +' @param speed how far to move per second, which is capped +' at the entity's max speed +' @return true if the entity reached the target +' during this move +function moveTowards(entity as object, speed as float) as boolean + return false +end function diff --git a/src/convert-brighterscript-docs.spec.ts b/src/convert-brighterscript-docs.spec.ts index c2d8d34..13bea73 100644 --- a/src/convert-brighterscript-docs.spec.ts +++ b/src/convert-brighterscript-docs.spec.ts @@ -72,6 +72,36 @@ describe('convertBrighterscriptDocs', () => { `); }); + it('keeps multi-line param and return descriptions with their tags', () => { + expectOutput(cbd.convertBrighterscriptDocs(` + ' Say hello + ' @param {string} name you want to say hello to + ' which can be long + ' @param count + ' how many times to say it + ' @returns the greeting + ' repeated count times + ' + ' More description after a blank line + function sayHello(name as string, count as integer) as string + return "Hello, " + name + "!") + end function + `), ` + /** + * Say hello + * + * More description after a blank line + * @function + * @param {string} name you want to say hello to + * which can be long + * @param {integer} count how many times to say it + * @returns {string} the greeting + * repeated count times + */ + function sayHello (name, count) { }; + `); + }); + it('uses comment type over given type', () => { expectOutput(cbd.convertBrighterscriptDocs(` ' Say hello diff --git a/src/convert-brighterscript-docs.ts b/src/convert-brighterscript-docs.ts index 4906875..c60ecbb 100644 --- a/src/convert-brighterscript-docs.ts +++ b/src/convert-brighterscript-docs.ts @@ -8,6 +8,8 @@ const paramRegexNoType = /@param\s+(?:\[(\w+).*\]|(\w+))[\s\-\s|\s]*(.*)/; const returnRegex = /@returns?\s*({(?:[^}]*)})?\s*(.*)/; const extendsRegex = /@extends/; const moduleRegex = /@module ([^\*\s]+)/; +const paramOrReturnTagRegex = /^ \* @(?:param|returns?)\b/; +const tagContinuationRegex = /^ \* [^@\s]/; const escapeCharEntities = { '&': '&', '<': '<', @@ -195,6 +197,43 @@ function paramOrReturnDescriptionHelper(desc = '') { return ''; } +/** + * Folds the lines that follow a `@param` or `@returns` tag (up to the next tag or blank line) into that + * tag's line, separated by newlines. Those tags get moved below the description, so without this any + * multi-line description would be left behind in the main description. + * + * @param commentLines jsdoc comment lines, as returned from convertCommentTextToJsDocLines() + * @returns the comment lines, with continuation lines merged into their tags + */ +function mergeTagContinuationLines(commentLines: string[]) { + const output: string[] = []; + let inParamOrReturnTag = false; + for (const line of commentLines) { + if (inParamOrReturnTag && tagContinuationRegex.test(line)) { + output[output.length - 1] += '\n' + line; + continue; + } + inParamOrReturnTag = paramOrReturnTagRegex.test(line); + output.push(line); + } + return output; +} + +/** + * Splits a (possibly merged) tag line into the tag line itself and the text of its continuation lines + */ +function splitTagLine(line: string) { + const [tagLine, ...continuationLines] = line.split('\n'); + return { tagLine: tagLine, continuationText: continuationLines.map(continuation => continuation.replace(/^ \* /, '')) }; +} + +/** + * Joins the first line of a tag's description with its continuation lines, each on its own jsdoc line + */ +function joinDescriptionLines(firstLine = '', continuationText: string[] = []) { + return [paramOrReturnDescriptionHelper(firstLine), ...continuationText].filter(line => line).join('\n * '); +} + function getMemberOf(moduleName = '', namespaceName = '') { const memberOf = namespaceName || moduleName; const memberType = namespaceName ? '' : 'module:'; @@ -267,7 +306,7 @@ function convertCommentTextToJsDocLines(commentLines: string[] = []) { */ function processFunction(func: bs.FunctionStatement | bs.InterfaceMethodStatement | bs.MethodStatement, moduleName = '', namespaceName = '') { const output: string[] = []; - let commentLines = convertCommentTextToJsDocLines(getCommentLines(func)); + let commentLines = mergeTagContinuationLines(convertCommentTextToJsDocLines(getCommentLines(func))); const paramNameList: string[] = []; const params = bs.isInterfaceMethodStatement(func) ? func.params : func.func.parameters; let returnTypeExpression = bs.isInterfaceMethodStatement(func) ? func.returnTypeExpression : func.func.returnTypeExpression; @@ -292,7 +331,8 @@ function processFunction(func: bs.FunctionStatement | bs.InterfaceMethodStatemen // remove @param lines for the current param commentLines = commentLines.filter(commentLine => { - let commentMatch = paramRegex.exec(commentLine); + const { tagLine, continuationText } = splitTagLine(commentLine); + let commentMatch = paramRegex.exec(tagLine); if (commentMatch) { const commentParamName = (commentMatch[2] || commentMatch[3]) || ''; @@ -302,17 +342,17 @@ function processFunction(func: bs.FunctionStatement | bs.InterfaceMethodStatemen // same parameter name - use these details if (commentParamType) { paramType = commentParamType.trim(); - paramDescription = commentMatch[4] || paramDescription; + paramDescription = joinDescriptionLines(commentMatch[4], continuationText) || paramDescription; } return false; } } else { - commentMatch = paramRegexNoType.exec(commentLine); + commentMatch = paramRegexNoType.exec(tagLine); if (commentMatch) { const commentParamName = (commentMatch[1] || commentMatch[2]) || ''; if (paramName.trim().toLowerCase() === commentParamName.trim().toLowerCase()) { // same parameter name - use these details - paramDescription = commentMatch[3] || paramDescription; + paramDescription = joinDescriptionLines(commentMatch[3], continuationText) || paramDescription; return false; } } @@ -329,7 +369,7 @@ function processFunction(func: bs.FunctionStatement | bs.InterfaceMethodStatemen } if (paramDescription) { - paramLine += ` ${paramOrReturnDescriptionHelper(paramDescription)}`; + paramLine += ` ${paramDescription}`; } output.push(paramLine); } @@ -348,7 +388,8 @@ function processFunction(func: bs.FunctionStatement | bs.InterfaceMethodStatemen let returnLine = ` * @returns {${returnTypeString}}`; // Find the return line in the comments for (let i = 0; i < commentLines.length; i++) { - let commentMatch = returnRegex.exec(commentLines[i]); + const { tagLine, continuationText } = splitTagLine(commentLines[i]); + let commentMatch = returnRegex.exec(tagLine); if (commentMatch) { let commentReturnType = returnTypeString; if (commentMatch[1] && commentMatch[1].trim().toLowerCase() === commentReturnType.toLowerCase()) { @@ -356,8 +397,9 @@ function processFunction(func: bs.FunctionStatement | bs.InterfaceMethodStatemen commentReturnType = commentMatch[1].trim(); } returnLine = ` * @returns {${commentReturnType}}`; - if (commentMatch[2]) { - returnLine += ' ' + paramOrReturnDescriptionHelper(commentMatch[2]); + const returnDescription = joinDescriptionLines(commentMatch[2], continuationText); + if (returnDescription) { + returnLine += ' ' + returnDescription; } // remove the original comment @returns line commentLines.splice(i, 1);