From 2e27a0185591d1fa0041faf37f97ab6e54c9506f Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Wed, 1 Jan 2025 18:43:26 -0400 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 + }); + } + } + } + } From 8bdad801e4fbeb3638bd0c34b5dc527d78065fb0 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Fri, 10 Jan 2025 09:13:13 -0400 Subject: [PATCH 04/11] Adds argument count check for annotations --- .../validation/BrsFileValidator.spec.ts | 62 +++++++++++++++++++ src/bscPlugin/validation/BrsFileValidator.ts | 19 +++++- src/bscPlugin/validation/ScopeValidator.ts | 20 +----- src/types/TypedFunctionType.ts | 27 +++++++- 4 files changed, 106 insertions(+), 22 deletions(-) diff --git a/src/bscPlugin/validation/BrsFileValidator.spec.ts b/src/bscPlugin/validation/BrsFileValidator.spec.ts index 4772c90b7..7a45e1cf5 100644 --- a/src/bscPlugin/validation/BrsFileValidator.spec.ts +++ b/src/bscPlugin/validation/BrsFileValidator.spec.ts @@ -1359,5 +1359,67 @@ describe('BrsFileValidator', () => { program.validate(); expectZeroDiagnostics(program); }); + + it('checks annotation function arg count', () => { + program.pluginAnnotationTable.addSymbol('takesOneOrTwo', { pluginName: 'Test' }, + new TypedFunctionType(VoidType.instance) + .setName('takesOneOrTwo') + .addParameter('x', DynamicType.instance) + .addParameter('y', DynamicType.instance, true), + SymbolTypeFlag.annotation); + + program.setFile('source/main.bs', ` + @takesOneOrTwo + sub someFunc() + print "hello" + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.mismatchArgumentCount('1-2', 0) + ]); + }); + + it('allows valid arg counts', () => { + program.pluginAnnotationTable.addSymbol('takesOneOrTwo', { pluginName: 'Test' }, + new TypedFunctionType(VoidType.instance) + .setName('takesOneOrTwo') + .addParameter('x', DynamicType.instance) + .addParameter('y', DynamicType.instance, true), + SymbolTypeFlag.annotation); + + program.setFile('source/main.bs', ` + @takesOneOrTwo(1) + sub someFunc() + end sub + + @takesOneOrTwo(1, "test") + sub otherFunc() + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('allows valid arg counts for variadic functions', () => { + program.pluginAnnotationTable.addSymbol('takesOneOrMore', { pluginName: 'Test' }, + new TypedFunctionType(VoidType.instance) + .setName('takesOneOrMore') + .addParameter('x', DynamicType.instance) + .seVariadic(true), + SymbolTypeFlag.annotation); + + program.setFile('source/main.bs', ` + @takesOneOrMore(1) + sub someFunc() + end sub + + @takesOneOrMore(1, "test", {test: 1}, [1,2,3], "more", "args") + sub otherFunc() + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); }); }); diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts index 7d272c9a5..eac2540c7 100644 --- a/src/bscPlugin/validation/BrsFileValidator.ts +++ b/src/bscPlugin/validation/BrsFileValidator.ts @@ -1,4 +1,4 @@ -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 { 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, isTypedFunctionType, isUnaryExpression, isVariableExpression, isVoidType, isWhileStatement } from '../../astUtils/reflection'; import { createVisitor, WalkMode } from '../../astUtils/visitors'; import { DiagnosticMessages } from '../../DiagnosticMessages'; import type { BrsFile } from '../../files/BrsFile'; @@ -682,14 +682,27 @@ export class BrsFileValidator { const extraData: ExtraSymbolData = {}; for (const annotation of statement.annotations) { - const annotationSymbol = symbolTable.getSymbolType(annotation.name, { flags: SymbolTypeFlag.annotation, data: extraData }); + const annotationType = symbolTable.getSymbolType(annotation.name, { flags: SymbolTypeFlag.annotation, data: extraData }); - if (!annotationSymbol) { + if (!annotationType || !annotationType?.isResolvable() || !isTypedFunctionType(annotationType)) { this.event.program.diagnostics.register({ ...DiagnosticMessages.cannotFindName(annotation.name), location: brsDocParser.getTypeLocationFromToken(annotation.tokens.name) ?? annotation.location }); + continue; + } + const { minParams, maxParams } = annotationType.getMinMaxParamCount(); + let expCallArgCount = annotation.call?.args.length ?? 0; + if (expCallArgCount > maxParams || expCallArgCount < minParams) { + let minMaxParamsText = minParams === maxParams ? maxParams : `${minParams}-${maxParams}`; + this.event.program.diagnostics.register({ + ...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount), + location: annotation.location + }); } + + const argTypes = annotation.call?.args.map(arg => arg.getType({ flags: SymbolTypeFlag.runtime })) ?? []; + } } diff --git a/src/bscPlugin/validation/ScopeValidator.ts b/src/bscPlugin/validation/ScopeValidator.ts index b89ec6ba4..12069ff52 100644 --- a/src/bscPlugin/validation/ScopeValidator.ts +++ b/src/bscPlugin/validation/ScopeValidator.ts @@ -13,8 +13,7 @@ import type { Token } from '../../lexer/Token'; import { AstNodeKind } from '../../parser/AstNode'; import type { AstNode } from '../../parser/AstNode'; import type { Expression } from '../../parser/AstNode'; -import type { VariableExpression, DottedGetExpression, BinaryExpression, UnaryExpression, NewExpression, LiteralExpression, FunctionExpression } from '../../parser/Expression'; -import { CallExpression } from '../../parser/Expression'; +import type { VariableExpression, DottedGetExpression, BinaryExpression, UnaryExpression, NewExpression, LiteralExpression, FunctionExpression, CallExpression } from '../../parser/Expression'; import { createVisitor, WalkMode } from '../../astUtils/visitors'; import type { BscType } from '../../types/BscType'; import type { BscFile } from '../../files/BscFile'; @@ -352,23 +351,8 @@ export class ScopeValidator { funcType = funcType.getMemberType('new', getTypeOptions); } if (funcType?.isResolvable() && isTypedFunctionType(funcType)) { - //funcType.setName(expression.callee. .name); - //get min/max parameter count for callable - let minParams = 0; - let maxParams = 0; - for (let param of funcType.params) { - maxParams++; - //optional parameters must come last, so we can assume that minParams won't increase once we hit - //the first isOptional - if (param.isOptional !== true) { - minParams++; - } - } - if (funcType.isVariadic) { - // function accepts variable number of arguments - maxParams = CallExpression.MaximumArguments; - } + const { minParams, maxParams } = funcType.getMinMaxParamCount(); let expCallArgCount = expression.args.length; if (expCallArgCount > maxParams || expCallArgCount < minParams) { let minMaxParamsText = minParams === maxParams ? maxParams : `${minParams}-${maxParams}`; diff --git a/src/types/TypedFunctionType.ts b/src/types/TypedFunctionType.ts index 1061e041f..c303598ac 100644 --- a/src/types/TypedFunctionType.ts +++ b/src/types/TypedFunctionType.ts @@ -5,6 +5,7 @@ import { BscTypeKind } from './BscTypeKind'; import { isUnionTypeCompatible } from './helpers'; import { BuiltInInterfaceAdder } from './BuiltInInterfaceAdder'; import type { TypeCompatibilityData } from '../interfaces'; +import { CallExpression } from '../parser/Expression'; export class TypedFunctionType extends BaseFunctionType { constructor( @@ -37,7 +38,7 @@ export class TypedFunctionType extends BaseFunctionType { return this; } - public addParameter(name: string, type: BscType, isOptional: boolean) { + public addParameter(name: string, type: BscType, isOptional = false) { this.params.push({ name: name, type: type, @@ -46,6 +47,11 @@ export class TypedFunctionType extends BaseFunctionType { return this; } + public seVariadic(variadic: boolean) { + this.isVariadic = variadic; + return this; + } + public isTypeCompatible(targetType: BscType, data: TypeCompatibilityData = {}) { if ( isDynamicType(targetType) || @@ -116,6 +122,25 @@ export class TypedFunctionType extends BaseFunctionType { //made it here, all params and return type pass predicate return true; } + + public getMinMaxParamCount(): { minParams: number; maxParams: number } { + //get min/max parameter count for callable + let minParams = 0; + let maxParams = 0; + for (let param of this.params) { + maxParams++; + //optional parameters must come last, so we can assume that minParams won't increase once we hit + //the first isOptional + if (param.isOptional !== true) { + minParams++; + } + } + if (this.isVariadic) { + // function accepts variable number of arguments + maxParams = CallExpression.MaximumArguments; + } + return { minParams: minParams, maxParams: maxParams }; + } } BuiltInInterfaceAdder.typedFunctionFactory = (returnType: BscType) => { From 6659efc16f6a4b2675662fd1d8e4a629b22c6748 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Sat, 11 Jan 2025 16:30:29 -0400 Subject: [PATCH 05/11] Using new diagnostic messages --- src/DiagnosticMessages.ts | 13 ++ .../validation/BrsFileValidator.spec.ts | 126 +++++++++++++++++- src/bscPlugin/validation/BrsFileValidator.ts | 48 ++++++- src/bscPlugin/validation/ScopeValidator.ts | 2 +- src/interfaces.ts | 1 + src/parser/Expression.ts | 2 +- 6 files changed, 183 insertions(+), 9 deletions(-) diff --git a/src/DiagnosticMessages.ts b/src/DiagnosticMessages.ts index 097d9e44d..5ccb11e12 100644 --- a/src/DiagnosticMessages.ts +++ b/src/DiagnosticMessages.ts @@ -1031,6 +1031,19 @@ export let DiagnosticMessages = { legacyCode: 1151, severity: DiagnosticSeverity.Error, code: 'return-type-coercion-mismatch' + }), + cannotFindAnnotation: (name: string) => ({ + message: `Cannot find annotation '${name}' `, + data: { + name: name + }, + severity: DiagnosticSeverity.Error, + code: 'cannot-find-annotation' + }), + expectedLiteralValue: (context: string, value: string) => ({ + message: `Expected literal value ${context}, but found '${value}' `, + severity: DiagnosticSeverity.Error, + code: 'expected-literal-value' }) }; export const defaultMaximumTruncationLength = 160; diff --git a/src/bscPlugin/validation/BrsFileValidator.spec.ts b/src/bscPlugin/validation/BrsFileValidator.spec.ts index 7a45e1cf5..ea54aa96f 100644 --- a/src/bscPlugin/validation/BrsFileValidator.spec.ts +++ b/src/bscPlugin/validation/BrsFileValidator.spec.ts @@ -17,6 +17,7 @@ import { StringType } from '../../types/StringType'; import { DynamicType, TypedFunctionType, VoidType } from '../../types'; import { ParseMode } from '../../parser/Parser'; import type { ExtraSymbolData } from '../../interfaces'; +import { AssociativeArrayType } from '../../types/AssociativeArrayType'; describe('BrsFileValidator', () => { let program: Program; @@ -1333,7 +1334,7 @@ describe('BrsFileValidator', () => { }); }); - describe('annotations', () => { + describe.only('annotations', () => { it('validates when unknown annotation is used', () => { program.setFile('source/main.bs', ` @unknownAnnotation @@ -1343,7 +1344,7 @@ describe('BrsFileValidator', () => { `); program.validate(); expectDiagnostics(program, [ - DiagnosticMessages.cannotFindName('unknownAnnotation') + DiagnosticMessages.cannotFindAnnotation('unknownAnnotation') ]); }); @@ -1421,5 +1422,126 @@ describe('BrsFileValidator', () => { program.validate(); expectZeroDiagnostics(program); }); + + it('ignores when multiple annotations of same name are added', () => { + program.pluginAnnotationTable.addSymbol('usedTwice', { pluginName: 'Test' }, + new TypedFunctionType(VoidType.instance) + .setName('usedTwice') + .addParameter('x', IntegerType.instance), + SymbolTypeFlag.annotation); + + program.pluginAnnotationTable.addSymbol('usedTwice', { pluginName: 'Other' }, + new TypedFunctionType(VoidType.instance) + .setName('usedTwice'), + SymbolTypeFlag.annotation); + + program.setFile('source/main.bs', ` + @usedTwice(1) + sub someFunc() + end sub + + @usedTwice + sub otherFunc() + end sub + + @usedTwice("TODO: this shouldn't be accepted", "Because there is no matching annotation") + sub yetAnotherFunc() + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('validates arg types', () => { + program.pluginAnnotationTable.addSymbol('annoStringInt', { pluginName: 'Test' }, + new TypedFunctionType(VoidType.instance) + .setName('annoStringInt') + .addParameter('str', StringType.instance) + .addParameter('int', IntegerType.instance), + SymbolTypeFlag.annotation); + + program.setFile('source/main.bs', ` + @annoStringInt(3.14, "test") + sub someFunc() + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.argumentTypeMismatch('float', 'string').message, + DiagnosticMessages.argumentTypeMismatch('string', 'integer').message + ]); + }); + + + it('allows valid arg types', () => { + program.pluginAnnotationTable.addSymbol('annoStringInt', { pluginName: 'Test' }, + new TypedFunctionType(VoidType.instance) + .setName('annoStringInt') + .addParameter('str', StringType.instance) + .addParameter('int', IntegerType.instance), + SymbolTypeFlag.annotation); + + program.setFile('source/main.bs', ` + @annoStringInt("test", 123) + sub someFunc() + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('validates uninitialized values', () => { + program.pluginAnnotationTable.addSymbol('annoString', { pluginName: 'Test' }, + new TypedFunctionType(VoidType.instance) + .setName('annoString') + .addParameter('str', StringType.instance), + SymbolTypeFlag.annotation); + + program.setFile('source/main.bs', ` + @annoString(someVar) + sub someFunc() + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.expectedLiteralValue('in annotation argument', 'someVar').message + ]); + }); + + it('validates uninitialized values', () => { + program.pluginAnnotationTable.addSymbol('annoString', { pluginName: 'Test' }, + new TypedFunctionType(VoidType.instance) + .setName('annoString') + .addParameter('str', StringType.instance), + SymbolTypeFlag.annotation); + + program.setFile('source/main.bs', ` + + const myConst = "hello" + @annoString(myConst) + sub someFunc() + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.expectedLiteralValue('in annotation argument', 'myConst').message + ]); + }); + + it('allows associative arrays', () => { + program.pluginAnnotationTable.addSymbol('annoAA', { pluginName: 'Test' }, + new TypedFunctionType(VoidType.instance) + .setName('annoAA') + .addParameter('aa', new AssociativeArrayType()), + SymbolTypeFlag.annotation); + + program.setFile('source/main.bs', ` + @annoAA({name: "John Doe", age: 25, ids: [1, 2, 3, 4]}) + sub someFunc() + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); }); }); diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts index eac2540c7..81f6193a5 100644 --- a/src/bscPlugin/validation/BrsFileValidator.ts +++ b/src/bscPlugin/validation/BrsFileValidator.ts @@ -1,8 +1,8 @@ -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, isTypedFunctionType, isUnaryExpression, isVariableExpression, isVoidType, isWhileStatement } from '../../astUtils/reflection'; +import { isAliasStatement, isArrayType, isBlock, isBody, isCallableType, isClassStatement, isClassType, isConditionalCompileConstStatement, isConditionalCompileErrorStatement, isConditionalCompileStatement, isConstStatement, isDottedGetExpression, isDottedSetStatement, isEnumStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isImportStatement, isIndexedGetExpression, isIndexedSetStatement, isInterfaceStatement, isInvalidType, isLibraryStatement, isLiteralExpression, isMethodStatement, isNamespaceStatement, isStatement, isTypecastExpression, isTypecastStatement, isTypedFunctionType, 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 type { ExtraSymbolData, OnFileValidateEvent, TypeCompatibilityData } from '../../interfaces'; import { TokenKind } from '../../lexer/TokenKind'; import type { AstNode, Expression, Statement } from '../../parser/AstNode'; import type { FunctionExpression, LiteralExpression } from '../../parser/Expression'; @@ -18,6 +18,7 @@ import type { Range } from 'vscode-languageserver'; import type { Token } from '../../lexer/Token'; import type { BrightScriptDoc } from '../../parser/BrightScriptDocParser'; import brsDocParser from '../../parser/BrightScriptDocParser'; +import { UninitializedType } from '../../types'; export class BrsFileValidator { constructor( @@ -684,13 +685,17 @@ export class BrsFileValidator { for (const annotation of statement.annotations) { const annotationType = symbolTable.getSymbolType(annotation.name, { flags: SymbolTypeFlag.annotation, data: extraData }); - if (!annotationType || !annotationType?.isResolvable() || !isTypedFunctionType(annotationType)) { + if (!annotationType || !annotationType?.isResolvable()) { this.event.program.diagnostics.register({ - ...DiagnosticMessages.cannotFindName(annotation.name), + ...DiagnosticMessages.cannotFindAnnotation(annotation.name), location: brsDocParser.getTypeLocationFromToken(annotation.tokens.name) ?? annotation.location }); continue; } + if (!isTypedFunctionType(annotationType)) { + // TODO: handle multiple function definitions - in that case this would be a UnionType + continue; + } const { minParams, maxParams } = annotationType.getMinMaxParamCount(); let expCallArgCount = annotation.call?.args.length ?? 0; if (expCallArgCount > maxParams || expCallArgCount < minParams) { @@ -701,8 +706,41 @@ export class BrsFileValidator { }); } - const argTypes = annotation.call?.args.map(arg => arg.getType({ flags: SymbolTypeFlag.runtime })) ?? []; + // validate the arg types - very similar to code in ScopeValidator + let paramIndex = 0; + for (let arg of annotation.call?.args ?? []) { + const data = {} as ExtraSymbolData; + let argType = arg.getType({ flags: SymbolTypeFlag.runtime, data: data, onlyAllowLiterals: true }); + + if (!argType || !argType.isResolvable()) { + this.event.program.diagnostics.register({ + ...DiagnosticMessages.expectedLiteralValue('in annotation argument', util.getAllDottedGetPartsAsString(arg)), + location: arg.location + }); + break; + } + let paramType = annotationType.params[paramIndex]?.type; + if (!paramType) { + // unable to find a paramType -- maybe there are more args than params + break; + } + if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) { + argType = data.definingNode?.getConstructorType(); + } + + const compatibilityData: TypeCompatibilityData = {}; + if (!argType || !argType.isResolvable() || !paramType?.isTypeCompatible(argType, compatibilityData)) { + + const argTypeStr = argType?.toString() ?? UninitializedType.instance.toString(); + + this.event.program.diagnostics.register({ + ...DiagnosticMessages.argumentTypeMismatch(argTypeStr, paramType.toString(), compatibilityData), + location: arg.location + }); + } + paramIndex++; + } } } diff --git a/src/bscPlugin/validation/ScopeValidator.ts b/src/bscPlugin/validation/ScopeValidator.ts index 12069ff52..ebe2c8b38 100644 --- a/src/bscPlugin/validation/ScopeValidator.ts +++ b/src/bscPlugin/validation/ScopeValidator.ts @@ -373,7 +373,7 @@ export class ScopeValidator { } if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) { - argType = data.definingNode.getConstructorType(); + argType = data.definingNode?.getConstructorType(); } const compatibilityData: TypeCompatibilityData = {}; diff --git a/src/interfaces.ts b/src/interfaces.ts index 207a96b00..0bd8e9898 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1024,6 +1024,7 @@ export interface GetTypeOptions { ignoreCacheForRetrieval?: boolean; isExistenceTest?: boolean; preferDocType?: boolean; + onlyAllowLiterals?: boolean; } export class TypeChainEntry { diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts index 4a89559a5..8191874da 100644 --- a/src/parser/Expression.ts +++ b/src/parser/Expression.ts @@ -1441,7 +1441,7 @@ export class VariableExpression extends Expression { getType(options: GetTypeOptions) { let resultType: BscType = util.tokenToBscType(this.tokens.name); const nameKey = this.getName(); - if (!resultType) { + if (!resultType || !options?.onlyAllowLiterals) { const symbolTable = this.getSymbolTable(); resultType = symbolTable?.getSymbolType(nameKey, { ...options, fullName: nameKey, tableProvider: () => this.getSymbolTable() }); From 7116e13b975b8e8990f81c029e7284abf7a36c1e Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Mon, 13 Jan 2025 10:16:46 -0400 Subject: [PATCH 06/11] fixed tests --- src/bscPlugin/validation/BrsFileValidator.spec.ts | 2 +- src/files/BrsFile.spec.ts | 12 +++++++++++- src/parser/Expression.ts | 2 +- .../tests/statement/InterfaceStatement.spec.ts | 6 ++++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/bscPlugin/validation/BrsFileValidator.spec.ts b/src/bscPlugin/validation/BrsFileValidator.spec.ts index ea54aa96f..7a876cb6a 100644 --- a/src/bscPlugin/validation/BrsFileValidator.spec.ts +++ b/src/bscPlugin/validation/BrsFileValidator.spec.ts @@ -1334,7 +1334,7 @@ describe('BrsFileValidator', () => { }); }); - describe.only('annotations', () => { + describe('annotations', () => { it('validates when unknown annotation is used', () => { program.setFile('source/main.bs', ` @unknownAnnotation diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts index 7a72616b1..699580534 100644 --- a/src/files/BrsFile.spec.ts +++ b/src/files/BrsFile.spec.ts @@ -21,7 +21,7 @@ import * as fsExtra from 'fs-extra'; import undent from 'undent'; import { tempDir, rootDir } from '../testHelpers.spec'; import { SymbolTypeFlag } from '../SymbolTypeFlag'; -import { ClassType, EnumType, FloatType, InterfaceType } from '../types'; +import { ClassType, EnumType, FloatType, InterfaceType, VoidType } from '../types'; import type { StandardizedFileEntry } from 'roku-deploy'; import * as fileUrl from 'file-url'; import { isAALiteralExpression, isBlock } from '../astUtils/reflection'; @@ -1830,6 +1830,7 @@ describe('BrsFile', () => { }); it('works for bs content', () => { + program.pluginAnnotationTable.addSymbol('annotation', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); program.setFile('source/lib.bs', ``); doTest(` import "pkg:/source/lib.bs" @@ -2985,6 +2986,7 @@ describe('BrsFile', () => { }); it('includes annotation comments for class', async () => { + program.pluginAnnotationTable.addSymbol('annotation', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); await testTranspile(` 'comment1 @annotation @@ -3012,6 +3014,7 @@ describe('BrsFile', () => { }); it('includes annotation comments for function', async () => { + program.pluginAnnotationTable.addSymbol('annotation', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); await testTranspile(` 'comment1 @annotation @@ -3030,6 +3033,7 @@ describe('BrsFile', () => { }); it('includes annotation comments for enum', async () => { + program.pluginAnnotationTable.addSymbol('annotation', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); await testTranspile(` 'comment1 @annotation @@ -3047,6 +3051,7 @@ describe('BrsFile', () => { }); it('includes annotation comments for const', async () => { + program.pluginAnnotationTable.addSymbol('annotation', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); await testTranspile(` 'comment1 @annotation @@ -3062,6 +3067,7 @@ describe('BrsFile', () => { }); it('includes annotation comments for empty namespaces', async () => { + program.pluginAnnotationTable.addSymbol('annotation', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); await testTranspile(` 'comment1 @annotation @@ -4374,6 +4380,10 @@ describe('BrsFile', () => { }); it('includes annotations', () => { + program.pluginAnnotationTable.addSymbol('an', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); + program.pluginAnnotationTable.addSymbol('anfunc', {}, new TypedFunctionType(VoidType.instance).addParameter('name', StringType.instance, true), SymbolTypeFlag.annotation); + program.pluginAnnotationTable.addSymbol('anmember', {}, new TypedFunctionType(VoidType.instance).addParameter('name', StringType.instance, true), SymbolTypeFlag.annotation); + testTypedef(` namespace test @an diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts index 8191874da..aec115d9f 100644 --- a/src/parser/Expression.ts +++ b/src/parser/Expression.ts @@ -1441,7 +1441,7 @@ export class VariableExpression extends Expression { getType(options: GetTypeOptions) { let resultType: BscType = util.tokenToBscType(this.tokens.name); const nameKey = this.getName(); - if (!resultType || !options?.onlyAllowLiterals) { + if (!resultType && !options?.onlyAllowLiterals) { const symbolTable = this.getSymbolTable(); resultType = symbolTable?.getSymbolType(nameKey, { ...options, fullName: nameKey, tableProvider: () => this.getSymbolTable() }); diff --git a/src/parser/tests/statement/InterfaceStatement.spec.ts b/src/parser/tests/statement/InterfaceStatement.spec.ts index d50ea62c1..3556d81db 100644 --- a/src/parser/tests/statement/InterfaceStatement.spec.ts +++ b/src/parser/tests/statement/InterfaceStatement.spec.ts @@ -1,6 +1,9 @@ import { expectZeroDiagnostics, getTestGetTypedef, getTestTranspile } from '../../../testHelpers.spec'; import { rootDir } from '../../../testHelpers.spec'; import { Program } from '../../../Program'; +import { TypedFunctionType } from '../../../types/TypedFunctionType'; +import { VoidType } from '../../../types/VoidType'; +import { SymbolTypeFlag } from '../../../SymbolTypeFlag'; describe('InterfaceStatement', () => { let program: Program; @@ -50,6 +53,9 @@ describe('InterfaceStatement', () => { }); it('includes annotations', async () => { + program.pluginAnnotationTable.addSymbol('IFace', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); + program.pluginAnnotationTable.addSymbol('Method', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); + program.pluginAnnotationTable.addSymbol('Field', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); await testGetTypedef(` @IFace interface Person From 38702985d428100ca61f2d8817ae14852f2c368d Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Mon, 13 Jan 2025 10:40:19 -0400 Subject: [PATCH 07/11] Refactored Program to add addAnnotationSymbol --- src/Program.ts | 12 +++- .../validation/BrsFileValidator.spec.ts | 56 +++++++------------ src/files/BrsFile.spec.ts | 18 +++--- .../statement/InterfaceStatement.spec.ts | 7 +-- 4 files changed, 43 insertions(+), 50 deletions(-) diff --git a/src/Program.ts b/src/Program.ts index 816758d5a..e86cd2a26 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -55,6 +55,7 @@ import { ProgramValidatorDiagnosticsTag } from './bscPlugin/validation/ProgramVa import type { ProvidedSymbolInfo, BrsFile } from './files/BrsFile'; import type { XmlFile } from './files/XmlFile'; import { SymbolTable } from './SymbolTable'; +import type { TypedFunctionType } from './types/TypedFunctionType'; const bslibNonAliasedRokuModulesPkgPath = s`source/roku_modules/rokucommunity_bslib/bslib.brs`; const bslibAliasedRokuModulesPkgPath = s`source/roku_modules/bslib/bslib.brs`; @@ -238,11 +239,11 @@ export class Program { 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); + this.addAnnotationSymbol(annotation.name, annotation, { pluginName: pluginName }); } 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); + this.addAnnotationSymbol(annoType.name, annoType, { pluginName: pluginName, description: description }); } else if (typeof annotation === 'string') { // TODO: Do we need to parse this? } @@ -250,6 +251,13 @@ export class Program { } } + public addAnnotationSymbol(name: string, annoType: TypedFunctionType, extraData: ExtraSymbolData = {}) { + if (name && annoType) { + annoType.setName(name); + this.pluginAnnotationTable.addSymbol(name, extraData, annoType, SymbolTypeFlag.annotation); + } + } + private fileSymbolInformation = new Map(); public addFileSymbolInfo(file: BrsFile) { diff --git a/src/bscPlugin/validation/BrsFileValidator.spec.ts b/src/bscPlugin/validation/BrsFileValidator.spec.ts index 7a876cb6a..af31be580 100644 --- a/src/bscPlugin/validation/BrsFileValidator.spec.ts +++ b/src/bscPlugin/validation/BrsFileValidator.spec.ts @@ -1349,7 +1349,7 @@ describe('BrsFileValidator', () => { }); it('allows known annotations', () => { - program.pluginAnnotationTable.addSymbol('knownAnnotation', { pluginName: 'Test' }, new TypedFunctionType(VoidType.instance).setName('knownAnnotation'), SymbolTypeFlag.annotation); + program.addAnnotationSymbol('knownAnnotation', new TypedFunctionType(VoidType.instance)); program.setFile('source/main.bs', ` @knownAnnotation @@ -1362,12 +1362,12 @@ describe('BrsFileValidator', () => { }); it('checks annotation function arg count', () => { - program.pluginAnnotationTable.addSymbol('takesOneOrTwo', { pluginName: 'Test' }, + program.addAnnotationSymbol('takesOneOrTwo', new TypedFunctionType(VoidType.instance) .setName('takesOneOrTwo') .addParameter('x', DynamicType.instance) .addParameter('y', DynamicType.instance, true), - SymbolTypeFlag.annotation); + { pluginName: 'Test' }); program.setFile('source/main.bs', ` @takesOneOrTwo @@ -1382,12 +1382,11 @@ describe('BrsFileValidator', () => { }); it('allows valid arg counts', () => { - program.pluginAnnotationTable.addSymbol('takesOneOrTwo', { pluginName: 'Test' }, + program.addAnnotationSymbol('takesOneOrTwo', new TypedFunctionType(VoidType.instance) .setName('takesOneOrTwo') .addParameter('x', DynamicType.instance) - .addParameter('y', DynamicType.instance, true), - SymbolTypeFlag.annotation); + .addParameter('y', DynamicType.instance, true)); program.setFile('source/main.bs', ` @takesOneOrTwo(1) @@ -1403,12 +1402,11 @@ describe('BrsFileValidator', () => { }); it('allows valid arg counts for variadic functions', () => { - program.pluginAnnotationTable.addSymbol('takesOneOrMore', { pluginName: 'Test' }, + program.addAnnotationSymbol('takesOneOrMore', new TypedFunctionType(VoidType.instance) .setName('takesOneOrMore') .addParameter('x', DynamicType.instance) - .seVariadic(true), - SymbolTypeFlag.annotation); + .seVariadic(true)); program.setFile('source/main.bs', ` @takesOneOrMore(1) @@ -1424,16 +1422,14 @@ describe('BrsFileValidator', () => { }); it('ignores when multiple annotations of same name are added', () => { - program.pluginAnnotationTable.addSymbol('usedTwice', { pluginName: 'Test' }, + program.addAnnotationSymbol('usedTwice', new TypedFunctionType(VoidType.instance) .setName('usedTwice') - .addParameter('x', IntegerType.instance), - SymbolTypeFlag.annotation); + .addParameter('x', IntegerType.instance)); - program.pluginAnnotationTable.addSymbol('usedTwice', { pluginName: 'Other' }, + program.addAnnotationSymbol('usedTwice', new TypedFunctionType(VoidType.instance) - .setName('usedTwice'), - SymbolTypeFlag.annotation); + .setName('usedTwice')); program.setFile('source/main.bs', ` @usedTwice(1) @@ -1453,12 +1449,10 @@ describe('BrsFileValidator', () => { }); it('validates arg types', () => { - program.pluginAnnotationTable.addSymbol('annoStringInt', { pluginName: 'Test' }, + program.addAnnotationSymbol('annoStringInt', new TypedFunctionType(VoidType.instance) - .setName('annoStringInt') .addParameter('str', StringType.instance) - .addParameter('int', IntegerType.instance), - SymbolTypeFlag.annotation); + .addParameter('int', IntegerType.instance)); program.setFile('source/main.bs', ` @annoStringInt(3.14, "test") @@ -1474,12 +1468,10 @@ describe('BrsFileValidator', () => { it('allows valid arg types', () => { - program.pluginAnnotationTable.addSymbol('annoStringInt', { pluginName: 'Test' }, + program.addAnnotationSymbol('annoStringInt', new TypedFunctionType(VoidType.instance) - .setName('annoStringInt') .addParameter('str', StringType.instance) - .addParameter('int', IntegerType.instance), - SymbolTypeFlag.annotation); + .addParameter('int', IntegerType.instance)); program.setFile('source/main.bs', ` @annoStringInt("test", 123) @@ -1491,11 +1483,9 @@ describe('BrsFileValidator', () => { }); it('validates uninitialized values', () => { - program.pluginAnnotationTable.addSymbol('annoString', { pluginName: 'Test' }, + program.addAnnotationSymbol('annoString', new TypedFunctionType(VoidType.instance) - .setName('annoString') - .addParameter('str', StringType.instance), - SymbolTypeFlag.annotation); + .addParameter('str', StringType.instance)); program.setFile('source/main.bs', ` @annoString(someVar) @@ -1509,11 +1499,9 @@ describe('BrsFileValidator', () => { }); it('validates uninitialized values', () => { - program.pluginAnnotationTable.addSymbol('annoString', { pluginName: 'Test' }, + program.addAnnotationSymbol('annoString', new TypedFunctionType(VoidType.instance) - .setName('annoString') - .addParameter('str', StringType.instance), - SymbolTypeFlag.annotation); + .addParameter('str', StringType.instance)); program.setFile('source/main.bs', ` @@ -1529,11 +1517,9 @@ describe('BrsFileValidator', () => { }); it('allows associative arrays', () => { - program.pluginAnnotationTable.addSymbol('annoAA', { pluginName: 'Test' }, + program.addAnnotationSymbol('annoAA', new TypedFunctionType(VoidType.instance) - .setName('annoAA') - .addParameter('aa', new AssociativeArrayType()), - SymbolTypeFlag.annotation); + .addParameter('aa', new AssociativeArrayType())); program.setFile('source/main.bs', ` @annoAA({name: "John Doe", age: 25, ids: [1, 2, 3, 4]}) diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts index 699580534..a1436b2a8 100644 --- a/src/files/BrsFile.spec.ts +++ b/src/files/BrsFile.spec.ts @@ -1830,7 +1830,7 @@ describe('BrsFile', () => { }); it('works for bs content', () => { - program.pluginAnnotationTable.addSymbol('annotation', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); + program.addAnnotationSymbol('annotation', new TypedFunctionType(VoidType.instance)); program.setFile('source/lib.bs', ``); doTest(` import "pkg:/source/lib.bs" @@ -2986,7 +2986,7 @@ describe('BrsFile', () => { }); it('includes annotation comments for class', async () => { - program.pluginAnnotationTable.addSymbol('annotation', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); + program.addAnnotationSymbol('annotation', new TypedFunctionType(VoidType.instance)); await testTranspile(` 'comment1 @annotation @@ -3014,7 +3014,7 @@ describe('BrsFile', () => { }); it('includes annotation comments for function', async () => { - program.pluginAnnotationTable.addSymbol('annotation', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); + program.addAnnotationSymbol('annotation', new TypedFunctionType(VoidType.instance)); await testTranspile(` 'comment1 @annotation @@ -3033,7 +3033,7 @@ describe('BrsFile', () => { }); it('includes annotation comments for enum', async () => { - program.pluginAnnotationTable.addSymbol('annotation', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); + program.addAnnotationSymbol('annotation', new TypedFunctionType(VoidType.instance)); await testTranspile(` 'comment1 @annotation @@ -3051,7 +3051,7 @@ describe('BrsFile', () => { }); it('includes annotation comments for const', async () => { - program.pluginAnnotationTable.addSymbol('annotation', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); + program.addAnnotationSymbol('annotation', new TypedFunctionType(VoidType.instance)); await testTranspile(` 'comment1 @annotation @@ -3067,7 +3067,7 @@ describe('BrsFile', () => { }); it('includes annotation comments for empty namespaces', async () => { - program.pluginAnnotationTable.addSymbol('annotation', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); + program.addAnnotationSymbol('annotation', new TypedFunctionType(VoidType.instance)); await testTranspile(` 'comment1 @annotation @@ -4380,9 +4380,9 @@ describe('BrsFile', () => { }); it('includes annotations', () => { - program.pluginAnnotationTable.addSymbol('an', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); - program.pluginAnnotationTable.addSymbol('anfunc', {}, new TypedFunctionType(VoidType.instance).addParameter('name', StringType.instance, true), SymbolTypeFlag.annotation); - program.pluginAnnotationTable.addSymbol('anmember', {}, new TypedFunctionType(VoidType.instance).addParameter('name', StringType.instance, true), SymbolTypeFlag.annotation); + program.addAnnotationSymbol('an', new TypedFunctionType(VoidType.instance)); + program.addAnnotationSymbol('anfunc', new TypedFunctionType(VoidType.instance).addParameter('name', StringType.instance, true)); + program.addAnnotationSymbol('anmember', new TypedFunctionType(VoidType.instance).addParameter('name', StringType.instance, true)); testTypedef(` namespace test diff --git a/src/parser/tests/statement/InterfaceStatement.spec.ts b/src/parser/tests/statement/InterfaceStatement.spec.ts index 3556d81db..81f847902 100644 --- a/src/parser/tests/statement/InterfaceStatement.spec.ts +++ b/src/parser/tests/statement/InterfaceStatement.spec.ts @@ -3,7 +3,6 @@ import { rootDir } from '../../../testHelpers.spec'; import { Program } from '../../../Program'; import { TypedFunctionType } from '../../../types/TypedFunctionType'; import { VoidType } from '../../../types/VoidType'; -import { SymbolTypeFlag } from '../../../SymbolTypeFlag'; describe('InterfaceStatement', () => { let program: Program; @@ -53,9 +52,9 @@ describe('InterfaceStatement', () => { }); it('includes annotations', async () => { - program.pluginAnnotationTable.addSymbol('IFace', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); - program.pluginAnnotationTable.addSymbol('Method', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); - program.pluginAnnotationTable.addSymbol('Field', {}, new TypedFunctionType(VoidType.instance), SymbolTypeFlag.annotation); + program.addAnnotationSymbol('IFace', new TypedFunctionType(VoidType.instance)); + program.addAnnotationSymbol('Method', new TypedFunctionType(VoidType.instance)); + program.addAnnotationSymbol('Field', new TypedFunctionType(VoidType.instance)); await testGetTypedef(` @IFace interface Person From 6255e8be5eb7ca8b893d9e83bf24ce9a858f11ce Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Mon, 13 Jan 2025 11:29:18 -0400 Subject: [PATCH 08/11] Added log line and fixed exception on hover of annotation --- src/Program.ts | 2 ++ src/bscPlugin/hover/HoverProcessor.ts | 3 +++ src/util.ts | 3 +++ 3 files changed, 8 insertions(+) diff --git a/src/Program.ts b/src/Program.ts index e86cd2a26..cd949c372 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -254,6 +254,8 @@ export class Program { public addAnnotationSymbol(name: string, annoType: TypedFunctionType, extraData: ExtraSymbolData = {}) { if (name && annoType) { annoType.setName(name); + const pluginName = extraData?.pluginName ?? ''; + this.logger.info(`Adding annotation '${name}' (${pluginName})`); this.pluginAnnotationTable.addSymbol(name, extraData, annoType, SymbolTypeFlag.annotation); } } diff --git a/src/bscPlugin/hover/HoverProcessor.ts b/src/bscPlugin/hover/HoverProcessor.ts index 5285bdf7f..b64905816 100644 --- a/src/bscPlugin/hover/HoverProcessor.ts +++ b/src/bscPlugin/hover/HoverProcessor.ts @@ -133,6 +133,9 @@ export class HoverProcessor { return null; } const expression = file.getClosestExpression(this.event.position); + if (!expression) { + return null; + } const hoverContents: string[] = []; for (let scope of this.event.scopes) { try { diff --git a/src/util.ts b/src/util.ts index cbb15cfd7..720e07651 100644 --- a/src/util.ts +++ b/src/util.ts @@ -2310,6 +2310,9 @@ export class Util { public isInTypeExpression(expression: AstNode): boolean { + if (!expression) { + return false; + } //TODO: this is much faster than node.findAncestor(), but may need to be updated for "complicated" type expressions if (isTypeExpression(expression) || isTypeExpression(expression.parent) || From 61da8ca2267ae0b6bd69e956f2f06b5e7afb825d Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Mon, 20 Jan 2025 19:07:13 -0400 Subject: [PATCH 09/11] Update src/interfaces.ts Co-authored-by: Luis Jacobetty Soares <57358121+luis-j-soares@users.noreply.github.com> --- src/interfaces.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces.ts b/src/interfaces.ts index 0bd8e9898..ba72f92de 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -526,7 +526,7 @@ export interface OnGetCodeActionsEvent { codeActions: CodeAction[]; } -export interface onPluginConfigureEvent { +export interface OnPluginConfigureEvent { builder: ProgramBuilder; } export interface BeforeProgramCreateEvent { From 25a708b2b551b34f023f277deaad01caf8bc9a8e Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Tue, 21 Jan 2025 08:15:26 -0400 Subject: [PATCH 10/11] Merge conflict fixes, fixes setVariadic typo --- .../validation/BrsFileValidator.spec.ts | 2 +- src/bscPlugin/validation/ScopeValidator.ts | 62 +------------------ src/interfaces.ts | 2 +- src/types/TypedFunctionType.ts | 2 +- 4 files changed, 6 insertions(+), 62 deletions(-) diff --git a/src/bscPlugin/validation/BrsFileValidator.spec.ts b/src/bscPlugin/validation/BrsFileValidator.spec.ts index af31be580..302b8dd8f 100644 --- a/src/bscPlugin/validation/BrsFileValidator.spec.ts +++ b/src/bscPlugin/validation/BrsFileValidator.spec.ts @@ -1406,7 +1406,7 @@ describe('BrsFileValidator', () => { new TypedFunctionType(VoidType.instance) .setName('takesOneOrMore') .addParameter('x', DynamicType.instance) - .seVariadic(true)); + .setVariadic(true)); program.setFile('source/main.bs', ` @takesOneOrMore(1) diff --git a/src/bscPlugin/validation/ScopeValidator.ts b/src/bscPlugin/validation/ScopeValidator.ts index d3b47d763..673d8d0db 100644 --- a/src/bscPlugin/validation/ScopeValidator.ts +++ b/src/bscPlugin/validation/ScopeValidator.ts @@ -13,12 +13,7 @@ import type { Token } from '../../lexer/Token'; import { AstNodeKind } from '../../parser/AstNode'; import type { AstNode } from '../../parser/AstNode'; import type { Expression } from '../../parser/AstNode'; -<<<<<<< HEAD -import type { VariableExpression, DottedGetExpression, BinaryExpression, UnaryExpression, NewExpression, LiteralExpression, FunctionExpression, CallExpression } from '../../parser/Expression'; -======= -import type { VariableExpression, DottedGetExpression, BinaryExpression, UnaryExpression, NewExpression, LiteralExpression, FunctionExpression, CallfuncExpression } from '../../parser/Expression'; -import { CallExpression } from '../../parser/Expression'; ->>>>>>> release-1.0.0 +import type { VariableExpression, DottedGetExpression, BinaryExpression, UnaryExpression, NewExpression, LiteralExpression, FunctionExpression, CallExpression, CallfuncExpression } from '../../parser/Expression'; import { createVisitor, WalkMode } from '../../astUtils/visitors'; import type { BscType } from '../../types/BscType'; import type { BscFile } from '../../files/BscFile'; @@ -399,14 +394,6 @@ export class ScopeValidator { // We're calling a class - get the constructor funcType = funcType.getMemberType('new', getTypeOptions); } -<<<<<<< HEAD - if (funcType?.isResolvable() && isTypedFunctionType(funcType)) { - //get min/max parameter count for callable - const { minParams, maxParams } = funcType.getMinMaxParamCount(); - let expCallArgCount = expression.args.length; - if (expCallArgCount > maxParams || expCallArgCount < minParams) { - let minMaxParamsText = minParams === maxParams ? maxParams : `${minParams}-${maxParams}`; -======= const callErrorLocation = expression?.callee?.location; return this.validateFunctionCall(file, funcType, callErrorLocation, expression.args); @@ -453,20 +440,7 @@ export class ScopeValidator { } //get min/max parameter count for callable - let minParams = 0; - let maxParams = 0; - for (let param of funcType.params) { - maxParams++; - //optional parameters must come last, so we can assume that minParams won't increase once we hit - //the first isOptional - if (param.isOptional !== true) { - minParams++; - } - } - if (funcType.isVariadic) { - // function accepts variable number of arguments - maxParams = CallExpression.MaximumArguments; - } + const { minParams, maxParams } = funcType.getMinMaxParamCount(); const argsForCall = argOffset < 1 ? args : args.slice(argOffset); let expCallArgCount = argsForCall.length; @@ -489,48 +463,18 @@ export class ScopeValidator { } if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) { - argType = data.definingNode.getConstructorType(); + argType = data.definingNode?.getConstructorType(); } const compatibilityData: TypeCompatibilityData = {}; const isAllowedArgConversion = this.checkAllowedArgConversions(paramType, argType); if (!isAllowedArgConversion && !paramType?.isTypeCompatible(argType, compatibilityData)) { ->>>>>>> release-1.0.0 this.addMultiScopeDiagnostic({ ...DiagnosticMessages.argumentTypeMismatch(argType.toString(), paramType.toString(), compatibilityData), location: arg.location }); } -<<<<<<< HEAD - let paramIndex = 0; - for (let arg of expression.args) { - const data = {} as ExtraSymbolData; - let argType = this.getNodeTypeWrapper(file, arg, { flags: SymbolTypeFlag.runtime, data: data }); - - let paramType = funcType.params[paramIndex]?.type; - if (!paramType) { - // unable to find a paramType -- maybe there are more args than params - break; - } - - if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) { - argType = data.definingNode?.getConstructorType(); - } - - const compatibilityData: TypeCompatibilityData = {}; - const isAllowedArgConversion = this.checkAllowedArgConversions(paramType, argType); - if (!isAllowedArgConversion && !paramType?.isTypeCompatible(argType, compatibilityData)) { - this.addMultiScopeDiagnostic({ - ...DiagnosticMessages.argumentTypeMismatch(argType.toString(), paramType.toString(), compatibilityData), - location: arg.location - }); - } - paramIndex++; - } - -======= paramIndex++; ->>>>>>> release-1.0.0 } } diff --git a/src/interfaces.ts b/src/interfaces.ts index f476a3fe4..74c638065 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -239,7 +239,7 @@ export interface CompilerPlugin { /** * Called when plugin is initially loaded */ - onPluginConfigure?(event: onPluginConfigureEvent): any; + onPluginConfigure?(event: OnPluginConfigureEvent): any; /** * Called before a new program is created diff --git a/src/types/TypedFunctionType.ts b/src/types/TypedFunctionType.ts index 9203df55f..9bbc897db 100644 --- a/src/types/TypedFunctionType.ts +++ b/src/types/TypedFunctionType.ts @@ -47,7 +47,7 @@ export class TypedFunctionType extends BaseFunctionType { return this; } - public seVariadic(variadic: boolean) { + public setVariadic(variadic: boolean) { this.isVariadic = variadic; return this; } From edb5b7b1472b4191c9ea4763db11d96352961cbc Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Tue, 21 Jan 2025 08:42:00 -0400 Subject: [PATCH 11/11] Updated docs. Changed annotation declarations so they must be TypedFunctionType --- docs/annotations.md | 14 +++++++++++++- docs/plugins.md | 36 +++++++++++++++++++++++------------- src/interfaces.ts | 8 ++------ 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/docs/annotations.md b/docs/annotations.md index 8ae916254..831dd131b 100644 --- a/docs/annotations.md +++ b/docs/annotations.md @@ -11,7 +11,7 @@ A statement can have multiple annotations. The name of the annotation should be a valid identifier and can not be a keyword (e.g. `for`, `while`, `else`...). -Annotations can have parameters - these parameters should be a list of valid BrighterScript expressions separated by commas. +Annotations can have parameters - these parameters should be a list of valid BrighterScript literal expressions separated by commas. ``` @[(parameters)] @@ -21,6 +21,18 @@ Annotations can have parameters - these parameters should be a list of valid Bri @[(parameters)] [more annotations] ``` +## Annotation Arguments + +Annotations can only take literal values as arguments. That includes literal strings, numbers, arrays with literal values and associative arrays with literal values. + +Examples: + + - literal numbers: `123`, `3.14`, `0`, `&HFF` + - literal strings: `"hello"`, `""`, `"any string with quotes"` + - literal arrays: `[1, 2, 3]`, `["array", "of", "strings"]`, `[1, {letter: "A"}, "mixed"]` + - literal associative arrays: `{key: "value"}`, `{translation: [200, 300], fields: {title: "Star Wars", description: "A long time ago in a galaxy far, far away..."}}` + + ## Examples ```brighterscript diff --git a/docs/plugins.md b/docs/plugins.md index 084f65ed0..30976bb86 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -164,18 +164,14 @@ export interface CompilerPlugin { name: string; /** - * A list of brighterscript-style function declarations of allowed annotations - * Eg.: [ - * `inline()`, - * `suite(suiteConfig as object)` - * ] + * A list of function declarations of allowed annotations */ - annotations?: string[]; + annotations?: Array; /** * Called when plugin is initially loaded */ - onPluginConfigure?(event: onPluginConfigureEvent): any; + onPluginConfigure?(event: OnPluginConfigureEvent): any; /** * Called before a new program is created @@ -648,14 +644,20 @@ export default function 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. +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 instances of `TypedFunctionType`, or, if you want to include a description as well, use `AnnotationDeclaration`. For example: ```typescript this.annotations = [ - 'inline()', - 'log(prefix as string, addLineNumbers = false as boolean)' + new TypedFunctionType(VoidType.instance).setName('inline'), + { + description: 'Add a log message whenever this function is called', + type: new TypedFunctionType(VoidType.instance) + .setName('log') + .addParameter('prefix', StringType.instance) + .addParameter('addLineNumbers', BooleanType.instance, true) + } ]; ``` @@ -669,9 +671,17 @@ import { isBrsFile, createVisitor, WalkMode, BeforePrepareFileEvent, CompilerPlu export default function plugin() { return { name: 'addLogging', - annotations: [ - 'log(prefix as string, addLineNumbers = false as boolean)' - ], + annotations: [{ + description: ` + Add a log message whenever this function is called + @param {string} prefix Words that appear before the regular log message + @param {boolean} [addLineNumbers=false] optional param to include line numbers + ` + type: new TypedFunctionType(VoidType.instance) + .setName('log') + .addParameter('prefix', StringType.instance) + .addParameter('addLineNumbers', BooleanType.instance, true) + }], beforePrepareFile: (event: BeforePrepareFileEvent) => { if (isBrsFile(event.file)) { event.file.ast.walk(createVisitor({ diff --git a/src/interfaces.ts b/src/interfaces.ts index 74c638065..c8e8c04f5 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -228,13 +228,9 @@ export interface CompilerPlugin { name: string; /** - * A list of brighterscript-style function declarations of allowed annotations - * Eg.: [ - * `inline()`, - * `suite(suiteConfig as object)` - * ] + * A list of function declarations of allowed annotations */ - annotations?: Array; + annotations?: Array; /** * Called when plugin is initially loaded