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

rokucommunity / bslint / #700

pending completion
#700

push

TwitchBronBron
0.8.8

686 of 783 branches covered (87.61%)

Branch coverage included in aggregate %.

825 of 871 relevant lines covered (94.72%)

56.98 hits per line

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

95.62
/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 { messages } from './diagnosticMessages';
1✔
6
import { extractFixes } from './styleFixes';
1✔
7

8
export default class CodeStyle {
1✔
9

10
    name: 'codeStyle';
11

12
    constructor(private lintContext: PluginContext) {
61✔
13
    }
14

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

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

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

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

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

74
                diagnostics.push(
3✔
75
                    messages.addEolLast(
76
                        penultimateToken.range,
77
                        preferredEol
78
                    )
79
                );
80
            }
81
        }
82

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

104
                if (validateCondition) {
74✔
105
                    if (isGroupingExpression(s.condition) !== requireConditionGroup) {
22✔
106
                        diagnostics.push(requireConditionGroup
11✔
107
                            ? messages.addParenthesisAroundCondition(s)
11✔
108
                            : messages.removeParenthesisAroundCondition(s)
109
                        );
110
                    }
111
                }
112
            },
113
            WhileStatement: s => {
114
                if (validateCondition) {
12✔
115
                    if (isGroupingExpression(s.condition) !== requireConditionGroup) {
6✔
116
                        diagnostics.push(requireConditionGroup
3✔
117
                            ? messages.addParenthesisAroundCondition(s)
3✔
118
                            : messages.removeParenthesisAroundCondition(s)
119
                        );
120
                    }
121
                }
122
            },
123
            PrintStatement: s => {
124
                if (validatePrint) {
112✔
125
                    diagnostics.push(messages.noPrint(s.tokens.print.range, noPrint));
2✔
126
                }
127
            },
128
            AALiteralExpression: e => {
129
                if (validateAAStyle) {
30!
130
                    this.validateAAStyle(e, aaCommaStyle, diagnostics);
30✔
131
                }
132
            },
133
            CommentStatement: e => {
134
                if (validateTodo) {
30✔
135
                    if (this.lintContext.todoPattern.test(e.text)) {
18✔
136
                        diagnostics.push(messages.noTodo(e.range, noTodo));
7✔
137
                    }
138
                }
139
            },
140
            StopStatement: s => {
141
                if (validateNoStop) {
1!
142
                    diagnostics.push(messages.noStop(s.tokens.stop.range, noStop));
1✔
143
                }
144
            }
145
        }), { walkMode: walkExpressions ? WalkMode.visitAllRecursive : WalkMode.visitStatementsRecursive });
42!
146

147
        // validate function style (`function` or `sub`)
148
        for (const fun of file.parser.references.functionExpressions) {
42✔
149
            this.validateFunctionStyle(fun, diagnostics);
121✔
150
        }
151

152
        // add file reference
153
        let bsDiagnostics: BsDiagnostic[] = diagnostics.map(diagnostic => ({
97✔
154
            ...diagnostic,
155
            file
156
        }));
157

158
        // apply fix
159
        if (fix) {
42✔
160
            bsDiagnostics = extractFixes(this.lintContext.addFixes, bsDiagnostics);
12✔
161
        }
162

163
        // append diagnostics
164
        file.addDiagnostics(bsDiagnostics);
42✔
165
    }
166

167
    validateAAStyle(aa: AALiteralExpression, aaCommaStyle: RuleAAComma, diagnostics: (Omit<BsDiagnostic, 'file'>)[]) {
168
        const indexes = collectWrappingAAMembersIndexes(aa);
30✔
169
        const last = indexes.length - 1;
30✔
170
        const isSingleLine = (aa: AALiteralExpression): boolean => {
30✔
171
            return aa.open.range.start.line === aa.close.range.end.line;
10✔
172
        };
173

174
        indexes.forEach((index, i) => {
30✔
175
            const member = aa.elements[index] as AAMemberExpression;
66✔
176
            const hasComma = !!member.commaToken;
66✔
177
            if (aaCommaStyle === 'never' || (i === last && ((aaCommaStyle === 'no-dangling') || isSingleLine(aa)))) {
66✔
178
                if (hasComma) {
36✔
179
                    diagnostics.push(messages.removeAAComma(member.commaToken.range));
18✔
180
                }
181
            } else if (!hasComma) {
30✔
182
                diagnostics.push(messages.addAAComma(member.value.range));
12✔
183
            }
184
        });
185
    }
186

187
    validateFunctionStyle(fun: FunctionExpression, diagnostics: (Omit<BsDiagnostic, 'file'>)[]) {
188
        const { severity } = this.lintContext;
121✔
189
        const { namedFunctionStyle, anonFunctionStyle, typeAnnotations } = severity;
121✔
190
        const style = fun.functionStatement ? namedFunctionStyle : anonFunctionStyle;
121✔
191
        const kind = fun.functionType.kind;
121✔
192
        const hasReturnedValue = style === 'auto' || typeAnnotations !== 'off' ? this.getFunctionReturns(fun) : false;
121✔
193

194
        // type annotations
195
        if (typeAnnotations !== 'off') {
121✔
196
            if (typeAnnotations !== 'args') {
9✔
197
                if (hasReturnedValue && !fun.returnTypeToken) {
6✔
198
                    diagnostics.push(messages.expectedReturnTypeAnnotation(
2✔
199
                        // add the error to the function keyword (or just highlight the whole function if that's somehow missing)
200
                        fun.functionType?.range ?? fun.range
12!
201
                    ));
202
                }
203
            }
204
            if (typeAnnotations !== 'return') {
9✔
205
                const missingAnnotation = fun.parameters.find(arg => !arg.typeToken);
8✔
206
                if (missingAnnotation) {
6✔
207
                    // only report 1st missing arg annotation to avoid error overload
208
                    diagnostics.push(messages.expectedTypeAnnotation(missingAnnotation.range));
4✔
209
                }
210
            }
211
        }
212

213
        // keyword style
214
        if (style === 'off') {
121✔
215
            return;
31✔
216
        }
217
        if (style === 'no-function') {
90✔
218
            if (kind === TokenKind.Function) {
12✔
219
                diagnostics.push(messages.expectedSubKeyword(fun, `(always use 'sub')`));
6✔
220
            }
221
            return;
12✔
222
        }
223

224
        if (style === 'no-sub') {
78✔
225
            if (kind === TokenKind.Sub) {
11✔
226
                diagnostics.push(messages.expectedFunctionKeyword(fun, `(always use 'function')`));
5✔
227
            }
228
            return;
11✔
229
        }
230

231
        // auto
232
        if (hasReturnedValue) {
67✔
233
            if (kind !== TokenKind.Function) {
6✔
234
                diagnostics.push(messages.expectedFunctionKeyword(fun, `(use 'function' when a value is returned)`));
2✔
235
            }
236
        } else if (kind !== TokenKind.Sub) {
61✔
237
            diagnostics.push(messages.expectedSubKeyword(fun, `(use 'sub' when no value is returned)`));
4✔
238
        }
239
    }
240

241
    getFunctionReturns(fun: FunctionExpression) {
242
        let hasReturnedValue = false;
76✔
243
        if (fun.returnTypeToken) {
76✔
244
            hasReturnedValue = fun.returnTypeToken.kind !== TokenKind.Void;
7✔
245
        } else {
246
            const cancel = new CancellationTokenSource();
69✔
247
            fun.body.walk(createVisitor({
69✔
248
                ReturnStatement: s => {
249
                    hasReturnedValue = !!s.value;
7✔
250
                    cancel.cancel();
7✔
251
                }
252
            }), { walkMode: WalkMode.visitStatements, cancel: cancel.token });
253
        }
254
        return hasReturnedValue;
76✔
255
    }
256
}
257

258
/**
259
 * Collect indexes of non-inline AA members
260
 */
261
export function collectWrappingAAMembersIndexes(aa: AALiteralExpression): number[] {
1✔
262
    const indexes: number[] = [];
38✔
263
    const { elements } = aa;
38✔
264
    const lastIndex = elements.length - 1;
38✔
265
    for (let i = 0; i < lastIndex; i++) {
38✔
266
        const e = elements[i];
82✔
267
        if (isCommentStatement(e)) {
82✔
268
            continue;
9✔
269
        }
270
        const ne = elements[i + 1];
73✔
271
        const hasNL = isCommentStatement(ne) || ne.range.start.line > e.range.end.line;
73✔
272
        if (hasNL) {
73✔
273
            indexes.push(i);
47✔
274
        }
275
    }
276
    const last = elements[lastIndex];
38✔
277
    if (last && !isCommentStatement(last)) {
38✔
278
        indexes.push(lastIndex);
29✔
279
    }
280
    return indexes;
38✔
281
}
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