All files / server/services sketch-prototypes.ts

90% Statements 81/90
83.07% Branches 54/65
100% Functions 10/10
95.71% Lines 67/70

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                          24x 24x 24x 24x 24x                                   6x 6x 6x 6x       260x 6x 6x   254x 6x 6x 6x   248x 246x 243x 248x         31x 31x 31x 288x 288x     288x   288x 3x 285x 3x 282x 8x     23x         22x 260x 22x 25x 3x 6x 6x   3x       46x 46x 46x                         37x   45x 45x 45x 45x 45x   45x 84x 84x 84x 84x 84x 84x 20x 20x 20x 20x 20x     45x                     40x 40x 10x 10x    
/**
 * Arduino-IDE-compatible function prototypes for the local g++ build.
 *
 * The Arduino IDE declares every sketch function before the first function
 * definition, so a helper may be called before it is defined. The prototypes
 * go there, not at the top of the file, because their signatures may use
 * types and constants that the sketch defines first (a `typedef`, or `M` in
 * `double mat[M][N]`). Default arguments are left out: C++ allows them only
 * once, and the definition already carries them.
 */
 
import { maskCommentsPreservingLines } from "@shared/source-project";
 
const FUNCTION_DEFINITION = /^((?:\w[\w*&]*\s+)*\w[\w*&]*)\s+(\w+)\s*(\([^)]*\))\s*\{/gm;
const NOT_A_FUNCTION = new Set(["if", "else", "while", "for", "do", "switch", "case", "return", "break", "continue", "goto", "class", "struct", "union", "enum", "namespace", "typedef"]);
const ENTRYPOINTS = new Set(["setup", "loop", "main"]);
const OPENING_BRACKETS = "([{";
const CLOSING_BRACKETS = ")]}";
 
export interface ForwardDeclarationPlan {
  /** Newline-separated prototypes, empty when the sketch needs none. */
  declarations: string;
  /** Offset in the code where the prototypes belong; -1 when there is no function definition. */
  insertAt: number;
}
 
interface ParameterScan {
  parts: string[];
  current: string;
  depth: number;
  quote: string | null;
  escaped: boolean;
}
 
function scanQuoted(scan: ParameterScan, character: string): void {
  scan.current += character;
  Iif (scan.escaped) scan.escaped = false;
  else Iif (character === "\\") scan.escaped = true;
  else if (character === scan.quote) scan.quote = null;
}
 
function scanParameterCharacter(scan: ParameterScan, character: string): void {
  if (scan.quote) {
    scanQuoted(scan, character);
    return;
  }
  if (character === "," && scan.depth === 0) {
    scan.parts.push(scan.current);
    scan.current = "";
    return;
  }
  if (character === '"' || character === "'") scan.quote = character;
  else if (OPENING_BRACKETS.includes(character)) scan.depth++;
  else if (CLOSING_BRACKETS.includes(character)) scan.depth--;
  scan.current += character;
}
 
/** Index of the `=` that starts a default argument, ignoring `=` inside quotes or brackets. */
function defaultArgumentStart(parameter: string): number {
  let depth = 0;
  let quote: string | null = null;
  for (let index = 0; index < parameter.length; index++) {
    const character = parameter[index];
    Iif (quote) {
      if (character === "\\") index++;
      else if (character === quote) quote = null;
    } else Iif (character === '"' || character === "'") {
      quote = character;
    } else if (OPENING_BRACKETS.includes(character)) {
      depth++;
    } else if (CLOSING_BRACKETS.includes(character)) {
      depth--;
    } else if (character === "=" && depth === 0) {
      return index;
    }
  }
  return -1;
}
 
/** Removes default arguments from a parenthesized parameter list such as `(int a, byte b = 2)`. */
export function stripDefaultArguments(parameterList: string): string {
  const scan: ParameterScan = { parts: [], current: "", depth: 0, quote: null, escaped: false };
  for (const character of parameterList.slice(1, -1)) scanParameterCharacter(scan, character);
  scan.parts.push(scan.current);
  if (!scan.parts.some((part) => defaultArgumentStart(part) >= 0)) return parameterList;
  const parameters = scan.parts.map((part) => {
    const start = defaultArgumentStart(part);
    return (start >= 0 ? part.slice(0, start) : part).trim();
  });
  return `(${parameters.join(", ")})`;
}
 
function lineIndexAt(code: string, offset: number): number {
  let line = 0;
  for (let index = code.indexOf("\n"); index >= 0 && index < offset; index = code.indexOf("\n", index + 1)) line++;
  return line;
}
 
/**
 * Plans the prototypes for every function defined in `code`.
 *
 * `isSketchLine` tells which lines belong to the sketch itself rather than to
 * an inlined header; the prototypes go before the first function the sketch
 * defines, like in the Arduino IDE. Without a sketch function they go before
 * the first function at all.
 */
export function planForwardDeclarations(
  code: string,
  isSketchLine: (lineIndex: number) => boolean = () => true,
): ForwardDeclarationPlan {
  const masked = maskCommentsPreservingLines(code);
  const seen = new Set<string>();
  const declarations: string[] = [];
  let firstDefinition = -1;
  let firstSketchDefinition = -1;
 
  for (const match of masked.matchAll(FUNCTION_DEFINITION)) {
    const name = match[2];
    Iif (NOT_A_FUNCTION.has(name)) continue;
    const offset = match.index ?? 0;
    if (firstDefinition < 0) firstDefinition = offset;
    if (firstSketchDefinition < 0 && isSketchLine(lineIndexAt(masked, offset))) firstSketchDefinition = offset;
    if (ENTRYPOINTS.has(name)) continue;
    const parameters = stripDefaultArguments(match[3]);
    const signature = `${name}${parameters}`;
    Iif (seen.has(signature)) continue;
    seen.add(signature);
    declarations.push(`${match[1].trim()} ${signature};`);
  }
 
  return {
    declarations: declarations.join("\n"),
    insertAt: firstSketchDefinition >= 0 ? firstSketchDefinition : firstDefinition,
  };
}
 
/** Inserts the planned prototypes into `code`; returns `code` unchanged when none are needed. */
export function insertForwardDeclarations(
  code: string,
  isSketchLine?: (lineIndex: number) => boolean,
): string {
  const plan = planForwardDeclarations(code, isSketchLine);
  if (!plan.declarations || plan.insertAt < 0) return code;
  const section = `// --- Forward declarations (auto-generated, mirrors Arduino IDE behaviour) ---\n${plan.declarations}\n\n`;
  return `${code.slice(0, plan.insertAt)}${section}${code.slice(plan.insertAt)}`;
}