All files / transpiler/output/codegen/helpers TypedefParamParser.ts

94.73% Statements 72/76
91.07% Branches 51/56
100% Functions 11/11
94.59% Lines 70/74

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 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306                                                                                  30x                                               40x 40x 90x   40x 3x       37x 1x           36x 36x 36x   36x                         49x 49x 4x     45x     45x 45x         45x 45x         45x 1x       44x 83x   44x                 45x       45x 45x 1051x 53x 998x 53x 53x   45x                         44x 44x 44x   44x 957x 8x 8x 949x 8x 8x 941x 39x 39x   902x       44x 44x     44x             83x     83x       83x     83x                 83x 40x       83x 1x     83x                             32x 32x 5x   27x                           16x                               16x                                     5x 9x       9x       9x                    
/**
 * TypedefParamParser - Parses C function pointer typedef signatures
 *
 * Extracts parameter types from typedef strings like:
 *   "void (*)(widget_t *, const rect_t *, uint8_t *)"
 *   "void (*)(Point p)"
 *
 * Used by Issue #895 to determine if callback params should be pointers or values.
 * Used by Issue #914 to resolve callback pointer/const info onto IParameterSymbol.
 */
 
import IParameterSymbol from "../../../../utils/types/IParameterSymbol";
 
/**
 * Parsed parameter info from a typedef.
 */
interface ITypedefParam {
  /** Full type string (e.g., "widget_t *", "const rect_t *", "uint8_t *") */
  type: string;
  /** Whether this is a pointer type */
  isPointer: boolean;
  /** Whether this has const qualifier */
  isConst: boolean;
  /** Base type without pointer/const (e.g., "widget_t", "rect_t", "uint8_t") */
  baseType: string;
}
 
/**
 * Parse result for a typedef signature.
 */
interface ITypedefParseResult {
  /** Return type */
  returnType: string;
  /** Parsed parameters */
  params: ITypedefParam[];
}
 
/**
 * C type keywords that can trail a multi-word type. A parameter name can never
 * be one of these, so a trailing word from this set belongs to the type.
 */
const TRAILING_TYPE_KEYWORDS = new Set([
  "int",
  "long",
  "short",
  "char",
  "float",
  "double",
  "signed",
  "unsigned",
]);
 
/**
 * Drop a trailing space-separated parameter name, as /\s+\w+$/ did -- except
 * for a trailing type keyword, which belongs to the type (#1189).
 *
 * Scanned from the end rather than matched, avoiding the super-linear
 * backtracking of /\s+\w+$/ (S8786).
 *
 * The word-character check is load-bearing: \w+ does not match a final token
 * containing anything else, so "int (fn)(void)" -- what a function-pointer
 * parameter looks like once the stars are stripped -- is left alone. Cutting
 * at the last space unconditionally would reduce it to "int".
 */
function stripTrailingIdentifier(text: string): string {
  let start = text.length;
  while (start > 0 && /\w/.test(text[start - 1])) {
    start -= 1;
  }
  if (start === text.length) {
    return text; // no trailing identifier
  }
  // Issue #1189: an unnamed multi-word type ends in a type keyword, not a
  // parameter name -- `unsigned int` must not become `unsigned`.
  if (TRAILING_TYPE_KEYWORDS.has(text.slice(start))) {
    return text;
  }
 
  // Consume the whole whitespace run, not just the last space. The caller
  // normalizes runs to a single space today, but depending on that would make
  // this correct only by coincidence.
  let cut = start;
  while (cut > 0 && /\s/.test(text[cut - 1])) {
    cut -= 1;
  }
  return cut === start ? text : text.slice(0, cut);
}
 
class TypedefParamParser {
  /**
   * Parse a function pointer typedef type string.
   *
   * @param typedefType - The type string, e.g., "void (*)(widget_t *, const rect_t *, uint8_t *)"
   * @returns Parsed result with return type and parameters, or null if parsing fails
   */
  static parse(typedefType: string): ITypedefParseResult | null {
    // Expected format: "return_type (*)(param1, param2, ...)"
    // Find the (*) marker first
    const funcPtrIndex = typedefType.indexOf("(*)");
    if (funcPtrIndex === -1) {
      return null;
    }
 
    const returnType = typedefType.substring(0, funcPtrIndex).trim();
 
    // Find the opening paren after (*)
    const afterFuncPtr = typedefType.substring(funcPtrIndex + 3).trim();
    Iif (!afterFuncPtr.startsWith("(")) {
      return null;
    }
 
    // Extract params by finding the matching closing paren
    const paramsStr = TypedefParamParser.extractParenContent(afterFuncPtr);
    Iif (paramsStr === null) {
      return null;
    }
 
    // Handle void or empty params
    if (!paramsStr || paramsStr === "void") {
      return { returnType, params: [] };
    }
 
    // Split by comma, handling nested parentheses
    const paramStrings = TypedefParamParser.splitParams(paramsStr);
    const params = paramStrings.map((p) => TypedefParamParser.parseParam(p));
 
    return { returnType, params };
  }
 
  /**
   * Extract content between matching parentheses, handling arbitrary nesting.
   * @param str - String starting with '('
   * @returns Content between outer parens, or null if no match
   */
  private static extractParenContent(str: string): string | null {
    Iif (!str.startsWith("(")) {
      return null;
    }
 
    let depth = 0;
    for (let i = 0; i < str.length; i++) {
      if (str[i] === "(") {
        depth++;
      } else if (str[i] === ")") {
        depth--;
        if (depth === 0) {
          // Found matching close paren - return content between
          return str.substring(1, i);
        }
      }
    }
 
    // No matching close paren found
    return null;
  }
 
  /**
   * Split parameter string by commas, respecting nested parentheses.
   */
  private static splitParams(paramsStr: string): string[] {
    const params: string[] = [];
    let current = "";
    let depth = 0;
 
    for (const char of paramsStr) {
      if (char === "(") {
        depth++;
        current += char;
      } else if (char === ")") {
        depth--;
        current += char;
      } else if (char === "," && depth === 0) {
        params.push(current.trim());
        current = "";
      } else {
        current += char;
      }
    }
 
    Eif (current.trim()) {
      params.push(current.trim());
    }
 
    return params;
  }
 
  /**
   * Parse a single parameter type string.
   */
  private static parseParam(paramStr: string): ITypedefParam {
    const trimmed = paramStr.trim();
 
    // Check for pointer
    const isPointer = trimmed.includes("*");
 
    // Check for const - handles both "const " (with space) and merged forms
    // C grammar getText() strips spaces, so "const rect_t*" may appear merged
    const isConst = /\bconst\b/.test(trimmed) || trimmed.startsWith("const");
 
    // Extract base type (remove const, *, and param name if present)
    let baseType = trimmed
      .replaceAll(/\bconst\b/g, "") // Remove const (with word boundary)
      .replace(/^const/, "") // Remove const at start (no space case) - only once
      .replaceAll("*", "") // Remove pointers
      .replaceAll(/\s+/g, " ") // Normalize whitespace
      .trim();
 
    // Remove trailing param name if present (e.g., "rect_t area" -> "rect_t")
    // Only remove if there are multiple words (space-separated)
    if (baseType.includes(" ")) {
      baseType = stripTrailingIdentifier(baseType);
    }
 
    // Handle struct keyword
    if (baseType.startsWith("struct ")) {
      baseType = baseType.substring(7);
    }
 
    return {
      type: trimmed,
      isPointer,
      isConst,
      baseType,
    };
  }
 
  /**
   * Get the parameter info at a given index, or null if not found.
   */
  private static getParamAt(
    typedefType: string,
    paramIndex: number,
  ): ITypedefParam | null {
    const parsed = TypedefParamParser.parse(typedefType);
    if (!parsed || paramIndex >= parsed.params.length) {
      return null;
    }
    return parsed.params[paramIndex];
  }
 
  /**
   * Check if a parameter at a given index should be a pointer based on the typedef.
   *
   * @param typedefType - The typedef type string
   * @param paramIndex - The parameter index (0-based)
   * @returns true if the param should be a pointer, false for value, null if unknown
   */
  static shouldBePointer(
    typedefType: string,
    paramIndex: number,
  ): boolean | null {
    return (
      TypedefParamParser.getParamAt(typedefType, paramIndex)?.isPointer ?? null
    );
  }
 
  /**
   * Check if a parameter at a given index should be const based on the typedef.
   *
   * @param typedefType - The typedef type string
   * @param paramIndex - The parameter index (0-based)
   * @returns true if the param should be const, false otherwise, null if unknown
   */
  static shouldBeConst(
    typedefType: string,
    paramIndex: number,
  ): boolean | null {
    return (
      TypedefParamParser.getParamAt(typedefType, paramIndex)?.isConst ?? null
    );
  }
 
  /**
   * Resolve callback pointer/const overrides onto an array of IParameterSymbol.
   *
   * Issue #914: This is the single point where callback typedef info is applied
   * to parameters — used by both .c and .h generation paths via IParameterSymbol.
   *
   * @param params - The original parameter symbols
   * @param callbackTypedefType - The typedef type string (e.g., "void (*)(widget_t *, const rect_t *)")
   * @returns New array with isCallbackPointer/isCallbackConst resolved
   */
  static resolveCallbackParams(
    params: readonly IParameterSymbol[],
    callbackTypedefType: string,
  ): IParameterSymbol[] {
    return params.map((param, index) => {
      const shouldBePointer = TypedefParamParser.shouldBePointer(
        callbackTypedefType,
        index,
      );
      const shouldBeConst = TypedefParamParser.shouldBeConst(
        callbackTypedefType,
        index,
      );
      return {
        ...param,
        isCallbackPointer: shouldBePointer ?? undefined,
        isCallbackConst: shouldBeConst ?? undefined,
      };
    });
  }
}
 
export default TypedefParamParser;