use std::collections::{BTreeSet, HashSet};
use std::ops::Range;
use std::path::{Component, Path, PathBuf};
use std::sync::Arc;
use harn_vm::process_sandbox::FsAccess;
use harn_vm::VmValue;
use sha2::{Digest, Sha256};
use tree_sitter::Node;
use crate::ast::{api as ast_api, Language};
use crate::error::HostlibError;
use crate::tools::args::{build_dict, str_value};
use crate::tools::permissions::enforce_path_scope;
use super::state::IndexState;
use super::symbol_graph::{EdgeKind, Node as GraphNode, NodeId, NodeKind, SymbolGraph};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Scope {
File,
Module,
Workspace,
}
impl Scope {
pub(super) fn parse(builtin: &'static str, raw: &str) -> Result<Self, HostlibError> {
match raw {
"file" => Ok(Self::File),
"module" => Ok(Self::Module),
"workspace" => Ok(Self::Workspace),
other => Err(HostlibError::InvalidParameter {
builtin,
param: "scope",
message: format!(
"expected one of \"file\" | \"module\" | \"workspace\", got `{other}`"
),
}),
}
}
pub(super) fn as_str(self) -> &'static str {
match self {
Self::File => "file",
Self::Module => "module",
Self::Workspace => "workspace",
}
}
}
pub(super) type SeedCandidate = (String, u32, &'static str);
pub(super) enum SeedLookup {
One(NodeId),
None,
Many(Vec<SeedCandidate>),
}
pub(super) fn resolve_seed(
graph: &SymbolGraph,
relative_path: &str,
name: &str,
line: Option<u32>,
kind: Option<NodeKind>,
) -> SeedLookup {
let candidates: Vec<&GraphNode> = graph
.nodes_named(name)
.iter()
.filter_map(|id| graph.node(*id))
.filter(|node| is_refactor_declaration(node))
.collect();
let mut narrowed: Vec<&GraphNode> = candidates
.iter()
.copied()
.filter(|node| paths_match(&node.path, relative_path))
.collect();
if narrowed.is_empty() {
narrowed = candidates;
}
if let Some(line) = line {
narrowed.retain(|node| node.line == line);
}
if let Some(kind) = kind {
narrowed.retain(|node| node.kind == kind);
}
match narrowed.len() {
0 => SeedLookup::None,
1 => SeedLookup::One(narrowed[0].id),
_ => SeedLookup::Many(
narrowed
.iter()
.map(|n| (n.path.clone(), n.line, n.kind.as_str()))
.collect(),
),
}
}
pub(super) fn is_refactor_declaration(node: &GraphNode) -> bool {
if node.kind == NodeKind::Module
&& (node.signature.strip_prefix("module ") == Some(node.path.as_str())
|| (node.language == "harn" && node.signature.starts_with("impl ")))
{
return false;
}
matches!(
node.kind,
NodeKind::Function
| NodeKind::Type
| NodeKind::Field
| NodeKind::EnumCase
| NodeKind::Module
)
}
pub(super) fn competing_declarations(
graph: &SymbolGraph,
seed: NodeId,
name: &str,
in_scope_files: &[String],
) -> Vec<SeedCandidate> {
graph
.nodes_named(name)
.iter()
.filter(|id| **id != seed)
.filter_map(|id| graph.node(*id))
.filter(|node| is_refactor_declaration(node) && in_scope_files.contains(&node.path))
.map(|node| (node.path.clone(), node.line, node.kind.as_str()))
.collect()
}
fn paths_match(a: &str, b: &str) -> bool {
a == b
|| a.replace('\\', "/") == b.replace('\\', "/")
|| a.ends_with(&format!("/{b}"))
|| b.ends_with(&format!("/{a}"))
}
pub(super) fn files_in_scope(
state: &IndexState,
scope: Scope,
name: &str,
seed_path: &str,
session_id: Option<&str>,
) -> Vec<String> {
let mut seen: BTreeSet<String> = BTreeSet::new();
seen.insert(seed_path.to_string());
if scope == Scope::Workspace {
for id in state.symbols.nodes_named(name) {
if let Some(node) = state.symbols.node(*id) {
seen.insert(node.path.clone());
}
}
for file in state.files.values() {
let mentions = match contained_path(
"hostlib_code_index",
&state.root,
&file.relative_path,
FsAccess::Read,
) {
Ok(abs) => file_contains_word(&abs, name, session_id),
Err(_) => {
!super::words::records(name)
|| state.words.get(name).iter().any(|hit| hit.file == file.id)
}
};
if mentions {
seen.insert(file.relative_path.clone());
}
}
}
seen.into_iter().collect()
}
fn file_contains_word(path: &Path, name: &str, session_id: Option<&str>) -> bool {
let bytes = match crate::fs::read(path, session_id) {
Some(Ok(bytes)) => bytes,
Some(Err(_)) => return false,
None => match std::fs::read(path) {
Ok(bytes) => bytes,
Err(_) => return false,
},
};
let Ok(text) = std::str::from_utf8(&bytes) else {
return false;
};
text.split(|c: char| !(c.is_alphanumeric() || c == '_'))
.any(|tok| tok == name)
}
pub(super) fn parse_kind(builtin: &'static str, raw: &str) -> Result<NodeKind, HostlibError> {
match raw {
"Function" => Ok(NodeKind::Function),
"Type" => Ok(NodeKind::Type),
"Field" => Ok(NodeKind::Field),
"EnumCase" => Ok(NodeKind::EnumCase),
"Module" => Ok(NodeKind::Module),
other => Err(HostlibError::InvalidParameter {
builtin,
param: "symbol_ref.kind",
message: format!(
"expected one of [Function, Type, Field, EnumCase, Module], got `{other}`"
),
}),
}
}
pub(super) fn is_identifier_token(text: &str) -> bool {
let mut chars = text.chars();
match chars.next() {
Some(c) if c.is_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_alphanumeric() || c == '_')
}
pub(super) fn is_skip_kind(kind: &str) -> bool {
matches!(
kind,
"comment"
| "line_comment"
| "block_comment"
| "doc_comment"
| "hash_bang_line"
| "shebang"
| "string"
| "string_literal"
| "string_fragment"
| "string_content"
| "raw_string_literal"
| "interpreted_string_literal"
| "interpreted_string"
| "char_literal"
| "character_literal"
| "template_string"
)
}
pub(super) fn is_interpolation_kind(kind: &str) -> bool {
matches!(kind, "interpolation" | "template_substitution")
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct IdentifierSpan {
pub start_byte: usize,
pub end_byte: usize,
pub start_row: usize,
pub start_col: usize,
pub end_row: usize,
pub end_col: usize,
}
impl IdentifierSpan {
fn of(node: Node<'_>) -> Self {
let start = node.start_position();
let end = node.end_position();
Self {
start_byte: node.start_byte(),
end_byte: node.end_byte(),
start_row: start.row,
start_col: start.column,
end_row: end.row,
end_col: end.column,
}
}
}
#[derive(Clone, Debug)]
pub(super) struct ShadowSite {
pub path: String,
pub row: usize,
pub col: usize,
}
fn for_each_identifier<'tree>(
root: Node<'tree>,
bytes: &[u8],
identifier_kinds: &[&str],
mut visit: impl FnMut(Node<'tree>, &str),
) {
let mut stack = vec![root];
while let Some(node) = stack.pop() {
if is_skip_kind(node.kind()) {
let mut cursor = node.walk();
stack.extend(
node.children(&mut cursor)
.filter(|child| is_interpolation_kind(child.kind())),
);
continue;
}
if identifier_kinds.contains(&node.kind()) {
if let Ok(text) = std::str::from_utf8(&bytes[node.start_byte()..node.end_byte()]) {
visit(node, text);
}
continue;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
stack.push(child);
}
}
}
pub(super) fn collect_identifier_spans(
root: Node<'_>,
bytes: &[u8],
target_name: &str,
shadow_name: &str,
identifier_kinds: &[&str],
path: &str,
targets: &mut Vec<IdentifierSpan>,
shadows: &mut Vec<ShadowSite>,
) {
for_each_identifier(root, bytes, identifier_kinds, |node, text| {
if text == target_name {
targets.push(IdentifierSpan::of(node));
} else if text == shadow_name {
let pos = node.start_position();
shadows.push(ShadowSite {
path: path.to_string(),
row: pos.row,
col: pos.column,
});
}
});
}
#[derive(Clone, Debug)]
pub(super) struct EditSpan {
pub span: IdentifierSpan,
pub before: String,
pub after: String,
}
pub(super) struct FilePlan {
pub path: String,
pub language: Language,
pub source: String,
pub patched: String,
pub edits: Vec<EditSpan>,
}
pub(super) fn plan_file(
path: String,
language: Language,
source: String,
edits: Vec<EditSpan>,
validate: bool,
) -> Result<FilePlan, String> {
let patched = splice(&source, &edits);
if validate {
if let Some(detail) = first_syntax_error(&patched, language) {
return Err(detail);
}
}
Ok(FilePlan {
path,
language,
source,
patched,
edits,
})
}
pub(super) fn splice(source: &str, edits: &[EditSpan]) -> String {
let mut ordered: Vec<&EditSpan> = edits.iter().collect();
ordered.sort_by_key(|e| std::cmp::Reverse(e.span.start_byte));
let mut out = source.to_string();
for edit in ordered {
out.replace_range(edit.span.start_byte..edit.span.end_byte, &edit.after);
}
out
}
pub(super) fn first_syntax_error(source: &str, language: Language) -> Option<String> {
let tree = ast_api::parse_tree(source, language).ok()?;
let root = tree.root_node();
if !root.has_error() {
return match language {
Language::Python => python_layout_error(root, source)
.or_else(|| python_continuation_error(root, source)),
_ => None,
};
}
let mut stack = vec![root];
while let Some(node) = stack.pop() {
if node.is_missing() {
let pos = node.start_position();
return Some(format!(
"missing `{}` at line {}, column {}",
node.kind(),
pos.row + 1,
pos.column + 1
));
}
if node.is_error() {
let pos = node.start_position();
return Some(format!(
"unexpected token at line {}, column {}",
pos.row + 1,
pos.column + 1
));
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.has_error() || child.is_missing() {
stack.push(child);
}
}
}
Some("post-edit source has parse errors".into())
}
fn python_continuation_error(root: Node<'_>, source: &str) -> Option<String> {
let mut stack = vec![root];
while let Some(node) = stack.pop() {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
stack.push(child);
let statement = match node.kind() {
"module" | "block" => !matches!(child.kind(), "decorated_definition" | "comment"),
"decorated_definition" => child.kind() != "decorator",
_ => child.kind().ends_with("_clause") && has_block_child(child),
};
if !statement {
continue;
}
let mut tokens = Vec::new();
logical_line_tokens(child, &mut tokens);
let mut depth = 0usize;
let mut previous: Option<Node<'_>> = None;
for token in tokens {
if token.kind() == "comment" && depth == 0 {
break;
}
if let Some(prev) = previous {
let gap = source
.get(prev.end_byte()..token.start_byte())
.unwrap_or("");
if depth == 0 && gap.contains('\n') && !gap.trim_start().starts_with('\\') {
let line = prev.end_position().row + 1;
return Some(format!(
"line {line} ends inside a statement without brackets or `\\`"
));
}
}
match token.kind() {
"(" | "[" | "{" => depth += 1,
")" | "]" | "}" => depth = depth.saturating_sub(1),
_ => {}
}
previous = Some(token);
}
}
}
None
}
fn has_block_child(node: Node<'_>) -> bool {
let mut cursor = node.walk();
let found = node
.named_children(&mut cursor)
.any(|c| c.kind() == "block");
found
}
fn logical_line_tokens<'t>(node: Node<'t>, out: &mut Vec<Node<'t>>) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"block" => return,
"string" | "concatenated_string" => out.push(child),
_ if child.child_count() == 0 => out.push(child),
_ => logical_line_tokens(child, out),
}
}
}
fn python_layout_error(root: Node<'_>, source: &str) -> Option<String> {
let bytes = source.as_bytes();
let starts_line = |node: Node<'_>| {
let start = node.start_byte();
let line_start = bytes[..start]
.iter()
.rposition(|b| *b == b'\n')
.map_or(0, |nl| nl + 1);
bytes[line_start..start]
.iter()
.all(|b| *b == b' ' || *b == b'\t')
};
let mut stack = vec![root];
while let Some(node) = stack.pop() {
let mut cursor = node.walk();
let children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
if matches!(node.kind(), "module" | "block") {
let floor = match node.kind() {
"module" => None,
_ => node.parent().map(|header| header.start_position().column),
};
let mut column = (node.kind() == "module").then_some(0);
for child in children
.iter()
.filter(|c| c.kind() != "comment" && starts_line(**c))
{
let at = child.start_position();
let expected = *column.get_or_insert(at.column);
if at.column != expected || floor.is_some_and(|f| at.column <= f) {
return Some(format!(
"inconsistent indentation at line {}, column {}",
at.row + 1,
at.column + 1
));
}
}
}
stack.extend(children);
}
None
}
pub(super) fn contained_path(
builtin: &'static str,
root: &Path,
rel: &str,
access: FsAccess,
) -> Result<PathBuf, HostlibError> {
let path = root.join(rel);
if !resolves_inside(root, rel) {
return Err(HostlibError::SandboxViolation {
builtin,
path: path.display().to_string(),
message: format!(
"`{rel}` resolves outside the indexed workspace `{}`; nothing was read or written",
root.display()
),
});
}
enforce_path_scope(builtin, &path, access)?;
Ok(path)
}
pub(super) fn resolves_inside(root: &Path, rel: &str) -> bool {
let lexical = Path::new(rel)
.components()
.all(|component| matches!(component, Component::Normal(_) | Component::CurDir));
if !lexical {
return false;
}
let Ok(canonical_root) = root.canonicalize() else {
return false;
};
let mut probe = root.join(rel);
loop {
if std::fs::symlink_metadata(&probe).is_ok() {
return probe
.canonicalize()
.is_ok_and(|resolved| resolved.starts_with(&canonical_root));
}
if !probe.pop() {
return false;
}
}
}
pub(super) fn read_source(
builtin: &'static str,
root: &Path,
rel: &str,
session_id: Option<&str>,
) -> Result<String, HostlibError> {
let path = contained_path(builtin, root, rel, FsAccess::Read)?;
let path = path.as_path();
let bytes = if let Some(result) = crate::fs::read(path, session_id) {
result.map_err(|err| HostlibError::Backend {
builtin,
message: format!("read `{}`: {err}", path.display()),
})?
} else {
std::fs::read(path).map_err(|err| HostlibError::Backend {
builtin,
message: format!("read `{}`: {err}", path.display()),
})?
};
Ok(String::from_utf8_lossy(&bytes).into_owned())
}
fn write_source(
builtin: &'static str,
path: &Path,
contents: &str,
session_id: Option<&str>,
) -> Result<(), String> {
match crate::fs::stage_write_or_none(builtin, path, contents.as_bytes(), true, true, session_id)
{
Ok(Some(_)) => return Ok(()),
Ok(None) => {}
Err(err) => return Err(err.to_string()),
}
crate::fs_snapshot::auto_capture_for_write(builtin, path);
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent)
.map_err(|err| format!("mkdir `{}`: {err}", parent.display()))?;
}
}
std::fs::write(path, contents).map_err(|err| format!("write `{}`: {err}", path.display()))?;
crate::fs_snapshot::record_completed_write(path);
Ok(())
}
pub(super) fn write_plans(
builtin: &'static str,
root: &Path,
plans: &[FilePlan],
session_id: Option<&str>,
) -> Result<Vec<(String, String)>, HostlibError> {
let targets = plans
.iter()
.map(|plan| contained_path(builtin, root, &plan.path, FsAccess::Write))
.collect::<Result<Vec<_>, _>>()?;
let mut failed = Vec::new();
for (plan, abs) in plans.iter().zip(&targets) {
if let Err(err) = write_source(builtin, abs, &plan.patched, session_id) {
failed.push((plan.path.clone(), err));
}
}
Ok(failed)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum ReferenceKind {
Call,
QualifiedCall,
MethodCall,
Import,
TypeReference,
ValueReference,
}
impl ReferenceKind {
#[cfg_attr(not(test), allow(dead_code))]
pub(super) fn as_str(self) -> &'static str {
match self {
Self::Call => "call",
Self::QualifiedCall => "qualified_call",
Self::MethodCall => "method_call",
Self::Import => "import",
Self::TypeReference => "type_reference",
Self::ValueReference => "value_reference",
}
}
}
#[cfg_attr(not(test), allow(dead_code))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct ReferenceSite {
pub path: String,
pub language: Language,
pub kind: ReferenceKind,
pub span: IdentifierSpan,
pub qualifier: Option<String>,
pub enclosing: Range<usize>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct SkippedFile {
pub path: String,
pub reason: String,
}
#[derive(Clone, Debug, Default)]
pub(super) struct ReferenceSites {
pub sites: Vec<ReferenceSite>,
pub skipped: Vec<SkippedFile>,
}
pub(super) fn reference_sites(
builtin: &'static str,
state: &IndexState,
seed: NodeId,
session_id: Option<&str>,
) -> Result<ReferenceSites, HostlibError> {
let Some(seed_node) = state.symbols.node(seed) else {
return Ok(ReferenceSites::default());
};
let name = seed_node.name.as_str();
let mut files: BTreeSet<String> =
files_in_scope(state, Scope::Workspace, name, &seed_node.path, session_id)
.into_iter()
.collect();
for edge in state.symbols.incoming(seed) {
if matches!(edge.kind, EdgeKind::Refs | EdgeKind::Calls) {
if let Some(from) = state.symbols.node(edge.from) {
files.insert(from.path.clone());
}
}
}
let mut out = ReferenceSites::default();
for path in files {
let Some(language) = Language::detect(Path::new(&path), None) else {
out.skipped.push(SkippedFile {
path,
reason: "no tree-sitter grammar".into(),
});
continue;
};
let Some(identifier_kinds) = language.rename_identifier_kinds() else {
out.skipped.push(SkippedFile {
path,
reason: format!("no identifier-kind table for `{}`", language.name()),
});
continue;
};
let source = read_source(builtin, &state.root, &path, session_id)?;
let tree = match ast_api::parse_tree(&source, language) {
Ok(tree) => tree,
Err(err) => {
out.skipped.push(SkippedFile {
path,
reason: format!("parse failed: {err}"),
});
continue;
}
};
let bytes = source.as_bytes();
let root = tree.root_node();
let import_bindings = import_bound_names(root, bytes, identifier_kinds);
let mut file_sites = Vec::new();
for_each_identifier(root, bytes, identifier_kinds, |node, text| {
if text != name {
return;
}
if let Some((kind, qualifier, enclosing)) = classify(node, bytes, &import_bindings) {
file_sites.push(ReferenceSite {
path: path.clone(),
language,
kind,
span: IdentifierSpan::of(node),
qualifier,
enclosing,
});
}
});
file_sites.sort_by_key(|site| site.span.start_byte);
out.sites.extend(file_sites);
}
Ok(out)
}
const IMPORT_STATEMENT_KINDS: &[&str] = &[
"use_declaration",
"import_statement",
"import_from_statement",
"future_import_statement",
"import_declaration",
];
const CALL_KINDS: &[(&str, &str)] = &[
("call_expression", "function"),
("call", "function"),
("new_expression", "constructor"),
];
const MEMBER_KINDS: &[(&str, &str, &str)] = &[
("field_expression", "value", "field"),
("attribute", "object", "attribute"),
("member_expression", "object", "property"),
("selector_expression", "operand", "field"),
];
const PATH_KINDS: &[&str] = &["scoped_identifier", "scoped_type_identifier"];
const DECLARATION_SUFFIXES: &[&str] = &[
"_item",
"_definition",
"_declaration",
"_declarator",
"_signature",
"_spec",
];
fn import_bound_names(root: Node<'_>, bytes: &[u8], identifier_kinds: &[&str]) -> HashSet<String> {
let mut names = HashSet::new();
let mut stack = vec![root];
while let Some(node) = stack.pop() {
if IMPORT_STATEMENT_KINDS.contains(&node.kind()) {
for_each_identifier(node, bytes, identifier_kinds, |_, text| {
names.insert(text.to_string());
});
continue;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
stack.push(child);
}
}
names
}
fn text_of<'a>(node: Node<'_>, bytes: &'a [u8]) -> &'a str {
std::str::from_utf8(&bytes[node.start_byte()..node.end_byte()]).unwrap_or("")
}
fn is_field_child(parent: Node<'_>, field: &str, node: Node<'_>) -> bool {
parent.child_by_field_name(field).map(|c| c.id()) == Some(node.id())
}
fn leftmost_leaf(mut node: Node<'_>) -> Node<'_> {
while let Some(child) = node.named_child(0) {
node = child;
}
node
}
fn classify(
node: Node<'_>,
bytes: &[u8],
import_bindings: &HashSet<String>,
) -> Option<(ReferenceKind, Option<String>, Range<usize>)> {
let mut ancestor = node.parent();
while let Some(current) = ancestor {
if IMPORT_STATEMENT_KINDS.contains(¤t.kind()) {
return Some((ReferenceKind::Import, None, current.byte_range()));
}
ancestor = current.parent();
}
let parent = node.parent()?;
if is_field_child(parent, "name", node)
&& DECLARATION_SUFFIXES
.iter()
.any(|suffix| parent.kind().ends_with(suffix))
{
return None;
}
let (callee, qualifier, receiver_imported) = if PATH_KINDS.contains(&parent.kind())
&& is_field_child(parent, "name", node)
{
let path = parent.child_by_field_name("path");
(parent, path.map(|p| text_of(p, bytes).to_string()), true)
} else if let Some((_, receiver_field, _)) = MEMBER_KINDS
.iter()
.find(|(kind, _, member)| parent.kind() == *kind && is_field_child(parent, member, node))
{
let receiver = parent.child_by_field_name(receiver_field);
let imported = receiver
.map(|r| import_bindings.contains(text_of(leftmost_leaf(r), bytes)))
.unwrap_or(false);
(
parent,
receiver.map(|r| text_of(r, bytes).to_string()),
imported,
)
} else {
(node, None, false)
};
let call = callee.parent().filter(|outer| {
CALL_KINDS
.iter()
.any(|(kind, field)| outer.kind() == *kind && is_field_child(*outer, field, callee))
});
let kind = match (call.is_some(), qualifier.is_some(), receiver_imported) {
(true, false, _) => ReferenceKind::Call,
(true, true, true) => ReferenceKind::QualifiedCall,
(true, true, false) => ReferenceKind::MethodCall,
(false, _, _) if node.kind() == "type_identifier" => ReferenceKind::TypeReference,
(false, _, _) => ReferenceKind::ValueReference,
};
let enclosing = call.unwrap_or(callee).byte_range();
Some((kind, qualifier, enclosing))
}
pub(super) struct EditSymbol<'a> {
pub name: &'a str,
pub new_name: Option<&'a str>,
pub path: &'a str,
pub line: Option<u32>,
pub kind: Option<NodeKind>,
}
#[derive(Default)]
pub(super) struct EditEnvelope {
pub applied: bool,
pub dry_run: bool,
pub touched_files: Vec<VmValue>,
pub conflicts: Vec<VmValue>,
pub warnings: Vec<VmValue>,
pub failed_paths: Vec<VmValue>,
pub match_count: usize,
pub details: String,
pub fallback_suggestion: Option<String>,
pub extra: Vec<(&'static str, VmValue)>,
}
pub(super) fn edit_envelope(
tag: &'static str,
scope: Scope,
symbol: &EditSymbol<'_>,
envelope: EditEnvelope,
) -> VmValue {
let list = |items: Vec<VmValue>| VmValue::List(Arc::new(items));
let mut symbol_entries = vec![("name", str_value(symbol.name))];
if let Some(new_name) = symbol.new_name {
symbol_entries.push(("new_name", str_value(new_name)));
}
symbol_entries.extend([
("path", str_value(symbol.path)),
(
"line",
symbol
.line
.map(|n| VmValue::Int(n as i64))
.unwrap_or(VmValue::Nil),
),
(
"kind",
symbol
.kind
.map(|k| str_value(k.as_str()))
.unwrap_or(VmValue::Nil),
),
]);
let mut entries: Vec<(&'static str, VmValue)> = vec![
("result", str_value(tag)),
("applied", VmValue::Bool(envelope.applied)),
("dry_run", VmValue::Bool(envelope.dry_run)),
("scope", str_value(scope.as_str())),
("symbol", build_dict(symbol_entries)),
("touched_files", list(envelope.touched_files)),
("conflicts", list(envelope.conflicts)),
("warnings", list(envelope.warnings)),
("failed_paths_with_reasons", list(envelope.failed_paths)),
("match_count", VmValue::Int(envelope.match_count as i64)),
("details", str_value(&envelope.details)),
];
if let Some(fallback) = envelope.fallback_suggestion {
entries.push(("fallback_suggestion", str_value(fallback)));
}
entries.extend(envelope.extra);
build_dict(entries)
}
pub(super) fn file_plan_value(plan: &FilePlan) -> VmValue {
let edits: Vec<VmValue> = plan
.edits
.iter()
.map(|edit| {
build_dict([
("start_byte", VmValue::Int(edit.span.start_byte as i64)),
("end_byte", VmValue::Int(edit.span.end_byte as i64)),
("start_row", VmValue::Int(edit.span.start_row as i64)),
("start_col", VmValue::Int(edit.span.start_col as i64)),
("end_row", VmValue::Int(edit.span.end_row as i64)),
("end_col", VmValue::Int(edit.span.end_col as i64)),
("before", str_value(&edit.before)),
("after", str_value(&edit.after)),
])
})
.collect();
build_dict([
("path", str_value(&plan.path)),
("language", str_value(plan.language.name())),
(
"before_sha256",
str_value(sha256_hex(plan.source.as_bytes())),
),
(
"after_sha256",
str_value(sha256_hex(plan.patched.as_bytes())),
),
("edits", VmValue::List(Arc::new(edits))),
])
}
pub(super) fn failed_paths_value(failed: &[(String, String)]) -> Vec<VmValue> {
failed
.iter()
.map(|(path, reason)| {
build_dict([("path", str_value(path)), ("reason", str_value(reason))])
})
.collect()
}
pub(super) fn candidates_value(candidates: &[SeedCandidate]) -> Vec<VmValue> {
candidates
.iter()
.map(|(path, line, kind)| {
build_dict([
("path", str_value(path)),
("line", VmValue::Int(*line as i64)),
("kind", str_value(*kind)),
])
})
.collect()
}
fn sha256_hex(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
hex::encode(hasher.finalize())
}
#[cfg(test)]
#[path = "refactor_core_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "containment_tests.rs"]
mod containment_tests;