use std::fs;
use std::process;
use tree_sitter::{Node, Parser};
const TYPE_KINDS: &[&str] = &[
"class_declaration",
"interface_declaration",
"struct_declaration",
"enum_declaration",
"record_declaration",
];
fn new_parser() -> Parser {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_c_sharp::LANGUAGE.into())
.expect("failed to load C# grammar");
parser
}
pub fn utf16_units(source: &str) -> Vec<u16> {
source.encode_utf16().collect()
}
pub fn utf16_bytes(units: &[u16]) -> Vec<u8> {
units.iter().flat_map(|u| u.to_le_bytes()).collect()
}
pub fn node_text(node: Node, src: &[u8]) -> String {
let range = node.byte_range();
if range.start > range.end || range.end > src.len() {
return String::new();
}
let units: Vec<u16> = src[range]
.chunks_exact(2)
.map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
.collect();
String::from_utf16_lossy(&units)
}
pub fn utf16_index(byte: usize) -> usize {
byte / 2
}
pub fn run_parse(path: &str) {
let source = fs::read_to_string(path).unwrap_or_else(|err| {
eprintln!("failed to read {path}: {err}");
process::exit(1);
});
let mut parser = new_parser();
let units = utf16_units(&source);
let tree = parser
.parse_utf16_le(&units, None)
.expect("parse returned no tree");
walk(tree.root_node(), &utf16_bytes(&units), 0);
}
fn walk(node: Node, src: &[u8], depth: usize) {
if TYPE_KINDS.contains(&node.kind()) {
print_type_decl(node, src, depth);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk(child, src, depth + 1);
}
}
fn print_type_decl(node: Node, src: &[u8], depth: usize) {
let indent = " ".repeat(depth.saturating_sub(1));
let kind = node.kind();
let name = node
.child_by_field_name("name")
.map(|n| text(n, src))
.unwrap_or_else(|| "<anonymous>".to_string());
let bases = base_list_text(node, src);
println!("{indent}{kind} {name} : [{bases}]");
if kind == "enum_declaration" {
if let Some(body) = node.child_by_field_name("body") {
let mut cursor = body.walk();
for member in body.named_children(&mut cursor) {
if member.kind() == "enum_member_declaration" {
if let Some(mname) = member.child_by_field_name("name") {
println!("{indent} enum member: {}", text(mname, src));
}
}
}
}
}
}
fn base_list_text(node: Node, src: &[u8]) -> String {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "base_list" {
let mut names = Vec::new();
let mut bl_cursor = child.walk();
for base_child in child.named_children(&mut bl_cursor) {
if base_child.kind() != "argument_list" {
names.push(text(base_child, src));
}
}
return names.join(", ");
}
}
String::new()
}
fn text(node: Node, src: &[u8]) -> String {
node_text(node, src)
}
const SPAN_KINDS: &[&str] = &[
"class_declaration",
"interface_declaration",
"struct_declaration",
"record_declaration",
"enum_declaration",
"enum_member_declaration",
];
pub struct SpanRecord {
pub kind: String,
pub name: String,
pub start_byte: usize,
pub end_byte: usize,
pub start_row: usize,
pub start_col: usize,
pub end_row: usize,
pub end_col: usize,
}
pub fn run_spans(path: &str) {
let source = fs::read_to_string(path).unwrap_or_else(|err| {
eprintln!("failed to read {path}: {err}");
process::exit(1);
});
let records = collect_spans(&source);
println!("{}", spans_json(&records));
}
pub fn collect_spans(source: &str) -> Vec<SpanRecord> {
let mut parser = new_parser();
let units = utf16_units(source);
let tree = parser
.parse_utf16_le(&units, None)
.expect("parse returned no tree");
let mut out = Vec::new();
walk_spans(tree.root_node(), &utf16_bytes(&units), &mut out);
out.sort_by(|a, b| {
a.start_byte
.cmp(&b.start_byte)
.then(a.end_byte.cmp(&b.end_byte))
.then(a.kind.cmp(&b.kind))
.then(a.name.cmp(&b.name))
});
out
}
fn walk_spans(node: Node, src: &[u8], out: &mut Vec<SpanRecord>) {
if SPAN_KINDS.contains(&node.kind()) {
if let Some(name_node) = node.child_by_field_name("name") {
let start_point = node.start_position();
let end_point = node.end_position();
let (start_byte, start_col) = (
utf16_index(node.start_byte()),
utf16_index(start_point.column),
);
let (end_byte, end_col) = (utf16_index(node.end_byte()), utf16_index(end_point.column));
out.push(SpanRecord {
kind: node.kind().to_string(),
name: text(name_node, src),
start_byte,
end_byte,
start_row: start_point.row,
start_col,
end_row: end_point.row,
end_col,
});
}
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
walk_spans(child, src, out);
}
}
pub fn spans_json(records: &[SpanRecord]) -> String {
if records.is_empty() {
return "[]".to_string();
}
let mut out = String::from("[\n");
let last = records.len() - 1;
for (i, r) in records.iter().enumerate() {
out.push_str(" {\n");
out.push_str(&format!(" \"kind\": {},\n", json_string(&r.kind)));
out.push_str(&format!(" \"name\": {},\n", json_string(&r.name)));
out.push_str(&format!(" \"start_byte\": {},\n", r.start_byte));
out.push_str(&format!(" \"end_byte\": {},\n", r.end_byte));
out.push_str(&format!(" \"start_row\": {},\n", r.start_row));
out.push_str(&format!(" \"start_col\": {},\n", r.start_col));
out.push_str(&format!(" \"end_row\": {},\n", r.end_row));
out.push_str(&format!(" \"end_col\": {}\n", r.end_col));
out.push_str(if i == last { " }\n" } else { " },\n" });
}
out.push(']');
out
}
fn json_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TsGrammar {
Typescript,
Tsx,
Javascript,
}
pub fn ts_grammar_for(rel: &str) -> Option<TsGrammar> {
let dot = rel.rfind('.')?;
match &rel[dot..] {
".ts" => Some(TsGrammar::Typescript),
".tsx" => Some(TsGrammar::Tsx),
".js" | ".jsx" => Some(TsGrammar::Javascript),
_ => None,
}
}
pub fn is_ts_js(rel: &str) -> bool {
ts_grammar_for(rel).is_some()
}
fn new_ts_parser(grammar: TsGrammar) -> Parser {
let mut parser = Parser::new();
let language = match grammar {
TsGrammar::Typescript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
TsGrammar::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
TsGrammar::Javascript => tree_sitter_javascript::LANGUAGE.into(),
};
parser
.set_language(&language)
.expect("failed to load TS/JS grammar");
parser
}
pub fn parse_ts_js(units: &[u16], grammar: TsGrammar) -> Option<tree_sitter::Tree> {
new_ts_parser(grammar).parse_utf16_le(units, None)
}
#[cfg(test)]
mod ts_grammar_tests {
use super::*;
#[test]
fn extension_maps_to_the_rt1_pinned_grammar() {
assert_eq!(ts_grammar_for("a/b.ts"), Some(TsGrammar::Typescript));
assert_eq!(ts_grammar_for("a/b.tsx"), Some(TsGrammar::Tsx));
assert_eq!(ts_grammar_for("a/b.js"), Some(TsGrammar::Javascript));
assert_eq!(ts_grammar_for("a/b.jsx"), Some(TsGrammar::Javascript));
assert_eq!(ts_grammar_for("a/b.cs"), None);
assert_eq!(ts_grammar_for("a/b"), None);
}
#[test]
fn is_ts_js_true_only_for_the_four_extensions() {
assert!(is_ts_js("x.ts"));
assert!(is_ts_js("x.tsx"));
assert!(is_ts_js("x.js"));
assert!(is_ts_js("x.jsx"));
assert!(!is_ts_js("x.cs"));
assert!(!is_ts_js("x.json"));
}
#[test]
fn each_grammar_loads_and_parses_without_panicking() {
assert!(parse_ts_js(
&utf16_units("const x: number = 1;\n"),
TsGrammar::Typescript
)
.is_some());
assert!(parse_ts_js(&utf16_units("const x = <div>hi</div>;\n"), TsGrammar::Tsx).is_some());
assert!(parse_ts_js(
&utf16_units("function f() { return 1; }\n"),
TsGrammar::Javascript
)
.is_some());
}
}