use super::*;
use harn_lexer::{Token, TokenKind};
use harn_parser::{peel_attributes, BindingPattern, Node, SNode};
struct PublicDeclaration<'a> {
kind: &'static str,
name: &'a str,
inner: &'a SNode,
strip_body: bool,
}
pub(crate) fn extract_api_symbols(source: &str) -> Vec<PackageApiSymbol> {
let mut lexer = harn_lexer::Lexer::new(source);
let Ok(tokens) = lexer.tokenize_with_comments() else {
return Vec::new();
};
let parser_tokens = tokens
.iter()
.filter(|token| !is_comment(&token.kind))
.cloned()
.collect::<Vec<_>>();
let mut parser = harn_parser::Parser::new(parser_tokens.clone());
let Ok(program) = parser.parse() else {
return Vec::new();
};
program
.iter()
.filter_map(|node| {
let declaration = public_declaration(node)?;
let pub_token = parser_tokens.iter().rev().find(|token| {
matches!(&token.kind, TokenKind::Pub)
&& token.span.end <= declaration.inner.span.start
})?;
let signature_end = if declaration.strip_body {
outer_body_start(&parser_tokens, declaration.inner)?
} else {
declaration.inner.span.end
};
let raw_signature = source
.get(pub_token.span.start..signature_end)?
.trim()
.replace("\r\n", "\n");
let signature_source = if declaration.strip_body {
format!("{raw_signature} {{}}\n")
} else {
format!("{raw_signature}\n")
};
let signature = harn_fmt::format_source(&signature_source)
.unwrap_or(signature_source)
.trim()
.to_string();
let pub_index = tokens
.iter()
.position(|token| token.span.start == pub_token.span.start)?;
let docs_index = if matches!(&node.node, Node::AttributedDecl { .. }) {
tokens
.iter()
.position(|token| token.span.start == node.span.start)
.unwrap_or(pub_index)
} else {
pub_index
};
Some(PackageApiSymbol {
kind: declaration.kind.to_string(),
name: declaration.name.to_string(),
signature,
docs: docs_before(&tokens, docs_index),
})
})
.collect()
}
pub(crate) fn extract_api_symbols_for_module(
path: &Path,
graph: &harn_modules::ModuleGraph,
) -> Vec<PackageApiSymbol> {
let source = harn_modules::read_module_source(path).unwrap_or_default();
let mut symbols = extract_api_symbols(&source);
let local_names = symbols
.iter()
.map(|symbol| symbol.name.clone())
.collect::<HashSet<_>>();
let mut origin_symbols = HashMap::<PathBuf, Vec<PackageApiSymbol>>::new();
for name in graph.exports_for_module(path) {
if local_names.contains(&name) {
continue;
}
let Some(definition) = graph.export_definition_of(path, &name) else {
continue;
};
let candidates = origin_symbols
.entry(definition.file)
.or_insert_with_key(|file| {
harn_modules::read_module_source(file)
.map(|source| extract_api_symbols(&source))
.unwrap_or_default()
});
if let Some(symbol) = candidates.iter().find(|symbol| symbol.name == name) {
symbols.push(symbol.clone());
}
}
symbols
}
fn public_declaration<'a>(node: &'a SNode) -> Option<PublicDeclaration<'a>> {
let (_, inner) = peel_attributes(node);
let (kind, name, is_pub, strip_body) = match &inner.node {
Node::FnDecl { name, is_pub, .. } => ("fn", name.as_str(), *is_pub, true),
Node::Pipeline { name, is_pub, .. } => ("pipeline", name.as_str(), *is_pub, true),
Node::ToolDecl { name, is_pub, .. } => ("tool", name.as_str(), *is_pub, true),
Node::SkillDecl { name, is_pub, .. } => ("skill", name.as_str(), *is_pub, false),
Node::StructDecl { name, is_pub, .. } => ("struct", name.as_str(), *is_pub, false),
Node::EnumDecl { name, is_pub, .. } => ("enum", name.as_str(), *is_pub, false),
Node::TypeDecl { name, is_pub, .. } => ("type", name.as_str(), *is_pub, false),
Node::ConstBinding {
pattern: BindingPattern::Identifier(name),
is_pub,
..
} => ("const", name.as_str(), *is_pub, false),
_ => return None,
};
is_pub.then_some(PublicDeclaration {
kind,
name,
inner,
strip_body,
})
}
fn outer_body_start(tokens: &[Token], declaration: &SNode) -> Option<usize> {
let mut depth = 0usize;
for token in tokens.iter().rev().filter(|token| {
token.span.start >= declaration.span.start && token.span.end <= declaration.span.end
}) {
match &token.kind {
TokenKind::RBrace => depth += 1,
TokenKind::LBrace => {
depth = depth.checked_sub(1)?;
if depth == 0 {
return Some(token.span.start);
}
}
_ => {}
}
}
None
}
fn is_comment(kind: &TokenKind) -> bool {
matches!(
kind,
TokenKind::LineComment { .. } | TokenKind::BlockComment { .. }
)
}
fn docs_before(tokens: &[Token], declaration_index: usize) -> Option<String> {
let mut docs = Vec::new();
for token in tokens[..declaration_index].iter().rev() {
match &token.kind {
TokenKind::Newline => continue,
TokenKind::LineComment { text, is_doc } if *is_doc => {
let text = text.trim();
if !text.is_empty() {
docs.push(text.to_string());
}
}
TokenKind::BlockComment { text, is_doc } if *is_doc => {
let text = text
.lines()
.map(|line| line.trim().strip_prefix('*').unwrap_or(line.trim()).trim())
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join("\n");
if !text.is_empty() {
docs.push(text);
}
}
_ => break,
}
}
(!docs.is_empty()).then(|| {
docs.reverse();
docs.join("\n")
})
}