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 | 1x 10x 10x 10x 10x 2x 1x 10x 39x 8x 11x 10x 10x 1x 1x 9x 10x 57x 97x 97x 40x 40x 57x 57x 57x 97x 349x 319x 309x 309x 39x 270x 57x | /**
* 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);
}
}
}
|