All files / utils/cache CachedSymbolReader.ts

97.43% Statements 38/39
97.61% Branches 41/42
100% Functions 6/6
97.43% Lines 38/39

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                    46x                   46x                                                                       59x       59x 59x 85x 85x 17x   68x   42x                                                         71x 4x     67x 67x 305x 305x 7x   298x 2x           58x                   17x 11x   6x     8x                             80x 3x   77x 77x                                     85x 1x     84x 84x               13x     71x 46x   25x 23x   2x          
import JsonCodec from "./JsonCodec";
import ESourceLanguage from "../types/ESourceLanguage";
import TJsonValue from "../types/TJsonValue";
import TCSymbol from "../../types/symbols/c/TCSymbol";
import TCppSymbol from "../../types/symbols/cpp/TCppSymbol";
import SymbolTable from "../../PARSE/3-Declare/SymbolTable";
import IStructSymbolState from "../../types/symbols/IStructSymbolState";
import TJsonSafe from "../types/TJsonSafe";
 
/** Kinds a cached C symbol may declare (mirrors TSymbolKindC). */
const C_KINDS: ReadonlySet<string> = new Set([
  "function",
  "variable",
  "struct",
  "enum",
  "enum_member",
  "type",
]);
 
/** Kinds a cached C++ symbol may declare (mirrors TSymbolKindCpp). */
const CPP_KINDS: ReadonlySet<string> = new Set([
  "function",
  "variable",
  "struct",
  "enum",
  "enum_member",
  "class",
  "namespace",
  "type",
]);
 
/**
 * CachedSymbolReader
 *
 * Turns the JSON in a cache entry back into real `TCSymbol`/`TCppSymbol`
 * values, or rejects the entry.
 *
 * Validation is deliberately shallow — discriminant, source language, and the
 * fields every symbol has. It does NOT re-check each kind's own fields, and
 * that is the point: re-declaring the symbol model here would recreate exactly
 * the hand-maintained parallel list that issue #1225 exists to remove. Shape
 * fidelity comes from `JsonCodec` copying every field rather than naming any,
 * and staleness is handled by CACHE_VERSION and the transpiler version.
 *
 * What is left to catch is "this file is not symbols at all" — a truncated
 * write, a hand-edited cache, a file from another tool. That warrants
 * discarding the entry, which reads as a cache miss and costs only a re-parse.
 */
class CachedSymbolReader {
  /**
   * Decode a cache entry's symbols.
   *
   * @returns the symbols, or `null` if the entry is not trustworthy — callers
   *   must treat `null` as a cache miss rather than as "no symbols".
   */
  static read(encoded: TJsonValue[]): Array<TCSymbol | TCppSymbol> | null {
    Iif (!Array.isArray(encoded)) {
      return null;
    }
 
    const symbols: Array<TCSymbol | TCppSymbol> = [];
    for (const entry of encoded) {
      const decoded = JsonCodec.decode(entry);
      if (!CachedSymbolReader.isCachedSymbol(decoded)) {
        return null;
      }
      symbols.push(decoded);
    }
    return symbols;
  }
 
  /**
   * Decode a cache entry's struct state.
   *
   * Issue #1225 review: the two halves of an entry used to get very different
   * trust — symbols went through the validation below, struct state got a
   * truthiness check and was handed straight to `new Set(...)` / `new Map(...)`.
   * Both failure modes that follow are real:
   *
   * - `{ opaqueTypes: 5 }` makes `new Map(5)` throw out of `_restoreCachedHeader`,
   *   aborting the transpile instead of reading as a miss.
   * - a *missing key* makes `new Set(undefined)` an empty Set, silently — which
   *   is #1225's own failure mode (a warm build that never heard of a fact the
   *   cold build knows) arriving through the unchecked half. Every entry on
   *   disk looks like that the moment `IStructSymbolState` gains a field.
   *
   * So both halves now fail the same way: as a cache miss, costing a re-parse.
   *
   * The expected keys come from `SymbolTable.structStateKeys()` rather than a
   * list here — a list would be the hand-maintained parallel model this issue
   * exists to remove.
   *
   * @returns the struct state, or `null` if the entry is not trustworthy.
   */
  static readStructState(
    value: TJsonValue | undefined,
  ): TJsonSafe<Required<IStructSymbolState>> | null {
    if (typeof value !== "object" || value === null || Array.isArray(value)) {
      return null;
    }
 
    const candidate = value as Record<string, TJsonValue>;
    for (const key of SymbolTable.structStateKeys()) {
      const entry = candidate[key];
      if (!Array.isArray(entry)) {
        return null;
      }
      if (!entry.every(CachedSymbolReader.isStructStateEntry)) {
        return null;
      }
    }
 
    // Validated structurally above: every expected key is present and holds
    // either Set members (strings) or Map entries (string pairs).
    return value as unknown as TJsonSafe<Required<IStructSymbolState>>;
  }
 
  /**
   * A serialized struct-state element: a Set member, or a Map entry pair.
   *
   * Shape-based rather than key-based, so a new field of either kind validates
   * without this method being edited.
   */
  private static isStructStateEntry(entry: TJsonValue): boolean {
    if (typeof entry === "string") {
      return true;
    }
    return (
      Array.isArray(entry) &&
      entry.length === 2 &&
      entry.every((part) => typeof part === "string")
    );
  }
 
  /**
   * Is this value a well-formed `ISourceSpan`?
   *
   * #1318: `sourceLine` was one `typeof` check; a span is four, and they cannot
   * be derived from the type, because `TCSymbol`/`TCppSymbol` are types and
   * their keys are not enumerable at runtime -- the same reason CACHE_VERSION is
   * bumped by hand. A cached entry written before the span existed carries
   * `sourceLine` and no `span`, and is rejected here rather than deserialized
   * into a symbol whose position is `undefined`.
   */
  private static isSpan(value: unknown): boolean {
    if (typeof value !== "object" || value === null) {
      return false;
    }
    const span = value as Record<string, unknown>;
    return (
      typeof span.line === "number" &&
      typeof span.column === "number" &&
      typeof span.endLine === "number" &&
      typeof span.endColumn === "number"
    );
  }
 
  /**
   * Does this decoded value carry the fields every C/C++ symbol has, with a
   * discriminant its language actually defines?
   *
   * C-Next symbols are rejected: they are never written to the cache (they are
   * re-parsed from source every run), so one appearing here means the entry is
   * not what it claims to be.
   */
  private static isCachedSymbol(
    value: unknown,
  ): value is TCSymbol | TCppSymbol {
    if (typeof value !== "object" || value === null) {
      return false;
    }
 
    const candidate = value as Record<string, unknown>;
    if (
      typeof candidate.name !== "string" ||
      typeof candidate.sourceFile !== "string" ||
      !CachedSymbolReader.isSpan(candidate.span) ||
      (candidate.visibility !== "public" &&
        candidate.visibility !== "private") ||
      typeof candidate.kind !== "string"
    ) {
      return false;
    }
 
    if (candidate.sourceLanguage === ESourceLanguage.C) {
      return C_KINDS.has(candidate.kind);
    }
    if (candidate.sourceLanguage === ESourceLanguage.Cpp) {
      return CPP_KINDS.has(candidate.kind);
    }
    return false;
  }
}
 
export default CachedSymbolReader;