1use std::path::Path;
2
3use compact_str::CompactString;
4use tree_sitter::Tree;
5
6use crate::imports::js;
7use crate::symbols::{MAX_SYMBOLS_PER_FILE, extract_symbols_from_tree};
8use crate::{ImportKind, ImportSpec, ParserPool, RawImport, Symbol};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct FileAnalysis {
13 pub path: CompactString,
15 pub content_hash: u64,
17 pub language: Option<CompactString>,
19 pub symbols: Vec<Symbol>,
21 pub imports: Vec<RawImport>,
23 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#[must_use]
41pub fn analyze_source(
42 source: &str,
43 path: &str,
44 content_hash: u64,
45 pool: &mut ParserPool<'_>,
46) -> FileAnalysis {
47 analyze(source, path, content_hash, pool, true)
48}
49
50pub(crate) fn extract_symbols_only(
51 source: &str,
52 path: &str,
53 pool: &mut ParserPool<'_>,
54) -> Vec<Symbol> {
55 analyze(source, path, 0, pool, false).symbols
56}
57
58fn analyze(
59 source: &str,
60 path: &str,
61 content_hash: u64,
62 pool: &mut ParserPool<'_>,
63 include_imports: bool,
64) -> FileAnalysis {
65 let mut analysis = FileAnalysis {
66 path: CompactString::from(path),
67 content_hash,
68 language: None,
69 symbols: Vec::new(),
70 imports: Vec::new(),
71 has_opaque_imports: false,
72 };
73
74 let Some(id) = pool.registry().for_path(Path::new(path)) else {
75 return analysis;
76 };
77 let Some(spec) = pool.registry().get(id) else {
78 return analysis;
79 };
80 analysis.language = Some(spec.name.clone());
81
82 if u32::try_from(source.len()).is_err() {
83 return analysis;
84 }
85
86 let has_direct_symbols = spec.tags_query.is_some();
87 let has_injections = spec.injections_query.is_some();
88 let import_extractor = include_imports
89 .then(|| spec.imports.as_ref().map(import_extractor))
90 .flatten();
91
92 let tree = {
93 let Some(parser) = pool.parser(id) else {
94 return analysis;
95 };
96 let Some(tree) = parser.parse(source, None) else {
97 return analysis;
98 };
99 tree
100 };
101
102 if has_direct_symbols {
103 analysis.symbols = extract_symbols_from_tree(source, &tree, id, pool);
104 }
105 if let Some(extractor) = import_extractor {
106 extract_imports(source, &tree, id, extractor, pool, &mut analysis);
107 }
108
109 if has_injections {
110 analyze_injections(source, &tree, id, include_imports, pool, &mut analysis);
111 }
112
113 analysis.symbols.sort_unstable_by(|a, b| {
114 a.def_start
115 .cmp(&b.def_start)
116 .then(b.def_end.cmp(&a.def_end))
117 .then(a.line.cmp(&b.line))
118 .then(a.name_start.cmp(&b.name_start))
119 });
120 analysis.symbols.truncate(MAX_SYMBOLS_PER_FILE);
121 analysis.imports.sort_by_key(|import| import.span.0);
122 analysis
123}
124
125fn import_extractor(imports: &ImportSpec) -> ImportExtractor {
126 match imports {
127 ImportSpec::Query { kind_map, .. } => ImportExtractor::Query(*kind_map),
128 ImportSpec::Custom(extract) => ImportExtractor::Custom(*extract),
129 }
130}
131
132fn extract_imports(
133 source: &str,
134 tree: &Tree,
135 id: crate::LanguageId,
136 extractor: ImportExtractor,
137 pool: &mut ParserPool<'_>,
138 analysis: &mut FileAnalysis,
139) {
140 match extractor {
141 ImportExtractor::Query(kind_map) => {
142 if let Some(query) = pool.imports_query(id) {
143 let extracted = js::extract(source, tree, query, kind_map);
144 analysis.imports.extend(extracted.imports);
145 analysis.has_opaque_imports |= extracted.opaque_count != 0;
146 }
147 }
148 ImportExtractor::Custom(extract) => {
149 analysis.imports.extend(extract(source, tree));
150 }
151 }
152}
153
154fn analyze_injections(
155 source: &str,
156 tree: &Tree,
157 parent_id: crate::LanguageId,
158 include_imports: bool,
159 pool: &mut ParserPool<'_>,
160 analysis: &mut FileAnalysis,
161) {
162 let injections = match pool.injections_query(parent_id) {
163 Some(query) => crate::injections::extract(tree, source, query),
164 None => return,
165 };
166 for injection in injections {
167 let Some(id) = pool.registry().for_name(&injection.language) else {
168 continue;
169 };
170 let Some(spec) = pool.registry().get(id) else {
171 continue;
172 };
173 let has_symbols = spec.tags_query.is_some();
174 let import_extractor = include_imports
175 .then(|| spec.imports.as_ref().map(import_extractor))
176 .flatten();
177 if !has_symbols && import_extractor.is_none() {
178 continue;
179 }
180
181 let injected_tree = {
184 let Some(parser) = pool.parser(id) else {
185 continue;
186 };
187 if parser
188 .set_included_ranges(std::slice::from_ref(&injection.range))
189 .is_err()
190 {
191 continue;
192 }
193 let tree = parser.parse(source, None);
194 if parser.set_included_ranges(&[]).is_err() {
195 continue;
196 }
197 let Some(tree) = tree else {
198 continue;
199 };
200 tree
201 };
202
203 if has_symbols {
204 analysis
205 .symbols
206 .extend(extract_symbols_from_tree(source, &injected_tree, id, pool));
207 }
208
209 if let Some(extractor) = import_extractor {
210 extract_imports(source, &injected_tree, id, extractor, pool, analysis);
211 }
212 }
213}