Skip to main content

weavatrix_parse/script/
mod.rs

1//! Structural extraction for JavaScript and TypeScript.
2//!
3//! The pass walks the token stream once, tracking brace depth to know which
4//! declaration owns what. It reads the forms a repository graph is built from:
5//! every import and re-export shape, declarations including class members,
6//! and call sites with their receiver and string arguments.
7//!
8//! What it does not do is parse expressions. A call is recognised by an
9//! identifier followed by `(`, not by building an expression tree, because no
10//! consumer of these facts asks about precedence.
11
12use crate::facts::{
13    Declaration, DeclarationKind, Facts, Import, ImportBinding, Reference, ReferenceKind, Span,
14};
15use crate::syntax::Language;
16use crate::token::{Mode, Token, TokenKind, Tokenizer};
17use std::collections::BTreeMap;
18
19/// Extracts structural facts from one JavaScript or TypeScript source.
20#[must_use]
21pub fn extract(source: &str, language: Language) -> Facts {
22    let tokens = Tokenizer::new(source, language)
23        .mode(Mode::Lite)
24        .collect::<Vec<_>>();
25    Extractor {
26        source,
27        tokens: &tokens,
28        language,
29        facts: Facts::default(),
30        scopes: Vec::new(),
31        import_bindings: BTreeMap::new(),
32        depth: 0,
33        paren_depth: 0,
34        bracket_depth: 0,
35    }
36    .run()
37}
38
39/// A declaration whose body the walk is currently inside.
40struct Scope {
41    name: String,
42    /// Depth of the body, once it opens. A declaration is recorded before its
43    /// `{` is seen, so until then the scope is waiting and must not be closed
44    /// by the very brace that opens it.
45    depth: Option<i32>,
46    /// Whether members declared directly inside are class or object members.
47    member_body: bool,
48    /// Classes declare fields; object literals only contribute named methods.
49    fields: bool,
50    /// Parenthesis/bracket nesting at the member body's opening brace.
51    paren_depth: i32,
52    bracket_depth: i32,
53}
54
55struct Extractor<'source, 'tokens> {
56    source: &'source str,
57    tokens: &'tokens [Token],
58    language: Language,
59    facts: Facts,
60    scopes: Vec<Scope>,
61    import_bindings: BTreeMap<String, (String, bool, String)>,
62    depth: i32,
63    paren_depth: i32,
64    bracket_depth: i32,
65}
66
67mod calls;
68mod declarations;
69mod modules;
70mod traversal;
71mod types;
72
73/// Whether a name is an HTTP method written as a route-table key.
74fn is_method(name: &str) -> bool {
75    matches!(
76        name,
77        "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS" | "ALL"
78    )
79}
80
81/// Byte ranges of the expressions enclosed by `${...}` in one JavaScript
82/// template token. A nested template is one token while matching the outer
83/// expression, so braces in its text cannot close the expression early.
84fn template_interpolation_ranges(template: &str, language: Language) -> Vec<(usize, usize)> {
85    let bytes = template.as_bytes();
86    let mut ranges = Vec::new();
87    let mut cursor = usize::from(bytes.first() == Some(&b'`'));
88    while cursor + 1 < bytes.len() {
89        if bytes[cursor] == b'\\' {
90            cursor = (cursor + 2).min(bytes.len());
91            continue;
92        }
93        if bytes[cursor] == b'`' {
94            break;
95        }
96        if bytes[cursor] != b'$' || bytes[cursor + 1] != b'{' {
97            cursor += 1;
98            continue;
99        }
100        let expression_start = cursor + 2;
101        let tail = &template[expression_start..];
102        let tokens = Tokenizer::new(tail, language)
103            .mode(Mode::Lite)
104            .collect::<Vec<_>>();
105        let mut depth = 1_i32;
106        let mut expression_end = None;
107        for token in tokens {
108            if token.kind != TokenKind::Punctuation {
109                continue;
110            }
111            match token.text(tail) {
112                "{" => depth += 1,
113                "}" => {
114                    depth -= 1;
115                    if depth == 0 {
116                        expression_end = Some(expression_start + token.start);
117                        cursor = expression_start + token.end;
118                        break;
119                    }
120                }
121                _ => {}
122            }
123        }
124        let Some(expression_end) = expression_end else {
125            break;
126        };
127        ranges.push((expression_start, expression_end));
128    }
129    ranges
130}
131
132fn position_at(source: &str, offset: usize) -> (u32, u32) {
133    let prefix = source.get(..offset).unwrap_or(source);
134    let line = u32::try_from(prefix.bytes().filter(|byte| *byte == b'\n').count())
135        .unwrap_or(u32::MAX)
136        .saturating_add(1);
137    let column = u32::try_from(
138        prefix
139            .rsplit_once('\n')
140            .map_or(prefix, |(_, suffix)| suffix)
141            .chars()
142            .count(),
143    )
144    .unwrap_or(u32::MAX)
145    .saturating_add(1);
146    (line, column)
147}
148
149#[cfg(test)]
150mod tests;