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

rokucommunity / brighterscript / #15131

28 Jan 2026 03:47PM UTC coverage: 87.193% (+0.01%) from 87.179%
#15131

push

web-flow
Merge aa579b555 into 610607efc

14637 of 17741 branches covered (82.5%)

Branch coverage included in aggregate %.

69 of 71 new or added lines in 11 files covered. (97.18%)

200 existing lines in 8 files now uncovered.

15395 of 16702 relevant lines covered (92.17%)

24786.07 hits per line

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

83.5
/src/bscPlugin/hover/HoverProcessor.ts
1
import { isAssignmentStatement, isBrsFile, isCallfuncExpression, isClassStatement, isDottedGetExpression, isEnumMemberStatement, isEnumStatement, isEnumType, isForStatement, isInheritableType, isInterfaceStatement, isMemberField, isNamespaceStatement, isNamespaceType, isNewExpression, isTypedFunctionType, 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');
84✔
18

19

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

25
    }
26

27
    public process() {
28
        let hover: Hover | undefined;
29
        if (isBrsFile(this.event.file)) {
89!
30
            hover = this.getBrsFileHover(this.event.file);
89✔
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) {
89✔
37
            //assign the hover to the event
38
            this.event.hovers.push(hover);
84✔
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];
64✔
52
        const docs = util.getNodeDocumentation(expression);
64✔
53
        if (docs) {
64✔
54
            parts.push('***', docs);
9✔
55
        }
56
        return parts.join('\n');
64✔
57
    }
58

59
    private isValidTokenForHover(token: Token) {
60
        let hoverTokenTypes = [
89✔
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));
89✔
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);
85!
74
        const fullName = util.getAllDottedGetParts(expression)?.map(x => x.text).join('.');
88✔
75

76
        //find a constant with this name
77
        const constant = scope?.getConstFileLink(fullName, containingNamespace);
85!
78
        if (constant) {
85✔
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();
69✔
87
        for (const labelStatement of functionScope.labelStatements) {
69✔
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 = '';
5✔
96
        let exprTypeString = expressionType.toString();
5✔
97
        let firstToken: Token;
98
        if (extraData?.definingNode) {
5!
99
            if (isClassStatement(extraData.definingNode)) {
5✔
100
                firstToken = extraData.definingNode.tokens.class;
3✔
101
                declarationText = firstToken?.text ?? TokenKind.Class;
3!
102
            } else if (isInterfaceStatement(extraData.definingNode)) {
2!
103
                firstToken = extraData.definingNode.tokens.interface;
×
104
                declarationText = firstToken?.text ?? TokenKind.Interface;
×
105
            } else if (isNamespaceStatement(extraData.definingNode)) {
2✔
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)) {
1!
110
                firstToken = extraData.definingNode.tokens.enum;
1✔
111
                exprTypeString = extraData.definingNode.fullName;
1✔
112
                declarationText = firstToken?.text ?? TokenKind.Enum;
1!
113
            }
114
        }
115
        const innerText = `${declarationText} ${exprTypeString}`.trim();
5✔
116
        let result = fence(innerText);
5✔
117
        return result;
5✔
118
    }
119

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

128
    private getBrsFileHover(file: BrsFile): Hover {
129
        //get the token at the position
130
        let token = file.getTokenAt(this.event.position);
89✔
131

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

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

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

215
                if (descriptionNode) {
78✔
216
                    hoverContent = this.buildContentsWithDocsFromExpression(hoverContent, descriptionNode);
8✔
217
                } else if (extraData.description) {
70✔
218
                    hoverContent = this.buildContentsWithDocsFromDescription(hoverContent, extraData.description);
6✔
219
                } else if (extraData.definingNode) {
64✔
220
                    hoverContent = this.buildContentsWithDocsFromExpression(hoverContent, extraData.definingNode);
50✔
221
                }
222
                hoverContents.push(hoverContent);
78✔
223

224
            } finally {
225
                scope?.unlinkSymbolTable();
84!
226
            }
227
        }
228
        return {
84✔
229
            range: token?.location.range,
252!
230
            contents: hoverContents
231
        };
232
    }
233

234
    /**
235
     * @param file the file
236
     */
237
    private getXmlFileHover(file: XmlFile) {
238
        //TODO add xml hovers
UNCOV
239
        return undefined;
×
240
    }
241
}
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