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

rokucommunity / brs / #103

01 May 2024 07:50PM UTC coverage: 89.383% (-0.2%) from 89.58%
#103

push

web-flow
Implement `try...catch` and `throw` (#72)

* Implemented try..catch and throw using existing interpreter.stack

* Added the Stack Trace to the Environment

* Updated e2e test case

* Updated Interpreter to use Roku Runtime Error messages

* Added stack trace back to Interpreter

* Improvements on try..catch error handling

* Added unit tests for Try Catch

* Re-added the return to prevent lint error on select case missing break

* Updated comment

* Removed unused references

* Code review recommendations

* Fixed Stack Trace test

* Removed redundant parameter in RuntimeError

* Changed parameter order on RuntimeError

* Prettier fix

* Renamed type ErrorCode to ErrorDetail

2096 of 2532 branches covered (82.78%)

Branch coverage included in aggregate %.

163 of 195 new or added lines in 12 files covered. (83.59%)

1 existing line in 1 file now uncovered.

5936 of 6454 relevant lines covered (91.97%)

29015.56 hits per line

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

94.95
/src/coverage/FileCoverage.ts
1
// tslint:disable-next-line
2
import type { FileCoverageData } from "istanbul-lib-coverage";
3
import { Stmt, Expr } from "../parser";
135✔
4
import { Location, isToken, Lexeme } from "../lexer";
135✔
5
import { BrsInvalid, BrsType } from "../brsTypes";
135✔
6
import { isStatement } from "../parser/Statement";
135✔
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> {
135✔
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
    visitContinueFor(statement: Stmt.ContinueFor): never {
221
        throw new Stmt.ContinueForReason(statement.location);
×
222
    }
223

224
    visitExitFor(statement: Stmt.ExitFor): never {
225
        throw new Stmt.ExitForReason(statement.location);
1✔
226
    }
227

228
    visitContinueWhile(statement: Stmt.ContinueWhile): never {
229
        throw new Stmt.ContinueWhileReason(statement.location);
×
230
    }
231

232
    visitExitWhile(statement: Stmt.ExitWhile): never {
233
        throw new Stmt.ExitWhileReason(statement.location);
1✔
234
    }
235

236
    visitPrint(statement: Stmt.Print) {
237
        statement.expressions.forEach((exprOrToken) => {
4✔
238
            if (!isToken(exprOrToken)) {
6✔
239
                this.evaluate(exprOrToken);
6✔
240
            }
241
        });
242
        return BrsInvalid.Instance;
4✔
243
    }
244

245
    visitIf(statement: Stmt.If) {
246
        this.evaluate(statement.condition);
6✔
247
        this.execute(statement.thenBranch);
6✔
248

249
        statement.elseIfs?.forEach((elseIf) => {
6!
250
            this.evaluate(elseIf.condition);
4✔
251
            this.execute(elseIf.thenBranch);
4✔
252
        });
253

254
        if (statement.elseBranch) {
6✔
255
            this.execute(statement.elseBranch);
4✔
256
        }
257

258
        return BrsInvalid.Instance;
6✔
259
    }
260

261
    visitBlock(block: Stmt.Block) {
262
        block.statements.forEach((statement) => this.execute(statement));
24✔
263
        return BrsInvalid.Instance;
24✔
264
    }
265

266
    visitTryCatch(statement: Stmt.TryCatch) {
267
        // TODO: implement statement/expression coverage for try/catch
268
        return BrsInvalid.Instance;
×
269
    }
270

271
    visitThrow(statement: Stmt.Throw) {
272
        // TODO: implement statement/expression coverage for throw
NEW
273
        return BrsInvalid.Instance;
×
274
    }
275

276
    visitFor(statement: Stmt.For) {
277
        this.execute(statement.counterDeclaration);
1✔
278
        this.evaluate(statement.counterDeclaration.value);
1✔
279
        this.evaluate(statement.finalValue);
1✔
280
        this.evaluate(statement.increment);
1✔
281
        this.execute(statement.body);
1✔
282

283
        return BrsInvalid.Instance;
1✔
284
    }
285

286
    visitForEach(statement: Stmt.ForEach) {
287
        this.evaluate(statement.target);
1✔
288
        this.execute(statement.body);
1✔
289

290
        return BrsInvalid.Instance;
1✔
291
    }
292

293
    visitWhile(statement: Stmt.While) {
294
        this.evaluate(statement.condition);
1✔
295
        this.execute(statement.body);
1✔
296

297
        return BrsInvalid.Instance;
1✔
298
    }
299

300
    visitNamedFunction(statement: Stmt.Function) {
301
        // don't record the Expr.Function so that we don't double-count named functions.
302
        this.execute(statement.func.body);
4✔
303
        return BrsInvalid.Instance;
4✔
304
    }
305

306
    visitReturn(statement: Stmt.Return): never {
307
        if (!statement.value) {
1!
308
            throw new Stmt.ReturnValue(statement.tokens.return.location);
×
309
        }
310

311
        let toReturn = this.evaluate(statement.value);
1✔
312
        throw new Stmt.ReturnValue(statement.tokens.return.location, toReturn);
1✔
313
    }
314

315
    visitDottedSet(statement: Stmt.DottedSet) {
316
        this.evaluate(statement.obj);
1✔
317
        this.evaluate(statement.value);
1✔
318

319
        return BrsInvalid.Instance;
1✔
320
    }
321

322
    visitIndexedSet(statement: Stmt.IndexedSet) {
323
        this.evaluate(statement.obj);
2✔
324
        this.evaluate(statement.index);
2✔
325
        this.evaluate(statement.value);
2✔
326

327
        return BrsInvalid.Instance;
2✔
328
    }
329

330
    visitIncrement(statement: Stmt.Increment) {
331
        this.evaluate(statement.value);
1✔
332

333
        return BrsInvalid.Instance;
1✔
334
    }
335

336
    visitLibrary(statement: Stmt.Library) {
337
        return BrsInvalid.Instance;
×
338
    }
339

340
    visitDim(statement: Stmt.Dim) {
341
        statement.dimensions.forEach((expr) => this.evaluate(expr));
1✔
342
        return BrsInvalid.Instance;
1✔
343
    }
344

345
    /**
346
     * EXPRESSIONS
347
     */
348

349
    visitBinary(expression: Expr.Binary) {
350
        this.evaluate(expression.left);
9✔
351
        this.evaluate(expression.right);
9✔
352
        return BrsInvalid.Instance;
9✔
353
    }
354

355
    visitCall(expression: Expr.Call) {
356
        this.evaluate(expression.callee);
3✔
357
        expression.args.map(this.evaluate, this);
3✔
358
        return BrsInvalid.Instance;
3✔
359
    }
360

361
    visitAnonymousFunction(func: Expr.Function) {
362
        this.execute(func.body);
2✔
363
        return BrsInvalid.Instance;
2✔
364
    }
365

366
    visitDottedGet(expression: Expr.DottedGet) {
367
        this.evaluate(expression.obj);
2✔
368
        return BrsInvalid.Instance;
2✔
369
    }
370

371
    visitIndexedGet(expression: Expr.IndexedGet) {
372
        this.evaluate(expression.obj);
1✔
373
        this.evaluate(expression.index);
1✔
374
        return BrsInvalid.Instance;
1✔
375
    }
376

377
    visitGrouping(expression: Expr.Grouping) {
378
        this.evaluate(expression.expression);
1✔
379
        return BrsInvalid.Instance;
1✔
380
    }
381

382
    visitLiteral(expression: Expr.Literal) {
383
        return BrsInvalid.Instance;
68✔
384
    }
385

386
    visitArrayLiteral(expression: Expr.ArrayLiteral) {
387
        expression.elements.forEach((expr) => this.evaluate(expr));
6✔
388
        return BrsInvalid.Instance;
2✔
389
    }
390

391
    visitAALiteral(expression: Expr.AALiteral) {
392
        expression.elements.forEach((member) => this.evaluate(member.value));
2✔
393
        return BrsInvalid.Instance;
2✔
394
    }
395

396
    visitUnary(expression: Expr.Unary) {
397
        this.evaluate(expression.right);
1✔
398
        return BrsInvalid.Instance;
1✔
399
    }
400

401
    visitVariable(expression: Expr.Variable) {
402
        return BrsInvalid.Instance;
12✔
403
    }
404

405
    evaluate(this: FileCoverage, expression: Expr.Expression) {
406
        this.add(expression);
93✔
407
        return expression.accept<BrsType>(this);
93✔
408
    }
409

410
    execute(this: FileCoverage, statement: Stmt.Statement): BrsType {
411
        this.add(statement);
81✔
412

413
        try {
81✔
414
            return statement.accept<BrsType>(this);
81✔
415
        } catch (err) {
416
            if (
3!
417
                !(
418
                    err instanceof Stmt.ReturnValue ||
9✔
419
                    err instanceof Stmt.ExitFor ||
420
                    err instanceof Stmt.ExitForReason ||
421
                    err instanceof Stmt.ExitWhile ||
422
                    err instanceof Stmt.ExitWhileReason
423
                )
424
            ) {
425
                throw err;
×
426
            }
427
        }
428

429
        return BrsInvalid.Instance;
3✔
430
    }
431
}
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