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    declaration: Option<usize>,
47    /// Whether members declared directly inside are class or object members.
48    member_body: bool,
49    /// Classes declare fields; object literals only contribute named methods.
50    fields: bool,
51    /// Parenthesis/bracket nesting at the member body's opening brace.
52    paren_depth: i32,
53    bracket_depth: i32,
54}
55
56struct Extractor<'source, 'tokens> {
57    source: &'source str,
58    tokens: &'tokens [Token],
59    language: Language,
60    facts: Facts,
61    scopes: Vec<Scope>,
62    import_bindings: BTreeMap<String, (String, bool, String)>,
63    depth: i32,
64    paren_depth: i32,
65    bracket_depth: i32,
66}
67
68mod calls;
69mod declarations;
70mod modules;
71mod traversal;
72mod types;
73
74/// Whether a name is an HTTP method written as a route-table key.
75fn is_method(name: &str) -> bool {
76    matches!(
77        name,
78        "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS" | "ALL"
79    )
80}
81
82/// Byte ranges of the expressions enclosed by `${...}` in one JavaScript
83/// template token. A nested template is one token while matching the outer
84/// expression, so braces in its text cannot close the expression early.
85fn template_interpolation_ranges(template: &str, language: Language) -> Vec<(usize, usize)> {
86    let bytes = template.as_bytes();
87    let mut ranges = Vec::new();
88    let mut cursor = usize::from(bytes.first() == Some(&b'`'));
89    while cursor + 1 < bytes.len() {
90        if bytes[cursor] == b'\\' {
91            cursor = (cursor + 2).min(bytes.len());
92            continue;
93        }
94        if bytes[cursor] == b'`' {
95            break;
96        }
97        if bytes[cursor] != b'$' || bytes[cursor + 1] != b'{' {
98            cursor += 1;
99            continue;
100        }
101        let expression_start = cursor + 2;
102        let tail = &template[expression_start..];
103        let tokens = Tokenizer::new(tail, language)
104            .mode(Mode::Lite)
105            .collect::<Vec<_>>();
106        let mut depth = 1_i32;
107        let mut expression_end = None;
108        for token in tokens {
109            if token.kind != TokenKind::Punctuation {
110                continue;
111            }
112            match token.text(tail) {
113                "{" => depth += 1,
114                "}" => {
115                    depth -= 1;
116                    if depth == 0 {
117                        expression_end = Some(expression_start + token.start);
118                        cursor = expression_start + token.end;
119                        break;
120                    }
121                }
122                _ => {}
123            }
124        }
125        let Some(expression_end) = expression_end else {
126            break;
127        };
128        ranges.push((expression_start, expression_end));
129    }
130    ranges
131}
132
133fn position_at(source: &str, offset: usize) -> (u32, u32) {
134    let prefix = source.get(..offset).unwrap_or(source);
135    let line = u32::try_from(prefix.bytes().filter(|byte| *byte == b'\n').count())
136        .unwrap_or(u32::MAX)
137        .saturating_add(1);
138    let column = u32::try_from(
139        prefix
140            .rsplit_once('\n')
141            .map_or(prefix, |(_, suffix)| suffix)
142            .chars()
143            .count(),
144    )
145    .unwrap_or(u32::MAX)
146    .saturating_add(1);
147    (line, column)
148}
149
150#[cfg(test)]
151mod tests;