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

rokucommunity / brighterscript / #14306

24 Apr 2025 07:32PM UTC coverage: 87.078% (+0.006%) from 87.072%
#14306

push

web-flow
Merge 46929bd4f into 9f25468b6

13644 of 16562 branches covered (82.38%)

Branch coverage included in aggregate %.

169 of 186 new or added lines in 10 files covered. (90.86%)

115 existing lines in 8 files now uncovered.

14606 of 15880 relevant lines covered (91.98%)

21525.93 hits per line

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

83.16
/src/bscPlugin/hover/HoverProcessor.ts
1
import { isAssignmentStatement, isBrsFile, isCallfuncExpression, isClassStatement, isDottedGetExpression, isEnumMemberStatement, isEnumStatement, isEnumType, 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');
79✔
18

19

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

25
    }
26

27
    public process() {
28
        let hover: Hover | undefined;
29
        if (isBrsFile(this.event.file)) {
84!
30
            hover = this.getBrsFileHover(this.event.file);
84✔
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) {
84✔
37
            //assign the hover to the event
38
            this.event.hovers.push(hover);
79✔
39
        }
40
    }
41

42
    private buildContentsWithDocsFromDescription(text: string, docs: string) {
43
        const parts = [text];
4✔
44
        if (docs) {
4!
45
            parts.push('***', docs);
4✔
46
        }
47
        return parts.join('\n');
4✔
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);
8✔
55
        }
56
        return parts.join('\n');
64✔
57
    }
58

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

76
        //find a constant with this name
77
        const constant = scope?.getConstFileLink(fullName, containingNamespace);
80!
78
        if (constant) {
80✔
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();
64✔
87
        for (const labelStatement of functionScope.labelStatements) {
64✔
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);
84✔
131

132
        if (!this.isValidTokenForHover(token)) {
84✔
133
            return null;
5✔
134
        }
135
        const expression = file.getClosestExpression(this.event.position);
79✔
136
        if (!expression) {
79!
137
            return null;
×
138
        }
139
        const hoverContents: string[] = [];
79✔
140
        for (let scope of this.event.scopes) {
79✔
141
            try {
79✔
142
                scope.linkSymbolTable();
79✔
143
                const constHover = this.getConstHover(token, file, scope, expression);
79✔
144
                if (constHover) {
79✔
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);
73✔
150
                if (functionScope) {
73✔
151
                    const labelHover = this.getLabelHover(token, functionScope);
64✔
152
                    if (labelHover) {
64!
153
                        hoverContents.push(labelHover);
×
154
                        continue;
×
155
                    }
156
                }
157
                const isInTypeExpression = util.isInTypeExpression(expression);
73✔
158
                const typeFlag = isInTypeExpression ? SymbolTypeFlag.typetime : SymbolTypeFlag.runtime;
73✔
159
                const typeChain: TypeChainEntry[] = [];
73✔
160
                const extraData = {} as ExtraSymbolData;
73✔
161
                let exprType: BscType;
162

163
                if (isAssignmentStatement(expression) && token === expression.tokens.name) {
73✔
164
                    // if this is an assignment, but we're really interested in the value AFTER the assignment
165
                    exprType = expression.getSymbolTable().getSymbolType(expression.tokens.name.text, { flags: typeFlag, typeChain: typeChain, data: extraData, statementIndex: expression.statementIndex + 1 }); //expression.getSymbolTable().getSymbolType(expression.tokens.name.text, { flags: typeFlag, typeChain: typeChain, data: extraData });
13✔
166
                } else {
167
                    exprType = expression.getType({ flags: typeFlag, typeChain: typeChain, data: extraData, ignoreCall: isCallfuncExpression(expression) });
60✔
168
                }
169

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

210
                if (descriptionNode) {
73✔
211
                    hoverContent = this.buildContentsWithDocsFromExpression(hoverContent, descriptionNode);
8✔
212
                } else if (extraData.description) {
65✔
213
                    hoverContent = this.buildContentsWithDocsFromDescription(hoverContent, extraData.description);
4✔
214
                } else if (extraData.definingNode) {
61✔
215
                    hoverContent = this.buildContentsWithDocsFromExpression(hoverContent, extraData.definingNode);
50✔
216
                }
217
                hoverContents.push(hoverContent);
73✔
218

219
            } finally {
220
                scope?.unlinkSymbolTable();
79!
221
            }
222
        }
223
        return {
79✔
224
            range: token?.location.range,
237!
225
            contents: hoverContents
226
        };
227
    }
228

229
    /**
230
     * @param file the file
231
     */
232
    private getXmlFileHover(file: XmlFile) {
233
        //TODO add xml hovers
UNCOV
234
        return undefined;
×
235
    }
236
}
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