All files / PARSE/2-Parse CommentScanner.ts

100% Statements 56/56
100% Branches 25/25
100% Functions 6/6
100% Lines 51/51

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 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156                                                7195x     7195x             522x 1x     521x 521x     521x 521x 14452x 14452x 491x 491x 489x         521x             2279x       2279x   84x 84x 208x 208x 207x     84x             4x       4x   3x 3x 3x   3x   3x   2x 2x 1x     3x             701x 701x         700x   30x 30x 30x   651x 651x 651x   16x 16x 16x   3x     697x                           6x   1x   2x   3x            
import { CommonTokenStream, Token } from "antlr4ng";
 
import { CNextLexer } from "./grammar/CNextLexer";
import ECommentType from "../../types/ECommentType";
import IComment from "../../types/IComment";
 
/**
 * Reads comments off the HIDDEN channel of a parsed token stream (ADR-043).
 *
 * #1322: extracted from `CommentExtractor`, which was one class doing two
 * passes' work. Reading what comments a file contains is a fact about the
 * PARSE -- codegen needs it to re-attach comments to generated declarations,
 * and it decides nothing about whether the program is legal. Checking those
 * comments against MISRA C:2012 Rules 3.1 and 3.2 is a rejection, and belongs
 * in 2.1.
 *
 * While they were one class, `output/` had to import an analyzer to get at the
 * extraction half -- the edge `render-cannot-import-analyzers` forbids, and
 * the reason standing up `src/TRANSPILE/1-Analyze/` would otherwise have been
 * a rename rather than a boundary.
 */
class CommentScanner {
  private readonly tokenStream: CommonTokenStream;
 
  private comments: IComment[] | null = null;
 
  constructor(tokenStream: CommonTokenStream) {
    this.tokenStream = tokenStream;
  }
 
  /**
   * Extract all comments from the token stream
   */
  extractAll(): IComment[] {
    if (this.comments !== null) {
      return this.comments;
    }
 
    this.tokenStream.fill();
    this.comments = [];
 
    // Get all tokens and filter for comments on HIDDEN channel
    const size = this.tokenStream.size;
    for (let i = 0; i < size; i++) {
      const token = this.tokenStream.get(i);
      if (token.channel === Token.HIDDEN_CHANNEL) {
        const comment = this.tokenToComment(token);
        if (comment) {
          this.comments.push(comment);
        }
      }
    }
 
    return this.comments;
  }
 
  /**
   * Get comments that appear before a given token index
   */
  getCommentsBefore(tokenIndex: number): IComment[] {
    const hiddenTokens = this.tokenStream.getHiddenTokensToLeft(
      tokenIndex,
      Token.HIDDEN_CHANNEL,
    );
    if (!hiddenTokens) return [];
 
    const comments: IComment[] = [];
    for (const token of hiddenTokens) {
      const comment = this.tokenToComment(token);
      if (comment) {
        comments.push(comment);
      }
    }
    return comments;
  }
 
  /**
   * Get inline comments that appear after a given token index (same line)
   */
  getCommentsAfter(tokenIndex: number): IComment[] {
    const hiddenTokens = this.tokenStream.getHiddenTokensToRight(
      tokenIndex,
      Token.HIDDEN_CHANNEL,
    );
    if (!hiddenTokens) return [];
 
    const comments: IComment[] = [];
    const sourceToken = this.tokenStream.get(tokenIndex);
    const sourceLine = sourceToken.line;
 
    for (const token of hiddenTokens) {
      // Only include comments on the same line (inline comments)
      if (token.line !== sourceLine) break;
 
      const comment = this.tokenToComment(token);
      if (comment) {
        comments.push(comment);
      }
    }
    return comments;
  }
 
  /**
   * Convert a token to an IComment, or null if not a comment token
   */
  private tokenToComment(token: Token): IComment | null {
    const text = token.text;
    if (!text) return null;
 
    let type: ECommentType;
    let content: string;
 
    switch (token.type) {
      case CNextLexer.DOC_COMMENT:
        type = ECommentType.Doc;
        content = text.slice(3).trim(); // Remove ///
        break;
      case CNextLexer.LINE_COMMENT:
        type = ECommentType.Line;
        content = text.slice(2); // Remove //
        break;
      case CNextLexer.BLOCK_COMMENT:
        type = ECommentType.Block;
        content = text.slice(2, -2); // Remove /* and */
        break;
      default:
        return null; // Not a comment token (e.g., whitespace)
    }
 
    return {
      type,
      raw: text,
      content,
      line: token.line,
      column: token.column,
      tokenIndex: token.tokenIndex,
    };
  }
 
  /**
   * Get the length of the comment marker for column calculation
   */
  static markerLength(type: ECommentType): number {
    switch (type) {
      case ECommentType.Doc:
        return 3; // ///
      case ECommentType.Line:
        return 2; // //
      case ECommentType.Block:
        return 2; // /*
    }
  }
}
 
export default CommentScanner;