# codemods/analyzers/literal.analyzer.ts

> 80 lines of code and 15 definitions.

Tree: Governance tree
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/governance#file-governance-codemods-analyzers-literal-analyzer-ts
Source text: https://banes-lab.com/source/governance/codemods/analyzers/literal.analyzer.ts.txt

Listed in [codemods/analyzers](https://banes-lab.com/api/source/governance/codemods/analyzers.md), after [codemods/analyzers/increment.analyzer.ts](https://banes-lab.com/source/governance/codemods/analyzers/increment.analyzer.ts.md) and before [codemods/analyzers/location.analyzer.ts](https://banes-lab.com/source/governance/codemods/analyzers/location.analyzer.ts.md).

## Definitions

- `literalFindingsIn` (lexical_declaration, line 27, exported)
- `filesUnder` (lexical_declaration, line 63)
- `isExcluded` (lexical_declaration, line 59)
- `collectLiteralFindings` (lexical_declaration, line 76, exported)
- `JSON_EXTENSION` (lexical_declaration, line 10)
- `NEWLINE` (lexical_declaration, line 11)
- `lineAt` (lexical_declaration, line 13)
- `sitesOf` (lexical_declaration, line 17)
- `sites` (lexical_declaration, line 18)
- `index` (lexical_declaration, line 19)
- `found` (lexical_declaration, line 32, exported)
- `edits` (lexical_declaration, line 47, exported)
- `reason` (lexical_declaration, line 55, exported)
- `full` (lexical_declaration, line 68)
- `wanted` (lexical_declaration, line 81, exported)

## Uses

- [codemods/selectors/edit.selector.ts](https://banes-lab.com/source/governance/codemods/selectors/edit.selector.ts.md)

## Source

```typescript
import type { LiteralFinding, LiteralPair } from "../../types/analyzer.types.ts";
import { disjoint, parsesAsJson, splice } from "../selectors/edit.selector.ts";
import { readFileSync, readdirSync, statSync } from "node:fs";
import { MASTER_EXCLUDE_MARKERS } from "../../shared/generated/exclusions.generated.ts";
import { ROOT } from "@ssot/paths";
import { isExcludedPath } from "@govlab/quality/core/matchers/exclusions.matcher.ts";
import path from "node:path";
import { relPath } from "../selectors/program.selector.ts";

const JSON_EXTENSION = ".json";
const NEWLINE = "\n";

const lineAt = function lineAt(content: string, index: number): number {
    return content.slice(0, index).split(NEWLINE).length;
};

const sitesOf = function sitesOf(content: string, pair: LiteralPair): number[] {
    const sites: number[] = [];
    let index = content.indexOf(pair.from);
    while (index !== -1) {
        sites.push(index);
        index = content.indexOf(pair.from, index + pair.from.length);
    }
    return sites;
};

export const literalFindingsIn = function literalFindingsIn(
    fileName: string,
    content: string,
    pairs: readonly LiteralPair[],
): LiteralFinding[] {
    const found = pairs.flatMap((pair) =>
        sitesOf(content, pair).map((start) => ({
            end: start + pair.from.length,
            file: relPath(fileName),
            fileName,
            from: pair.from,
            line: lineAt(content, start),
            reason: null,
            start,
            to: pair.to,
        })),
    );
    if (found.length === 0 || path.extname(fileName) !== JSON_EXTENSION) {
        return found;
    }
    const edits = disjoint(
        found
            .map((finding) => ({ end: finding.end, replacement: finding.to, start: finding.start }))
            .toSorted((a, b) => b.start - a.start),
    );
    if (parsesAsJson(splice(content, edits))) {
        return found;
    }
    const reason = `replacing every match in ${relPath(fileName)} leaves it unparsable as JSON, so the file is left unchanged`;
    return found.map((finding) => ({ ...finding, reason }));
};

const isExcluded = function isExcluded(full: string): boolean {
    return isExcludedPath(path.relative(ROOT, full), MASTER_EXCLUDE_MARKERS);
};

const filesUnder = function filesUnder(target: string): string[] {
    if (!statSync(target).isDirectory()) {
        return [target];
    }
    return readdirSync(target, { withFileTypes: true }).flatMap((entry) => {
        const full = path.join(target, entry.name);
        if (isExcluded(full)) {
            return [];
        }
        return entry.isDirectory() ? filesUnder(full) : [full];
    });
};

export const collectLiteralFindings = function collectLiteralFindings(
    roots: readonly string[],
    pairs: readonly LiteralPair[],
    extensions: readonly string[],
): LiteralFinding[] {
    const wanted = (fileName: string): boolean =>
        extensions.length === 0 || extensions.includes(path.extname(fileName));
    return roots
        .flatMap((root) => filesUnder(path.resolve(ROOT, root)))
        .filter(wanted)
        .flatMap((fileName) => literalFindingsIn(fileName, readFileSync(fileName, "utf8"), pairs));
};
```
