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 | 2x 8x 8x 8x 8x 8x 8x 11x 10x 10x 10x 11x 11x 11x 11x 11x 11x 11x 8x 8x 8x 8x 8x | import { applyOptionsDefaults } from './apply-options-defaults.js';
import { applyVariableSubstitution } from './apply-variable-substitution.js';
import { decomposeCk } from './decompose.js';
import { DiagnosticCollector } from './diagnostics.js';
import { parseCk } from './parser.js';
import { validateInheritance } from './validate-inheritance.js';
import { validateOp } from './validate-operation.js';
import { validateRefs } from './validate-refs.js';
import type { CkRootNode, ContractRootNode, OpRootNode } from './ast.js';
/** A `.ck` source paired with the absolute path the parser should report in diagnostics. */
export interface ProjectFile {
filePath: string;
/** Pre-parsed AST, when the caller already has one (e.g. an LSP holding parses). Mutually exclusive with `source`. */
ast?: CkRootNode;
/** Raw source. Parsed by {@link validateProject} when `ast` is omitted. */
source?: string;
}
/** Options accepted by {@link validateProject}. */
export interface ValidateProjectOptions {
files: ProjectFile[];
/** Fallback `{{key}}` substitutions applied workspace-wide. Passed through to {@link applyVariableSubstitution}. Ignored for any file that matches `getKeysForFile`. */
fallbackKeys?: Record<string, string>;
/**
* Per-file fallback keys resolver, used when different files in the same project resolve
* different `contractkit.config.json` files (e.g. an LSP serving a workspace with multiple
* configs). Returning `undefined` falls back to `fallbackKeys`.
*/
getKeysForFile?: (filePath: string) => Record<string, string> | undefined;
/** Existing collector to append into. A fresh one is created when omitted. */
diag?: DiagnosticCollector;
}
/** Output of {@link validateProject}. */
export interface ValidateProjectResult {
/** Diagnostics collected across every phase. Same instance as `options.diag` when provided. */
diag: DiagnosticCollector;
/** Decomposed contract roots, one per file that produced any models. */
contracts: ContractRootNode[];
/** Decomposed op roots, one per file that produced any routes. */
ops: OpRootNode[];
/** Normalized (variable-substituted, options-defaulted) ASTs, one per input file. */
asts: { filePath: string; ast: CkRootNode }[];
}
/**
* Run the contractkit validation pipeline across a set of `.ck` files: parse
* (when needed), apply options defaults, substitute `{{var}}` references,
* decompose, then run cross-file ref/inheritance/operation validation.
*
* Designed as the single source of truth for both the CLI and the language
* server so they enforce identical semantics. Plugin `validate`/`transform`
* hooks are deliberately *not* invoked here — those live in the CLI because
* they touch the filesystem and load arbitrary user code.
*
* Diagnostics are aggregated rather than thrown; callers inspect `diag` to
* decide how to surface them (CLI prints + non-zero exit, LSP publishes
* per-URI).
*/
export const validateProject = (options: ValidateProjectOptions): ValidateProjectResult => {
const diag = options.diag ?? new DiagnosticCollector();
const fallbackKeys = options.fallbackKeys ?? {};
const contracts: ContractRootNode[] = [];
const ops: OpRootNode[] = [];
const asts: { filePath: string; ast: CkRootNode }[] = [];
for (const file of options.files) {
let ast: CkRootNode;
if (file.ast) ast = file.ast;
else if (file.source !== undefined) {
try {
ast = parseCk(file.source, file.filePath, diag);
} catch {
continue;
}
} else E{
diag.warn(file.filePath, 0, `validateProject: file entry has neither 'ast' nor 'source'`);
continue;
}
applyOptionsDefaults(ast, diag);
const keysForFile = options.getKeysForFile?.(file.filePath) ?? fallbackKeys;
applyVariableSubstitution(ast, diag, keysForFile);
asts.push({ filePath: file.filePath, ast });
const { contract, op } = decomposeCk(ast);
if (contract.models.length > 0) contracts.push(contract);
if (op.routes.length > 0) ops.push(op);
}
// Cross-file: refs and inheritance only make sense once every file has contributed its models.
validateRefs(contracts, ops, diag);
validateInheritance(contracts, diag);
for (const op of ops) {
validateOp(op, diag);
}
return { diag, contracts, ops, asts };
};
|