harn-stdlib 0.8.65

Embedded Harn standard library source catalog
Documentation
import { unified_diff } from "std/diff"
import { rules_apply, rules_fold } from "std/rules"

/**
 * `harn codemod` — apply a codemod rule's `fix` across a fileset.
 *
 * **Dry-run by default**: prints a unified diff per file that would change.
 * `--apply` writes the fixes (gated by the deterministic-tools capability);
 * a rule above the machine-applicable safety tier additionally needs
 * `--allow-unsafe`.
 *
 * The Rust shim (crates/harn-cli/src/commands/codemod.rs) resolves the rule(s)
 * and the matching files and passes them as a plan; this handler runs the
 * engine (`std/rules` `rules_apply`) and formats the diffs / summary.
 *
 * Inputs:
 *   HARN_CODEMOD_PLAN_JSON          — JSON array of {rule, language, files: [...]}.
 *   HARN_CODEMOD_RECIPE             — Built-in recipe id, e.g. "destructure-defaults".
 *   HARN_CODEMOD_RECIPE_FILES_JSON  — JSON array of files for the built-in recipe.
 *   HARN_CODEMOD_APPLY              — "1" to write, "0" for a dry-run preview.
 *   HARN_CODEMOD_ALLOW_UNSAFE       — "1" to apply above machine-applicable safety.
 *   HARN_OUTPUT_JSON                — "1" for the JSON envelope, else human diffs.
 */
fn summary_line(changed: int, applied: int, apply: bool) -> string {
  if apply {
    return to_string(applied) + " file(s) rewritten (" + to_string(changed) + " changed)"
  }
  return to_string(changed) + " file(s) would change (dry run; pass --apply to write)"
}

fn run_codemod(plan: list, apply: bool, allow_unsafe: bool) -> dict {
  var file_results = []
  var changed = 0
  var applied = 0
  for entry in plan {
    if len(entry.files) == 0 {
      continue
    }
    let res = rules_apply({rule: entry.rule, paths: entry.files, dry_run: !apply, allow_unsafe: allow_unsafe})
    for f in res.files ?? [] {
      if f.changed {
        changed = changed + 1
        if f.applied {
          applied = applied + 1
        }
        file_results = file_results.push(f)
      }
    }
  }
  return {files: file_results, changed: changed, applied: applied}
}

fn run_builtin_recipe(name: string, files: list, apply: bool) -> dict {
  if name == "destructure-defaults" {
    let res = rules_fold({paths: files, dry_run: !apply})
    var file_results = []
    var changed = 0
    var applied = 0
    for f in res.files ?? [] {
      if f.changed {
        changed = changed + 1
        if f.applied {
          applied = applied + 1
        }
        file_results = file_results.push(f)
      }
    }
    return {files: file_results, changed: changed, applied: applied}
  }
  throw "unknown built-in codemod recipe: " + name
}

fn main(harness: Harness) {
  let apply = harness.env.get_or("HARN_CODEMOD_APPLY", "0") == "1"
  let allow_unsafe = harness.env.get_or("HARN_CODEMOD_ALLOW_UNSAFE", "0") == "1"
  let json_mode = harness.env.get_or("HARN_OUTPUT_JSON", "0") == "1"
  // `rules_apply` is a gated deterministic tool, so the capability is required
  // even for a dry run (which still never writes — the engine's own
  // `dry_run`/safety checks protect the files). `harn codemod` is a first-party
  // command the user invoked directly, so enabling it here is the intended use.
  hostlib_enable("tools:deterministic")
  let recipe = harness.env.get_or("HARN_CODEMOD_RECIPE", "")
  let out = if recipe {
    let files = json_parse(harness.env.get_or("HARN_CODEMOD_RECIPE_FILES_JSON", "[]"))
    run_builtin_recipe(recipe, files, apply)
  } else {
    let raw = harness.env.get_or("HARN_CODEMOD_PLAN_JSON", "")
    if raw == "" {
      harness.stdio.eprintln("codemod: internal error — no codemod plan or recipe set by the shim")
      exit(70)
    }
    run_codemod(json_parse(raw), apply, allow_unsafe)
  }
  if json_mode {
    let envelope = {
      schemaVersion: 1,
      mode: "codemod",
      apply: apply,
      files: out.files,
      summary: {changed: out.changed, applied: out.applied},
    }
    harness.stdio.println(json_stringify_pretty(envelope))
  } else {
    for f in out.files {
      if apply {
        harness.stdio.println("rewrote " + f.path + "  [safety=" + f.safety + "]")
      } else {
        harness.stdio
          .println(
          "would change "
            + f.path
            + "  [safety="
            + f.safety
            + ", idempotent="
            + to_string(f.idempotent)
            + "]",
        )
        harness.stdio.println(unified_diff(f.before ?? "", f.preview))
      }
    }
    harness.stdio.println(summary_line(out.changed, out.applied, apply))
  }
}