use std::path::Path;
use std::sync::OnceLock;
use tree_sitter::{Language as TsLanguage, Query};
#[cfg(feature = "lang-ts")]
use tree_sitter_typescript::{LANGUAGE_TSX, LANGUAGE_TYPESCRIPT};
#[cfg(not(any(
feature = "lang-ts",
feature = "lang-js",
feature = "lang-py",
feature = "lang-go",
feature = "lang-scala",
feature = "lang-rust"
)))]
compile_error!(
"heal-observer requires at least one language feature: lang-ts / lang-js / lang-py / lang-go / lang-scala / lang-rust"
);
#[cfg(feature = "lang-ts")]
const TYPESCRIPT_FUNCTIONS_QUERY: &str = include_str!("../../queries/typescript/functions.scm");
#[cfg(feature = "lang-ts")]
const TYPESCRIPT_CCN_QUERY: &str = include_str!("../../queries/typescript/ccn.scm");
#[cfg(feature = "lang-ts")]
const TYPESCRIPT_COGNITIVE_QUERY: &str = include_str!("../../queries/typescript/cognitive.scm");
#[cfg(feature = "lang-ts")]
const TYPESCRIPT_LCOM_QUERY: &str = include_str!("../../queries/typescript/lcom.scm");
#[cfg(feature = "lang-js")]
const JAVASCRIPT_FUNCTIONS_QUERY: &str = include_str!("../../queries/javascript/functions.scm");
#[cfg(feature = "lang-js")]
const JAVASCRIPT_CCN_QUERY: &str = include_str!("../../queries/javascript/ccn.scm");
#[cfg(feature = "lang-js")]
const JAVASCRIPT_COGNITIVE_QUERY: &str = include_str!("../../queries/javascript/cognitive.scm");
#[cfg(feature = "lang-js")]
const JAVASCRIPT_LCOM_QUERY: &str = include_str!("../../queries/javascript/lcom.scm");
#[cfg(feature = "lang-py")]
const PYTHON_FUNCTIONS_QUERY: &str = include_str!("../../queries/python/functions.scm");
#[cfg(feature = "lang-py")]
const PYTHON_CCN_QUERY: &str = include_str!("../../queries/python/ccn.scm");
#[cfg(feature = "lang-py")]
const PYTHON_COGNITIVE_QUERY: &str = include_str!("../../queries/python/cognitive.scm");
#[cfg(feature = "lang-py")]
const PYTHON_LCOM_QUERY: &str = include_str!("../../queries/python/lcom.scm");
#[cfg(feature = "lang-go")]
const GO_FUNCTIONS_QUERY: &str = include_str!("../../queries/go/functions.scm");
#[cfg(feature = "lang-go")]
const GO_CCN_QUERY: &str = include_str!("../../queries/go/ccn.scm");
#[cfg(feature = "lang-go")]
const GO_COGNITIVE_QUERY: &str = include_str!("../../queries/go/cognitive.scm");
#[cfg(feature = "lang-go")]
const GO_LCOM_QUERY: &str = include_str!("../../queries/go/lcom.scm");
#[cfg(feature = "lang-scala")]
const SCALA_FUNCTIONS_QUERY: &str = include_str!("../../queries/scala/functions.scm");
#[cfg(feature = "lang-scala")]
const SCALA_CCN_QUERY: &str = include_str!("../../queries/scala/ccn.scm");
#[cfg(feature = "lang-scala")]
const SCALA_COGNITIVE_QUERY: &str = include_str!("../../queries/scala/cognitive.scm");
#[cfg(feature = "lang-scala")]
const SCALA_LCOM_QUERY: &str = include_str!("../../queries/scala/lcom.scm");
#[cfg(feature = "lang-rust")]
const RUST_FUNCTIONS_QUERY: &str = include_str!("../../queries/rust/functions.scm");
#[cfg(feature = "lang-rust")]
const RUST_CCN_QUERY: &str = include_str!("../../queries/rust/ccn.scm");
#[cfg(feature = "lang-rust")]
const RUST_COGNITIVE_QUERY: &str = include_str!("../../queries/rust/cognitive.scm");
#[cfg(feature = "lang-rust")]
const RUST_LCOM_QUERY: &str = include_str!("../../queries/rust/lcom.scm");
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Language {
#[cfg(feature = "lang-ts")]
TypeScript,
#[cfg(feature = "lang-ts")]
Tsx,
#[cfg(feature = "lang-js")]
JavaScript,
#[cfg(feature = "lang-js")]
Jsx,
#[cfg(feature = "lang-py")]
Python,
#[cfg(feature = "lang-go")]
Go,
#[cfg(feature = "lang-scala")]
Scala,
#[cfg(feature = "lang-rust")]
Rust,
}
pub struct CompiledQuery<C: 'static> {
pub query: Query,
pub captures: C,
}
pub struct FunctionCaptures {
pub scope: u32,
}
pub struct CcnCaptures {
pub point: u32,
pub binary: u32,
}
pub struct CognitiveCaptures {
pub if_: u32,
pub else_: u32,
pub inc_and_nest: u32,
pub inc: u32,
pub binary: u32,
}
pub struct LcomCaptures {
pub class_scope: u32,
}
struct LanguageQueries {
functions: OnceLock<CompiledQuery<FunctionCaptures>>,
ccn: OnceLock<CompiledQuery<CcnCaptures>>,
cognitive: OnceLock<CompiledQuery<CognitiveCaptures>>,
lcom: OnceLock<CompiledQuery<LcomCaptures>>,
}
impl LanguageQueries {
const fn new() -> Self {
Self {
functions: OnceLock::new(),
ccn: OnceLock::new(),
cognitive: OnceLock::new(),
lcom: OnceLock::new(),
}
}
}
#[cfg(feature = "lang-ts")]
static TYPESCRIPT_QUERIES: LanguageQueries = LanguageQueries::new();
#[cfg(feature = "lang-ts")]
static TSX_QUERIES: LanguageQueries = LanguageQueries::new();
#[cfg(feature = "lang-js")]
static JAVASCRIPT_QUERIES: LanguageQueries = LanguageQueries::new();
#[cfg(feature = "lang-py")]
static PYTHON_QUERIES: LanguageQueries = LanguageQueries::new();
#[cfg(feature = "lang-go")]
static GO_QUERIES: LanguageQueries = LanguageQueries::new();
#[cfg(feature = "lang-scala")]
static SCALA_QUERIES: LanguageQueries = LanguageQueries::new();
#[cfg(feature = "lang-rust")]
static RUST_QUERIES: LanguageQueries = LanguageQueries::new();
impl Language {
#[must_use]
pub fn from_path(path: &Path) -> Option<Self> {
let ext = path.extension()?.to_str()?;
match ext {
#[cfg(feature = "lang-ts")]
"ts" | "mts" | "cts" => Some(Self::TypeScript),
#[cfg(feature = "lang-ts")]
"tsx" => Some(Self::Tsx),
#[cfg(feature = "lang-js")]
"js" | "mjs" | "cjs" => Some(Self::JavaScript),
#[cfg(feature = "lang-js")]
"jsx" => Some(Self::Jsx),
#[cfg(feature = "lang-py")]
"py" | "pyi" => Some(Self::Python),
#[cfg(feature = "lang-go")]
"go" => Some(Self::Go),
#[cfg(feature = "lang-scala")]
"scala" | "sc" => Some(Self::Scala),
#[cfg(feature = "lang-rust")]
"rs" => Some(Self::Rust),
_ => None,
}
}
#[must_use]
pub fn name(self) -> &'static str {
match self {
#[cfg(feature = "lang-ts")]
Self::TypeScript => "typescript",
#[cfg(feature = "lang-ts")]
Self::Tsx => "tsx",
#[cfg(feature = "lang-js")]
Self::JavaScript => "javascript",
#[cfg(feature = "lang-js")]
Self::Jsx => "jsx",
#[cfg(feature = "lang-py")]
Self::Python => "python",
#[cfg(feature = "lang-go")]
Self::Go => "go",
#[cfg(feature = "lang-scala")]
Self::Scala => "scala",
#[cfg(feature = "lang-rust")]
Self::Rust => "rust",
}
}
#[must_use]
pub fn supports_lcom(self) -> bool {
match self {
#[cfg(feature = "lang-ts")]
Self::TypeScript | Self::Tsx => true,
#[cfg(feature = "lang-js")]
Self::JavaScript | Self::Jsx => true,
#[cfg(feature = "lang-py")]
Self::Python => true,
#[cfg(feature = "lang-go")]
Self::Go => false,
#[cfg(feature = "lang-scala")]
Self::Scala => false,
#[cfg(feature = "lang-rust")]
Self::Rust => true,
}
}
#[must_use]
pub fn ts_language(self) -> TsLanguage {
match self {
#[cfg(feature = "lang-ts")]
Self::TypeScript => LANGUAGE_TYPESCRIPT.into(),
#[cfg(feature = "lang-ts")]
Self::Tsx => LANGUAGE_TSX.into(),
#[cfg(feature = "lang-js")]
Self::JavaScript | Self::Jsx => tree_sitter_javascript::LANGUAGE.into(),
#[cfg(feature = "lang-py")]
Self::Python => tree_sitter_python::LANGUAGE.into(),
#[cfg(feature = "lang-go")]
Self::Go => tree_sitter_go::LANGUAGE.into(),
#[cfg(feature = "lang-scala")]
Self::Scala => tree_sitter_scala::LANGUAGE.into(),
#[cfg(feature = "lang-rust")]
Self::Rust => tree_sitter_rust::LANGUAGE.into(),
}
}
#[must_use]
pub fn functions_query(self) -> &'static CompiledQuery<FunctionCaptures> {
self.cache().functions.get_or_init(|| {
let lang = self.ts_language();
let query = Query::new(&lang, self.functions_query_source())
.expect("functions.scm must compile");
let captures = FunctionCaptures {
scope: capture_index(&query, "function.scope"),
};
CompiledQuery { query, captures }
})
}
#[must_use]
pub fn ccn_query(self) -> &'static CompiledQuery<CcnCaptures> {
self.cache().ccn.get_or_init(|| {
let lang = self.ts_language();
let query = Query::new(&lang, self.ccn_query_source()).expect("ccn.scm must compile");
let captures = CcnCaptures {
point: capture_index(&query, "ccn.point"),
binary: capture_index(&query, "ccn.binary"),
};
CompiledQuery { query, captures }
})
}
#[must_use]
pub fn cognitive_query(self) -> &'static CompiledQuery<CognitiveCaptures> {
self.cache().cognitive.get_or_init(|| {
let lang = self.ts_language();
let query = Query::new(&lang, self.cognitive_query_source())
.expect("cognitive.scm must compile");
let captures = CognitiveCaptures {
if_: capture_index(&query, "if"),
else_: capture_index(&query, "else"),
inc_and_nest: capture_index(&query, "inc_and_nest"),
inc: capture_index(&query, "inc"),
binary: capture_index(&query, "binary"),
};
CompiledQuery { query, captures }
})
}
#[must_use]
pub fn lcom_query(self) -> &'static CompiledQuery<LcomCaptures> {
self.cache().lcom.get_or_init(|| {
let lang = self.ts_language();
let query = Query::new(&lang, self.lcom_query_source()).expect("lcom.scm must compile");
let captures = LcomCaptures {
class_scope: capture_index(&query, "class.scope"),
};
CompiledQuery { query, captures }
})
}
fn cache(self) -> &'static LanguageQueries {
match self {
#[cfg(feature = "lang-ts")]
Self::TypeScript => &TYPESCRIPT_QUERIES,
#[cfg(feature = "lang-ts")]
Self::Tsx => &TSX_QUERIES,
#[cfg(feature = "lang-js")]
Self::JavaScript | Self::Jsx => &JAVASCRIPT_QUERIES,
#[cfg(feature = "lang-py")]
Self::Python => &PYTHON_QUERIES,
#[cfg(feature = "lang-go")]
Self::Go => &GO_QUERIES,
#[cfg(feature = "lang-scala")]
Self::Scala => &SCALA_QUERIES,
#[cfg(feature = "lang-rust")]
Self::Rust => &RUST_QUERIES,
}
}
fn functions_query_source(self) -> &'static str {
match self {
#[cfg(feature = "lang-ts")]
Self::TypeScript | Self::Tsx => TYPESCRIPT_FUNCTIONS_QUERY,
#[cfg(feature = "lang-js")]
Self::JavaScript | Self::Jsx => JAVASCRIPT_FUNCTIONS_QUERY,
#[cfg(feature = "lang-py")]
Self::Python => PYTHON_FUNCTIONS_QUERY,
#[cfg(feature = "lang-go")]
Self::Go => GO_FUNCTIONS_QUERY,
#[cfg(feature = "lang-scala")]
Self::Scala => SCALA_FUNCTIONS_QUERY,
#[cfg(feature = "lang-rust")]
Self::Rust => RUST_FUNCTIONS_QUERY,
}
}
fn ccn_query_source(self) -> &'static str {
match self {
#[cfg(feature = "lang-ts")]
Self::TypeScript | Self::Tsx => TYPESCRIPT_CCN_QUERY,
#[cfg(feature = "lang-js")]
Self::JavaScript | Self::Jsx => JAVASCRIPT_CCN_QUERY,
#[cfg(feature = "lang-py")]
Self::Python => PYTHON_CCN_QUERY,
#[cfg(feature = "lang-go")]
Self::Go => GO_CCN_QUERY,
#[cfg(feature = "lang-scala")]
Self::Scala => SCALA_CCN_QUERY,
#[cfg(feature = "lang-rust")]
Self::Rust => RUST_CCN_QUERY,
}
}
fn cognitive_query_source(self) -> &'static str {
match self {
#[cfg(feature = "lang-ts")]
Self::TypeScript | Self::Tsx => TYPESCRIPT_COGNITIVE_QUERY,
#[cfg(feature = "lang-js")]
Self::JavaScript | Self::Jsx => JAVASCRIPT_COGNITIVE_QUERY,
#[cfg(feature = "lang-py")]
Self::Python => PYTHON_COGNITIVE_QUERY,
#[cfg(feature = "lang-go")]
Self::Go => GO_COGNITIVE_QUERY,
#[cfg(feature = "lang-scala")]
Self::Scala => SCALA_COGNITIVE_QUERY,
#[cfg(feature = "lang-rust")]
Self::Rust => RUST_COGNITIVE_QUERY,
}
}
fn lcom_query_source(self) -> &'static str {
match self {
#[cfg(feature = "lang-ts")]
Self::TypeScript | Self::Tsx => TYPESCRIPT_LCOM_QUERY,
#[cfg(feature = "lang-js")]
Self::JavaScript | Self::Jsx => JAVASCRIPT_LCOM_QUERY,
#[cfg(feature = "lang-py")]
Self::Python => PYTHON_LCOM_QUERY,
#[cfg(feature = "lang-go")]
Self::Go => GO_LCOM_QUERY,
#[cfg(feature = "lang-scala")]
Self::Scala => SCALA_LCOM_QUERY,
#[cfg(feature = "lang-rust")]
Self::Rust => RUST_LCOM_QUERY,
}
}
}
fn capture_index(query: &Query, name: &str) -> u32 {
query
.capture_index_for_name(name)
.unwrap_or_else(|| panic!("query missing @{name} capture"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[cfg(feature = "lang-ts")]
#[test]
fn dispatches_typescript_extensions() {
assert_eq!(
Language::from_path(&PathBuf::from("foo.ts")),
Some(Language::TypeScript)
);
assert_eq!(
Language::from_path(&PathBuf::from("nested/dir/foo.mts")),
Some(Language::TypeScript)
);
assert_eq!(
Language::from_path(&PathBuf::from("foo.cts")),
Some(Language::TypeScript)
);
}
#[cfg(feature = "lang-ts")]
#[test]
fn dispatches_tsx_extension() {
assert_eq!(
Language::from_path(&PathBuf::from("Component.tsx")),
Some(Language::Tsx)
);
}
#[cfg(feature = "lang-rust")]
#[test]
fn dispatches_rust_extension() {
assert_eq!(
Language::from_path(&PathBuf::from("crates/core/src/lib.rs")),
Some(Language::Rust)
);
}
#[test]
fn rejects_unsupported_extensions() {
assert_eq!(Language::from_path(&PathBuf::from("README.md")), None);
assert_eq!(Language::from_path(&PathBuf::from("Cargo.toml")), None);
}
#[cfg(feature = "lang-js")]
#[test]
fn dispatches_javascript_extensions() {
assert_eq!(
Language::from_path(&PathBuf::from("src/foo.js")),
Some(Language::JavaScript)
);
assert_eq!(
Language::from_path(&PathBuf::from("src/foo.mjs")),
Some(Language::JavaScript)
);
assert_eq!(
Language::from_path(&PathBuf::from("src/foo.cjs")),
Some(Language::JavaScript)
);
assert_eq!(
Language::from_path(&PathBuf::from("src/foo.jsx")),
Some(Language::Jsx)
);
}
#[test]
fn rejects_extensionless_paths() {
assert_eq!(Language::from_path(&PathBuf::from("Makefile")), None);
assert_eq!(Language::from_path(&PathBuf::from("")), None);
}
#[cfg(all(feature = "lang-ts", feature = "lang-rust"))]
#[test]
fn loads_grammars() {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&Language::TypeScript.ts_language())
.expect("typescript grammar loads");
parser
.set_language(&Language::Tsx.ts_language())
.expect("tsx grammar loads");
parser
.set_language(&Language::Rust.ts_language())
.expect("rust grammar loads");
}
#[cfg(all(feature = "lang-ts", feature = "lang-rust"))]
#[test]
fn cached_queries_compile_and_index() {
for lang in [Language::TypeScript, Language::Tsx, Language::Rust] {
let f = lang.functions_query();
assert!(f.query.pattern_count() > 0);
let _ = f.captures.scope;
let c = lang.ccn_query();
assert!(c.query.pattern_count() > 0);
let _ = (c.captures.point, c.captures.binary);
let g = lang.cognitive_query();
assert!(g.query.pattern_count() > 0);
let _ = (
g.captures.if_,
g.captures.else_,
g.captures.inc_and_nest,
g.captures.inc,
g.captures.binary,
);
}
}
#[cfg(feature = "lang-js")]
#[test]
fn javascript_queries_compile_and_grammar_loads() {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&Language::JavaScript.ts_language())
.expect("javascript grammar loads");
parser
.set_language(&Language::Jsx.ts_language())
.expect("jsx grammar loads");
for lang in [Language::JavaScript, Language::Jsx] {
assert!(lang.functions_query().query.pattern_count() > 0);
assert!(lang.ccn_query().query.pattern_count() > 0);
assert!(lang.cognitive_query().query.pattern_count() > 0);
assert!(lang.lcom_query().query.pattern_count() > 0);
}
}
#[cfg(feature = "lang-py")]
#[test]
fn dispatches_python_extensions() {
assert_eq!(
Language::from_path(&PathBuf::from("src/foo.py")),
Some(Language::Python)
);
assert_eq!(
Language::from_path(&PathBuf::from("stubs/foo.pyi")),
Some(Language::Python)
);
}
#[cfg(feature = "lang-py")]
#[test]
fn python_queries_compile_and_grammar_loads() {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&Language::Python.ts_language())
.expect("python grammar loads");
assert!(Language::Python.functions_query().query.pattern_count() > 0);
assert!(Language::Python.ccn_query().query.pattern_count() > 0);
assert!(Language::Python.cognitive_query().query.pattern_count() > 0);
assert!(Language::Python.lcom_query().query.pattern_count() > 0);
}
#[cfg(feature = "lang-go")]
#[test]
fn dispatches_go_extension() {
assert_eq!(
Language::from_path(&PathBuf::from("cmd/main.go")),
Some(Language::Go)
);
}
#[cfg(feature = "lang-go")]
#[test]
fn go_queries_compile_and_grammar_loads() {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&Language::Go.ts_language())
.expect("go grammar loads");
assert!(Language::Go.functions_query().query.pattern_count() > 0);
assert!(Language::Go.ccn_query().query.pattern_count() > 0);
assert!(Language::Go.cognitive_query().query.pattern_count() > 0);
assert!(Language::Go.lcom_query().query.pattern_count() > 0);
}
#[cfg(feature = "lang-scala")]
#[test]
fn dispatches_scala_extensions() {
assert_eq!(
Language::from_path(&PathBuf::from("src/main/scala/Foo.scala")),
Some(Language::Scala)
);
assert_eq!(
Language::from_path(&PathBuf::from("script.sc")),
Some(Language::Scala)
);
}
#[cfg(feature = "lang-scala")]
#[test]
fn scala_queries_compile_and_grammar_loads() {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&Language::Scala.ts_language())
.expect("scala grammar loads");
assert!(Language::Scala.functions_query().query.pattern_count() > 0);
assert!(Language::Scala.ccn_query().query.pattern_count() > 0);
assert!(Language::Scala.cognitive_query().query.pattern_count() > 0);
assert!(Language::Scala.lcom_query().query.pattern_count() > 0);
}
}