All files / cnx/PARSE/2-Parse GrammarCoverageListener.ts

100% Statements 16/16
100% Branches 10/10
100% Functions 6/6
100% Lines 15/15

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92                                          13x 13x         13x 13x               11x 11x 10x 10x                                 10x   10x       9x 9x 8x 8x                               13x                    
/**
 * Grammar Coverage Listener
 * Tracks which ANTLR grammar rules are executed during parsing
 *
 * This listener attaches to the parser and records:
 * - Parser rules visited (e.g., program, expression, statement)
 * - Lexer rules matched (e.g., IDENTIFIER, INTEGER_LITERAL)
 *
 * Used to identify dead grammar code and untested language constructs.
 */
 
import {
  ErrorNode,
  ParserRuleContext,
  ParseTreeListener,
  TerminalNode,
} from "antlr4ng";
import IGrammarCoverageReport from "../../types/IGrammarCoverageReport";
import GrammarCoverageReportBuilder from "./types/GrammarCoverageReportBuilder";
 
class GrammarCoverageListener implements ParseTreeListener {
  private readonly parserRuleVisits: Map<string, number> = new Map();
  private readonly lexerRuleVisits: Map<string, number> = new Map();
  private readonly parserRuleNames: string[];
  private readonly lexerRuleNames: string[];
 
  constructor(parserRuleNames: string[], lexerRuleNames: string[]) {
    this.parserRuleNames = parserRuleNames;
    this.lexerRuleNames = lexerRuleNames;
  }
 
  /**
   * Called when entering every parser rule
   * @public ParseTreeWalker calls this through ParseTreeListener; no caller names it
   */
  enterEveryRule(ctx: ParserRuleContext): void {
    const ruleName = this.parserRuleNames[ctx.ruleIndex];
    if (ruleName) {
      const count = this.parserRuleVisits.get(ruleName) || 0;
      this.parserRuleVisits.set(ruleName, count + 1);
    }
  }
 
  /**
   * Called when exiting every parser rule
   * @public ParseTreeWalker calls this through ParseTreeListener; no caller names it
   */
  exitEveryRule(_ctx: ParserRuleContext): void {
    // Not needed for coverage tracking
  }
 
  /**
   * Called when visiting a terminal node (token)
   * @public ParseTreeWalker calls this through ParseTreeListener; no caller names it
   */
  visitTerminal(node: TerminalNode): void {
    const tokenType = node.symbol.type;
    // Token type -1 is EOF, skip it
    if (tokenType < 0) return;
 
    // Token types are 1-indexed in ANTLR, but the array is 0-indexed
    // The first element (index 0) corresponds to token type 1
    const ruleName = this.lexerRuleNames[tokenType - 1];
    if (ruleName) {
      const count = this.lexerRuleVisits.get(ruleName) || 0;
      this.lexerRuleVisits.set(ruleName, count + 1);
    }
  }
 
  /**
   * Called when visiting an error node
   * @public ParseTreeWalker calls this through ParseTreeListener; no caller names it
   */
  visitErrorNode(_node: ErrorNode): void {
    // Track error nodes if needed in the future
  }
 
  /**
   * Generate a coverage report
   */
  getReport(): IGrammarCoverageReport {
    return GrammarCoverageReportBuilder.build({
      parserRuleNames: this.parserRuleNames,
      lexerRuleNames: this.lexerRuleNames,
      parserRuleVisits: this.parserRuleVisits,
      lexerRuleVisits: this.lexerRuleVisits,
    });
  }
}
 
export default GrammarCoverageListener;