core/validators/grammar.validator.ts
core/validators/grammar.validator.ts is a file in GovLab Context. 89 lines of code and 17 definitions.
import {
DOCUMENT_TYPE_CATEGORY,
DOCUMENT_VERB_CATEGORY,
NONTERMINAL_CLOSE,
} from "#configuration/constants/grammar.constants";
import type { DocumentTypeRecord, GrammarIssues, PagData } from "#types/grammar.types";
import { hasRoleIn, keywordIdOf } from "#core/selectors/grammar.selector";
import { NONTERMINAL_OPEN } from "#configuration/constants/algorithm.constants";
import { slotIsDangling } from "#core/resolvers/template.resolver";
const byName = function byName(a: string, b: string): number {
return a.localeCompare(b);
};
const duplicateKeywords = function duplicateKeywords(data: PagData): string[] {
const seen = new Set<string>();
const duplicates: string[] = [];
for (const id of data.keywords.map(keywordIdOf)) {
if (seen.has(id)) {
duplicates.push(id);
}
seen.add(id);
}
return duplicates;
};
const nonterminalRefs = function nonterminalRefs(rhs: string): string[] {
const refs: string[] = [];
let open = rhs.indexOf(NONTERMINAL_OPEN);
while (open !== -1) {
const close = rhs.indexOf(NONTERMINAL_CLOSE, open);
if (close === -1) {
break;
}
refs.push(rhs.slice(open + 1, close));
open = rhs.indexOf(NONTERMINAL_OPEN, close + 1);
}
return refs;
};
const grammarSymbolsOf = function grammarSymbolsOf(
data: PagData,
): Pick<GrammarIssues, "danglingNonterminals" | "unusedTerminals"> {
const defined = new Set(data.productions.map((record) => record.lhs));
const terminals = new Set(data.terminals);
const refs = data.productions.flatMap((production) =>
nonterminalRefs(production.rhs).map((ref) => ({ production: production.lhs, ref })),
);
const referenced = new Set(refs.map((entry) => entry.ref));
return {
danglingNonterminals: refs.filter((entry) => !defined.has(entry.ref) && !terminals.has(entry.ref)),
unusedTerminals: [...terminals].filter((terminal) => !referenced.has(terminal)).toSorted(byName),
};
};
const keywordValuesOf = function keywordValuesOf(data: PagData, category: string): Set<string> {
return new Set(data.keywords.filter((record) => hasRoleIn(record, category)).map((record) => record.keyword));
};
const recognitionGaps = function recognitionGaps(
data: PagData,
): Pick<GrammarIssues, "unrecognizedDocumentTypes" | "unrecognizedDocumentVerbs"> {
const typeKeywords = keywordValuesOf(data, DOCUMENT_TYPE_CATEGORY);
const verbKeywords = keywordValuesOf(data, DOCUMENT_VERB_CATEGORY);
return {
unrecognizedDocumentTypes: data.documentTypes
.filter((record) => !typeKeywords.has(record.type))
.map((record) => record.type)
.toSorted(byName),
unrecognizedDocumentVerbs: data.documentTypes
.flatMap((record) => record.verbs.map((verb) => ({ type: record.type, verb })))
.filter((entry) => !verbKeywords.has(entry.verb)),
};
};
export const grammarIssuesOf = function grammarIssuesOf(
data: PagData,
typeByName: ReadonlyMap<string, DocumentTypeRecord>,
): GrammarIssues {
return {
...recognitionGaps(data),
...grammarSymbolsOf(data),
danglingTemplateSlots: data.templates.flatMap((template) =>
template.slots
.filter((slot) => slotIsDangling(template, slot))
.map((slot) => ({ slot: slot.name, type: template.type })),
),
docTypesWithoutVerb: data.documentTypes
.filter((record) => record.defaultVerb === "" || record.verbs.length === 0)
.map((record) => record.type),
duplicateKeywordIds: duplicateKeywords(data),
unknownTemplateTypes: data.templates
.filter((template) => !typeByName.has(template.type))
.map((template) => template.type),
};
};