All files / transpiler/logic/analysis GrammarCoverageListener.ts

100% Statements 26/26
100% Branches 14/14
100% Functions 10/10
100% Lines 25/25

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 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124                                          17x 17x         17x 17x             16x 16x 15x 15x                             14x   14x       13x 13x 12x 12x                             1x 2x 2x   1x 2x 2x               1x 1x             7x             6x             6x                    
/**
 * 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
   */
  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
   */
  exitEveryRule(_ctx: ParserRuleContext): void {
    // Not needed for coverage tracking
  }
 
  /**
   * Called when visiting a terminal node (token)
   */
  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
   */
  visitErrorNode(_node: ErrorNode): void {
    // Track error nodes if needed in the future
  }
 
  /**
   * Merge coverage from another listener (for aggregating across files)
   */
  merge(other: GrammarCoverageListener): void {
    for (const [rule, count] of other.parserRuleVisits) {
      const current = this.parserRuleVisits.get(rule) || 0;
      this.parserRuleVisits.set(rule, current + count);
    }
    for (const [rule, count] of other.lexerRuleVisits) {
      const current = this.lexerRuleVisits.get(rule) || 0;
      this.lexerRuleVisits.set(rule, current + count);
    }
  }
 
  /**
   * Reset all coverage counters
   */
  reset(): void {
    this.parserRuleVisits.clear();
    this.lexerRuleVisits.clear();
  }
 
  /**
   * Get the current parser rule visit counts
   */
  getParserRuleVisits(): Map<string, number> {
    return new Map(this.parserRuleVisits);
  }
 
  /**
   * Get the current lexer rule visit counts
   */
  getLexerRuleVisits(): Map<string, number> {
    return new Map(this.lexerRuleVisits);
  }
 
  /**
   * Generate a coverage report
   */
  getReport(): IGrammarCoverageReport {
    return GrammarCoverageReportBuilder.build({
      parserRuleNames: this.parserRuleNames,
      lexerRuleNames: this.lexerRuleNames,
      parserRuleVisits: this.parserRuleVisits,
      lexerRuleVisits: this.lexerRuleVisits,
    });
  }
}
 
export default GrammarCoverageListener;