use std::collections::BTreeSet;
use tree_sitter::Node;
use super::elixir;
use super::intelligence::{
apply_comment_lines, comment_at, diagnostic_at, doc_comment_at, docstring_at, export_at, import_at,
is_comment_node, mark_comment_rows, resolve_structure_name, span_from_node, structure_kind_at, structure_signature,
symbol_at,
};
use super::types::*;
use super::walk::{Descend, walk_bounded, warn_if_truncated};
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct Wanted {
pub structure: bool,
pub imports: bool,
pub exports: bool,
pub comments: bool,
pub docstrings: bool,
pub symbols: bool,
pub diagnostics: bool,
}
impl Wanted {
#[allow(dead_code)]
pub(crate) fn all() -> Self {
Self {
structure: true,
imports: true,
exports: true,
comments: true,
docstrings: true,
symbols: true,
diagnostics: true,
}
}
}
pub(crate) fn extract_all(root: &Node<'_>, source: &str, language: &str, wanted: Wanted, out: &mut ProcessResult) {
let mut collector = Collector::new(source, language, wanted);
let truncated = walk_bounded(root, |node, depth| collector.visit(node, depth));
warn_if_truncated(truncated, "intel::extract", language);
collector.finish(out);
tracing::debug!(
target: "ts_pack::intel",
operation = "intel::extract",
language,
nodes = out.metrics.node_count,
max_depth = out.metrics.max_depth,
error_nodes = out.metrics.error_count,
structure = out.structure.len(),
imports = out.imports.len(),
exports = out.exports.len(),
comments = out.comments.len(),
docstrings = out.docstrings.len(),
symbols = out.symbols.len(),
diagnostics = out.diagnostics.len(),
"extraction complete"
);
}
struct Collector<'a> {
source: &'a str,
language: &'a str,
wanted: Wanted,
node_count: usize,
error_count: usize,
max_depth: usize,
comment_rows: BTreeSet<usize>,
comments: Vec<CommentInfo>,
docstrings: Vec<DocstringInfo>,
exports: Vec<ExportInfo>,
symbols: Vec<SymbolInfo>,
diagnostics: Vec<Diagnostic>,
imports: ImportScope,
structure: StructureScope,
}
impl<'a> Collector<'a> {
fn new(source: &'a str, language: &'a str, wanted: Wanted) -> Self {
Self {
source,
language,
wanted,
node_count: 0,
error_count: 0,
max_depth: 0,
comment_rows: BTreeSet::new(),
comments: Vec::new(),
docstrings: Vec::new(),
exports: Vec::new(),
symbols: Vec::new(),
diagnostics: Vec::new(),
imports: ImportScope::default(),
structure: StructureScope::default(),
}
}
fn visit(&mut self, node: &Node<'_>, depth: usize) -> Descend {
self.node_count += 1;
self.max_depth = self.max_depth.max(depth);
if node.is_error() || node.is_missing() {
self.error_count += 1;
}
if is_comment_node(node) {
mark_comment_rows(node, self.source, &mut self.comment_rows);
}
if self.wanted.comments
&& let Some(comment) = comment_at(node, self.source)
{
self.comments.push(comment);
}
if self.wanted.docstrings
&& let Some(docstring) = docstring_at(node, self.source, self.language)
{
self.docstrings.push(docstring);
}
if self.wanted.exports
&& let Some(export) = export_at(node, self.source, self.language)
{
self.exports.push(export);
}
if self.wanted.symbols
&& let Some(symbol) = symbol_at(node, self.source)
{
self.symbols.push(symbol);
}
if self.wanted.diagnostics
&& let Some(diagnostic) = diagnostic_at(node, self.source)
{
self.diagnostics.push(diagnostic);
}
if self.wanted.imports {
self.imports.visit(node, depth, self.source, self.language);
}
if self.wanted.structure {
self.structure.visit(node, depth, self.source, self.language);
}
Descend::Children
}
fn finish(self, out: &mut ProcessResult) {
out.metrics.node_count = self.node_count;
out.metrics.error_count = self.error_count;
out.metrics.max_depth = self.max_depth;
apply_comment_lines(&mut out.metrics, self.source, &self.comment_rows);
out.comments = self.comments;
out.docstrings = self.docstrings;
out.exports = self.exports;
out.symbols = self.symbols;
out.diagnostics = self.diagnostics;
out.imports = self.imports.found;
out.structure = self.structure.finish();
}
}
#[derive(Default)]
struct ImportScope {
found: Vec<ImportInfo>,
quote_depth: Option<usize>,
}
impl ImportScope {
fn visit(&mut self, node: &Node<'_>, depth: usize, source: &str, language: &str) {
if self.quote_depth.is_some_and(|quoted| depth <= quoted) {
self.quote_depth = None;
}
if self.quote_depth.is_some() {
return;
}
if language == "elixir" && node.kind() == "call" {
if elixir::is_quote_call(node, source) {
self.quote_depth = Some(depth);
} else if let Some(import) = elixir::import_directive(node, source) {
self.found.push(import);
}
return;
}
if let Some(import) = import_at(node, source, language) {
self.found.push(import);
}
}
}
struct OpenItem {
depth: usize,
body_id: Option<usize>,
body_depth: Option<usize>,
item: StructureItem,
children: Vec<StructureItem>,
}
#[derive(Default)]
struct StructureScope {
open: Vec<OpenItem>,
done: Vec<StructureItem>,
quote_depth: Option<usize>,
}
impl StructureScope {
fn visit(&mut self, node: &Node<'_>, depth: usize, source: &str, language: &str) {
self.leave(depth);
if self.quote_depth.is_some() {
return;
}
if !self.is_in_scope(node, depth) {
return;
}
self.try_open(node, depth, source, language);
}
fn leave(&mut self, depth: usize) {
if self.quote_depth.is_some_and(|quoted| depth <= quoted) {
self.quote_depth = None;
}
while self.open.last().is_some_and(|open| depth <= open.depth) {
let Some(mut open) = self.open.pop() else { break };
open.item.children = open.children;
match self.open.last_mut() {
Some(parent) => parent.children.push(open.item),
None => self.done.push(open.item),
}
}
if let Some(top) = self.open.last_mut()
&& top.body_depth.is_some_and(|body_depth| depth <= body_depth)
{
top.body_depth = None;
}
}
fn is_in_scope(&mut self, node: &Node<'_>, depth: usize) -> bool {
let Some(top) = self.open.last_mut() else {
return true;
};
if top.body_id == Some(node.id()) {
top.body_depth = Some(depth);
}
top.body_depth.is_some()
}
fn try_open(&mut self, node: &Node<'_>, depth: usize, source: &str, language: &str) {
if language == "elixir" {
if elixir::is_quote_call(node, source) {
self.quote_depth = Some(depth);
return;
}
if let Some(definition) = elixir::definition(node, source) {
let body = definition.body;
self.push(
node,
depth,
source,
definition.kind,
definition.name,
definition.visibility,
body,
);
return;
}
}
if let Some(kind) = structure_kind_at(node, language) {
let name = resolve_structure_name(node, source);
let body = node.child_by_field_name("body");
self.push(node, depth, source, kind, name, None, body);
}
}
#[allow(clippy::too_many_arguments)]
fn push(
&mut self,
node: &Node<'_>,
depth: usize,
source: &str,
kind: StructureKind,
name: Option<String>,
visibility: Option<String>,
body: Option<Node<'_>>,
) {
let signature = structure_signature(node, source, body.as_ref());
self.open.push(OpenItem {
depth,
body_id: body.as_ref().map(Node::id),
body_depth: None,
item: StructureItem {
kind,
name,
visibility,
span: span_from_node(node),
children: Vec::new(),
decorators: Vec::new(),
doc_comment: doc_comment_at(node, source),
signature,
body_span: body.as_ref().map(span_from_node),
},
children: Vec::new(),
});
}
fn finish(mut self) -> Vec<StructureItem> {
self.leave(0);
self.done
}
}