use std::collections::HashMap;
use super::types::{FieldDescriptor, FieldType, Operator};
#[derive(Debug, Clone)]
pub struct FieldRegistry {
fields: HashMap<String, FieldDescriptor>,
aliases: HashMap<String, String>,
}
impl FieldRegistry {
#[must_use]
pub fn new() -> Self {
Self {
fields: HashMap::new(),
aliases: HashMap::new(),
}
}
#[must_use]
pub fn with_core_fields() -> Self {
let mut registry = Self::new();
for field in core_fields() {
registry.add_field(field);
}
registry.add_alias("file", "path"); registry.add_alias("language", "lang");
registry
}
pub fn add_field(&mut self, descriptor: FieldDescriptor) {
self.fields.insert(descriptor.name.to_string(), descriptor);
}
pub fn add_alias(&mut self, alias: impl Into<String>, canonical: impl Into<String>) {
let alias = alias.into();
let canonical = canonical.into();
assert!(
self.fields.contains_key(&canonical),
"Cannot add alias '{alias}' -> '{canonical}': canonical field '{canonical}' does not exist"
);
self.aliases.insert(alias, canonical);
}
#[inline]
#[must_use]
pub fn get(&self, name: &str) -> Option<&FieldDescriptor> {
if let Some(descriptor) = self.fields.get(name) {
return Some(descriptor);
}
if let Some(canonical) = self.aliases.get(name) {
return self.fields.get(canonical);
}
None
}
pub fn resolve_canonical<'a>(&'a self, name: &'a str) -> Option<&'a str> {
if self.fields.contains_key(name) {
return Some(name);
}
self.aliases.get(name).map(std::string::String::as_str)
}
pub fn field_names(&self) -> Vec<&str> {
self.fields
.keys()
.map(std::string::String::as_str)
.collect()
}
#[inline]
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.fields.contains_key(name) || self.aliases.contains_key(name)
}
#[must_use]
pub fn len(&self) -> usize {
self.fields.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.fields.is_empty()
}
#[must_use = "collision information should be logged or handled"]
pub fn add_plugin_fields(&mut self, fields: &[FieldDescriptor]) -> Vec<String> {
let mut collisions = Vec::new();
for field in fields {
if self.contains(field.name) {
log::debug!(
"Plugin field collision: '{}' already exists, skipping plugin version",
field.name
);
collisions.push(field.name.to_string());
} else {
log::debug!("Registering plugin field: '{}'", field.name);
self.add_field(field.clone());
}
}
if collisions.is_empty() {
log::debug!(
"Plugin field registration complete: {} fields added, no collisions",
fields.len()
);
} else {
log::debug!(
"Plugin field registration complete: {} fields added, {} collisions ({})",
fields.len() - collisions.len(),
collisions.len(),
collisions.join(", ")
);
}
collisions
}
#[must_use]
pub fn plugin_ids(&self) -> Vec<String> {
Vec::new()
}
}
impl Default for FieldRegistry {
fn default() -> Self {
Self::with_core_fields()
}
}
#[must_use]
#[allow(clippy::too_many_lines)] pub fn core_fields() -> Vec<FieldDescriptor> {
vec![
FieldDescriptor {
name: "kind",
field_type: FieldType::Enum(vec![
"function",
"method",
"class",
"struct",
"trait",
"enum",
"interface",
"module",
"variable",
"constant",
"type",
"namespace",
"property",
"parameter",
"import",
]),
operators: &[Operator::Equal, Operator::Regex],
indexed: true,
doc: "Node type: function, class, method, struct, etc.",
},
FieldDescriptor {
name: "name",
field_type: FieldType::String,
operators: &[Operator::Equal, Operator::Regex],
indexed: true,
doc: "Node name (exact match with ':' or regex with '~=')",
},
FieldDescriptor {
name: "path",
field_type: FieldType::Path,
operators: &[Operator::Equal, Operator::Regex],
indexed: true,
doc: "File path (glob pattern with ':' or regex with '~=')",
},
FieldDescriptor {
name: "lang",
field_type: FieldType::String,
operators: &[Operator::Equal, Operator::Regex],
indexed: true,
doc: "Programming language",
},
FieldDescriptor {
name: "repo",
field_type: FieldType::String,
operators: &[Operator::Equal],
indexed: false,
doc: "Repository filter (glob pattern, e.g., 'repo:backend-*'). Used in workspace queries to filter results by repository name.",
},
FieldDescriptor {
name: "parent",
field_type: FieldType::String,
operators: &[Operator::Equal, Operator::Regex],
indexed: false,
doc: "Parent symbol name (exact match with ':' or regex with '~='). Matches symbols with a specific parent (e.g., methods in a class).",
},
FieldDescriptor {
name: "scope",
field_type: FieldType::Enum(vec!["file", "module", "class", "function", "block"]),
operators: &[Operator::Equal],
indexed: false,
doc: "Scope type: file, module, class, function, block",
},
FieldDescriptor {
name: "text",
field_type: FieldType::String,
operators: &[Operator::Regex],
indexed: false,
doc: "Full-text search in symbol body (code + comments). NOT indexed - use indexed fields first for performance!",
},
FieldDescriptor {
name: "callers",
field_type: FieldType::String,
operators: &[Operator::Equal],
indexed: true,
doc: "Match symbols that call the specified function (requires index with relations)",
},
FieldDescriptor {
name: "callees",
field_type: FieldType::String,
operators: &[Operator::Equal],
indexed: true,
doc: "Match symbols called by the specified function (requires index with relations)",
},
FieldDescriptor {
name: "imports",
field_type: FieldType::String,
operators: &[Operator::Equal],
indexed: true,
doc: "Match files that import the specified module (requires index with relations)",
},
FieldDescriptor {
name: "exports",
field_type: FieldType::String,
operators: &[Operator::Equal],
indexed: true,
doc: "Match files that export the specified symbol (requires index with relations)",
},
FieldDescriptor {
name: "returns",
field_type: FieldType::String,
operators: &[Operator::Equal, Operator::Regex],
indexed: true,
doc: "Match functions with the specified return type (requires index with type metadata)",
},
FieldDescriptor {
name: "impl",
field_type: FieldType::String,
operators: &[Operator::Equal],
indexed: true,
doc: "Match types implementing a trait (e.g., impl:Debug finds types that implement Debug)",
},
FieldDescriptor {
name: "references",
field_type: FieldType::String,
operators: &[Operator::Equal],
indexed: true,
doc: "Match symbols that have cross-file references (requires graph reference edges). Example: references:connect finds symbols named 'connect' that are referenced elsewhere.",
},
FieldDescriptor {
name: "scope.type",
field_type: FieldType::Enum(vec![
"module",
"function",
"class",
"struct",
"method",
"block",
"namespace",
"trait",
"impl",
"interface",
"enum",
]),
operators: &[Operator::Equal, Operator::Regex],
indexed: false,
doc: "Type of the scope containing this symbol (e.g., 'function', 'class', 'module'). Evaluated via sequential scan over file_scopes + scope_id.",
},
FieldDescriptor {
name: "scope.name",
field_type: FieldType::String,
operators: &[Operator::Equal, Operator::Regex],
indexed: false,
doc: "Name of the scope containing this symbol (e.g., 'UserService', 'connect'). Evaluated via sequential scan over file_scopes + scope_id.",
},
FieldDescriptor {
name: "scope.parent",
field_type: FieldType::String,
operators: &[Operator::Equal, Operator::Regex],
indexed: false,
doc: "Name of the parent scope (immediate containing scope). Evaluated via sequential scan over file_scopes + scope_id.",
},
FieldDescriptor {
name: "scope.ancestor",
field_type: FieldType::String,
operators: &[Operator::Equal, Operator::Regex],
indexed: false,
doc: "Name of any ancestor scope (walks up the scope hierarchy). Evaluated via sequential scan over file_scopes + scope_id.",
},
FieldDescriptor {
name: "duplicates",
field_type: FieldType::Enum(vec!["body", "function", "signature", "struct"]),
operators: &[Operator::Equal],
indexed: false,
doc: "Find duplicate code: duplicates:body (function bodies), duplicates:signature (return types), duplicates:struct (struct layouts)",
},
FieldDescriptor {
name: "unused",
field_type: FieldType::Enum(vec!["public", "private", "function", "struct", "all"]),
operators: &[Operator::Equal],
indexed: false,
doc: "Find unused symbols: unused:public, unused:private, unused:function, unused:struct, or unused:all",
},
FieldDescriptor {
name: "circular",
field_type: FieldType::Enum(vec!["calls", "imports", "all"]),
operators: &[Operator::Equal],
indexed: false,
doc: "Find circular dependencies: circular:calls (mutual recursion), circular:imports (import cycles), circular:all",
},
FieldDescriptor {
name: "async",
field_type: FieldType::Bool,
operators: &[Operator::Equal],
indexed: false,
doc: "Whether the function/method is async: async:true or async:false",
},
FieldDescriptor {
name: "static",
field_type: FieldType::Bool,
operators: &[Operator::Equal],
indexed: false,
doc: "Whether the member is static: static:true or static:false",
},
FieldDescriptor {
name: "visibility",
field_type: FieldType::String,
operators: &[Operator::Equal],
indexed: false,
doc: "Visibility modifier: visibility:pub, visibility:private, etc.",
},
FieldDescriptor {
name: "returns",
field_type: FieldType::String,
operators: &[Operator::Equal],
indexed: false,
doc: "Return type filter: returns:bool, returns:Optional<User>, etc. Uses substring matching.",
},
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_registry_is_empty() {
let registry = FieldRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
}
#[test]
fn test_with_core_fields() {
let registry = FieldRegistry::with_core_fields();
assert!(!registry.is_empty());
assert_eq!(registry.len(), 25); }
#[test]
fn test_default_has_core_fields() {
let registry = FieldRegistry::default();
assert_eq!(registry.len(), 25); }
#[test]
fn test_core_field_kind_exists() {
let registry = FieldRegistry::with_core_fields();
let kind_field = registry.get("kind");
assert!(kind_field.is_some());
let kind = kind_field.unwrap();
assert_eq!(kind.name, "kind");
assert!(kind.indexed);
assert!(kind.supports_operator(&Operator::Equal));
assert!(kind.supports_operator(&Operator::Regex));
assert!(!kind.supports_operator(&Operator::Greater));
}
#[test]
fn test_core_field_name_exists() {
let registry = FieldRegistry::with_core_fields();
let name_field = registry.get("name");
assert!(name_field.is_some());
let name = name_field.unwrap();
assert_eq!(name.name, "name");
assert!(name.indexed);
assert!(matches!(name.field_type, FieldType::String));
}
#[test]
fn test_core_field_path_exists() {
let registry = FieldRegistry::with_core_fields();
let path_field = registry.get("path");
assert!(path_field.is_some());
let path = path_field.unwrap();
assert_eq!(path.name, "path");
assert!(path.indexed);
assert!(matches!(path.field_type, FieldType::Path));
}
#[test]
fn test_core_field_lang_exists() {
let registry = FieldRegistry::with_core_fields();
let lang_field = registry.get("lang");
assert!(lang_field.is_some());
let lang = lang_field.unwrap();
assert_eq!(lang.name, "lang");
assert!(lang.indexed);
assert!(matches!(lang.field_type, FieldType::String));
assert!(lang.supports_operator(&Operator::Equal));
assert!(lang.supports_operator(&Operator::Regex));
}
#[test]
fn test_core_field_scope_exists() {
let registry = FieldRegistry::with_core_fields();
let scope_field = registry.get("scope");
assert!(scope_field.is_some());
let scope = scope_field.unwrap();
assert_eq!(scope.name, "scope");
assert!(!scope.indexed); }
#[test]
fn test_core_field_text_exists() {
let registry = FieldRegistry::with_core_fields();
let text_field = registry.get("text");
assert!(text_field.is_some());
let text = text_field.unwrap();
assert_eq!(text.name, "text");
assert!(!text.indexed); assert!(text.supports_operator(&Operator::Regex));
assert!(!text.supports_operator(&Operator::Equal));
}
#[test]
fn test_get_nonexistent_field() {
let registry = FieldRegistry::with_core_fields();
assert!(registry.get("nonexistent").is_none());
}
#[test]
fn test_add_custom_field() {
let mut registry = FieldRegistry::new();
let custom_field = FieldDescriptor {
name: "async",
field_type: FieldType::Bool,
operators: &[Operator::Equal],
indexed: false,
doc: "Whether function is async",
};
registry.add_field(custom_field);
assert_eq!(registry.len(), 1);
assert!(registry.contains("async"));
let async_field = registry.get("async").unwrap();
assert_eq!(async_field.name, "async");
assert!(matches!(async_field.field_type, FieldType::Bool));
}
#[test]
fn test_add_field_replaces_existing() {
let mut registry = FieldRegistry::with_core_fields();
let original_len = registry.len();
let custom_kind = FieldDescriptor {
name: "kind",
field_type: FieldType::String,
operators: &[Operator::Equal],
indexed: false,
doc: "Custom kind field",
};
registry.add_field(custom_kind);
assert_eq!(registry.len(), original_len);
let kind_field = registry.get("kind").unwrap();
assert!(matches!(kind_field.field_type, FieldType::String));
assert!(!kind_field.indexed);
}
#[test]
fn test_field_names() {
let registry = FieldRegistry::with_core_fields();
let names = registry.field_names();
assert_eq!(names.len(), 25); assert!(names.contains(&"kind"));
assert!(names.contains(&"name"));
assert!(names.contains(&"path"));
assert!(names.contains(&"lang"));
assert!(names.contains(&"scope"));
assert!(names.contains(&"text"));
assert!(names.contains(&"callers"));
assert!(names.contains(&"callees"));
assert!(names.contains(&"imports"));
assert!(names.contains(&"exports"));
assert!(names.contains(&"returns"));
assert!(names.contains(&"references"));
assert!(names.contains(&"repo"));
assert!(names.contains(&"parent"));
assert!(names.contains(&"scope.type")); assert!(names.contains(&"scope.name")); assert!(names.contains(&"scope.parent")); assert!(names.contains(&"scope.ancestor")); }
#[test]
fn test_contains() {
let registry = FieldRegistry::with_core_fields();
assert!(registry.contains("kind"));
assert!(registry.contains("name"));
assert!(registry.contains("async"));
assert!(registry.contains("static"));
assert!(registry.contains("visibility"));
assert!(!registry.contains("nonexistent_field"));
}
#[test]
fn test_kind_enum_values() {
let registry = FieldRegistry::with_core_fields();
let kind_field = registry.get("kind").unwrap();
if let FieldType::Enum(values) = &kind_field.field_type {
assert!(values.contains(&"function"));
assert!(values.contains(&"method"));
assert!(values.contains(&"class"));
assert!(values.contains(&"struct"));
assert!(values.contains(&"trait"));
assert!(values.contains(&"enum"));
assert!(values.contains(&"interface"));
assert!(values.contains(&"module"));
} else {
panic!("Expected Enum field type for 'kind'");
}
}
#[test]
fn test_lang_enum_values() {
let registry = FieldRegistry::with_core_fields();
let lang_field = registry.get("lang").unwrap();
assert!(matches!(lang_field.field_type, FieldType::String));
assert!(lang_field.supports_operator(&Operator::Equal));
assert!(lang_field.supports_operator(&Operator::Regex));
}
#[test]
fn test_scope_enum_values() {
let registry = FieldRegistry::with_core_fields();
let scope_field = registry.get("scope").unwrap();
if let FieldType::Enum(values) = &scope_field.field_type {
assert!(values.contains(&"file"));
assert!(values.contains(&"module"));
assert!(values.contains(&"class"));
assert!(values.contains(&"function"));
assert!(values.contains(&"block"));
assert_eq!(values.len(), 5);
} else {
panic!("Expected Enum field type for 'scope'");
}
}
#[test]
fn test_add_plugin_fields_no_collision() {
let mut registry = FieldRegistry::with_core_fields();
let original_len = registry.len();
let plugin_fields = vec![
FieldDescriptor {
name: "plugin_async",
field_type: FieldType::Bool,
operators: &[Operator::Equal],
indexed: false,
doc: "Plugin-specific async field",
},
FieldDescriptor {
name: "plugin_visibility",
field_type: FieldType::String,
operators: &[Operator::Equal],
indexed: false,
doc: "Plugin-specific visibility field",
},
];
let collisions = registry.add_plugin_fields(&plugin_fields);
assert_eq!(collisions.len(), 0);
assert_eq!(registry.len(), original_len + 2);
assert!(registry.contains("plugin_async"));
assert!(registry.contains("plugin_visibility"));
}
#[test]
fn test_add_plugin_fields_with_collision() {
let mut registry = FieldRegistry::with_core_fields();
let original_len = registry.len();
let plugin_fields = vec![
FieldDescriptor {
name: "kind", field_type: FieldType::String,
operators: &[Operator::Equal],
indexed: false,
doc: "Custom kind field",
},
FieldDescriptor {
name: "plugin_unique",
field_type: FieldType::Bool,
operators: &[Operator::Equal],
indexed: false,
doc: "Unique plugin field",
},
];
let collisions = registry.add_plugin_fields(&plugin_fields);
assert_eq!(collisions.len(), 1);
assert_eq!(collisions[0], "kind");
assert_eq!(registry.len(), original_len + 1);
assert!(registry.contains("plugin_unique"));
let kind_field = registry.get("kind").unwrap();
assert!(matches!(kind_field.field_type, FieldType::Enum(_)));
assert!(kind_field.indexed);
}
#[test]
fn test_add_plugin_fields_all_collisions() {
let mut registry = FieldRegistry::with_core_fields();
let original_len = registry.len();
let plugin_fields = vec![
FieldDescriptor {
name: "kind",
field_type: FieldType::String,
operators: &[Operator::Equal],
indexed: false,
doc: "Custom kind",
},
FieldDescriptor {
name: "name",
field_type: FieldType::String,
operators: &[Operator::Equal],
indexed: false,
doc: "Custom name",
},
];
let collisions = registry.add_plugin_fields(&plugin_fields);
assert_eq!(collisions.len(), 2);
assert!(collisions.contains(&"kind".to_string()));
assert!(collisions.contains(&"name".to_string()));
assert_eq!(registry.len(), original_len);
}
#[test]
fn test_plugin_ids_placeholder() {
let registry = FieldRegistry::with_core_fields();
let ids = registry.plugin_ids();
assert_eq!(ids.len(), 0);
}
#[test]
fn test_alias_file_to_path() {
let registry = FieldRegistry::with_core_fields();
let via_alias = registry.get("file");
let direct = registry.get("path");
assert!(via_alias.is_some());
assert!(direct.is_some());
assert_eq!(via_alias.unwrap().name, direct.unwrap().name);
assert_eq!(via_alias.unwrap().name, "path");
}
#[test]
fn test_alias_language_to_lang() {
let registry = FieldRegistry::with_core_fields();
let via_alias = registry.get("language");
let direct = registry.get("lang");
assert!(via_alias.is_some());
assert!(direct.is_some());
assert_eq!(via_alias.unwrap().name, direct.unwrap().name);
assert_eq!(via_alias.unwrap().name, "lang");
}
#[test]
fn test_contains_recognizes_aliases() {
let registry = FieldRegistry::with_core_fields();
assert!(registry.contains("path"));
assert!(registry.contains("lang"));
assert!(registry.contains("file"));
assert!(registry.contains("language"));
}
#[test]
fn test_resolve_canonical_with_direct_field() {
let registry = FieldRegistry::with_core_fields();
assert_eq!(registry.resolve_canonical("kind"), Some("kind"));
assert_eq!(registry.resolve_canonical("path"), Some("path"));
}
#[test]
fn test_resolve_canonical_with_alias() {
let registry = FieldRegistry::with_core_fields();
assert_eq!(registry.resolve_canonical("file"), Some("path"));
assert_eq!(registry.resolve_canonical("language"), Some("lang"));
}
#[test]
fn test_resolve_canonical_with_nonexistent() {
let registry = FieldRegistry::with_core_fields();
assert_eq!(registry.resolve_canonical("nonexistent"), None);
assert_eq!(registry.resolve_canonical("unknown_alias"), None);
}
#[test]
fn test_add_alias_to_custom_field() {
let mut registry = FieldRegistry::new();
let custom_field = FieldDescriptor {
name: "async",
field_type: FieldType::Bool,
operators: &[Operator::Equal],
indexed: false,
doc: "Whether function is async",
};
registry.add_field(custom_field);
registry.add_alias("is_async", "async");
assert!(registry.contains("async"));
assert!(registry.contains("is_async"));
let via_alias = registry.get("is_async").unwrap();
assert_eq!(via_alias.name, "async");
}
#[test]
#[should_panic(expected = "canonical field 'nonexistent' does not exist")]
fn test_add_alias_to_nonexistent_field_panics() {
let mut registry = FieldRegistry::new();
registry.add_alias("bad_alias", "nonexistent");
}
#[test]
fn test_core_field_repo_exists() {
let registry = FieldRegistry::with_core_fields();
let repo_field = registry.get("repo");
assert!(repo_field.is_some());
let repo = repo_field.unwrap();
assert_eq!(repo.name, "repo");
assert!(!repo.indexed); assert!(matches!(repo.field_type, FieldType::String));
assert!(repo.supports_operator(&Operator::Equal));
assert!(!repo.supports_operator(&Operator::Greater));
}
#[test]
fn test_core_field_parent_exists() {
let registry = FieldRegistry::with_core_fields();
let parent_field = registry.get("parent");
assert!(parent_field.is_some());
let parent = parent_field.unwrap();
assert_eq!(parent.name, "parent");
assert!(!parent.indexed); assert!(matches!(parent.field_type, FieldType::String));
assert!(parent.supports_operator(&Operator::Equal));
assert!(parent.supports_operator(&Operator::Regex));
}
#[test]
fn test_alias_does_not_affect_field_count() {
let registry = FieldRegistry::with_core_fields();
assert_eq!(registry.len(), 25);
assert!(registry.contains("file"));
assert!(registry.contains("path"));
assert!(registry.contains("language"));
assert!(registry.contains("lang"));
}
#[test]
fn test_scope_type_field_exists() {
let registry = FieldRegistry::with_core_fields();
let field = registry.get("scope.type");
assert!(field.is_some());
let scope_type = field.unwrap();
assert_eq!(scope_type.name, "scope.type");
assert!(!scope_type.indexed); assert!(matches!(scope_type.field_type, FieldType::Enum(_)));
assert!(scope_type.supports_operator(&Operator::Equal));
assert!(scope_type.supports_operator(&Operator::Regex));
}
#[test]
fn test_scope_name_field_exists() {
let registry = FieldRegistry::with_core_fields();
let field = registry.get("scope.name");
assert!(field.is_some());
let scope_name = field.unwrap();
assert_eq!(scope_name.name, "scope.name");
assert!(!scope_name.indexed); assert!(matches!(scope_name.field_type, FieldType::String));
assert!(scope_name.supports_operator(&Operator::Equal));
assert!(scope_name.supports_operator(&Operator::Regex));
}
#[test]
fn test_scope_parent_field_exists() {
let registry = FieldRegistry::with_core_fields();
let field = registry.get("scope.parent");
assert!(field.is_some());
let scope_parent = field.unwrap();
assert_eq!(scope_parent.name, "scope.parent");
assert!(!scope_parent.indexed); assert!(matches!(scope_parent.field_type, FieldType::String));
assert!(scope_parent.supports_operator(&Operator::Equal));
assert!(scope_parent.supports_operator(&Operator::Regex));
}
#[test]
fn test_scope_ancestor_field_exists() {
let registry = FieldRegistry::with_core_fields();
let field = registry.get("scope.ancestor");
assert!(field.is_some());
let scope_ancestor = field.unwrap();
assert_eq!(scope_ancestor.name, "scope.ancestor");
assert!(!scope_ancestor.indexed); assert!(matches!(scope_ancestor.field_type, FieldType::String));
assert!(scope_ancestor.supports_operator(&Operator::Equal));
assert!(scope_ancestor.supports_operator(&Operator::Regex));
}
}