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

rokucommunity / brs / #294

24 Apr 2023 07:40PM UTC coverage: 91.705% (+5.4%) from 86.275%
#294

push

web-flow
Merge pull request #1 from rokucommunity/adoption

refactor in preparation for adoption the project

1736 of 2013 branches covered (86.24%)

Branch coverage included in aggregate %.

5152 of 5498 relevant lines covered (93.71%)

8882.84 hits per line

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

96.73
/src/coverage/FileCoverage.ts
1
// tslint:disable-next-line
2
import type { FileCoverageData } from "istanbul-lib-coverage";
3
import { Stmt, Expr } from "../parser";
130✔
4
import { Location, isToken, Lexeme } from "../lexer";
130✔
5
import { BrsInvalid, BrsType } from "../brsTypes";
130✔
6
import { isStatement } from "../parser/Statement";
130✔
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> {
130✔
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);
139✔
27
        return this.statements.get(key);
139✔
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;
329✔
45
        let kind = isStatement(statement) ? "stmt" : "expr";
329✔
46
        return `${kind}:${statement.type}:${start.line},${start.column}-${end.line},${end.column}`;
329✔
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);
103✔
55
        if (coverage) {
103✔
56
            coverage.hits++;
103✔
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
    visitFor(statement: Stmt.For) {
259
        this.execute(statement.counterDeclaration);
1✔
260
        this.evaluate(statement.counterDeclaration.value);
1✔
261
        this.evaluate(statement.finalValue);
1✔
262
        this.evaluate(statement.increment);
1✔
263
        this.execute(statement.body);
1✔
264

265
        return BrsInvalid.Instance;
1✔
266
    }
267

268
    visitForEach(statement: Stmt.ForEach) {
269
        this.evaluate(statement.target);
1✔
270
        this.execute(statement.body);
1✔
271

272
        return BrsInvalid.Instance;
1✔
273
    }
274

275
    visitWhile(statement: Stmt.While) {
276
        this.evaluate(statement.condition);
1✔
277
        this.execute(statement.body);
1✔
278

279
        return BrsInvalid.Instance;
1✔
280
    }
281

282
    visitNamedFunction(statement: Stmt.Function) {
283
        // don't record the Expr.Function so that we don't double-count named functions.
284
        this.execute(statement.func.body);
4✔
285
        return BrsInvalid.Instance;
4✔
286
    }
287

288
    visitReturn(statement: Stmt.Return): never {
289
        if (!statement.value) {
1!
290
            throw new Stmt.ReturnValue(statement.tokens.return.location);
×
291
        }
292

293
        let toReturn = this.evaluate(statement.value);
1✔
294
        throw new Stmt.ReturnValue(statement.tokens.return.location, toReturn);
1✔
295
    }
296

297
    visitDottedSet(statement: Stmt.DottedSet) {
298
        this.evaluate(statement.obj);
1✔
299
        this.evaluate(statement.value);
1✔
300

301
        return BrsInvalid.Instance;
1✔
302
    }
303

304
    visitIndexedSet(statement: Stmt.IndexedSet) {
305
        this.evaluate(statement.obj);
2✔
306
        this.evaluate(statement.index);
2✔
307
        this.evaluate(statement.value);
2✔
308

309
        return BrsInvalid.Instance;
2✔
310
    }
311

312
    visitIncrement(statement: Stmt.Increment) {
313
        this.evaluate(statement.value);
1✔
314

315
        return BrsInvalid.Instance;
1✔
316
    }
317

318
    visitLibrary(statement: Stmt.Library) {
319
        return BrsInvalid.Instance;
×
320
    }
321

322
    visitDim(statement: Stmt.Dim) {
323
        statement.dimensions.forEach((expr) => this.evaluate(expr));
1✔
324
        return BrsInvalid.Instance;
1✔
325
    }
326

327
    /**
328
     * EXPRESSIONS
329
     */
330

331
    visitBinary(expression: Expr.Binary) {
332
        this.evaluate(expression.left);
9✔
333
        this.evaluate(expression.right);
9✔
334
        return BrsInvalid.Instance;
9✔
335
    }
336

337
    visitCall(expression: Expr.Call) {
338
        this.evaluate(expression.callee);
3✔
339
        expression.args.map(this.evaluate, this);
3✔
340
        return BrsInvalid.Instance;
3✔
341
    }
342

343
    visitAnonymousFunction(func: Expr.Function) {
344
        this.execute(func.body);
2✔
345
        return BrsInvalid.Instance;
2✔
346
    }
347

348
    visitDottedGet(expression: Expr.DottedGet) {
349
        this.evaluate(expression.obj);
2✔
350
        return BrsInvalid.Instance;
2✔
351
    }
352

353
    visitIndexedGet(expression: Expr.IndexedGet) {
354
        this.evaluate(expression.obj);
1✔
355
        this.evaluate(expression.index);
1✔
356
        return BrsInvalid.Instance;
1✔
357
    }
358

359
    visitGrouping(expression: Expr.Grouping) {
360
        this.evaluate(expression.expression);
1✔
361
        return BrsInvalid.Instance;
1✔
362
    }
363

364
    visitLiteral(expression: Expr.Literal) {
365
        return BrsInvalid.Instance;
68✔
366
    }
367

368
    visitArrayLiteral(expression: Expr.ArrayLiteral) {
369
        expression.elements.forEach((expr) => this.evaluate(expr));
6✔
370
        return BrsInvalid.Instance;
2✔
371
    }
372

373
    visitAALiteral(expression: Expr.AALiteral) {
374
        expression.elements.forEach((member) => this.evaluate(member.value));
2✔
375
        return BrsInvalid.Instance;
2✔
376
    }
377

378
    visitUnary(expression: Expr.Unary) {
379
        this.evaluate(expression.right);
1✔
380
        return BrsInvalid.Instance;
1✔
381
    }
382

383
    visitVariable(expression: Expr.Variable) {
384
        return BrsInvalid.Instance;
12✔
385
    }
386

387
    evaluate(this: FileCoverage, expression: Expr.Expression) {
388
        this.add(expression);
93✔
389
        return expression.accept<BrsType>(this);
93✔
390
    }
391

392
    execute(this: FileCoverage, statement: Stmt.Statement): BrsType {
393
        this.add(statement);
81✔
394

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

411
        return BrsInvalid.Instance;
3✔
412
    }
413
}
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