import type { PlacementFinding, TaxonomyFinding, TaxonomyReport } from "../../types/taxonomy.types.ts"; import { containersFor, fixtureMarkerOf, isDeclaredContainer, isForeignContainer, isGeneratedFolder, isIgnoredName, isNameExempt, isNestedRoot, isSpecialContainer, isTestRoot, taxonomyRoots, testMarkerOf, vocabularyFor, } from "../manifests/taxonomy.manifest.ts"; import { existsSync, readdirSync, statSync } from "node:fs"; import { namingFinding, placementFinding, testFinding } from "./taxonomy.analyzer.ts"; import { WORKSPACE_ROOT } from "../resolvers/anchor.resolver.ts"; import { isTextFile } from "../predicates/text.predicate.ts"; import { join } from "node:path"; const PATH_SEPARATOR = "/"; interface Children { dirs: string[]; files: string[]; } const childrenOf = function childrenOf(relDir: string, root?: string): Children { const absDir = join(WORKSPACE_ROOT, relDir); const named = readdirSync(absDir) .filter((name) => !isIgnoredName(name, root)) .toSorted((a, b) => a.localeCompare(b)); const dirs = named.filter((name) => statSync(join(absDir, name)).isDirectory()); return { dirs, files: named.filter((name) => !dirs.includes(name)) }; }; const textFilesUnder = function textFilesUnder(relDir: string, root?: string): string[] { const { dirs, files } = childrenOf(relDir, root); const own = files .map((name) => `${relDir}${PATH_SEPARATOR}${name}`) .filter((path) => isTextFile(join(WORKSPACE_ROOT, path))); return [...own, ...dirs.flatMap((name) => textFilesUnder(`${relDir}${PATH_SEPARATOR}${name}`, root))]; }; const carriesMarker = function carriesMarker(basename: string, marker: string, separator: string): boolean { const segments = basename.split(separator); return segments.length > 2 && segments.at(-2) === marker; }; export const generatedFolderFindings = function generatedFolderFindings(relFolder: string, root?: string): TaxonomyFinding[] { const vocabulary = vocabularyFor(root); const marker = vocabulary.generatedFolder?.marker ?? ""; return textFilesUnder(relFolder, root) .filter((path) => !carriesMarker(path.slice(path.lastIndexOf(PATH_SEPARATOR) + 1), marker, vocabulary.separator)) .map((path): TaxonomyFinding => ({ data: { folder: relFolder, marker }, messageId: "generatedFolderIntruder", path, })); }; const at = function at(path: string, finding: PlacementFinding | null): TaxonomyFinding[] { return finding === null ? [] : [{ ...finding, path }]; }; const fileFindings = function fileFindings(path: string, root: string, special: boolean): TaxonomyFinding[] { const below = path.slice(root.length + 1).split(PATH_SEPARATOR); const basename = below.at(-1) ?? ""; const folders = below.slice(0, -1); if (isTestRoot(root) || testMarkerOf(basename) !== undefined || fixtureMarkerOf(basename, root) !== undefined) { return at(path, testFinding(basename, folders, root)); } const placement = special ? null : placementFinding(basename, folders, root); return [...at(path, placement), ...at(path, namingFinding(basename, folders, root))]; }; const bucketFindings = function bucketFindings(root: string, container: string, files: string[]): TaxonomyFinding[] { const relContainer = `${root}${PATH_SEPARATOR}${container}`; const nested = childrenOf(relContainer, root).dirs.map((name): TaxonomyFinding => ({ data: { container, nested: name, root }, messageId: "nestedInSpecial", path: `${relContainer}${PATH_SEPARATOR}${name}`, })); return [...nested, ...files.flatMap((path) => fileFindings(path, root, true))]; }; const containerFindings = function containerFindings( root: string, container: string, files: string[], ): TaxonomyFinding[] { if (!isDeclaredContainer(root, container)) { const data = { container, declared: containersFor(root).join(", "), root }; return [{ data, messageId: "undeclaredContainer", path: `${root}${PATH_SEPARATOR}${container}` }]; } if (isSpecialContainer(root, container)) { const direct = files.filter( (path) => path.split(PATH_SEPARATOR).length === root.split(PATH_SEPARATOR).length + 2, ); return bucketFindings(root, container, direct); } return files.flatMap((path) => fileFindings(path, root, false)); }; const rootReport = function rootReport(root: string): TaxonomyReport { if (!existsSync(join(WORKSPACE_ROOT, root))) { return { assessed: 0, findings: [{ data: { root }, messageId: "missingRoot", path: root }] }; } const children = childrenOf(root, root); const outputs = children.dirs.filter((name) => isGeneratedFolder(name, root)); const dirs = children.dirs.filter( (name) => !isNestedRoot(root, name) && !isForeignContainer(root, name) && !outputs.includes(name), ); const missing = containersFor(root) .filter((name) => !dirs.includes(name) && !isTestRoot(root)) .map((container): TaxonomyFinding => ({ data: { container, root }, messageId: "missingContainer", path: root, })); const loose = children.files .filter((name) => !isNameExempt(name, root) && isTextFile(join(WORKSPACE_ROOT, root, name))) .map((name): TaxonomyFinding => ({ data: { name, root }, messageId: "looseFileAtRoot", path: `${root}${PATH_SEPARATOR}${name}`, })); const perContainer = dirs.map((name) => textFilesUnder(`${root}${PATH_SEPARATOR}${name}`, root)); const findings = dirs.flatMap((name, index) => containerFindings(root, name, perContainer[index] ?? [])); const generated = outputs.flatMap((name) => generatedFolderFindings(`${root}${PATH_SEPARATOR}${name}`, root)); const assessed = loose.length + perContainer.reduce((sum, files) => sum + files.length, 0); return { assessed, findings: [...missing, ...loose, ...findings, ...generated] }; }; export const taxonomyReport = function taxonomyReport(): TaxonomyReport { const reports = taxonomyRoots() .toSorted((a, b) => a.localeCompare(b)) .map(rootReport); return { assessed: reports.reduce((sum, report) => sum + report.assessed, 0), findings: reports.flatMap((report) => report.findings), }; };