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

rokucommunity / brighterscript / #13125

01 Oct 2024 02:12PM UTC coverage: 86.842% (-1.4%) from 88.193%
#13125

push

web-flow
Merge d4a9e5fff into 3a2dc7282

11554 of 14068 branches covered (82.13%)

Branch coverage included in aggregate %.

7000 of 7592 new or added lines in 100 files covered. (92.2%)

83 existing lines in 18 files now uncovered.

12701 of 13862 relevant lines covered (91.62%)

29529.09 hits per line

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

74.38
/src/bscPlugin/definition/DefinitionProvider.ts
1
import { isBrsFile, isClassStatement, isDottedGetExpression, 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
19✔
16
    ) { }
17

18
    public process(): Location[] {
19
        if (isBrsFile(this.event.file)) {
19✔
20
            this.brsFileGetDefinition(this.event.file);
17✔
21
        } else if (isXmlFile(this.event.file)) {
2✔
22
            this.xmlFileGetDefinition(this.event.file);
1✔
23
        }
24
        return this.event.definitions;
19✔
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);
17✔
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 = [
17✔
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)) {
17✔
42
            return;
2✔
43
        }
44

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

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

54
            //find a constant with this name
55
            const constant = scope?.getConstFileLink(fullName, containingNamespace);
15!
56
            if (constant) {
15✔
57
                this.event.definitions.push(
1✔
58
                    constant.item.tokens.name?.location
3!
59
                );
60
                return;
1✔
61
            }
62
            if (isDottedGetExpression(expression) || isVariableExpression(expression)) {
14✔
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();
8✔
98

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

101
        if (previousToken?.kind === TokenKind.Callfunc) {
8!
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);
6✔
124
        if (classToken) {
6!
NEW
125
            let cs = file.parser.ast.findChild<ClassStatement>((klass) => isClassStatement(klass) && klass.tokens.class.location?.range === classToken.location?.range);
×
126
            if (cs?.parentClassName) {
×
127
                const nameParts = cs.parentClassName.getNameParts();
×
128
                let extendedClass = file.getClassFileLink(nameParts[nameParts.length - 1], nameParts.slice(0, -1).join('.'));
×
129
                if (extendedClass) {
×
NEW
130
                    this.event.definitions.push(util.createLocationFromRange(util.pathToUri(extendedClass.file.srcPath), extendedClass.item.location?.range));
×
131
                }
132
            }
133
            return;
×
134
        }
135

136
        if (token.kind === TokenKind.StringLiteral) {
6✔
137
            // We need to strip off the quotes but only if present
138
            const startIndex = textToSearchFor.startsWith('"') ? 1 : 0;
1!
139

140
            let endIndex = textToSearchFor.length;
1✔
141
            if (textToSearchFor.endsWith('"')) {
1!
142
                endIndex--;
1✔
143
            }
144
            textToSearchFor = textToSearchFor.substring(startIndex, endIndex);
1✔
145
        }
146

147
        //look through local variables first, get the function scope for this position (if it exists)
148
        const functionScope = file.getFunctionScopeAtPosition(this.event.position);
6✔
149
        if (functionScope) {
6!
150
            //find any variable or label with this name
151
            for (const varDeclaration of functionScope.variableDeclarations) {
6✔
152
                //we found a variable declaration with this token text!
153
                if (varDeclaration.name.toLowerCase() === textToSearchFor) {
4✔
154
                    const uri = util.pathToUri(file.srcPath);
1✔
155
                    this.event.definitions.push(util.createLocationFromRange(uri, varDeclaration.nameRange));
1✔
156
                }
157
            }
158
            // eslint-disable-next-line @typescript-eslint/dot-notation
159
            if (file['tokenFollows'](token, TokenKind.Goto)) {
6✔
160
                for (const label of functionScope.labelStatements) {
1✔
161
                    if (label.name.toLocaleLowerCase() === textToSearchFor) {
1!
162
                        const uri = util.pathToUri(file.srcPath);
1✔
163
                        this.event.definitions.push(util.createLocationFromRange(uri, label.nameRange));
1✔
164
                    }
165
                }
166
            }
167
        }
168

169
        const filesSearched = new Set<BrsFile>();
6✔
170
        //look through all files in scope for matches
171
        for (const scope of scopesForFile) {
6✔
172
            for (const file of scope.getAllFiles()) {
6✔
173
                if (!isBrsFile(file) || filesSearched.has(file)) {
14✔
174
                    continue;
4✔
175
                }
176
                filesSearched.add(file);
10✔
177

178
                if (previousToken?.kind === TokenKind.Dot && file.parseMode === ParseMode.BrighterScript) {
10!
179
                    this.event.definitions.push(...file.getClassMemberDefinitions(textToSearchFor, file));
2✔
180
                    const namespaceDefinition = this.brsFileGetDefinitionsForNamespace(token, file);
2✔
181
                    if (namespaceDefinition) {
2!
182
                        this.event.definitions.push(namespaceDefinition);
×
183
                    }
184
                }
185

186
                file.parser.ast.walk(createVisitor({
10✔
187
                    FunctionStatement: (statement: FunctionStatement) => {
188
                        if (statement.getName(file.parseMode).toLowerCase() === textToSearchFor) {
12✔
189
                            const uri = util.pathToUri(file.srcPath);
2✔
190
                            this.event.definitions.push(util.createLocationFromRange(uri, statement.location?.range));
2!
191
                        }
192
                    }
193
                }), {
194
                    walkMode: WalkMode.visitStatements
195
                });
196
            }
197
        }
198
    }
199

200

201
    private brsFileGetDefinitionsForNamespace(token: Token, file: BrsFile): Location {
202
        //BrightScript does not support namespaces, so return an empty list in that case
203
        if (!token) {
2!
204
            return undefined;
×
205
        }
206
        let location;
207

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

212
        const statementHandler = (statement: NamespaceStatement) => {
2✔
213
            if (!location && statement.getName(ParseMode.BrighterScript).toLowerCase() === namespaceName) {
×
214
                const namespaceItemStatementHandler = (statement: ClassStatement | FunctionStatement) => {
×
NEW
215
                    if (!location && statement.tokens.name.text.toLowerCase() === endName) {
×
216
                        const uri = util.pathToUri(file.srcPath);
×
NEW
217
                        location = util.createLocationFromRange(uri, statement.location?.range);
×
218
                    }
219
                };
220

221
                file.parser.ast.walk(createVisitor({
×
222
                    ClassStatement: namespaceItemStatementHandler,
223
                    FunctionStatement: namespaceItemStatementHandler
224
                }), {
225
                    walkMode: WalkMode.visitStatements
226
                });
227

228
            }
229
        };
230

231
        file.parser.ast.walk(createVisitor({
2✔
232
            NamespaceStatement: statementHandler
233
        }), {
234
            walkMode: WalkMode.visitStatements
235
        });
236

237
        return location;
2✔
238
    }
239

240
    private xmlFileGetDefinition(file: XmlFile) {
241
        //if the position is within the file's parent component name
242
        if (
1!
243
            isXmlFile(file) &&
4✔
244
            file.parentComponent &&
245
            file.parentComponentName &&
246
            util.rangeContains(file.parentComponentName.location?.range, this.event.position)
3!
247
        ) {
248
            this.event.definitions.push({
1✔
249
                range: util.createRange(0, 0, 0, 0),
250
                uri: util.pathToUri(file.parentComponent.srcPath)
251
            });
252
        }
253
    }
254
}
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