core/producers/tool.rubocop.producer.ts

core/producers/tool.rubocop.producer.ts is a file in GovLab Quality. 189 lines of code and 44 definitions.

import type { CatalogProduct, CatalogRule, KnobEntry } from "#types/catalog.types";
import { commandOutput, commandVersion } from "#core/adapters/invocation.adapter";
import { INTEGER_TYPE } from "#configuration/constants/knob.constants";
import { byRuleId } from "#core/comparators/rule.comparator";
import { defineProducer } from "#core/registries/producer.registry";
import { firstRemoteText } from "#core/adapters/remote.adapter";
import { isRecord } from "#core/selectors/record.selector";
import { isThreshold } from "#core/predicates/knob.predicate";
import { parseYaml } from "#core/parsers/config.yaml.parser";
import { tallyBy } from "#core/aggregators/catalog.aggregator";
import { withoutRubyTags } from "#core/parsers/knob.parser";

const TOOL = "rubocop";
const PLUGINS_SOURCE = "rubocop-plugins";
const DOCS = "https://docs.rubocop.org";
const DESCRIPTION_KEY = "Description:";
const ENABLED_KEY = "Enabled:";
const ENABLED_LEVEL = "enabled";
const DISABLED_LEVEL = "disabled";
const PENDING_LEVEL = "pending";
const REPOSITORIES: readonly [string, string][] = [
    ["rubocop", "rubocop/rubocop"],
    ["rubocop-rails", "rubocop/rubocop-rails"],
    ["rubocop-rspec", "rubocop/rubocop-rspec"],
    ["rubocop-performance", "rubocop/rubocop-performance"],
    ["rubocop-minitest", "rubocop/rubocop-minitest"],
    ["rubocop-rake", "rubocop/rubocop-rake"],
    ["rubocop-thread_safety", "rubocop/rubocop-thread_safety"],
    ["rubocop-capybara", "rubocop/rubocop-capybara"],
    ["rubocop-factory_bot", "rubocop/rubocop-factory_bot"],
    ["rubocop-graphql", "DmitryTsepelev/rubocop-graphql"],
    ["rubocop-sequel", "rubocop/rubocop-sequel"],
];
const META_KEYS: ReadonlySet<string> = new Set([
    "Description",
    "Enabled",
    "VersionAdded",
    "VersionChanged",
    "StyleGuide",
    "Reference",
    "References",
    "SafeAutoCorrect",
    "Safe",
    "AutoCorrect",
    "Include",
    "Exclude",
    "Severity",
    "Details",
]);

interface Cop {
    department: string;
    description: string | null;
    level: string | null;
    name: string;
}

const stripQuotes = function stripQuotes(value: string): string {
    const text = value.trim();
    const quoted = (text.startsWith("'") && text.endsWith("'")) || (text.startsWith('"') && text.endsWith('"'));
    return quoted ? text.slice(1, -1) : text;
};

const levelOfDeclared = function levelOfDeclared(declared: string): string {
    if (declared === "false") {
        return DISABLED_LEVEL;
    }
    return declared === PENDING_LEVEL ? PENDING_LEVEL : ENABLED_LEVEL;
};

const isCopHeading = function isCopHeading(line: string): boolean {
    const unindented = line.length > 0 && !line.startsWith(" ") && !line.startsWith("\t");
    return unindented && line.includes("/") && line.endsWith(":");
};

const updatedCop = function updatedCop(cop: Cop, text: string): Cop {
    if (text.startsWith(DESCRIPTION_KEY)) {
        return { ...cop, description: stripQuotes(text.slice(DESCRIPTION_KEY.length)) };
    }
    if (text.startsWith(ENABLED_KEY)) {
        return { ...cop, level: levelOfDeclared(text.slice(ENABLED_KEY.length).trim()) };
    }
    return cop;
};

const readCopLine = function readCopLine(cops: readonly Cop[], line: string): Cop[] {
    if (isCopHeading(line)) {
        const name = line.slice(0, -1);
        return [...cops, { department: name.slice(0, name.indexOf("/")), description: null, level: null, name }];
    }
    const last = cops.at(-1);
    return last === undefined ? [...cops] : [...cops.slice(0, -1), updatedCop(last, line.trim())];
};

const copsOf = function copsOf(lines: readonly string[]): Cop[] {
    let cops: Cop[] = [];
    for (const line of lines) {
        cops = readCopLine(cops, line);
    }
    return cops;
};

const defaultConfig = async function defaultConfig(repository: string): Promise<Record<string, unknown>> {
    const root = `https://raw.githubusercontent.com/${repository}`;
    const text = await firstRemoteText([`${root}/master/config/default.yml`, `${root}/main/config/default.yml`]);
    const doc = parseYaml(withoutRubyTags(text ?? ""));
    return isRecord(doc) ? doc : {};
};

const copEntries = function copEntries(doc: Record<string, unknown>): [string, Record<string, unknown>][] {
    return Object.entries(doc).filter(
        (entry): entry is [string, Record<string, unknown>] => entry[0].includes("/") && isRecord(entry[1]),
    );
};

const levelOf = function levelOf(body: Record<string, unknown>): string {
    if (body["Enabled"] === false) {
        return DISABLED_LEVEL;
    }
    return body["Enabled"] === PENDING_LEVEL ? PENDING_LEVEL : ENABLED_LEVEL;
};

const coreProduct = function coreProduct(): CatalogProduct {
    const version = commandVersion(TOOL, ["--version"]);
    const lines = commandOutput(TOOL, ["--show-cops"])
        .split("\n")
        .map((line) => (line.endsWith("\r") ? line.slice(0, -1) : line));
    const rules = copsOf(lines)
        .map((cop): CatalogRule => ({
            canonical: null,
            category: cop.department,
            defaultLevel: cop.level ?? ENABLED_LEVEL,
            description: cop.description,
            ecosystem: "ruby",
            name: cop.name,
            ruleId: cop.name,
            tool: TOOL,
            toolVersion: version,
            url: `${DOCS}/rubocop/cops_${cop.department.toLowerCase()}.html`,
        }))
        .toSorted(byRuleId);
    return {
        rules,
        source: TOOL,
        summary: { byCategory: tallyBy(rules, "category"), tool: TOOL, toolVersion: version, total: rules.length },
    };
};

const pluginRules = function pluginRules(tool: string, doc: Record<string, unknown>): CatalogRule[] {
    return copEntries(doc).map(([key, body]): CatalogRule => {
        const description = body["Description"];
        return {
            canonical: null,
            category: key.slice(0, key.indexOf("/")),
            defaultLevel: levelOf(body),
            description: typeof description === "string" ? description : null,
            ecosystem: "ruby",
            name: key,
            ruleId: key,
            tool,
            toolVersion: "latest",
            url: `${DOCS}/${tool}/cops.html`,
        };
    });
};

const pluginProduct = async function pluginProduct(): Promise<CatalogProduct> {
    const plugins = REPOSITORIES.filter(([tool]) => tool !== TOOL);
    const docs = await Promise.all(plugins.map(async ([, repository]) => defaultConfig(repository)));
    const perPlugin = plugins.map(([tool], index) => ({ rules: pluginRules(tool, docs[index] ?? {}), tool }));
    const rules = perPlugin.flatMap((entry) => entry.rules).toSorted(byRuleId);
    const extensions = perPlugin.map((entry) => ({ cops: entry.rules.length, tool: entry.tool }));
    return { rules, source: PLUGINS_SOURCE, summary: { extensions, tool: PLUGINS_SOURCE, total: rules.length } };
};

const typeOf = function typeOf(value: unknown): string {
    if (typeof value === "number") {
        return INTEGER_TYPE;
    }
    if (typeof value === "boolean") {
        return "boolean";
    }
    return Array.isArray(value) ? "array" : "string";
};

const copKnobs = function copKnobs(tool: string, cop: string, body: Record<string, unknown>): KnobEntry[] {
    return Object.entries(body)
        .filter(([key]) => !META_KEYS.has(key))
        .map(([key, value]): KnobEntry => {
            const type = typeOf(value);
            const fallback = typeof value === "object" ? null : value;
            return [
                tool,
                cop,
                { default: fallback, knob: key, threshold: type === INTEGER_TYPE && isThreshold(key), type },
            ];
        });
};

const knobs = async function knobs(): Promise<KnobEntry[]> {
    const docs = await Promise.all(REPOSITORIES.map(async ([, repository]) => defaultConfig(repository)));
    return REPOSITORIES.flatMap(([tool], index) =>
        copEntries(docs[index] ?? {}).flatMap(([cop, body]) => copKnobs(tool, cop, body)),
    );
};

defineProducer({ knobs, name: TOOL, produce: async () => [coreProduct(), await pluginProduct()], refresh: "manual" });