All files / server/services/compiler compiler-output-parser.ts

94.11% Statements 48/51
93.33% Branches 28/30
93.33% Functions 14/15
95% Lines 38/40

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                                                                  57x 57x 5037x 5003x       57x 26x 22x   7x 7x         7x                   57x         57x 57x   57x 4969x 4969x 4969x     4969x 4969x             4969x 4951x           4969x   57x       5060x 2x       5037x 57x 5001x     57x   1x 2x 1x 4x   1x                           5010x 5009x     1x                   22x    
/**
 * Compiler Output Parser
 * 
 * Centralizes Arduino CLI output parsing logic including:
 * - Error/warning extraction from gcc-style error messages
 * - Deduplication of error entries
 * - Fallback generic error parsing when regex doesn't match
 */
 
import { basename } from "node:path";
 
export interface CompilationError {
  file: string;
  line: number;
  column: number;
  type: 'error' | 'warning';
  message: string;
}
 
export class CompilerOutputParser {
  /**
   * Parse compiler stderr output into structured error list.
   * 
   * Handles patterns like:
   * - 'file:line:column: error: message'
   * - 'file:line: error: message' (column optional)
   * - Falls back to per-line generic errors if regex doesn't match
   * 
   * @param stderr Raw stderr output from arduino-cli
   * @param lineOffset Optional offset to adjust line numbers (e.g., header injection)
   * @returns Array of structured compilation errors/warnings
   */
  static parseErrors(stderr: string, lineOffset: number = 0): CompilationError[] {
    const results = parseStructuredErrors(stderr, lineOffset);
    appendUnique(results, parseMemoryOverflow(stderr));
    for (const line of stderr.split(/\r?\n/).filter((l) => l.trim())) {
      appendUnique(results, parseLibraryScanWarning(line));
    }
 
    // if nothing parsed but stderr is present, create generic entries per line
    if (results.length === 0 && stderr.trim()) {
      for (const line of stderr.split(/\r?\n/).filter((l) => l.trim())) {
        if (isInformationalCompilerLine(line)) continue;
 
        const libraryWarning = parseLibraryScanWarning(line);
        Iif (libraryWarning) {
          results.push(libraryWarning);
          continue;
        }
 
        results.push({
          file: "",
          line: 0,
          column: 0,
          type: "error",
          message: line.trim(),
        });
      }
    }
 
    return results;
  }
}
 
function parseStructuredErrors(stderr: string, lineOffset: number): CompilationError[] {
  const regex = /^([^:\n]+):(\d+)(?::(\d+))?: +(warning|error): +([^\n]*)$/gm; // NOSONAR S5843
  const results: CompilationError[] = [];
  let match: RegExpExecArray | null;
  while ((match = regex.exec(stderr))) {
    const [, rawFile, lineStr, colStr, type, message] = match;
    const file = basename(rawFile);
    const line = lineOffset > 0
      ? Math.max(1, Number.parseInt(lineStr, 10) - lineOffset)
      : Number.parseInt(lineStr, 10);
    const column = colStr ? Number.parseInt(colStr, 10) : 0;
    const item: CompilationError = {
      file,
      line,
      column,
      type: type as 'error' | 'warning',
      message,
    };
    const duplicate = results.some((result) =>
      result.file === item.file &&
      result.line === item.line &&
      result.column === item.column &&
      result.type === item.type &&
      result.message === item.message,
    );
    if (!duplicate) results.push(item);
  }
  return results;
}
 
function appendUnique(results: CompilationError[], item: CompilationError | undefined): void {
  if (!item || results.some((result) => result.message === item.message)) return;
  results.push(item);
}
 
function parseMemoryOverflow(output: string): CompilationError | undefined {
  const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
  const hasMemoryOverflow = lines.some((line) =>
    /not enough memory|data section exceeds available space(?: in| on) board/i.test(line),
  );
 
  if (!hasMemoryOverflow) return undefined;
 
  const flashUsage = lines.find((line) => /^sketch uses\b|^der sketch verwendet\b/i.test(line));
  const sramUsage = lines.find((line) => /^global variables use\b|^globale variablen verwenden\b/i.test(line));
  const details = [flashUsage, sramUsage].filter(Boolean).join(" ");
  const reason = lines.find((line) => /data section exceeds available space/i.test(line));
 
  return {
    file: "",
    line: 0,
    column: 0,
    type: "error",
    message: [
      "Not enough memory",
      reason ? `${reason}.` : "Memory usage exceeds the board limit.",
      details,
    ].filter(Boolean).join(" "),
  };
}
 
function parseLibraryScanWarning(line: string): CompilationError | undefined {
  if (!/(?:warning\s*:\s*library\b|multiple libraries were found|library .*\b(?:not found|incompatible|failed)\b)/i.test(line)) {
    return undefined;
  }
 
  return {
    file: "",
    line: 0,
    column: 0,
    type: "warning",
    message: line.trim().replace(/^warning\s*:\s*/i, ""),
  };
}
 
function isInformationalCompilerLine(line: string): boolean {
  return /^(?:sketch uses|global variables use|der sketch verwendet|globale variablen verwenden)\b/i.test(line.trim());
}