From 65422b2baa6e244a23a2df4b5efce29134c8a075 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Mon, 2 May 2022 09:01:58 -0400 Subject: [PATCH] Allow interfaces and enums as function param types (#580) --- src/Scope.ts | 101 +++++++++++++++++++++++++++++++++++++- src/files/BrsFile.spec.ts | 40 +++++++++++++++ src/parser/Statement.ts | 13 +++++ 3 files changed, 152 insertions(+), 2 deletions(-) diff --git a/src/Scope.ts b/src/Scope.ts index 132a4dc95..c00820507 100644 --- a/src/Scope.ts +++ b/src/Scope.ts @@ -7,7 +7,7 @@ import { DiagnosticMessages } from './DiagnosticMessages'; import type { CallableContainer, BsDiagnostic, FileReference, BscFile, CallableContainerMap, FileLink } from './interfaces'; import type { Program } from './Program'; import { BsClassValidator } from './validators/ClassValidator'; -import type { NamespaceStatement, Statement, FunctionStatement, ClassStatement, EnumStatement } from './parser/Statement'; +import type { NamespaceStatement, Statement, FunctionStatement, ClassStatement, EnumStatement, InterfaceStatement } from './parser/Statement'; import type { NewExpression } from './parser/Expression'; import { ParseMode } from './parser/Parser'; import { standardizePath as s, util } from './util'; @@ -63,6 +63,24 @@ export class Scope { return this.getClassFileLink(className, containingNamespace)?.item; } + /** + * Get the interface with the specified name. + * @param ifaceName - The interface name, including the namespace of the interface if possible + * @param containingNamespace - The namespace used to resolve relative interface names. (i.e. the namespace around the current statement trying to find a interface) + */ + public getInterface(ifaceName: string, containingNamespace?: string): InterfaceStatement { + return this.getInterfaceFileLink(ifaceName, containingNamespace)?.item; + } + + /** + * Get the enum with the specified name. + * @param enumName - The enum name, including the namespace if possible + * @param containingNamespace - The namespace used to resolve relative enum names. (i.e. the namespace around the current statement trying to find an enum) + */ + public getEnum(enumName: string, containingNamespace?: string): EnumStatement { + return this.getEnumFileLink(enumName, containingNamespace)?.item; + } + /** * Get a class and its containing file by the class name * @param className - The class name, including the namespace of the class if possible @@ -82,6 +100,45 @@ export class Scope { return cls; } + + /** + * Get an interface and its containing file by the interface name + * @param ifaceName - The interface name, including the namespace of the interface if possible + * @param containingNamespace - The namespace used to resolve relative interface names. (i.e. the namespace around the current statement trying to find a interface) + */ + public getInterfaceFileLink(ifaceName: string, containingNamespace?: string): FileLink { + const lowerName = ifaceName?.toLowerCase(); + const ifaceMap = this.getInterfaceMap(); + + let iface = ifaceMap.get( + util.getFullyQualifiedClassName(lowerName, containingNamespace?.toLowerCase()) + ); + //if we couldn't find the iface by its full namespaced name, look for a global class with that name + if (!iface) { + iface = ifaceMap.get(lowerName); + } + return iface; + } + + /** + * Get an Enum and its containing file by the Enum name + * @param enumName - The Enum name, including the namespace of the enum if possible + * @param containingNamespace - The namespace used to resolve relative enum names. (i.e. the namespace around the current statement trying to find a enum) + */ + public getEnumFileLink(enumName: string, containingNamespace?: string): FileLink { + const lowerName = enumName?.toLowerCase(); + const enumMap = this.getEnumMap(); + + let enumeration = enumMap.get( + util.getFullyQualifiedClassName(lowerName, containingNamespace?.toLowerCase()) + ); + //if we couldn't find the enum by its full namespaced name, look for a global enum with that name + if (!enumeration) { + enumeration = enumMap.get(lowerName); + } + return enumeration; + } + /** * Tests if a class exists with the specified name * @param className - the all-lower-case namespace-included class name @@ -91,6 +148,24 @@ export class Scope { return !!this.getClass(className, namespaceName); } + /** + * Tests if an interface exists with the specified name + * @param ifaceName - the all-lower-case namespace-included interface name + * @param namespaceName - the current namespace name + */ + public hasInterface(ifaceName: string, namespaceName?: string): boolean { + return !!this.getInterface(ifaceName, namespaceName); + } + + /** + * Tests if an enum exists with the specified name + * @param enumName - the all-lower-case namespace-included enum name + * @param namespaceName - the current namespace name + */ + public hasEnum(enumName: string, namespaceName?: string): boolean { + return !!this.getEnum(enumName, namespaceName); + } + /** * A dictionary of all classes in this scope. This includes namespaced classes always with their full name. * The key is stored in lower case @@ -113,6 +188,28 @@ export class Scope { }); } + /** + * A dictionary of all Interfaces in this scope. This includes namespaced Interfaces always with their full name. + * The key is stored in lower case + */ + public getInterfaceMap(): Map> { + return this.cache.getOrAdd('interfaceMap', () => { + const map = new Map>(); + this.enumerateBrsFiles((file) => { + if (isBrsFile(file)) { + for (let iface of file.parser.references.interfaceStatements) { + const lowerIfaceName = iface.getName(ParseMode.BrighterScript)?.toLowerCase(); + //only track classes with a defined name (i.e. exclude nameless malformed classes) + if (lowerIfaceName) { + map.set(lowerIfaceName, { item: iface, file: file }); + } + } + } + }); + return map; + }); + } + /** * A dictionary of all enums in this scope. This includes namespaced enums always with their full name. * The key is stored in lower case @@ -603,7 +700,7 @@ export class Scope { if (isCustomType(param.type) && param.typeToken) { const paramTypeName = param.type.name; const currentNamespaceName = func.namespaceName?.getName(ParseMode.BrighterScript); - if (!this.hasClass(paramTypeName, currentNamespaceName)) { + if (!this.hasClass(paramTypeName, currentNamespaceName) && !this.hasInterface(paramTypeName) && !this.hasEnum(paramTypeName)) { this.diagnostics.push({ ...DiagnosticMessages.functionParameterTypeIsInvalid(param.name.text, paramTypeName), range: param.typeToken.range, diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts index f9b4c6c42..03208264b 100644 --- a/src/files/BrsFile.spec.ts +++ b/src/files/BrsFile.spec.ts @@ -549,6 +549,46 @@ describe('BrsFile', () => { }); describe('parse', () => { + it('allows class as parameter type', () => { + program.setFile(`source/main.bs`, ` + class Person + name as string + end class + + sub PrintPerson(p as Person) + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('allows interface as parameter type', () => { + program.setFile(`source/main.bs`, ` + interface Person + name as string + end interface + + sub PrintPerson(p as Person) + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('allows enum as parameter type', () => { + program.setFile(`source/main.bs`, ` + enum Direction + up + down + end enum + + sub PrintDirection(d as Direction) + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + it('supports iife in assignment', () => { program.setFile('source/main.brs', ` sub main() diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts index 606e06ad8..2829e860b 100644 --- a/src/parser/Statement.ts +++ b/src/parser/Statement.ts @@ -1276,6 +1276,19 @@ export class InterfaceStatement extends Statement implements TypedefProvider { return this.tokens.name?.text; } + /** + * Get the name of this expression based on the parse mode + */ + public getName(parseMode: ParseMode) { + if (this.namespaceName) { + let delimiter = parseMode === ParseMode.BrighterScript ? '.' : '_'; + let namespaceName = this.namespaceName.getName(parseMode); + return namespaceName + delimiter + this.name; + } else { + return this.name; + } + } + public transpile(state: BrsTranspileState): TranspileResult { //interfaces should completely disappear at runtime return [];