use std::path::Path;
use anyhow::Result;
use tree_sitter::{Language, Node};
use super::{Entity, FileInsight, ImportStmt, KindRule, LanguageProcessor, SharedProcessor};
pub struct JavaScriptProcessor;
impl JavaScriptProcessor {
pub fn new() -> Result<Self> {
Ok(Self)
}
}
const KINDS: &[KindRule] = &[
KindRule::plain("class_declaration", "class"),
KindRule::with_sig("function_declaration", "function", '{'),
KindRule::with_sig("method_definition", "function", '{'),
];
impl SharedProcessor for JavaScriptProcessor {
fn language() -> &'static str { "JavaScript" }
fn grammar() -> Language { tree_sitter_javascript::LANGUAGE.into() }
fn kinds() -> &'static [KindRule] {
KINDS
}
fn handle_special(node: Node, bytes: &[u8], entities: &mut Vec<Entity>, imports: &mut Vec<ImportStmt>) {
match node.kind() {
"variable_declarator" => {
let name = node.child_by_field_name("name").and_then(|n| n.utf8_text(bytes).ok());
if let Some(name) = name {
let is_arrow = node.child_by_field_name("value")
.map(|v| v.kind() == "arrow_function").unwrap_or(false);
let kind = if is_arrow { "function" } else { "variable" };
entities.push(Entity {
name: name.to_string(), kind: kind.to_string(),
line_start: node.start_position().row + 1,
line_end: node.end_position().row + 1,
doc_comment: None, signature: None, visibility: None,
});
}
}
"import_statement" => {
if let Some(src) = node.child_by_field_name("source").and_then(|n| n.utf8_text(bytes).ok()) {
imports.push(ImportStmt {
source: src.trim_matches(&['"', '\''][..]).to_string(),
alias: None,
line: node.start_position().row + 1,
});
}
}
"export_statement" => {
if let Some(src) = node.child_by_field_name("source").and_then(|n| n.utf8_text(bytes).ok()) {
imports.push(ImportStmt {
source: src.trim_matches(&['"', '\''][..]).to_string(),
alias: None,
line: node.start_position().row + 1,
});
}
}
_ => {}
}
}
fn fallback(source: &str) -> (Vec<Entity>, Vec<ImportStmt>) {
let mut entities = Vec::new();
let mut imports = Vec::new();
for (i, line) in source.lines().enumerate() {
let line_no = i + 1;
let t = line.trim();
if let Some(rest) = t.strip_prefix("import ") {
if let Some(from_pos) = rest.find(" from ") {
let src = rest[from_pos + 6..].trim().trim_matches(&['"', '\'', ';'][..]);
imports.push(ImportStmt { source: src.to_string(), alias: None, line: line_no });
}
continue;
}
if let Some(rest) = t.strip_prefix("export ")
&& let Some(from_pos) = rest.find(" from ")
{
let src = rest[from_pos + 6..].trim().trim_matches(&['"', '\'', ';'][..]);
imports.push(ImportStmt { source: src.to_string(), alias: None, line: line_no });
}
let core = t.strip_prefix("export ")
.or_else(|| t.strip_prefix("export default "))
.or_else(|| t.strip_prefix("export async "))
.or_else(|| t.strip_prefix("async "))
.unwrap_or(t);
let core = core.trim();
if let Some(name) = core.strip_prefix("class ")
.and_then(|s| s.split(&['{', ' ', '<', '(', ';', '}'][..]).next()).map(|s| s.trim())
{
entities.push(Entity {
name: name.to_string(), kind: "class".into(),
line_start: line_no, line_end: line_no,
doc_comment: None, signature: None, visibility: None,
});
} else if let Some(name) = core.strip_prefix("function ")
.and_then(|s| s.split(&['(', ' ', '<', '{', ';', '}'][..]).next()).map(|s| s.trim())
{
entities.push(Entity {
name: name.to_string(), kind: "function".into(),
line_start: line_no, line_end: line_no,
doc_comment: None, signature: Some(t.to_string()), visibility: None,
});
} else if let Some(name) = core.strip_prefix("const ")
.or_else(|| core.strip_prefix("let "))
.or_else(|| core.strip_prefix("var "))
.and_then(|s| s.split(&['=', ':', ' ', ';'][..]).next())
.map(|s| s.trim())
{
let is_arrow = core.contains("=>");
let kind = if is_arrow { "function" } else { "variable" };
entities.push(Entity {
name: name.to_string(), kind: kind.into(),
line_start: line_no, line_end: line_no,
doc_comment: None, signature: Some(t.to_string()), visibility: None,
});
}
}
(entities, imports)
}
}
impl LanguageProcessor for JavaScriptProcessor {
fn name(&self) -> &'static str { Self::language() }
fn extensions(&self) -> &[&str] { &[".js", ".jsx", ".mjs", ".cjs"] }
fn parse(&self, source: &str, path: &Path) -> Result<FileInsight> {
Self::parse_file(source, path)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_js_basics() {
let source = r#"import { useState } from "react";
import "./style.css";
function greet(name) { return "hello " + name; }
class MyClass { constructor() {} }
const helper = () => 42;
let x = 1;
"#;
let proc = JavaScriptProcessor::new().unwrap();
let result = proc.parse(source, Path::new("test.js")).unwrap();
assert_eq!(result.entities.len(), 5);
assert!(result.entities.iter().any(|e| e.name == "greet"));
assert!(result.entities.iter().any(|e| e.name == "MyClass"));
assert!(result.entities.iter().any(|e| e.name == "constructor"));
assert!(result.entities.iter().any(|e| e.name == "helper"));
assert!(result.entities.iter().any(|e| e.name == "x"));
assert_eq!(result.imports[0].source, "react");
assert_eq!(result.imports[1].source, "./style.css");
}
#[test]
fn test_parse_js_arrow_function_is_function_kind() {
let source = r#"const add = (a, b) => a + b;
const greet = name => `hello ${name}`;
"#;
let proc = JavaScriptProcessor::new().unwrap();
let result = proc.parse(source, Path::new("test.js")).unwrap();
let add = result.entities.iter().find(|e| e.name == "add").unwrap();
assert_eq!(add.kind, "function");
let greet = result.entities.iter().find(|e| e.name == "greet").unwrap();
assert_eq!(greet.kind, "function");
}
#[test]
fn test_parse_js_export_reexport() {
let source = r#"export function sum(a, b) { return a + b; }
export { Component } from "react";
export class Button {}
"#;
let proc = JavaScriptProcessor::new().unwrap();
let result = proc.parse(source, Path::new("test.js")).unwrap();
assert!(result.entities.iter().any(|e| e.name == "sum"));
assert!(result.entities.iter().any(|e| e.name == "Button"));
assert!(result.imports.iter().any(|i| i.source == "react"));
}
}