use std::collections::BTreeSet;
use thiserror::Error;
use crate::manifest::{levenshtein, Contract, ContractName, Manifest};
#[derive(Debug, Error)]
pub enum ReferenceError {
#[error("contract reference '{reference}' is ambiguous; use one of: {candidates}")]
Ambiguous {
reference: String,
candidates: String,
},
#[error("unknown contract '{reference}'; declared contracts: {candidates}")]
Unknown {
reference: String,
candidates: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContractSlice {
pub contract: ContractName,
pub symbols: Vec<String>,
}
pub fn resolve_reference<'m>(
manifest: &'m Manifest,
reference: &str,
) -> Result<&'m ContractName, ReferenceError> {
if let Some(exact) = manifest
.contracts
.iter()
.find(|contract| contract.name.as_str() == reference)
{
return Ok(&exact.name);
}
let matches: Vec<&Contract> = manifest
.contracts
.iter()
.filter(|contract| shorthand_matches(contract, reference))
.collect();
match matches.as_slice() {
[single] => Ok(&single.name),
[] => Err(ReferenceError::Unknown {
reference: reference.to_owned(),
candidates: nearest_contracts(manifest, reference),
}),
several => Err(ReferenceError::Ambiguous {
reference: reference.to_owned(),
candidates: several
.iter()
.map(|contract| contract.name.as_str())
.collect::<Vec<_>>()
.join(", "),
}),
}
}
fn shorthand_matches(contract: &Contract, reference: &str) -> bool {
contract.name.as_str().starts_with(reference)
|| contract.source == reference
|| contract.source.rsplit('/').next() == Some(reference)
}
fn nearest_contracts(manifest: &Manifest, reference: &str) -> String {
let mut scored: Vec<(usize, &str)> = manifest
.contracts
.iter()
.map(|contract| {
(
levenshtein(reference, contract.name.as_str()),
contract.name.as_str(),
)
})
.collect();
scored.sort_unstable();
scored
.into_iter()
.map(|(_, name)| name)
.collect::<Vec<_>>()
.join(", ")
}
#[must_use]
pub fn slices_for_write(
manifest: &Manifest,
path: &str,
content: &str,
contract_sources: &[(&str, &str)],
) -> Vec<ContractSlice> {
let Some(mapping) = manifest.mapping_for(path) else {
return Vec::new();
};
let referenced = identifiers(language_for_path(path), content);
mapping
.contracts
.iter()
.map(|name| ContractSlice {
contract: name.clone(),
symbols: touched_symbols(manifest, name, contract_sources, &referenced),
})
.collect()
}
fn touched_symbols(
manifest: &Manifest,
name: &ContractName,
contract_sources: &[(&str, &str)],
referenced: &BTreeSet<String>,
) -> Vec<String> {
let source_language = manifest
.contracts
.iter()
.find(|contract| contract.name == *name)
.and_then(|contract| language_for_path(&contract.source));
let Some(source_text) = contract_sources
.iter()
.find(|(source_name, _)| *source_name == name.as_str())
.map(|(_, text)| *text)
else {
return Vec::new();
};
exported_symbols(source_language, source_text)
.into_iter()
.filter(|symbol| referenced.contains(symbol))
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SourceLanguage {
TypeScript,
Python,
Rust,
Sql,
}
fn language_for_path(path: &str) -> Option<SourceLanguage> {
let extension = path.rsplit('.').next()?;
match extension {
"ts" | "tsx" | "mts" | "cts" => Some(SourceLanguage::TypeScript),
"py" => Some(SourceLanguage::Python),
"rs" => Some(SourceLanguage::Rust),
"sql" => Some(SourceLanguage::Sql),
_ => None,
}
}
fn grammar(language: SourceLanguage) -> tree_sitter::Language {
match language {
SourceLanguage::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
SourceLanguage::Python => tree_sitter_python::LANGUAGE.into(),
SourceLanguage::Rust => tree_sitter_rust::LANGUAGE.into(),
SourceLanguage::Sql => tree_sitter_sequel::LANGUAGE.into(),
}
}
fn parse(language: Option<SourceLanguage>, text: &str) -> Option<tree_sitter::Tree> {
let mut parser = tree_sitter::Parser::new();
parser.set_language(&grammar(language?)).ok()?;
parser.parse(text, None)
}
fn descendants(root: tree_sitter::Node<'_>) -> Vec<tree_sitter::Node<'_>> {
let mut nodes = vec![root];
let mut index = 0;
while index < nodes.len() {
let node = nodes[index];
let mut cursor = node.walk();
nodes.extend(node.children(&mut cursor));
index += 1;
}
nodes
}
const IDENTIFIER_KINDS: &[&str] = &[
"identifier",
"type_identifier",
"property_identifier",
"shorthand_property_identifier",
];
fn identifiers(language: Option<SourceLanguage>, text: &str) -> BTreeSet<String> {
let Some(tree) = parse(language, text) else {
return BTreeSet::new();
};
descendants(tree.root_node())
.into_iter()
.filter(|node| IDENTIFIER_KINDS.contains(&node.kind()))
.filter_map(|node| node.utf8_text(text.as_bytes()).ok())
.map(str::to_owned)
.collect()
}
const DECLARATION_KINDS: &[&str] = &[
"variable_declarator",
"function_declaration",
"class_declaration",
"interface_declaration",
"type_alias_declaration",
"enum_declaration",
"export_specifier",
"function_definition",
"class_definition",
"function_item",
"struct_item",
"enum_item",
"const_item",
"static_item",
"type_item",
];
fn exported_symbols(language: Option<SourceLanguage>, text: &str) -> Vec<String> {
let Some(tree) = parse(language, text) else {
return Vec::new();
};
let mut symbols = Vec::new();
for node in descendants(tree.root_node()) {
if !DECLARATION_KINDS.contains(&node.kind()) || !is_exported(language, node) {
continue;
}
let named = node
.child_by_field_name("name")
.and_then(|name| name.utf8_text(text.as_bytes()).ok());
if let Some(symbol) = named {
if !symbols.iter().any(|existing| existing == symbol) {
symbols.push(symbol.to_owned());
}
}
}
symbols
}
fn is_exported(language: Option<SourceLanguage>, node: tree_sitter::Node<'_>) -> bool {
match language {
Some(SourceLanguage::TypeScript) => {
let mut ancestor = node.parent();
while let Some(current) = ancestor {
if current.kind() == "export_statement" {
return true;
}
ancestor = current.parent();
}
false
}
Some(SourceLanguage::Python) => true,
Some(SourceLanguage::Rust) => node
.child(0)
.is_some_and(|first| first.kind() == "visibility_modifier"),
Some(SourceLanguage::Sql) | None => false,
}
}