Skip to main content

sim_codec_typescript/
parser.rs

1//! TypeScript overlay scanner over the public JavaScript seam.
2
3use sim_codec_javascript::{Node, NodeKind, Span, TokenKind, tokenize_with_limits};
4
5use crate::{Diagnostic, DiagnosticCode, Language, Limits, SyntaxKind, SyntaxNode, SyntaxTree};
6
7/// Parse a TypeScript module.
8pub fn parse_module(source: &str) -> Result<SyntaxTree, Diagnostic> {
9    parse_module_with_limits(source, Limits::default())
10}
11/// Parse a TypeScript module with explicit bounds.
12pub fn parse_module_with_limits(source: &str, limits: Limits) -> Result<SyntaxTree, Diagnostic> {
13    parse(source, Language::TypeScript, limits)
14}
15/// Parse a TSX module.
16pub fn parse_tsx(source: &str) -> Result<SyntaxTree, Diagnostic> {
17    parse_tsx_with_limits(source, Limits::default())
18}
19/// Parse a TSX module with explicit bounds.
20pub fn parse_tsx_with_limits(source: &str, limits: Limits) -> Result<SyntaxTree, Diagnostic> {
21    parse(source, Language::Tsx, limits)
22}
23
24fn parse(source: &str, language: Language, limits: Limits) -> Result<SyntaxTree, Diagnostic> {
25    if source.len() > limits.max_bytes {
26        return Err(error(
27            source,
28            DiagnosticCode::ResourceLimit,
29            0,
30            "source byte limit exceeded",
31        ));
32    }
33    let js_limits = sim_codec_javascript::Limits {
34        max_bytes: limits.max_bytes,
35        max_tokens: limits.max_nodes,
36        max_nesting: limits.max_nesting,
37        ..sim_codec_javascript::Limits::default()
38    };
39    let tokens = tokenize_with_limits(source, js_limits).map_err(|diagnostic| Diagnostic {
40        code: match diagnostic.code {
41            sim_codec_javascript::DiagnosticCode::ResourceLimit => DiagnosticCode::ResourceLimit,
42            _ => DiagnosticCode::UnclosedSyntax,
43        },
44        span: diagnostic.span,
45        line: diagnostic.line,
46        column: diagnostic.column,
47        message: diagnostic.message,
48    })?;
49    let mut nodes = vec![SyntaxNode::JavaScript(Node {
50        kind: NodeKind::Module,
51        tokens: 0..tokens.len(),
52        children: Vec::new(),
53        asi: None,
54    })];
55    let mut context = Vec::<String>::new();
56    for (index, token) in tokens.iter().enumerate() {
57        let word = &source[token.span.start..token.span.end];
58        if matches!(
59            word,
60            "class" | "interface" | "type" | "enum" | "namespace" | "module" | "function"
61        ) {
62            context.clear();
63            context.push(word.to_owned());
64        }
65        let kind = if matches!(
66            word,
67            "interface" | "type" | "enum" | "namespace" | "declare"
68        ) {
69            Some(SyntaxKind::Declaration)
70        } else if matches!(
71            word,
72            "public"
73                | "private"
74                | "protected"
75                | "readonly"
76                | "abstract"
77                | "override"
78                | "accessor"
79                | "declare"
80        ) {
81            Some(SyntaxKind::Modifier)
82        } else if matches!(
83            word,
84            "keyof"
85                | "infer"
86                | "is"
87                | "asserts"
88                | "satisfies"
89                | "typeof"
90                | "unique"
91                | "unknown"
92                | "never"
93                | "any"
94        ) {
95            Some(SyntaxKind::TypeNode)
96        } else if word == ":" {
97            Some(SyntaxKind::Annotation)
98        } else if word == "<" && looks_like_jsx(source, token.span.start) {
99            if language == Language::TypeScript {
100                return Err(error(
101                    source,
102                    DiagnosticCode::JsxInTypeScript,
103                    token.span.start,
104                    "JSX requires TSX mode",
105                ));
106            }
107            Some(SyntaxKind::Jsx)
108        } else if word == "<" && looks_like_type_arguments(&tokens, index, source) {
109            Some(SyntaxKind::TypeArguments)
110        } else {
111            None
112        };
113        if let Some(kind) = kind {
114            nodes.push(SyntaxNode::TypeScript {
115                kind,
116                span: token.span,
117                context: context.clone(),
118            });
119            if nodes.len() > limits.max_nodes {
120                return Err(error(
121                    source,
122                    DiagnosticCode::ResourceLimit,
123                    token.span.start,
124                    "node limit exceeded",
125                ));
126            }
127        }
128    }
129    Ok(SyntaxTree {
130        source: source.to_owned(),
131        language,
132        tokens,
133        nodes,
134    })
135}
136
137fn looks_like_jsx(source: &str, at: usize) -> bool {
138    let tail = &source[at + 1..];
139    tail.starts_with('>')
140        || tail.starts_with('/')
141        || (tail
142            .chars()
143            .next()
144            .is_some_and(|ch| ch.is_ascii_alphabetic())
145            && (tail.contains("</") || tail.contains("/>")))
146}
147fn looks_like_type_arguments(
148    tokens: &[sim_codec_javascript::Token],
149    index: usize,
150    source: &str,
151) -> bool {
152    tokens.get(index + 1).is_some_and(|next| {
153        let text = &source[next.span.start..next.span.end];
154        next.kind == TokenKind::Identifier
155            && !looks_like_jsx(source, tokens[index].span.start)
156            && !text.is_empty()
157    })
158}
159fn error(source: &str, code: DiagnosticCode, at: usize, message: &str) -> Diagnostic {
160    let prefix = &source[..at.min(source.len())];
161    let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
162    let column = prefix
163        .rsplit('\n')
164        .next()
165        .unwrap_or_default()
166        .chars()
167        .count();
168    Diagnostic {
169        code,
170        span: Span { start: at, end: at },
171        line,
172        column,
173        message: message.to_owned(),
174    }
175}