core/adapters/tool.htmlhint.adapter.ts

core/adapters/tool.htmlhint.adapter.ts is a file in GovLab Quality. 107 lines of code and 29 definitions.

import type { Finding, RunResult } from "#types/finding.types";
import { existsSync, readFileSync, statSync } from "node:fs";
import { isRecord, stringArrayField } from "#core/selectors/record.selector";
import { safeReaddir, safeStat } from "#core/loaders/source.loader";
import { MISSING_VERIFY } from "#configuration/strings/tool.strings";
import type { PathExclusion } from "#types/exclusions.types";
import type { RunnerContext } from "#types/tool.types";
import { createRequire } from "node:module";
import { defineTool } from "#core/registries/tool.registry";
import { excludeMatcher } from "#core/factories/exclusions.factory";
import { findingsResult } from "#core/factories/finding.factory";
import { loadGovlabConfig } from "#core/loaders/config.loader";
import path from "node:path";
import process from "node:process";
import { sectionOf } from "#core/selectors/config.selector";
import { withMasterExclude } from "#core/selectors/exclusions.selector";

const SECTION = "htmlhint";
const HTML_EXTENSION = ".html";
const WILDCARD = "*";

interface HtmlhintMessage {
    col: number;
    line: number;
    message: string;
    rule: { id: string };
    type: string;
}

type HtmlhintVerify = (html: string, ruleset?: Record<string, unknown>) => HtmlhintMessage[];

const isVerify = function isVerify(value: unknown): value is HtmlhintVerify {
    return typeof value === "function";
};

const loadVerify = function loadVerify(): HtmlhintVerify {
    const loaded: unknown = createRequire(import.meta.url)(SECTION);
    const api = isRecord(loaded) ? loaded["HTMLHint"] : undefined;
    const verify = isRecord(api) ? api["verify"] : undefined;
    if (!isVerify(verify)) {
        throw new Error(MISSING_VERIFY);
    }
    return (html, ruleset) => verify.call(api, html, ruleset);
};

export const govlabHtmlhintConfig = async function govlabHtmlhintConfig(
    consumerRoot: string = process.cwd(),
): Promise<{ rules: Record<string, unknown>; ignore: string[] }> {
    const config = await loadGovlabConfig(consumerRoot);
    const section = sectionOf(config, SECTION);
    return {
        ignore: withMasterExclude(config, stringArrayField(section, "ignore")),
        rules: isRecord(section["rules"]) ? section["rules"] : {},
    };
};

const isIgnored = function isIgnored(file: string, ignore: readonly string[]): boolean {
    const normalized = file.split(path.sep).join("/");
    return ignore.some((entry) => normalized.includes(entry.split(WILDCARD).join("")));
};

interface Walk {
    excluded: PathExclusion;
    ignore: readonly string[];
}

const walk = function walk(dir: string, scan: Walk): string[] {
    return safeReaddir(dir)
        .map((entry) => path.join(dir, entry.name))
        .filter((full) => !scan.excluded(full))
        .flatMap((full): string[] => {
            const stat = safeStat(full);
            if (stat === null) {
                return [];
            }
            if (stat.isDirectory()) {
                return walk(full, scan);
            }
            return full.endsWith(HTML_EXTENSION) && !isIgnored(full, scan.ignore) ? [full] : [];
        });
};

const baseDir = function baseDir(entry: string): string {
    const star = entry.indexOf(WILDCARD);
    const trimmed = star === -1 ? entry : entry.slice(0, star);
    const sep = trimmed.lastIndexOf("/");
    return sep === -1 ? "." : trimmed.slice(0, sep);
};

const entryTargets = function entryTargets(root: string, entry: string, scan: Walk): string[] {
    if (!entry.includes(WILDCARD) && entry.endsWith(HTML_EXTENSION)) {
        return [path.resolve(root, entry)];
    }
    const dir = path.resolve(root, baseDir(entry));
    return existsSync(dir) && statSync(dir).isDirectory() ? walk(dir, scan) : [];
};

const runHtmlhint = async function runHtmlhint(context: RunnerContext): Promise<RunResult> {
    const { rules, ignore } = await govlabHtmlhintConfig(context.root);
    const scan: Walk = { excluded: await excludeMatcher(context.root), ignore };
    const verify = loadVerify();
    const files = [...new Set(context.paths.flatMap((entry) => entryTargets(context.root, entry, scan)))];
    const findings = files.flatMap((file): Finding[] =>
        verify(readFileSync(file, "utf8"), rules).map((message) => ({
            advisory: false,
            column: message.col,
            ecosystem: context.ecosystem,
            file,
            fixable: false,
            line: message.line,
            message: message.message,
            ruleId: message.rule.id,
            severity: "error",
            tool: SECTION,
        })),
    );
    return findingsResult(findings);
};

defineTool({ ecosystems: ["html"], run: runHtmlhint, tool: SECTION });