All files / PARSE/3-Declare/cnext/collectors CallbackUseCollector.ts

98.19% Statements 109/111
95.45% Branches 63/66
100% Functions 8/8
99.03% Lines 103/104

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 307 308 309 310 311 312 313 314 315                                                            3343x 3343x 6166x 6166x 2858x 2858x         3308x 3308x 570x 570x 960x 960x 459x         3343x                       3317x 3317x 3317x                     3661x 5144x                           5389x 5389x 2720x 2720x       2669x 2669x 268x         268x       2401x 2401x 147x 169x   147x     2254x 2254x 22x         22x     2232x 2232x 54x 54x     2178x 2178x 17x 17x     2161x 2161x 38x 76x   38x 38x 23x   38x     2123x 2123x 15x 15x       2108x 2108x 213x                         2720x 2720x   2418x       2418x   591x                                     268x 268x   264x 264x   244x 212x       212x 158x                                         2630x     2630x 2630x 9x     9x       2621x 2621x 2x               2619x 2619x 738x         1881x                   264x 264x     264x 264x     264x 15x         249x 264x     264x 300x   56x 244x   244x 244x       249x 244x     244x 244x   264x          
/**
 * ADR-029's per-file half: where one file names what may be a function, in a
 * position a C callback could be expected (#1825).
 *
 * Two shapes, both from #895: a variable declared with an initializer
 * (`PointCallback cb <- my_handler;`) and a call argument
 * (`global.widget_set_flush_cb(w, my_flush);`). Neither is a callback yet --
 * that needs the C header's typedef, and whether the name is a function at all
 * needs every file's declarations (#1544) -- so 1.4 decides
 * (`CallbackCompatibility`), and this records only what the file spells.
 *
 * This walk sat in 2.1 Analyze inside `FunctionCallAnalyzer`, and the
 * orchestrator ran the whole analyzer over every tree, diagnostics discarded,
 * for the map it filled as a side effect.
 */
 
import type TCallbackUse from "../../../../types/TCallbackUse";
import * as Parser from "../../../2-Parse/grammar/CNextParser";
import ExpressionUnwrapper from "../../../../utils/ExpressionUnwrapper";
import ScopeUtils from "../../../../utils/ScopeUtils";
 
class CallbackUseCollector {
  /**
   * @param scopePathOf the path of a scope this file declares, by name
   * @returns every candidate use, in source order
   */
  static collect(
    tree: Parser.ProgramContext,
    scopePathOf: (scopeName: string) => string,
  ): TCallbackUse[] {
    const uses: TCallbackUse[] = [];
    for (const decl of tree.declaration()) {
      const funcDecl = decl.functionDeclaration();
      if (funcDecl) {
        CallbackUseCollector.scanFunction(uses, funcDecl, "");
        continue;
      }
 
      // Issue #895: every member function of a scope, which may name its
      // siblings as `this.member`.
      const scopeDecl = decl.scopeDeclaration();
      if (scopeDecl) {
        const scopePath = scopePathOf(scopeDecl.IDENTIFIER().getText());
        for (const member of scopeDecl.scopeMember()) {
          const memberFunc = member.functionDeclaration();
          if (memberFunc) {
            CallbackUseCollector.scanFunction(uses, memberFunc, scopePath);
          }
        }
      }
    }
    return uses;
  }
 
  /**
   * @param scopePath the scope the function is a member of, which `this.`
   *        names; `""` for a standalone function
   */
  private static scanFunction(
    uses: TCallbackUse[],
    funcDecl: Parser.FunctionDeclarationContext,
    scopePath: string,
  ): void {
    const block = funcDecl.block();
    Iif (!block) return;
    CallbackUseCollector.scanBlock(uses, block, scopePath);
  }
 
  /**
   * Recursively scan all statements in a block.
   */
  private static scanBlock(
    uses: TCallbackUse[],
    block: Parser.BlockContext,
    scopePath: string,
  ): void {
    for (const stmt of block.statement()) {
      CallbackUseCollector.scanStatement(uses, stmt, scopePath);
    }
  }
 
  /**
   * Scan a single statement, recursing into nested blocks
   * (if/while/for/do-while/switch/critical).
   */
  private static scanStatement(
    uses: TCallbackUse[],
    stmt: Parser.StatementContext,
    scopePath: string,
  ): void {
    // Check variable declarations for callback assignments
    const varDecl = stmt.variableDeclaration();
    if (varDecl) {
      CallbackUseCollector.recordInitializer(uses, varDecl, scopePath);
      return;
    }
 
    // Check expression statements for function calls with callback arguments
    const exprStmt = stmt.expressionStatement();
    if (exprStmt) {
      CallbackUseCollector.recordArguments(
        uses,
        exprStmt.expression(),
        scopePath,
      );
      return;
    }
 
    // Recurse into nested blocks/statements
    const ifStmt = stmt.ifStatement();
    if (ifStmt) {
      for (const child of ifStmt.statement()) {
        CallbackUseCollector.scanStatement(uses, child, scopePath);
      }
      return;
    }
 
    const whileStmt = stmt.whileStatement();
    if (whileStmt) {
      CallbackUseCollector.scanStatement(
        uses,
        whileStmt.statement(),
        scopePath,
      );
      return;
    }
 
    const forStmt = stmt.forStatement();
    if (forStmt) {
      CallbackUseCollector.scanStatement(uses, forStmt.statement(), scopePath);
      return;
    }
 
    const doWhileStmt = stmt.doWhileStatement();
    if (doWhileStmt) {
      CallbackUseCollector.scanBlock(uses, doWhileStmt.block(), scopePath);
      return;
    }
 
    const switchStmt = stmt.switchStatement();
    if (switchStmt) {
      for (const caseCtx of switchStmt.switchCase()) {
        CallbackUseCollector.scanBlock(uses, caseCtx.block(), scopePath);
      }
      const defaultCtx = switchStmt.defaultCase();
      if (defaultCtx) {
        CallbackUseCollector.scanBlock(uses, defaultCtx.block(), scopePath);
      }
      return;
    }
 
    const criticalStmt = stmt.criticalStatement();
    if (criticalStmt) {
      CallbackUseCollector.scanBlock(uses, criticalStmt.block(), scopePath);
      return;
    }
 
    // A statement can itself be a block
    const nestedBlock = stmt.block();
    if (nestedBlock) {
      CallbackUseCollector.scanBlock(uses, nestedBlock, scopePath);
    }
  }
 
  /**
   * `PointCallback cb <- my_handler;` -- a declaration whose initializer names
   * what may be a function.
   */
  private static recordInitializer(
    uses: TCallbackUse[],
    varDecl: Parser.VariableDeclarationContext,
    scopePath: string,
  ): void {
    const expr = varDecl.expression();
    if (!expr) return;
 
    const functionName = CallbackUseCollector.functionReference(
      expr,
      scopePath,
    );
    if (!functionName) return;
 
    uses.push({
      kind: "initializer",
      functionName,
      typeName: varDecl.type().getText(),
    });
  }
 
  /**
   * Issue #895: `global.widget_set_flush_cb(w, my_flush)` -- a call whose
   * arguments name what may be functions. Whether the callee is a C function
   * and the parameter a function pointer typedef is 1.4's question.
   */
  private static recordArguments(
    uses: TCallbackUse[],
    expr: Parser.ExpressionContext,
    scopePath: string,
  ): void {
    // Uses ExpressionUnwrapper, which validates that the expression is
    // "simple" (single term at each level), so a complex one yields nothing.
    const postfix = ExpressionUnwrapper.getPostfixExpression(expr);
    if (!postfix) return;
 
    const callInfo = CallbackUseCollector.extractCallInfo(postfix);
    if (!callInfo) return;
 
    for (const [argIndex, arg] of callInfo.args.entries()) {
      const functionName = CallbackUseCollector.functionReference(
        arg,
        scopePath,
      );
      if (!functionName) continue;
      uses.push({
        kind: "argument",
        functionName,
        callee: callInfo.funcName,
        argIndex,
      });
    }
  }
 
  /**
   * The lookup key of a function reference -- the transpiled C name it spells
   * -- or null if the expression is not one. Matches:
   *   - Bare identifiers: "my_handler"
   *   - Qualified scope names: "MyScope.handler"
   *   - Self-scope reference: "this.handler", in the enclosing scope
   *   - Global scope reference: "global.ScopeName.handler"
   */
  private static functionReference(
    expr: Parser.ExpressionContext,
    scopePath: string,
  ): string | null {
    const text = expr.getText();
 
    // Pattern 1: this.member -> CurrentScope.member (Issue #895)
    const thisMatch = /^this\.(\w+)$/.exec(text);
    if (thisMatch) {
      Iif (!scopePath) {
        return null; // this.member outside scope context
      }
      return ScopeUtils.qualifyInScope(thisMatch[1], scopePath);
    }
 
    // Pattern 2: global.Scope.member -> Scope.member (Issue #895)
    const globalMatch = /^global\.(\w+)\.(\w+)$/.exec(text);
    if (globalMatch) {
      return ScopeUtils.qualifyPathInScope(
        [globalMatch[1], globalMatch[2]],
        "",
      );
    }
 
    // Pattern 3: Bare identifier or simple Scope.member. An identifier cannot
    // start with a digit, and `\w` alone would read `5` as one.
    const simpleMatch = /^([A-Za-z_]\w*)(?:\.(\w+))?$/.exec(text);
    if (simpleMatch) {
      return simpleMatch[2]
        ? ScopeUtils.qualifyPathInScope([simpleMatch[1], simpleMatch[2]], "")
        : simpleMatch[1];
    }
 
    return null;
  }
 
  /**
   * Extract function name and arguments from a postfix expression.
   * Returns null if not a function call.
   */
  private static extractCallInfo(
    postfix: Parser.PostfixExpressionContext,
  ): { funcName: string; args: Parser.ExpressionContext[] } | null {
    const primary = postfix.primaryExpression();
    const ops = postfix.postfixOp();
 
    // Start with primary expression (identifier or 'global')
    const ident = primary.IDENTIFIER();
    const globalKw = primary.GLOBAL();
 
    // Early return: neither identifier nor global keyword means not a function call
    if (!ident && !globalKw) {
      return null;
    }
 
    // Build function name from primary + member access ops
    // For 'global' keyword, the path starts empty and gets built from member access
    const path: string[] = ident ? [ident.getText()] : [];
    let argListOp: Parser.PostfixOpContext | null = null;
 
    // Walk postfix ops to find function name and call
    for (const op of ops) {
      if (op.IDENTIFIER()) {
        // Member access: one more component of the qualified name
        path.push(op.IDENTIFIER()!.getText());
      E} else if (op.argumentList() || op.getText().startsWith("(")) {
        // Found the call - this op has the arguments
        argListOp = op;
        break;
      }
    }
 
    if (!argListOp || path.length === 0) return null;
    const funcName = ScopeUtils.qualifyPathInScope(path, "");
 
    // Extract arguments
    const argList = argListOp.argumentList();
    const args = argList?.expression() ?? [];
 
    return { funcName, args };
  }
}
 
export default CallbackUseCollector;