Skip to main content

altium_format/query/
schql_parser.rs

1//! Query parser - tokenizes and parses SchQL query strings.
2
3use super::ast::*;
4
5/// Query parsing error
6#[derive(Debug, Clone)]
7pub enum QueryError {
8    /// Syntax error in query
9    ParseError { position: usize, message: String },
10    /// Unknown element type
11    UnknownElement(String),
12    /// Unknown attribute
13    UnknownAttribute(String),
14    /// Unknown pseudo-selector
15    UnknownPseudo(String),
16    /// Invalid combinator
17    InvalidCombinator(String),
18    /// Type mismatch (e.g., pin selector on net)
19    TypeMismatch { expected: String, got: String },
20    /// Empty query
21    EmptyQuery,
22}
23
24impl std::fmt::Display for QueryError {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            QueryError::ParseError { position, message } => {
28                write!(f, "Parse error at position {}: {}", position, message)
29            }
30            QueryError::UnknownElement(e) => write!(f, "Unknown element type: {}", e),
31            QueryError::UnknownAttribute(a) => write!(f, "Unknown attribute: {}", a),
32            QueryError::UnknownPseudo(p) => write!(f, "Unknown pseudo-selector: :{}", p),
33            QueryError::InvalidCombinator(c) => write!(f, "Invalid combinator: {}", c),
34            QueryError::TypeMismatch { expected, got } => {
35                write!(f, "Type mismatch: expected {}, got {}", expected, got)
36            }
37            QueryError::EmptyQuery => write!(f, "Empty query"),
38        }
39    }
40}
41
42impl std::error::Error for QueryError {}
43
44/// Token types for the query lexer
45#[derive(Debug, Clone, PartialEq)]
46enum Token {
47    // Identifiers and values
48    Ident(String),
49    String(String),
50    Number(i64),
51    Float(f64),
52
53    // Symbols
54    Hash,          // #
55    Dot,           // .
56    Comma,         // ,
57    Colon,         // :
58    DoubleColon,   // ::
59    Star,          // *
60    Greater,       // >
61    DoubleGreater, // >>
62    Plus,          // +
63    Tilde,         // ~
64    LBracket,      // [
65    RBracket,      // ]
66    LParen,        // (
67    RParen,        // )
68    Equals,        // =
69    NotEquals,     // !=
70    TildeEquals,   // ~=
71    CaretEquals,   // ^=
72    DollarEquals,  // $=
73    StarEquals,    // *=
74    GreaterEquals, // >=
75    LessEquals,    // <=
76    Less,          // <
77
78    // End of input
79    Eof,
80}
81
82/// Query parser
83pub struct QueryParser {
84    // Configuration could go here
85}
86
87impl QueryParser {
88    /// Create a new parser
89    pub fn new() -> Self {
90        Self {}
91    }
92
93    /// Parse a query string into a Selector AST
94    pub fn parse(&self, input: &str) -> Result<Selector, QueryError> {
95        let input = input.trim();
96        if input.is_empty() {
97            return Err(QueryError::EmptyQuery);
98        }
99
100        let tokens = self.tokenize(input)?;
101        let mut parser = SelectorParser::new(tokens);
102        parser.parse_selector_list()
103    }
104
105    /// Tokenize input string
106    fn tokenize(&self, input: &str) -> Result<Vec<Token>, QueryError> {
107        let mut tokens = Vec::new();
108        let mut chars = input.chars().peekable();
109        let mut pos = 0;
110
111        while let Some(&c) = chars.peek() {
112            match c {
113                // Whitespace - skip but track position
114                ' ' | '\t' | '\n' | '\r' => {
115                    chars.next();
116                    pos += 1;
117                }
118
119                // Single-char tokens
120                '#' => {
121                    chars.next();
122                    pos += 1;
123                    tokens.push(Token::Hash);
124                }
125                '.' => {
126                    chars.next();
127                    pos += 1;
128                    tokens.push(Token::Dot);
129                }
130                ',' => {
131                    chars.next();
132                    pos += 1;
133                    tokens.push(Token::Comma);
134                }
135                '[' => {
136                    chars.next();
137                    pos += 1;
138                    tokens.push(Token::LBracket);
139                }
140                ']' => {
141                    chars.next();
142                    pos += 1;
143                    tokens.push(Token::RBracket);
144                }
145                '(' => {
146                    chars.next();
147                    pos += 1;
148                    tokens.push(Token::LParen);
149                }
150                ')' => {
151                    chars.next();
152                    pos += 1;
153                    tokens.push(Token::RParen);
154                }
155                '+' => {
156                    chars.next();
157                    pos += 1;
158                    tokens.push(Token::Plus);
159                }
160
161                // Multi-char tokens starting with *
162                '*' => {
163                    chars.next();
164                    pos += 1;
165                    if chars.peek() == Some(&'=') {
166                        chars.next();
167                        pos += 1;
168                        tokens.push(Token::StarEquals);
169                    } else {
170                        tokens.push(Token::Star);
171                    }
172                }
173
174                // Multi-char tokens starting with :
175                ':' => {
176                    chars.next();
177                    pos += 1;
178                    if chars.peek() == Some(&':') {
179                        chars.next();
180                        pos += 1;
181                        tokens.push(Token::DoubleColon);
182                    } else {
183                        tokens.push(Token::Colon);
184                    }
185                }
186
187                // Multi-char tokens starting with >
188                '>' => {
189                    chars.next();
190                    pos += 1;
191                    if chars.peek() == Some(&'>') {
192                        chars.next();
193                        pos += 1;
194                        tokens.push(Token::DoubleGreater);
195                    } else if chars.peek() == Some(&'=') {
196                        chars.next();
197                        pos += 1;
198                        tokens.push(Token::GreaterEquals);
199                    } else {
200                        tokens.push(Token::Greater);
201                    }
202                }
203
204                // Multi-char tokens starting with <
205                '<' => {
206                    chars.next();
207                    pos += 1;
208                    if chars.peek() == Some(&'=') {
209                        chars.next();
210                        pos += 1;
211                        tokens.push(Token::LessEquals);
212                    } else {
213                        tokens.push(Token::Less);
214                    }
215                }
216
217                // Multi-char tokens starting with ~
218                '~' => {
219                    chars.next();
220                    pos += 1;
221                    if chars.peek() == Some(&'=') {
222                        chars.next();
223                        pos += 1;
224                        tokens.push(Token::TildeEquals);
225                    } else {
226                        tokens.push(Token::Tilde);
227                    }
228                }
229
230                // ^= (starts with)
231                '^' => {
232                    chars.next();
233                    pos += 1;
234                    if chars.peek() == Some(&'=') {
235                        chars.next();
236                        pos += 1;
237                        tokens.push(Token::CaretEquals);
238                    } else {
239                        return Err(QueryError::ParseError {
240                            position: pos,
241                            message: "Expected '=' after '^'".to_string(),
242                        });
243                    }
244                }
245
246                // $= (ends with)
247                '$' => {
248                    chars.next();
249                    pos += 1;
250                    if chars.peek() == Some(&'=') {
251                        chars.next();
252                        pos += 1;
253                        tokens.push(Token::DollarEquals);
254                    } else {
255                        return Err(QueryError::ParseError {
256                            position: pos,
257                            message: "Expected '=' after '$'".to_string(),
258                        });
259                    }
260                }
261
262                // != (not equals)
263                '!' => {
264                    chars.next();
265                    pos += 1;
266                    if chars.peek() == Some(&'=') {
267                        chars.next();
268                        pos += 1;
269                        tokens.push(Token::NotEquals);
270                    } else {
271                        return Err(QueryError::ParseError {
272                            position: pos,
273                            message: "Expected '=' after '!'".to_string(),
274                        });
275                    }
276                }
277
278                '=' => {
279                    chars.next();
280                    pos += 1;
281                    tokens.push(Token::Equals);
282                }
283
284                // String literals
285                '"' | '\'' => {
286                    let quote = c;
287                    chars.next();
288                    pos += 1;
289                    let mut s = String::new();
290                    loop {
291                        match chars.peek() {
292                            Some(&c) if c == quote => {
293                                chars.next();
294                                pos += 1;
295                                break;
296                            }
297                            Some(&'\\') => {
298                                chars.next();
299                                pos += 1;
300                                if let Some(&escaped) = chars.peek() {
301                                    chars.next();
302                                    pos += 1;
303                                    s.push(escaped);
304                                }
305                            }
306                            Some(&c) => {
307                                s.push(c);
308                                chars.next();
309                                pos += 1;
310                            }
311                            None => {
312                                return Err(QueryError::ParseError {
313                                    position: pos,
314                                    message: "Unterminated string".to_string(),
315                                });
316                            }
317                        }
318                    }
319                    tokens.push(Token::String(s));
320                }
321
322                // Numbers (including negative)
323                '0'..='9' => {
324                    let mut num_str = String::new();
325                    let mut has_dot = false;
326                    while let Some(&c) = chars.peek() {
327                        if c.is_ascii_digit() {
328                            num_str.push(c);
329                            chars.next();
330                            pos += 1;
331                        } else if c == '.' && !has_dot {
332                            has_dot = true;
333                            num_str.push(c);
334                            chars.next();
335                            pos += 1;
336                        } else {
337                            break;
338                        }
339                    }
340                    if has_dot {
341                        tokens.push(Token::Float(num_str.parse().unwrap_or(0.0)));
342                    } else {
343                        tokens.push(Token::Number(num_str.parse().unwrap_or(0)));
344                    }
345                }
346
347                // Negative numbers
348                '-' => {
349                    chars.next();
350                    pos += 1;
351                    if chars.peek().is_some_and(|c| c.is_ascii_digit()) {
352                        let mut num_str = String::from("-");
353                        let mut has_dot = false;
354                        while let Some(&c) = chars.peek() {
355                            if c.is_ascii_digit() {
356                                num_str.push(c);
357                                chars.next();
358                                pos += 1;
359                            } else if c == '.' && !has_dot {
360                                has_dot = true;
361                                num_str.push(c);
362                                chars.next();
363                                pos += 1;
364                            } else {
365                                break;
366                            }
367                        }
368                        if has_dot {
369                            tokens.push(Token::Float(num_str.parse().unwrap_or(0.0)));
370                        } else {
371                            tokens.push(Token::Number(num_str.parse().unwrap_or(0)));
372                        }
373                    } else {
374                        // Treat as part of an identifier
375                        let mut ident = String::from("-");
376                        while let Some(&c) = chars.peek() {
377                            if c.is_alphanumeric() || c == '_' || c == '-' {
378                                ident.push(c);
379                                chars.next();
380                                pos += 1;
381                            } else {
382                                break;
383                            }
384                        }
385                        tokens.push(Token::Ident(ident));
386                    }
387                }
388
389                // Identifiers
390                _ if c.is_alphabetic() || c == '_' => {
391                    let mut ident = String::new();
392                    while let Some(&c) = chars.peek() {
393                        if c.is_alphanumeric() || c == '_' || c == '-' {
394                            ident.push(c);
395                            chars.next();
396                            pos += 1;
397                        } else {
398                            break;
399                        }
400                    }
401                    tokens.push(Token::Ident(ident));
402                }
403
404                _ => {
405                    return Err(QueryError::ParseError {
406                        position: pos,
407                        message: format!("Unexpected character: '{}'", c),
408                    });
409                }
410            }
411        }
412
413        tokens.push(Token::Eof);
414        Ok(tokens)
415    }
416}
417
418impl Default for QueryParser {
419    fn default() -> Self {
420        Self::new()
421    }
422}
423
424/// Recursive descent parser for selectors
425struct SelectorParser {
426    tokens: Vec<Token>,
427    pos: usize,
428}
429
430impl SelectorParser {
431    fn new(tokens: Vec<Token>) -> Self {
432        Self { tokens, pos: 0 }
433    }
434
435    fn current(&self) -> &Token {
436        self.tokens.get(self.pos).unwrap_or(&Token::Eof)
437    }
438
439    fn _peek_next(&self) -> &Token {
440        self.tokens.get(self.pos + 1).unwrap_or(&Token::Eof)
441    }
442
443    fn advance(&mut self) -> Token {
444        let tok = self.current().clone();
445        if self.pos < self.tokens.len() {
446            self.pos += 1;
447        }
448        tok
449    }
450
451    fn expect(&mut self, expected: &Token) -> Result<(), QueryError> {
452        if self.current() == expected {
453            self.advance();
454            Ok(())
455        } else {
456            Err(QueryError::ParseError {
457                position: self.pos,
458                message: format!("Expected {:?}, got {:?}", expected, self.current()),
459            })
460        }
461    }
462
463    /// Parse comma-separated selector list
464    fn parse_selector_list(&mut self) -> Result<Selector, QueryError> {
465        let mut selectors = vec![self.parse_combinator_chain()?];
466
467        while self.current() == &Token::Comma {
468            self.advance();
469            selectors.push(self.parse_combinator_chain()?);
470        }
471
472        if selectors.len() == 1 {
473            Ok(selectors.pop().unwrap())
474        } else {
475            Ok(Selector::Union(selectors))
476        }
477    }
478
479    /// Parse combinators: A B, A > B, A >> B, A ~ B, A + B, A :: B
480    fn parse_combinator_chain(&mut self) -> Result<Selector, QueryError> {
481        let mut left = self.parse_compound_selector()?;
482
483        loop {
484            let combinator = match self.current() {
485                Token::Greater => {
486                    self.advance();
487                    CombinatorType::Child
488                }
489                Token::DoubleGreater => {
490                    self.advance();
491                    CombinatorType::Connected
492                }
493                Token::Tilde => {
494                    self.advance();
495                    CombinatorType::Sibling
496                }
497                Token::Plus => {
498                    self.advance();
499                    CombinatorType::Adjacent
500                }
501                Token::DoubleColon => {
502                    self.advance();
503                    CombinatorType::OnNet
504                }
505                // Check for implicit descendant combinator (when next token starts a selector)
506                // These tokens can start a new selector, indicating an implicit descendant relationship
507                Token::Ident(_) | Token::Hash | Token::Star | Token::LBracket | Token::Colon => {
508                    CombinatorType::Descendant
509                }
510                _ => break,
511            };
512
513            let right = self.parse_compound_selector()?;
514            left = Selector::Combinator {
515                left: Box::new(left),
516                combinator,
517                right: Box::new(right),
518            };
519        }
520
521        Ok(left)
522    }
523
524    /// Parse compound selector (multiple simple selectors on same element)
525    ///
526    /// In CSS-style selectors, a compound can have at most one type selector or ID selector,
527    /// followed by any number of attribute selectors and pseudo-selectors.
528    /// Examples:
529    /// - `component` - just a type selector
530    /// - `#U1` - just an ID selector
531    /// - `pin[type=input]` - type + attribute
532    /// - `component:first` - type + pseudo
533    ///
534    /// But NOT: `component pin` (that's a descendant, not a compound)
535    fn parse_compound_selector(&mut self) -> Result<Selector, QueryError> {
536        let mut parts = Vec::new();
537        let mut has_primary = false; // Track if we have an element/ID/universal selector
538
539        loop {
540            match self.current() {
541                Token::Ident(name) if !has_primary => {
542                    let name = name.clone();
543                    self.advance();
544                    if let Some(elem_type) = ElementType::try_parse(&name) {
545                        parts.push(Selector::Element(elem_type));
546                        has_primary = true;
547                    } else {
548                        return Err(QueryError::UnknownElement(name));
549                    }
550                }
551                Token::Hash if !has_primary => {
552                    self.advance();
553                    match self.current() {
554                        Token::Ident(id) => {
555                            let id = id.clone();
556                            self.advance();
557                            parts.push(Selector::Id(id));
558                            has_primary = true;
559                        }
560                        Token::Number(n) => {
561                            // Allow numeric IDs like #1, #2
562                            let id = n.to_string();
563                            self.advance();
564                            parts.push(Selector::Id(id));
565                            has_primary = true;
566                        }
567                        _ => {
568                            return Err(QueryError::ParseError {
569                                position: self.pos,
570                                message: "Expected identifier after #".to_string(),
571                            });
572                        }
573                    }
574                }
575                Token::Star if !has_primary => {
576                    self.advance();
577                    parts.push(Selector::Universal);
578                    has_primary = true;
579                }
580                Token::LBracket => {
581                    parts.push(self.parse_attribute_selector()?);
582                }
583                Token::Colon => {
584                    parts.push(self.parse_pseudo_selector()?);
585                }
586                _ => break,
587            }
588        }
589
590        if parts.is_empty() {
591            Err(QueryError::ParseError {
592                position: self.pos,
593                message: format!("Expected selector, got {:?}", self.current()),
594            })
595        } else if parts.len() == 1 {
596            Ok(parts.pop().unwrap())
597        } else {
598            Ok(Selector::Compound(parts))
599        }
600    }
601
602    /// Parse [attr=value] selector
603    fn parse_attribute_selector(&mut self) -> Result<Selector, QueryError> {
604        self.expect(&Token::LBracket)?;
605
606        let name = match self.current() {
607            Token::Ident(name) => {
608                let n = name.clone().to_lowercase();
609                self.advance();
610                n
611            }
612            _ => {
613                return Err(QueryError::ParseError {
614                    position: self.pos,
615                    message: "Expected attribute name".to_string(),
616                });
617            }
618        };
619
620        // Check for operator
621        let (op, value) = match self.current() {
622            Token::RBracket => (AttributeOp::Exists, None),
623            Token::Equals => {
624                self.advance();
625                (AttributeOp::Equals, Some(self.parse_value()?))
626            }
627            Token::NotEquals => {
628                self.advance();
629                (AttributeOp::NotEquals, Some(self.parse_value()?))
630            }
631            Token::TildeEquals => {
632                self.advance();
633                (AttributeOp::WordMatch, Some(self.parse_value()?))
634            }
635            Token::CaretEquals => {
636                self.advance();
637                (AttributeOp::StartsWith, Some(self.parse_value()?))
638            }
639            Token::DollarEquals => {
640                self.advance();
641                (AttributeOp::EndsWith, Some(self.parse_value()?))
642            }
643            Token::StarEquals => {
644                self.advance();
645                (AttributeOp::Contains, Some(self.parse_value()?))
646            }
647            Token::Greater => {
648                self.advance();
649                (AttributeOp::GreaterThan, Some(self.parse_value()?))
650            }
651            Token::Less => {
652                self.advance();
653                (AttributeOp::LessThan, Some(self.parse_value()?))
654            }
655            Token::GreaterEquals => {
656                self.advance();
657                (AttributeOp::GreaterOrEqual, Some(self.parse_value()?))
658            }
659            Token::LessEquals => {
660                self.advance();
661                (AttributeOp::LessOrEqual, Some(self.parse_value()?))
662            }
663            _ => {
664                return Err(QueryError::ParseError {
665                    position: self.pos,
666                    message: format!("Expected operator or ], got {:?}", self.current()),
667                });
668            }
669        };
670
671        // Check for case-insensitive flag
672        let case_insensitive = if let Token::Ident(flag) = self.current() {
673            if flag.to_lowercase() == "i" {
674                self.advance();
675                true
676            } else {
677                false
678            }
679        } else {
680            false
681        };
682
683        self.expect(&Token::RBracket)?;
684
685        Ok(Selector::Attribute(AttributeSelector {
686            name,
687            op,
688            value,
689            case_insensitive,
690        }))
691    }
692
693    /// Parse :pseudo or :pseudo(arg)
694    fn parse_pseudo_selector(&mut self) -> Result<Selector, QueryError> {
695        self.expect(&Token::Colon)?;
696
697        let name = match self.current() {
698            Token::Ident(name) => {
699                let n = name.clone().to_lowercase();
700                self.advance();
701                n
702            }
703            _ => {
704                return Err(QueryError::ParseError {
705                    position: self.pos,
706                    message: "Expected pseudo-selector name".to_string(),
707                });
708            }
709        };
710
711        // Handle functional pseudo-selectors: :not() and :has()
712        if name == "not" {
713            self.expect(&Token::LParen)?;
714            let inner = self.parse_selector_list()?;
715            self.expect(&Token::RParen)?;
716            return Ok(Selector::Not(Box::new(inner)));
717        }
718
719        if name == "has" {
720            self.expect(&Token::LParen)?;
721            let inner = self.parse_selector_list()?;
722            self.expect(&Token::RParen)?;
723            return Ok(Selector::Has(Box::new(inner)));
724        }
725
726        // Parse optional argument for pseudo-selectors like :nth(3), :limit(10)
727        let arg = if self.current() == &Token::LParen {
728            self.advance();
729            let arg = self.parse_value()?;
730            self.expect(&Token::RParen)?;
731            Some(arg)
732        } else {
733            None
734        };
735
736        if let Some(pseudo) = PseudoSelector::try_parse(&name, arg.as_deref()) {
737            Ok(Selector::Pseudo(pseudo))
738        } else {
739            Err(QueryError::UnknownPseudo(name))
740        }
741    }
742
743    /// Parse a value (string, number, or identifier)
744    fn parse_value(&mut self) -> Result<String, QueryError> {
745        match self.current().clone() {
746            Token::String(s) => {
747                self.advance();
748                Ok(s)
749            }
750            Token::Number(n) => {
751                self.advance();
752                Ok(n.to_string())
753            }
754            Token::Float(f) => {
755                self.advance();
756                Ok(f.to_string())
757            }
758            Token::Ident(s) => {
759                self.advance();
760                Ok(s)
761            }
762            _ => Err(QueryError::ParseError {
763                position: self.pos,
764                message: format!("Expected value, got {:?}", self.current()),
765            }),
766        }
767    }
768}
769
770#[cfg(test)]
771mod tests {
772    use super::*;
773
774    #[test]
775    fn test_parse_element() {
776        let parser = QueryParser::new();
777        let sel = parser.parse("component").unwrap();
778        assert_eq!(sel, Selector::Element(ElementType::Component));
779    }
780
781    #[test]
782    fn test_parse_id() {
783        let parser = QueryParser::new();
784        let sel = parser.parse("#U1").unwrap();
785        assert_eq!(sel, Selector::Id("U1".to_string()));
786    }
787
788    #[test]
789    fn test_parse_attribute() {
790        let parser = QueryParser::new();
791        let sel = parser.parse("[part=LM7805]").unwrap();
792        match sel {
793            Selector::Attribute(attr) => {
794                assert_eq!(attr.name, "part");
795                assert_eq!(attr.op, AttributeOp::Equals);
796                assert_eq!(attr.value, Some("LM7805".to_string()));
797            }
798            _ => panic!("Expected attribute selector"),
799        }
800    }
801
802    #[test]
803    fn test_parse_attribute_contains() {
804        let parser = QueryParser::new();
805        let sel = parser.parse("[part*=7805]").unwrap();
806        match sel {
807            Selector::Attribute(attr) => {
808                assert_eq!(attr.name, "part");
809                assert_eq!(attr.op, AttributeOp::Contains);
810                assert_eq!(attr.value, Some("7805".to_string()));
811            }
812            _ => panic!("Expected attribute selector"),
813        }
814    }
815
816    #[test]
817    fn test_parse_pseudo() {
818        let parser = QueryParser::new();
819        let sel = parser.parse(":connected").unwrap();
820        assert_eq!(sel, Selector::Pseudo(PseudoSelector::Connected));
821    }
822
823    #[test]
824    fn test_parse_pseudo_with_arg() {
825        let parser = QueryParser::new();
826        let sel = parser.parse(":limit(10)").unwrap();
827        assert_eq!(sel, Selector::Pseudo(PseudoSelector::Limit(10)));
828    }
829
830    #[test]
831    fn test_parse_compound() {
832        let parser = QueryParser::new();
833        let sel = parser.parse("pin[type=input]").unwrap();
834        match sel {
835            Selector::Compound(parts) => {
836                assert_eq!(parts.len(), 2);
837                assert_eq!(parts[0], Selector::Element(ElementType::Pin));
838            }
839            _ => panic!("Expected compound selector"),
840        }
841    }
842
843    #[test]
844    fn test_parse_child_combinator() {
845        let parser = QueryParser::new();
846        let sel = parser.parse("#U1 > pin").unwrap();
847        match sel {
848            Selector::Combinator {
849                left,
850                combinator,
851                right,
852            } => {
853                assert_eq!(*left, Selector::Id("U1".to_string()));
854                assert_eq!(combinator, CombinatorType::Child);
855                assert_eq!(*right, Selector::Element(ElementType::Pin));
856            }
857            _ => panic!("Expected combinator selector"),
858        }
859    }
860
861    #[test]
862    fn test_parse_descendant_combinator() {
863        let parser = QueryParser::new();
864        let sel = parser.parse("#U1 pin").unwrap();
865        match sel {
866            Selector::Combinator { combinator, .. } => {
867                assert_eq!(combinator, CombinatorType::Descendant);
868            }
869            _ => panic!("Expected combinator selector"),
870        }
871    }
872
873    #[test]
874    fn test_parse_union() {
875        let parser = QueryParser::new();
876        let sel = parser.parse("component, port").unwrap();
877        match sel {
878            Selector::Union(selectors) => {
879                assert_eq!(selectors.len(), 2);
880            }
881            _ => panic!("Expected union selector"),
882        }
883    }
884
885    #[test]
886    fn test_parse_not() {
887        let parser = QueryParser::new();
888        let sel = parser.parse(":not(pin)").unwrap();
889        match sel {
890            Selector::Not(inner) => {
891                assert_eq!(*inner, Selector::Element(ElementType::Pin));
892            }
893            _ => panic!("Expected not selector"),
894        }
895    }
896
897    #[test]
898    fn test_parse_complex() {
899        let parser = QueryParser::new();
900        // Complex query: components with 7805 in part name, get their pins
901        let sel = parser
902            .parse("component[part*=7805] > pin[type=input]")
903            .unwrap();
904        match sel {
905            Selector::Combinator {
906                left,
907                combinator,
908                right,
909            } => {
910                assert_eq!(combinator, CombinatorType::Child);
911                match *left {
912                    Selector::Compound(ref parts) => {
913                        assert_eq!(parts.len(), 2);
914                    }
915                    _ => panic!("Expected compound on left"),
916                }
917                match *right {
918                    Selector::Compound(ref parts) => {
919                        assert_eq!(parts.len(), 2);
920                    }
921                    _ => panic!("Expected compound on right"),
922                }
923            }
924            _ => panic!("Expected combinator"),
925        }
926    }
927
928    #[test]
929    fn test_parse_on_net() {
930        let parser = QueryParser::new();
931        let sel = parser.parse("#VCC :: pin").unwrap();
932        match sel {
933            Selector::Combinator { combinator, .. } => {
934                assert_eq!(combinator, CombinatorType::OnNet);
935            }
936            _ => panic!("Expected on-net combinator"),
937        }
938    }
939
940    #[test]
941    fn test_parse_string_value() {
942        let parser = QueryParser::new();
943        let sel = parser.parse("[name=\"CLK IN\"]").unwrap();
944        match sel {
945            Selector::Attribute(attr) => {
946                assert_eq!(attr.value, Some("CLK IN".to_string()));
947            }
948            _ => panic!("Expected attribute selector"),
949        }
950    }
951}