Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<string>}`, 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.<string>}`, 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
10 changes: 10 additions & 0 deletions examples/source/MultiLineDescriptions.bs
Original file line number Diff line number Diff line change
@@ -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
30 changes: 30 additions & 0 deletions src/convert-brighterscript-docs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 51 additions & 9 deletions src/convert-brighterscript-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
'&': '&amp;',
'<': '&lt;',
Expand Down Expand Up @@ -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:';
Expand Down Expand Up @@ -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;
Expand All @@ -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]) || '';
Expand All @@ -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;
}
}
Expand All @@ -329,7 +369,7 @@ function processFunction(func: bs.FunctionStatement | bs.InterfaceMethodStatemen
}

if (paramDescription) {
paramLine += ` ${paramOrReturnDescriptionHelper(paramDescription)}`;
paramLine += ` ${paramDescription}`;
}
output.push(paramLine);
}
Expand All @@ -348,16 +388,18 @@ 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()) {
// there is a return type given, and it matches the type of the function
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);
Expand Down
Loading