From 2e27a0185591d1fa0041faf37f97ab6e54c9506f Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Wed, 1 Jan 2025 18:43:26 -0400 Subject: [PATCH 1/3] Fixes documentation about plugins, and adds docs about declaring annotations --- docs/plugins.md | 110 ++++++++++++++++++++++++++++++++++-------- src/ProgramBuilder.ts | 3 ++ src/interfaces.ts | 18 +++++++ 3 files changed, 111 insertions(+), 20 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index 9a2e37ef3..084f65ed0 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -50,6 +50,7 @@ While there are no restrictions on plugin names, it helps others to find your pl Full compiler lifecycle: +- `onPluginConfigure` - `beforeProgramCreate` - `afterProgramCreate` - `afterScopeCreate` ("source" scope) @@ -75,13 +76,13 @@ Full compiler lifecycle: - `afterProgramValidate` - `beforePrepublish` - `afterPrepublish` -- `beforePublish` - - `beforeProgramTranspile` +- `beforeSerializeProgram` + - `beforeBuildProgram` - For each file: - - `beforeFileTranspile` - - `afterFileTranspile` - - `afterProgramTranspile` -- `afterPublish` + - `beforePrepareFile` + - `afterPrepareFile` + - `afterBuildProgram` +- `afterSerializeProgram` - `beforeProgramDispose` ### Language server @@ -90,15 +91,15 @@ Once the program has been validated, the language server runs a special loop - i When a file is removed: -- `beforeFileDispose` +- `beforeFileRemove` - `beforeScopeDispose` (component scope) - `afterScopeDispose` (component scope) -- `afterFileDispose` +- `afterFileRemove` When a file is added: -- `beforeFileParse` -- `afterFileParse` +- `beforeProvideFile` +- `afterProvideFile` - `afterScopeCreate` (component scope) - `afterFileValidate` @@ -157,10 +158,25 @@ The top level object is the `ProgramBuilder` which runs the overall process: pre Here are some important interfaces. You can view them in the code at [this link](https://github.com/rokucommunity/brighterscript/blob/ddcb7b2cd219bd9fecec93d52fbbe7f9b972816b/src/interfaces.ts#L190:~:text=export%20interface%20CompilerPlugin%20%7B). ```typescript -export type CompilerPluginFactory = () => CompilierPlugin; +export type CompilerPluginFactory = () => CompilerPlugin; export interface CompilerPlugin { name: string; + + /** + * A list of brighterscript-style function declarations of allowed annotations + * Eg.: [ + * `inline()`, + * `suite(suiteConfig as object)` + * ] + */ + annotations?: string[]; + + /** + * Called when plugin is initially loaded + */ + onPluginConfigure?(event: onPluginConfigureEvent): any; + /** * Called before a new program is created */ @@ -240,7 +256,6 @@ export interface CompilerPlugin { afterScopeDispose?(event: AfterScopeDisposeEvent): any; beforeScopeValidate?(event: BeforeScopeValidateEvent): any; - /** * Called before the `provideDefinition` hook */ @@ -256,7 +271,6 @@ export interface CompilerPlugin { */ afterProvideDefinition?(event: AfterProvideDefinitionEvent): any; - /** * Called before the `provideReferences` hook */ @@ -304,8 +318,6 @@ export interface CompilerPlugin { */ afterProvideWorkspaceSymbols?(event: AfterProvideWorkspaceSymbolsEvent): any; - - onGetSemanticTokens?: PluginHandler; //scope events onScopeValidate?(event: OnScopeValidateEvent): any; afterScopeValidate?(event: BeforeScopeValidateEvent): any; @@ -554,7 +566,7 @@ export default function () { ## Modifying code Sometimes plugins will want to modify code before the project is transpiled. While you can technically edit the AST directly at any point in the file's lifecycle, this is not recommended as those changes will remain changed as long as that file exists in memory and could cause issues with file validation if the plugin is used in a language-server context (i.e. inside vscode). -Instead, we provide an instace of an `Editor` class in the `beforeFileTranspile` event that allows you to modify AST before the file is transpiled, and then those modifications are undone `afterFileTranspile`. +Instead, we provide an instance of an `Editor` class in the `beforeBuildProgram` and `beforePrepareFile` events that allows you to modify AST before the file is transpiled, and then those modifications are undone after the `afterBuildProgram` event. For example, consider the following brightscript code: ```brightscript @@ -566,14 +578,14 @@ end sub Here's the plugin: ```typescript -import { CompilerPlugin, BeforeFileTranspileEvent, isBrsFile, WalkMode, createVisitor, TokenKind } from 'brighterscript'; +import { CompilerPlugin, BeforePrepareFileEvent, isBrsFile, WalkMode, createVisitor, TokenKind } from 'brighterscript'; // plugin factory export default function () { return { name: 'replacePlaceholders', // transform AST before transpilation - beforeFileTranspile: (event: BeforeFileTranspileEvent) => { + beforePrepareFile: (event: BeforePrepareFileEvent) => { if (isBrsFile(event.file)) { event.file.ast.walk(createVisitor({ LiteralExpression: (literal) => { @@ -600,12 +612,12 @@ Another common use case is to remove print statements and comments. Here's a plu Note: Comments are not regular nodes in the AST. They're considered "trivia". To access them, you need to ask each AstNode for its trivia. to help with this, we've included the `AstNode` visitor method. Here's how you'd do that: ```typescript -import { isBrsFile, createVisitor, WalkMode, BeforeFileTranspileEvent, CompilerPlugin } from 'brighterscript'; +import { isBrsFile, createVisitor, WalkMode, BeforePrepareFileEvent, CompilerPlugin } from 'brighterscript'; export default function plugin() { return { name: 'removeCommentAndPrintStatements', - beforeFileTranspile: (event: BeforeFileTranspileEvent) => { + beforePrepareFile: (event: BeforePrepareFileEvent) => { if (isBrsFile(event.file)) { // visit functions bodies event.file.ast.walk(createVisitor({ @@ -632,6 +644,64 @@ export default function plugin() { } ``` +## Providing Annotations via a plugin + +Plugins may provide [annotations](annotations.md) that can be used to add metadata to any statement in the code. + +Plugins must declare the annotations they support, so they can be validated properly. To declare an annotation, it must be listed in the `annotations` property - a list of Brighterscript-style function declarations. + +For example: + +```typescript + this.annotations = [ + 'inline()', + 'log(prefix as string, addLineNumbers = false as boolean)' + ]; +``` + +Annotations that do not require any arguments are listed as functions with no parameters. Annotations that require arguments may have their parameters types listed as well. + +Here's an example plugin that provides the `log` annotation above: + +```typescript +import { isBrsFile, createVisitor, WalkMode, BeforePrepareFileEvent, CompilerPlugin, FunctionStatement, PrintStatement, createStringLiteral, VariableExpression, createToken, TokenKind, Identifier } from 'brighterscript'; + +export default function plugin() { + return { + name: 'addLogging', + annotations: [ + 'log(prefix as string, addLineNumbers = false as boolean)' + ], + beforePrepareFile: (event: BeforePrepareFileEvent) => { + if (isBrsFile(event.file)) { + event.file.ast.walk(createVisitor({ + FunctionStatement: (funcStmt: FunctionStatement, _parent, owner, key) => { + const logAnnotation = funcStmt.annotations?.find(anno => anno.name === 'log'); + if (logAnnotation) { + const args = logAnnotation.getArguments(); + const logPrintStmt = new PrintStatement({ + print: createToken(TokenKind.Print), + expressions:[ + createStringLiteral(args[0].toString()), // prefix, + createStringLiteral(funcStmt.tokens.name.text) // function name + ] + }); + if(args[1]) { // add line num + logPrintStmt.expressions.unshift(new VariableExpression({ name: createToken(TokenKind.SourceLineNumLiteral) as Identifier })) + } + event.editor.arrayUnshift(funcStmt.func.body.statements, logPrintStmt) + } + } + }), { + walkMode: WalkMode.visitStatements + }); + } + } + } as CompilerPlugin; +} +``` + + ## Modifying `bsconfig.json` via a plugin In some cases you may want to modify the project's configuration via a plugin, such as to change settings based on environment variables or to dynamically modify the project's `files` array. Plugins may do so in the `beforeProgramCreate` step. For example, here's a plugin which adds an additional file to the build: diff --git a/src/ProgramBuilder.ts b/src/ProgramBuilder.ts index 8269d949c..c96ad76cd 100644 --- a/src/ProgramBuilder.ts +++ b/src/ProgramBuilder.ts @@ -163,6 +163,9 @@ export class ProgramBuilder { for (let plugin of plugins) { this.plugins.add(plugin); } + this.plugins.emit('onPluginConfigure', { + builder: this + }); this.plugins.emit('beforeProgramCreate', { builder: this diff --git a/src/interfaces.ts b/src/interfaces.ts index 36de3cc06..cb63ee18b 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -221,6 +221,21 @@ export type CompilerPluginFactory = () => CompilerPlugin; export interface CompilerPlugin { name: string; + + /** + * A list of brighterscript-style function declarations of allowed annotations + * Eg.: [ + * `inline()`, + * `suite(suiteConfig as object)` + * ] + */ + annotations?: string[]; + + /** + * Called when plugin is initially loaded + */ + onPluginConfigure?(event: onPluginConfigureEvent): any; + /** * Called before a new program is created */ @@ -506,6 +521,9 @@ export interface OnGetCodeActionsEvent { codeActions: CodeAction[]; } +export interface onPluginConfigureEvent { + builder: ProgramBuilder; +} export interface BeforeProgramCreateEvent { builder: ProgramBuilder; } From c666d1433a1aa4778517494aa57c479866ce7261 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Fri, 3 Jan 2025 10:09:06 -0400 Subject: [PATCH 2/3] Adds annotation symbol table --- src/PluginInterface.ts | 21 +++++++++++++++++++-- src/Program.ts | 24 +++++++++++++++++++++++- src/ProgramBuilder.spec.ts | 31 +++++++++++++++++++++++++++++-- src/SymbolTable.ts | 1 + src/SymbolTypeFlag.ts | 3 ++- src/astUtils/reflection.ts | 9 ++++++++- src/interfaces.ts | 11 ++++++++++- 7 files changed, 92 insertions(+), 8 deletions(-) diff --git a/src/PluginInterface.ts b/src/PluginInterface.ts index a245b60a2..93448209b 100644 --- a/src/PluginInterface.ts +++ b/src/PluginInterface.ts @@ -1,5 +1,6 @@ -import type { CompilerPlugin } from './interfaces'; +import type { AnnotationDeclaration, CompilerPlugin } from './interfaces'; import { LogLevel, createLogger, type Logger } from './logging'; +import type { TypedFunctionType } from './types/TypedFunctionType'; /* * we use `Required` everywhere here because we expect that the methods on plugin objects will * be optional, and we don't want to deal with `undefined`. @@ -74,7 +75,7 @@ export default class PluginInterface /** * Call `event` on plugins, but allow the plugins to return promises that will be awaited before the next plugin is notified */ - public async emitAsync & string>(event: K, ...args: PluginEventArgs[K]): Promise< PluginEventArgs[K][0]> { + public async emitAsync & string>(event: K, ...args: PluginEventArgs[K]): Promise[K][0]> { for (let plugin of this.plugins) { if ((plugin as any)[event]) { try { @@ -169,4 +170,20 @@ export default class PluginInterface public clear() { this.plugins = []; } + + + private annotationMap: Map>; + + public getAnnotationMap() { + if (this.annotationMap) { + return this.annotationMap; + } + this.annotationMap = new Map>(); + for (let plugin of this.plugins) { + if (plugin.annotations?.length > 0) { + this.annotationMap.set(plugin.name, plugin.annotations); + } + } + return this.annotationMap; + } } diff --git a/src/Program.ts b/src/Program.ts index 4900ccf2d..f7f288e7b 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -16,7 +16,7 @@ import { globalCallables, globalFile } from './globalCallables'; import { parseManifest, getBsConst } from './preprocessor/Manifest'; import { URI } from 'vscode-uri'; import PluginInterface from './PluginInterface'; -import { isBrsFile, isXmlFile, isXmlScope, isNamespaceStatement } from './astUtils/reflection'; +import { isBrsFile, isXmlFile, isXmlScope, isNamespaceStatement, isTypedFunctionType, isAnnotationDeclaration } from './astUtils/reflection'; import type { FunctionStatement, MethodStatement, NamespaceStatement } from './parser/Statement'; import { BscPlugin } from './bscPlugin/BscPlugin'; import { Editor } from './astUtils/Editor'; @@ -54,6 +54,7 @@ import { DiagnosticManager } from './DiagnosticManager'; import { ProgramValidatorDiagnosticsTag } from './bscPlugin/validation/ProgramValidator'; import type { ProvidedSymbolInfo, BrsFile } from './files/BrsFile'; import type { XmlFile } from './files/XmlFile'; +import { SymbolTable } from './SymbolTable'; const bslibNonAliasedRokuModulesPkgPath = s`source/roku_modules/rokucommunity_bslib/bslib.brs`; const bslibAliasedRokuModulesPkgPath = s`source/roku_modules/bslib/bslib.brs`; @@ -123,6 +124,9 @@ export class Program { //TODO we might need to fix this because the isValidated clears stuff now (this.globalScope as any).isValidated = true; + + // Get declarations for all annotations from all plugins + this.populateAnnotationSymbolTable(); } @@ -226,6 +230,24 @@ export class Program { */ public plugins: PluginInterface; + public pluginAnnotationTable = new SymbolTable('Plugin Annotations', () => this.globalScope?.symbolTable); + + private populateAnnotationSymbolTable() { + for (const [pluginName, annotations] of this.plugins.getAnnotationMap().entries()) { + for (const annotation of annotations) { + if (isTypedFunctionType(annotation) && annotation.name) { + this.pluginAnnotationTable.addSymbol(annotation.name, { pluginName: pluginName }, annotation, SymbolTypeFlag.annotation); + } else if (isAnnotationDeclaration(annotation)) { + const annoType = annotation.type; + let description = (typeof annotation.description === 'string') ? annotation.description : undefined; + this.pluginAnnotationTable.addSymbol(annoType.name, { pluginName: pluginName, description: description }, annoType, SymbolTypeFlag.annotation); + } else if (typeof annotation === 'string') { + // TODO: Do we need to parse this? + } + } + } + } + private fileSymbolInformation = new Map(); public addFileSymbolInfo(file: BrsFile) { diff --git a/src/ProgramBuilder.spec.ts b/src/ProgramBuilder.spec.ts index 041bdcf02..8e2b307dd 100644 --- a/src/ProgramBuilder.spec.ts +++ b/src/ProgramBuilder.spec.ts @@ -9,12 +9,14 @@ import { LogLevel, createLogger } from './logging'; import * as diagnosticUtils from './diagnosticUtils'; import { DiagnosticSeverity } from 'vscode-languageserver'; import { BrsFile } from './files/BrsFile'; -import { expectZeroDiagnostics } from './testHelpers.spec'; +import { expectTypeToBe, expectZeroDiagnostics } from './testHelpers.spec'; import type { BsConfig } from './BsConfig'; import type { BscFile } from './files/BscFile'; import { tempDir, rootDir, stagingDir } from './testHelpers.spec'; import { Deferred } from './deferred'; -import type { AfterProgramCreateEvent, BsDiagnostic } from './interfaces'; +import type { AfterProgramCreateEvent, BsDiagnostic, CompilerPlugin, ExtraSymbolData } from './interfaces'; +import { StringType, TypedFunctionType, VoidType } from './types'; +import { SymbolTypeFlag } from './SymbolTypeFlag'; describe('ProgramBuilder', () => { @@ -312,6 +314,31 @@ describe('ProgramBuilder', () => { }); + describe('plugins', () => { + it('adds annotations defined in a plugin to the annotation symbol table', async () => { + builder = new ProgramBuilder(); + + const plugin: CompilerPlugin = { + name: 'test', + annotations: [{ + type: new TypedFunctionType(VoidType.instance) + .setName('myAnnotation') + .addParameter('id', StringType.instance, false), + description: 'Extra description' + }] + }; + builder.plugins.add(plugin); + await builder.load({}); + + const extraData: ExtraSymbolData = {}; + const foundAnnotation = builder.program.pluginAnnotationTable.getSymbolType('myAnnotation', { flags: SymbolTypeFlag.annotation, data: extraData }); + + expectTypeToBe(foundAnnotation, TypedFunctionType); + expect(extraData.pluginName).to.eql('test'); + expect(extraData.description).to.eql('Extra description'); + }); + }); + describe('printDiagnostics', () => { it('does not crash when a diagnostic is missing range informtaion', () => { diff --git a/src/SymbolTable.ts b/src/SymbolTable.ts index 4457d26ca..a1fa1da18 100644 --- a/src/SymbolTable.ts +++ b/src/SymbolTable.ts @@ -233,6 +233,7 @@ export class SymbolTable implements SymbolTypeGetter { options.data.isAlias = data?.isAlias; options.data.isInstance = data?.isInstance; options.data.isFromDocComment = data?.isFromDocComment; + options.data.pluginName = data?.pluginName; } return resolvedType; } diff --git a/src/SymbolTypeFlag.ts b/src/SymbolTypeFlag.ts index cdb962992..a65c05fb5 100644 --- a/src/SymbolTypeFlag.ts +++ b/src/SymbolTypeFlag.ts @@ -6,5 +6,6 @@ export const enum SymbolTypeFlag { private = 8, protected = 16, postTranspile = 32, - deprecated = 64 + deprecated = 64, + annotation = 128 } diff --git a/src/astUtils/reflection.ts b/src/astUtils/reflection.ts index 5ec4fc498..995603480 100644 --- a/src/astUtils/reflection.ts +++ b/src/astUtils/reflection.ts @@ -2,7 +2,7 @@ import type { Body, AssignmentStatement, Block, ExpressionStatement, ExitStateme import type { LiteralExpression, BinaryExpression, CallExpression, FunctionExpression, DottedGetExpression, XmlAttributeGetExpression, IndexedGetExpression, GroupingExpression, EscapedCharCodeLiteralExpression, ArrayLiteralExpression, AALiteralExpression, UnaryExpression, VariableExpression, SourceLiteralExpression, NewExpression, CallfuncExpression, TemplateStringQuasiExpression, TemplateStringExpression, TaggedTemplateStringExpression, AnnotationExpression, FunctionParameterExpression, AAMemberExpression, TypecastExpression, TypeExpression, TypedArrayExpression, TernaryExpression, NullCoalescingExpression } from '../parser/Expression'; import type { BrsFile } from '../files/BrsFile'; import type { XmlFile } from '../files/XmlFile'; -import type { BsDiagnostic, TypedefProvider } from '../interfaces'; +import type { AnnotationDeclaration, BsDiagnostic, TypedefProvider } from '../interfaces'; import type { InvalidType } from '../types/InvalidType'; import type { VoidType } from '../types/VoidType'; import { InternalWalkMode } from './visitors'; @@ -462,3 +462,10 @@ export function isLiteralDouble(value: any): value is LiteralExpression & { type export function isBsDiagnostic(value: any): value is BsDiagnostic { return value.message; } + + +// Plugins + +export function isAnnotationDeclaration(value: any): value is AnnotationDeclaration { + return isTypedFunctionType(value.type); +} diff --git a/src/interfaces.ts b/src/interfaces.ts index cb63ee18b..65e8f77f3 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -219,6 +219,11 @@ export interface CommentFlag { export type CompilerPluginFactory = () => CompilerPlugin; +export interface AnnotationDeclaration { + description?: string; + type: TypedFunctionType; +} + export interface CompilerPlugin { name: string; @@ -229,7 +234,7 @@ export interface CompilerPlugin { * `suite(suiteConfig as object)` * ] */ - annotations?: string[]; + annotations?: Array; /** * Called when plugin is initially loaded @@ -1000,6 +1005,10 @@ export interface ExtraSymbolData { * Is this type as defined in a doc comment? */ isFromDocComment?: boolean; + /** + * Name of plugin that defined this symbol + */ + pluginName?: string; } export interface GetTypeOptions { From 0123053c7327fc119d57b510dc2af6799722622e Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Wed, 8 Jan 2025 10:31:10 -0400 Subject: [PATCH 3/3] Unknown annotations are flagged as 'cannot-find-name' --- .../validation/BrsFileValidator.spec.ts | 30 ++++++++- src/bscPlugin/validation/BrsFileValidator.ts | 66 ++++++++++++++----- 2 files changed, 77 insertions(+), 19 deletions(-) diff --git a/src/bscPlugin/validation/BrsFileValidator.spec.ts b/src/bscPlugin/validation/BrsFileValidator.spec.ts index 7e5c9cfcc..4772c90b7 100644 --- a/src/bscPlugin/validation/BrsFileValidator.spec.ts +++ b/src/bscPlugin/validation/BrsFileValidator.spec.ts @@ -14,7 +14,7 @@ import { FloatType } from '../../types/FloatType'; import { IntegerType } from '../../types/IntegerType'; import { InterfaceType } from '../../types/InterfaceType'; import { StringType } from '../../types/StringType'; -import { DynamicType, TypedFunctionType } from '../../types'; +import { DynamicType, TypedFunctionType, VoidType } from '../../types'; import { ParseMode } from '../../parser/Parser'; import type { ExtraSymbolData } from '../../interfaces'; @@ -1332,4 +1332,32 @@ describe('BrsFileValidator', () => { expectDiagnostics(program, []); }); }); + + describe('annotations', () => { + it('validates when unknown annotation is used', () => { + program.setFile('source/main.bs', ` + @unknownAnnotation + sub someFunc() + print "hello" + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.cannotFindName('unknownAnnotation') + ]); + }); + + it('allows known annotations', () => { + program.pluginAnnotationTable.addSymbol('knownAnnotation', { pluginName: 'Test' }, new TypedFunctionType(VoidType.instance).setName('knownAnnotation'), SymbolTypeFlag.annotation); + + program.setFile('source/main.bs', ` + @knownAnnotation + sub someFunc() + print "hello" + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + }); }); diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts index 1f050c7dc..7d272c9a5 100644 --- a/src/bscPlugin/validation/BrsFileValidator.ts +++ b/src/bscPlugin/validation/BrsFileValidator.ts @@ -1,11 +1,12 @@ -import { isAliasStatement, isArrayType, isBlock, isBody, isClassStatement, isConditionalCompileConstStatement, isConditionalCompileErrorStatement, isConditionalCompileStatement, isConstStatement, isDottedGetExpression, isDottedSetStatement, isEnumStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isImportStatement, isIndexedGetExpression, isIndexedSetStatement, isInterfaceStatement, isInvalidType, isLibraryStatement, isLiteralExpression, isMethodStatement, isNamespaceStatement, isTypecastExpression, isTypecastStatement, isUnaryExpression, isVariableExpression, isVoidType, isWhileStatement } from '../../astUtils/reflection'; +import { isAliasStatement, isArrayType, isBlock, isBody, isClassStatement, isConditionalCompileConstStatement, isConditionalCompileErrorStatement, isConditionalCompileStatement, isConstStatement, isDottedGetExpression, isDottedSetStatement, isEnumStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isImportStatement, isIndexedGetExpression, isIndexedSetStatement, isInterfaceStatement, isInvalidType, isLibraryStatement, isLiteralExpression, isMethodStatement, isNamespaceStatement, isStatement, isTypecastExpression, isTypecastStatement, isUnaryExpression, isVariableExpression, isVoidType, isWhileStatement } from '../../astUtils/reflection'; import { createVisitor, WalkMode } from '../../astUtils/visitors'; import { DiagnosticMessages } from '../../DiagnosticMessages'; import type { BrsFile } from '../../files/BrsFile'; import type { ExtraSymbolData, OnFileValidateEvent } from '../../interfaces'; import { TokenKind } from '../../lexer/TokenKind'; import type { AstNode, Expression, Statement } from '../../parser/AstNode'; -import { CallExpression, type FunctionExpression, type LiteralExpression } from '../../parser/Expression'; +import type { FunctionExpression, LiteralExpression } from '../../parser/Expression'; +import { CallExpression } from '../../parser/Expression'; import { ParseMode } from '../../parser/Parser'; import type { ContinueStatement, EnumMemberStatement, EnumStatement, ForEachStatement, ForStatement, ImportStatement, LibraryStatement, Body, WhileStatement, TypecastStatement, Block, AliasStatement } from '../../parser/Statement'; import { SymbolTypeFlag } from '../../SymbolTypeFlag'; @@ -277,23 +278,10 @@ export class BrsFileValidator { }, 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); + if (isStatement(node)) { + this.validateAnnotations(node); } + this.handleDocTags(node); } }); @@ -304,6 +292,27 @@ export class BrsFileValidator { }); } + + private handleDocTags(node: AstNode) { + //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); + } + } + private processDocTagsInFunction(doc: BrightScriptDoc, node: AstNode, funcExpr: FunctionExpression) { //TODO: Handle doc tags that influence the function they're in @@ -663,4 +672,25 @@ export class BrsFileValidator { } } } + + private validateAnnotations(statement: Statement) { + if (!statement.annotations || statement.annotations.length < 1) { + return; + } + + const symbolTable = this.event.program.pluginAnnotationTable; + const extraData: ExtraSymbolData = {}; + + for (const annotation of statement.annotations) { + const annotationSymbol = symbolTable.getSymbolType(annotation.name, { flags: SymbolTypeFlag.annotation, data: extraData }); + + if (!annotationSymbol) { + this.event.program.diagnostics.register({ + ...DiagnosticMessages.cannotFindName(annotation.name), + location: brsDocParser.getTypeLocationFromToken(annotation.tokens.name) ?? annotation.location + }); + } + } + } + }