1use crate::model::{SourceLocation, StepDefinition};
4use crate::resource_limits::{read_utf8, MAX_PROJECT_INPUT_BYTES};
5use anyhow::{Context, Result};
6use std::any::{Any, TypeId};
7use std::collections::HashMap;
8use std::panic::{RefUnwindSafe, UnwindSafe};
9use std::path::{Path, PathBuf};
10
11pub(crate) const UNRESOLVED_REGISTRATION_DIAGNOSTIC_PREFIX: &str =
12 "unresolved step-registration calls:";
13
14#[non_exhaustive]
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum SourceLanguage {
18 JavaScript,
20 TypeScript,
22 Tsx,
24}
25
26pub(crate) fn grammar_for_language(language: SourceLanguage) -> tree_sitter::Language {
28 match language {
29 SourceLanguage::JavaScript => tree_sitter_javascript::LANGUAGE.into(),
30 SourceLanguage::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
31 SourceLanguage::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct SourceFile {
38 pub path: PathBuf,
40 pub language: SourceLanguage,
42}
43
44#[non_exhaustive]
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum ExtractionDiagnosticLevel {
48 Warning,
50 Error,
52}
53
54#[non_exhaustive]
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ExtractionDiagnostic {
58 pub level: ExtractionDiagnosticLevel,
60 pub location: SourceLocation,
62 pub message: String,
64}
65
66impl ExtractionDiagnostic {
67 pub fn new(
69 level: ExtractionDiagnosticLevel,
70 location: SourceLocation,
71 message: impl Into<String>,
72 ) -> Self {
73 Self {
74 level,
75 location,
76 message: message.into(),
77 }
78 }
79}
80
81pub(crate) fn is_completeness_diagnostic(diagnostic: &ExtractionDiagnostic) -> bool {
82 diagnostic
83 .message
84 .starts_with(UNRESOLVED_REGISTRATION_DIAGNOSTIC_PREFIX)
85}
86
87#[non_exhaustive]
89#[derive(Debug, Clone, Default, PartialEq)]
90pub struct Extraction {
91 pub definitions: Vec<StepDefinition>,
93 pub diagnostics: Vec<ExtractionDiagnostic>,
95}
96
97impl Extraction {
98 pub fn new(definitions: Vec<StepDefinition>, diagnostics: Vec<ExtractionDiagnostic>) -> Self {
100 Self {
101 definitions,
102 diagnostics,
103 }
104 }
105}
106
107#[derive(Default)]
112pub struct SourceExtractionSession {
113 root: Option<PathBuf>,
114 registrations: Vec<String>,
115 states: HashMap<TypeId, Box<dyn Any + Send + Sync + UnwindSafe + RefUnwindSafe>>,
116}
117
118pub(crate) trait AdapterSessionState:
121 Any + Send + Sync + UnwindSafe + RefUnwindSafe
122{
123 fn initialize(root: Option<&Path>, registrations: &[String]) -> Self;
124}
125
126pub(crate) trait StatefulSourceAdapter: SourceAdapter {
128 type State: AdapterSessionState;
129}
130
131impl SourceExtractionSession {
132 pub fn new(root: &Path) -> Self {
134 Self::with_registrations(root, &[])
135 }
136
137 pub fn with_registrations(root: &Path, registrations: &[String]) -> Self {
142 let mut session = Self {
143 root: Some(root.to_owned()),
144 registrations: registrations.to_vec(),
145 ..Self::default()
146 };
147 for registration in SOURCE_ADAPTER_REGISTRY {
149 if let Some(initialize) = registration.initialize_session {
150 initialize(&mut session);
151 }
152 }
153 session
154 }
155
156 pub(crate) fn initialize<T: AdapterSessionState>(&mut self) {
157 self.states
158 .entry(TypeId::of::<T>())
159 .or_insert_with(|| Box::new(T::initialize(self.root.as_deref(), &self.registrations)));
160 }
161
162 pub(crate) fn state<T: AdapterSessionState>(&mut self) -> Result<&mut T> {
163 if self.root.is_none() {
165 self.initialize::<T>();
166 }
167 self.states
168 .get_mut(&TypeId::of::<T>())
169 .and_then(|state| {
170 let state: &mut dyn Any = state.as_mut();
171 state.downcast_mut::<T>()
172 })
173 .with_context(|| {
174 format!(
175 "source adapter state {} is uninitialized or incompatible",
176 std::any::type_name::<T>()
177 )
178 })
179 }
180}
181
182pub trait SourceAdapter: Sync {
184 fn name(&self) -> &'static str;
186
187 fn language(&self) -> SourceLanguage;
189
190 fn extract(&self, source: &str, file: &SourceFile) -> Result<Extraction>;
192
193 fn extract_with_session(
195 &self,
196 source: &str,
197 file: &SourceFile,
198 _session: &mut SourceExtractionSession,
199 ) -> Result<Extraction> {
200 self.extract(source, file)
201 }
202
203 fn extract_file(&self, file: &SourceFile) -> Result<Extraction> {
205 let source = read_utf8(&file.path, "definition source", MAX_PROJECT_INPUT_BYTES)?;
206 self.extract(&source, file)
207 }
208
209 fn extract_file_with_session(
211 &self,
212 file: &SourceFile,
213 session: &mut SourceExtractionSession,
214 ) -> Result<Extraction> {
215 let source = read_utf8(&file.path, "definition source", MAX_PROJECT_INPUT_BYTES)?;
216 self.extract_with_session(&source, file, session)
217 }
218}
219
220#[non_exhaustive]
222#[derive(Clone, Copy)]
223pub struct SourceAdapterRegistration {
224 pub suffix: &'static str,
226 pub adapter: &'static dyn SourceAdapter,
228 initialize_session: Option<fn(&mut SourceExtractionSession)>,
229}
230
231impl SourceAdapterRegistration {
232 pub const fn new(suffix: &'static str, adapter: &'static dyn SourceAdapter) -> Self {
234 Self {
235 suffix,
236 adapter,
237 initialize_session: None,
238 }
239 }
240
241 const fn with_session<A: StatefulSourceAdapter>(
242 suffix: &'static str,
243 adapter: &'static A,
244 ) -> Self {
245 Self {
246 suffix,
247 adapter,
248 initialize_session: Some(SourceExtractionSession::initialize::<A::State>),
249 }
250 }
251}
252
253macro_rules! register_source_adapters {
258 ( $( $language:ident => $adapter:path : $($suffix:literal),+ );+ ; ) => {
259 pub static SOURCE_ADAPTER_REGISTRY: &[SourceAdapterRegistration] = &[
261 $(
262 $(
263 SourceAdapterRegistration::with_session($suffix, &$adapter),
264 )+
265 )+
266 ];
267
268 pub fn adapter_for_language(language: SourceLanguage) -> &'static dyn SourceAdapter {
270 match language {
271 $(SourceLanguage::$language => &$adapter,)+
272 }
273 }
274 };
275}
276
277register_source_adapters! {
278 JavaScript => crate::typescript::JAVASCRIPT_ADAPTER : ".mjs", ".cjs", ".jsx", ".js";
279 TypeScript => crate::typescript::TYPESCRIPT_ADAPTER : ".mts", ".cts", ".ts";
280 Tsx => crate::typescript::TSX_ADAPTER : ".tsx";
281}
282
283pub fn adapter_for_path(path: &Path) -> Option<&'static dyn SourceAdapter> {
285 let extension = path.extension()?.to_str()?;
286 if extension == "map" || is_declaration_file(path, extension) {
287 return None;
288 }
289 SOURCE_ADAPTER_REGISTRY
290 .iter()
291 .find(|registration| extension == registration.suffix.trim_start_matches('.'))
292 .map(|registration| registration.adapter)
293}
294
295fn is_declaration_file(path: &Path, extension: &str) -> bool {
296 matches!(extension, "ts" | "mts" | "cts")
297 && path.file_stem().is_some_and(|stem| {
298 stem == ".d"
299 || Path::new(stem)
300 .extension()
301 .is_some_and(|extension| extension == "d")
302 })
303}
304
305pub fn language_for_path(path: &Path) -> Option<SourceLanguage> {
307 adapter_for_path(path).map(SourceAdapter::language)
308}
309
310pub fn registered_suffixes() -> Vec<&'static str> {
312 SOURCE_ADAPTER_REGISTRY
313 .iter()
314 .map(|registration| registration.suffix)
315 .collect()
316}
317
318#[cfg(test)]
319mod tests;