core/adapters/tool.sqlfluff.adapter.ts

core/adapters/tool.sqlfluff.adapter.ts is a file in GovLab Quality. 111 lines of code and 20 definitions.

import type { AdvisoryContext, RunnerContext, ScanSpec, ToolCall } from "#types/tool.types";
import {
    EMPTY_RESULT,
    findingsResult,
    fixedResult,
    signalDeathResult,
    toolFailure,
} from "#core/factories/finding.factory";
import { NO_STDERR_DETAIL, commandHint, toolErrorBecause } from "#configuration/strings/tool.strings";
import { fixCycle, passTool } from "#core/adapters/tool.adapter";
import { freshToolFile, readToolReport, writeToolFile } from "#core/persistence/tool.persistence";
import { nativeToolConfig, toolSetting } from "#core/resolvers/tool.resolver";
import { signalKilled, statusIn } from "#core/predicates/failure.predicate";
import { FINDING_STATUSES } from "#configuration/constants/tool.constants";
import type { RunResult } from "#types/finding.types";
import { commandOf } from "#core/lexers/invocation.lexer";
import { defineTool } from "#core/registries/tool.registry";
import { failedOnStatus } from "#core/classifiers/invocation.classifier";
import { gatingAdvisory } from "#core/factories/tool.factory";
import { parseSqlfluffOutput } from "#core/parsers/tool.sqlfluff.parser";
import { scopedFiles } from "#core/resolvers/scope.resolver";
import { spawnTool } from "#core/adapters/invocation.adapter";
import { tailLines } from "#core/selectors/failure.selector";

const TOOL = "sqlfluff";
const LANGUAGE = "sql";
const CONFIG_FILE = ".sqlfluff";
const REPORT_FILE = "sqlfluff-report.json";
const DEFAULTS = { command: "python -m sqlfluff", dialect: "ansi" };

interface Job {
    advisory: AdvisoryContext;
    bin: string;
    common: string[];
    files: string[];
    prefix: string[];
    reportFile: string;
}

const lintCall = function lintCall(job: Job): ToolCall {
    const args = [
        ...job.prefix,
        "lint",
        ...job.common,
        "--format",
        "json",
        "--write-output",
        job.reportFile,
        ...job.files,
    ];
    return { args, bin: job.bin, cwd: job.advisory.root };
};

const lintSpec = function lintSpec(job: Job): ScanSpec {
    return {
        failed: failedOnStatus(FINDING_STATUSES),
        failure: (exit) =>
            toolFailure(
                job.advisory,
                toolErrorBecause(
                    TOOL,
                    exit.status,
                    "a bad dialect or config",
                    tailLines(exit.stderr, NO_STDERR_DETAIL),
                ),
            ),
        parse: () => parseSqlfluffOutput(readToolReport(job.reportFile), job.advisory.ecosystem),
    };
};

const applyFix = function applyFix(job: Job, reported: RunResult["findings"]): RunResult {
    const fixArgs = [...job.prefix, "fix", ...job.common, "--disable-progress-bar", ...job.files];
    const fixed = spawnTool(job.bin, fixArgs, { cwd: job.advisory.root, encoding: "utf8", shell: false });
    if (fixed.error) {
        return findingsResult(reported);
    }
    if (signalKilled(fixed.status)) {
        return signalDeathResult(job.advisory, fixed.signal);
    }
    const call = lintCall(job);
    const after = spawnTool(call.bin, call.args, { cwd: call.cwd, encoding: "utf8", shell: false });
    if (signalKilled(after.status)) {
        return signalDeathResult(job.advisory, after.signal);
    }
    if (!statusIn(FINDING_STATUSES, after.status)) {
        return findingsResult(reported);
    }
    return fixedResult(reported, parseSqlfluffOutput(readToolReport(job.reportFile), job.advisory.ecosystem));
};

const runSqlfluff = async function runSqlfluff(context: RunnerContext): Promise<RunResult> {
    const files = await scopedFiles(context.root, context.paths);
    if (files.length === 0) {
        return EMPTY_RESULT;
    }
    const { command, dialect } = await toolSetting(context.root, TOOL, DEFAULTS);
    const configFile = writeToolFile(
        context.root,
        CONFIG_FILE,
        await nativeToolConfig(context.root, LANGUAGE, CONFIG_FILE),
    );
    const { bin, prefix } = commandOf(command, TOOL);
    const job: Job = {
        advisory: gatingAdvisory(context, TOOL, commandHint(TOOL, bin)),
        bin,
        common: ["--config", configFile, "--dialect", dialect],
        files,
        prefix,
        reportFile: freshToolFile(context.root, REPORT_FILE),
    };
    return fixCycle(
        passTool(job.advisory, lintCall(job), lintSpec(job)),
        (findings) => context.fix && findings.some((finding) => finding.fixable),
        (reported) => applyFix(job, reported),
    );
};

defineTool({ ecosystems: [LANGUAGE], run: runSqlfluff, tool: TOOL });