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 | 3x 21x 21x 21x 14x 6x 1x 21x 104x 12x 15x 14x 14x 1x 1x 13x 21x 118x 199x 199x 81x 81x 118x 118x 118x 199x 710x 646x 625x 625x 104x 521x 117x | /**
* Normalization pass — substitutes `{{name}}` references in every string-bearing field
* of the AST with values from `root.meta` (the file's `options { keys }` block) or, when
* absent, from a workspace-wide `fallbackKeys` map (typically merged from each plugin's
* `options.keys` in `contractkit.config.json`).
*
* Runs in the CLI between `parseCk` and `decomposeCk`, after `applyOptionsDefaults`. It
* deliberately does NOT run inside `parseCk` so the prettier plugin sees the un-substituted
* source form and can round-trip the file.
*
* Substitution rules:
* - `{{name}}` → value lookup; warns and emits the literal string `undefined` when missing.
* - `\{{name}}` → literal `{{name}}` (no substitution, no warning).
*
* `root.meta` is itself excluded from the walk — keys are not recursively substituted.
*/
import type { CkRootNode, OpRootNode, SourceLocation } from './ast.js';
import type { DiagnosticCollector } from './diagnostics.js';
const SUBSTITUTION_RE = /\\\{\{(\w+)\}\}|\{\{(\w+)\}\}/g;
type Root = CkRootNode | OpRootNode;
export function applyVariableSubstitution(root: Root, diag: DiagnosticCollector, fallbackKeys: Record<string, string> = {}): void {
const file = root.file;
const meta = root.meta ?? {};
const lookup = (name: string): string | undefined => {
if (Object.prototype.hasOwnProperty.call(meta, name)) return meta[name];
if (Object.prototype.hasOwnProperty.call(fallbackKeys, name)) return fallbackKeys[name];
return undefined;
};
const substitute = (input: string, line: number): string => {
if (!input.includes('{{')) return input;
return input.replace(SUBSTITUTION_RE, (_match, escapedName: string | undefined, varName: string | undefined) => {
if (escapedName !== undefined) return `{{${escapedName}}}`;
const value = lookup(varName!);
if (value === undefined) {
diag.warn(file, line, `Unknown variable '{{${varName}}}'`);
return 'undefined';
}
return value;
});
};
walk(root, substitute, 0, /* isRoot */ true);
}
function isLoc(value: unknown): value is SourceLocation {
return typeof value === 'object' && value !== null && typeof (value as SourceLocation).line === 'number' && typeof (value as SourceLocation).file === 'string';
}
function walk(node: unknown, substitute: (s: string, line: number) => string, currentLine: number, isRoot: boolean): void {
Iif (node === null || typeof node !== 'object') return;
if (Array.isArray(node)) {
for (const item of node) walk(item, substitute, currentLine, false);
return;
}
const obj = node as Record<string, unknown>;
// Promote `loc.line` to the running context so warnings emitted while walking
// this node's descendants can attribute themselves accurately.
const ownLoc = obj['loc'];
const lineHere = isLoc(ownLoc) ? ownLoc.line : currentLine;
for (const key of Object.keys(obj)) {
// Skip book-keeping fields and the substitution source itself.
if (key === 'loc' || key === 'file') continue;
if (isRoot && key === 'meta') continue;
const value = obj[key];
if (typeof value === 'string') {
obj[key] = substitute(value, lineHere);
} else if (value !== null && typeof value === 'object') {
walk(value, substitute, lineHere, false);
}
}
}
|