1#![warn(missing_docs)]
25
26mod array_comments;
27mod cache;
28mod collect;
29mod commands;
30mod comments;
31mod components;
32mod config;
33mod custom_elements;
34mod declaration;
35mod error;
36mod finder;
37mod jsdoc;
38mod marko;
39mod mdx;
40mod members;
41mod model;
42mod semantic;
43mod sfc;
44mod sharing;
45mod signature_refs;
46mod style;
47mod template;
48mod tsrx;
49
50use skif_core::HashSet;
51use std::cell::RefCell;
52use std::path::Path;
53
54use oxc_allocator::Allocator;
55use oxc_ast_visit::Visit;
56use oxc_parser::{ParseOptions as OxcParseOptions, Parser};
57use oxc_span::SourceType;
58use skif_core::LineIndex;
59
60pub use array_comments::{
61 ConfigComments, ElementComment, PathSegment, PropertyComment, extract_array_comments, extract_config_comments,
62};
63pub use cache::{
64 CACHE_SCHEMA, CACHE_SCHEMA_DIGEST, CachedModule, MemoryModuleCache, ModuleCache, ModuleKey, ParseCoalescer,
65 cache_version, parse_cached,
66};
67pub use components::{
68 RegisteredComponent, camelize, capitalize, is_native_tag, registered_components, registered_name, resolved_names,
69};
70pub use config::{
71 ConfigValue, ExtractedConfig, FILE_DIR, ImportedCall, ModuleLoader, OWN_SPREAD, TEMPLATE_SUBSTITUTION,
72 UNKNOWN_SPREAD, command_in_call_dir, command_line_call, extract_command_calls, extract_config,
73 extract_config_object, extract_config_object_with, extract_constructor_options, extract_imported_calls,
74 extract_method_calls, extract_property_calls, extract_property_values, file_call_options, literal_command,
75 place_file_dir,
76};
77pub use declaration::{DECLARATION_SUFFIXES, is_declaration_file};
78pub use error::SyntaxError;
79pub use model::{
80 Export, ExportKind, Import, ImportKind, ImportedKind, ImportedName, Member, MockCall, ModuleInfo, ParseIssue,
81 ParseOptions, Registration, Removal, RemovalKind, coalesce_ranges, is_source_query,
82};
83pub use style::{is_stylesheet, parse_stylesheet};
84
85pub fn parse_module(path: &Path, source: &str, options: &ParseOptions) -> Result<ModuleInfo, SyntaxError> {
95 let lines = LineIndex::new(source);
96 let extension = path
97 .extension()
98 .and_then(|e| e.to_str())
99 .unwrap_or_default()
100 .to_ascii_lowercase();
101 match extension.as_str() {
102 "vue" | "svelte" | "astro" => parse_component(&extension, source, &lines, *options),
103 "mdx" => Ok(parse_mdx(source, &lines, *options)),
104 "tsrx" => Ok(parse_tsrx(source, &lines, *options)),
105 "marko" => Ok(parse_marko(source, &lines, *options)),
106 _ => {
107 let source_type = source_type_for(path).ok_or_else(|| unsupported(&extension))?;
108 Ok(parse_block(source, 0, source_type, &lines, *options, None))
109 }
110 }
111}
112
113fn source_type_for(path: &Path) -> Option<SourceType> {
115 let source_type = SourceType::from_path(path).ok()?;
116 Some(if source_type.is_javascript() {
117 source_type.with_jsx(true)
118 } else {
119 source_type
120 })
121}
122
123fn unsupported(extension: &str) -> SyntaxError {
124 SyntaxError::Unsupported {
125 extension: extension.to_string(),
126 }
127}
128
129const RETAINED_ARENA_BYTES: usize = 16 * 1024 * 1024;
132
133thread_local! {
134 static ARENA: RefCell<Allocator> = RefCell::new(Allocator::default());
136}
137
138pub(crate) fn with_arena<R>(f: impl FnOnce(&Allocator) -> R) -> R {
141 ARENA.with(|arena| {
142 let Ok(mut allocator) = arena.try_borrow_mut() else {
143 return f(&Allocator::default());
144 };
145 allocator.reset();
146 let result = f(&allocator);
147 if allocator.capacity() > RETAINED_ARENA_BYTES {
148 *allocator = Allocator::default();
149 }
150 result
151 })
152}
153
154fn parse_block(
156 src: &str,
157 base: u32,
158 source_type: SourceType,
159 lines: &LineIndex,
160 options: ParseOptions,
161 uses: Option<&mut members::Uses>,
162) -> ModuleInfo {
163 with_arena(|allocator| parse_block_in(allocator, src, base, source_type, lines, options, uses))
164}
165
166fn parse_block_in(
167 allocator: &Allocator,
168 src: &str,
169 base: u32,
170 source_type: SourceType,
171 lines: &LineIndex,
172 options: ParseOptions,
173 uses: Option<&mut members::Uses>,
174) -> ModuleInfo {
175 let parser_options = OxcParseOptions {
176 preserve_parens: false,
177 allow_return_outside_function: true,
178 ..OxcParseOptions::default()
179 };
180 let ret = Parser::new(allocator, src, source_type)
181 .with_options(parser_options)
182 .parse();
183 let parse_diagnostics: Vec<ParseIssue> = ret
184 .diagnostics
185 .errors()
186 .map(|diagnostic| {
187 let (offset, len) = diagnostic
188 .labels
189 .as_slice()
190 .first()
191 .map_or((0, 0), |l| (l.offset(), l.len()));
192 ParseIssue {
193 message: diagnostic.message.to_string(),
194 span: lines.span(
195 base.saturating_add(offset),
196 base.saturating_add(offset).saturating_add(len),
197 ),
198 }
199 })
200 .collect();
201 if ret.fatal_error {
202 return ModuleInfo {
203 parse_diagnostics,
204 fatal: true,
205 ..ModuleInfo::default()
206 };
207 }
208
209 let ctx = collect::Ctx { src, base, lines };
210 let mut info = ModuleInfo {
211 parse_diagnostics,
212 ..ModuleInfo::default()
213 };
214 let collected = collect::collect(&ctx, &ret.program, &mut info);
215 let mut finder = finder::Finder::new(&ctx, &mut info, &ret.program);
216 finder.visit_program(&ret.program);
217 let cjs_shorthand = finder.into_cjs_shorthand();
218 info.imports.sort_by_key(|i| i.span.start);
219 let analysis = semantic::Analysis::new(&ret.program);
221 if options.member_accesses {
222 let excluded: HashSet<(u32, u32)> = collected.reexport_spans.iter().copied().collect();
223 members::track(&ret.program, &analysis, &mut info, &excluded, base);
224 if let Some(uses) = uses {
225 *uses = members::uses(&ret.program, &analysis);
226 }
227 }
228 if options.in_file_references {
229 mark_in_file_references(
230 &ret.program,
231 &analysis,
232 &collected.export_clause_spans,
233 &cjs_shorthand,
234 &mut info,
235 );
236 signature_refs::mark(&ret.program, &mut info);
237 members::mark_local_reads(&ret.program, &analysis, &mut info);
238 }
239 info.directives = ret
240 .program
241 .directives
242 .iter()
243 .map(|d| d.directive.as_str().to_string())
244 .collect();
245 custom_elements::mark(&ret.program, &analysis, &mut info);
246 comments::collect(&ctx, &ret.program, &mut info);
247 info.commands = commands::child_process_commands(&ret.program, &info.imports);
248 if src.contains("resolveComponent") {
249 info.template_tags = components::resolved_components(&ret.program);
250 info.template_tags.sort();
251 info.template_tags.dedup();
252 }
253 info
254}
255
256fn parse_component(
263 extension: &str,
264 source: &str,
265 lines: &LineIndex,
266 options: ParseOptions,
267) -> Result<ModuleInfo, SyntaxError> {
268 let blocks = sfc::blocks(source, extension == "astro");
269 let mut parts = Vec::new();
270 let mut all_uses = Vec::new();
271 for block in blocks.scripts.iter().filter(|block| block.parse) {
272 let name = match (block.typescript, block.jsx) {
273 (true, true) => "block.tsx",
274 (true, false) => "block.ts",
275 (false, true) => "block.jsx",
276 (false, false) => "block.js",
277 };
278 let source_type = source_type_for(Path::new(name)).ok_or_else(|| unsupported(extension))?;
279 let base = u32::try_from(block.start).unwrap_or(u32::MAX);
280 let mut uses = members::Uses::default();
281 let mut part = parse_block(
282 &source[block.start..block.end],
283 base,
284 source_type,
285 lines,
286 options,
287 Some(&mut uses),
288 );
289 if extension == "svelte" && block.context == sfc::ScriptContext::Instance {
290 part.exports.clear();
293 }
294 parts.push(part);
295 all_uses.push(uses);
296 }
297 if options.member_accesses {
298 for (index, part) in parts.iter_mut().enumerate() {
299 let mut others = members::Uses::default();
300 for (other, uses) in all_uses.iter().enumerate() {
301 if other != index {
302 members::merge_uses(&mut others, uses);
303 }
304 }
305 members::credit(part, &others);
306 }
307 }
308 let mut info = ModuleInfo::default();
309 for part in parts {
310 info.merge(part);
311 }
312 add_style_and_source_imports(&mut info, &blocks, source, lines);
313 if options.member_accesses {
314 let flavor = match extension {
315 "vue" => template::Flavor::Vue,
316 "svelte" => template::Flavor::Svelte,
317 _ => template::Flavor::Astro,
318 };
319 let mut uses = members::Uses::default();
320 let mut tags = std::collections::BTreeSet::new();
321 for (start, end) in blocks.markup(source) {
322 tags.extend(template::scan(&source[start..end], flavor, &mut uses));
323 }
324 members::credit(&mut info, &uses);
325 if flavor == template::Flavor::Vue {
326 tags.extend(std::mem::take(&mut info.template_tags));
327 info.template_tags = tags.into_iter().collect();
328 }
329 }
330 Ok(info)
331}
332
333fn parse_mdx(source: &str, lines: &LineIndex, options: ParseOptions) -> ModuleInfo {
335 let extracted = mdx::extract(source);
336 let source_type = SourceType::jsx();
337 let mut info = parse_block(&extracted.esm, 0, source_type, lines, options, None);
338 if options.member_accesses {
339 let mut markup = members::Uses::default();
340 for (start, end) in extracted.markup {
341 let text = mdx::blank_inline_code(&source[start..end]);
343 template::scan(&text, template::Flavor::Astro, &mut markup);
344 }
345 members::credit(&mut info, &markup);
346 }
347 info
348}
349
350fn parse_tsrx(source: &str, lines: &LineIndex, options: ParseOptions) -> ModuleInfo {
353 let mut info = parse_block(&tsrx::extract(source), 0, SourceType::tsx(), lines, options, None);
354 for name in info.imports.iter_mut().flat_map(|import| &mut import.names) {
355 name.escapes = true;
356 }
357 info
358}
359
360fn parse_marko(source: &str, lines: &LineIndex, options: ParseOptions) -> ModuleInfo {
364 let blocks = sfc::Blocks {
365 styles: marko::styles(source),
366 ..sfc::Blocks::default()
367 };
368 let mut code = source.as_bytes().to_vec();
369 for style in &blocks.styles {
370 for byte in &mut code[style.start..style.end] {
371 if !matches!(*byte, b'\n' | b'\r') {
372 *byte = b' ';
373 }
374 }
375 }
376 let code = String::from_utf8(code).unwrap_or_default();
378 let mut info = parse_tsrx(&code, lines, options);
379 info.imports.retain(|import| !import.specifier.starts_with('<'));
380 add_style_and_source_imports(&mut info, &blocks, source, lines);
381 info
382}
383
384fn add_style_and_source_imports(info: &mut ModuleInfo, blocks: &sfc::Blocks, source: &str, lines: &LineIndex) {
386 for style in &blocks.styles {
387 let path = format!("block.{}", style.extension);
388 let base = u32::try_from(style.start).unwrap_or(u32::MAX);
389 for mut import in parse_stylesheet(Path::new(&path), &source[style.start..style.end]).imports {
390 import.span = lines.span(
391 base.saturating_add(import.span.start),
392 base.saturating_add(import.span.end),
393 );
394 info.imports.push(import);
395 }
396 }
397 for reference in &blocks.sources {
398 info.imports.push(Import {
399 specifier: reference.specifier.clone(),
400 kind: ImportKind::SideEffect,
401 names: Vec::new(),
402 call: None,
403 span: lines.span(
404 u32::try_from(reference.start).unwrap_or(u32::MAX),
405 u32::try_from(reference.end).unwrap_or(u32::MAX),
406 ),
407 });
408 }
409 info.imports.sort_by_key(|import| import.span.start);
410}
411
412fn mark_in_file_references(
417 program: &oxc_ast::ast::Program<'_>,
418 analysis: &semantic::Analysis<'_>,
419 clause_spans: &[oxc_span::Span],
420 cjs_shorthand: &[(String, oxc_span::Span)],
421 info: &mut ModuleInfo,
422) {
423 let cjs_names: HashSet<&str> = cjs_shorthand.iter().map(|(name, _)| name.as_str()).collect();
424 let binding = |export: &Export| -> Option<String> {
425 match export.kind {
426 ExportKind::Default => None,
427 ExportKind::ReExport | ExportKind::StarReExport => export.local_name.clone(),
429 _ if export.local_name.is_none() && export.from.is_none() => {
430 cjs_names.contains(export.name.as_str()).then(|| export.name.clone())
431 }
432 _ => export.local_name.clone(),
433 }
434 };
435 let bindings: Vec<Option<String>> = info.exports.iter().map(binding).collect();
436 let wanted: HashSet<&str> = bindings.iter().flatten().map(String::as_str).collect();
437 let excluded: Vec<oxc_span::Span> = clause_spans
438 .iter()
439 .copied()
440 .chain(cjs_shorthand.iter().map(|(_, span)| *span))
441 .collect();
442 let referenced = semantic::referenced_locals(program, analysis, &wanted, &excluded);
443 for (export, local) in info.exports.iter_mut().zip(&bindings) {
444 let used = local.as_deref().is_some_and(|l| referenced.contains(l));
445 export.referenced_in_file = used;
446 if used {
447 info.in_file_references.push(export.name.clone());
448 }
449 }
450 info.in_file_references.sort();
451 info.in_file_references.dedup();
452}
453
454pub(crate) fn js_string(value: oxc_str::JSStr<'_>) -> String {
457 value
458 .as_str()
459 .map_or_else(|| value.display().to_string(), str::to_owned)
460}
461
462#[cfg(test)]
463mod tests {
464 use super::{ParseOptions, Path, parse_module, with_arena};
465
466 #[test]
467 fn parses_in_a_reused_or_nested_arena_give_the_same_facts() {
468 let source =
469 "import { a } from 'a';\nimport * as ns from 'ns';\nexport const b = a.c + ns.d;\nexport { b as e };\n";
470 let parse = || parse_module(Path::new("x.ts"), source, &ParseOptions::default()).ok();
471 let first = parse();
472 let nested = with_arena(|outer| {
473 let held = outer.alloc_str("held");
474 let inner = parse();
475 assert_eq!(held, "held");
477 inner
478 });
479 let again = parse();
480 assert!(first.as_ref().is_some_and(|info| info.exports.len() == 2));
481 assert_eq!(nested, first);
482 assert_eq!(again, first);
483 }
484}