use std::ops::Range;
use tree_sitter::Language;
use tree_sitter_tags::{TagsConfiguration, TagsContext};
use crate::error::ParseError;
#[derive(Debug, Clone)]
pub struct NamedScope {
pub node_range: Range<usize>,
pub name_range: Range<usize>,
pub name: String,
pub kind: ScopeKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScopeKind {
Function,
Method,
Class,
Module,
Interface,
Type,
Macro,
Other(String),
}
impl ScopeKind {
#[must_use]
pub fn from_suffix(s: &str) -> Self {
match s {
"function" => Self::Function,
"method" => Self::Method,
"class" => Self::Class,
"module" => Self::Module,
"interface" => Self::Interface,
"type" => Self::Type,
"macro" => Self::Macro,
other => Self::Other(other.to_owned()),
}
}
#[must_use]
pub fn as_suffix(&self) -> &str {
match self {
Self::Function => "function",
Self::Method => "method",
Self::Class => "class",
Self::Module => "module",
Self::Interface => "interface",
Self::Type => "type",
Self::Macro => "macro",
Self::Other(s) => s.as_str(),
}
}
}
pub struct ScopeDetector {
config: Option<TagsConfiguration>,
ctx: TagsContext,
}
impl std::fmt::Debug for ScopeDetector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ScopeDetector")
.field("has_config", &self.config.is_some())
.finish_non_exhaustive()
}
}
impl ScopeDetector {
pub fn new(
language: &Language,
base_query: Option<&str>,
project_override: Option<&str>,
) -> Result<Self, ParseError> {
let combined = match (base_query, project_override) {
(None, None) => {
return Ok(Self {
config: None,
ctx: TagsContext::new(),
});
}
(Some(base), None) => base.to_owned(),
(None, Some(ov)) => ov.to_owned(),
(Some(base), Some(ov)) => format!("{ov}\n{base}"),
};
let config = TagsConfiguration::new(language.clone(), &combined, "").map_err(|e| {
ParseError::ScopeQueryCompile {
reason: e.to_string(),
}
})?;
Ok(Self {
config: Some(config),
ctx: TagsContext::new(),
})
}
#[must_use]
pub const fn has_query(&self) -> bool {
self.config.is_some()
}
#[must_use]
pub fn scopes(&mut self, source: &[u8]) -> Vec<NamedScope> {
let Some(config) = self.config.as_ref() else {
return Vec::new();
};
let Ok((iter, _had_parse_error)) = self.ctx.generate_tags(config, source, None) else {
return Vec::new();
};
let mut scopes = Vec::new();
for tag_result in iter {
let Ok(tag) = tag_result else { continue };
if !tag.is_definition {
continue;
}
let syntax = config.syntax_type_name(tag.syntax_type_id);
let kind = ScopeKind::from_suffix(syntax);
let Some(name_bytes) = source.get(tag.name_range.clone()) else {
continue;
};
let Ok(name) = std::str::from_utf8(name_bytes) else {
continue;
};
scopes.push(NamedScope {
node_range: tag.range,
name_range: tag.name_range,
name: name.to_owned(),
kind,
});
}
scopes
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn no_query_is_empty() {
#[cfg(feature = "grammars")]
{
let lang = panproto_grammars::grammars()
.into_iter()
.find(|g| g.name == "rust")
.map(|g| g.language);
if let Some(lang) = lang {
let mut det = ScopeDetector::new(&lang, None, None).unwrap();
assert!(!det.has_query());
assert!(det.scopes(b"fn f() {}").is_empty());
}
}
}
#[test]
#[cfg(feature = "grammars")]
fn rust_function_item_is_detected() {
let grammar = panproto_grammars::grammars()
.into_iter()
.find(|g| g.name == "rust");
let Some(g) = grammar else {
return; };
let tags = g.tags_query;
if tags.is_none() {
return; }
let mut det = ScopeDetector::new(&g.language, tags, None).unwrap();
assert!(det.has_query());
let source = b"fn verify_push(token: &str) -> bool { true }\n\
struct Foo { x: u32 }\n";
let scopes = det.scopes(source);
let names: Vec<&str> = scopes.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"verify_push"), "got {names:?}");
assert!(names.contains(&"Foo"), "got {names:?}");
let fn_scope = scopes.iter().find(|s| s.name == "verify_push").unwrap();
assert_eq!(fn_scope.kind, ScopeKind::Function);
}
#[test]
#[cfg(feature = "grammars")]
fn rust_impl_method_is_detected_as_method() {
let Some(g) = panproto_grammars::grammars()
.into_iter()
.find(|g| g.name == "rust")
else {
return;
};
let Some(tags) = g.tags_query else {
return;
};
let mut det = ScopeDetector::new(&g.language, Some(tags), None).unwrap();
let source = b"impl Foo { fn bar(&self) {} }";
let scopes = det.scopes(source);
let bar = scopes.iter().find(|s| s.name == "bar");
assert!(bar.is_some(), "expected bar method, got {scopes:?}");
let k = &bar.unwrap().kind;
assert!(matches!(k, ScopeKind::Method | ScopeKind::Function));
}
}