diff --git a/src/bscPlugin/references/ReferencesProcessor.spec.ts b/src/bscPlugin/references/ReferencesProcessor.spec.ts index e3ad0b0e3..235570548 100644 --- a/src/bscPlugin/references/ReferencesProcessor.spec.ts +++ b/src/bscPlugin/references/ReferencesProcessor.spec.ts @@ -3,9 +3,10 @@ import { Program } from '../../Program'; import { util } from '../../util'; import { createSandbox } from 'sinon'; import { rootDir } from '../../testHelpers.spec'; +// import { Reference } from '../..'; let sinon = createSandbox(); -describe('ReferencesProcessor', () => { +describe.only('ReferencesProcessor', () => { let program: Program; beforeEach(() => { program = new Program({ rootDir: rootDir, sourceMap: true }); @@ -15,25 +16,219 @@ describe('ReferencesProcessor', () => { program.dispose(); }); - it('provides references', () => { - const file = program.setFile('source/main.bs', ` + describe('local variables', () => { + it('provides references for local variable', () => { + const file = program.setFile('source/main.bs', ` sub main() hello = 1 print hello end sub `); - const references = program.getReferences({ - srcPath: file.srcPath, - position: util.createPosition(3, 25) + const references = program.getReferences({ + srcPath: file.srcPath, + position: util.createPosition(3, 25) + }); + expect( + references + ).to.eql([{ + srcPath: file.srcPath, + range: util.createRange(2, 16, 2, 21) + }, { + srcPath: file.srcPath, + range: util.createRange(3, 22, 3, 27) + }]); + }); + it('provides multiple references for local variable', () => { + const file = program.setFile('source/main.bs', ` + sub main() + hello = 1 + print hello + speech = hello + " world" + print left(hello, 20) + end sub + `); + const references = program.getReferences({ + srcPath: file.srcPath, + position: util.createPosition(3, 25) + }); + expect( + references + ).to.eql([ + { + srcPath: file.srcPath, + range: util.createRange(2, 16, 2, 21) + }, + { + srcPath: file.srcPath, + range: util.createRange(3, 22, 3, 27) + }, + { + srcPath: file.srcPath, + range: util.createRange(4, 25, 4, 30) + }, + { + srcPath: file.srcPath, + range: util.createRange(5, 27, 5, 32) + } + ]); + }); + }); + describe('globally scoped functions', () => { + it('provides reference from function definition', () => { + const file = program.setFile('source/main.bs', ` + function getName() + return "John Doe" + end function + + sub main() + print getName() + len(getName()) + myFunc = getName + return { + getName: getName + getName: getName() + } + end sub + + `); + const references = program.getReferences({ + srcPath: file.srcPath, + position: util.createPosition(1, 25) + }); + expect( + references + ).to.eql([ + { + srcPath: file.srcPath, + range: util.createRange(6, 22, 6, 29) + }, + { + srcPath: file.srcPath, + range: util.createRange(7, 20, 7, 27) + }, + { + srcPath: file.srcPath, + range: util.createRange(8, 25, 8, 32) + }, + { + srcPath: file.srcPath, + range: util.createRange(10, 29, 10, 36) + }, + { + srcPath: file.srcPath, + range: util.createRange(11, 29, 11, 36) + } + ] + + ); + }); + it.only('provides reference from function call', () => { + const file = program.setFile('source/main.bs', ` + function getName() + return "John Doe" + end function + + sub main() + print getName() + len(getName()) + myFunc = getName + return { + getName: getName + getName: getName() + } + end sub + + `); + let references = program.getReferences({ + srcPath: file.srcPath, + position: util.createPosition(6, 25) + }); + expect( + references + ).to.eql([ + { + srcPath: file.srcPath, + range: util.createRange(6, 22, 6, 29) + }, + { + srcPath: file.srcPath, + range: util.createRange(7, 20, 7, 27) + }, + { + srcPath: file.srcPath, + range: util.createRange(8, 25, 8, 32) + }, + { + srcPath: file.srcPath, + range: util.createRange(10, 29, 10, 36) + }, + { + srcPath: file.srcPath, + range: util.createRange(11, 29, 11, 36) + } + ]); + + + references = program.getReferences({ + srcPath: file.srcPath, + position: util.createPosition(10, 30) + }); + expect( + references + ).to.eql([{ + srcPath: file.srcPath, + range: util.createRange(2, 16, 2, 21) + }, { + srcPath: file.srcPath, + range: util.createRange(3, 22, 3, 27) + }]); + }); + + it('provides multiple references for local variable', () => { + const file = program.setFile('source/main.bs', ` + sub main() + hello = 1 + print hello + speech = hello + " world" + print left(hello, 20) + end sub + `); + const references = program.getReferences({ + srcPath: file.srcPath, + position: util.createPosition(3, 25) + }); + expect( + references + ).to.eql([ + { + srcPath: file.srcPath, + range: util.createRange(6, 22, 6, 29) + }, + { + srcPath: file.srcPath, + range: util.createRange(7, 20, 7, 27) + }, + { + srcPath: file.srcPath, + range: util.createRange(8, 25, 8, 32) + }, + { + srcPath: file.srcPath, + range: util.createRange(10, 29, 10, 36) + }, + { + srcPath: file.srcPath, + range: util.createRange(11, 29, 11, 36) + } + ]); }); - expect( - references - ).to.eql([{ - srcPath: file.srcPath, - range: util.createRange(2, 16, 2, 21) - }, { - srcPath: file.srcPath, - range: util.createRange(3, 22, 3, 27) - }]); }); }); + + +// function dumpReferences(references: Reference[]) { +// return '[\n' + references.map((r) => `{ +// srcPath: file.srcPath, +// range: util.createRange(${r.range.start.line}, ${r.range.start.character}, ${r.range.end.line}, ${r.range.end.character}) +// }`).join(',\n') + '\n]\n'; +// } diff --git a/src/bscPlugin/references/ReferencesProcessor.ts b/src/bscPlugin/references/ReferencesProcessor.ts index b3f00483f..c58ebc275 100644 --- a/src/bscPlugin/references/ReferencesProcessor.ts +++ b/src/bscPlugin/references/ReferencesProcessor.ts @@ -1,7 +1,24 @@ -import { isBrsFile, isXmlFile } from '../../astUtils/reflection'; +import { util } from '../../util'; +import { isBrsFile, isCallExpression, isClassStatement, isDottedGetExpression, isFunctionExpression, isNamespaceStatement, isVariableExpression, isXmlFile } from '../../astUtils/reflection'; import { createVisitor, WalkMode } from '../../astUtils/visitors'; import type { BrsFile } from '../../files/BrsFile'; import type { ProvideReferencesEvent, Reference } from '../../interfaces'; +import { TokenKind } from '../../lexer/TokenKind'; +import { ParseMode } from '../../parser/Parser'; +import type { NamespaceStatement } from '../../parser/Statement'; +import type { Expression } from '../../parser/AstNode'; + +enum ReferenceType { + constant = 'constant', + call = 'call', + callFunc = 'callFunc', + enum = 'enum', + enumMember = 'enumMember', + unknown = 'unknown', + variable = 'variable', + class = 'class', + namespaceFunctionCall = 'namespaceFunctionCall' +} export class ReferencesProcessor { public constructor( @@ -11,19 +28,232 @@ export class ReferencesProcessor { } public process() { - if (isBrsFile(this.event.file)) { - this.event.references.push( - ...this.findVariableReferences(this.event.file) - ); + let file = this.event.file as BrsFile; + if (isBrsFile(file)) { + const callSiteToken = file.getTokenAt(this.event.position); + const searchFor = callSiteToken.text.toLowerCase(); + + let [referenceType, expression] = this.getReferenceType(this.event); + switch (referenceType) { + case ReferenceType.constant: + this.event.references.push( + ...this.findConstantReferences(file, searchFor, expression) + ); + break; + case ReferenceType.call: + this.event.references.push( + ...this.findCallReferences(file, searchFor, expression) + ); + break; + case ReferenceType.callFunc: + this.event.references.push( + ...this.findCallFuncReferences(file, searchFor, expression) + ); + break; + case ReferenceType.enum: + this.event.references.push( + ...this.findEnumReferences(file, searchFor, expression) + ); + break; + case ReferenceType.enumMember: + this.event.references.push( + ...this.findEnumReferences(file, searchFor, expression) + ); + break; + break; + case ReferenceType.variable: + this.event.references.push( + ...this.findVariableReferences(file, searchFor, expression) + ); + break; + case ReferenceType.class: + this.event.references.push( + ...this.findClassReferences(file, searchFor, expression) + ); + break; + default: + console.log('Unknown reference type'); + } + } + + } + private findEnumReferences(file: BrsFile, searchFor: string, expression?: Expression): Reference[] { + const fullName = util.getAllDottedGetParts(expression)?.map(x => x.text).join('.'); + return this.findDottedGetReferences(file, fullName, expression); + } + + private findConstantReferences(file: BrsFile, searchFor: string, expression?: Expression): Reference[] { + const fullName = util.getAllDottedGetParts(expression)?.map(x => x.text).join('.'); + return this.findDottedGetReferences(file, fullName, expression); + } + + private findCallFuncReferences(file: BrsFile, searchFor: string, expression?: Expression): Reference[] { + //TODO - can further limit this + return this.findIdentifierReferences(file, searchFor, expression); + } + + private findCallReferences(file: BrsFile, searchFor: string, expression?: Expression): Reference[] { + return [ + ...this.findIdentifierReferences(file, searchFor, expression) + ]; + } + + private findClassReferences(file: BrsFile, searchFor: string, expression?: Expression): Reference[] { + let references: Reference[] = []; + return references; + } + + private findIdentifierReferences(file: BrsFile, searchFor: string, expression?: Expression): Reference[] { + let references = [] as Reference[]; + + for (const scope of this.event.scopes) { + const processedFiles = new Set(); + for (const file of scope.getAllFiles()) { + if (isXmlFile(file) || processedFiles.has(file)) { + continue; + } + processedFiles.add(file); + file.ast.walk(createVisitor({ + VariableExpression: (e) => { + if (e.name.text.toLowerCase() === searchFor) { + references.push({ + srcPath: file.srcPath, + range: e.range + }); + } + }, + CallExpression: (e) => { + if ((e.callee as any)?.name?.text.toLowerCase() === searchFor) { + references.push({ + srcPath: file.srcPath, + range: e.range + }); + } + } + }), { + walkMode: WalkMode.visitAllRecursive + }); + } + } + + + return references; + } + + private findDottedGetReferences(file: BrsFile, searchFor: string, expression?: Expression): Reference[] { + let references = [] as Reference[]; + + for (const scope of this.event.scopes) { + const processedFiles = new Set(); + for (const file of scope.getAllFiles()) { + if (isXmlFile(file) || processedFiles.has(file)) { + continue; + } + processedFiles.add(file); + file.ast.walk(createVisitor({ + DottedGetExpression: (e) => { + const fullName = util.getAllDottedGetParts(expression)?.map(x => x.text).join('.'); + if (fullName === searchFor) { + references.push({ + srcPath: file.srcPath, + range: e.range + }); + } + } + }), { + walkMode: WalkMode.visitAllRecursive + }); + } + } + + + return references; + } + + private getReferenceType(event: ProvideReferencesEvent): [ReferenceType, Expression?] { + //get the token at the position + let file = event.file as BrsFile; + let position = event.position; + const token = file.getTokenAt(position); + + // While certain other tokens are allowed as local variables (AllowedLocalIdentifiers: https://github.com/rokucommunity/brighterscript/blob/master/src/lexer/TokenKind.ts#L418), these are converted by the parser to TokenKind.Identifier by the time we retrieve the token using getTokenAt + let definitionTokenTypes = [ + TokenKind.Identifier, + TokenKind.StringLiteral + ]; + + //throw out invalid tokens and the wrong kind of tokens + if (!token || !definitionTokenTypes.includes(token.kind)) { + return [ReferenceType.unknown, undefined]; + } + + const scopesForFile = file.program.getScopesForFile(file); + const [scope] = scopesForFile; + + const expression = file.getClosestExpression(position); + if (scope && expression) { + scope.linkSymbolTable(); + let containingNamespace = expression.findAncestor(isNamespaceStatement)?.getName(ParseMode.BrighterScript); + const fullName = util.getAllDottedGetParts(expression)?.map(x => x.text).join('.'); + + //find a constant with this name + const constant = scope?.getConstFileLink(fullName, containingNamespace); + if (constant) { + return [ReferenceType.constant, constant.item]; + } + if (isDottedGetExpression(expression)) { + + const enumLink = scope.getEnumFileLink(fullName, containingNamespace); + if (enumLink) { + return [ReferenceType.enum, enumLink.item]; + } + const enumMemberLink = scope.getEnumMemberFileLink(fullName, containingNamespace); + if (enumMemberLink) { + return [ReferenceType.enum, enumMemberLink.item]; + } + } else if (isCallExpression(expression)) { + // TODO CHECK THIS + return [ReferenceType.namespaceFunctionCall, expression]; + } + } + + const previousToken = file.getTokenAt({ line: token.range.start.line, character: token.range.start.character }); + + if (previousToken?.kind === TokenKind.Callfunc) { + return [ReferenceType.callFunc, expression]; + } + + let classToken = file.getTokenBefore(token, TokenKind.Class); + if (classToken) { + return [ReferenceType.class, expression]; + } + + if (token.kind === TokenKind.StringLiteral) { + return [ReferenceType.unknown, undefined]; + } + + if (isCallExpression(expression)) { + return [ReferenceType.call, expression]; + } + + if (isFunctionExpression(expression)) { + return [ReferenceType.call, expression]; + } + + if (isClassStatement(expression)) { + return [ReferenceType.class, expression]; + } + + if (isVariableExpression(expression)) { + return [ReferenceType.variable, expression]; } + return [ReferenceType.call, expression]; } - private findVariableReferences(file: BrsFile) { - const callSiteToken = file.getTokenAt(this.event.position); + private findVariableReferences(file: BrsFile, searchFor: string, expression?: Expression): Reference[] { - let locations = [] as Reference[]; + let references = [] as Reference[]; - const searchFor = callSiteToken.text.toLowerCase(); for (const scope of this.event.scopes) { const processedFiles = new Set(); @@ -35,7 +265,7 @@ export class ReferencesProcessor { file.ast.walk(createVisitor({ VariableExpression: (e) => { if (e.name.text.toLowerCase() === searchFor) { - locations.push({ + references.push({ srcPath: file.srcPath, range: e.range }); @@ -43,7 +273,7 @@ export class ReferencesProcessor { }, AssignmentStatement: (e) => { if (e.name.text.toLowerCase() === searchFor) { - locations.push({ + references.push({ srcPath: file.srcPath, range: e.name.range }); @@ -54,6 +284,6 @@ export class ReferencesProcessor { }); } } - return locations; + return references; } } diff --git a/src/files/BrsFile.ts b/src/files/BrsFile.ts index 12cc8e91d..8cc5a5373 100644 --- a/src/files/BrsFile.ts +++ b/src/files/BrsFile.ts @@ -1192,7 +1192,7 @@ export class BrsFile { } } - private getTokenBefore(currentToken: Token, tokenKind: TokenKind): Token { + getTokenBefore(currentToken: Token, tokenKind: TokenKind): Token { const index = this.parser.tokens.indexOf(currentToken); for (let i = index - 1; i >= 0; i--) { currentToken = this.parser.tokens[i];