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

rokucommunity / bslint / #1019

16 Sep 2024 01:13PM CUT coverage: 91.474% (+0.05%) from 91.427%
#1019

push

web-flow
Merge a7c402a8d into 3f893d0b1

925 of 1055 branches covered (87.68%)

Branch coverage included in aggregate %.

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

2 existing lines in 1 file now uncovered.

1017 of 1068 relevant lines covered (95.22%)

68.99 hits per line

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

92.46
/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
    CompilerPlugin,
16
    AfterFileValidateEvent,
17
    Expression,
18
    isVoidType,
19
    Statement,
20
    SymbolTypeFlag,
21
    XmlFile,
22
    AstNode,
23
    Token,
24
    isNamespaceStatement,
25
    util,
26
    isAnyReferenceType,
27
    ExtraSymbolData,
28
    OnScopeValidateEvent,
29
    InternalWalkMode,
30
    isCallableType
31
} from 'brighterscript';
32
import { RuleAAComma } from '../..';
33
import { addFixesToEvent } from '../../textEdit';
1โœ”
34
import { PluginContext } from '../../util';
35
import { createColorValidator } from '../../createColorValidator';
1โœ”
36
import { messages } from './diagnosticMessages';
1โœ”
37
import { extractFixes } from './styleFixes';
1โœ”
38
import { BsLintDiagnosticContext } from '../../Linter';
1โœ”
39
import { Location } from 'vscode-languageserver-types';
40

41
export default class CodeStyle implements CompilerPlugin {
1โœ”
42

43
    name: 'codeStyle';
44

45
    constructor(private lintContext: PluginContext) {
149โœ”
46
    }
47

48
    onGetCodeActions(event: OnGetCodeActionsEvent) {
49
        const addFixes = addFixesToEvent(event);
ร—
50
        extractFixes(event.file, addFixes, event.diagnostics);
ร—
51
    }
52

53
    validateXMLFile(file: XmlFile) {
54
        const diagnostics: BsDiagnostic[] = [];
2โœ”
55
        const { noArrayComponentFieldType, noAssocarrayComponentFieldType } = this.lintContext.severity;
2โœ”
56

57
        const validateArrayComponentFieldType = noArrayComponentFieldType !== DiagnosticSeverity.Hint;
2โœ”
58
        const validateAssocarrayComponentFieldType = noAssocarrayComponentFieldType !== DiagnosticSeverity.Hint;
2โœ”
59

60
        for (const field of file.parser?.ast?.componentElement?.interfaceElement?.fields ?? []) {
2!
61
            if (field.tokens.startTagName?.text?.toLowerCase() === 'field') {
6!
62
                const typeAttribute = field.getAttribute('type');
6โœ”
63

64
                const typeValue = typeAttribute?.tokens?.value?.text?.toLowerCase();
6!
65
                if (typeValue === 'array' && validateArrayComponentFieldType) {
6โœ”
66
                    diagnostics.push(
1โœ”
67
                        messages.noArrayFieldType(
68
                            typeAttribute?.tokens?.value?.location,
9!
69
                            noArrayComponentFieldType
70
                        )
71
                    );
72
                } else if (typeValue === 'assocarray' && validateAssocarrayComponentFieldType) {
5โœ”
73
                    diagnostics.push(
1โœ”
74
                        messages.noAssocarrayFieldType(
75
                            typeAttribute?.tokens?.value?.location,
9!
76
                            noAssocarrayComponentFieldType
77
                        )
78
                    );
79
                }
80
            }
81
        }
82

83
        return diagnostics;
2โœ”
84
    }
85

86
    validateBrsFile(file: BrsFile) {
87
        const diagnostics: (BsDiagnostic)[] = [];
64โœ”
88
        const { severity } = this.lintContext;
64โœ”
89
        const { inlineIfStyle, blockIfStyle, conditionStyle, noPrint, noTodo, noStop, aaCommaStyle, eolLast, colorFormat } = severity;
64โœ”
90
        const validatePrint = noPrint !== DiagnosticSeverity.Hint;
64โœ”
91
        const validateTodo = noTodo !== DiagnosticSeverity.Hint;
64โœ”
92
        const validateNoStop = noStop !== DiagnosticSeverity.Hint;
64โœ”
93
        const validateInlineIf = inlineIfStyle !== 'off';
64โœ”
94
        const validateColorFormat = (colorFormat === 'hash-hex' || colorFormat === 'quoted-numeric-hex' || colorFormat === 'never');
64โœ”
95
        const disallowInlineIf = inlineIfStyle === 'never';
64โœ”
96
        const requireInlineIfThen = inlineIfStyle === 'then';
64โœ”
97
        const validateBlockIf = blockIfStyle !== 'off';
64โœ”
98
        const requireBlockIfThen = blockIfStyle === 'then';
64โœ”
99
        const validateCondition = conditionStyle !== 'off';
64โœ”
100
        const requireConditionGroup = conditionStyle === 'group';
64โœ”
101
        const validateAAStyle = aaCommaStyle !== 'off';
64โœ”
102
        const validateEolLast = eolLast !== 'off';
64โœ”
103
        const disallowEolLast = eolLast === 'never';
64โœ”
104
        const validateColorStyle = validateColorFormat ? createColorValidator(severity) : undefined;
64โœ”
105

106
        // Check if the file is empty by going backwards from the last token,
107
        // meaning there are tokens other than `Eof` and `Newline`.
108
        const { tokens } = file.parser;
64โœ”
109
        let isFileEmpty = true;
64โœ”
110
        for (let i = tokens.length - 1; i >= 0; i--) {
64โœ”
111
            if (tokens[i].kind !== TokenKind.Eof &&
180โœ”
112
                tokens[i].kind !== TokenKind.Newline) {
113
                isFileEmpty = false;
63โœ”
114
                break;
63โœ”
115
            }
116
        }
117

118
        // Validate `eol-last` on non-empty files
119
        if (validateEolLast && !isFileEmpty) {
64โœ”
120
            const penultimateToken = tokens[tokens.length - 2];
45โœ”
121
            if (disallowEolLast) {
45โœ”
122
                if (penultimateToken?.kind === TokenKind.Newline) {
2!
123
                    diagnostics.push(messages.removeEolLast(penultimateToken.location));
2โœ”
124
                }
125
            } else if (penultimateToken?.kind !== TokenKind.Newline) {
43!
126
                // Set the preferredEol as the last newline.
127
                // The fix function will handle the case where preferredEol is undefined.
128
                // This could happen in valid single line files, like:
129
                // `sub foo() end sub\EOF`
130
                let preferredEol;
131
                for (let i = tokens.length - 1; i >= 0; i--) {
3โœ”
132
                    if (tokens[i].kind === TokenKind.Newline) {
38โœ”
133
                        preferredEol = tokens[i].text;
4โœ”
134
                    }
135
                }
136

137
                diagnostics.push(
3โœ”
138
                    messages.addEolLast(
139
                        penultimateToken.location,
140
                        preferredEol
141
                    )
142
                );
143
            }
144
        }
145

146
        file.ast.walk(createVisitor({
64โœ”
147
            // validate function style (`function` or `sub`)
148
            FunctionExpression: (func) => {
149
                this.validateFunctionStyle(func, diagnostics);
152โœ”
150
            },
151
            IfStatement: s => {
152
                const hasThenToken = !!s.tokens.then;
74โœ”
153
                if (!s.isInline && validateBlockIf) {
74โœ”
154
                    if (hasThenToken !== requireBlockIfThen) {
14โœ”
155
                        diagnostics.push(requireBlockIfThen
7โœ”
156
                            ? messages.addBlockIfThenKeyword(s)
157
                            : messages.removeBlockIfThenKeyword(s)
158
                        );
159
                    }
160
                } else if (s.isInline && validateInlineIf) {
60โœ”
161
                    if (disallowInlineIf) {
14โœ”
162
                        diagnostics.push(messages.inlineIfNotAllowed(s.location));
2โœ”
163
                    } else if (hasThenToken !== requireInlineIfThen) {
12โœ”
164
                        diagnostics.push(requireInlineIfThen
6โœ”
165
                            ? messages.addInlineIfThenKeyword(s)
166
                            : messages.removeInlineIfThenKeyword(s)
167
                        );
168
                    }
169
                }
170

171
                if (validateCondition) {
74โœ”
172
                    if (isGroupingExpression(s.condition) !== requireConditionGroup) {
22โœ”
173
                        diagnostics.push(requireConditionGroup
11โœ”
174
                            ? messages.addParenthesisAroundCondition(s)
175
                            : messages.removeParenthesisAroundCondition(s)
176
                        );
177
                    }
178
                }
179
            },
180
            WhileStatement: s => {
181
                if (validateCondition) {
12โœ”
182
                    if (isGroupingExpression(s.condition) !== requireConditionGroup) {
6โœ”
183
                        diagnostics.push(requireConditionGroup
3โœ”
184
                            ? messages.addParenthesisAroundCondition(s)
185
                            : messages.removeParenthesisAroundCondition(s)
186
                        );
187
                    }
188
                }
189
            },
190
            PrintStatement: s => {
191
                if (validatePrint) {
120โœ”
192
                    diagnostics.push(messages.noPrint(s.tokens.print.location, noPrint));
2โœ”
193
                }
194
            },
195
            LiteralExpression: e => {
196
                if (validateColorStyle && e.tokens.value.kind === TokenKind.StringLiteral) {
434โœ”
197
                    validateColorStyle(e.tokens.value.text, e.tokens.value.location, diagnostics);
63โœ”
198
                }
199
            },
200
            TemplateStringExpression: e => {
201
                // only validate template strings that look like regular strings (i.e. `0xAABBCC`)
202
                if (validateColorStyle && e.quasis.length === 1 && e.quasis[0].expressions.length === 1) {
11โœ”
203
                    validateColorStyle(e.quasis[0].expressions[0].tokens.value.text, e.quasis[0].expressions[0].tokens.value.location, diagnostics);
6โœ”
204
                }
205
            },
206
            AALiteralExpression: e => {
207
                if (validateAAStyle) {
33โœ”
208
                    this.validateAAStyle(e, aaCommaStyle, diagnostics);
30โœ”
209
                }
210
            },
211
            StopStatement: s => {
212
                if (validateNoStop) {
1!
213
                    diagnostics.push(messages.noStop(s.tokens.stop.location, noStop));
1โœ”
214
                }
215
            },
216
            AstNode: (node: Statement | Expression) => {
217
                const comments = [...node.leadingTrivia, ...node.endTrivia].filter(t => t.kind === TokenKind.Comment);
3,010โœ”
218
                if (validateTodo && comments.length > 0) {
1,915โœ”
219
                    for (const e of comments) {
6โœ”
220
                        if (this.lintContext.todoPattern.test(e.text)) {
26โœ”
221
                            diagnostics.push(messages.noTodo(e.location, noTodo));
10โœ”
222
                        }
223
                    }
224
                }
225
            }
226
        }), { walkMode: WalkMode.visitAllRecursive });
227

228
        return diagnostics;
64โœ”
229
    }
230

231
    validateBrsFileInScope(file: BrsFile) {
232
        const diagnostics: (BsDiagnostic)[] = [];
64โœ”
233
        const { severity } = this.lintContext;
64โœ”
234
        const { nameShadowing } = severity;
64โœ”
235

236
        file.ast.walk(createVisitor({
64โœ”
237
            NamespaceStatement: (nsStmt) => {
238
                this.validateNameShadowing(file, nsStmt, nsStmt.getNameParts()?.[0], nameShadowing, diagnostics);
4!
239
            },
240
            ClassStatement: (classStmt) => {
241
                this.validateNameShadowing(file, classStmt, classStmt.tokens.name, nameShadowing, diagnostics);
7โœ”
242
            },
243
            InterfaceStatement: (ifaceStmt) => {
244
                this.validateNameShadowing(file, ifaceStmt, ifaceStmt.tokens.name, nameShadowing, diagnostics);
7โœ”
245
            },
246
            ConstStatement: (constStmt) => {
247
                this.validateNameShadowing(file, constStmt, constStmt.tokens.name, nameShadowing, diagnostics);
11โœ”
248
            },
249
            EnumStatement: (enumStmt) => {
250
                this.validateNameShadowing(file, enumStmt, enumStmt.tokens.name, nameShadowing, diagnostics);
7โœ”
251
            }
252
            // eslint-disable-next-line no-bitwise
253
        }), { walkMode: WalkMode.visitStatementsRecursive | InternalWalkMode.visitFalseConditionalCompilationBlocks });
254

255
        return diagnostics;
64โœ”
256
    }
257

258
    afterFileValidate(event: AfterFileValidateEvent) {
259
        const { file } = event;
66โœ”
260
        if (this.lintContext.ignores(file)) {
66!
261
            return;
ร—
262
        }
263

264
        const diagnostics: (BsDiagnostic)[] = [];
66โœ”
265
        if (isXmlFile(file)) {
66โœ”
266
            diagnostics.push(...this.validateXMLFile(file));
2โœ”
267
        } else if (isBrsFile(file)) {
64!
268
            diagnostics.push(...this.validateBrsFile(file));
64โœ”
269
        }
270

271
        // add file reference
272
        let bsDiagnostics: BsDiagnostic[] = diagnostics.map(diagnostic => ({
134โœ”
273
            ...diagnostic,
274
            file
275
        }));
276

277
        const { fix } = this.lintContext;
66โœ”
278

279
        // apply fix
280
        if (fix) {
66โœ”
281
            bsDiagnostics = extractFixes(event.file, this.lintContext.addFixes, bsDiagnostics);
12โœ”
282
        }
283

284
        // append diagnostics
285
        event.program.diagnostics.register(bsDiagnostics, BsLintDiagnosticContext);
66โœ”
286
    }
287

288
    onScopeValidate(event: OnScopeValidateEvent) {
289
        for (const file of event.scope.getOwnFiles()) {
65โœ”
290
            if (this.lintContext.ignores(file)) {
66!
291
                return;
ร—
292
            }
293

294
            const diagnostics: (BsDiagnostic)[] = [];
66โœ”
295
            if (isBrsFile(file)) {
66โœ”
296
                diagnostics.push(...this.validateBrsFileInScope(file));
64โœ”
297
            }
298

299
            // add file reference
300
            let bsDiagnostics: BsDiagnostic[] = diagnostics.map(diagnostic => ({
66โœ”
301
                ...diagnostic,
302
                file
303
            }));
304

305
            const { fix } = this.lintContext;
66โœ”
306

307
            // apply fix
308
            if (fix) {
66โœ”
309
                bsDiagnostics = extractFixes(file, this.lintContext.addFixes, bsDiagnostics);
12โœ”
310
            }
311

312
            // append diagnostics
313
            event.program.diagnostics.register(bsDiagnostics, BsLintDiagnosticContext);
66โœ”
314
        }
315
    }
316

317
    validateAAStyle(aa: AALiteralExpression, aaCommaStyle: RuleAAComma, diagnostics: (BsDiagnostic)[]) {
318
        const indexes = collectWrappingAAMembersIndexes(aa);
30โœ”
319
        const last = indexes.length - 1;
30โœ”
320
        const isSingleLine = (aa: AALiteralExpression): boolean => {
30โœ”
321
            return aa.tokens.open.location.range.start.line === aa.tokens.close.location.range.end.line;
10โœ”
322
        };
323

324
        indexes.forEach((index, i) => {
30โœ”
325
            const member = aa.elements[index];
66โœ”
326
            const hasComma = !!member.tokens.comma;
66โœ”
327
            if (aaCommaStyle === 'never' || (i === last && ((aaCommaStyle === 'no-dangling') || isSingleLine(aa)))) {
66โœ”
328
                if (hasComma) {
36โœ”
329
                    diagnostics.push(messages.removeAAComma(member.tokens.comma.location));
18โœ”
330
                }
331
            } else if (!hasComma) {
30โœ”
332
                diagnostics.push(messages.addAAComma(member.value.location));
12โœ”
333
            }
334
        });
335
    }
336

337
    validateFunctionStyle(fun: FunctionExpression, diagnostics: (BsDiagnostic)[]) {
338
        const { severity } = this.lintContext;
152โœ”
339
        const { namedFunctionStyle, anonFunctionStyle, typeAnnotations } = severity;
152โœ”
340
        const style = fun.functionStatement ? namedFunctionStyle : anonFunctionStyle;
152โœ”
341
        const kind = fun.tokens.functionType.kind;
152โœ”
342
        const hasReturnedValue = style === 'auto' || typeAnnotations !== 'off' ? this.getFunctionReturns(fun) : false;
152โœ”
343

344
        // type annotations
345
        if (typeAnnotations !== 'off') {
152โœ”
346
            const needsArgType = typeAnnotations.startsWith('args') || typeAnnotations.startsWith('all');
22โœ”
347
            const needsReturnType = typeAnnotations.startsWith('return') || typeAnnotations.startsWith('all');
22โœ”
348
            const allowImplicit = typeAnnotations.includes('allow-implicit');
22โœ”
349

350
            if (needsReturnType) {
22โœ”
351
                if (hasReturnedValue && !fun.returnTypeExpression) {
13โœ”
352
                    diagnostics.push(messages.expectedReturnTypeAnnotation(
4โœ”
353
                        // add the error to the function keyword (or just highlight the whole function if that's somehow missing)
354
                        fun.tokens.functionType?.location ?? fun.location
24!
355
                    ));
356
                }
357
            }
358
            if (needsArgType) {
22โœ”
359
                const missingAnnotation = fun.parameters.find(arg => {
18โœ”
360
                    if (!arg.typeExpression) {
26โœ”
361
                        if (allowImplicit && arg.defaultValue) {
14โœ”
362
                            return false;
4โœ”
363
                        }
364
                        return true;
10โœ”
365
                    }
366
                    return false;
12โœ”
367
                });
368
                if (missingAnnotation) {
18โœ”
369
                    // only report 1st missing arg annotation to avoid error overload
370
                    diagnostics.push(messages.expectedTypeAnnotation(missingAnnotation.location));
10โœ”
371
                }
372
            }
373
        }
374

375
        // keyword style
376
        if (style === 'off') {
152โœ”
377
            return;
61โœ”
378
        }
379
        if (style === 'no-function') {
91โœ”
380
            if (kind === TokenKind.Function) {
12โœ”
381
                diagnostics.push(messages.expectedSubKeyword(fun, `(always use 'sub')`));
6โœ”
382
            }
383
            return;
12โœ”
384
        }
385

386
        if (style === 'no-sub') {
79โœ”
387
            if (kind === TokenKind.Sub) {
11โœ”
388
                diagnostics.push(messages.expectedFunctionKeyword(fun, `(always use 'function')`));
5โœ”
389
            }
390
            return;
11โœ”
391
        }
392

393
        // auto
394
        if (hasReturnedValue) {
68โœ”
395
            if (kind !== TokenKind.Function) {
7โœ”
396
                diagnostics.push(messages.expectedFunctionKeyword(fun, `(use 'function' when a value is returned)`));
2โœ”
397
            }
398
        } else if (kind !== TokenKind.Sub) {
61โœ”
399
            diagnostics.push(messages.expectedSubKeyword(fun, `(use 'sub' when no value is returned)`));
4โœ”
400
        }
401
    }
402

403
    getFunctionReturns(fun: FunctionExpression) {
404
        let hasReturnedValue = false;
90โœ”
405
        if (fun.returnTypeExpression) {
90โœ”
406
            hasReturnedValue = !isVoidType(fun.returnTypeExpression.getType({ flags: SymbolTypeFlag.typetime }));
14โœ”
407
        } else {
408
            const cancel = new CancellationTokenSource();
76โœ”
409
            fun.body.walk(createVisitor({
76โœ”
410
                ReturnStatement: s => {
411
                    hasReturnedValue = !!s.value;
12โœ”
412
                    cancel.cancel();
12โœ”
413
                }
414
            }), { walkMode: WalkMode.visitStatements, cancel: cancel.token });
415
        }
416
        return hasReturnedValue;
90โœ”
417
    }
418

419
    validateNameShadowing(file: BrsFile, node: AstNode, nameIdentifier: Token, severity: DiagnosticSeverity, diagnostics: (BsDiagnostic)[]) {
420
        const name = nameIdentifier?.text;
36!
421
        if (!name || !node) {
36!
UNCOV
422
            return;
ร—
423
        }
424
        const nameLocation = nameIdentifier.location;
36โœ”
425

426
        const astTable = file.ast.getSymbolTable();
36โœ”
427
        const data = {} as ExtraSymbolData;
36โœ”
428
        const typeChain = [];
36โœ”
429
        // eslint-disable-next-line no-bitwise
430
        const existingType = astTable.getSymbolType(name, { flags: SymbolTypeFlag.runtime | SymbolTypeFlag.typetime, data: data, typeChain: typeChain });
36โœ”
431

432
        if (!existingType || isAnyReferenceType(existingType)) {
36!
UNCOV
433
            return;
ร—
434
        }
435
        if ((data.definingNode === node) || (isNamespaceStatement(data.definingNode) && isNamespaceStatement(node))) {
36โœ”
436
            return;
20โœ”
437
        }
438
        const otherNode = data.definingNode as unknown as { tokens: { name: Token }; location: Location };
16โœ”
439
        const thisNodeKindName = util.getAstNodeFriendlyName(node);
16โœ”
440
        let thatNodeKindName = util.getAstNodeFriendlyName(data.definingNode) ?? '';
16โœ”
441
        if (!thatNodeKindName && isCallableType(existingType)) {
16โœ”
442
            thatNodeKindName = 'Global Function';
1โœ”
443
        }
444

445
        let thatNameLocation = otherNode?.tokens?.name?.location ?? otherNode?.location;
16โœ”
446

447
        if (isNamespaceStatement(data.definingNode)) {
16โœ”
448
            thatNameLocation = data.definingNode.getNameParts()?.[0]?.location;
2!
449
        }
450

451
        const relatedInformation = thatNameLocation ? [{
16โœ”
452
            message: `${thatNodeKindName} declared here`,
453
            location: thatNameLocation
454
        }] : undefined;
455

456
        diagnostics.push({
16โœ”
457
            ...messages.nameShadowing(thisNodeKindName, thatNodeKindName, name, severity),
458
            location: nameLocation,
459
            relatedInformation: relatedInformation
460
        });
461
    }
462
}
463

464
/**
465
 * Collect indexes of non-inline AA members
466
 */
467
export function collectWrappingAAMembersIndexes(aa: AALiteralExpression): number[] {
1โœ”
468
    const indexes: number[] = [];
38โœ”
469
    const { elements } = aa;
38โœ”
470
    const lastIndex = elements.length - 1;
38โœ”
471
    for (let i = 0; i < lastIndex; i++) {
38โœ”
472
        const e = elements[i];
65โœ”
473

474
        const ne = elements[i + 1];
65โœ”
475
        const hasNL = ne.location.range.start.line > e.location.range.end.line;
65โœ”
476
        if (hasNL) {
65โœ”
477
            indexes.push(i);
39โœ”
478
        }
479
    }
480
    const last = elements[lastIndex];
38โœ”
481
    if (last) {
38โœ”
482
        indexes.push(lastIndex);
37โœ”
483
    }
484
    return indexes;
38โœ”
485
}
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