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

rokucommunity / bslint / #883

17 May 2024 06:10PM UTC coverage: 91.908% (-0.2%) from 92.085%
#883

push

TwitchBronBron
0.8.21

775 of 875 branches covered (88.57%)

Branch coverage included in aggregate %.

906 of 954 relevant lines covered (94.97%)

63.17 hits per line

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

96.3
/src/plugins/codeStyle/index.ts
1
import { BscFile, BsDiagnostic, createVisitor, FunctionExpression, isBrsFile, isGroupingExpression, TokenKind, WalkMode, CancellationTokenSource, DiagnosticSeverity, OnGetCodeActionsEvent, isCommentStatement, AALiteralExpression, AAMemberExpression } from 'brighterscript';
1✔
2
import { RuleAAComma } from '../..';
3
import { addFixesToEvent } from '../../textEdit';
1✔
4
import { PluginContext } from '../../util';
5
import { createColorValidator } from '../../createColorValidator';
1✔
6
import { messages } from './diagnosticMessages';
1✔
7
import { extractFixes } from './styleFixes';
1✔
8

9
export default class CodeStyle {
1✔
10

11
    name: 'codeStyle';
12

13
    constructor(private lintContext: PluginContext) {
132✔
14
    }
15

16
    onGetCodeActions(event: OnGetCodeActionsEvent) {
17
        const addFixes = addFixesToEvent(event);
×
18
        extractFixes(addFixes, event.diagnostics);
×
19
    }
20

21
    afterFileValidate(file: BscFile) {
22
        if (!isBrsFile(file) || this.lintContext.ignores(file)) {
58!
23
            return;
×
24
        }
25

26
        const diagnostics: (Omit<BsDiagnostic, 'file'>)[] = [];
58✔
27
        const { severity, fix } = this.lintContext;
58✔
28
        const { inlineIfStyle, blockIfStyle, conditionStyle, noPrint, noTodo, noStop, aaCommaStyle, eolLast, colorFormat } = severity;
58✔
29
        const validatePrint = noPrint !== DiagnosticSeverity.Hint;
58✔
30
        const validateTodo = noTodo !== DiagnosticSeverity.Hint;
58✔
31
        const validateNoStop = noStop !== DiagnosticSeverity.Hint;
58✔
32
        const validateInlineIf = inlineIfStyle !== 'off';
58✔
33
        const validateColorFormat = (colorFormat === 'hash-hex' || colorFormat === 'quoted-numeric-hex' || colorFormat === 'never');
58✔
34
        const disallowInlineIf = inlineIfStyle === 'never';
58✔
35
        const requireInlineIfThen = inlineIfStyle === 'then';
58✔
36
        const validateBlockIf = blockIfStyle !== 'off';
58✔
37
        const requireBlockIfThen = blockIfStyle === 'then';
58✔
38
        const validateCondition = conditionStyle !== 'off';
58✔
39
        const requireConditionGroup = conditionStyle === 'group';
58✔
40
        const validateAAStyle = aaCommaStyle !== 'off';
58✔
41
        const walkExpressions = validateAAStyle || validateColorFormat;
58✔
42
        const validateEolLast = eolLast !== 'off';
58✔
43
        const disallowEolLast = eolLast === 'never';
58✔
44
        const validateColorStyle = validateColorFormat ? createColorValidator(severity) : undefined;
58✔
45

46
        // Check if the file is empty by going backwards from the last token,
47
        // meaning there are tokens other than `Eof` and `Newline`.
48
        const { tokens } = file.parser;
58✔
49
        let isFileEmpty = true;
58✔
50
        for (let i = tokens.length - 1; i >= 0; i--) {
58✔
51
            if (tokens[i].kind !== TokenKind.Eof &&
162✔
52
                tokens[i].kind !== TokenKind.Newline) {
53
                isFileEmpty = false;
57✔
54
                break;
57✔
55
            }
56
        }
57

58
        // Validate `eol-last` on non-empty files
59
        if (validateEolLast && !isFileEmpty) {
58✔
60
            const penultimateToken = tokens[tokens.length - 2];
39✔
61
            if (disallowEolLast) {
39✔
62
                if (penultimateToken?.kind === TokenKind.Newline) {
2!
63
                    diagnostics.push(messages.removeEolLast(penultimateToken.range));
2✔
64
                }
65
            } else if (penultimateToken?.kind !== TokenKind.Newline) {
37!
66
                // Set the preferredEol as the last newline.
67
                // The fix function will handle the case where preferredEol is undefined.
68
                // This could happen in valid single line files, like:
69
                // `sub foo() end sub\EOF`
70
                let preferredEol;
71
                for (let i = tokens.length - 1; i >= 0; i--) {
3✔
72
                    if (tokens[i].kind === TokenKind.Newline) {
38✔
73
                        preferredEol = tokens[i].text;
4✔
74
                    }
75
                }
76

77
                diagnostics.push(
3✔
78
                    messages.addEolLast(
79
                        penultimateToken.range,
80
                        preferredEol
81
                    )
82
                );
83
            }
84
        }
85

86
        file.ast.walk(createVisitor({
58✔
87
            IfStatement: s => {
88
                const hasThenToken = !!s.tokens.then;
74✔
89
                if (!s.isInline && validateBlockIf) {
74✔
90
                    if (hasThenToken !== requireBlockIfThen) {
14✔
91
                        diagnostics.push(requireBlockIfThen
7✔
92
                            ? messages.addBlockIfThenKeyword(s)
7✔
93
                            : messages.removeBlockIfThenKeyword(s)
94
                        );
95
                    }
96
                } else if (s.isInline && validateInlineIf) {
60✔
97
                    if (disallowInlineIf) {
14✔
98
                        diagnostics.push(messages.inlineIfNotAllowed(s.range));
2✔
99
                    } else if (hasThenToken !== requireInlineIfThen) {
12✔
100
                        diagnostics.push(requireInlineIfThen
6✔
101
                            ? messages.addInlineIfThenKeyword(s)
6✔
102
                            : messages.removeInlineIfThenKeyword(s)
103
                        );
104
                    }
105
                }
106

107
                if (validateCondition) {
74✔
108
                    if (isGroupingExpression(s.condition) !== requireConditionGroup) {
22✔
109
                        diagnostics.push(requireConditionGroup
11✔
110
                            ? messages.addParenthesisAroundCondition(s)
11✔
111
                            : messages.removeParenthesisAroundCondition(s)
112
                        );
113
                    }
114
                }
115
            },
116
            WhileStatement: s => {
117
                if (validateCondition) {
12✔
118
                    if (isGroupingExpression(s.condition) !== requireConditionGroup) {
6✔
119
                        diagnostics.push(requireConditionGroup
3✔
120
                            ? messages.addParenthesisAroundCondition(s)
3✔
121
                            : messages.removeParenthesisAroundCondition(s)
122
                        );
123
                    }
124
                }
125
            },
126
            PrintStatement: s => {
127
                if (validatePrint) {
118✔
128
                    diagnostics.push(messages.noPrint(s.tokens.print.range, noPrint));
2✔
129
                }
130
            },
131
            LiteralExpression: e => {
132
                if (validateColorStyle && e.token.kind === TokenKind.StringLiteral) {
404✔
133
                    validateColorStyle(e.token.text, e.token.range, diagnostics);
63✔
134
                }
135
            },
136
            TemplateStringExpression: e => {
137
                // only validate template strings that look like regular strings (i.e. `0xAABBCC`)
138
                if (validateColorStyle && e.quasis.length === 1 && e.quasis[0].expressions.length === 1) {
11✔
139
                    validateColorStyle(e.quasis[0].expressions[0].token.text, e.quasis[0].expressions[0].token.range, diagnostics);
6✔
140
                }
141
            },
142
            AALiteralExpression: e => {
143
                if (validateAAStyle) {
33✔
144
                    this.validateAAStyle(e, aaCommaStyle, diagnostics);
30✔
145
                }
146
            },
147
            CommentStatement: e => {
148
                if (validateTodo) {
31✔
149
                    if (this.lintContext.todoPattern.test(e.text)) {
18✔
150
                        diagnostics.push(messages.noTodo(e.range, noTodo));
7✔
151
                    }
152
                }
153
            },
154
            StopStatement: s => {
155
                if (validateNoStop) {
1!
156
                    diagnostics.push(messages.noStop(s.tokens.stop.range, noStop));
1✔
157
                }
158
            }
159
        }), { walkMode: walkExpressions ? WalkMode.visitAllRecursive : WalkMode.visitStatementsRecursive });
58!
160

161
        // validate function style (`function` or `sub`)
162
        for (const fun of file.parser.references.functionExpressions) {
58✔
163
            this.validateFunctionStyle(fun, diagnostics);
137✔
164
        }
165

166
        // add file reference
167
        let bsDiagnostics: BsDiagnostic[] = diagnostics.map(diagnostic => ({
121✔
168
            ...diagnostic,
169
            file
170
        }));
171

172
        // apply fix
173
        if (fix) {
58✔
174
            bsDiagnostics = extractFixes(this.lintContext.addFixes, bsDiagnostics);
12✔
175
        }
176

177
        // append diagnostics
178
        file.addDiagnostics(bsDiagnostics);
58✔
179
    }
180

181
    validateAAStyle(aa: AALiteralExpression, aaCommaStyle: RuleAAComma, diagnostics: (Omit<BsDiagnostic, 'file'>)[]) {
182
        const indexes = collectWrappingAAMembersIndexes(aa);
30✔
183
        const last = indexes.length - 1;
30✔
184
        const isSingleLine = (aa: AALiteralExpression): boolean => {
30✔
185
            return aa.open.range.start.line === aa.close.range.end.line;
10✔
186
        };
187

188
        indexes.forEach((index, i) => {
30✔
189
            const member = aa.elements[index] as AAMemberExpression;
66✔
190
            const hasComma = !!member.commaToken;
66✔
191
            if (aaCommaStyle === 'never' || (i === last && ((aaCommaStyle === 'no-dangling') || isSingleLine(aa)))) {
66✔
192
                if (hasComma) {
36✔
193
                    diagnostics.push(messages.removeAAComma(member.commaToken.range));
18✔
194
                }
195
            } else if (!hasComma) {
30✔
196
                diagnostics.push(messages.addAAComma(member.value.range));
12✔
197
            }
198
        });
199
    }
200

201
    validateFunctionStyle(fun: FunctionExpression, diagnostics: (Omit<BsDiagnostic, 'file'>)[]) {
202
        const { severity } = this.lintContext;
137✔
203
        const { namedFunctionStyle, anonFunctionStyle, typeAnnotations } = severity;
137✔
204
        const style = fun.functionStatement ? namedFunctionStyle : anonFunctionStyle;
137✔
205
        const kind = fun.functionType.kind;
137✔
206
        const hasReturnedValue = style === 'auto' || typeAnnotations !== 'off' ? this.getFunctionReturns(fun) : false;
137✔
207

208
        // type annotations
209
        if (typeAnnotations !== 'off') {
137✔
210
            if (typeAnnotations !== 'args') {
9✔
211
                if (hasReturnedValue && !fun.returnTypeToken) {
6✔
212
                    diagnostics.push(messages.expectedReturnTypeAnnotation(
2✔
213
                        // add the error to the function keyword (or just highlight the whole function if that's somehow missing)
214
                        fun.functionType?.range ?? fun.range
12!
215
                    ));
216
                }
217
            }
218
            if (typeAnnotations !== 'return') {
9✔
219
                const missingAnnotation = fun.parameters.find(arg => !arg.typeToken);
8✔
220
                if (missingAnnotation) {
6✔
221
                    // only report 1st missing arg annotation to avoid error overload
222
                    diagnostics.push(messages.expectedTypeAnnotation(missingAnnotation.range));
4✔
223
                }
224
            }
225
        }
226

227
        // keyword style
228
        if (style === 'off') {
137✔
229
            return;
47✔
230
        }
231
        if (style === 'no-function') {
90✔
232
            if (kind === TokenKind.Function) {
12✔
233
                diagnostics.push(messages.expectedSubKeyword(fun, `(always use 'sub')`));
6✔
234
            }
235
            return;
12✔
236
        }
237

238
        if (style === 'no-sub') {
78✔
239
            if (kind === TokenKind.Sub) {
11✔
240
                diagnostics.push(messages.expectedFunctionKeyword(fun, `(always use 'function')`));
5✔
241
            }
242
            return;
11✔
243
        }
244

245
        // auto
246
        if (hasReturnedValue) {
67✔
247
            if (kind !== TokenKind.Function) {
6✔
248
                diagnostics.push(messages.expectedFunctionKeyword(fun, `(use 'function' when a value is returned)`));
2✔
249
            }
250
        } else if (kind !== TokenKind.Sub) {
61✔
251
            diagnostics.push(messages.expectedSubKeyword(fun, `(use 'sub' when no value is returned)`));
4✔
252
        }
253
    }
254

255
    getFunctionReturns(fun: FunctionExpression) {
256
        let hasReturnedValue = false;
76✔
257
        if (fun.returnTypeToken) {
76✔
258
            hasReturnedValue = fun.returnTypeToken.kind !== TokenKind.Void;
7✔
259
        } else {
260
            const cancel = new CancellationTokenSource();
69✔
261
            fun.body.walk(createVisitor({
69✔
262
                ReturnStatement: s => {
263
                    hasReturnedValue = !!s.value;
7✔
264
                    cancel.cancel();
7✔
265
                }
266
            }), { walkMode: WalkMode.visitStatements, cancel: cancel.token });
267
        }
268
        return hasReturnedValue;
76✔
269
    }
270
}
271

272
/**
273
 * Collect indexes of non-inline AA members
274
 */
275
export function collectWrappingAAMembersIndexes(aa: AALiteralExpression): number[] {
1✔
276
    const indexes: number[] = [];
38✔
277
    const { elements } = aa;
38✔
278
    const lastIndex = elements.length - 1;
38✔
279
    for (let i = 0; i < lastIndex; i++) {
38✔
280
        const e = elements[i];
82✔
281
        if (isCommentStatement(e)) {
82✔
282
            continue;
9✔
283
        }
284
        const ne = elements[i + 1];
73✔
285
        const hasNL = isCommentStatement(ne) || ne.range.start.line > e.range.end.line;
73✔
286
        if (hasNL) {
73✔
287
            indexes.push(i);
47✔
288
        }
289
    }
290
    const last = elements[lastIndex];
38✔
291
    if (last && !isCommentStatement(last)) {
38✔
292
        indexes.push(lastIndex);
29✔
293
    }
294
    return indexes;
38✔
295
}
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