1use std::fs;
6use std::process;
7
8use tree_sitter::{Node, Parser};
9
10const TYPE_KINDS: &[&str] = &[
11 "class_declaration",
12 "interface_declaration",
13 "struct_declaration",
14 "enum_declaration",
15 "record_declaration",
16];
17
18fn new_parser() -> Parser {
19 let mut parser = Parser::new();
20 parser
21 .set_language(&tree_sitter_c_sharp::LANGUAGE.into())
22 .expect("failed to load C# grammar");
23 parser
24}
25
26pub fn utf16_units(source: &str) -> Vec<u16> {
49 source.encode_utf16().collect()
50}
51
52pub fn utf16_bytes(units: &[u16]) -> Vec<u8> {
55 units.iter().flat_map(|u| u.to_le_bytes()).collect()
56}
57
58pub fn node_text(node: Node, src: &[u8]) -> String {
61 let range = node.byte_range();
62 if range.start > range.end || range.end > src.len() {
63 return String::new();
64 }
65 let units: Vec<u16> = src[range]
66 .chunks_exact(2)
67 .map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
68 .collect();
69 String::from_utf16_lossy(&units)
70}
71
72pub fn utf16_index(byte: usize) -> usize {
75 byte / 2
76}
77
78pub fn run_parse(path: &str) {
84 let source = fs::read_to_string(path).unwrap_or_else(|err| {
85 eprintln!("failed to read {path}: {err}");
86 process::exit(1);
87 });
88
89 let mut parser = new_parser();
90 let units = utf16_units(&source);
91 let tree = parser
92 .parse_utf16_le(&units, None)
93 .expect("parse returned no tree");
94 walk(tree.root_node(), &utf16_bytes(&units), 0);
95}
96
97fn walk(node: Node, src: &[u8], depth: usize) {
98 if TYPE_KINDS.contains(&node.kind()) {
99 print_type_decl(node, src, depth);
100 }
101
102 let mut cursor = node.walk();
103 for child in node.children(&mut cursor) {
104 walk(child, src, depth + 1);
105 }
106}
107
108fn print_type_decl(node: Node, src: &[u8], depth: usize) {
109 let indent = " ".repeat(depth.saturating_sub(1));
110 let kind = node.kind();
111 let name = node
112 .child_by_field_name("name")
113 .map(|n| text(n, src))
114 .unwrap_or_else(|| "<anonymous>".to_string());
115 let bases = base_list_text(node, src);
116
117 println!("{indent}{kind} {name} : [{bases}]");
118
119 if kind == "enum_declaration" {
120 if let Some(body) = node.child_by_field_name("body") {
121 let mut cursor = body.walk();
122 for member in body.named_children(&mut cursor) {
123 if member.kind() == "enum_member_declaration" {
124 if let Some(mname) = member.child_by_field_name("name") {
125 println!("{indent} enum member: {}", text(mname, src));
126 }
127 }
128 }
129 }
130 }
131}
132
133fn base_list_text(node: Node, src: &[u8]) -> String {
134 let mut cursor = node.walk();
135 for child in node.children(&mut cursor) {
136 if child.kind() == "base_list" {
137 let mut names = Vec::new();
138 let mut bl_cursor = child.walk();
139 for base_child in child.named_children(&mut bl_cursor) {
140 if base_child.kind() != "argument_list" {
141 names.push(text(base_child, src));
142 }
143 }
144 return names.join(", ");
145 }
146 }
147 String::new()
148}
149
150fn text(node: Node, src: &[u8]) -> String {
151 node_text(node, src)
152}
153
154const SPAN_KINDS: &[&str] = &[
161 "class_declaration",
162 "interface_declaration",
163 "struct_declaration",
164 "record_declaration",
165 "enum_declaration",
166 "enum_member_declaration",
167];
168
169pub struct SpanRecord {
171 pub kind: String,
173 pub name: String,
175 pub start_byte: usize,
177 pub end_byte: usize,
179 pub start_row: usize,
181 pub start_col: usize,
183 pub end_row: usize,
185 pub end_col: usize,
187}
188
189pub fn run_spans(path: &str) {
191 let source = fs::read_to_string(path).unwrap_or_else(|err| {
192 eprintln!("failed to read {path}: {err}");
193 process::exit(1);
194 });
195 let records = collect_spans(&source);
196 println!("{}", spans_json(&records));
197}
198
199pub fn collect_spans(source: &str) -> Vec<SpanRecord> {
201 let mut parser = new_parser();
202 let units = utf16_units(source);
203 let tree = parser
204 .parse_utf16_le(&units, None)
205 .expect("parse returned no tree");
206 let mut out = Vec::new();
207 walk_spans(tree.root_node(), &utf16_bytes(&units), &mut out);
208 out.sort_by(|a, b| {
209 a.start_byte
210 .cmp(&b.start_byte)
211 .then(a.end_byte.cmp(&b.end_byte))
212 .then(a.kind.cmp(&b.kind))
213 .then(a.name.cmp(&b.name))
214 });
215 out
216}
217
218fn walk_spans(node: Node, src: &[u8], out: &mut Vec<SpanRecord>) {
219 if SPAN_KINDS.contains(&node.kind()) {
220 if let Some(name_node) = node.child_by_field_name("name") {
221 let start_point = node.start_position();
222 let end_point = node.end_position();
223 let (start_byte, start_col) = (
224 utf16_index(node.start_byte()),
225 utf16_index(start_point.column),
226 );
227 let (end_byte, end_col) = (utf16_index(node.end_byte()), utf16_index(end_point.column));
228 out.push(SpanRecord {
229 kind: node.kind().to_string(),
230 name: text(name_node, src),
231 start_byte,
232 end_byte,
233 start_row: start_point.row,
234 start_col,
235 end_row: end_point.row,
236 end_col,
237 });
238 }
239 }
240
241 let mut cursor = node.walk();
242 for child in node.named_children(&mut cursor) {
243 walk_spans(child, src, out);
244 }
245}
246
247pub fn spans_json(records: &[SpanRecord]) -> String {
251 if records.is_empty() {
252 return "[]".to_string();
253 }
254 let mut out = String::from("[\n");
255 let last = records.len() - 1;
256 for (i, r) in records.iter().enumerate() {
257 out.push_str(" {\n");
258 out.push_str(&format!(" \"kind\": {},\n", json_string(&r.kind)));
259 out.push_str(&format!(" \"name\": {},\n", json_string(&r.name)));
260 out.push_str(&format!(" \"start_byte\": {},\n", r.start_byte));
261 out.push_str(&format!(" \"end_byte\": {},\n", r.end_byte));
262 out.push_str(&format!(" \"start_row\": {},\n", r.start_row));
263 out.push_str(&format!(" \"start_col\": {},\n", r.start_col));
264 out.push_str(&format!(" \"end_row\": {},\n", r.end_row));
265 out.push_str(&format!(" \"end_col\": {}\n", r.end_col));
266 out.push_str(if i == last { " }\n" } else { " },\n" });
267 }
268 out.push(']');
269 out
270}
271
272fn json_string(s: &str) -> String {
273 let mut out = String::with_capacity(s.len() + 2);
274 out.push('"');
275 for ch in s.chars() {
276 match ch {
277 '"' => out.push_str("\\\""),
278 '\\' => out.push_str("\\\\"),
279 '\n' => out.push_str("\\n"),
280 '\r' => out.push_str("\\r"),
281 '\t' => out.push_str("\\t"),
282 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
283 c => out.push(c),
284 }
285 }
286 out.push('"');
287 out
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
303pub enum TsGrammar {
305 Typescript,
307 Tsx,
309 Javascript,
311}
312
313pub fn ts_grammar_for(rel: &str) -> Option<TsGrammar> {
317 let dot = rel.rfind('.')?;
318 match &rel[dot..] {
319 ".ts" => Some(TsGrammar::Typescript),
320 ".tsx" => Some(TsGrammar::Tsx),
321 ".js" | ".jsx" => Some(TsGrammar::Javascript),
322 _ => None,
323 }
324}
325
326pub fn is_ts_js(rel: &str) -> bool {
328 ts_grammar_for(rel).is_some()
329}
330
331fn new_ts_parser(grammar: TsGrammar) -> Parser {
332 let mut parser = Parser::new();
333 let language = match grammar {
334 TsGrammar::Typescript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
335 TsGrammar::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
336 TsGrammar::Javascript => tree_sitter_javascript::LANGUAGE.into(),
337 };
338 parser
339 .set_language(&language)
340 .expect("failed to load TS/JS grammar");
341 parser
342}
343
344pub fn parse_ts_js(units: &[u16], grammar: TsGrammar) -> Option<tree_sitter::Tree> {
348 new_ts_parser(grammar).parse_utf16_le(units, None)
349}
350
351#[cfg(test)]
352mod ts_grammar_tests {
353 use super::*;
354
355 #[test]
356 fn extension_maps_to_the_rt1_pinned_grammar() {
357 assert_eq!(ts_grammar_for("a/b.ts"), Some(TsGrammar::Typescript));
358 assert_eq!(ts_grammar_for("a/b.tsx"), Some(TsGrammar::Tsx));
359 assert_eq!(ts_grammar_for("a/b.js"), Some(TsGrammar::Javascript));
360 assert_eq!(ts_grammar_for("a/b.jsx"), Some(TsGrammar::Javascript));
361 assert_eq!(ts_grammar_for("a/b.cs"), None);
362 assert_eq!(ts_grammar_for("a/b"), None);
363 }
364
365 #[test]
366 fn is_ts_js_true_only_for_the_four_extensions() {
367 assert!(is_ts_js("x.ts"));
368 assert!(is_ts_js("x.tsx"));
369 assert!(is_ts_js("x.js"));
370 assert!(is_ts_js("x.jsx"));
371 assert!(!is_ts_js("x.cs"));
372 assert!(!is_ts_js("x.json"));
373 }
374
375 #[test]
376 fn each_grammar_loads_and_parses_without_panicking() {
377 assert!(parse_ts_js(
378 &utf16_units("const x: number = 1;\n"),
379 TsGrammar::Typescript
380 )
381 .is_some());
382 assert!(parse_ts_js(&utf16_units("const x = <div>hi</div>;\n"), TsGrammar::Tsx).is_some());
383 assert!(parse_ts_js(
384 &utf16_units("function f() { return 1; }\n"),
385 TsGrammar::Javascript
386 )
387 .is_some());
388 }
389}