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

rokucommunity / bslint / #722

18 Sep 2023 06:19PM UTC coverage: 86.935% (-4.4%) from 91.354%
#722

push

web-flow
Merge e615f8c65 into 3567a926a

726 of 872 branches covered (0.0%)

Branch coverage included in aggregate %.

96 of 96 new or added lines in 4 files covered. (100.0%)

871 of 965 relevant lines covered (90.26%)

52.26 hits per line

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

93.54
/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, BsLintRules } from '../..';
3
import { SGNode } from 'brighterscript/dist/parser/SGTypes';
4
import { addFixesToEvent } from '../../textEdit';
1✔
5
import { PluginContext } from '../../util';
6
import { createColorValidator } from '../../createColorValidator';
1✔
7
import { messages } from './diagnosticMessages';
1✔
8
import { extractFixes } from './styleFixes';
1✔
9

10
export default class CodeStyle {
1✔
11

12
    name: 'codeStyle';
13

14
    constructor(private lintContext: PluginContext) {
62✔
15
    }
16

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

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

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

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

60
        // if (isXmlFile(file)) {
61
        //     const children = file.ast.component?.children;
62
        //     if (children) {
63
        //         this.walkChildren(severity, children.children, diagnostics);
64
        //     }
65
        // }
66

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

86
                diagnostics.push(
3✔
87
                    messages.addEolLast(
88
                        penultimateToken.range,
89
                        preferredEol
90
                    )
91
                );
92
            }
93
        }
94

95
        file.ast.walk(createVisitor({
43✔
96
            IfStatement: s => {
97
                const hasThenToken = !!s.tokens.then;
74✔
98
                if (!s.isInline && validateBlockIf) {
74✔
99
                    if (hasThenToken !== requireBlockIfThen) {
14✔
100
                        diagnostics.push(requireBlockIfThen
7✔
101
                            ? messages.addBlockIfThenKeyword(s)
7✔
102
                            : messages.removeBlockIfThenKeyword(s)
103
                        );
104
                    }
105
                } else if (s.isInline && validateInlineIf) {
60✔
106
                    if (disallowInlineIf) {
14✔
107
                        diagnostics.push(messages.inlineIfNotAllowed(s.range));
2✔
108
                    } else if (hasThenToken !== requireInlineIfThen) {
12✔
109
                        diagnostics.push(requireInlineIfThen
6✔
110
                            ? messages.addInlineIfThenKeyword(s)
6✔
111
                            : messages.removeInlineIfThenKeyword(s)
112
                        );
113
                    }
114
                }
115

116
                if (validateCondition) {
74✔
117
                    if (isGroupingExpression(s.condition) !== requireConditionGroup) {
22✔
118
                        diagnostics.push(requireConditionGroup
11✔
119
                            ? messages.addParenthesisAroundCondition(s)
11✔
120
                            : messages.removeParenthesisAroundCondition(s)
121
                        );
122
                    }
123
                }
124
            },
125
            WhileStatement: s => {
126
                if (validateCondition) {
12✔
127
                    if (isGroupingExpression(s.condition) !== requireConditionGroup) {
6✔
128
                        diagnostics.push(requireConditionGroup
3✔
129
                            ? messages.addParenthesisAroundCondition(s)
3✔
130
                            : messages.removeParenthesisAroundCondition(s)
131
                        );
132
                    }
133
                }
134
            },
135
            PrintStatement: s => {
136
                if (validatePrint) {
112✔
137
                    diagnostics.push(messages.noPrint(s.tokens.print.range, noPrint));
2✔
138
                }
139
            },
140
            LiteralExpression: e => {
141
                if (validateColorFormat && e.token.kind === TokenKind.StringLiteral) {
329✔
142
                    // debugger;
143
                    validateColorStyle(e.token.text, e.token.range, diagnostics);
5✔
144
                }
145
            },
146
            AALiteralExpression: e => {
147
                if (validateAAStyle) {
30!
148
                    this.validateAAStyle(e, aaCommaStyle, diagnostics);
30✔
149
                }
150
            },
151
            CommentStatement: e => {
152
                if (validateTodo) {
30✔
153
                    if (this.lintContext.todoPattern.test(e.text)) {
18✔
154
                        diagnostics.push(messages.noTodo(e.range, noTodo));
7✔
155
                    }
156
                }
157
            },
158
            StopStatement: s => {
159
                if (validateNoStop) {
1!
160
                    diagnostics.push(messages.noStop(s.tokens.stop.range, noStop));
1✔
161
                }
162
            }
163
        }), { walkMode: walkExpressions ? WalkMode.visitAllRecursive : WalkMode.visitStatementsRecursive });
43!
164

165
        // validate function style (`function` or `sub`)
166
        for (const fun of file.parser.references.functionExpressions) {
43✔
167
            this.validateFunctionStyle(fun, diagnostics);
122✔
168
        }
169

170
        // add file reference
171
        let bsDiagnostics: BsDiagnostic[] = diagnostics.map(diagnostic => ({
99✔
172
            ...diagnostic,
173
            file
174
        }));
175

176
        // apply fix
177
        if (fix) {
43✔
178
            bsDiagnostics = extractFixes(this.lintContext.addFixes, bsDiagnostics);
12✔
179
        }
180

181
        // append diagnostics
182
        file.addDiagnostics(bsDiagnostics);
43✔
183
    }
184

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

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

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

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

231
        // keyword style
232
        if (style === 'off') {
122✔
233
            return;
31✔
234
        }
235
        if (style === 'no-function') {
91✔
236
            if (kind === TokenKind.Function) {
12✔
237
                diagnostics.push(messages.expectedSubKeyword(fun, `(always use 'sub')`));
6✔
238
            }
239
            return;
12✔
240
        }
241

242
        if (style === 'no-sub') {
79✔
243
            if (kind === TokenKind.Sub) {
11✔
244
                diagnostics.push(messages.expectedFunctionKeyword(fun, `(always use 'function')`));
5✔
245
            }
246
            return;
11✔
247
        }
248

249
        // auto
250
        if (hasReturnedValue) {
68✔
251
            if (kind !== TokenKind.Function) {
6✔
252
                diagnostics.push(messages.expectedFunctionKeyword(fun, `(use 'function' when a value is returned)`));
2✔
253
            }
254
        } else if (kind !== TokenKind.Sub) {
62✔
255
            diagnostics.push(messages.expectedSubKeyword(fun, `(use 'sub' when no value is returned)`));
4✔
256
        }
257
    }
258

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

275
    walkChildren(severity: Readonly<BsLintRules>, children: SGNode[], diagnostics: (Omit<BsDiagnostic, 'file'>)[]) {
276
        children.forEach(node => {
×
277
            const colorAttr = node.getAttribute('color');
×
278
            if (colorAttr) {
×
279
                // debugger;
280
                const validateColorStyle = createColorValidator(severity);
×
281
                validateColorStyle(colorAttr.value.text, node.tag.range, diagnostics);
×
282
            }
283
        });
284
    }
285
}
286

287
/**
288
 * Collect indexes of non-inline AA members
289
 */
290
export function collectWrappingAAMembersIndexes(aa: AALiteralExpression): number[] {
1✔
291
    const indexes: number[] = [];
38✔
292
    const { elements } = aa;
38✔
293
    const lastIndex = elements.length - 1;
38✔
294
    for (let i = 0; i < lastIndex; i++) {
38✔
295
        const e = elements[i];
82✔
296
        if (isCommentStatement(e)) {
82✔
297
            continue;
9✔
298
        }
299
        const ne = elements[i + 1];
73✔
300
        const hasNL = isCommentStatement(ne) || ne.range.start.line > e.range.end.line;
73✔
301
        if (hasNL) {
73✔
302
            indexes.push(i);
47✔
303
        }
304
    }
305
    const last = elements[lastIndex];
38✔
306
    if (last && !isCommentStatement(last)) {
38✔
307
        indexes.push(lastIndex);
29✔
308
    }
309
    return indexes;
38✔
310
}
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