Skip to main content

sqry_core/query/
registry.rs

1//! Field registry for query validation and optimization
2//!
3//! This module provides the field registry which maintains a catalog of all queryable fields,
4//! both core fields and plugin-provided fields. The registry is used during validation to
5//! check field names, operators, and value types.
6
7use std::collections::HashMap;
8
9use super::types::{FieldDescriptor, FieldType, Operator};
10
11/// Registry of all queryable fields (core + plugin fields)
12///
13/// The field registry maintains a mapping of field names to their descriptors,
14/// which include type information, supported operators, indexing status, and documentation.
15///
16/// # Example
17///
18/// ```
19/// use sqry_core::query::registry::FieldRegistry;
20///
21/// let registry = FieldRegistry::with_core_fields();
22/// assert!(registry.get("kind").is_some());
23/// assert!(registry.get("name").is_some());
24/// ```
25#[derive(Debug, Clone)]
26pub struct FieldRegistry {
27    /// Map of field names to descriptors
28    fields: HashMap<String, FieldDescriptor>,
29    /// Map of field aliases to canonical names (e.g., "file" -> "path", "language" -> "lang")
30    aliases: HashMap<String, String>,
31}
32
33impl FieldRegistry {
34    /// Create an empty field registry
35    #[must_use]
36    pub fn new() -> Self {
37        Self {
38            fields: HashMap::new(),
39            aliases: HashMap::new(),
40        }
41    }
42
43    /// Create a field registry with core fields pre-populated
44    ///
45    /// Core fields are always available and include:
46    /// - `kind`: Node type (function, method, class, etc.)
47    /// - `name`: Node name (exact or regex match)
48    /// - `path`: File path (glob or regex match) - aliased as `file`
49    /// - `lang`: Programming language - aliased as `language`
50    /// - `repo`: Repository filter (glob pattern)
51    /// - `parent`: Parent symbol name (regex match)
52    /// - `scope`: Scope type (file, module, class, function, block)
53    /// - `text`: Full-text search in symbol body (NOT indexed)
54    #[must_use]
55    pub fn with_core_fields() -> Self {
56        let mut registry = Self::new();
57
58        for field in core_fields() {
59            registry.add_field(field);
60        }
61
62        // Register field aliases for backward compatibility and convenience
63        registry.add_alias("file", "path"); // file: is alias for path:
64        registry.add_alias("language", "lang"); // language: is alias for lang:
65
66        registry
67    }
68
69    /// Add a field descriptor to the registry
70    ///
71    /// If a field with the same name already exists, it will be replaced.
72    pub fn add_field(&mut self, descriptor: FieldDescriptor) {
73        self.fields.insert(descriptor.name.to_string(), descriptor);
74    }
75
76    /// Register an alias for a field
77    ///
78    /// Aliases allow alternative names for fields (e.g., "file" as alias for "path").
79    /// The canonical field must already exist in the registry.
80    ///
81    /// # Arguments
82    ///
83    /// * `alias` - The alias name (e.g., "file")
84    /// * `canonical` - The canonical field name (e.g., "path")
85    ///
86    /// # Panics
87    ///
88    /// Panics if the canonical field does not exist in the registry.
89    pub fn add_alias(&mut self, alias: impl Into<String>, canonical: impl Into<String>) {
90        let alias = alias.into();
91        let canonical = canonical.into();
92
93        // Verify canonical field exists
94        assert!(
95            self.fields.contains_key(&canonical),
96            "Cannot add alias '{alias}' -> '{canonical}': canonical field '{canonical}' does not exist"
97        );
98
99        self.aliases.insert(alias, canonical);
100    }
101
102    /// Get a field descriptor by name or alias
103    ///
104    /// If the name is an alias, this resolves to the canonical field.
105    #[inline]
106    #[must_use]
107    pub fn get(&self, name: &str) -> Option<&FieldDescriptor> {
108        // Try direct lookup first
109        if let Some(descriptor) = self.fields.get(name) {
110            return Some(descriptor);
111        }
112
113        // Try alias resolution
114        if let Some(canonical) = self.aliases.get(name) {
115            return self.fields.get(canonical);
116        }
117
118        None
119    }
120
121    /// Resolve a field name to its canonical form
122    ///
123    /// If the name is an alias, returns the canonical name.
124    /// Otherwise, returns the name unchanged if it exists in the registry.
125    ///
126    /// Returns None if the field name (or alias) doesn't exist.
127    pub fn resolve_canonical<'a>(&'a self, name: &'a str) -> Option<&'a str> {
128        // If it's a direct field, return it
129        if self.fields.contains_key(name) {
130            return Some(name);
131        }
132
133        // If it's an alias, return the canonical name
134        self.aliases.get(name).map(std::string::String::as_str)
135    }
136
137    /// Get all field names
138    pub fn field_names(&self) -> Vec<&str> {
139        self.fields
140            .keys()
141            .map(std::string::String::as_str)
142            .collect()
143    }
144
145    /// Check if a field or alias exists
146    #[inline]
147    #[must_use]
148    pub fn contains(&self, name: &str) -> bool {
149        self.fields.contains_key(name) || self.aliases.contains_key(name)
150    }
151
152    /// Get the number of fields in the registry
153    #[must_use]
154    pub fn len(&self) -> usize {
155        self.fields.len()
156    }
157
158    /// Check if the registry is empty
159    #[must_use]
160    pub fn is_empty(&self) -> bool {
161        self.fields.is_empty()
162    }
163
164    /// Add plugin fields to the registry (Phase 7+)
165    ///
166    /// Merges plugin-specific fields with existing fields. If a field name collides with an
167    /// existing field, the plugin field will NOT be added.
168    ///
169    /// # Arguments
170    ///
171    /// * `fields` - Slice of field descriptors from a plugin
172    ///
173    /// # Returns
174    ///
175    /// Vector of field names that collided (were NOT added)
176    ///
177    /// # Example
178    ///
179    /// ```ignore
180    /// let mut registry = FieldRegistry::with_core_fields();
181    /// let rust_plugin = RustPlugin::new();
182    /// let collisions = registry.add_plugin_fields(rust_plugin.fields());
183    /// assert!(collisions.is_empty()); // No collisions with core fields
184    /// ```
185    #[must_use = "collision information should be logged or handled"]
186    pub fn add_plugin_fields(&mut self, fields: &[FieldDescriptor]) -> Vec<String> {
187        let mut collisions = Vec::new();
188
189        for field in fields {
190            if self.contains(field.name) {
191                // Collision detected - skip this field
192                log::debug!(
193                    "Plugin field collision: '{}' already exists, skipping plugin version",
194                    field.name
195                );
196                collisions.push(field.name.to_string());
197            } else {
198                // No collision - add the field
199                log::debug!("Registering plugin field: '{}'", field.name);
200                self.add_field(field.clone());
201            }
202        }
203
204        if collisions.is_empty() {
205            log::debug!(
206                "Plugin field registration complete: {} fields added, no collisions",
207                fields.len()
208            );
209        } else {
210            log::debug!(
211                "Plugin field registration complete: {} fields added, {} collisions ({})",
212                fields.len() - collisions.len(),
213                collisions.len(),
214                collisions.join(", ")
215            );
216        }
217
218        collisions
219    }
220
221    /// Get plugin IDs for cache key generation (Phase 7+)
222    ///
223    /// Returns a sorted list of unique plugin identifiers based on registered fields.
224    /// This is used to create cache keys that are sensitive to which plugins are loaded.
225    ///
226    /// Note: Since we don't track which field came from which plugin, this method
227    /// currently returns an empty vector. In the future, we could add plugin metadata
228    /// to `FieldDescriptor` to enable this functionality.
229    #[must_use]
230    pub fn plugin_ids(&self) -> Vec<String> {
231        // Phase 7: Not implemented yet - would require tracking plugin source per field
232        Vec::new()
233    }
234}
235
236impl Default for FieldRegistry {
237    fn default() -> Self {
238        Self::with_core_fields()
239    }
240}
241
242/// Core field descriptors
243///
244/// These fields are always available regardless of which plugins are loaded.
245///
246/// # Core Fields
247///
248/// - `kind`: Node type (function, method, class, etc.) - indexed
249/// - `name`: Node name - indexed
250/// - `path`: File path (glob-aware) - indexed (aliased as `file`)
251/// - `lang`: Programming language - indexed (aliased as `language`)
252/// - `repo`: Repository filter (glob pattern) - NOT indexed
253/// - `parent`: Parent symbol name (regex) - NOT indexed
254/// - `scope`: Scope type - partially indexed
255/// - `text`: Full-text search in symbol body - NOT indexed
256/// - `scope.type`: Type of scope containing symbol (P2-34) - NOT indexed
257/// - `scope.name`: Name of scope containing symbol (P2-34) - NOT indexed
258/// - `scope.parent`: Name of parent scope (P2-34) - NOT indexed
259/// - `scope.ancestor`: Name of ancestor scope (P2-34) - NOT indexed
260#[must_use]
261#[allow(clippy::too_many_lines)] // Large field registry kept together for readability and auditing.
262pub fn core_fields() -> Vec<FieldDescriptor> {
263    vec![
264        FieldDescriptor {
265            name: "kind",
266            field_type: FieldType::Enum(vec![
267                "function",
268                "method",
269                "class",
270                "struct",
271                "trait",
272                "enum",
273                "interface",
274                "module",
275                "variable",
276                "constant",
277                "type",
278                "namespace",
279                "property",
280                "parameter",
281                "import",
282            ]),
283            operators: &[Operator::Equal, Operator::Regex],
284            indexed: true,
285            doc: "Node type: function, class, method, struct, etc.",
286        },
287        FieldDescriptor {
288            name: "name",
289            field_type: FieldType::String,
290            operators: &[Operator::Equal, Operator::Regex],
291            indexed: true,
292            doc: "Node name (exact match with ':' or regex with '~=')",
293        },
294        FieldDescriptor {
295            name: "path",
296            field_type: FieldType::Path,
297            operators: &[Operator::Equal, Operator::Regex],
298            indexed: true,
299            doc: "File path (glob pattern with ':' or regex with '~=')",
300        },
301        FieldDescriptor {
302            name: "lang",
303            field_type: FieldType::String,
304            operators: &[Operator::Equal, Operator::Regex],
305            indexed: true,
306            doc: "Programming language",
307        },
308        FieldDescriptor {
309            name: "repo",
310            field_type: FieldType::String,
311            operators: &[Operator::Equal],
312            indexed: false,
313            doc: "Repository filter (glob pattern, e.g., 'repo:backend-*'). Used in workspace queries to filter results by repository name.",
314        },
315        FieldDescriptor {
316            name: "parent",
317            field_type: FieldType::String,
318            operators: &[Operator::Equal, Operator::Regex],
319            indexed: false,
320            doc: "Parent symbol name (exact match with ':' or regex with '~='). Matches symbols with a specific parent (e.g., methods in a class).",
321        },
322        FieldDescriptor {
323            name: "scope",
324            field_type: FieldType::Enum(vec!["file", "module", "class", "function", "block"]),
325            operators: &[Operator::Equal],
326            indexed: false,
327            doc: "Scope type: file, module, class, function, block",
328        },
329        FieldDescriptor {
330            name: "text",
331            field_type: FieldType::String,
332            operators: &[Operator::Regex],
333            indexed: false,
334            doc: "Full-text search in symbol body (code + comments). NOT indexed - use indexed fields first for performance!",
335        },
336        // Sprint 2: Relation fields
337        FieldDescriptor {
338            name: "callers",
339            field_type: FieldType::String,
340            operators: &[Operator::Equal],
341            indexed: true,
342            doc: "Match symbols that call the specified function (requires index with relations)",
343        },
344        FieldDescriptor {
345            name: "callees",
346            field_type: FieldType::String,
347            operators: &[Operator::Equal],
348            indexed: true,
349            doc: "Match symbols called by the specified function (requires index with relations)",
350        },
351        FieldDescriptor {
352            name: "imports",
353            field_type: FieldType::String,
354            operators: &[Operator::Equal],
355            indexed: true,
356            doc: "Match files that import the specified module (requires index with relations)",
357        },
358        FieldDescriptor {
359            name: "exports",
360            field_type: FieldType::String,
361            operators: &[Operator::Equal],
362            indexed: true,
363            doc: "Match files that export the specified symbol (requires index with relations)",
364        },
365        FieldDescriptor {
366            name: "returns",
367            field_type: FieldType::String,
368            operators: &[Operator::Equal, Operator::Regex],
369            indexed: true,
370            doc: "Match functions with the specified return type (requires index with type metadata)",
371        },
372        // CD Static Analysis: Trait implementation predicates
373        FieldDescriptor {
374            name: "impl",
375            field_type: FieldType::String,
376            operators: &[Operator::Equal],
377            indexed: true,
378            doc: "Match types implementing a trait (e.g., impl:Debug finds types that implement Debug)",
379        },
380        // P2-33: Cross-file reference predicates
381        FieldDescriptor {
382            name: "references",
383            field_type: FieldType::String,
384            operators: &[Operator::Equal],
385            indexed: true,
386            doc: "Match symbols that have cross-file references (requires graph reference edges). Example: references:connect finds symbols named 'connect' that are referenced elsewhere.",
387        },
388        // P2-34 Phase 2: Scope filtering fields
389        FieldDescriptor {
390            name: "scope.type",
391            field_type: FieldType::Enum(vec![
392                "module",
393                "function",
394                "class",
395                "struct",
396                "method",
397                "block",
398                "namespace",
399                "trait",
400                "impl",
401                "interface",
402                "enum",
403            ]),
404            operators: &[Operator::Equal, Operator::Regex],
405            indexed: false,
406            doc: "Type of the scope containing this symbol (e.g., 'function', 'class', 'module'). Evaluated via sequential scan over file_scopes + scope_id.",
407        },
408        FieldDescriptor {
409            name: "scope.name",
410            field_type: FieldType::String,
411            operators: &[Operator::Equal, Operator::Regex],
412            indexed: false,
413            doc: "Name of the scope containing this symbol (e.g., 'UserService', 'connect'). Evaluated via sequential scan over file_scopes + scope_id.",
414        },
415        FieldDescriptor {
416            name: "scope.parent",
417            field_type: FieldType::String,
418            operators: &[Operator::Equal, Operator::Regex],
419            indexed: false,
420            doc: "Name of the parent scope (immediate containing scope). Evaluated via sequential scan over file_scopes + scope_id.",
421        },
422        FieldDescriptor {
423            name: "scope.ancestor",
424            field_type: FieldType::String,
425            operators: &[Operator::Equal, Operator::Regex],
426            indexed: false,
427            doc: "Name of any ancestor scope (walks up the scope hierarchy). Evaluated via sequential scan over file_scopes + scope_id.",
428        },
429        // CD Static Analysis: Cross-file discovery predicates
430        FieldDescriptor {
431            name: "duplicates",
432            field_type: FieldType::Enum(vec!["body", "function", "signature", "struct"]),
433            operators: &[Operator::Equal],
434            indexed: false,
435            doc: "Find duplicate code: duplicates:body (function bodies), duplicates:signature (return types), duplicates:struct (struct layouts)",
436        },
437        FieldDescriptor {
438            name: "unused",
439            field_type: FieldType::Enum(vec!["public", "private", "function", "struct", "all"]),
440            operators: &[Operator::Equal],
441            indexed: false,
442            doc: "Find unused symbols: unused:public, unused:private, unused:function, unused:struct, or unused:all",
443        },
444        FieldDescriptor {
445            name: "circular",
446            field_type: FieldType::Enum(vec!["calls", "imports", "all"]),
447            operators: &[Operator::Equal],
448            indexed: false,
449            doc: "Find circular dependencies: circular:calls (mutual recursion), circular:imports (import cycles), circular:all",
450        },
451        // P2-XX: Boolean metadata fields (from NodeEntry)
452        FieldDescriptor {
453            name: "async",
454            field_type: FieldType::Bool,
455            operators: &[Operator::Equal],
456            indexed: false,
457            doc: "Whether the function/method is async: async:true or async:false",
458        },
459        FieldDescriptor {
460            name: "static",
461            field_type: FieldType::Bool,
462            operators: &[Operator::Equal],
463            indexed: false,
464            doc: "Whether the member is static: static:true or static:false",
465        },
466        FieldDescriptor {
467            name: "visibility",
468            field_type: FieldType::String,
469            operators: &[Operator::Equal],
470            indexed: false,
471            doc: "Visibility modifier: visibility:pub, visibility:private, etc.",
472        },
473        FieldDescriptor {
474            name: "returns",
475            field_type: FieldType::String,
476            operators: &[Operator::Equal],
477            indexed: false,
478            doc: "Return type filter: returns:bool, returns:Optional<User>, etc. Uses substring matching.",
479        },
480    ]
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486
487    #[test]
488    fn test_new_registry_is_empty() {
489        let registry = FieldRegistry::new();
490        assert!(registry.is_empty());
491        assert_eq!(registry.len(), 0);
492    }
493
494    #[test]
495    fn test_with_core_fields() {
496        let registry = FieldRegistry::with_core_fields();
497        assert!(!registry.is_empty());
498        assert_eq!(registry.len(), 25); // kind, name, path, lang, scope, text, callers, callees, imports, exports, returns, impl, repo, parent, references (P2-33) + scope.type, scope.name, scope.parent, scope.ancestor (P2-34) + duplicates, unused, circular (CD predicates)
499    }
500
501    #[test]
502    fn test_default_has_core_fields() {
503        let registry = FieldRegistry::default();
504        assert_eq!(registry.len(), 25); // Updated for Sprint 2 relation fields + P2-33 references + P2-34 scope.* fields + CD Static Analysis predicates
505    }
506
507    #[test]
508    fn test_core_field_kind_exists() {
509        let registry = FieldRegistry::with_core_fields();
510        let kind_field = registry.get("kind");
511        assert!(kind_field.is_some());
512
513        let kind = kind_field.unwrap();
514        assert_eq!(kind.name, "kind");
515        assert!(kind.indexed);
516        assert!(kind.supports_operator(&Operator::Equal));
517        assert!(kind.supports_operator(&Operator::Regex));
518        assert!(!kind.supports_operator(&Operator::Greater));
519    }
520
521    #[test]
522    fn test_core_field_name_exists() {
523        let registry = FieldRegistry::with_core_fields();
524        let name_field = registry.get("name");
525        assert!(name_field.is_some());
526
527        let name = name_field.unwrap();
528        assert_eq!(name.name, "name");
529        assert!(name.indexed);
530        assert!(matches!(name.field_type, FieldType::String));
531    }
532
533    #[test]
534    fn test_core_field_path_exists() {
535        let registry = FieldRegistry::with_core_fields();
536        let path_field = registry.get("path");
537        assert!(path_field.is_some());
538
539        let path = path_field.unwrap();
540        assert_eq!(path.name, "path");
541        assert!(path.indexed);
542        assert!(matches!(path.field_type, FieldType::Path));
543    }
544
545    #[test]
546    fn test_core_field_lang_exists() {
547        let registry = FieldRegistry::with_core_fields();
548        let lang_field = registry.get("lang");
549        assert!(lang_field.is_some());
550
551        let lang = lang_field.unwrap();
552        assert_eq!(lang.name, "lang");
553        assert!(lang.indexed);
554        assert!(matches!(lang.field_type, FieldType::String));
555        assert!(lang.supports_operator(&Operator::Equal));
556        assert!(lang.supports_operator(&Operator::Regex));
557    }
558
559    #[test]
560    fn test_core_field_scope_exists() {
561        let registry = FieldRegistry::with_core_fields();
562        let scope_field = registry.get("scope");
563        assert!(scope_field.is_some());
564
565        let scope = scope_field.unwrap();
566        assert_eq!(scope.name, "scope");
567        assert!(!scope.indexed); // Scope is NOT indexed
568    }
569
570    #[test]
571    fn test_core_field_text_exists() {
572        let registry = FieldRegistry::with_core_fields();
573        let text_field = registry.get("text");
574        assert!(text_field.is_some());
575
576        let text = text_field.unwrap();
577        assert_eq!(text.name, "text");
578        assert!(!text.indexed); // Text is NOT indexed
579        assert!(text.supports_operator(&Operator::Regex));
580        assert!(!text.supports_operator(&Operator::Equal));
581    }
582
583    #[test]
584    fn test_get_nonexistent_field() {
585        let registry = FieldRegistry::with_core_fields();
586        assert!(registry.get("nonexistent").is_none());
587    }
588
589    #[test]
590    fn test_add_custom_field() {
591        let mut registry = FieldRegistry::new();
592
593        let custom_field = FieldDescriptor {
594            name: "async",
595            field_type: FieldType::Bool,
596            operators: &[Operator::Equal],
597            indexed: false,
598            doc: "Whether function is async",
599        };
600
601        registry.add_field(custom_field);
602
603        assert_eq!(registry.len(), 1);
604        assert!(registry.contains("async"));
605
606        let async_field = registry.get("async").unwrap();
607        assert_eq!(async_field.name, "async");
608        assert!(matches!(async_field.field_type, FieldType::Bool));
609    }
610
611    #[test]
612    fn test_add_field_replaces_existing() {
613        let mut registry = FieldRegistry::with_core_fields();
614        let original_len = registry.len();
615
616        // Replace "kind" field with custom version
617        let custom_kind = FieldDescriptor {
618            name: "kind",
619            field_type: FieldType::String,
620            operators: &[Operator::Equal],
621            indexed: false,
622            doc: "Custom kind field",
623        };
624
625        registry.add_field(custom_kind);
626
627        // Should have same number of fields (replaced, not added)
628        assert_eq!(registry.len(), original_len);
629
630        let kind_field = registry.get("kind").unwrap();
631        assert!(matches!(kind_field.field_type, FieldType::String));
632        assert!(!kind_field.indexed);
633    }
634
635    #[test]
636    fn test_field_names() {
637        let registry = FieldRegistry::with_core_fields();
638        let names = registry.field_names();
639
640        assert_eq!(names.len(), 25); // Updated for Sprint 2 + P2-33 references + P2-34 scope.* + CD predicates (impl, duplicates, unused, circular)
641        assert!(names.contains(&"kind"));
642        assert!(names.contains(&"name"));
643        assert!(names.contains(&"path"));
644        assert!(names.contains(&"lang"));
645        assert!(names.contains(&"scope"));
646        assert!(names.contains(&"text"));
647        assert!(names.contains(&"callers"));
648        assert!(names.contains(&"callees"));
649        assert!(names.contains(&"imports"));
650        assert!(names.contains(&"exports"));
651        assert!(names.contains(&"returns"));
652        assert!(names.contains(&"references"));
653        assert!(names.contains(&"repo"));
654        assert!(names.contains(&"parent"));
655        assert!(names.contains(&"scope.type")); // P2-34
656        assert!(names.contains(&"scope.name")); // P2-34
657        assert!(names.contains(&"scope.parent")); // P2-34
658        assert!(names.contains(&"scope.ancestor")); // P2-34
659    }
660
661    #[test]
662    fn test_contains() {
663        let registry = FieldRegistry::with_core_fields();
664
665        assert!(registry.contains("kind"));
666        assert!(registry.contains("name"));
667        // async, static, visibility are now core fields (added for NodeEntry metadata)
668        assert!(registry.contains("async"));
669        assert!(registry.contains("static"));
670        assert!(registry.contains("visibility"));
671        assert!(!registry.contains("nonexistent_field"));
672    }
673
674    #[test]
675    fn test_kind_enum_values() {
676        let registry = FieldRegistry::with_core_fields();
677        let kind_field = registry.get("kind").unwrap();
678
679        if let FieldType::Enum(values) = &kind_field.field_type {
680            assert!(values.contains(&"function"));
681            assert!(values.contains(&"method"));
682            assert!(values.contains(&"class"));
683            assert!(values.contains(&"struct"));
684            assert!(values.contains(&"trait"));
685            assert!(values.contains(&"enum"));
686            assert!(values.contains(&"interface"));
687            assert!(values.contains(&"module"));
688        } else {
689            panic!("Expected Enum field type for 'kind'");
690        }
691    }
692
693    #[test]
694    fn test_lang_enum_values() {
695        let registry = FieldRegistry::with_core_fields();
696        let lang_field = registry.get("lang").unwrap();
697
698        assert!(matches!(lang_field.field_type, FieldType::String));
699        assert!(lang_field.supports_operator(&Operator::Equal));
700        assert!(lang_field.supports_operator(&Operator::Regex));
701    }
702
703    #[test]
704    fn test_scope_enum_values() {
705        let registry = FieldRegistry::with_core_fields();
706        let scope_field = registry.get("scope").unwrap();
707
708        if let FieldType::Enum(values) = &scope_field.field_type {
709            assert!(values.contains(&"file"));
710            assert!(values.contains(&"module"));
711            assert!(values.contains(&"class"));
712            assert!(values.contains(&"function"));
713            assert!(values.contains(&"block"));
714            assert_eq!(values.len(), 5);
715        } else {
716            panic!("Expected Enum field type for 'scope'");
717        }
718    }
719
720    // Task 7: Plugin Integration Tests
721
722    #[test]
723    fn test_add_plugin_fields_no_collision() {
724        let mut registry = FieldRegistry::with_core_fields();
725        let original_len = registry.len();
726
727        // Use unique plugin field names that don't collide with core fields
728        let plugin_fields = vec![
729            FieldDescriptor {
730                name: "plugin_async",
731                field_type: FieldType::Bool,
732                operators: &[Operator::Equal],
733                indexed: false,
734                doc: "Plugin-specific async field",
735            },
736            FieldDescriptor {
737                name: "plugin_visibility",
738                field_type: FieldType::String,
739                operators: &[Operator::Equal],
740                indexed: false,
741                doc: "Plugin-specific visibility field",
742            },
743        ];
744
745        let collisions = registry.add_plugin_fields(&plugin_fields);
746
747        // No collisions expected
748        assert_eq!(collisions.len(), 0);
749
750        // Fields should be added
751        assert_eq!(registry.len(), original_len + 2);
752        assert!(registry.contains("plugin_async"));
753        assert!(registry.contains("plugin_visibility"));
754    }
755
756    #[test]
757    fn test_add_plugin_fields_with_collision() {
758        let mut registry = FieldRegistry::with_core_fields();
759        let original_len = registry.len();
760
761        let plugin_fields = vec![
762            FieldDescriptor {
763                name: "kind", // Collides with core field
764                field_type: FieldType::String,
765                operators: &[Operator::Equal],
766                indexed: false,
767                doc: "Custom kind field",
768            },
769            FieldDescriptor {
770                name: "plugin_unique",
771                field_type: FieldType::Bool,
772                operators: &[Operator::Equal],
773                indexed: false,
774                doc: "Unique plugin field",
775            },
776        ];
777
778        let collisions = registry.add_plugin_fields(&plugin_fields);
779
780        // Should have 1 collision (kind)
781        assert_eq!(collisions.len(), 1);
782        assert_eq!(collisions[0], "kind");
783
784        // Only plugin_unique should be added
785        assert_eq!(registry.len(), original_len + 1);
786        assert!(registry.contains("plugin_unique"));
787
788        // kind should still be the original core field
789        let kind_field = registry.get("kind").unwrap();
790        assert!(matches!(kind_field.field_type, FieldType::Enum(_)));
791        assert!(kind_field.indexed);
792    }
793
794    #[test]
795    fn test_add_plugin_fields_all_collisions() {
796        let mut registry = FieldRegistry::with_core_fields();
797        let original_len = registry.len();
798
799        let plugin_fields = vec![
800            FieldDescriptor {
801                name: "kind",
802                field_type: FieldType::String,
803                operators: &[Operator::Equal],
804                indexed: false,
805                doc: "Custom kind",
806            },
807            FieldDescriptor {
808                name: "name",
809                field_type: FieldType::String,
810                operators: &[Operator::Equal],
811                indexed: false,
812                doc: "Custom name",
813            },
814        ];
815
816        let collisions = registry.add_plugin_fields(&plugin_fields);
817
818        // All fields should collide
819        assert_eq!(collisions.len(), 2);
820        assert!(collisions.contains(&"kind".to_string()));
821        assert!(collisions.contains(&"name".to_string()));
822
823        // No new fields added
824        assert_eq!(registry.len(), original_len);
825    }
826
827    #[test]
828    fn test_plugin_ids_placeholder() {
829        let registry = FieldRegistry::with_core_fields();
830        let ids = registry.plugin_ids();
831
832        // Currently returns empty vector (Phase 7 placeholder)
833        assert_eq!(ids.len(), 0);
834    }
835
836    // : Tests for alias support and new predicates (repo, parent)
837
838    #[test]
839    fn test_alias_file_to_path() {
840        let registry = FieldRegistry::with_core_fields();
841
842        // "file" should resolve to "path"
843        let via_alias = registry.get("file");
844        let direct = registry.get("path");
845
846        assert!(via_alias.is_some());
847        assert!(direct.is_some());
848
849        // Should return the same descriptor
850        assert_eq!(via_alias.unwrap().name, direct.unwrap().name);
851        assert_eq!(via_alias.unwrap().name, "path");
852    }
853
854    #[test]
855    fn test_alias_language_to_lang() {
856        let registry = FieldRegistry::with_core_fields();
857
858        // "language" should resolve to "lang"
859        let via_alias = registry.get("language");
860        let direct = registry.get("lang");
861
862        assert!(via_alias.is_some());
863        assert!(direct.is_some());
864
865        // Should return the same descriptor
866        assert_eq!(via_alias.unwrap().name, direct.unwrap().name);
867        assert_eq!(via_alias.unwrap().name, "lang");
868    }
869
870    #[test]
871    fn test_contains_recognizes_aliases() {
872        let registry = FieldRegistry::with_core_fields();
873
874        // Direct fields
875        assert!(registry.contains("path"));
876        assert!(registry.contains("lang"));
877
878        // Aliases
879        assert!(registry.contains("file"));
880        assert!(registry.contains("language"));
881    }
882
883    #[test]
884    fn test_resolve_canonical_with_direct_field() {
885        let registry = FieldRegistry::with_core_fields();
886
887        assert_eq!(registry.resolve_canonical("kind"), Some("kind"));
888        assert_eq!(registry.resolve_canonical("path"), Some("path"));
889    }
890
891    #[test]
892    fn test_resolve_canonical_with_alias() {
893        let registry = FieldRegistry::with_core_fields();
894
895        assert_eq!(registry.resolve_canonical("file"), Some("path"));
896        assert_eq!(registry.resolve_canonical("language"), Some("lang"));
897    }
898
899    #[test]
900    fn test_resolve_canonical_with_nonexistent() {
901        let registry = FieldRegistry::with_core_fields();
902
903        assert_eq!(registry.resolve_canonical("nonexistent"), None);
904        assert_eq!(registry.resolve_canonical("unknown_alias"), None);
905    }
906
907    #[test]
908    fn test_add_alias_to_custom_field() {
909        let mut registry = FieldRegistry::new();
910
911        let custom_field = FieldDescriptor {
912            name: "async",
913            field_type: FieldType::Bool,
914            operators: &[Operator::Equal],
915            indexed: false,
916            doc: "Whether function is async",
917        };
918
919        registry.add_field(custom_field);
920        registry.add_alias("is_async", "async");
921
922        // Both should work
923        assert!(registry.contains("async"));
924        assert!(registry.contains("is_async"));
925
926        let via_alias = registry.get("is_async").unwrap();
927        assert_eq!(via_alias.name, "async");
928    }
929
930    #[test]
931    #[should_panic(expected = "canonical field 'nonexistent' does not exist")]
932    fn test_add_alias_to_nonexistent_field_panics() {
933        let mut registry = FieldRegistry::new();
934        registry.add_alias("bad_alias", "nonexistent");
935    }
936
937    #[test]
938    fn test_core_field_repo_exists() {
939        let registry = FieldRegistry::with_core_fields();
940        let repo_field = registry.get("repo");
941        assert!(repo_field.is_some());
942
943        let repo = repo_field.unwrap();
944        assert_eq!(repo.name, "repo");
945        assert!(!repo.indexed); // Repo is a filter, not indexed
946        assert!(matches!(repo.field_type, FieldType::String));
947        assert!(repo.supports_operator(&Operator::Equal));
948        assert!(!repo.supports_operator(&Operator::Greater));
949    }
950
951    #[test]
952    fn test_core_field_parent_exists() {
953        let registry = FieldRegistry::with_core_fields();
954        let parent_field = registry.get("parent");
955        assert!(parent_field.is_some());
956
957        let parent = parent_field.unwrap();
958        assert_eq!(parent.name, "parent");
959        assert!(!parent.indexed); // Parent is not indexed
960        assert!(matches!(parent.field_type, FieldType::String));
961        assert!(parent.supports_operator(&Operator::Equal));
962        assert!(parent.supports_operator(&Operator::Regex));
963    }
964
965    #[test]
966    fn test_alias_does_not_affect_field_count() {
967        let registry = FieldRegistry::with_core_fields();
968
969        // Aliases should not be counted as separate fields
970        assert_eq!(registry.len(), 25); // Updated for P2-33 references + P2-34 scope.* + CD predicates
971
972        // But both aliases and canonical names should be accessible
973        assert!(registry.contains("file"));
974        assert!(registry.contains("path"));
975        assert!(registry.contains("language"));
976        assert!(registry.contains("lang"));
977    }
978
979    // P2-34 Phase 2: Tests for scope.* fields
980
981    #[test]
982    fn test_scope_type_field_exists() {
983        let registry = FieldRegistry::with_core_fields();
984        let field = registry.get("scope.type");
985        assert!(field.is_some());
986
987        let scope_type = field.unwrap();
988        assert_eq!(scope_type.name, "scope.type");
989        assert!(!scope_type.indexed); // scope.* fields are NOT indexed
990        assert!(matches!(scope_type.field_type, FieldType::Enum(_)));
991        assert!(scope_type.supports_operator(&Operator::Equal));
992        assert!(scope_type.supports_operator(&Operator::Regex));
993    }
994
995    #[test]
996    fn test_scope_name_field_exists() {
997        let registry = FieldRegistry::with_core_fields();
998        let field = registry.get("scope.name");
999        assert!(field.is_some());
1000
1001        let scope_name = field.unwrap();
1002        assert_eq!(scope_name.name, "scope.name");
1003        assert!(!scope_name.indexed); // scope.* fields are NOT indexed
1004        assert!(matches!(scope_name.field_type, FieldType::String));
1005        assert!(scope_name.supports_operator(&Operator::Equal));
1006        assert!(scope_name.supports_operator(&Operator::Regex));
1007    }
1008
1009    #[test]
1010    fn test_scope_parent_field_exists() {
1011        let registry = FieldRegistry::with_core_fields();
1012        let field = registry.get("scope.parent");
1013        assert!(field.is_some());
1014
1015        let scope_parent = field.unwrap();
1016        assert_eq!(scope_parent.name, "scope.parent");
1017        assert!(!scope_parent.indexed); // scope.* fields are NOT indexed
1018        assert!(matches!(scope_parent.field_type, FieldType::String));
1019        assert!(scope_parent.supports_operator(&Operator::Equal));
1020        assert!(scope_parent.supports_operator(&Operator::Regex));
1021    }
1022
1023    #[test]
1024    fn test_scope_ancestor_field_exists() {
1025        let registry = FieldRegistry::with_core_fields();
1026        let field = registry.get("scope.ancestor");
1027        assert!(field.is_some());
1028
1029        let scope_ancestor = field.unwrap();
1030        assert_eq!(scope_ancestor.name, "scope.ancestor");
1031        assert!(!scope_ancestor.indexed); // scope.* fields are NOT indexed
1032        assert!(matches!(scope_ancestor.field_type, FieldType::String));
1033        assert!(scope_ancestor.supports_operator(&Operator::Equal));
1034        assert!(scope_ancestor.supports_operator(&Operator::Regex));
1035    }
1036}