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

rokucommunity / brs / #305

18 Jan 2024 09:05PM UTC coverage: 91.463% (+5.2%) from 86.214%
#305

push

TwitchBronBron
0.45.4

1796 of 2095 branches covered (85.73%)

Branch coverage included in aggregate %.

5275 of 5636 relevant lines covered (93.59%)

8947.19 hits per line

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

96.28
/src/coverage/FileCoverage.ts
1
// tslint:disable-next-line
2
import type { FileCoverageData } from "istanbul-lib-coverage";
3
import { Stmt, Expr } from "../parser";
131✔
4
import { Location, isToken, Lexeme } from "../lexer";
131✔
5
import { BrsInvalid, BrsType } from "../brsTypes";
131✔
6
import { isStatement } from "../parser/Statement";
131✔
7

8
/** Keeps track of the number of hits on a given statement or expression. */
9
interface StatementCoverage {
10
    /** Number of times the interpreter has executed/evaluated this statement. */
11
    hits: number;
12
    /** Combine expressions and statements because we need to log some expressions to get the coverage report. */
13
    statement: Expr.Expression | Stmt.Statement;
14
}
15

16
export class FileCoverage implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType> {
131✔
17
    private statements = new Map<string, StatementCoverage>();
37✔
18

19
    constructor(readonly filePath: string) {}
37✔
20

21
    /**
22
     * Returns the StatementCoverage object for a given statement.
23
     * @param statement statement for which to get coverage.
24
     */
25
    private get(statement: Expr.Expression | Stmt.Statement) {
26
        let key = this.getStatementKey(statement);
138✔
27
        return this.statements.get(key);
138✔
28
    }
29

30
    /**
31
     * Creates a StatementCoverage object for a given statement.
32
     * @param statement statement to add.
33
     */
34
    private add(statement: Expr.Expression | Stmt.Statement) {
35
        let key = this.getStatementKey(statement);
174✔
36
        this.statements.set(key, { hits: 0, statement });
174✔
37
    }
38

39
    /**
40
     * Generates a key for the statement using its location and type.
41
     * @param statement statement for which to generate a key.
42
     */
43
    getStatementKey(statement: Expr.Expression | Stmt.Statement) {
44
        let { start, end } = statement.location;
328✔
45
        let kind = isStatement(statement) ? "stmt" : "expr";
328✔
46
        return `${kind}:${statement.type}:${start.line},${start.column}-${end.line},${end.column}`;
328✔
47
    }
48

49
    /**
50
     * Logs a hit to a particular statement, indicating the statement was used.
51
     * @param statement statement for which to log a hit
52
     */
53
    logHit(statement: Expr.Expression | Stmt.Statement) {
54
        let coverage = this.get(statement);
102✔
55
        if (coverage) {
102✔
56
            coverage.hits++;
102✔
57
        }
58
    }
59

60
    /**
61
     * Converts the coverage data to a POJO that's more friendly for consumers.
62
     */
63
    getCoverage(): FileCoverageData {
64
        let coverageSummary: FileCoverageData = {
37✔
65
            path: this.filePath,
66
            statementMap: {},
67
            fnMap: {},
68
            branchMap: {},
69
            s: {},
70
            f: {},
71
            b: {},
72
        };
73

74
        this.statements.forEach(({ statement, hits }, key) => {
37✔
75
            if (statement instanceof Stmt.If) {
162✔
76
                let locations: Location[] = [];
6✔
77
                let branchHits: number[] = [];
6✔
78

79
                // Add the "if" coverage
80
                let thenBranchCoverage = this.get(statement.thenBranch);
6✔
81
                if (thenBranchCoverage) {
6✔
82
                    locations.push({
6✔
83
                        ...statement.location,
84
                        end: statement.condition.location.end,
85
                    });
86
                    branchHits.push(thenBranchCoverage.hits);
6✔
87
                }
88

89
                // the condition is a statement as well as a branch, so put it in the statement map
90
                let ifCondition = this.get(statement.condition);
6✔
91
                if (ifCondition) {
6✔
92
                    coverageSummary.statementMap[`${key}.if`] = statement.condition.location;
6✔
93
                    coverageSummary.s[`${key}.if`] = ifCondition.hits;
6✔
94
                }
95

96
                // Add the "else if" coverage
97
                statement.elseIfs?.forEach((branch, index) => {
6!
98
                    let elseIfCoverage = this.get(branch.condition);
4✔
99
                    if (elseIfCoverage) {
4✔
100
                        // the condition is a statement as well as a branch, so put it in the statement map
101
                        coverageSummary.statementMap[`${key}.elseif-${index}`] =
4✔
102
                            branch.condition.location;
103
                        coverageSummary.s[`${key}.elseif-${index}`] = elseIfCoverage.hits;
4✔
104

105
                        // add to the list of branches
106
                        let elseIfBlock = this.get(branch.thenBranch);
4✔
107
                        if (elseIfBlock) {
4✔
108
                            // use the tokens as the start for the branch rather than the condition
109
                            let start =
110
                                statement.tokens.elseIfs?.[index].location.start ||
4✔
111
                                branch.condition.location.start;
112
                            locations.push({ ...branch.condition.location, start });
4✔
113
                            branchHits.push(elseIfBlock.hits);
4✔
114
                        }
115
                    }
116
                });
117

118
                // Add the "else" coverage
119
                if (statement.elseBranch) {
6✔
120
                    let elseCoverage = this.get(statement.elseBranch);
4✔
121
                    if (elseCoverage) {
4✔
122
                        // use the tokens as the start rather than the condition
123
                        let start =
124
                            statement.tokens.else?.location.start ||
4✔
125
                            statement.elseBranch.location.start;
126
                        locations.push({ ...statement.elseBranch.location, start });
4✔
127
                        branchHits.push(elseCoverage.hits);
4✔
128
                    }
129
                }
130

131
                coverageSummary.branchMap[key] = {
6✔
132
                    loc: statement.location,
133
                    type: "if",
134
                    locations,
135
                    line: statement.location.start.line,
136
                };
137
                coverageSummary.b[key] = branchHits;
6✔
138
            } else if (statement instanceof Stmt.Function) {
156✔
139
                // Named functions
140
                let functionCoverage = this.get(statement.func.body);
4✔
141
                if (functionCoverage) {
4✔
142
                    coverageSummary.fnMap[key] = {
4✔
143
                        name: statement.name.text,
144
                        loc: statement.location,
145
                        decl: {
146
                            ...statement.func.keyword.location,
147
                            end: statement.name.location.end,
148
                        },
149
                        line: statement.location.start.line,
150
                    };
151
                    coverageSummary.f[key] = functionCoverage.hits;
4✔
152
                }
153
            } else if (statement instanceof Expr.Function) {
152✔
154
                // Anonymous functions
155
                let functionCoverage = this.get(statement.body);
2✔
156
                if (functionCoverage) {
2✔
157
                    coverageSummary.fnMap[key] = {
2✔
158
                        name: "[Function]",
159
                        loc: statement.location,
160
                        decl: statement.keyword.location,
161
                        line: statement.location.start.line,
162
                    };
163
                    coverageSummary.f[key] = functionCoverage.hits;
2✔
164
                }
165
            } else if (
150✔
166
                statement instanceof Expr.Binary &&
166✔
167
                (statement.token.kind === Lexeme.And || statement.token.kind === Lexeme.Or)
168
            ) {
169
                let locations: Location[] = [];
3✔
170
                let branchHits: number[] = [];
3✔
171

172
                let leftCoverage = this.get(statement.left);
3✔
173
                if (leftCoverage) {
3✔
174
                    locations.push(statement.left.location);
3✔
175
                    branchHits.push(leftCoverage.hits);
3✔
176
                }
177
                let rightCoverage = this.get(statement.right);
3✔
178
                if (rightCoverage) {
3✔
179
                    locations.push(statement.right.location);
3✔
180
                    branchHits.push(rightCoverage.hits);
3✔
181
                }
182

183
                coverageSummary.branchMap[key] = {
3✔
184
                    loc: statement.location,
185
                    type: statement.token.kind,
186
                    locations,
187
                    line: statement.location.start.line,
188
                };
189
                coverageSummary.b[key] = branchHits;
3✔
190

191
                // this is a statement as well as a branch, so put it in the statement map
192
                coverageSummary.statementMap[key] = statement.location;
3✔
193
                coverageSummary.s[key] = hits;
3✔
194
            } else if (
147✔
195
                isStatement(statement) &&
208✔
196
                !(statement instanceof Stmt.Block) // blocks are part of other statements, so don't include them
197
            ) {
198
                coverageSummary.statementMap[key] = statement.location;
37✔
199
                coverageSummary.s[key] = hits;
37✔
200
            }
201
        });
202

203
        return coverageSummary;
37✔
204
    }
205

206
    /**
207
     *  STATEMENTS
208
     */
209

210
    visitAssignment(statement: Stmt.Assignment) {
211
        this.evaluate(statement.value);
16✔
212
        return BrsInvalid.Instance;
16✔
213
    }
214

215
    visitExpression(statement: Stmt.Expression) {
216
        this.evaluate(statement.expression);
6✔
217
        return BrsInvalid.Instance;
6✔
218
    }
219

220
    visitExitFor(statement: Stmt.ExitFor): never {
221
        throw new Stmt.ExitForReason(statement.location);
1✔
222
    }
223

224
    visitExitWhile(statement: Stmt.ExitWhile): never {
225
        throw new Stmt.ExitWhileReason(statement.location);
1✔
226
    }
227

228
    visitPrint(statement: Stmt.Print) {
229
        statement.expressions.forEach((exprOrToken) => {
4✔
230
            if (!isToken(exprOrToken)) {
6✔
231
                this.evaluate(exprOrToken);
6✔
232
            }
233
        });
234
        return BrsInvalid.Instance;
4✔
235
    }
236

237
    visitIf(statement: Stmt.If) {
238
        this.evaluate(statement.condition);
6✔
239
        this.execute(statement.thenBranch);
6✔
240

241
        statement.elseIfs?.forEach((elseIf) => {
6!
242
            this.evaluate(elseIf.condition);
4✔
243
            this.execute(elseIf.thenBranch);
4✔
244
        });
245

246
        if (statement.elseBranch) {
6✔
247
            this.execute(statement.elseBranch);
4✔
248
        }
249

250
        return BrsInvalid.Instance;
6✔
251
    }
252

253
    visitBlock(block: Stmt.Block) {
254
        block.statements.forEach((statement) => this.execute(statement));
24✔
255
        return BrsInvalid.Instance;
24✔
256
    }
257

258
    visitTryCatch(statement: Stmt.TryCatch) {
259
        // TODO: implement statement/expression coverage for try/catch
260
        return BrsInvalid.Instance;
×
261
    }
262

263
    visitFor(statement: Stmt.For) {
264
        this.execute(statement.counterDeclaration);
1✔
265
        this.evaluate(statement.counterDeclaration.value);
1✔
266
        this.evaluate(statement.finalValue);
1✔
267
        this.evaluate(statement.increment);
1✔
268
        this.execute(statement.body);
1✔
269

270
        return BrsInvalid.Instance;
1✔
271
    }
272

273
    visitForEach(statement: Stmt.ForEach) {
274
        this.evaluate(statement.target);
1✔
275
        this.execute(statement.body);
1✔
276

277
        return BrsInvalid.Instance;
1✔
278
    }
279

280
    visitWhile(statement: Stmt.While) {
281
        this.evaluate(statement.condition);
1✔
282
        this.execute(statement.body);
1✔
283

284
        return BrsInvalid.Instance;
1✔
285
    }
286

287
    visitNamedFunction(statement: Stmt.Function) {
288
        // don't record the Expr.Function so that we don't double-count named functions.
289
        this.execute(statement.func.body);
4✔
290
        return BrsInvalid.Instance;
4✔
291
    }
292

293
    visitReturn(statement: Stmt.Return): never {
294
        if (!statement.value) {
1!
295
            throw new Stmt.ReturnValue(statement.tokens.return.location);
×
296
        }
297

298
        let toReturn = this.evaluate(statement.value);
1✔
299
        throw new Stmt.ReturnValue(statement.tokens.return.location, toReturn);
1✔
300
    }
301

302
    visitDottedSet(statement: Stmt.DottedSet) {
303
        this.evaluate(statement.obj);
1✔
304
        this.evaluate(statement.value);
1✔
305

306
        return BrsInvalid.Instance;
1✔
307
    }
308

309
    visitIndexedSet(statement: Stmt.IndexedSet) {
310
        this.evaluate(statement.obj);
2✔
311
        this.evaluate(statement.index);
2✔
312
        this.evaluate(statement.value);
2✔
313

314
        return BrsInvalid.Instance;
2✔
315
    }
316

317
    visitIncrement(statement: Stmt.Increment) {
318
        this.evaluate(statement.value);
1✔
319

320
        return BrsInvalid.Instance;
1✔
321
    }
322

323
    visitLibrary(statement: Stmt.Library) {
324
        return BrsInvalid.Instance;
×
325
    }
326

327
    visitDim(statement: Stmt.Dim) {
328
        statement.dimensions.forEach((expr) => this.evaluate(expr));
1✔
329
        return BrsInvalid.Instance;
1✔
330
    }
331

332
    /**
333
     * EXPRESSIONS
334
     */
335

336
    visitBinary(expression: Expr.Binary) {
337
        this.evaluate(expression.left);
9✔
338
        this.evaluate(expression.right);
9✔
339
        return BrsInvalid.Instance;
9✔
340
    }
341

342
    visitCall(expression: Expr.Call) {
343
        this.evaluate(expression.callee);
3✔
344
        expression.args.map(this.evaluate, this);
3✔
345
        return BrsInvalid.Instance;
3✔
346
    }
347

348
    visitAnonymousFunction(func: Expr.Function) {
349
        this.execute(func.body);
2✔
350
        return BrsInvalid.Instance;
2✔
351
    }
352

353
    visitDottedGet(expression: Expr.DottedGet) {
354
        this.evaluate(expression.obj);
2✔
355
        return BrsInvalid.Instance;
2✔
356
    }
357

358
    visitIndexedGet(expression: Expr.IndexedGet) {
359
        this.evaluate(expression.obj);
1✔
360
        this.evaluate(expression.index);
1✔
361
        return BrsInvalid.Instance;
1✔
362
    }
363

364
    visitGrouping(expression: Expr.Grouping) {
365
        this.evaluate(expression.expression);
1✔
366
        return BrsInvalid.Instance;
1✔
367
    }
368

369
    visitLiteral(expression: Expr.Literal) {
370
        return BrsInvalid.Instance;
68✔
371
    }
372

373
    visitArrayLiteral(expression: Expr.ArrayLiteral) {
374
        expression.elements.forEach((expr) => this.evaluate(expr));
6✔
375
        return BrsInvalid.Instance;
2✔
376
    }
377

378
    visitAALiteral(expression: Expr.AALiteral) {
379
        expression.elements.forEach((member) => this.evaluate(member.value));
2✔
380
        return BrsInvalid.Instance;
2✔
381
    }
382

383
    visitUnary(expression: Expr.Unary) {
384
        this.evaluate(expression.right);
1✔
385
        return BrsInvalid.Instance;
1✔
386
    }
387

388
    visitVariable(expression: Expr.Variable) {
389
        return BrsInvalid.Instance;
12✔
390
    }
391

392
    evaluate(this: FileCoverage, expression: Expr.Expression) {
393
        this.add(expression);
93✔
394
        return expression.accept<BrsType>(this);
93✔
395
    }
396

397
    execute(this: FileCoverage, statement: Stmt.Statement): BrsType {
398
        this.add(statement);
81✔
399

400
        try {
81✔
401
            return statement.accept<BrsType>(this);
81✔
402
        } catch (err) {
403
            if (
3!
404
                !(
405
                    err instanceof Stmt.ReturnValue ||
9✔
406
                    err instanceof Stmt.ExitFor ||
407
                    err instanceof Stmt.ExitForReason ||
408
                    err instanceof Stmt.ExitWhile ||
409
                    err instanceof Stmt.ExitWhileReason
410
                )
411
            ) {
412
                throw err;
×
413
            }
414
        }
415

416
        return BrsInvalid.Instance;
3✔
417
    }
418
}
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

© 2026 Coveralls, Inc