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

rokucommunity / brighterscript / #15221

22 Feb 2026 02:27AM UTC coverage: 87.213% (+0.01%) from 87.199%
#15221

push

web-flow
Merge d0c9a16a7 into 1556715dd

14752 of 17875 branches covered (82.53%)

Branch coverage included in aggregate %.

107 of 117 new or added lines in 19 files covered. (91.45%)

160 existing lines in 15 files now uncovered.

15497 of 16809 relevant lines covered (92.19%)

25606.73 hits per line

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

83.95
/src/bscPlugin/hover/HoverProcessor.ts
1
import { isAssignmentStatement, isBrsFile, isCallfuncExpression, isClassStatement, isDottedGetExpression, isEnumMemberStatement, isEnumStatement, isEnumType, isForStatement, isInheritableType, isInterfaceStatement, isMemberField, isNamespaceStatement, isNamespaceType, isNewExpression, isTypedFunctionType, isTypeStatement, isTypeStatementType, isXmlFile } from '../../astUtils/reflection';
1✔
2
import type { BrsFile } from '../../files/BrsFile';
3
import type { XmlFile } from '../../files/XmlFile';
4
import type { ExtraSymbolData, Hover, ProvideHoverEvent, TypeChainEntry } from '../../interfaces';
5
import type { Token } from '../../lexer/Token';
6
import { TokenKind } from '../../lexer/TokenKind';
1✔
7
import { BrsTranspileState } from '../../parser/BrsTranspileState';
1✔
8
import { ParseMode } from '../../parser/Parser';
1✔
9
import util from '../../util';
1✔
10
import { SymbolTypeFlag } from '../../SymbolTypeFlag';
1✔
11
import type { AstNode, Expression } from '../../parser/AstNode';
12
import type { Scope } from '../../Scope';
13
import type { FunctionScope } from '../../FunctionScope';
14
import type { BscType } from '../../types/BscType';
15
import type { ClassStatement, EnumStatement, FieldStatement, InterfaceFieldStatement, InterfaceStatement } from '../../parser/Statement';
16

17
const fence = (code: string) => util.mdFence(code, 'brightscript');
92✔
18

19

20
export class HoverProcessor {
1✔
21
    public constructor(
22
        public event: ProvideHoverEvent
98✔
23
    ) {
24

25
    }
26

27
    public process() {
28
        let hover: Hover | undefined;
29
        if (isBrsFile(this.event.file)) {
97!
30
            hover = this.getBrsFileHover(this.event.file);
97✔
31
        } else if (isXmlFile(this.event.file)) {
×
32
            hover = this.getXmlFileHover(this.event.file);
×
33
        }
34

35
        //if we got a result, "return" it
36
        if (hover) {
97✔
37
            //assign the hover to the event
38
            this.event.hovers.push(hover);
92✔
39
        }
40
    }
41

42
    private buildContentsWithDocsFromDescription(text: string, docs: string) {
43
        const parts = [text];
6✔
44
        if (docs) {
6!
45
            parts.push('***', docs);
6✔
46
        }
47
        return parts.join('\n');
6✔
48
    }
49

50
    private buildContentsWithDocsFromExpression(text: string, expression: AstNode) {
51
        const parts = [text];
68✔
52
        const docs = util.getNodeDocumentation(expression);
68✔
53
        if (docs) {
68✔
54
            parts.push('***', docs);
9✔
55
        }
56
        return parts.join('\n');
68✔
57
    }
58

59
    private isValidTokenForHover(token: Token) {
60
        let hoverTokenTypes = [
97✔
61
            TokenKind.Identifier,
62
            TokenKind.Function,
63
            TokenKind.EndFunction,
64
            TokenKind.Sub,
65
            TokenKind.EndSub
66
        ];
67

68
        //throw out invalid tokens and the wrong kind of tokens
69
        return (token && hoverTokenTypes.includes(token.kind));
97✔
70
    }
71

72
    private getConstHover(token: Token, file: BrsFile, scope: Scope, expression: Expression) {
73
        let containingNamespace = file.getNamespaceStatementForPosition(expression?.location?.range?.start)?.getName(ParseMode.BrighterScript);
93!
74
        const fullName = util.getAllDottedGetParts(expression)?.map(x => x.text).join('.');
94✔
75

76
        //find a constant with this name
77
        const constant = scope?.getConstFileLink(fullName, containingNamespace);
93!
78
        if (constant) {
93✔
79
            const constantValue = util.sourceNodeFromTranspileResult(null, null, null, constant.item.value.transpile(new BrsTranspileState(file))).toString();
6✔
80
            const constantType = constant.item.getType({ flags: SymbolTypeFlag.runtime });
6✔
81
            return this.buildContentsWithDocsFromExpression(fence(`const ${constant.item.fullName} = ${constantValue} as ${constantType.toString()}`), constant.item);
6✔
82
        }
83
    }
84

85
    private getLabelHover(token: Token, functionScope: FunctionScope) {
86
        let lowerTokenText = token.text.toLowerCase();
77✔
87
        for (const labelStatement of functionScope.labelStatements) {
77✔
88
            if (labelStatement.name.toLocaleLowerCase() === lowerTokenText) {
×
89
                return fence(`${labelStatement.name}: label`);
×
90
            }
91
        }
92
    }
93

94
    private getCustomTypeHover(expressionType: BscType, extraData: ExtraSymbolData) {
95
        let declarationText = '';
6✔
96
        let exprTypeString = expressionType.toString();
6✔
97
        let firstToken: Token;
98
        if (extraData?.definingNode) {
6!
99
            if (isClassStatement(extraData.definingNode)) {
6✔
100
                firstToken = extraData.definingNode.tokens.class;
3✔
101
                declarationText = firstToken?.text ?? TokenKind.Class;
3!
102
            } else if (isInterfaceStatement(extraData.definingNode)) {
3!
103
                firstToken = extraData.definingNode.tokens.interface;
×
104
                declarationText = firstToken?.text ?? TokenKind.Interface;
×
105
            } else if (isNamespaceStatement(extraData.definingNode)) {
3✔
106
                firstToken = extraData.definingNode.tokens.namespace;
1✔
107
                exprTypeString = extraData.definingNode.getName(ParseMode.BrighterScript);
1✔
108
                declarationText = firstToken?.text ?? TokenKind.Namespace;
1!
109
            } else if (isEnumStatement(extraData.definingNode)) {
2✔
110
                firstToken = extraData.definingNode.tokens.enum;
1✔
111
                exprTypeString = extraData.definingNode.fullName;
1✔
112
                declarationText = firstToken?.text ?? TokenKind.Enum;
1!
113
            } else if (isTypeStatementType(expressionType) && isTypeStatement(extraData.definingNode)) {
1!
114
                firstToken = extraData.definingNode.tokens.type;
1✔
115
                declarationText = (firstToken?.text ?? 'type');
1!
116
                exprTypeString = expressionType.toString();
1✔
117
            }
118
        }
119
        const innerText = `${declarationText} ${exprTypeString}`.trim();
6✔
120
        let result = fence(innerText);
6✔
121
        return result;
6✔
122
    }
123

124
    private getMemberHover(memberExpression: FieldStatement | InterfaceFieldStatement, expressionType: BscType) {
125
        let nameText = `${(memberExpression.parent as ClassStatement | InterfaceStatement)?.getName(ParseMode.BrighterScript)}.${memberExpression.tokens.name.text}`;
7!
126
        let exprTypeString = expressionType.isResolvable() ? expressionType.toString() : 'invalid';
7!
127
        const innerText = `${nameText} as ${exprTypeString}`.trim();
7✔
128
        let result = fence(innerText);
7✔
129
        return result;
7✔
130
    }
131

132
    private getBrsFileHover(file: BrsFile): Hover {
133
        //get the token at the position
134
        let token = file.getTokenAt(this.event.position);
97✔
135

136
        if (!this.isValidTokenForHover(token)) {
97✔
137
            return null;
5✔
138
        }
139
        const expression = file.getClosestExpression(this.event.position);
92✔
140
        if (!expression) {
92!
UNCOV
141
            return null;
×
142
        }
143
        const hoverContents: string[] = [];
92✔
144
        for (let scope of this.event.scopes) {
92✔
145
            try {
92✔
146
                scope.linkSymbolTable();
92✔
147
                const constHover = this.getConstHover(token, file, scope, expression);
92✔
148
                if (constHover) {
92✔
149
                    hoverContents.push(constHover);
6✔
150
                    continue;
6✔
151
                }
152
                //get the function scope for this position (if exists)
153
                let functionScope = file.getFunctionScopeAtPosition(this.event.position);
86✔
154
                if (functionScope) {
86✔
155
                    const labelHover = this.getLabelHover(token, functionScope);
77✔
156
                    if (labelHover) {
77!
UNCOV
157
                        hoverContents.push(labelHover);
×
UNCOV
158
                        continue;
×
159
                    }
160
                }
161
                const isInTypeExpression = util.isInTypeExpression(expression);
86✔
162
                const typeFlag = isInTypeExpression ? SymbolTypeFlag.typetime : SymbolTypeFlag.runtime;
86✔
163
                const typeChain: TypeChainEntry[] = [];
86✔
164
                const extraData = {} as ExtraSymbolData;
86✔
165
                let exprType: BscType;
166

167
                if (isAssignmentStatement(expression) && token === expression.tokens.name) {
86✔
168
                    if (isForStatement(expression.parent) && expression.parent.counterDeclaration === expression) {
13✔
169
                        // for loop counter variable - its type is always integer
170
                        exprType = expression.value.getType({ flags: typeFlag, typeChain: typeChain, data: extraData, statementIndex: expression.statementIndex });
1✔
171
                    } else {
172
                        // if this is an assignment, but we're really interested in the value AFTER the assignment
173
                        exprType = expression.getSymbolTable().getSymbolType(expression.tokens.name.text, { flags: typeFlag, typeChain: typeChain, data: extraData, statementIndex: expression.statementIndex + 1 });
12✔
174
                    }
175
                } else {
176
                    exprType = expression.getType({ flags: typeFlag, typeChain: typeChain, data: extraData, ignoreCall: isCallfuncExpression(expression) });
73✔
177
                }
178

179
                const processedTypeChain = util.processTypeChain(typeChain);
86✔
180
                const fullName = processedTypeChain.fullNameOfItem || token.text;
86✔
181
                // if the type chain has dynamic in it, then just say the token text
182
                let exprNameString = !processedTypeChain.containsDynamic ? fullName : token.text;
86✔
183
                if (isTypedFunctionType(exprType)) {
86✔
184
                    exprNameString = processedTypeChain.fullNameOfItem;
21✔
185
                }
186
                const useCustomTypeHover = isInTypeExpression || expression?.findAncestor(isNewExpression);
86!
187
                let hoverContent = '';
86✔
188
                let descriptionNode;
189
                if (useCustomTypeHover && (isInheritableType(exprType) || isTypeStatementType(exprType))) {
86✔
190
                    hoverContent = this.getCustomTypeHover(exprType, extraData);
4✔
191
                } else if (isMemberField(expression)) {
82✔
192
                    hoverContent = this.getMemberHover(expression, exprType);
7✔
193
                    descriptionNode = expression;
7✔
194
                } else if (isEnumMemberStatement(expression)) {
75✔
195
                    hoverContent = fence(`${(expression.parent as EnumStatement)?.name}.${expression.tokens.name.text} as ${exprType.toString()}`.trim());
1!
196
                    descriptionNode = expression;
1✔
197
                } else if (isNamespaceType(exprType) ||
74✔
198
                    isEnumType(expression.getType({ flags: SymbolTypeFlag.typetime }))) {
199
                    hoverContent = this.getCustomTypeHover(exprType, extraData);
2✔
200
                } else {
201
                    const variableName = !isTypedFunctionType(exprType) ? `${exprNameString} as ` : '';
72✔
202
                    if (isTypedFunctionType(exprType)) {
72✔
203
                        exprType.setName(exprNameString);
21✔
204
                    }
205
                    let exprTypeString = exprType.toString();
72✔
206
                    if (!exprType.isResolvable()) {
72✔
207
                        if (isDottedGetExpression(expression)) {
2✔
208
                            exprTypeString = 'invalid';
1✔
209
                        }
210
                    }
211
                    hoverContent = fence(`${variableName}${exprTypeString}`);
72✔
212
                }
213
                const modifiers = [];
86✔
214
                // eslint-disable-next-line no-bitwise
215
                if (extraData?.flags && extraData?.flags & SymbolTypeFlag.optional) {
86!
UNCOV
216
                    modifiers.push(TokenKind.Optional);
×
217
                }
218
                if (modifiers.length > 0) {
86!
UNCOV
219
                    hoverContent += `\n * (${modifiers.join(', ').toLowerCase()})* `;
×
220
                }
221

222
                if (descriptionNode) {
86✔
223
                    hoverContent = this.buildContentsWithDocsFromExpression(hoverContent, descriptionNode);
8✔
224
                } else if (extraData.description) {
78✔
225
                    hoverContent = this.buildContentsWithDocsFromDescription(hoverContent, extraData.description);
6✔
226
                } else if (extraData.definingNode) {
72✔
227
                    hoverContent = this.buildContentsWithDocsFromExpression(hoverContent, extraData.definingNode);
54✔
228
                }
229
                hoverContents.push(hoverContent);
86✔
230

231
            } finally {
232
                scope?.unlinkSymbolTable();
92!
233
            }
234
        }
235
        return {
92✔
236
            range: token?.location.range,
276!
237
            contents: hoverContents
238
        };
239
    }
240

241
    /**
242
     * @param file the file
243
     */
244
    private getXmlFileHover(file: XmlFile) {
245
        //TODO add xml hovers
UNCOV
246
        return undefined;
×
247
    }
248
}
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