diff --git a/src/prefixer/File.ts b/src/prefixer/File.ts index c519189..aadac0a 100644 --- a/src/prefixer/File.ts +++ b/src/prefixer/File.ts @@ -6,8 +6,8 @@ import { buildAst } from '@xml-tools/ast'; import type { RopmOptions } from '../util'; import { util } from '../util'; import * as path from 'path'; -import type { BrsFile, Position, Program, XmlFile } from 'brighterscript'; -import { createVisitor, isCallExpression, isDottedGetExpression, isDottedSetStatement, isIndexedGetExpression, isIndexedSetStatement, WalkMode } from 'brighterscript'; +import type { BrsFile, Position, Program, Range, XmlFile } from 'brighterscript'; +import { ParseMode, createVisitor, isCallExpression, isCustomType, isDottedGetExpression, isDottedSetStatement, isIndexedGetExpression, isIndexedSetStatement, WalkMode, util as bsUtil } from 'brighterscript'; export class File { constructor( @@ -94,6 +94,15 @@ export class File { endOffset: number; }>; + /** + * Anywhere that a class is used as a type (like in class `extends` or function parameters) + */ + public classReferences = [] as Array<{ + fullyQualifiedName: string; + offsetBegin: number; + offsetEnd: number; + }>; + public functionReferences = [] as Array<{ name: string; offset: number; @@ -231,12 +240,31 @@ export class File { } } + private addClassRef(className: string, containingNamespace: string | undefined, range: Range) { + //look up the class. If we can find it, use it + const cls = (this.bscFile as BrsFile).getClassFileLink(className, containingNamespace)?.item; + + let fullyQualifiedName: string; + if (cls) { + fullyQualifiedName = bsUtil.getFullyQualifiedClassName(cls.getName(ParseMode.BrighterScript), cls.namespaceName?.getName(ParseMode.BrighterScript)); + } else { + fullyQualifiedName = bsUtil.getFullyQualifiedClassName(className, containingNamespace); + } + + this.classReferences.push({ + fullyQualifiedName: fullyQualifiedName, + offsetBegin: this.positionToOffset(range.start), + offsetEnd: this.positionToOffset(range.end) + }); + } + /** * find various items from this file. */ public walkAst() { + const file = this.bscFile as BrsFile; /* eslint-disable @typescript-eslint/naming-convention */ - (this.bscFile as BrsFile).parser.ast.walk(createVisitor({ + file.parser.ast.walk(createVisitor({ ImportStatement: (stmt) => { //skip pkg paths, those are collected elsewhere if (!stmt.filePath.startsWith('pkg:/')) { @@ -283,6 +311,34 @@ export class File { ), endOffset: this.positionToOffset(cls.end.range.end) }); + + if (cls.parentClassName) { + this.addClassRef( + cls.parentClassName.getName(ParseMode.BrighterScript), + cls.namespaceName?.getName(ParseMode.BrighterScript), + cls.parentClassName.range + ); + } + }, + FunctionExpression: (func) => { + const namespaceName = func.namespaceName?.getName(ParseMode.BrighterScript); + //any parameters containing custom types + for (const param of func.parameters) { + if (isCustomType(param.type)) { + this.addClassRef( + param.type.name, + namespaceName, + param.typeToken!.range + ); + } + } + if (isCustomType(func.returnType)) { + this.addClassRef( + func.returnType.name, + namespaceName, + func.returnTypeToken!.range + ); + } }, FunctionStatement: (func) => { this.functionDefinitions.push({ diff --git a/src/prefixer/ModuleManager.spec.ts b/src/prefixer/ModuleManager.spec.ts index aa75ac8..1c42833 100644 --- a/src/prefixer/ModuleManager.spec.ts +++ b/src/prefixer/ModuleManager.spec.ts @@ -1272,6 +1272,165 @@ describe('ModuleManager', () => { }); }); + it('prefixes classes used in parameters', async () => { + await testProcess({ + 'logger:source/lib.d.bs': [ + trim` + namespace animals + class Dog + sub new(brother as Dog, sister as animals.Dog, owner as Human) + end sub + end class + end namespace + + class Human + end class + `, + trim` + namespace logger.animals + class Dog + sub new(brother as logger.animals.Dog, sister as logger.animals.Dog, owner as logger.Human) + end sub + end class + end namespace + + namespace logger + class Human + end class + end namespace + ` + ] + }); + }); + + it('prefixes classes used in extends', async () => { + await testProcess({ + 'logger:source/lib.d.bs': [ + trim` + namespace animals + class Animal + end class + class Dog extends Animal + end class + class Cat extends animals.Animal + end class + class Warewolf extends Human + end class + end namespace + class Human + end class + class Warecat extends animals.Cat + end class + `, + trim` + namespace logger.animals + class Animal + end class + class Dog extends logger.animals.Animal + end class + class Cat extends logger.animals.Animal + end class + class Warewolf extends logger.Human + end class + end namespace + namespace logger + class Human + end class + end namespace + namespace logger + class Warecat extends logger.animals.Cat + end class + end namespace + ` + ] + }); + }); + + it('prefixes classes used as return type', async () => { + await testProcess({ + 'logger:source/lib.d.bs': [ + trim` + namespace animals + class Dog + end class + function GetDog1() as Dog + end function + function GetDog2() as animals.Dog + end function + function GetHuman() as Human + end function + end namespace + + function GetDog3() as animals.Dog + end function + + class Human + end class + `, + trim` + namespace logger.animals + class Dog + end class + function GetDog1() as logger.animals.Dog + end function + function GetDog2() as logger.animals.Dog + end function + function GetHuman() as logger.Human + end function + end namespace + + namespace logger + function GetDog3() as logger.animals.Dog + end function + end namespace + + namespace logger + class Human + end class + end namespace + ` + ] + }); + }); + + it('does not prefix other module namespaced class names', async () => { + manager.modules = createProjects(hostDir, hostDir, { + name: 'host', + dependencies: [{ + name: 'logger', + _files: { + 'source/lib.d.bs': trim` + class Person + sub new(pet as a.Duck) + end sub + sub watchPetForFriend(friendPet as dogs.Poodle) + end sub + end class + ` + }, + dependencies: [{ + name: 'animals', + alias: 'a' + }, { + name: 'dogs' + }] + }] + }); + + await process(); + + fsEqual(`${hostDir}/source/roku_modules/logger/lib.d.bs`, ` + namespace logger + class Person + sub new(pet as animals_v1.Duck) + end sub + sub watchPetForFriend(friendPet as dogs_v1.Poodle) + end sub + end class + end namespace + `); + }); + it('properly handles annotations', async () => { await testProcess({ 'logger:source/lib.d.bs': [ diff --git a/src/prefixer/RopmModule.ts b/src/prefixer/RopmModule.ts index 7f3fb8f..bf8ead6 100644 --- a/src/prefixer/RopmModule.ts +++ b/src/prefixer/RopmModule.ts @@ -311,6 +311,21 @@ export class RopmModule { const ownComponentNames = this.getDistinctComponentDeclarationNames(); const prefixMapKeys = Object.keys(this.prefixMap); const prefixMapKeysLower = prefixMapKeys.map(x => x.toLowerCase()); + + /** + * Get the alias for a namespace. Only returns if it exists and is different than what is given. + */ + const getAlias = (namespace?: string) => { + if (namespace) { + const lowerNamespaceName = namespace.toLowerCase(); + const idx = prefixMapKeysLower.indexOf(lowerNamespaceName); + const prefix = this.prefixMap[prefixMapKeys[idx]]; + if (prefix && prefix.toLowerCase() !== lowerNamespaceName) { + return prefix; + } + } + }; + const nonPrefixedFunctionMap = { ...this.nonPrefixedFunctionMap, ...this.getInterfaceFunctions() @@ -362,6 +377,22 @@ export class RopmModule { } } + //prefix d.bs class references + for (const ref of file.classReferences) { + const baseNamespace = util.getBaseNamespace(ref.fullyQualifiedName); + + const alias = getAlias(baseNamespace); + let fullyQualifiedName: string; + //if we have an alias, this is a class from another module. + if (alias) { + fullyQualifiedName = ref.fullyQualifiedName.replace(/^.*?\./, alias + '.'); + } else { + //this is an internal-module class, so append our prefix to it + fullyQualifiedName = `${brighterscriptPrefix}.${ref.fullyQualifiedName}`; + } + file.addEdit(ref.offsetBegin, ref.offsetEnd, fullyQualifiedName); + } + //prefix d.bs namespaces for (const namespace of file.namespaces) { file.addEdit(namespace.offset, namespace.offset, brighterscriptPrefix + '.'); diff --git a/src/util.ts b/src/util.ts index 5ad76c0..129ac1d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -283,6 +283,16 @@ export class Util { } } + /** + * Get the base namespace from a namespace statement, or undefined if there are no dots + */ + public getBaseNamespace(text: string) { + const parts = text.split('.'); + if (parts.length > 1) { + return parts[0]; + } + } + } function mockProgramValidate() {