sim_codec_typescript/
parser.rs1use sim_codec_javascript::{Node, NodeKind, Span, TokenKind, tokenize_with_limits};
4
5use crate::{Diagnostic, DiagnosticCode, Language, Limits, SyntaxKind, SyntaxNode, SyntaxTree};
6
7pub fn parse_module(source: &str) -> Result<SyntaxTree, Diagnostic> {
9 parse_module_with_limits(source, Limits::default())
10}
11pub fn parse_module_with_limits(source: &str, limits: Limits) -> Result<SyntaxTree, Diagnostic> {
13 parse(source, Language::TypeScript, limits)
14}
15pub fn parse_tsx(source: &str) -> Result<SyntaxTree, Diagnostic> {
17 parse_tsx_with_limits(source, Limits::default())
18}
19pub 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}