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

rokucommunity / brighterscript / 28390310190

29 Jun 2026 05:22PM UTC coverage: 88.483% (+0.04%) from 88.445%
28390310190

Pull #1698

github

web-flow
Merge e4f9da1c4 into 824751874
Pull Request #1698: Validate eval/rsg_version against firmware lifecycle

8967 of 10668 branches covered (84.06%)

Branch coverage included in aggregate %.

99 of 105 new or added lines in 6 files covered. (94.29%)

11254 of 12185 relevant lines covered (92.36%)

2053.73 hits per line

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

90.77
/src/bscPlugin/validation/BrsFileValidator.ts
1
import { isAliasStatement, isBody, isCallExpression, isClassStatement, isCommentStatement, isConstStatement, isDottedGetExpression, isDottedSetStatement, isEnumStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isImportStatement, isIndexedGetExpression, isIndexedSetStatement, isInterfaceStatement, isLibraryStatement, isLiteralExpression, isNamespaceStatement, isTypecastStatement, isTypeStatement, isUnaryExpression, isVariableExpression, isWhileStatement } from '../../astUtils/reflection';
1✔
2
import { createVisitor, WalkMode } from '../../astUtils/visitors';
1✔
3
import { DiagnosticMessages } from '../../DiagnosticMessages';
1✔
4
import type { BrsFile } from '../../files/BrsFile';
5
import type { OnFileValidateEvent } from '../../interfaces';
6
import { TokenKind, UnreferencableBuiltins } from '../../lexer/TokenKind';
1✔
7
import type { AstNode, Expression, Statement } from '../../parser/AstNode';
8
import { CallExpression, type FunctionExpression, type LiteralExpression } from '../../parser/Expression';
1✔
9
import { ParseMode } from '../../parser/Parser';
1✔
10
import type { ContinueStatement, EnumMemberStatement, EnumStatement, ForEachStatement, ForStatement, ImportStatement, LibraryStatement, WhileStatement } from '../../parser/Statement';
11
import { DynamicType } from '../../types/DynamicType';
1✔
12
import { InterfaceType } from '../../types/InterfaceType';
1✔
13
import util from '../../util';
1✔
14
import type { Range } from 'vscode-languageserver';
15
import * as semver from 'semver';
1✔
16
import { OPTIONAL_CHAINING_MIN_FIRMWARE_VERSION } from '../../RokuConstants';
1✔
17
import type { AvailabilityAxis } from '../../DiagnosticMessages';
18
import { globalCallableMap } from '../../globalCallables';
1✔
19

20
export class BrsFileValidator {
1✔
21
    constructor(
22
        public event: OnFileValidateEvent<BrsFile>
1,097✔
23
    ) {
24
    }
25

26
    public process() {
27
        util.validateTooDeepFile(this.event.file);
1,097✔
28
        this.walk();
1,097✔
29
        this.flagTopLevelStatements();
1,097✔
30
        //only validate the file if it was actually parsed (skip files containing typedefs)
31
        if (!this.event.file.hasTypedef) {
1,097✔
32
            this.validateImportStatements();
1,096✔
33
        }
34
    }
35

36
    /**
37
     * Walk the full AST
38
     */
39
    private walk() {
40
        const visitor = createVisitor({
1,097✔
41
            MethodStatement: (node) => {
42
                //add the `super` symbol to class methods
43
                node.func.body.symbolTable.addSymbol('super', undefined, DynamicType.instance);
142✔
44
            },
45
            CallfuncExpression: (node) => {
46
                if (node.args.length > 5) {
14✔
47
                    this.event.file.addDiagnostic({
1✔
48
                        ...DiagnosticMessages.callfuncHasToManyArgs(node.args.length),
49
                        range: node.methodName.range
50
                    });
51
                }
52
            },
53
            DottedGetExpression: (node) => {
54
                if (node.dot?.kind === TokenKind.QuestionDot) {
598!
55
                    this.validateMinFirmwareVersionForOptionalChaining(node.dot.range);
23✔
56
                }
57
            },
58
            IndexedGetExpression: (node) => {
59
                if (node.questionDotToken || node.openingSquare?.kind === TokenKind.QuestionLeftSquare) {
76!
60
                    const range = node.questionDotToken?.range ?? node.openingSquare?.range;
9!
61
                    this.validateMinFirmwareVersionForOptionalChaining(range);
9✔
62
                }
63
            },
64
            CallExpression: (node) => {
65
                if (node.openingParen?.kind === TokenKind.QuestionLeftParen) {
487!
66
                    this.validateMinFirmwareVersionForOptionalChaining(node.openingParen.range);
5✔
67
                }
68
                this.validateGlobalCallableAvailability(node);
487✔
69
            },
70
            EnumStatement: (node) => {
71
                this.validateDeclarationLocations(node, 'enum', () => util.createBoundingRange(node.tokens.enum, node.tokens.name));
90✔
72

73
                this.validateEnumDeclaration(node);
90✔
74

75
                //register this enum declaration
76
                if (node.tokens.name) {
90!
77
                    node.parent.getSymbolTable()?.addSymbol(node.tokens.name.text, node.tokens.name.range, DynamicType.instance);
90!
78
                }
79
            },
80
            ClassStatement: (node) => {
81
                this.validateDeclarationLocations(node, 'class', () => util.createBoundingRange(node.classKeyword, node.name));
210✔
82

83
                //register this class
84
                if (node.name) {
210✔
85
                    node.parent.getSymbolTable()?.addSymbol(node.name.text, node.name.range, DynamicType.instance);
209!
86
                }
87
            },
88
            AssignmentStatement: (node) => {
89
                //register this variable
90
                if (node.name) {
479!
91
                    node.parent.getSymbolTable()?.addSymbol(node.name.text, node.name.range, DynamicType.instance);
479!
92
                }
93
            },
94
            DottedSetStatement: (node) => {
95
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
24✔
96
            },
97
            IndexedSetStatement: (node) => {
98
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
20✔
99
            },
100
            ForEachStatement: (node) => {
101
                //register the for loop variable
102
                node.parent.getSymbolTable()?.addSymbol(node.item.text, node.item.range, DynamicType.instance);
10!
103
            },
104
            NamespaceStatement: (node) => {
105
                this.validateDeclarationLocations(node, 'namespace', () => util.createBoundingRange(node.keyword, node.nameExpression));
218✔
106

107
                if (node.name) {
218!
108
                    node.parent.getSymbolTable().addSymbol(
218✔
109
                        node.name.split('.')[0],
110
                        node.nameExpression.range,
111
                        DynamicType.instance
112
                    );
113
                }
114
            },
115
            FunctionStatement: (node) => {
116
                this.validateDeclarationLocations(node, 'function', () => util.createBoundingRange(node.func.functionType, node.name));
1,017✔
117
                if (node.name?.text) {
1,017✔
118
                    node.parent.getSymbolTable().addSymbol(
1,016✔
119
                        node.name.text,
120
                        node.name.range,
121
                        DynamicType.instance
122
                    );
123
                }
124

125
                const namespace = node.findAncestor(isNamespaceStatement);
1,017✔
126
                //this function is declared inside a namespace
127
                if (namespace) {
1,017✔
128
                    //add the transpiled name for namespaced functions to the root symbol table
129
                    const transpiledNamespaceFunctionName = node.getName(ParseMode.BrightScript);
97✔
130
                    const funcType = node.func.getFunctionType();
97✔
131
                    funcType.setName(transpiledNamespaceFunctionName);
97✔
132

133
                    if (node.name) {
97✔
134
                        this.event.file.parser.ast.symbolTable.addSymbol(
96✔
135
                            transpiledNamespaceFunctionName,
136
                            node.name.range,
137
                            funcType
138
                        );
139
                    }
140
                }
141
            },
142
            FunctionExpression: (node) => {
143
                if (!node.symbolTable.hasSymbol('m')) {
1,180✔
144
                    node.symbolTable.addSymbol('m', undefined, DynamicType.instance);
1,157✔
145
                }
146
                this.validateFunctionParameterCount(node);
1,180✔
147
            },
148
            FunctionParameterExpression: (node) => {
149
                if (node.name) {
563!
150
                    const paramName = node.name.text;
563✔
151
                    const symbolTable = node.getSymbolTable();
563✔
152
                    symbolTable?.addSymbol(paramName, node.name.range, node.type);
563!
153
                }
154
            },
155
            InterfaceStatement: (node) => {
156
                this.validateDeclarationLocations(node, 'interface', () => util.createBoundingRange(node.tokens.interface, node.tokens.name));
38✔
157
                if (node.tokens.name) {
38!
158
                    node.parent?.getSymbolTable()?.addSymbol(node.tokens.name.text, node.tokens.name.range, new InterfaceType(new Map()));
38!
159
                }
160
            },
161
            ConstStatement: (node) => {
162
                this.validateDeclarationLocations(node, 'const', () => util.createBoundingRange(node.tokens.const, node.tokens.name));
96✔
163
                if (node.tokens.name) {
96!
164
                    node.parent.getSymbolTable().addSymbol(node.tokens.name.text, node.tokens.name.range, DynamicType.instance);
96✔
165
                }
166
            },
167
            CatchStatement: (node) => {
168
                node.parent.getSymbolTable().addSymbol(node.exceptionVariable.text, node.exceptionVariable.range, DynamicType.instance);
4✔
169
            },
170
            DimStatement: (node) => {
171
                if (node.identifier) {
18!
172
                    node.parent.getSymbolTable().addSymbol(node.identifier.text, node.identifier.range, DynamicType.instance);
18✔
173
                }
174
            },
175
            ReturnStatement: (node) => {
176
                const func = node.findAncestor<FunctionExpression>(isFunctionExpression);
132✔
177
                //these situations cannot have a value next to `return`
178
                if (
132✔
179
                    //`function as void`, `sub as void`
180
                    func?.returnTypeToken?.kind === TokenKind.Void ||
1,073!
181
                    //`sub` <without return value>
182
                    (func.functionType?.kind === TokenKind.Sub && !func.returnTypeToken)
375!
183
                ) {
184
                    //there may not be a return value
185
                    if (node.value) {
16✔
186
                        this.event.file.addDiagnostic({
15✔
187
                            ...DiagnosticMessages.voidFunctionMayNotReturnValue(func.functionType?.text),
45!
188
                            range: node.range
189
                        });
190
                    }
191

192
                } else {
193
                    //there MUST be a return value
194
                    if (!node.value) {
116✔
195
                        this.event.file.addDiagnostic({
18✔
196
                            ...DiagnosticMessages.nonVoidFunctionMustReturnValue(func?.functionType?.text),
108!
197
                            range: node.range
198
                        });
199
                    }
200
                }
201
            },
202
            ContinueStatement: (node) => {
203
                this.validateContinueStatement(node);
8✔
204
            },
205
            VariableExpression: (node) => {
206
                //flag reserved unreferencable builtins (e.g. `ObjFun`, `type`) used in non-call position.
207
                //these compile cleanly as values today but are device compile errors
208
                //(`Syntax Error. Builtin function call expected`).
209
                const name = node.name?.text;
1,234!
210
                if (
1,234✔
211
                    name &&
2,581✔
212
                    UnreferencableBuiltins.has(name.toLowerCase()) &&
213
                    //only valid use is as the callee of a CallExpression
214
                    !(isCallExpression(node.parent) && node.parent.callee === node)
213✔
215
                ) {
216
                    this.event.file.addDiagnostic({
15✔
217
                        ...DiagnosticMessages.reservedBuiltinUsedAsValue(name),
218
                        range: node.name.range
219
                    });
220
                }
221
            }
222
        });
223

224
        this.event.file.ast.walk((node, parent) => {
1,097✔
225
            visitor(node, parent);
12,048✔
226
        }, {
227
            walkMode: WalkMode.visitAllRecursive
228
        });
229
    }
230

231
    /**
232
     * Validate that a statement is defined in one of these specific locations
233
     *  - the root of the AST
234
     *  - inside a namespace
235
     * This is applicable to things like FunctionStatement, ClassStatement, NamespaceStatement, EnumStatement, InterfaceStatement
236
     */
237
    private validateDeclarationLocations(statement: Statement, keyword: string, rangeFactory?: () => (Range | undefined)) {
238
        //if nested inside a namespace, or defined at the root of the AST (i.e. in a body that has no parent)
239
        if (isNamespaceStatement(statement.parent?.parent) || (isBody(statement.parent) && !statement.parent?.parent)) {
1,669!
240
            return;
1,656✔
241
        }
242
        //the statement was defined in the wrong place. Flag it.
243
        this.event.file.addDiagnostic({
13✔
244
            ...DiagnosticMessages.keywordMustBeDeclaredAtNamespaceLevel(keyword),
245
            range: rangeFactory?.() ?? statement.range
78!
246
        });
247
    }
248

249
    private validateFunctionParameterCount(func: FunctionExpression) {
250
        if (func.parameters.length > CallExpression.MaximumArguments) {
1,180✔
251
            //flag every parameter over the limit
252
            for (let i = CallExpression.MaximumArguments; i < func.parameters.length; i++) {
2✔
253
                this.event.file.addDiagnostic({
3✔
254
                    ...DiagnosticMessages.tooManyCallableParameters(func.parameters.length, CallExpression.MaximumArguments),
255
                    range: func.parameters[i].name.range
256
                });
257
            }
258
        }
259
    }
260

261
    private validateEnumDeclaration(stmt: EnumStatement) {
262
        const members = stmt.getMembers();
90✔
263
        //the enum data type is based on the first member value
264
        const enumValueKind = (members.find(x => x.value)?.value as LiteralExpression)?.token?.kind ?? TokenKind.IntegerLiteral;
107✔
265
        const memberNames = new Set<string>();
90✔
266
        for (const member of members) {
90✔
267
            const memberNameLower = member.name?.toLowerCase();
146!
268

269
            /**
270
             * flag duplicate member names
271
             */
272
            if (memberNames.has(memberNameLower)) {
146✔
273
                this.event.file.addDiagnostic({
1✔
274
                    ...DiagnosticMessages.duplicateIdentifier(member.name),
275
                    range: member.range
276
                });
277
            } else {
278
                memberNames.add(memberNameLower);
145✔
279
            }
280

281
            //Enforce all member values are the same type
282
            this.validateEnumValueTypes(member, enumValueKind);
146✔
283
        }
284
    }
285

286
    private validateEnumValueTypes(member: EnumMemberStatement, enumValueKind: TokenKind) {
287
        let memberValueKind: TokenKind;
288
        let memberValue: Expression;
289
        if (isUnaryExpression(member.value)) {
146✔
290
            memberValueKind = (member.value?.right as LiteralExpression)?.token?.kind;
2!
291
            memberValue = member.value?.right;
2!
292
        } else {
293
            memberValueKind = (member.value as LiteralExpression)?.token?.kind;
144✔
294
            memberValue = member.value;
144✔
295
        }
296
        const range = (memberValue ?? member)?.range;
146!
297
        if (
146✔
298
            //is integer enum, has value, that value type is not integer
299
            (enumValueKind === TokenKind.IntegerLiteral && memberValueKind && memberValueKind !== enumValueKind) ||
476✔
300
            //has value, that value is not a literal
301
            (memberValue && !isLiteralExpression(memberValue))
302
        ) {
303
            this.event.file.addDiagnostic({
5✔
304
                ...DiagnosticMessages.enumValueMustBeType(
305
                    enumValueKind.replace(/literal$/i, '').toLowerCase()
306
                ),
307
                range: range
308
            });
309
        }
310

311
        //is non integer value
312
        if (enumValueKind !== TokenKind.IntegerLiteral) {
146✔
313
            //default value present
314
            if (memberValueKind) {
72✔
315
                //member value is same as enum
316
                if (memberValueKind !== enumValueKind) {
70✔
317
                    this.event.file.addDiagnostic({
1✔
318
                        ...DiagnosticMessages.enumValueMustBeType(
319
                            enumValueKind.replace(/literal$/i, '').toLowerCase()
320
                        ),
321
                        range: range
322
                    });
323
                }
324

325
                //default value missing
326
            } else {
327
                this.event.file.addDiagnostic({
2✔
328
                    file: this.event.file,
329
                    ...DiagnosticMessages.enumValueIsRequired(
330
                        enumValueKind.replace(/literal$/i, '').toLowerCase()
331
                    ),
332
                    range: range
333
                });
334
            }
335
        }
336
    }
337

338
    /**
339
     * Find statements defined at the top level (or inside a namespace body) that are not allowed to be there
340
     */
341
    private flagTopLevelStatements() {
342
        const statements = [...this.event.file.ast.statements];
1,097✔
343
        while (statements.length > 0) {
1,097✔
344
            const statement = statements.pop();
1,728✔
345
            if (isNamespaceStatement(statement)) {
1,728✔
346
                statements.push(...statement.body.statements);
215✔
347
            } else {
348
                //only allow these statement types
349
                if (
1,513✔
350
                    !isFunctionStatement(statement) &&
3,099✔
351
                    !isClassStatement(statement) &&
352
                    !isEnumStatement(statement) &&
353
                    !isInterfaceStatement(statement) &&
354
                    !isCommentStatement(statement) &&
355
                    !isLibraryStatement(statement) &&
356
                    !isImportStatement(statement) &&
357
                    !isConstStatement(statement) &&
358
                    !isTypecastStatement(statement) &&
359
                    !isAliasStatement(statement) &&
360
                    !isTypeStatement(statement)
361
                ) {
362
                    this.event.file.addDiagnostic({
8✔
363
                        ...DiagnosticMessages.unexpectedStatementOutsideFunction(),
364
                        range: statement.range
365
                    });
366
                }
367
            }
368
        }
369
    }
370

371
    private validateImportStatements() {
372
        let topOfFileIncludeStatements = [] as Array<LibraryStatement | ImportStatement>;
1,096✔
373
        for (let stmt of this.event.file.parser.ast.statements) {
1,096✔
374
            //skip comments
375
            if (isCommentStatement(stmt)) {
1,060✔
376
                continue;
16✔
377
            }
378
            //if we found a non-library statement, this statement is not at the top of the file
379
            if (isLibraryStatement(stmt) || isImportStatement(stmt)) {
1,044✔
380
                topOfFileIncludeStatements.push(stmt);
27✔
381
            } else {
382
                //break out of the loop, we found all of our library statements
383
                break;
1,017✔
384
            }
385
        }
386

387
        let statements = [
1,096✔
388
            // eslint-disable-next-line @typescript-eslint/dot-notation
389
            ...this.event.file['_parser'].references.libraryStatements,
390
            // eslint-disable-next-line @typescript-eslint/dot-notation
391
            ...this.event.file['_parser'].references.importStatements
392
        ];
393
        for (let result of statements) {
1,096✔
394
            //if this statement is not one of the top-of-file statements,
395
            //then add a diagnostic explaining that it is invalid
396
            if (!topOfFileIncludeStatements.includes(result)) {
30✔
397
                if (isLibraryStatement(result)) {
3✔
398
                    this.event.file.diagnostics.push({
2✔
399
                        ...DiagnosticMessages.libraryStatementMustBeDeclaredAtTopOfFile(),
400
                        range: result.range,
401
                        file: this.event.file
402
                    });
403
                } else if (isImportStatement(result)) {
1!
404
                    this.event.file.diagnostics.push({
1✔
405
                        ...DiagnosticMessages.importStatementMustBeDeclaredAtTopOfFile(),
406
                        range: result.range,
407
                        file: this.event.file
408
                    });
409
                }
410
            }
411
        }
412
    }
413

414
    private validateContinueStatement(statement: ContinueStatement) {
415
        const validateLoopTypeMatch = (expectedLoopType: TokenKind) => {
8✔
416
            //coerce ForEach to For
417
            expectedLoopType = expectedLoopType === TokenKind.ForEach ? TokenKind.For : expectedLoopType;
7✔
418
            const actualLoopType = statement.tokens.loopType;
7✔
419
            if (actualLoopType && expectedLoopType?.toLowerCase() !== actualLoopType.text?.toLowerCase()) {
7!
420
                this.event.file.addDiagnostic({
3✔
421
                    range: statement.tokens.loopType.range,
422
                    ...DiagnosticMessages.expectedToken(expectedLoopType)
423
                });
424
            }
425
        };
426

427
        //find the parent loop statement
428
        const parent = statement.findAncestor<WhileStatement | ForStatement | ForEachStatement>((node) => {
8✔
429
            if (isWhileStatement(node)) {
18✔
430
                validateLoopTypeMatch(node.tokens.while.kind);
3✔
431
                return true;
3✔
432
            } else if (isForStatement(node)) {
15✔
433
                validateLoopTypeMatch(node.forToken.kind);
3✔
434
                return true;
3✔
435
            } else if (isForEachStatement(node)) {
12✔
436
                validateLoopTypeMatch(node.tokens.forEach.kind);
1✔
437
                return true;
1✔
438
            }
439
        });
440
        //flag continue statements found outside of a loop
441
        if (!parent) {
8✔
442
            this.event.file.addDiagnostic({
1✔
443
                range: statement.range,
444
                ...DiagnosticMessages.illegalContinueStatement()
445
            });
446
        }
447
    }
448

449
    /**
450
     * Validate that there are no optional chaining operators on the left-hand-side of an assignment, indexed set, or dotted get
451
     */
452
    private validateNoOptionalChainingInVarSet(parent: AstNode, children: AstNode[]) {
453
        const nodes = [...children, parent];
44✔
454
        //flag optional chaining anywhere in the left of this statement
455
        while (nodes.length > 0) {
44✔
456
            const node = nodes.shift();
88✔
457
            if (
88✔
458
                // a?.b = true or a.b?.c = true
459
                ((isDottedSetStatement(node) || isDottedGetExpression(node)) && node.dot?.kind === TokenKind.QuestionDot) ||
480!
460
                // a.b?[2] = true
461
                (isIndexedGetExpression(node) && (node?.questionDotToken?.kind === TokenKind.QuestionDot || node.openingSquare?.kind === TokenKind.QuestionLeftSquare)) ||
27!
462
                // a?[1] = true
463
                (isIndexedSetStatement(node) && node.openingSquare?.kind === TokenKind.QuestionLeftSquare)
60!
464
            ) {
465
                //try to highlight the entire left-hand-side expression if possible
466
                let range: Range;
467
                if (isDottedSetStatement(parent)) {
8✔
468
                    range = util.createBoundingRange(parent.obj, parent.dot, parent.name);
5✔
469
                } else if (isIndexedSetStatement(parent)) {
3!
470
                    range = util.createBoundingRange(parent.obj, parent.openingSquare, parent.index, parent.closingSquare);
3✔
471
                } else {
472
                    range = node.range;
×
473
                }
474

475
                this.event.file.addDiagnostic({
8✔
476
                    ...DiagnosticMessages.noOptionalChainingInLeftHandSideOfAssignment(),
477
                    range: range
478
                });
479
            }
480

481
            if (node === parent) {
88✔
482
                break;
44✔
483
            } else {
484
                nodes.push(node.parent);
44✔
485
            }
486
        }
487
    }
488

489
    /**
490
     * Add a diagnostic if the configured minFirmwareVersion is lower than the version that
491
     * introduced optional chaining support (Roku OS 11).
492
     * This applies to both .brs and .bs files because optional chaining is not transpiled —
493
     * it is emitted as-is, so the target device must natively support it.
494
     */
495
    private validateMinFirmwareVersionForOptionalChaining(range: Range | undefined) {
496
        const minFirmwareVersion = this.event.file.program.getMinFirmwareVersion();
37✔
497
        if (semver.lt(minFirmwareVersion, OPTIONAL_CHAINING_MIN_FIRMWARE_VERSION)) {
37✔
498
            this.event.file.addDiagnostic({
4✔
499
                ...DiagnosticMessages.featureRequiresMinFirmwareVersion(
500
                    'optional chaining',
501
                    OPTIONAL_CHAINING_MIN_FIRMWARE_VERSION,
502
                    minFirmwareVersion
503
                ),
504
                range: range
505
            });
506
        }
507
    }
508

509
    /**
510
     * For a bare top-level call to a known global callable, fire one deprecation/removal
511
     * diagnostic driven by `callable.availability`. The rsg axis takes precedence: if it
512
     * fires, the os axis is skipped entirely. The os axis is only consulted when rsg is
513
     * silent (rsg axis not configured, or effective rsg below its thresholds).
514
     *
515
     * Skips method calls (`m.foo()`) and namespaced calls (`alpha.foo()`) — only the bare
516
     * top-level builtin form resolves to a global callable.
517
     */
518
    private validateGlobalCallableAvailability(node: CallExpression) {
519
        if (!isVariableExpression(node.callee)) {
487✔
520
            return;
210✔
521
        }
522
        const calleeName = node.callee.name?.text;
277!
523
        if (!calleeName) {
277!
NEW
524
            return;
×
525
        }
526
        const callable = globalCallableMap.get(calleeName.toLowerCase());
277✔
527
        const availability = callable?.availability;
277✔
528
        if (!availability) {
277✔
529
            return;
268✔
530
        }
531
        const rsgDiagnostic = this.computeAvailabilityDiagnostic(calleeName, 'rsg', availability.rsg, this.event.file.program.getRsgVersion());
9✔
532
        const diagnostic = rsgDiagnostic ??
9✔
533
            this.computeAvailabilityDiagnostic(calleeName, 'os', availability.os, this.event.file.program.getMinFirmwareVersion());
534
        if (diagnostic) {
9✔
535
            this.event.file.addDiagnostic({
8✔
536
                ...diagnostic,
537
                range: node.callee.range
538
            });
539
        }
540
    }
541

542
    /**
543
     * Compute (but don't emit) the diagnostic for one axis of {@link Availability}: returns
544
     * `globalCallableRemoved` if the project's effective version is at/past the axis's
545
     * `removed` threshold, otherwise `globalCallableDeprecated` if it's at/past `deprecated`,
546
     * otherwise `undefined`.
547
     *
548
     * `effectiveVersion` is expected in canonical semver form (program getters guarantee this);
549
     * availability constants are authored in canonical form too, so no coercion is needed here.
550
     */
551
    private computeAvailabilityDiagnostic(calleeName: string, axis: AvailabilityAxis, info: { added?: string; deprecated?: string; removed?: string } | undefined, effectiveVersion: string) {
552
        if (!info) {
11!
NEW
553
            return undefined;
×
554
        }
555
        if (info.removed && semver.gte(effectiveVersion, info.removed)) {
11✔
556
            return DiagnosticMessages.globalCallableRemoved(calleeName, axis, info.removed, effectiveVersion);
7✔
557
        }
558
        if (info.deprecated && semver.gte(effectiveVersion, info.deprecated)) {
4✔
559
            return DiagnosticMessages.globalCallableDeprecated(calleeName, axis, info.deprecated, effectiveVersion);
1✔
560
        }
561
        return undefined;
3✔
562
    }
563
}
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