js_source_scopes/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3
4use std::fmt::Display;
5use std::ops::Range;
6
7mod name_resolver;
8mod scope_index;
9mod scope_name;
10mod source;
11mod swc;
12
13pub use name_resolver::NameResolver;
14pub use scope_index::{ScopeIndex, ScopeIndexError, ScopeLookupResult};
15pub use scope_name::{NameComponent, ScopeName};
16pub use source::{SourceContext, SourceContextError, SourcePosition};
17use swc_common::Spanned;
18
19/// The Scopes extracted from a piece of JS Code.
20pub type Scopes = Vec<(Range<u32>, Option<ScopeName>)>;
21
22/// Extracts function scopes from the given JS-like `src`.
23///
24/// The returned Vec includes the [`Range`] of the function scope, in byte offsets
25/// inside the `src`, and the corresponding function name. `None` in this case
26/// denotes a function scope for which no name could be inferred from the
27/// surrounding code, which can mostly happen for anonymous or arrow functions
28/// used as immediate callbacks.
29///
30/// The range includes the whole range of the function expression, including the
31/// leading `function` keyword, function argument parentheses and trailing brace
32/// in case there is one.
33/// The returned vector does not have a guaranteed sorting order, and is
34/// implementation dependent.
35///
36/// # Examples
37///
38/// ```
39/// let src = "const arrowFnExpr = (a) => a; function namedFnDecl() {}";
40/// //                arrowFnExpr -^------^  ^------namedFnDecl------^
41/// let mut scopes: Vec<_> = js_source_scopes::extract_scope_names(src)
42///     .unwrap()
43///     .into_iter()
44///     .map(|res| {
45///         let components = res.1.map(|n| n.components().map(|c| {
46///             (c.text().to_string(), c.range())
47///         }).collect::<Vec<_>>());
48///         (res.0, components)
49///     }).collect();
50/// scopes.sort_by_key(|s| s.0.start);
51///
52/// let expected = vec![
53///   (20..28, Some(vec![(String::from("arrowFnExpr"), Some(6..17))])),
54///   (30..55, Some(vec![(String::from("namedFnDecl"),Some(39..50))])),
55/// ];
56/// assert_eq!(scopes, expected);
57/// ```
58#[tracing::instrument(level = "trace", skip_all)]
59pub fn extract_scope_names(src: &str) -> Result<Scopes, ParseError> {
60    let mut scopes = swc::parse_with_swc(src).map_err(|e| ParseError { inner: e })?;
61
62    // filter out empty names
63    for scope in &mut scopes {
64        if let Some(ref name) = scope.1 {
65            if name.components.is_empty() {
66                scope.1 = None;
67            }
68        }
69    }
70
71    Ok(scopes)
72}
73
74/// An error parsing the JS Source provided to [`extract_scope_names`].
75#[derive(Debug)]
76pub struct ParseError {
77    inner: swc::ParseError,
78}
79
80impl Display for ParseError {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        let span = self.inner.span();
83        f.write_fmt(format_args!(
84            "{}:{}:{}",
85            span.lo.0,
86            span.hi.0,
87            self.inner.kind().msg()
88        ))
89    }
90}
91
92impl std::error::Error for ParseError {}