use crate::schema::{
GenericsMatch, MatchAttrs, NameMatcher, NameMatcherDetailed, Query, QueryKind, ReceiverKind,
Scope, Visibility,
};
use ryo_analysis::{DiscoveryQuery, Pattern, SymbolKind, TypeFilter};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConvertError {
#[error("invalid regex pattern: {0}")]
InvalidRegex(String),
#[error("unsupported query kind for conversion: {kind:?}")]
UnsupportedKind {
kind: QueryKind,
},
#[error("Pattern query requires 'name' field")]
PatternNameRequired,
#[error("Or/And query requires 'queries' field")]
QueriesRequired,
}
pub struct QueryConverter;
impl QueryConverter {
pub fn to_discovery_query(query: &Query) -> Result<ConversionResult, ConvertError> {
match query.kind {
QueryKind::Or | QueryKind::And => {
if query.queries.is_empty() {
return Err(ConvertError::QueriesRequired);
}
let sub_results: Result<Vec<_>, _> =
query.queries.iter().map(Self::to_discovery_query).collect();
Ok(ConversionResult {
discovery_query: None,
composite: Some(CompositeQuery {
op: if query.kind == QueryKind::Or {
CompositeOp::Or
} else {
CompositeOp::And
},
queries: sub_results?,
}),
post_filters: vec![],
unsupported: vec![],
})
}
QueryKind::Pattern => {
let name = query
.name
.as_ref()
.ok_or(ConvertError::PatternNameRequired)?;
Ok(ConversionResult {
discovery_query: Some(DiscoveryQuery::exact(name)),
composite: None,
post_filters: vec![PostFilter::PatternSearch(name.clone())],
unsupported: vec!["Pattern search (requires pattern registry)".to_string()],
})
}
_ => Self::convert_simple_query(query),
}
}
fn convert_simple_query(query: &Query) -> Result<ConversionResult, ConvertError> {
let mut unsupported = Vec::new();
let mut post_filters = Vec::new();
let pattern = Self::convert_name_matcher(query.r#match.as_ref())?;
let mut dq = match pattern {
Some(p) => DiscoveryQuery::symbol(p.as_str()),
None => DiscoveryQuery::symbol("*"),
};
if let Some(kinds) = Self::convert_kind(query.kind) {
dq = dq.kinds(kinds);
}
if let Some(ref scope) = query.scope {
dq = Self::apply_scope(dq, scope, &mut post_filters, &mut unsupported);
}
if let Some(limit) = query.limit {
dq = dq.limit(limit);
}
if !query.inner.is_empty() {
Self::collect_inner_post_filters(&query.inner, &mut post_filters)?;
}
let mut type_filter: Option<TypeFilter> = None;
if let Some(ref attrs) = query.r#match {
if let Some(ref generics) = attrs.generics {
if let Some(ref bounds) = generics.bounds {
if let Some(bound_pattern) = Self::convert_bounds_to_pattern(bounds)? {
let filter = type_filter.get_or_insert_with(TypeFilter::default);
filter.has_bound = Some(bound_pattern);
}
}
}
}
if let Some(filter) = type_filter {
dq = dq.with_type_filter(filter);
}
if let Some(ref attrs) = query.r#match {
Self::collect_post_filters(attrs, &mut post_filters, &mut unsupported);
}
if let Some(ref body) = query.body {
post_filters.push(PostFilter::BodyMatch(body.clone()));
}
if let Some(ref relations) = query.relations {
if !relations.is_empty() {
post_filters.push(PostFilter::Relations(relations.clone()));
}
}
Ok(ConversionResult {
discovery_query: Some(dq),
composite: None,
post_filters,
unsupported,
})
}
fn convert_name_matcher(attrs: Option<&MatchAttrs>) -> Result<Option<Pattern>, ConvertError> {
let attrs = match attrs {
Some(a) => a,
None => return Ok(None),
};
if let Some(ref name) = attrs.name {
let pattern = match name {
NameMatcher::Exact(s) => Pattern::exact(s),
NameMatcher::Detailed(d) => Self::convert_detailed_matcher(d)?,
};
return Ok(Some(pattern));
}
if let Some(ref pattern_str) = attrs.pattern {
return Ok(Some(Pattern::glob(pattern_str)));
}
Ok(None)
}
fn convert_detailed_matcher(d: &NameMatcherDetailed) -> Result<Pattern, ConvertError> {
if let Some(ref regex) = d.regex {
return Pattern::regex(regex).map_err(|e| ConvertError::InvalidRegex(e.to_string()));
}
if let Some(ref glob) = d.glob {
return Ok(Pattern::glob(glob));
}
if let Some(ref contains) = d.contains {
return Ok(Pattern::glob(format!("*{}*", contains)));
}
if let Some(ref starts) = d.starts_with {
return Ok(Pattern::glob(format!("{}*", starts)));
}
if let Some(ref ends) = d.ends_with {
return Ok(Pattern::glob(format!("*{}", ends)));
}
Ok(Pattern::glob("*"))
}
fn convert_kind(kind: QueryKind) -> Option<Vec<SymbolKind>> {
match kind {
QueryKind::Any => None,
QueryKind::Function => Some(vec![SymbolKind::Function]),
QueryKind::Struct => Some(vec![SymbolKind::Struct]),
QueryKind::Enum => Some(vec![SymbolKind::Enum]),
QueryKind::Trait => Some(vec![SymbolKind::Trait]),
QueryKind::Impl => Some(vec![SymbolKind::Impl]),
QueryKind::Mod => Some(vec![SymbolKind::Mod]),
QueryKind::Const => Some(vec![SymbolKind::Const]),
QueryKind::Static => Some(vec![SymbolKind::Static]),
QueryKind::TypeAlias => Some(vec![SymbolKind::TypeAlias]),
QueryKind::ReturnType
| QueryKind::Parameter
| QueryKind::Field
| QueryKind::Variant
| QueryKind::Or
| QueryKind::And
| QueryKind::Pattern => None,
QueryKind::Literal => None,
}
}
fn apply_scope(
mut dq: DiscoveryQuery,
scope: &Scope,
post_filters: &mut Vec<PostFilter>,
_unsupported: &mut Vec<String>,
) -> DiscoveryQuery {
if let Some(ref module) = scope.module {
dq = dq.in_module(module);
}
if let Some(ref path) = scope.path {
post_filters.push(PostFilter::PathInclude(path.clone()));
}
if let Some(ref exclude) = scope.exclude_path {
post_filters.push(PostFilter::PathExclude(exclude.clone()));
}
dq
}
fn convert_bounds_to_pattern(bounds: &[NameMatcher]) -> Result<Option<Pattern>, ConvertError> {
if bounds.is_empty() {
return Ok(None);
}
let first = &bounds[0];
let pattern = match first {
NameMatcher::Exact(s) => Pattern::exact(s),
NameMatcher::Detailed(d) => Self::convert_detailed_matcher(d)?,
};
Ok(Some(pattern))
}
fn collect_inner_post_filters(
inner: &[Query],
post_filters: &mut Vec<PostFilter>,
) -> Result<(), ConvertError> {
for q in inner {
let pattern_str = if let Some(ref attrs) = q.r#match {
if let Some(ref name) = attrs.name {
Self::name_matcher_to_pattern_str(name)
} else {
continue;
}
} else {
continue;
};
match q.kind {
QueryKind::ReturnType => {
post_filters.push(PostFilter::ReturnType(pattern_str));
}
QueryKind::Parameter => {
post_filters.push(PostFilter::ParamType(pattern_str));
}
QueryKind::Field => {
post_filters.push(PostFilter::FieldType(pattern_str));
}
QueryKind::Variant => {
post_filters.push(PostFilter::FieldType(pattern_str));
}
_ => {
}
}
}
Ok(())
}
fn name_matcher_to_pattern_str(name: &NameMatcher) -> String {
match name {
NameMatcher::Exact(s) => s.clone(),
NameMatcher::Detailed(d) => {
if let Some(ref glob) = d.glob {
glob.clone()
} else if let Some(ref regex) = d.regex {
format!("regex:{}", regex)
} else if let Some(ref contains) = d.contains {
format!("*{}*", contains)
} else if let Some(ref starts_with) = d.starts_with {
format!("{}*", starts_with)
} else if let Some(ref ends_with) = d.ends_with {
format!("*{}", ends_with)
} else {
"*".to_string()
}
}
}
}
#[allow(dead_code)]
fn convert_inner_to_type_filter(inner: &[Query]) -> Result<Option<TypeFilter>, ConvertError> {
let mut filter = TypeFilter::default();
let mut has_filter = false;
for q in inner {
let pattern = Self::convert_name_matcher(q.r#match.as_ref())?;
let Some(pattern) = pattern else {
continue;
};
match q.kind {
QueryKind::ReturnType => {
filter.return_type = Some(pattern);
has_filter = true;
}
QueryKind::Parameter => {
filter.param_type = Some(pattern);
has_filter = true;
}
QueryKind::Field => {
filter.field_type = Some(pattern);
has_filter = true;
}
QueryKind::Variant => {
filter.field_type = Some(pattern);
has_filter = true;
}
_ => {
}
}
}
if has_filter {
Ok(Some(filter))
} else {
Ok(None)
}
}
fn collect_post_filters(
attrs: &MatchAttrs,
post_filters: &mut Vec<PostFilter>,
unsupported: &mut Vec<String>,
) {
if let Some(ref sid) = attrs.symbol_id {
post_filters.push(PostFilter::SymbolId(sid.clone()));
}
if let Some(is_async) = attrs.is_async {
post_filters.push(PostFilter::IsAsync(is_async));
unsupported.push(format!("is_async: {}", is_async));
}
if let Some(is_unsafe) = attrs.is_unsafe {
post_filters.push(PostFilter::IsUnsafe(is_unsafe));
unsupported.push(format!("is_unsafe: {}", is_unsafe));
}
if let Some(ref vis) = attrs.vis {
post_filters.push(PostFilter::Visibility(vis.clone()));
unsupported.push(format!("visibility: {:?}", vis));
}
if let Some(ref receiver) = attrs.receiver {
post_filters.push(PostFilter::Receiver(*receiver));
unsupported.push(format!("receiver: {:?}", receiver));
}
if let Some(ref attributes) = attrs.attributes {
post_filters.push(PostFilter::Attributes(attributes.clone()));
unsupported.push(format!("attributes: {:?}", attributes));
}
if let Some(ref generics) = attrs.generics {
let has_params = generics.params.as_ref().is_some_and(|p| !p.is_empty());
let has_lifetimes = generics.lifetimes.as_ref().is_some_and(|l| !l.is_empty());
if has_params || has_lifetimes {
let filtered_generics = GenericsMatch {
params: generics.params.clone(),
bounds: None, lifetimes: generics.lifetimes.clone(),
};
post_filters.push(PostFilter::Generics(filtered_generics));
unsupported.push(format!(
"generics (params/lifetimes): params={:?}, lifetimes={:?}",
generics.params, generics.lifetimes
));
}
}
if attrs.on_empty.is_some() {
post_filters.push(PostFilter::OnEmpty);
}
if let Some(ref parent) = attrs.parent {
let pattern = match parent {
NameMatcher::Exact(s) => Pattern::exact(s),
NameMatcher::Detailed(d) => {
if let Some(ref regex) = d.regex {
Pattern::regex(regex).unwrap_or_else(|_| Pattern::glob(regex))
} else if let Some(ref glob) = d.glob {
Pattern::glob(glob)
} else if let Some(ref contains) = d.contains {
Pattern::glob(format!("*{}*", contains))
} else if let Some(ref starts) = d.starts_with {
Pattern::glob(format!("{}*", starts))
} else if let Some(ref ends) = d.ends_with {
Pattern::glob(format!("*{}", ends))
} else {
Pattern::glob("*")
}
}
};
post_filters.push(PostFilter::Parent(pattern));
}
}
}
#[derive(Debug, Clone)]
pub struct ConversionResult {
pub discovery_query: Option<DiscoveryQuery>,
pub composite: Option<CompositeQuery>,
pub post_filters: Vec<PostFilter>,
pub unsupported: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CompositeQuery {
pub op: CompositeOp,
pub queries: Vec<ConversionResult>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompositeOp {
Or,
And,
}
#[derive(Debug, Clone)]
pub enum PostFilter {
IsAsync(bool),
IsUnsafe(bool),
Visibility(Visibility),
Receiver(ReceiverKind),
Attributes(Vec<String>),
Generics(GenericsMatch),
PathInclude(String),
PathExclude(String),
PatternSearch(String),
OnEmpty,
ReturnType(String),
ParamType(String),
FieldType(String),
Parent(Pattern),
SymbolId(String),
BodyMatch(ryo_pattern::BodyMatch),
Relations(ryo_pattern::Relations),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::QueryParser;
#[test]
fn test_convert_simple_function_query() {
let yaml = r#"
kind: Function
match:
name: "process"
"#;
let query = QueryParser::from_yaml(yaml).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
assert!(result.discovery_query.is_some());
assert!(result.composite.is_none());
}
#[test]
fn test_convert_glob_pattern() {
let yaml = r#"
kind: Struct
match:
name: { glob: "*Config" }
"#;
let query = QueryParser::from_yaml(yaml).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
let dq = result.discovery_query.unwrap();
assert!(dq.pattern.matches("AppConfig"));
assert!(dq.pattern.matches("Config"));
assert!(!dq.pattern.matches("ConfigManager"));
}
#[test]
fn test_convert_contains_to_glob() {
let yaml = r#"
kind: Function
match:
name: { contains: "process" }
"#;
let query = QueryParser::from_yaml(yaml).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
let dq = result.discovery_query.unwrap();
assert!(dq.pattern.matches("process"));
assert!(dq.pattern.matches("process_event"));
assert!(dq.pattern.matches("do_process"));
}
#[test]
fn test_convert_with_post_filters() {
let yaml = r#"
kind: Function
match:
name: "handler"
is_async: true
vis: Public
"#;
let query = QueryParser::from_yaml(yaml).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
assert!(result
.post_filters
.iter()
.any(|f| matches!(f, PostFilter::IsAsync(true))));
assert!(result
.post_filters
.iter()
.any(|f| matches!(f, PostFilter::Visibility(_))));
assert!(!result.unsupported.is_empty());
}
#[test]
fn test_convert_or_query() {
let yaml = r#"
kind: Or
queries:
- kind: Struct
match:
name: { contains: "Error" }
- kind: Enum
match:
name: { contains: "Error" }
"#;
let query = QueryParser::from_yaml(yaml).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
assert!(result.discovery_query.is_none());
assert!(result.composite.is_some());
let composite = result.composite.unwrap();
assert_eq!(composite.op, CompositeOp::Or);
assert_eq!(composite.queries.len(), 2);
}
#[test]
fn test_convert_with_scope() {
let yaml = r#"
kind: Function
match:
name: "*"
scope:
module: "handlers"
path: "src/**"
"#;
let query = QueryParser::from_yaml(yaml).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
let dq = result.discovery_query.unwrap();
assert_eq!(dq.in_module, Some("handlers".to_string()));
assert!(result
.post_filters
.iter()
.any(|f| matches!(f, PostFilter::PathInclude(_))));
}
#[test]
fn test_convert_with_inner() {
let yaml = r#"
kind: Function
match:
name: { starts_with: "process_" }
inner:
- kind: ReturnType
match:
name: "Result"
"#;
let query = QueryParser::from_yaml(yaml).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
assert!(result
.post_filters
.iter()
.any(|f| matches!(f, PostFilter::ReturnType(_))));
}
#[test]
fn test_convert_with_multiple_inner() {
let yaml = r#"
kind: Function
match:
name: "*"
inner:
- kind: ReturnType
match:
name: { contains: "Result" }
- kind: Parameter
match:
name: { contains: "Config" }
"#;
let query = QueryParser::from_yaml(yaml).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
assert!(result
.post_filters
.iter()
.any(|f| matches!(f, PostFilter::ReturnType(_))));
assert!(result
.post_filters
.iter()
.any(|f| matches!(f, PostFilter::ParamType(_))));
}
#[test]
fn test_convert_field_inner() {
let yaml = r#"
kind: Struct
match:
name: "*"
inner:
- kind: Field
match:
name: "String"
"#;
let query = QueryParser::from_yaml(yaml).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
assert!(result
.post_filters
.iter()
.any(|f| matches!(f, PostFilter::FieldType(_))));
}
#[test]
fn test_convert_kind_function_excludes_method() {
let kinds = QueryConverter::convert_kind(QueryKind::Function).unwrap();
assert_eq!(kinds, vec![SymbolKind::Function]);
assert!(!kinds.contains(&SymbolKind::Method));
}
#[test]
fn test_convert_kind_struct_single() {
let kinds = QueryConverter::convert_kind(QueryKind::Struct).unwrap();
assert_eq!(kinds, vec![SymbolKind::Struct]);
}
#[test]
fn test_convert_generics_bounds_to_type_filter() {
let yaml = r#"
kind: Function
match:
name: "*"
generics:
bounds: ["Clone"]
"#;
let query = QueryParser::from_yaml(yaml).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
let dq = result.discovery_query.unwrap();
assert!(dq.type_filter.is_some());
let type_filter = dq.type_filter.unwrap();
assert!(type_filter.has_bound.is_some());
assert!(type_filter.has_bound.unwrap().matches("Clone"));
}
#[test]
fn test_convert_generics_params_to_post_filter() {
let yaml = r#"
kind: Function
match:
name: "*"
generics:
params: ["T", "E"]
lifetimes: ["'a"]
"#;
let query = QueryParser::from_yaml(yaml).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
assert!(result.post_filters.iter().any(|f| {
matches!(f, PostFilter::Generics(g) if g.params.is_some() && g.bounds.is_none())
}));
}
#[test]
fn test_convert_generics_mixed_bounds_and_params() {
let yaml = r#"
kind: Struct
match:
name: "*"
generics:
params: ["T"]
bounds: [{ glob: "*Send*" }]
"#;
let query = QueryParser::from_yaml(yaml).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
let dq = result.discovery_query.unwrap();
let type_filter = dq.type_filter.unwrap();
assert!(type_filter.has_bound.is_some());
assert!(type_filter.has_bound.unwrap().matches("MySendTrait"));
assert!(result.post_filters.iter().any(|f| {
matches!(f, PostFilter::Generics(g) if g.params.is_some() && g.bounds.is_none())
}));
}
#[test]
fn test_convert_symbol_id_filter() {
let json = r#"{"kind":"Function","match":{"symbol_id":"2421v1"}}"#;
let query = QueryParser::from_json(json).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
assert!(
result
.post_filters
.iter()
.any(|f| matches!(f, PostFilter::SymbolId(ref s) if s == "2421v1")),
"symbol_id must be converted to PostFilter::SymbolId"
);
}
#[test]
fn test_convert_symbol_id_with_prefix() {
let json = r#"{"kind":"Any","match":{"symbol_id":"SymbolId(165v1)"}}"#;
let query = QueryParser::from_json(json).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
assert!(
result
.post_filters
.iter()
.any(|f| matches!(f, PostFilter::SymbolId(ref s) if s == "SymbolId(165v1)")),
"SymbolId(xxx) format must also be preserved in PostFilter"
);
}
#[test]
fn test_convert_body_contains_filter() {
let json = r#"{
"kind": "Function",
"body": {
"contains": [
{"node": "MethodCall"}
]
}
}"#;
let query = QueryParser::from_json(json).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
assert!(
result
.post_filters
.iter()
.any(|f| matches!(f, PostFilter::BodyMatch(_))),
"body.contains must be converted to PostFilter::BodyMatch"
);
}
#[test]
fn test_convert_body_not_contains_filter() {
let json = r#"{
"kind": "Function",
"body": {
"not_contains": [
{"node": "MethodCall", "children": {"method": {"name": "unwrap"}}}
]
}
}"#;
let query = QueryParser::from_json(json).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
assert!(
result
.post_filters
.iter()
.any(|f| matches!(f, PostFilter::BodyMatch(bm) if bm.not_contains.is_some())),
"body.not_contains must be preserved in PostFilter::BodyMatch"
);
}
#[test]
fn test_convert_relations_any_filter() {
let json = r#"{
"kind": "Function",
"relations": {
"any": [
{"kind": "Calls", "target": {"kind": "Function", "match": {"name": "serve"}}}
]
}
}"#;
let query = QueryParser::from_json(json).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
assert!(
result
.post_filters
.iter()
.any(|f| matches!(f, PostFilter::Relations(r) if r.any.is_some())),
"relations.any must be converted to PostFilter::Relations"
);
}
#[test]
fn test_convert_relations_none_filter() {
let json = r#"{
"kind": "Trait",
"relations": {
"none": [
{"kind": "ImplementedBy", "target": {"kind": "Struct", "match": {"name": "Router"}}}
]
}
}"#;
let query = QueryParser::from_json(json).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
assert!(
result
.post_filters
.iter()
.any(|f| matches!(f, PostFilter::Relations(r) if r.none.is_some())),
"relations.none must be converted to PostFilter::Relations"
);
}
#[test]
fn test_convert_empty_relations_skipped() {
let json = r#"{"kind": "Function", "relations": {}}"#;
let query = QueryParser::from_json(json).unwrap();
let result = QueryConverter::to_discovery_query(&query).unwrap();
assert!(
!result
.post_filters
.iter()
.any(|f| matches!(f, PostFilter::Relations(_))),
"empty relations must not produce PostFilter"
);
}
}