diff --git a/src/bscPlugin/validation/BrsFileValidator.spec.ts b/src/bscPlugin/validation/BrsFileValidator.spec.ts index 3c80e79c1..09d7a04be 100644 --- a/src/bscPlugin/validation/BrsFileValidator.spec.ts +++ b/src/bscPlugin/validation/BrsFileValidator.spec.ts @@ -1172,5 +1172,39 @@ describe('BrsFileValidator', () => { }); }); + + // Skipped until we can figure out how to handle @var tags + describe.skip('@var', () => { + it('uses @var type in brs file to define types of variables', () => { + const file = program.setFile('source/main.brs', ` + function getPie() as string + ' @var {string} someDate + if m.top.isTrue + someDate = getDate() + else + someDate = m.date2 + end if + + if m.someProp + someDate = m.someProp.date + end if + + return someDate + end function + + function getDate() + return "Dec 25" + end function + `); + program.validate(); + expectZeroDiagnostics(program); + const data = {} as ExtraSymbolData; + const funcStmt = file.ast.findChild(isFunctionStatement); + const returnStmt = funcStmt.findChild(isReturnStatement); + const varType = returnStmt.getSymbolTable().getSymbolType('someDate', { flags: SymbolTypeFlag.runtime, data: data }); + expectTypeToBe(varType, StringType); + }); + + }); }); }); diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts index ee1ae73aa..6d7ead3ab 100644 --- a/src/bscPlugin/validation/BrsFileValidator.ts +++ b/src/bscPlugin/validation/BrsFileValidator.ts @@ -15,6 +15,8 @@ import { DynamicType } from '../../types/DynamicType'; import util from '../../util'; import type { Range } from 'vscode-languageserver'; import type { Token } from '../../lexer/Token'; +import type { BrightScriptDoc } from '../../parser/BrightScriptDocParser'; +import brsDocParser from '../../parser/BrightScriptDocParser'; export class BrsFileValidator { constructor( @@ -238,6 +240,25 @@ export class BrsFileValidator { // eslint-disable-next-line no-bitwise node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node, doNotMerge: true, isAlias: true }, targetType, SymbolTypeFlag.runtime | SymbolTypeFlag.typetime); + }, + AstNode: (node) => { + //check for doc comments + if (!node.leadingTrivia || node.leadingTrivia.length === 0) { + return; + } + const doc = brsDocParser.parseNode(node); + if (doc.tags.length === 0) { + return; + } + + let funcExpr = node.findAncestor(isFunctionExpression); + if (funcExpr) { + // handle comment tags inside a function expression + this.processDocTagsInFunction(doc, node, funcExpr); + } else { + //handle comment tags outside of a function expression + this.processDocTagsAtTopLevel(doc, node); + } } }); @@ -248,6 +269,33 @@ export class BrsFileValidator { }); } + private processDocTagsInFunction(doc: BrightScriptDoc, node: AstNode, funcExpr: FunctionExpression) { + //TODO: Handle doc tags that influence the function they're in + + // For example, declaring variable types: + // const symbolTable = funcExpr.body.getSymbolTable(); + + // for (const varTag of doc.getAllTags(BrsDocTagKind.Var)) { + // const varName = (varTag as BrsDocParamTag).name; + // const varTypeStr = (varTag as BrsDocParamTag).type; + // const data: ExtraSymbolData = {}; + // const type = doc.getTypeFromContext(varTypeStr, node, { flags: SymbolTypeFlag.typetime, fullName: varTypeStr, data: data, tableProvider: () => symbolTable }); + // if (type) { + // symbolTable.addSymbol(varName, { ...data, isFromDocComment: true }, type, SymbolTypeFlag.runtime); + // } + // } + } + + private processDocTagsAtTopLevel(doc: BrightScriptDoc, node: AstNode) { + //TODO: + // - handle import statements? + // - handle library statements? + // - handle typecast statements? + // - handle alias statements? + // - handle const statements? + // - allow interface definitions? + } + /** * Validate that a statement is defined in one of these specific locations * - the root of the AST diff --git a/src/parser/BrightScriptDocParser.spec.ts b/src/parser/BrightScriptDocParser.spec.ts index da0397545..575cde352 100644 --- a/src/parser/BrightScriptDocParser.spec.ts +++ b/src/parser/BrightScriptDocParser.spec.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { brsDocParser } from './BrightScriptDocParser'; import { Parser } from './Parser'; -describe('BrightScriptbrsDocParser', () => { +describe('BrightScriptDocParser', () => { it('should get a comment', () => { @@ -155,6 +155,26 @@ describe('BrightScriptbrsDocParser', () => { `); expect(doc.getTypeTag().type).to.equal('integer'); }); + it('finds the var tag', () => { + const doc = brsDocParser.parse(` + @var {integer} varName + `); + expect(doc.getVar('VarName').type).to.equal('integer'); + }); + + it('is case INSENSITIVE for param names', () => { + const doc = brsDocParser.parse(` + @param {integer} ALLCAPS + `); + expect(doc.getParam('allcaps').type).to.equal('integer'); + }); + + it('is case SENSITIVE for tag names', () => { + const doc = brsDocParser.parse(` + @ALLCAPS + `); + expect(doc.getTag('allcaps')).to.be.undefined; + }); describe('nodes', () => { const parser = new Parser(); @@ -191,6 +211,28 @@ describe('BrightScriptbrsDocParser', () => { expect(doc.getReturn().type).to.equal('integer'); }); + it('should get documentation when it is wrapped in jsdoc /** */', () => { + let { ast } = parser.parse(` + ' /** + ' * My description + ' * of this function + ' * @param p1 this is p1 + ' * @param p2 this is p2 + ' * @return {integer} sum of p1 and p2 + ' */ + function foo(p1, p2) + return p1 + p2 + end function + `); + + const doc = brsDocParser.parseNode(ast.statements[0]); + expect(doc.description).to.equal('My description\nof this function'); + expect(doc.getAllTags('param').length).to.equal(2); + expect(doc.getParam('p1').description).to.equal('this is p1'); + expect(doc.getParam('p2').description).to.equal('this is p2'); + expect(doc.getReturn().description).to.equal('sum of p1 and p2'); + expect(doc.getReturn().type).to.equal('integer'); + }); }); }); diff --git a/src/parser/BrightScriptDocParser.ts b/src/parser/BrightScriptDocParser.ts index 78aaf9383..27242f60c 100644 --- a/src/parser/BrightScriptDocParser.ts +++ b/src/parser/BrightScriptDocParser.ts @@ -9,6 +9,15 @@ const paramRegex = /(?:{([^}]*)}\s+)?(?:(\[?\w+\]?))\s*(.*)/; const returnRegex = /(?:{([^}]*)})?\s*(.*)/; const typeTagRegex = /(?:{([^}]*)})?/; +export enum BrsDocTagKind { + Description = 'description', + Param = 'param', + Return = 'return', + Type = 'type', + Var = 'var' +} + + export class BrightScriptDocParser { public parseNode(node: AstNode) { @@ -72,17 +81,19 @@ export class BrightScriptDocParser { if (!match) { return; } - const tagName = match[1].toLowerCase(); + const tagName = match[1]; const detail = match[2] ?? ''; switch (tagName) { - case 'param': + case BrsDocTagKind.Param: return this.parseParam(detail); - case 'return': + case BrsDocTagKind.Return: case 'returns': return this.parseReturn(detail); - case 'type': + case BrsDocTagKind.Type: return this.parseType(detail); + case BrsDocTagKind.Var: + return { ...this.parseParam(detail), tagName: BrsDocTagKind.Var }; } return { tagName: tagName, @@ -108,7 +119,7 @@ export class BrightScriptDocParser { paramName = paramName.replace(/\[|\]/g, '').trim(); } return { - tagName: 'param', + tagName: BrsDocTagKind.Param, name: paramName, type: type, description: description, @@ -126,7 +137,7 @@ export class BrightScriptDocParser { description = match[2] ?? ''; } return { - tagName: 'return', + tagName: BrsDocTagKind.Return, type: type, description: description, detail: detail @@ -142,14 +153,14 @@ export class BrightScriptDocParser { } } return { - tagName: 'type', + tagName: BrsDocTagKind.Type, type: type, detail: detail }; } } -class BrightScriptDoc { +export class BrightScriptDoc { protected _description: string; @@ -166,7 +177,7 @@ class BrightScriptDoc { get description() { const descTag = this.tags.find((tag) => { - return tag.tagName === 'description'; + return tag.tagName === BrsDocTagKind.Description; }); let result = this._description ?? ''; @@ -178,34 +189,41 @@ class BrightScriptDoc { } getParam(name: string) { + const lowerName = name.toLowerCase(); + return this.tags.find((tag) => { + return tag.tagName === BrsDocTagKind.Param && (tag as BrsDocParamTag).name.toLowerCase() === lowerName; + }) as BrsDocParamTag; + } + + getVar(name: string) { + const lowerName = name.toLowerCase(); return this.tags.find((tag) => { - return tag.tagName === 'param' && (tag as BrsDocParamTag).name === name; + return tag.tagName === BrsDocTagKind.Var && (tag as BrsDocParamTag).name.toLowerCase() === lowerName; }) as BrsDocParamTag; } + getReturn() { return this.tags.find((tag) => { - return tag.tagName === 'return' || tag.tagName === 'returns'; + return tag.tagName === BrsDocTagKind.Return || tag.tagName === 'returns'; }) as BrsDocWithDescription; } getTypeTag() { return this.tags.find((tag) => { - return tag.tagName === 'type'; + return tag.tagName === BrsDocTagKind.Type; }) as BrsDocWithType; } getTag(tagName: string) { - const lowerTagName = tagName.toLowerCase(); return this.tags.find((tag) => { - return tag.tagName === lowerTagName; + return tag.tagName === tagName; }); } getAllTags(tagName: string) { - const lowerTagName = tagName.toLowerCase(); return this.tags.filter((tag) => { - return tag.tagName === lowerTagName; + return tag.tagName === tagName; }); } @@ -215,6 +233,12 @@ class BrightScriptDoc { return this.getTypeFromContext(param?.type, nodeContext, options); } + getVarBscType(name: string, nodeContext: AstNode, options: GetSymbolTypeOptions) { + const param = this.getVar(name); + + return this.getTypeFromContext(param?.type, nodeContext, options); + } + getReturnBscType(nodeContext: AstNode, options: GetSymbolTypeOptions) { const retTag = this.getReturn(); @@ -227,7 +251,7 @@ class BrightScriptDoc { return this.getTypeFromContext(retTag?.type, nodeContext, options); } - private getTypeFromContext(typeName: string, nodeContext: AstNode, options: GetSymbolTypeOptions) { + getTypeFromContext(typeName: string, nodeContext: AstNode, options: GetSymbolTypeOptions) { // TODO: Add support for union types here const topSymbolTable = nodeContext?.getSymbolTable(); if (!topSymbolTable || !typeName) { @@ -249,19 +273,19 @@ class BrightScriptDoc { } } -interface BrsDocTag { +export interface BrsDocTag { tagName: string; detail?: string; } -interface BrsDocWithType extends BrsDocTag { +export interface BrsDocWithType extends BrsDocTag { type?: string; } -interface BrsDocWithDescription extends BrsDocWithType { +export interface BrsDocWithDescription extends BrsDocWithType { description?: string; } -interface BrsDocParamTag extends BrsDocWithDescription { +export interface BrsDocParamTag extends BrsDocWithDescription { name: string; optional?: boolean; }