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

rokucommunity / brighterscript / 26661434192

29 May 2026 08:48PM UTC coverage: 87.054%. First build
26661434192

Pull #1726

github

web-flow
Merge 883a1db73 into 3ab3aaf0c
Pull Request #1726: Merge master into v1

15999 of 19398 branches covered (82.48%)

Branch coverage included in aggregate %.

74 of 122 new or added lines in 7 files covered. (60.66%)

16660 of 18118 relevant lines covered (91.95%)

27556.73 hits per line

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

59.76
/src/bscPlugin/inlayHints/InlayHintProcessor.ts
1
import { InlayHintKind } from 'vscode-languageserver-protocol';
1✔
2
import {
1✔
3
    isBrsFile,
4
    isClassStatement,
5
    isDottedGetExpression,
6
    isFunctionStatement,
7
    isMethodStatement,
8
    isNamespaceStatement,
9
    isNewExpression,
10
    isVariableExpression,
11
    isXmlScope
12
} from '../../astUtils/reflection';
13
import { WalkMode, createVisitor } from '../../astUtils/visitors';
1✔
14
import type { BrsFile } from '../../files/BrsFile';
15
import type { ProvideInlayHintsEvent } from '../../interfaces';
16
import type { CallExpression, CallfuncExpression, FunctionParameterExpression } from '../../parser/Expression';
17
import type { Expression } from '../../parser/AstNode';
18
import type { ClassStatement, FunctionStatement, MethodStatement, NamespaceStatement } from '../../parser/Statement';
19
import { ParseMode } from '../../parser/Parser';
1✔
20
import { util } from '../../util';
1✔
21
import type { XmlScope } from '../../XmlScope';
22

23
export class InlayHintProcessor {
1✔
24
    public constructor(
25
        public event: ProvideInlayHintsEvent
11✔
26
    ) { }
27

28
    public process() {
29
        if (!isBrsFile(this.event.file)) {
11!
NEW
30
            return;
×
31
        }
32
        this.collectParameterNameHints(this.event.file);
11✔
33
    }
34

35
    private collectParameterNameHints(file: BrsFile) {
36
        const range = this.event.range;
11✔
37

38
        file.ast.walk(createVisitor({
11✔
39
            CallExpression: (call) => {
40
                if (!call.location?.range || !util.rangesIntersectOrTouch(call.location.range, range)) {
11!
41
                    return;
1✔
42
                }
43
                this.emitParameterNameHints(file, call);
10✔
44
            },
45
            CallfuncExpression: (call) => {
NEW
46
                if (!call.location?.range || !util.rangesIntersectOrTouch(call.location.range, range)) {
×
NEW
47
                    return;
×
48
                }
NEW
49
                this.emitParameterNameHintsForCallfunc(file, call);
×
50
            }
51
        }), {
52
            walkMode: WalkMode.visitAllRecursive
53
        });
54
    }
55

56
    private emitParameterNameHints(file: BrsFile, call: CallExpression) {
57
        if (!call.args || call.args.length === 0) {
10✔
58
            return;
1✔
59
        }
60

61
        const params = this.resolveCallParameters(file, call);
9✔
62
        if (!params) {
9✔
63
            return;
1✔
64
        }
65

66
        this.pushHintsForArgs(call.args, params);
8✔
67
    }
68

69
    private emitParameterNameHintsForCallfunc(file: BrsFile, call: CallfuncExpression) {
NEW
70
        if (!call.args || call.args.length === 0) {
×
NEW
71
            return;
×
72
        }
NEW
73
        const name = call.tokens.methodName?.text;
×
NEW
74
        if (!name) {
×
NEW
75
            return;
×
76
        }
NEW
77
        const params = this.resolveCallfuncParameters(file, name);
×
NEW
78
        if (!params) {
×
NEW
79
            return;
×
80
        }
NEW
81
        this.pushHintsForArgs(call.args, params);
×
82
    }
83

84
    /**
85
     * For a CallExpression, find the function/method being called and return its parameter list,
86
     * or undefined if the target cannot be uniquely resolved.
87
     */
88
    private resolveCallParameters(file: BrsFile, call: CallExpression): FunctionParameterExpression[] | undefined {
89
        //constructor: `new Foo(...)`
90
        if (isNewExpression(call.parent)) {
9✔
91
            const className = call.parent.className.getName(ParseMode.BrighterScript);
1✔
92
            const classLink = file.getClassFileLink(className);
1✔
93
            if (!classLink) {
1!
NEW
94
                return undefined;
×
95
            }
96
            const ctor = file.getClassMethod(classLink.item, 'new');
1✔
97
            return ctor?.func?.parameters;
1!
98
        }
99

100
        const callee = call.callee;
8✔
101

102
        //plain function call: `foo(...)`
103
        if (isVariableExpression(callee)) {
8✔
104
            const name = callee.tokens.name.text;
6✔
105
            const containingNamespace = callee.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
6!
106
            return this.lookupFunctionParameters(file, name, containingNamespace);
6✔
107
        }
108

109
        //namespace call or method call: `ns.foo(...)` / `m.foo(...)` / `instance.foo(...)`
110
        if (isDottedGetExpression(callee)) {
2!
111
            const name = callee.tokens.name.text;
2✔
112
            const parts = util.getAllDottedGetParts(callee);
2✔
113
            if (!parts || parts.length < 2) {
2!
NEW
114
                return undefined;
×
115
            }
116
            //drop the last part (the function name) to get the namespace/receiver path
117
            const dotPart = parts.slice(0, parts.length - 1).map(x => x.text).join('.');
2✔
118

119
            //prefer namespace lookup when the dotPart is a known namespace
120
            const scope = file.program.getFirstScopeForFile(file);
2✔
121
            const namespace = scope?.namespaceLookup?.get(dotPart.toLowerCase());
2!
122
            if (namespace) {
2✔
123
                return this.lookupFunctionParameters(file, name, dotPart);
1✔
124
            }
125

126
            //otherwise, treat as method call on m or another receiver - look across class methods
127
            return this.lookupClassMethodParameters(file, callee, name);
1✔
128
        }
129

NEW
130
        return undefined;
×
131
    }
132

133
    private lookupFunctionParameters(file: BrsFile, name: string, namespaceName?: string): FunctionParameterExpression[] | undefined {
134
        const matches = file.program.getStatementsByName(name, file, namespaceName);
7✔
135
        if (matches.length !== 1) {
7✔
136
            return undefined;
1✔
137
        }
138
        const statement = matches[0].item;
6✔
139
        if (isFunctionStatement(statement) || isMethodStatement(statement)) {
6!
140
            return (statement as FunctionStatement | MethodStatement).func?.parameters;
6!
141
        }
NEW
142
        return undefined;
×
143
    }
144

145
    /**
146
     * For a method call like `m.foo(...)`, find a uniquely-resolvable method statement.
147
     * If the receiver is `m`, prefer the enclosing class. Otherwise fall back to a name search
148
     * across all classes (only used when there's exactly one match).
149
     */
150
    private lookupClassMethodParameters(file: BrsFile, callee: { obj?: any }, name: string): FunctionParameterExpression[] | undefined {
151
        if (isVariableExpression(callee.obj) && callee.obj.tokens.name.text === 'm') {
1!
152
            const enclosingClass = callee.obj.findAncestor<ClassStatement>(isClassStatement);
1✔
153
            if (enclosingClass) {
1!
154
                const method = file.getClassMethod(enclosingClass, name, true);
1✔
155
                if (method) {
1!
156
                    return method.func?.parameters;
1!
157
                }
158
            }
159
        }
160

161
        //fallback: search all classes for a single matching method name
NEW
162
        const matches = file.program.getStatementsByName(name, file).filter(link => isClassStatement(link.item.parent));
×
NEW
163
        if (matches.length !== 1) {
×
NEW
164
            return undefined;
×
165
        }
NEW
166
        const statement = matches[0].item;
×
NEW
167
        if (isMethodStatement(statement) || isFunctionStatement(statement)) {
×
NEW
168
            return (statement as FunctionStatement | MethodStatement).func?.parameters;
×
169
        }
NEW
170
        return undefined;
×
171
    }
172

173
    private resolveCallfuncParameters(file: BrsFile, name: string): FunctionParameterExpression[] | undefined {
174
        //callfunc invocations are dispatched through XML components - look across xml scopes for a function with this name
NEW
175
        const matches = file.program.getScopes()
×
NEW
176
            .filter(scope => isXmlScope(scope))
×
NEW
177
            .flatMap(scope => file.program.getStatementsForXmlFile(scope as XmlScope, name));
×
178

NEW
179
        if (matches.length !== 1) {
×
NEW
180
            return undefined;
×
181
        }
NEW
182
        const statement = matches[0].item;
×
NEW
183
        if (isFunctionStatement(statement) || isMethodStatement(statement)) {
×
NEW
184
            return (statement as FunctionStatement | MethodStatement).func?.parameters;
×
185
        }
NEW
186
        return undefined;
×
187
    }
188

189
    private pushHintsForArgs(args: Expression[], params: FunctionParameterExpression[]) {
190
        for (let i = 0; i < args.length && i < params.length; i++) {
8✔
191
            const arg = args[i];
15✔
192
            const param = params[i];
15✔
193
            const paramName = param?.tokens?.name?.text;
15!
194
            const argRange = arg?.location?.range;
15!
195
            if (!paramName || !argRange) {
15!
NEW
196
                continue;
×
197
            }
198
            //skip when the argument is just an identifier that already matches the parameter name
199
            if (isVariableExpression(arg) && arg.tokens.name?.text?.toLowerCase() === paramName.toLowerCase()) {
15!
200
                continue;
1✔
201
            }
202
            this.event.inlayHints.push({
14✔
203
                position: argRange.start,
204
                label: `${paramName}:`,
205
                kind: InlayHintKind.Parameter,
206
                paddingRight: true
207
            });
208
        }
209
    }
210
}
211

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