All files / TRANSPILE/2-Plan HeaderIncludes.ts

100% Statements 14/14
100% Branches 6/6
100% Functions 3/3
100% Lines 13/13

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                                                                        45x                           316x 316x 437x     316x 318x 205x   316x 12x   316x                             437x 437x 437x          
/**
 * 2.2 Plan -- which system headers a file's public header includes.
 *
 * `docs/architecture/README.md` ยง1: "**2.2 decides, 2.3 formats.** 'Does this
 * file need `<stdint.h>`?' is decided once, in the plan."
 *
 * ## Why this can be a symbol walk now, and could not be before
 *
 * #1517 first made the header precise by scanning the declarations it had just
 * rendered, and deliberately so: deciding from the SYMBOLS would have meant
 * working out, a second time, what `mapType` works out when it renders -- that
 * `u32` becomes `uint32_t`. Two derivations of one mapping is the defect the
 * whole pass exists to remove, and putting one in the plan would only have
 * moved it somewhere more respectable.
 *
 * #1520 removed the second derivation. `headerCType` is now the one answer to
 * "what does this header call this type", shared by struct fields, function
 * returns and parameters -- so asking it here is asking the renderer's own
 * question, not re-deciding it. The scan can go, and with it Render's last
 * say in what a header contains.
 *
 * ## Under-including is the direction that breaks a build
 *
 * A type this misses loses its header. `npm run headers:standalone:check`
 * compiles all 1305 generated headers alone, so a miss is a red build rather
 * than something a consumer discovers -- which is what makes a walk safe to
 * prefer over a text scan that could only ever over-include.
 */
 
import type SymbolTable from "../../PARSE/3-Declare/SymbolTable";
import type TSymbol from "../../types/symbols/TSymbol";
import SYSTEM_INCLUDE_TARGETS from "./SYSTEM_INCLUDE_TARGETS";
import headerCType from "../../utils/headerCType";
import HeaderTypeNames from "./HeaderTypeNames";
 
/** Fixed-width integer types, which `<stdint.h>` declares. */
const STDINT_TYPES = /^(?:u?int(?:8|16|32|64)_t|u?intptr_t|u?intmax_t)$/;
 
class HeaderIncludes {
  /**
   * The system headers this file's public header emits, in emission order.
   *
   * @param exportedSymbols the file's public interface, as `PublicInterface`
   *   settled it -- the same list the header is generated from
   * @param symbolTable for the C++ namespace spelling `headerCType` needs
   */
  static decide(
    exportedSymbols: readonly TSymbol[],
    symbolTable: SymbolTable | undefined,
  ): string[] {
    const cTypes = new Set<string>();
    for (const named of HeaderTypeNames.collect(exportedSymbols)) {
      cTypes.add(HeaderIncludes.baseTypeOf(headerCType(named, symbolTable)));
    }
 
    const decided: string[] = [];
    if ([...cTypes].some((type) => STDINT_TYPES.test(type))) {
      decided.push(SYSTEM_INCLUDE_TARGETS.stdint!);
    }
    if (cTypes.has("bool")) {
      decided.push(SYSTEM_INCLUDE_TARGETS.stdbool!);
    }
    return decided;
  }
 
  /**
   * The type without its pointer or array decoration.
   *
   * `mapType` returns `uint8_t*` for a pointer and `char[65]` for a
   * `string<64>`, and the question here is about the element type either way.
   */
  private static baseTypeOf(cType: string): string {
    // Written without regular expressions on purpose. `/\s*\*+$/` and
    // `/\[[^\]]*\]$/` both let one quantifier feed another over the same
    // input, which SonarCloud flags as super-linear backtracking (S5852) --
    // and this runs once per named type per file. Index arithmetic answers the
    // same question in one pass and reads no worse.
    const array = cType.indexOf("[");
    const withoutArray = array === -1 ? cType : cType.slice(0, array);
    return withoutArray.replaceAll("*", "").trim();
  }
}
 
export default HeaderIncludes;