harn-stdlib 0.8.63

Embedded Harn standard library source catalog
Documentation
import { rules_report, rules_search } from "std/rules"

/**
 * `harn scan` — read-only structural search + lint over a fileset.
 *
 * The Rust shim (crates/harn-cli/src/commands/scan.rs) resolves the rule(s)
 * and the matching files (walking directories, filtering by each rule's
 * language) and hands this handler a *plan*. This handler runs the engine
 * (`std/rules`) and formats human or `--json` output.
 *
 * Inputs:
 *   HARN_SCAN_PLAN_JSON   — JSON array of {rule, language, files: [...]}.
 *   HARN_SCAN_REPORT_ONLY — "1" for data-table counts, "0" for each match.
 *   HARN_OUTPUT_JSON      — "1" for the JSON envelope, else human text.
 */
fn join_strs(parts: list, sep: string) -> string {
  var out = ""
  var first = true
  for part in parts {
    if first {
      out = part
      first = false
    } else {
      out = out + sep + part
    }
  }
  return out
}

fn loc(m: dict) -> string {
  // 1-based row/col, editor-friendly.
  return to_string(m.start_row + 1) + ":" + to_string(m.start_col + 1)
}

fn captures_suffix(caps: dict) -> string {
  let names = keys(caps)
  if len(names) == 0 {
    return ""
  }
  var parts = []
  for name in names {
    parts = parts.push(name + "=" + caps[name])
  }
  return "  [" + join_strs(parts, " ") + "]"
}

fn summary_line(total: int, files: int) -> string {
  return to_string(total) + " match(es) in " + to_string(files) + " file(s)"
}

fn run_search(plan: list) -> dict {
  var lines = []
  var results = []
  var total = 0
  var seen = {}
  for entry in plan {
    if len(entry.files) == 0 {
      continue
    }
    let res = rules_search({rule: entry.rule, paths: entry.files})
    let matches = res.matches ?? []
    total = total + len(matches)
    for m in matches {
      seen = seen.merge({[m.path]: true})
      lines = lines.push(m.path + ":" + loc(m) + ": " + m.text + captures_suffix(m.captures ?? {}))
    }
    results = results.push({language: entry.language, match_count: len(matches), matches: matches})
  }
  return {lines: lines, results: results, total: total, files: len(keys(seen))}
}

fn run_report(plan: list) -> dict {
  var lines = []
  var tables = []
  var total = 0
  var seen = {}
  for entry in plan {
    if len(entry.files) == 0 {
      continue
    }
    let table = rules_report({rule: entry.rule, paths: entry.files})
    let summary = table.summary ?? {total_rows: 0, files: 0, per_file: {}}
    total = total + summary.total_rows
    let per_file = summary.per_file ?? {}
    for path in keys(per_file) {
      seen = seen.merge({[path]: true})
      lines = lines.push(path + ": " + to_string(per_file[path]))
    }
    tables = tables.push(table)
  }
  return {lines: lines, tables: tables, total: total, files: len(keys(seen))}
}

fn main(harness: Harness) {
  let raw = harness.env.get_or("HARN_SCAN_PLAN_JSON", "")
  if raw == "" {
    harness.stdio.eprintln("scan: internal error — HARN_SCAN_PLAN_JSON not set by the shim")
    exit(70)
  }
  let plan = json_parse(raw)
  let report_only = harness.env.get_or("HARN_SCAN_REPORT_ONLY", "0") == "1"
  let json_mode = harness.env.get_or("HARN_OUTPUT_JSON", "0") == "1"
  if report_only {
    let out = run_report(plan)
    if json_mode {
      let envelope = {
        schemaVersion: 1,
        mode: "report",
        tables: out.tables,
        summary: {total: out.total, files: out.files},
      }
      harness.stdio.println(json_stringify_pretty(envelope))
    } else {
      for line in out.lines {
        harness.stdio.println(line)
      }
      harness.stdio.println(summary_line(out.total, out.files))
    }
  } else {
    let out = run_search(plan)
    if json_mode {
      let envelope = {
        schemaVersion: 1,
        mode: "search",
        results: out.results,
        summary: {total: out.total, files: out.files},
      }
      harness.stdio.println(json_stringify_pretty(envelope))
    } else {
      for line in out.lines {
        harness.stdio.println(line)
      }
      harness.stdio.println(summary_line(out.total, out.files))
    }
  }
}