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

rokucommunity / bslint / #1405

10 Dec 2025 08:12PM UTC coverage: 91.345% (-0.2%) from 91.556%
#1405

push

web-flow
Merge 0a6df2897 into fcd0578a6

993 of 1134 branches covered (87.57%)

Branch coverage included in aggregate %.

2 of 2 new or added lines in 1 file covered. (100.0%)

51 existing lines in 9 files now uncovered.

1065 of 1119 relevant lines covered (95.17%)

72.63 hits per line

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

91.27
/src/plugins/codeStyle/index.ts
1
import {
1✔
2
    BsDiagnostic,
3
    createVisitor,
4
    FunctionExpression,
5
    isBrsFile,
6
    isXmlFile,
7
    isGroupingExpression,
8
    TokenKind,
9
    WalkMode,
10
    CancellationTokenSource,
11
    DiagnosticSeverity,
12
    OnGetCodeActionsEvent,
13
    AALiteralExpression,
14
    BrsFile,
15
    AfterFileValidateEvent,
16
    Expression,
17
    isVoidType,
18
    Statement,
19
    SymbolTypeFlag,
20
    XmlFile,
21
    AstNode,
22
    Token,
23
    isNamespaceStatement,
24
    util,
25
    isAnyReferenceType,
26
    ExtraSymbolData,
27
    OnScopeValidateEvent,
28
    InternalWalkMode,
29
    isCallableType,
30
    AssignmentStatement,
31
    isFunctionExpression,
32
    isDynamicType,
33
    Plugin,
34
    CallExpression,
35
    isForEachStatement,
36
    isForStatement,
37
    isIfStatement,
38
    isLiteralExpression,
39
    isVariableExpression,
40
    isWhileStatement,
41
    isCallExpression
42
} from 'brighterscript';
43
import { RuleAAComma } from '../..';
44
import { addFixesToEvent } from '../../textEdit';
1✔
45
import { PluginContext } from '../../util';
46
import { createColorValidator } from '../../createColorValidator';
1✔
47
import { messages } from './diagnosticMessages';
1✔
48
import { extractFixes } from './styleFixes';
1✔
49
import { BsLintDiagnosticContext } from '../../Linter';
1✔
50
import { Location } from 'vscode-languageserver-types';
51

52
export default class CodeStyle implements Plugin {
1✔
53

54
    name = 'bslint-codeStyle';
162✔
55

56
    constructor(private lintContext: PluginContext) {
162✔
57
    }
58

59
    onGetCodeActions(event: OnGetCodeActionsEvent) {
UNCOV
60
        const addFixes = addFixesToEvent(event);
×
UNCOV
61
        extractFixes(event.file, addFixes, event.diagnostics);
×
62
    }
63

64
    validateXMLFile(file: XmlFile) {
65
        const diagnostics: BsDiagnostic[] = [];
2✔
66
        const { noArrayComponentFieldType, noAssocarrayComponentFieldType } = this.lintContext.severity;
2✔
67

68
        const validateArrayComponentFieldType = noArrayComponentFieldType !== DiagnosticSeverity.Hint;
2✔
69
        const validateAssocarrayComponentFieldType = noAssocarrayComponentFieldType !== DiagnosticSeverity.Hint;
2✔
70

71
        for (const field of file.parser?.ast?.componentElement?.interfaceElement?.fields ?? []) {
2!
72
            if (field.tokens.startTagName?.text?.toLowerCase() === 'field') {
10!
73
                const typeAttribute = field.getAttribute('type');
10✔
74

75
                const typeValue = typeAttribute?.tokens?.value?.text?.toLowerCase();
10!
76
                if (typeValue === 'array' && validateArrayComponentFieldType) {
10✔
77
                    diagnostics.push(
2✔
78
                        messages.noArrayFieldType(
79
                            typeAttribute?.tokens?.value?.location,
18!
80
                            noArrayComponentFieldType
81
                        )
82
                    );
83
                } else if (typeValue === 'assocarray' && validateAssocarrayComponentFieldType) {
8✔
84
                    diagnostics.push(
2✔
85
                        messages.noAssocarrayFieldType(
86
                            typeAttribute?.tokens?.value?.location,
18!
87
                            noAssocarrayComponentFieldType
88
                        )
89
                    );
90
                }
91
            }
92
        }
93

94
        return diagnostics;
2✔
95
    }
96

97
    validateBrsFile(file: BrsFile) {
98
        const diagnostics: (BsDiagnostic)[] = [];
71✔
99
        const { severity } = this.lintContext;
71✔
100
        const { inlineIfStyle, blockIfStyle, conditionStyle, noPrint, noTodo, noStop, aaCommaStyle, eolLast, colorFormat, noRegexDuplicates } = severity;
71✔
101
        const validatePrint = noPrint !== DiagnosticSeverity.Hint;
71✔
102
        const validateTodo = noTodo !== DiagnosticSeverity.Hint;
71✔
103
        const validateNoStop = noStop !== DiagnosticSeverity.Hint;
71✔
104
        const validateNoRegexDuplicates = noRegexDuplicates !== DiagnosticSeverity.Hint;
71✔
105
        const validateInlineIf = inlineIfStyle !== 'off';
71✔
106
        const validateColorFormat = (colorFormat === 'hash-hex' || colorFormat === 'quoted-numeric-hex' || colorFormat === 'never');
71✔
107
        const disallowInlineIf = inlineIfStyle === 'never';
71✔
108
        const requireInlineIfThen = inlineIfStyle === 'then';
71✔
109
        const validateBlockIf = blockIfStyle !== 'off';
71✔
110
        const requireBlockIfThen = blockIfStyle === 'then';
71✔
111
        const validateCondition = conditionStyle !== 'off';
71✔
112
        const requireConditionGroup = conditionStyle === 'group';
71✔
113
        const validateAAStyle = aaCommaStyle !== 'off';
71✔
114
        const validateEolLast = eolLast !== 'off';
71✔
115
        const disallowEolLast = eolLast === 'never';
71✔
116
        const validateColorStyle = validateColorFormat ? createColorValidator(severity) : undefined;
71✔
117

118
        // Check if the file is empty by going backwards from the last token,
119
        // meaning there are tokens other than `Eof` and `Newline`.
120
        const { tokens } = file.parser;
71✔
121
        let isFileEmpty = true;
71✔
122
        for (let i = tokens.length - 1; i >= 0; i--) {
71✔
123
            if (tokens[i].kind !== TokenKind.Eof &&
201✔
124
                tokens[i].kind !== TokenKind.Newline) {
125
                isFileEmpty = false;
70✔
126
                break;
70✔
127
            }
128
        }
129

130
        // Validate `eol-last` on non-empty files
131
        if (validateEolLast && !isFileEmpty) {
71✔
132
            const penultimateToken = tokens[tokens.length - 2];
47✔
133
            if (disallowEolLast) {
47✔
134
                if (penultimateToken?.kind === TokenKind.Newline) {
2!
135
                    diagnostics.push(messages.removeEolLast(penultimateToken.location));
2✔
136
                }
137
            } else if (penultimateToken?.kind !== TokenKind.Newline) {
45!
138
                // Set the preferredEol as the last newline.
139
                // The fix function will handle the case where preferredEol is undefined.
140
                // This could happen in valid single line files, like:
141
                // `sub foo() end sub\EOF`
142
                let preferredEol;
143
                for (let i = tokens.length - 1; i >= 0; i--) {
3✔
144
                    if (tokens[i].kind === TokenKind.Newline) {
38✔
145
                        preferredEol = tokens[i].text;
4✔
146
                    }
147
                }
148

149
                diagnostics.push(
3✔
150
                    messages.addEolLast(
151
                        penultimateToken.location,
152
                        preferredEol
153
                    )
154
                );
155
            }
156
        }
157

158
        if (validateNoRegexDuplicates) {
71✔
159
            this.validateRegex(file, diagnostics, noRegexDuplicates);
3✔
160
        }
161

162
        file.ast.walk(createVisitor({
71✔
163
            // validate function style (`function` or `sub`)
164
            FunctionExpression: (func) => {
165
                this.validateFunctionStyle(func, diagnostics);
176✔
166
            },
167
            IfStatement: s => {
168
                const hasThenToken = !!s.tokens.then;
78✔
169
                if (!s.isInline && validateBlockIf) {
78✔
170
                    if (hasThenToken !== requireBlockIfThen) {
16✔
171
                        diagnostics.push(requireBlockIfThen
7✔
172
                            ? messages.addBlockIfThenKeyword(s)
173
                            : messages.removeBlockIfThenKeyword(s)
174
                        );
175
                    }
176
                } else if (s.isInline && validateInlineIf) {
62✔
177
                    if (disallowInlineIf) {
14✔
178
                        diagnostics.push(messages.inlineIfNotAllowed(s.location));
2✔
179
                    } else if (hasThenToken !== requireInlineIfThen) {
12✔
180
                        diagnostics.push(requireInlineIfThen
6✔
181
                            ? messages.addInlineIfThenKeyword(s)
182
                            : messages.removeInlineIfThenKeyword(s)
183
                        );
184
                    }
185
                }
186

187
                if (validateCondition) {
78✔
188
                    if (isGroupingExpression(s.condition) !== requireConditionGroup) {
24✔
189
                        diagnostics.push(requireConditionGroup
11✔
190
                            ? messages.addParenthesisAroundCondition(s)
191
                            : messages.removeParenthesisAroundCondition(s)
192
                        );
193
                    }
194
                }
195
            },
196
            WhileStatement: s => {
197
                if (validateCondition) {
12✔
198
                    if (isGroupingExpression(s.condition) !== requireConditionGroup) {
6✔
199
                        diagnostics.push(requireConditionGroup
3✔
200
                            ? messages.addParenthesisAroundCondition(s)
201
                            : messages.removeParenthesisAroundCondition(s)
202
                        );
203
                    }
204
                }
205
            },
206
            PrintStatement: s => {
207
                if (validatePrint) {
124✔
208
                    diagnostics.push(messages.noPrint(s.tokens.print.location, noPrint));
2✔
209
                }
210
            },
211
            LiteralExpression: e => {
212
                if (validateColorStyle && e.tokens.value.kind === TokenKind.StringLiteral) {
492✔
213
                    validateColorStyle(e.tokens.value.text, e.tokens.value.location, diagnostics);
63✔
214
                }
215
            },
216
            TemplateStringExpression: e => {
217
                // only validate template strings that look like regular strings (i.e. `0xAABBCC`)
218
                if (validateColorStyle && e.quasis.length === 1 && e.quasis[0].expressions.length === 1) {
11✔
219
                    validateColorStyle(e.quasis[0].expressions[0].tokens.value.text, e.quasis[0].expressions[0].tokens.value.location, diagnostics);
6✔
220
                }
221
            },
222
            AALiteralExpression: e => {
223
                if (validateAAStyle) {
36✔
224
                    this.validateAAStyle(e, aaCommaStyle, diagnostics);
33✔
225
                }
226
            },
227
            StopStatement: s => {
228
                if (validateNoStop) {
1!
229
                    diagnostics.push(messages.noStop(s.tokens.stop.location, noStop));
1✔
230
                }
231
            },
232
            AstNode: (node: Statement | Expression) => {
233
                const comments = [...node.leadingTrivia ?? [], ...node.endTrivia ?? []].filter(t => t.kind === TokenKind.Comment);
3,522!
234
                if (validateTodo && comments.length > 0) {
2,235✔
235
                    for (const e of comments) {
6✔
236
                        if (this.lintContext.todoPattern.test(e.text)) {
26✔
237
                            diagnostics.push(messages.noTodo(e.location, noTodo));
10✔
238
                        }
239
                    }
240
                }
241
            }
242
        }), { walkMode: WalkMode.visitAllRecursive });
243

244
        return diagnostics;
71✔
245
    }
246

247
    validateBrsFileInScope(file: BrsFile) {
248
        const diagnostics: (BsDiagnostic)[] = [];
70✔
249
        const { severity } = this.lintContext;
70✔
250
        const { nameShadowing, typeReassignment } = severity;
70✔
251

252
        file.ast.walk(createVisitor({
70✔
253
            NamespaceStatement: (nsStmt) => {
254
                this.validateNameShadowing(file, nsStmt, nsStmt.getNameParts()?.[0], nameShadowing, diagnostics);
4!
255
            },
256
            ClassStatement: (classStmt) => {
257
                this.validateNameShadowing(file, classStmt, classStmt.tokens.name, nameShadowing, diagnostics);
9✔
258
            },
259
            InterfaceStatement: (ifaceStmt) => {
260
                this.validateNameShadowing(file, ifaceStmt, ifaceStmt.tokens.name, nameShadowing, diagnostics);
10✔
261
            },
262
            ConstStatement: (constStmt) => {
263
                this.validateNameShadowing(file, constStmt, constStmt.tokens.name, nameShadowing, diagnostics);
11✔
264
            },
265
            EnumStatement: (enumStmt) => {
266
                this.validateNameShadowing(file, enumStmt, enumStmt.tokens.name, nameShadowing, diagnostics);
7✔
267
            },
268
            AssignmentStatement: (assignStmt) => {
269
                this.validateTypeReassignment(file, assignStmt, typeReassignment, diagnostics);
129✔
270
            }
271
            // eslint-disable-next-line no-bitwise
272
        }), { walkMode: WalkMode.visitStatementsRecursive | InternalWalkMode.visitFalseConditionalCompilationBlocks });
273

274
        return diagnostics;
70✔
275
    }
276

277
    validateRegex(file: BrsFile, diagnostics: (Omit<BsDiagnostic, 'file'>)[], severity: DiagnosticSeverity) {
278
        const regexesByFunction = new Map<FunctionExpression, Set<string>>();
3✔
279

280
        const callExpressions = file.parser.ast.findChildren<CallExpression>(node => {
3✔
281
            return isCallExpression(node) && this.isCreateObject(node);
98✔
282
        });
283
        // walk all callExpressions
284
        for (const callExpression of callExpressions) {
3✔
285
            const func = callExpression.findAncestor<FunctionExpression>(isFunctionExpression);
9✔
286
            if (!func) {
9!
UNCOV
287
                continue;
×
288
            }
289

290
            // Check if all args are literals and get them as string
291
            const callArgs = this.getLiteralArgs(callExpression.args);
9✔
292

293
            // CreateObject for roRegex expects 3 params,
294
            // they should be literals because only in this case we can guarante that call regex is the same
295
            if (callArgs?.length === 3 && callArgs[0] === 'roRegex') {
9!
296
                const parentStatement = callExpression.findAncestor((node, cancel) => {
9✔
297
                    if (isIfStatement(node)) {
27✔
298
                        cancel.cancel();
2✔
299
                    } else if (this.isLoop(node) || isFunctionExpression(node)) {
25✔
300
                        return true;
7✔
301
                    }
302
                });
303

304
                if (!regexesByFunction.has(func)) {
9✔
305
                    regexesByFunction.set(func, new Set<string>());
4✔
306
                }
307
                const regexes = regexesByFunction.get(func);
9✔
308

309
                const joinedArgs = callArgs.join();
9✔
310
                const isRegexAlreadyExist = regexes.has(joinedArgs);
9✔
311
                if (!isRegexAlreadyExist) {
9✔
312
                    regexes.add(joinedArgs);
7✔
313
                }
314

315
                if (isFunctionExpression(parentStatement)) {
9✔
316
                    if (isRegexAlreadyExist) {
5✔
317
                        diagnostics.push(messages.noRegexRedeclaring(callExpression.location, severity));
1✔
318
                    }
319
                } else if (this.isLoop(parentStatement)) {
4✔
320
                    diagnostics.push(messages.noIdenticalRegexInLoop(callExpression.location, severity));
2✔
321
                }
322
            }
323
        }
324
    }
325

326
    afterFileValidate(event: AfterFileValidateEvent) {
327
        const { file } = event;
72✔
328
        if (this.lintContext.ignores(file)) {
72!
UNCOV
329
            return;
×
330
        }
331

332
        const diagnostics: (BsDiagnostic)[] = [];
72✔
333
        if (isXmlFile(file)) {
72✔
334
            diagnostics.push(...this.validateXMLFile(file));
2✔
335
        } else if (isBrsFile(file)) {
70!
336
            diagnostics.push(...this.validateBrsFile(file));
70✔
337
        }
338

339
        // add file reference
340
        let bsDiagnostics: BsDiagnostic[] = diagnostics.map(diagnostic => ({
139✔
341
            ...diagnostic,
342
            file
343
        }));
344

345
        const { fix } = this.lintContext;
72✔
346

347
        // apply fix
348
        if (fix) {
72✔
349
            bsDiagnostics = extractFixes(event.file, this.lintContext.addFixes, bsDiagnostics);
12✔
350
        }
351

352
        // append diagnostics
353
        event.program.diagnostics.register(bsDiagnostics, BsLintDiagnosticContext);
72✔
354
    }
355

356
    onScopeValidate(event: OnScopeValidateEvent) {
357
        for (const file of event.scope.getOwnFiles()) {
71✔
358
            if (this.lintContext.ignores(file)) {
72!
UNCOV
359
                return;
×
360
            }
361

362
            const diagnostics: (BsDiagnostic)[] = [];
72✔
363
            if (isBrsFile(file)) {
72✔
364
                diagnostics.push(...this.validateBrsFileInScope(file));
70✔
365
            }
366

367
            // add file reference
368
            let bsDiagnostics: BsDiagnostic[] = diagnostics.map(diagnostic => ({
72✔
369
                ...diagnostic,
370
                file
371
            }));
372

373
            const { fix } = this.lintContext;
72✔
374

375
            // apply fix
376
            if (fix) {
72✔
377
                bsDiagnostics = extractFixes(file, this.lintContext.addFixes, bsDiagnostics);
12✔
378
            }
379

380
            // append diagnostics
381
            event.program.diagnostics.register(bsDiagnostics, BsLintDiagnosticContext);
72✔
382
        }
383
    }
384

385
    validateAAStyle(aa: AALiteralExpression, aaCommaStyle: RuleAAComma, diagnostics: (BsDiagnostic)[]) {
386
        const indexes = collectWrappingAAMembersIndexes(aa);
33✔
387
        const last = indexes.length - 1;
33✔
388
        const isSingleLine = (aa: AALiteralExpression): boolean => {
33✔
389
            return aa.tokens.open.location.range.start.line === aa.tokens.close.location.range.end.line;
10✔
390
        };
391

392
        indexes.forEach((index, i) => {
33✔
393
            const member = aa.elements[index];
69✔
394
            const hasComma = !!member.tokens.comma;
69✔
395
            if (aaCommaStyle === 'never' || (i === last && ((aaCommaStyle === 'no-dangling') || isSingleLine(aa)))) {
69✔
396
                if (hasComma) {
39✔
397
                    diagnostics.push(messages.removeAAComma(member.tokens.comma.location));
18✔
398
                }
399
            } else if (!hasComma) {
30✔
400
                diagnostics.push(messages.addAAComma(member.value.location));
12✔
401
            }
402
        });
403
    }
404

405
    validateFunctionStyle(fun: FunctionExpression, diagnostics: (BsDiagnostic)[]) {
406
        const { severity } = this.lintContext;
176✔
407
        const { namedFunctionStyle, anonFunctionStyle, typeAnnotations } = severity;
176✔
408
        const style = fun.parent ? namedFunctionStyle : anonFunctionStyle;
176!
409
        const kind = fun.tokens.functionType.kind;
176✔
410
        const hasReturnedValue = style === 'auto' || typeAnnotations !== 'off' ? this.getFunctionReturns(fun) : false;
176✔
411

412
        // type annotations
413
        if (typeAnnotations !== 'off') {
176✔
414
            const needsArgType = typeAnnotations.startsWith('args') || typeAnnotations.startsWith('all');
22✔
415
            const needsReturnType = typeAnnotations.startsWith('return') || typeAnnotations.startsWith('all');
22✔
416
            const allowImplicit = typeAnnotations.includes('allow-implicit');
22✔
417

418
            if (needsReturnType) {
22✔
419
                if (hasReturnedValue && !fun.returnTypeExpression) {
13✔
420
                    diagnostics.push(messages.expectedReturnTypeAnnotation(
4✔
421
                        // add the error to the function keyword (or just highlight the whole function if that's somehow missing)
422
                        fun.tokens.functionType?.location ?? fun.location
24!
423
                    ));
424
                }
425
            }
426
            if (needsArgType) {
22✔
427
                const missingAnnotation = fun.parameters.find(arg => {
18✔
428
                    if (!arg.typeExpression) {
26✔
429
                        if (allowImplicit && arg.defaultValue) {
14✔
430
                            return false;
4✔
431
                        }
432
                        return true;
10✔
433
                    }
434
                    return false;
12✔
435
                });
436
                if (missingAnnotation) {
18✔
437
                    // only report 1st missing arg annotation to avoid error overload
438
                    diagnostics.push(messages.expectedTypeAnnotation(missingAnnotation.location));
10✔
439
                }
440
            }
441
        }
442

443
        // keyword style
444
        if (style === 'off') {
176✔
445
            return;
69✔
446
        }
447
        if (style === 'no-function') {
107✔
448
            if (kind === TokenKind.Function) {
12✔
449
                diagnostics.push(messages.expectedSubKeyword(fun, `(always use 'sub')`));
6✔
450
            }
451
            return;
12✔
452
        }
453

454
        if (style === 'no-sub') {
95✔
455
            if (kind === TokenKind.Sub) {
11✔
456
                diagnostics.push(messages.expectedFunctionKeyword(fun, `(always use 'function')`));
5✔
457
            }
458
            return;
11✔
459
        }
460

461
        // auto
462
        if (hasReturnedValue) {
84✔
463
            if (kind !== TokenKind.Function) {
14✔
464
                diagnostics.push(messages.expectedFunctionKeyword(fun, `(use 'function' when a value is returned)`));
2✔
465
            }
466
        } else if (kind !== TokenKind.Sub) {
70✔
467
            diagnostics.push(messages.expectedSubKeyword(fun, `(use 'sub' when no value is returned)`));
4✔
468
        }
469
    }
470

471
    getFunctionReturns(fun: FunctionExpression) {
472
        let hasReturnedValue = false;
106✔
473
        if (fun.returnTypeExpression) {
106✔
474
            hasReturnedValue = !isVoidType(fun.returnTypeExpression.getType({ flags: SymbolTypeFlag.typetime }));
16✔
475
        } else {
476
            const cancel = new CancellationTokenSource();
90✔
477
            fun.body.walk(createVisitor({
90✔
478
                ReturnStatement: s => {
479
                    hasReturnedValue = !!s.value;
17✔
480
                    cancel.cancel();
17✔
481
                }
482
            }), { walkMode: WalkMode.visitStatements, cancel: cancel.token });
483
        }
484
        return hasReturnedValue;
106✔
485
    }
486

487
    validateNameShadowing(file: BrsFile, node: AstNode, nameIdentifier: Token, severity: DiagnosticSeverity, diagnostics: (BsDiagnostic)[]) {
488
        const name = nameIdentifier?.text;
41!
489
        if (!name || !node) {
41!
UNCOV
490
            return;
×
491
        }
492
        const nameLocation = nameIdentifier.location;
41✔
493

494
        const astTable = file.ast.getSymbolTable();
41✔
495
        const data = {} as ExtraSymbolData;
41✔
496
        const typeChain = [];
41✔
497
        // eslint-disable-next-line no-bitwise
498
        const existingType = astTable.getSymbolType(name, { flags: SymbolTypeFlag.runtime | SymbolTypeFlag.typetime, data: data, typeChain: typeChain });
41✔
499

500
        if (!existingType || isAnyReferenceType(existingType)) {
41!
UNCOV
501
            return;
×
502
        }
503
        if ((data.definingNode === node) || (isNamespaceStatement(data.definingNode) && isNamespaceStatement(node))) {
41✔
504
            return;
25✔
505
        }
506
        const otherNode = data.definingNode as unknown as { tokens: { name: Token }; location: Location };
16✔
507
        const thisNodeKindName = util.getAstNodeFriendlyName(node);
16✔
508
        let thatNodeKindName = util.getAstNodeFriendlyName(data.definingNode) ?? '';
16✔
509
        if (!thatNodeKindName && isCallableType(existingType)) {
16✔
510
            thatNodeKindName = 'Global Function';
1✔
511
        }
512

513
        let thatNameLocation = otherNode?.tokens?.name?.location ?? otherNode?.location;
16✔
514

515
        if (isNamespaceStatement(data.definingNode)) {
16✔
516
            thatNameLocation = data.definingNode.getNameParts()?.[0]?.location;
2!
517
        }
518

519
        const relatedInformation = thatNameLocation ? [{
16✔
520
            message: `${thatNodeKindName} declared here`,
521
            location: thatNameLocation
522
        }] : undefined;
523

524
        diagnostics.push({
16✔
525
            ...messages.nameShadowing(thisNodeKindName, thatNodeKindName, name, severity),
526
            location: nameLocation,
527
            relatedInformation: relatedInformation
528
        });
529
    }
530

531
    validateTypeReassignment(file: BrsFile, assignStmt: AssignmentStatement, severity: DiagnosticSeverity, diagnostics: (BsDiagnostic)[]) {
532
        const functionExpression = assignStmt.findAncestor<FunctionExpression>(isFunctionExpression);
129✔
533
        if (!functionExpression) {
129!
UNCOV
534
            return;
×
535
        }
536
        const rhsType = assignStmt.value?.getType({ flags: SymbolTypeFlag.runtime });
129!
537
        if (!rhsType.isResolvable()) {
129!
UNCOV
538
            return;
×
539
        }
540
        const varName = assignStmt.tokens.name.text;
129✔
541
        const previousType = assignStmt.getSymbolTable().getSymbolType(varName, {
129✔
542
            flags: SymbolTypeFlag.runtime,
543
            statementIndex: assignStmt.statementIndex
544
        });
545

546
        if (previousType?.isResolvable()) {
129✔
547
            // is this different?
548
            if (!isDynamicType(previousType)) {
40✔
549
                if (isDynamicType(rhsType) || !previousType.isTypeCompatible(rhsType)) {
39✔
550
                    diagnostics.push(
6✔
551
                        messages.typeReassignment(assignStmt.location, varName, previousType.toString(), rhsType.toString(), severity)
552
                    );
553
                }
554
            }
555
        }
556
    }
557

558
    private isLoop(node: AstNode) {
559
        return isForStatement(node) || isForEachStatement(node) || isWhileStatement(node);
29✔
560
    }
561

562
    private isCreateObject(s: CallExpression) {
563
        return isVariableExpression(s.callee) && s.callee.tokens?.name.text.toLowerCase() === 'createobject';
13!
564
    }
565

566
    private getLiteralArgs(args: Expression[]) {
567
        const argsStringValue: string[] = [];
9✔
568
        for (const arg of args) {
9✔
569
            if (isLiteralExpression(arg)) {
27!
570
                argsStringValue.push(arg?.tokens?.value?.text?.replace(/"/g, ''));
27!
571
            } else {
UNCOV
572
                return;
×
573
            }
574
        }
575

576
        return argsStringValue;
9✔
577
    }
578
}
579

580
/**
581
 * Collect indexes of non-inline AA members
582
 */
583
export function collectWrappingAAMembersIndexes(aa: AALiteralExpression): number[] {
1✔
584
    const indexes: number[] = [];
41✔
585
    const { elements } = aa;
41✔
586
    const lastIndex = elements.length - 1;
41✔
587
    for (let i = 0; i < lastIndex; i++) {
41✔
588
        const e = elements[i];
66✔
589

590
        const ne = elements[i + 1];
66✔
591
        const hasNL = ne.location.range.start.line > e.location.range.end.line;
66✔
592
        if (hasNL) {
66✔
593
            indexes.push(i);
39✔
594
        }
595
    }
596
    const last = elements[lastIndex];
41✔
597
    if (last) {
41✔
598
        indexes.push(lastIndex);
40✔
599
    }
600
    return indexes;
41✔
601
}
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

© 2025 Coveralls, Inc