• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / brighterscript / #15903

11 May 2026 06:41PM UTC coverage: 86.896% (-2.2%) from 89.094%
#15903

push

web-flow
Merge 70dfd6181 into ce68f5cb7

15597 of 18958 branches covered (82.27%)

Branch coverage included in aggregate %.

9 of 9 new or added lines in 3 files covered. (100.0%)

955 existing lines in 53 files now uncovered.

16351 of 17808 relevant lines covered (91.82%)

27326.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.91
/src/bscPlugin/definition/DefinitionProvider.ts
1
import { isBrsFile, isClassStatement, isDottedGetExpression, isImportStatement, isNamespaceStatement, isVariableExpression, isXmlFile, isXmlScope } from '../../astUtils/reflection';
1✔
2
import type { BrsFile } from '../../files/BrsFile';
3
import type { ProvideDefinitionEvent } from '../../interfaces';
4
import { TokenKind } from '../../lexer/TokenKind';
1✔
5
import type { Location } from 'vscode-languageserver-protocol';
6
import type { ClassStatement, FunctionStatement, NamespaceStatement } from '../../parser/Statement';
7
import { ParseMode } from '../../parser/Parser';
1✔
8
import util from '../../util';
1✔
9
import { WalkMode, createVisitor } from '../../astUtils/visitors';
1✔
10
import type { Token } from '../../lexer/Token';
11
import type { XmlFile } from '../../files/XmlFile';
12

13
export class DefinitionProvider {
1✔
14
    constructor(
15
        private event: ProvideDefinitionEvent
25✔
16
    ) { }
17

18
    public process(): Location[] {
19
        if (isBrsFile(this.event.file)) {
25✔
20
            this.brsFileGetDefinition(this.event.file);
23✔
21
        } else if (isXmlFile(this.event.file)) {
2✔
22
            this.xmlFileGetDefinition(this.event.file);
1✔
23
        }
24
        return this.event.definitions;
25✔
25
    }
26

27
    /**
28
     * For a position in a BrsFile, get the location where the token at that position was defined
29
     */
30
    private brsFileGetDefinition(file: BrsFile): void {
31
        //get the token at the position
32
        const token = file.getTokenAt(this.event.position);
23✔
33

34
        // 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
35
        let definitionTokenTypes = [
23✔
36
            TokenKind.Identifier,
37
            TokenKind.StringLiteral
38
        ];
39

40
        //throw out invalid tokens and the wrong kind of tokens
41
        if (!token || !definitionTokenTypes.includes(token.kind)) {
23✔
42
            return;
2✔
43
        }
44

45
        const scopesForFile = this.event.program.getScopesForFile(file);
21✔
46
        const [scope] = scopesForFile;
21✔
47

48
        const expression = file.getClosestExpression(this.event.position);
21✔
49
        if (scope && expression) {
21!
50
            scope.linkSymbolTable();
21✔
51
            let containingNamespace = expression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
21!
52
            const fullName = util.getAllDottedGetParts(expression)?.map(x => x.text).join('.');
27✔
53

54
            //find a constant with this name
55
            const constant = scope?.getConstFileLink(fullName, containingNamespace);
21!
56
            if (constant) {
21✔
57
                this.event.definitions.push(
1✔
58
                    constant.item.tokens.name?.location
3!
59
                );
60
                return;
1✔
61
            }
62
            if (isDottedGetExpression(expression) || isVariableExpression(expression)) {
20✔
63

64
                const enumLink = scope.getEnumFileLink(fullName, containingNamespace);
10✔
65
                if (enumLink) {
10✔
66
                    this.event.definitions.push(
1✔
67
                        enumLink.item.tokens.name.location
68
                    );
69
                    return;
1✔
70
                }
71
                const enumMemberLink = scope.getEnumMemberFileLink(fullName, containingNamespace);
9✔
72
                if (enumMemberLink) {
9✔
73
                    this.event.definitions.push(
1✔
74
                        enumMemberLink.item.tokens.name.location
75
                    );
76
                    return;
1✔
77
                }
78

79
                const interfaceFileLink = scope.getInterfaceFileLink(fullName, containingNamespace);
8✔
80
                if (interfaceFileLink) {
8✔
81
                    this.event.definitions.push(
2✔
82
                        interfaceFileLink.item.tokens.name.location
83
                    );
84
                    return;
2✔
85
                }
86

87
                const classFileLink = scope.getClassFileLink(fullName, containingNamespace);
6✔
88
                if (classFileLink) {
6✔
89
                    this.event.definitions.push(
2✔
90
                        classFileLink.item.tokens.name.location
91
                    );
92
                    return;
2✔
93
                }
94
            }
95
        }
96

97
        let textToSearchFor = token.text.toLowerCase();
14✔
98

99
        const previousToken = file.getTokenAt({ line: token.location?.range.start.line, character: token.location?.range.start.character });
14!
100

101
        if (previousToken?.kind === TokenKind.Callfunc) {
14!
102
            for (const scope of this.event.program.getScopes()) {
2✔
103
                //does this xml file declare this function in its interface?
104
                if (isXmlScope(scope)) {
6✔
105
                    const apiFunc = scope.xmlFile.ast?.componentElement?.interfaceElement?.functions?.find(x => x.name.toLowerCase() === textToSearchFor); // eslint-disable-line @typescript-eslint/no-loop-func
2!
106
                    if (apiFunc) {
2✔
107
                        this.event.definitions.push(
1✔
108
                            util.createLocationFromRange(util.pathToUri(scope.xmlFile.srcPath), apiFunc.getAttribute('name').tokens.value.location?.range)
3!
109
                        );
110
                        const callable = scope.getAllCallables().find((c) => c.callable.name.toLowerCase() === textToSearchFor); // eslint-disable-line @typescript-eslint/no-loop-func
1✔
111
                        if (callable) {
1!
112
                            this.event.definitions.push(
1✔
113
                                util.createLocationFromRange(util.pathToUri((callable.callable.file as BrsFile).srcPath), callable.callable.functionStatement.tokens.name.location?.range)
3!
114
                            );
115
                        }
116
                    }
117
                }
118
            }
119
            return;
2✔
120
        }
121

122
        // eslint-disable-next-line @typescript-eslint/dot-notation
123
        let classToken = file['getTokenBefore'](token, TokenKind.Class);
12✔
124
        if (classToken) {
12!
UNCOV
125
            let cs = file.parser.ast.findChild<ClassStatement>((klass) => isClassStatement(klass) && klass.tokens.class.location?.range === classToken.location?.range);
×
UNCOV
126
            if (cs?.parentClassName) {
×
127
                const nameParts = cs.parentClassName.getNameParts();
×
UNCOV
128
                let extendedClass = file.getClassFileLink(nameParts[nameParts.length - 1], nameParts.slice(0, -1).join('.'));
×
UNCOV
129
                if (extendedClass) {
×
UNCOV
130
                    this.event.definitions.push(util.createLocationFromRange(util.pathToUri(extendedClass.file.srcPath), extendedClass.item.location?.range));
×
131
                }
132
            }
UNCOV
133
            return;
×
134
        }
135

136
        if (token.kind === TokenKind.StringLiteral) {
12✔
137
            if (isImportStatement(expression)) {
7✔
138
                const pkgPath = util.getPkgPathFromTarget(file.pkgPath, expression.filePath);
6✔
139
                const importedFile = this.event.program.getFile(pkgPath);
6✔
140
                if (importedFile) {
6!
141
                    this.event.definitions.push(
6✔
142
                        util.createLocationFromRange(
143
                            util.pathToUri(importedFile.srcPath),
144
                            util.createRange(1, 0, 1, 0)
145
                        )
146
                    );
147
                    return;
6✔
148
                }
149
            }
150

151
            // We need to strip off the quotes but only if present
152
            const startIndex = textToSearchFor.startsWith('"') ? 1 : 0;
1!
153

154
            let endIndex = textToSearchFor.length;
1✔
155
            if (textToSearchFor.endsWith('"')) {
1!
156
                endIndex--;
1✔
157
            }
158
            textToSearchFor = textToSearchFor.substring(startIndex, endIndex);
1✔
159
        }
160

161
        //look through local variables first, get the function scope for this position (if it exists)
162
        const functionScope = file.getFunctionScopeAtPosition(this.event.position);
6✔
163
        if (functionScope) {
6!
164
            //find any variable or label with this name
165
            for (const varDeclaration of functionScope.variableDeclarations) {
6✔
166
                //we found a variable declaration with this token text!
167
                if (varDeclaration.name.toLowerCase() === textToSearchFor) {
4✔
168
                    const uri = util.pathToUri(file.srcPath);
1✔
169
                    this.event.definitions.push(util.createLocationFromRange(uri, varDeclaration.nameRange));
1✔
170
                }
171
            }
172
            // eslint-disable-next-line @typescript-eslint/dot-notation
173
            if (file['tokenFollows'](token, TokenKind.Goto)) {
6✔
174
                for (const label of functionScope.labelStatements) {
1✔
175
                    if (label.name.toLocaleLowerCase() === textToSearchFor) {
1!
176
                        const uri = util.pathToUri(file.srcPath);
1✔
177
                        this.event.definitions.push(util.createLocationFromRange(uri, label.nameRange));
1✔
178
                    }
179
                }
180
            }
181
        }
182

183
        const filesSearched = new Set<BrsFile>();
6✔
184
        //look through all files in scope for matches
185
        for (const scope of scopesForFile) {
6✔
186
            for (const file of scope.getAllFiles()) {
6✔
187
                if (!isBrsFile(file) || filesSearched.has(file)) {
14✔
188
                    continue;
4✔
189
                }
190
                filesSearched.add(file);
10✔
191

192
                if (previousToken?.kind === TokenKind.Dot && file.parseMode === ParseMode.BrighterScript) {
10!
193
                    this.event.definitions.push(...file.getClassMemberDefinitions(textToSearchFor, file));
2✔
194
                    const namespaceDefinition = this.brsFileGetDefinitionsForNamespace(token, file);
2✔
195
                    if (namespaceDefinition) {
2!
UNCOV
196
                        this.event.definitions.push(namespaceDefinition);
×
197
                    }
198
                }
199

200
                file.parser.ast.walk(createVisitor({
10✔
201
                    FunctionStatement: (statement: FunctionStatement) => {
202
                        if (statement.getName(file.parseMode).toLowerCase() === textToSearchFor) {
12✔
203
                            const uri = util.pathToUri(file.srcPath);
2✔
204
                            this.event.definitions.push(util.createLocationFromRange(uri, statement.location?.range));
2!
205
                        }
206
                    }
207
                }), {
208
                    walkMode: WalkMode.visitStatements
209
                });
210
            }
211
        }
212
    }
213

214

215
    private brsFileGetDefinitionsForNamespace(token: Token, file: BrsFile): Location {
216
        //BrightScript does not support namespaces, so return an empty list in that case
217
        if (!token) {
2!
UNCOV
218
            return undefined;
×
219
        }
220
        let location;
221

222
        const nameParts = (this.event.file as BrsFile).getPartialVariableName(token, [TokenKind.New]).split('.');
2✔
223
        const endName = nameParts[nameParts.length - 1].toLowerCase();
2✔
224
        const namespaceName = nameParts.slice(0, -1).join('.').toLowerCase();
2✔
225

226
        const statementHandler = (statement: NamespaceStatement) => {
2✔
UNCOV
227
            if (!location && statement.getName(ParseMode.BrighterScript).toLowerCase() === namespaceName) {
×
UNCOV
228
                const namespaceItemStatementHandler = (statement: ClassStatement | FunctionStatement) => {
×
229
                    if (!location && statement.tokens.name.text.toLowerCase() === endName) {
×
UNCOV
230
                        const uri = util.pathToUri(file.srcPath);
×
UNCOV
231
                        location = util.createLocationFromRange(uri, statement.location?.range);
×
232
                    }
233
                };
234

UNCOV
235
                file.parser.ast.walk(createVisitor({
×
236
                    ClassStatement: namespaceItemStatementHandler,
237
                    FunctionStatement: namespaceItemStatementHandler
238
                }), {
239
                    walkMode: WalkMode.visitStatements
240
                });
241

242
            }
243
        };
244

245
        file.parser.ast.walk(createVisitor({
2✔
246
            NamespaceStatement: statementHandler
247
        }), {
248
            walkMode: WalkMode.visitStatements
249
        });
250

251
        return location;
2✔
252
    }
253

254
    private xmlFileGetDefinition(file: XmlFile) {
255
        //if the position is within the file's parent component name
256
        if (
1!
257
            isXmlFile(file) &&
4✔
258
            file.parentComponent &&
259
            file.parentComponentName &&
260
            util.rangeContains(file.parentComponentName.location?.range, this.event.position)
3!
261
        ) {
262
            this.event.definitions.push({
1✔
263
                range: util.createRange(0, 0, 0, 0),
264
                uri: util.pathToUri(file.parentComponent.srcPath)
265
            });
266
        }
267
    }
268
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc