Skip to main content

hearth_graph/
analyze.rs

1use std::path::Path;
2
3use compact_str::CompactString;
4use tree_sitter::Tree;
5
6use crate::imports::js;
7use crate::symbols::extract_symbols_from_tree;
8use crate::{ImportKind, ImportSpec, ParserPool, RawImport, Symbol};
9
10/// Symbols and imports extracted from one source file.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct FileAnalysis {
13    /// Repository-relative source path.
14    pub path: CompactString,
15    /// Content hash supplied by the caller.
16    pub content_hash: u64,
17    /// Resolved language name, or `None` when the path is unsupported.
18    pub language: Option<CompactString>,
19    /// Symbols in source order.
20    pub symbols: Vec<Symbol>,
21    /// Literal imports in source order.
22    pub imports: Vec<RawImport>,
23    /// Whether at least one dynamic or CommonJS import had a non-literal argument.
24    pub has_opaque_imports: bool,
25}
26
27#[derive(Clone, Copy)]
28enum ImportExtractor {
29    Query(fn(&str) -> ImportKind),
30    Custom(fn(&str, &Tree) -> Vec<RawImport>),
31}
32
33/// Parses and analyzes one source file.
34///
35/// Symbol and import extraction share the same syntax tree. Unsupported paths,
36/// parser failures, and sources whose byte offsets cannot fit in `u32` produce
37/// empty results.
38#[must_use]
39pub fn analyze_source(
40    source: &str,
41    path: &str,
42    content_hash: u64,
43    pool: &mut ParserPool<'_>,
44) -> FileAnalysis {
45    let mut analysis = FileAnalysis {
46        path: CompactString::from(path),
47        content_hash,
48        language: None,
49        symbols: Vec::new(),
50        imports: Vec::new(),
51        has_opaque_imports: false,
52    };
53
54    let Some(id) = pool.registry().for_path(Path::new(path)) else {
55        return analysis;
56    };
57    let Some(spec) = pool.registry().get(id) else {
58        return analysis;
59    };
60    analysis.language = Some(spec.name.clone());
61
62    if u32::try_from(source.len()).is_err() {
63        return analysis;
64    }
65
66    let import_extractor = spec.imports.as_ref().map(|imports| match imports {
67        ImportSpec::Query { kind_map, .. } => ImportExtractor::Query(*kind_map),
68        ImportSpec::Custom(extract) => ImportExtractor::Custom(*extract),
69    });
70
71    let tree = {
72        let Some(parser) = pool.parser(id) else {
73            return analysis;
74        };
75        let Some(tree) = parser.parse(source, None) else {
76            return analysis;
77        };
78        tree
79    };
80
81    analysis.symbols = extract_symbols_from_tree(source, &tree, id, pool);
82    match import_extractor {
83        Some(ImportExtractor::Query(kind_map)) => {
84            if let Some(query) = pool.imports_query(id) {
85                let extracted = js::extract(source, &tree, query, kind_map);
86                analysis.imports = extracted.imports;
87                analysis.has_opaque_imports = extracted.opaque_count != 0;
88            }
89        }
90        Some(ImportExtractor::Custom(extract)) => {
91            analysis.imports = extract(source, &tree);
92        }
93        None => {}
94    }
95
96    analysis
97}