1pub mod classify;
4pub mod docs;
5pub mod first_pass;
6pub mod members;
7pub mod merge;
8pub mod resolve;
9pub mod scope;
10pub mod types;
11
12use std::path::{Path, PathBuf};
13
14use anyhow::{Context, Result};
15use oxc_allocator::Allocator;
16use oxc_parser::Parser;
17use oxc_span::SourceType;
18
19use crate::context::GlobalContext;
20use crate::ir::Module;
21use crate::parse::scope::ScopeId;
22
23pub fn parse_dts_files(
25 paths: &[impl AsRef<Path>],
26 lib_name: Option<&str>,
27) -> Result<(Module, GlobalContext)> {
28 let mut gctx = GlobalContext::new();
29 let mut input_type_ids = Vec::new();
30 let mut input_file_scopes = Vec::new();
31
32 let builtin = gctx.create_root_scope();
34 populate_builtin_scope(&mut gctx, builtin);
35
36 let mut parsed_files: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
38 let mut all_file_scopes = Vec::new();
40 let mut scope_dirs: std::collections::HashMap<ScopeId, PathBuf> =
42 std::collections::HashMap::new();
43
44 for path in paths {
45 let path = path.as_ref();
46 let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
47 parsed_files.insert(canonical);
48
49 let source = std::fs::read_to_string(path)
50 .with_context(|| format!("Failed to read {}", path.display()))?;
51
52 let file_scope = gctx.create_child_scope(builtin);
53 input_file_scopes.push(file_scope);
54 all_file_scopes.push(file_scope);
55 if let Some(parent) = path.parent() {
56 scope_dirs.insert(file_scope, parent.to_path_buf());
57 }
58 let type_ids = parse_single_file(&source, path, lib_name, &mut gctx, file_scope)?;
59 input_type_ids.extend(type_ids);
60 }
61
62 resolve_imports(
64 &mut gctx,
65 builtin,
66 &mut all_file_scopes,
67 &mut scope_dirs,
68 &mut parsed_files,
69 lib_name,
70 )?;
71
72 Ok((
73 Module {
74 types: input_type_ids,
75 lib_name: lib_name.map(|s| s.to_string()),
76 builtin_scope: builtin,
77 file_scopes: input_file_scopes,
78 },
79 gctx,
80 ))
81}
82
83pub fn parse_single_source(
85 source: &str,
86 lib_name: Option<&str>,
87) -> Result<(Module, GlobalContext)> {
88 let mut gctx = GlobalContext::new();
89 let builtin = gctx.create_root_scope();
90 populate_builtin_scope(&mut gctx, builtin);
91 let file_scope = gctx.create_child_scope(builtin);
92 let path = Path::new("<input>");
93 let type_ids = parse_single_file(source, path, lib_name, &mut gctx, file_scope)?;
94 Ok((
95 Module {
96 types: type_ids,
97 lib_name: lib_name.map(|s| s.to_string()),
98 builtin_scope: builtin,
99 file_scopes: vec![file_scope],
100 },
101 gctx,
102 ))
103}
104
105fn parse_single_file(
106 source: &str,
107 path: &Path,
108 lib_name: Option<&str>,
109 gctx: &mut GlobalContext,
110 file_scope: ScopeId,
111) -> Result<Vec<crate::context::TypeId>> {
112 let allocator = Allocator::default();
113 let source_type = SourceType::d_ts();
114
115 gctx.diagnostics.set_file(path, source);
116
117 let parser_return = Parser::new(&allocator, source, source_type).parse();
118
119 if !parser_return.errors.is_empty() {
120 for error in &parser_return.errors {
121 gctx.warn(format!("Parse error in {}: {}", path.display(), error));
122 }
123 }
124
125 let program = &parser_return.program;
126 let doc_comments = docs::DocComments::new(&program.comments, source);
127
128 let mut diag = std::mem::take(&mut gctx.diagnostics);
131 let registry = first_pass::collect_type_names(program, lib_name, &mut diag, gctx, file_scope);
132 gctx.diagnostics = diag;
133
134 gctx.info(format!(
135 "Collected {} type names from {}",
136 registry.types.len(),
137 path.display()
138 ));
139
140 let import_logs: Vec<String> = gctx
142 .pending_imports
143 .iter()
144 .filter(|imp| imp.scope == file_scope)
145 .map(|imp| {
146 format!(
147 "Import: {} from \"{}\" (pending)",
148 imp.local_name, imp.from_module
149 )
150 })
151 .collect();
152 for msg in import_logs {
153 gctx.info(msg);
154 }
155
156 let scopes_snapshot = gctx.scopes.clone();
159 let type_arena_snapshot = gctx.type_arena().to_vec();
160 let declarations = first_pass::populate_declarations(
161 program,
162 ®istry,
163 lib_name,
164 &doc_comments,
165 &mut gctx.diagnostics,
166 &scopes_snapshot,
167 &type_arena_snapshot,
168 file_scope,
169 );
170
171 let merged = merge_class_pairs(declarations);
173 let merged = merge_namespaces(merged);
174 let merged = dedup_function_overloads(merged);
175
176 let is_script = !first_pass::is_module(program);
178
179 let type_ids: Vec<crate::context::TypeId> = merged
180 .into_iter()
181 .map(|mut decl| {
182 if is_script {
184 decl.exported = true;
185 }
186 let name = declaration_name(&decl.kind);
187 let type_id = gctx.insert_type(decl);
188 if let Some(ref name) = name {
190 gctx.scopes.insert(file_scope, name.clone(), type_id);
191 }
192 if is_script {
195 if let Some(parent) = gctx.scopes.get(file_scope).parent {
196 if let Some(name) = name {
197 gctx.scopes.insert(parent, name, type_id);
198 }
199 }
200 }
201 type_id
202 })
203 .collect();
204
205 Ok(type_ids)
206}
207
208fn dedup_function_overloads(
212 declarations: Vec<crate::ir::TypeDeclaration>,
213) -> Vec<crate::ir::TypeDeclaration> {
214 use crate::ir::{ModuleContext, TypeDeclaration, TypeKind};
215
216 let mut best: std::collections::HashMap<(String, ModuleContext), usize> =
218 std::collections::HashMap::new();
219 let mut result: Vec<TypeDeclaration> = Vec::new();
220 let mut skip: std::collections::HashSet<usize> = std::collections::HashSet::new();
221
222 for (i, decl) in declarations.iter().enumerate() {
224 if let TypeKind::Function(ref f) = decl.kind {
225 let key = (f.name.clone(), decl.module_context.clone());
226 if let Some(&existing_idx) = best.get(&key) {
227 if let TypeKind::Function(ref existing_f) = declarations[existing_idx].kind {
229 if f.params.len() > existing_f.params.len() {
230 skip.insert(existing_idx);
231 best.insert(key, i);
232 } else {
233 skip.insert(i);
234 }
235 }
236 } else {
237 best.insert(key, i);
238 }
239 }
240 }
241
242 for (i, decl) in declarations.into_iter().enumerate() {
243 if !skip.contains(&i) {
244 result.push(decl);
245 }
246 }
247
248 result
249}
250
251fn resolve_imports(
255 gctx: &mut GlobalContext,
256 builtin: ScopeId,
257 file_scopes: &mut Vec<ScopeId>,
258 scope_dirs: &mut std::collections::HashMap<ScopeId, PathBuf>,
259 parsed_files: &mut std::collections::HashSet<PathBuf>,
260 lib_name: Option<&str>,
261) -> Result<()> {
262 let mut failed_modules: std::collections::HashSet<String> = std::collections::HashSet::new();
265
266 loop {
267 let pending: Vec<scope::PendingImport> = gctx
269 .pending_imports
270 .drain(..)
271 .filter(|p| !failed_modules.contains(&p.from_module))
272 .collect();
273
274 if pending.is_empty() {
275 break;
276 }
277
278 let mut new_files_parsed = false;
279 let mut still_pending = Vec::new();
280
281 for import in pending {
282 if let Some(module_id) = gctx.find_module(&import.from_module) {
284 let target_scope = gctx.get_module(module_id).scope;
286 if let Some(type_id) = gctx.scopes.resolve(target_scope, &import.original_name) {
287 gctx.scopes
288 .insert(import.scope, import.local_name.clone(), type_id);
289 } else {
290 gctx.warn(format!(
291 "Import `{}` not found in module \"{}\"",
292 import.original_name, import.from_module
293 ));
294 }
295 continue;
296 }
297
298 let base_dir = scope_dirs
300 .get(&import.scope)
301 .cloned()
302 .unwrap_or_else(|| PathBuf::from("."));
303 let resolved_path = resolve::resolve_module(&import.from_module, &base_dir);
304
305 if let Some(path) = resolved_path {
306 let canonical = path.canonicalize().unwrap_or_else(|_| path.clone());
307
308 if !parsed_files.contains(&canonical) {
309 gctx.info(format!(
310 "Resolving import \"{}\" → {}",
311 import.from_module,
312 path.display()
313 ));
314
315 match std::fs::read_to_string(&path) {
316 Ok(source) => {
317 let dep_scope = gctx.create_child_scope(builtin);
318 file_scopes.push(dep_scope);
319 parsed_files.insert(canonical);
320 if let Some(parent) = path.parent() {
321 scope_dirs.insert(dep_scope, parent.to_path_buf());
322 }
323
324 let module_id =
325 gctx.register_module(import.from_module.clone(), dep_scope);
326
327 match parse_single_file(&source, &path, lib_name, gctx, dep_scope) {
328 Ok(dep_type_ids) => {
329 gctx.get_module_mut(module_id).types = dep_type_ids;
330 new_files_parsed = true;
331
332 if let Some(type_id) =
334 gctx.scopes.resolve(dep_scope, &import.original_name)
335 {
336 gctx.scopes.insert(
337 import.scope,
338 import.local_name.clone(),
339 type_id,
340 );
341 } else {
342 gctx.warn(format!(
343 "Import `{}` not found in \"{}\"",
344 import.original_name, import.from_module
345 ));
346 }
347 }
348 Err(e) => {
349 gctx.warn(format!(
350 "Failed to parse dependency \"{}\" ({}): {e}",
351 import.from_module,
352 path.display()
353 ));
354 failed_modules.insert(import.from_module.clone());
355 }
356 }
357 }
358 Err(e) => {
359 gctx.warn(format!(
360 "Failed to read dependency \"{}\" ({}): {e}",
361 import.from_module,
362 path.display()
363 ));
364 failed_modules.insert(import.from_module.clone());
365 }
366 }
367 } else {
368 still_pending.push(import);
370 }
371 } else {
372 gctx.warn(format!(
373 "Could not resolve import \"{}\" — use --external to map this type",
374 import.from_module
375 ));
376 failed_modules.insert(import.from_module.clone());
377 }
378 }
379
380 gctx.pending_imports.extend(still_pending);
382
383 if !new_files_parsed {
384 break;
385 }
386 }
387
388 Ok(())
389}
390
391fn populate_builtin_scope(gctx: &mut GlobalContext, scope: ScopeId) {
399 for &name in crate::codegen::typemap::JS_SYS_RESERVED {
402 let type_id = gctx.insert_type(crate::ir::TypeDeclaration {
403 kind: crate::ir::TypeKind::Interface(crate::ir::InterfaceDecl {
404 name: name.to_string(),
405 js_name: name.to_string(),
406 type_params: vec![],
407 extends: vec![],
408 members: vec![],
409 classification: crate::ir::InterfaceClassification::ClassLike,
410 }),
411 module_context: crate::ir::ModuleContext::Global,
412 doc: None,
413 scope_id: scope,
414 exported: false,
415 });
416 gctx.scopes.insert(scope, name.to_string(), type_id);
417 }
418
419 for name in &[
423 "ReadableStream",
424 "WritableStream",
425 "TransformStream",
426 "Request",
427 "Response",
428 "Headers",
429 "Blob",
430 "File",
431 "FormData",
432 "URL",
433 "URLSearchParams",
434 "Event",
435 "EventTarget",
436 "AbortController",
437 "AbortSignal",
438 "WebSocket",
439 "Worker",
440 "Crypto",
441 "CryptoKey",
442 "SubtleCrypto",
443 "TextEncoder",
444 "TextDecoder",
445 ] {
446 let type_id = gctx.insert_type(crate::ir::TypeDeclaration {
447 kind: crate::ir::TypeKind::Interface(crate::ir::InterfaceDecl {
448 name: name.to_string(),
449 js_name: name.to_string(),
450 type_params: vec![],
451 extends: vec![],
452 members: vec![],
453 classification: crate::ir::InterfaceClassification::ClassLike,
454 }),
455 module_context: crate::ir::ModuleContext::Global,
456 doc: None,
457 scope_id: scope,
458 exported: false,
459 });
460 gctx.scopes.insert(scope, name.to_string(), type_id);
461 }
462}
463
464fn merge_namespaces(
469 declarations: Vec<crate::ir::TypeDeclaration>,
470) -> Vec<crate::ir::TypeDeclaration> {
471 use crate::ir::{TypeDeclaration, TypeKind};
472 use std::collections::HashMap;
473
474 let mut ns_map: HashMap<String, usize> = HashMap::new();
475 let mut result: Vec<TypeDeclaration> = Vec::new();
476
477 for decl in declarations {
478 if let TypeKind::Namespace(ref ns_decl) = decl.kind {
479 if let Some(&existing_idx) = ns_map.get(&ns_decl.name) {
480 if let TypeKind::Namespace(ref mut existing) = result[existing_idx].kind {
482 existing.declarations.extend(ns_decl.declarations.clone());
483 }
484 continue;
485 }
486 let name = ns_decl.name.clone();
487 let idx = result.len();
488 ns_map.insert(name, idx);
489 }
490 result.push(decl);
491 }
492
493 result
494}
495
496fn declaration_name(kind: &crate::ir::TypeKind) -> Option<String> {
498 use crate::ir::TypeKind;
499 match kind {
500 TypeKind::Class(c) => Some(c.name.clone()),
501 TypeKind::Interface(i) => Some(i.name.clone()),
502 TypeKind::TypeAlias(a) => Some(a.name.clone()),
503 TypeKind::StringEnum(e) => Some(e.name.clone()),
504 TypeKind::NumericEnum(e) => Some(e.name.clone()),
505 TypeKind::Function(f) => Some(f.name.clone()),
506 TypeKind::Variable(v) => Some(v.name.clone()),
507 TypeKind::Namespace(n) => Some(n.name.clone()),
508 }
509}
510
511fn merge_members(base: &mut Vec<crate::ir::Member>, incoming: Vec<crate::ir::Member>) {
514 use std::collections::HashMap;
515
516 let mut by_key: HashMap<MemberKey, usize> = HashMap::new();
518 for (i, m) in base.iter().enumerate() {
519 by_key.insert(member_key(m), i);
520 }
521
522 for m in incoming {
523 let key = member_key(&m);
524 if let Some(&idx) = by_key.get(&key) {
525 base[idx] = m;
527 } else {
528 by_key.insert(key, base.len());
529 base.push(m);
530 }
531 }
532}
533
534#[derive(PartialEq, Eq, Hash)]
539enum MemberKey {
540 Constructor,
541 StaticMethod(String),
542 StaticGetter(String),
543 StaticSetter(String),
544 Proto(String),
545 ProtoGetter(String),
546 ProtoSetter(String),
547}
548
549fn member_key(member: &crate::ir::Member) -> MemberKey {
550 match member {
551 crate::ir::Member::Constructor(_) => MemberKey::Constructor,
552 crate::ir::Member::StaticMethod(m) => MemberKey::StaticMethod(m.name.clone()),
553 crate::ir::Member::StaticGetter(g) => MemberKey::StaticGetter(g.js_name.clone()),
554 crate::ir::Member::StaticSetter(s) => MemberKey::StaticSetter(s.js_name.clone()),
555 crate::ir::Member::Method(m) => MemberKey::Proto(m.name.clone()),
556 crate::ir::Member::Getter(g) => MemberKey::ProtoGetter(g.js_name.clone()),
557 crate::ir::Member::Setter(s) => MemberKey::ProtoSetter(s.js_name.clone()),
558 crate::ir::Member::IndexSignature(_) => MemberKey::Proto("[index]".to_string()),
559 }
560}
561
562fn merge_class_pairs(
567 declarations: Vec<crate::ir::TypeDeclaration>,
568) -> Vec<crate::ir::TypeDeclaration> {
569 use crate::ir::{TypeDeclaration, TypeKind};
570 use std::collections::HashMap;
571
572 let mut class_map: HashMap<String, usize> = HashMap::new();
573 let mut iface_map: HashMap<String, usize> = HashMap::new();
574 let mut result: Vec<TypeDeclaration> = Vec::new();
575
576 for decl in declarations {
577 match &decl.kind {
578 TypeKind::Class(class_decl) => {
579 let name = class_decl.name.clone();
580
581 if let Some(&existing_idx) = class_map.get(&name) {
583 if let TypeKind::Class(ref mut existing) = result[existing_idx].kind {
584 merge_members(&mut existing.members, class_decl.members.clone());
585 }
586 continue;
587 }
588
589 if let Some(&iface_idx) = iface_map.get(&name) {
591 let mut new_class = class_decl.clone();
593 if let TypeKind::Interface(ref iface) = result[iface_idx].kind {
594 merge_members(&mut new_class.members, iface.members.clone());
595 if new_class.extends.is_none() {
596 new_class.extends = iface.extends.first().cloned();
597 }
598 if new_class.type_params.is_empty() {
599 new_class.type_params = iface.type_params.clone();
600 }
601 }
602 result[iface_idx] = TypeDeclaration {
603 kind: TypeKind::Class(new_class),
604 module_context: decl.module_context.clone(),
605 doc: decl.doc.clone(),
606 scope_id: decl.scope_id,
607 exported: decl.exported,
608 };
609 class_map.insert(name.clone(), iface_idx);
610 iface_map.remove(&name);
611 continue;
612 }
613
614 let idx = result.len();
615 class_map.insert(name, idx);
616 result.push(decl);
617 }
618 TypeKind::Interface(iface_decl) => {
619 let name = iface_decl.name.clone();
620
621 if let Some(&class_idx) = class_map.get(&name) {
623 if let TypeKind::Class(ref mut class) = result[class_idx].kind {
624 merge_members(&mut class.members, iface_decl.members.clone());
625 if class.extends.is_none() {
626 class.extends = iface_decl.extends.first().cloned();
627 }
628 if class.type_params.is_empty() {
629 class.type_params = iface_decl.type_params.clone();
630 }
631 }
632 continue;
633 }
634
635 if let Some(&existing_idx) = iface_map.get(&name) {
637 if let TypeKind::Interface(ref mut existing) = result[existing_idx].kind {
638 merge_members(&mut existing.members, iface_decl.members.clone());
639 for ext in &iface_decl.extends {
641 if !existing.extends.contains(ext) {
642 existing.extends.push(ext.clone());
643 }
644 }
645 }
646 continue;
647 }
648
649 let idx = result.len();
650 iface_map.insert(name, idx);
651 result.push(decl);
652 }
653 _ => {
654 result.push(decl);
655 }
656 }
657 }
658
659 result
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665 use crate::ir::{GetterMember, Member, MethodMember, TypeRef};
666
667 fn method(name: &str) -> Member {
668 Member::Method(MethodMember {
669 name: name.to_string(),
670 js_name: name.to_string(),
671 type_params: vec![],
672 params: vec![],
673 return_type: TypeRef::Void,
674 optional: false,
675 doc: None,
676 })
677 }
678
679 fn getter(name: &str) -> Member {
680 Member::Getter(GetterMember {
681 js_name: name.to_string(),
682 type_ref: TypeRef::String,
683 optional: false,
684 doc: None,
685 })
686 }
687
688 #[test]
689 fn test_merge_members_dedup() {
690 let mut base = vec![method("read"), method("write"), getter("name")];
691 let incoming = vec![method("write"), method("end")];
692 merge_members(&mut base, incoming);
693
694 assert_eq!(base.len(), 4);
696 assert!(matches!(&base[0], Member::Method(m) if m.name == "read"));
697 assert!(matches!(&base[1], Member::Method(m) if m.name == "write"));
698 assert!(matches!(&base[2], Member::Getter(g) if g.js_name == "name"));
699 assert!(matches!(&base[3], Member::Method(m) if m.name == "end"));
700 }
701
702 #[test]
703 fn test_merge_members_no_overlap() {
704 let mut base = vec![method("foo")];
705 let incoming = vec![method("bar")];
706 merge_members(&mut base, incoming);
707 assert_eq!(base.len(), 2);
708 }
709
710 #[test]
711 fn test_merge_members_getter_and_method_coexist() {
712 let mut base = vec![getter("data")];
714 let incoming = vec![method("data")];
715 merge_members(&mut base, incoming);
716 assert_eq!(base.len(), 2);
717 assert!(matches!(&base[0], Member::Getter(g) if g.js_name == "data"));
718 assert!(matches!(&base[1], Member::Method(m) if m.name == "data"));
719 }
720}