import { isWordCharacter } from "../predicates/token.predicate.ts"; export interface RequestedOperation { readonly operation: string; readonly requested: boolean; readonly supplied: boolean; readonly refusal: string; } export const unsuppliedOperands = function unsuppliedOperands(requested: readonly RequestedOperation[]): string[] { return requested.filter((entry) => entry.requested && !entry.supplied).map((entry) => entry.refusal); }; const FLAG_LEAD = "--"; export const writesNothing = function writesNothing( argv: readonly string[], flag: string, selfRehearsing: readonly string[], ): boolean { return argv.includes(flag) && !selfRehearsing.some((form) => argv.includes(form)); }; const flagOf = function flagOf(token: string): string { const equals = token.indexOf("="); return equals === -1 ? token : token.slice(0, equals); }; export const unknownArguments = function unknownArguments(argv: readonly string[], known: readonly string[]): string[] { const flags = argv .filter((token) => token.startsWith(FLAG_LEAD) && token.length > FLAG_LEAD.length) .map(flagOf) .filter((flag) => !known.includes(flag)); return [...new Set(flags)]; }; export const unshareableOperations = function unshareableOperations( requested: readonly string[], exclusive: readonly string[], ): string[] { const held = requested.filter((flag) => exclusive.includes(flag)); return held.length > 0 && requested.length > 1 ? held : []; }; const READ = "readFileSync"; const WRITE = "writeFileSync"; const COMPARISONS = ["!==", "===", "!=", "=="]; const QUOTES = new Set(["'", '"', "`"]); const ESCAPE = "\\"; const DECLARE_CONST = "const "; const FUNCTION_WORD = "function "; const FUNCTION_LEADS = ["function ", "async function ", "export function ", "export async function "]; const BLOCK_CLOSE = "}"; const BRACE_DELTA = new Map([ ["{", 1], ["}", -1], ]); export interface Mutation { readonly target: string; readonly line: number; readonly reads: number; readonly witnessed: boolean; } interface Event { readonly kind: "read" | "write"; readonly target: string; readonly index: number; readonly line: number; } interface QuoteState { readonly quote: string; readonly escaped: boolean; } interface MaskStep extends QuoteState { readonly code: boolean; } const isWordChar = function isWordChar(char: string): boolean { return isWordCharacter(char) || char === "$"; }; const isLetter = function isLetter(char: string): boolean { return (char >= "a" && char <= "z") || (char >= "A" && char <= "Z"); }; const isAllLetters = function isAllLetters(text: string): boolean { for (const character of text) { if (!isLetter(character)) { return false; } } return true; }; const maskStep = function maskStep(state: QuoteState, char: string): MaskStep { if (state.quote === "") { return QUOTES.has(char) ? { code: false, escaped: false, quote: char } : { ...state, code: true }; } if (state.escaped) { return { ...state, code: false, escaped: false }; } return { code: false, escaped: char === ESCAPE, quote: char === state.quote ? "" : state.quote }; }; const codeMask = function codeMask(source: string): boolean[] { const mask: boolean[] = []; let state: QuoteState = { escaped: false, quote: "" }; for (let index = 0; index < source.length; index += 1) { const step = maskStep(state, source.charAt(index)); mask.push(step.code); state = step; } return mask; }; const matchesAt = function matchesAt(source: string, index: number, word: string): boolean { if (!source.startsWith(word, index)) { return false; } if (index > 0 && isWordChar(source.charAt(index - 1))) { return false; } return !isWordChar(source.charAt(index + word.length)); }; const spacesFrom = function spacesFrom(source: string, from: number): number { let cursor = from; while (cursor < source.length && source.charAt(cursor) === " ") { cursor += 1; } return cursor; }; const wordFrom = function wordFrom(source: string, from: number): string { let cursor = from; while (cursor < source.length && isWordChar(source.charAt(cursor))) { cursor += 1; } return source.slice(from, cursor); }; const argumentAt = function argumentAt(source: string, after: number): string | null { const open = spacesFrom(source, after); if (source.charAt(open) !== "(") { return null; } const argument = wordFrom(source, spacesFrom(source, open + 1)); return argument.length === 0 ? null : argument; }; const eventAt = function eventAt(source: string, index: number, line: number): Event | null { const read = matchesAt(source, index, READ); if (!read && !matchesAt(source, index, WRITE)) { return null; } const target = argumentAt(source, index + (read ? READ : WRITE).length); return target === null ? null : { index, kind: read ? "read" : "write", line, target }; }; const events = function events(source: string): Event[] { const mask = codeMask(source); const out: Event[] = []; let line = 1; for (let index = 0; index < source.length; index += 1) { const event = mask[index] === true ? eventAt(source, index, line) : null; if (event !== null) { out.push(event); } line += source.charAt(index) === "\n" ? 1 : 0; } return out; }; const comparedBetween = function comparedBetween(source: string, from: number, to: number): boolean { return COMPARISONS.some((operator) => { const at = source.indexOf(operator, from); return at !== -1 && at + operator.length <= to; }); }; export interface BranchOperand { readonly name: string; readonly line: number; } const BRANCH_MARKS = ["?", "if (", "&&", "||"]; const memberValue = function memberValue(trimmed: string): string { const colon = trimmed.indexOf(":"); if (colon <= 0) { return ""; } const raw = trimmed.slice(colon + 1).trim(); const value = raw.endsWith(",") ? raw.slice(0, -1).trim() : raw; return isAllLetters(value) ? value : ""; }; const memberKey = function memberKey(trimmed: string): string { const colon = trimmed.indexOf(":"); const key = colon <= 0 ? "" : trimmed.slice(0, colon).trim(); return isAllLetters(key) ? key : ""; }; const declaresCarrier = function declaresCarrier(trimmed: string, carrier: string): boolean { const lead = `${DECLARE_CONST}${carrier}`; if (!trimmed.startsWith(lead)) { return false; } const next = trimmed.charAt(lead.length); return next === " " || next === ":" || next === "="; }; const braceDelta = function braceDelta(line: string): number { let delta = 0; for (const character of line) { delta += BRACE_DELTA.get(character) ?? 0; } return delta; }; const carrierIn = function carrierIn(line: string, reportCall: string): string { const at = line.indexOf(`${reportCall}(`); if (at === -1) { return ""; } const inside = line.slice(at + reportCall.length + 1); const close = inside.indexOf(")"); return close === -1 ? "" : (inside.slice(0, close).split(",").at(-1) ?? "").trim(); }; const reportCarrier = function reportCarrier(lines: readonly string[], reportCall: string): string { return lines.map((line) => carrierIn(line, reportCall)).find((carrier) => carrier.length > 0) ?? ""; }; interface ScanState { readonly inRun: boolean; readonly inReport: boolean; readonly runDepth: number; readonly reportDepth: number; } interface LineFacts { readonly line: number; readonly branching: string | null; readonly passed: string | null; readonly published: readonly string[]; } interface Calls { readonly carrier: string; readonly runCall: string; readonly reportCall: string; } const OUTSIDE: ScanState = { inReport: false, inRun: false, reportDepth: 0, runDepth: 0 }; const branchingName = function branchingName(trimmed: string): string | null { if (!trimmed.startsWith(DECLARE_CONST) || !BRANCH_MARKS.some((mark) => trimmed.includes(mark))) { return null; } const rest = trimmed.slice(DECLARE_CONST.length); const stop = rest.indexOf(" "); return stop <= 0 ? null : rest.slice(0, stop); }; const opened = function opened(state: ScanState, trimmed: string, calls: Calls): ScanState { const declared = calls.carrier.length > 0 && declaresCarrier(trimmed, calls.carrier); const opensReport = declared || trimmed.includes(`${calls.reportCall}(`); const opensRun = trimmed.includes(`${calls.runCall}(`); return { inReport: state.inReport || opensReport, inRun: state.inRun || opensRun, reportDepth: opensReport ? 0 : state.reportDepth, runDepth: opensRun ? 0 : state.runDepth, }; }; const closed = function closed(state: ScanState, trimmed: string): ScanState { const delta = braceDelta(trimmed); const runDepth = state.runDepth + (state.inRun ? delta : 0); const reportDepth = state.reportDepth + (state.inReport ? delta : 0); const closes = trimmed.startsWith(BLOCK_CLOSE); return { inReport: state.inReport && !(reportDepth <= 0 && closes), inRun: state.inRun && !(runDepth <= 0 && closes), reportDepth, runDepth, }; }; const factsOf = function factsOf(state: ScanState, trimmed: string, line: number): LineFacts { const value = memberValue(trimmed); return { branching: branchingName(trimmed), line, passed: state.inRun && value.length > 0 ? value : null, published: state.inReport ? [memberKey(trimmed), value].filter((word) => word.length > 0) : [], }; }; const scannedFacts = function scannedFacts(lines: readonly string[], calls: Calls): LineFacts[] { const out: LineFacts[] = []; let state = OUTSIDE; for (const [index, line] of lines.entries()) { const trimmed = line.trim(); state = opened(state, trimmed, calls); out.push(factsOf(state, trimmed, index + 1)); state = closed(state, trimmed); } return out; }; export const unpublishedBranchOperands = function unpublishedBranchOperands( source: string, runCall: string, reportCall: string, ): BranchOperand[] { const lines = source.split("\n"); const facts = scannedFacts(lines, { carrier: reportCarrier(lines, reportCall), reportCall, runCall }); const branching = new Set(facts.flatMap((fact) => (fact.branching === null ? [] : [fact.branching]))); const published = new Set(facts.flatMap((fact) => fact.published)); const passed = new Map( facts.flatMap((fact): [string, number][] => (fact.passed === null ? [] : [[fact.passed, fact.line]])).toReversed(), ); return [...passed] .filter(([name]) => branching.has(name) && !published.has(name)) .toSorted((left, right) => left[1] - right[1]) .map(([name, line]) => ({ line, name })); }; export interface PresenceBackedGuard { readonly guard: string; readonly reader: string; readonly line: number; readonly chain: readonly string[]; } const identifiersIn = function identifiersIn(text: string): string[] { const mask = codeMask(text); const out: string[] = []; let held = ""; for (let index = 0; index < text.length; index += 1) { const char = text.charAt(index); if (mask[index] === true && isWordChar(char)) { held += char; } else { out.push(...(held.length > 0 ? [held] : [])); held = ""; } } return [...out, ...(held.length > 0 ? [held] : [])]; }; const constEntry = function constEntry(trimmed: string): [string, string] | null { const name = wordFrom(trimmed, DECLARE_CONST.length); const equals = trimmed.indexOf("="); return name.length > 0 && equals !== -1 ? [name, trimmed.slice(equals + 1)] : null; }; const functionBody = function functionBody(lines: readonly string[], index: number): string { const rest = lines.slice(index + 1); const end = rest.findIndex((line) => line.startsWith(BLOCK_CLOSE)); return (end === -1 ? rest : rest.slice(0, end)).map((line) => `${line}\n`).join(""); }; const functionEntry = function functionEntry(lines: readonly string[], index: number, trimmed: string): [string, string] | null { if (!FUNCTION_LEADS.some((lead) => trimmed.startsWith(lead))) { return null; } const name = wordFrom(trimmed, trimmed.indexOf(FUNCTION_WORD) + FUNCTION_WORD.length); return name.length === 0 ? null : [name, functionBody(lines, index)]; }; const declarationBodies = function declarationBodies(lines: readonly string[]): Map { const entries = lines.flatMap((line, index) => { const trimmed = line.trim(); const entry = trimmed.startsWith(DECLARE_CONST) ? constEntry(trimmed) : functionEntry(lines, index, trimmed); return entry === null ? [] : [entry]; }); return new Map(entries.toReversed()); }; const mutationRoots = function mutationRoots(lines: readonly string[], fields: ReadonlySet): Map { const roots = lines.flatMap((line, index): [string, number][] => { const trimmed = line.trim(); const value = fields.has(memberKey(trimmed)) ? memberValue(trimmed) : ""; return value.length > 0 ? [[value, index + 1]] : []; }); return new Map(roots.toReversed()); }; const searchGuard = function searchGuard( guard: string, line: number, bodies: ReadonlyMap, presence: ReadonlySet, depth: number, ): PresenceBackedGuard | null { const seen = new Set([guard]); const chain: string[] = [guard]; let frontier: readonly string[] = [guard]; for (let level = 0; level < depth && frontier.length > 0; level += 1) { const identifiers = frontier.flatMap((name) => identifiersIn(bodies.get(name) ?? "")); const hit = identifiers.findIndex((identifier) => presence.has(identifier)); const scanned = hit === -1 ? identifiers : identifiers.slice(0, hit); const unseen = [...new Set(scanned)].filter((identifier) => !seen.has(identifier)); const expanding = unseen.filter((identifier) => bodies.has(identifier)); for (const identifier of unseen) { seen.add(identifier); } chain.push(...expanding); const reader = identifiers[hit]; if (reader !== undefined) { return { chain: [...chain, reader], guard, line, reader }; } frontier = expanding; } return null; }; export const presenceBackedMutationGuards = function presenceBackedMutationGuards( source: string, mutationFields: readonly string[], presenceReaders: readonly string[], depth: number, ): PresenceBackedGuard[] { const lines = source.split("\n"); const bodies = declarationBodies(lines); const presence = new Set(presenceReaders); return [...mutationRoots(lines, new Set(mutationFields))] .toSorted((left, right) => left[1] - right[1]) .flatMap(([guard, line]) => { const found = searchGuard(guard, line, bodies, presence, depth); return found === null ? [] : [found]; }); }; const witnessedWrite = function witnessedWrite(source: string, priors: readonly Event[], write: Event): boolean { const last = priors.at(-1); return priors.length >= 2 && last !== undefined && comparedBetween(source, last.index, write.index); }; export const unwitnessedWrites = function unwitnessedWrites(source: string): Mutation[] { const ordered = events(source); return ordered .filter((event) => event.kind === "write") .flatMap((write) => { const priors = ordered.filter( (candidate) => candidate.kind === "read" && candidate.target === write.target && candidate.index < write.index, ); const witnessed = witnessedWrite(source, priors, write); return priors.length === 0 || witnessed ? [] : [{ line: write.line, reads: priors.length, target: write.target, witnessed }]; }); };