core/validators/document.validator.ts

core/validators/document.validator.ts is a file in GovLab Context. 104 lines of code and 22 definitions.

import type {
    DocumentSource,
    DocumentVerdict,
    PagDefect,
    PagDocument,
    ValidateResult,
} from "#types/grammar.document.types";
import { FIRST_LINE, PAG_FENCE_INFO } from "#configuration/constants/document.constants";
import { FOR_EACH_HEAD, FOR_HEAD, LOWERCASE_KEYWORDS, NEEDS_COLON } from "#configuration/constants/grammar.constants";
import { gateDefects, nodeDefects } from "#core/validators/node.validator";
import { pagTextOf } from "#core/selectors/document.selector";
import { parse } from "#core/parsers/document.parser";
import { readFileSync } from "node:fs";
import { splitLines } from "#core/converters/text.converter";

const COLON = ":";
const DECLARATION_TOKEN = "THIS";

const lineDefect = function lineDefect(trimmed: string, lineNo: number): PagDefect | null {
    if (trimmed.startsWith(FOR_HEAD) && !trimmed.startsWith(FOR_EACH_HEAD)) {
        return { code: "for_without_each", line: lineNo, token: FOR_HEAD.trim() };
    }
    const lower = LOWERCASE_KEYWORDS.find((keyword) => trimmed.startsWith(keyword));
    if (lower !== undefined) {
        return { code: "lowercase_keyword", line: lineNo, token: lower.trim() };
    }
    const needsColon = NEEDS_COLON.find((head) => trimmed.startsWith(head) && !trimmed.includes(COLON));
    return needsColon === undefined ? null : { code: "missing_colon", line: lineNo, token: needsColon.trim() };
};

const tokenDefects = function tokenDefects(text: string): PagDefect[] {
    return splitLines(text).flatMap((line, index) => {
        const trimmed = line.trim();
        const defect = trimmed === "" ? null : lineDefect(trimmed, index + 1);
        return defect === null ? [] : [defect];
    });
};

const retiredDefects = function retiredDefects(doc: PagDocument): PagDefect[] {
    return doc.retired.map((entry) => ({
        code: entry.kind === "invariant" ? "bare_invariant_block" : "retired_unit_head",
        line: entry.line,
        token: entry.token,
    }));
};

const invariantDefects = function invariantDefects(doc: PagDocument): PagDefect[] {
    return doc.invariants.flatMap((invariant): PagDefect[] => [
        ...(invariant.set === null || invariant.set === ""
            ? [{ code: "invariant_without_set" as const, line: invariant.line, token: invariant.name }]
            : []),
        ...(invariant.parties === null || invariant.parties === ""
            ? [{ code: "invariant_without_parties" as const, line: invariant.line, token: invariant.name }]
            : []),
        ...(invariant.objector === null || invariant.objector === ""
            ? [{ code: "invariant_without_objector" as const, line: invariant.line, token: invariant.name }]
            : []),
    ]);
};

const duplicateNodeDefects = function duplicateNodeDefects(doc: PagDocument): PagDefect[] {
    const seen = new Set<string>();
    const defects: PagDefect[] = [];
    for (const node of doc.nodes) {
        const number = node.number ?? "";
        if (number !== "" && seen.has(number)) {
            defects.push({ code: "node_declared_twice", line: node.line, token: number });
        }
        seen.add(number);
    }
    return defects;
};

const byLine = function byLine(a: PagDefect, b: PagDefect): number {
    return a.line - b.line || a.code.localeCompare(b.code);
};

export const validate = function validate(text: string): ValidateResult {
    const doc = parse(text);
    const declType = doc.declaration?.type;
    const defects: PagDefect[] = [
        ...(typeof declType !== "string" || declType.length === 0
            ? [{ code: "no_declaration" as const, line: 1, token: DECLARATION_TOKEN }]
            : []),
        ...retiredDefects(doc),
        ...doc.nodes.flatMap((node) => nodeDefects(node, doc)),
        ...doc.nodes.flatMap(gateDefects),
        ...duplicateNodeDefects(doc),
        ...invariantDefects(doc),
        ...tokenDefects(text),
    ].toSorted(byLine);
    return { defects, wellFormed: defects.length === 0 };
};

export const verdictOf = function verdictOf(source: DocumentSource, text: string): DocumentVerdict {
    const pag = pagTextOf(source, text);
    if (pag === null) {
        return {
            defects: [{ code: "template_without_block", line: FIRST_LINE, token: PAG_FENCE_INFO }],
            extracted: false,
            source,
        };
    }
    const offset = pag.firstLine - FIRST_LINE;
    return {
        defects: validate(pag.text).defects.map((defect) => ({ ...defect, line: defect.line + offset })),
        extracted: true,
        source,
    };
};

export const validateDocuments = function validateDocuments(sources: readonly DocumentSource[]): DocumentVerdict[] {
    return sources.map((source) => verdictOf(source, readFileSync(source.path, "utf8")));
};