Skip to main content

reflex/
line_filter.rs

1//! Line-based pre-filtering to detect comments and string literals
2//!
3//! This module provides language-specific filters that analyze lines of code
4//! to determine if a pattern match occurs inside a comment or string literal.
5//! This enables us to skip files where ALL matches are in non-code contexts,
6//! avoiding expensive tree-sitter parsing when possible.
7//!
8//! # Performance Impact
9//!
10//! Pre-filtering can reduce tree-sitter parsing workload by 2-5x:
11//! - Pattern "mb_" in Linux kernel: 2,500 files → ~500 files to parse
12//! - Expected speedup: 38s → ~1-2s for symbol queries
13//!
14//! # Design Philosophy
15//!
16//! - **Conservative**: Only skip files when 100% certain ALL matches are in comments/strings
17//! - **Language-specific**: Each language has its own comment/string syntax rules
18//! - **Line-based**: Fast heuristic analysis without full parsing
19//! - **No false negatives**: Never skip files with valid code matches
20
21use crate::models::Language;
22
23/// Trait for language-specific line filtering
24pub trait LineFilter {
25    /// Check if a position in a line is inside a comment
26    ///
27    /// # Arguments
28    /// * `line` - The full line of text
29    /// * `pattern_pos` - Byte position where the pattern starts (0-indexed)
30    ///
31    /// # Returns
32    /// `true` if the pattern is definitely inside a comment, `false` otherwise
33    fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool;
34
35    /// Check if a position in a line is inside a string literal
36    ///
37    /// # Arguments
38    /// * `line` - The full line of text
39    /// * `pattern_pos` - Byte position where the pattern starts (0-indexed)
40    ///
41    /// # Returns
42    /// `true` if the pattern is definitely inside a string literal, `false` otherwise
43    fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool;
44}
45
46/// Get a LineFilter for a specific language
47pub fn get_filter(lang: Language) -> Option<Box<dyn LineFilter>> {
48    match lang {
49        Language::Rust => Some(Box::new(RustLineFilter)),
50        Language::C => Some(Box::new(CLineFilter)),
51        Language::Cpp => Some(Box::new(CppLineFilter)),
52        Language::Go => Some(Box::new(GoLineFilter)),
53        Language::Java => Some(Box::new(JavaLineFilter)),
54        Language::JavaScript => Some(Box::new(JavaScriptLineFilter)),
55        Language::TypeScript => Some(Box::new(TypeScriptLineFilter)),
56        Language::Python => Some(Box::new(PythonLineFilter)),
57        Language::Ruby => Some(Box::new(RubyLineFilter)),
58        Language::PHP => Some(Box::new(PHPLineFilter)),
59        Language::CSharp => Some(Box::new(CSharpLineFilter)),
60        Language::Kotlin => Some(Box::new(KotlinLineFilter)),
61        Language::Zig => Some(Box::new(ZigLineFilter)),
62        Language::Vue => Some(Box::new(VueLineFilter)),
63        Language::Svelte => Some(Box::new(SvelteLineFilter)),
64        Language::Swift | Language::Unknown => None,
65    }
66}
67
68// ============================================================================
69// Rust Line Filter
70// ============================================================================
71
72struct RustLineFilter;
73
74impl LineFilter for RustLineFilter {
75    fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
76        // Check for single-line comment: // before pattern
77        if let Some(comment_start) = line.find("//")
78            && comment_start <= pattern_pos
79        {
80            return true;
81        }
82
83        // Check for multi-line comment start: /* before pattern (unclosed on this line)
84        // Note: We can't reliably detect multi-line comment continuations without state,
85        // so we conservatively return false for those cases
86        if let Some(ml_start) = line.find("/*")
87            && ml_start <= pattern_pos
88        {
89            // Check if comment is closed before pattern
90            if let Some(ml_end) = line[ml_start..].find("*/") {
91                let ml_end_pos = ml_start + ml_end + 2;
92                if pattern_pos >= ml_end_pos {
93                    // Pattern is after comment closure
94                    return false;
95                }
96            }
97            // Comment not closed, or pattern is inside
98            return true;
99        }
100
101        false
102    }
103
104    fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool {
105        // Rust has multiple string types: "...", r"...", r#"..."#, r##"..."##, etc.
106
107        // Check for raw strings first (they don't have escape sequences)
108        if let Some(raw_start) = line.find("r#")
109            && raw_start <= pattern_pos
110        {
111            // Count the number of # symbols
112            let hash_count = line[raw_start + 1..]
113                .chars()
114                .take_while(|&c| c == '#')
115                .count();
116            let closing = format!("\"{}#", "#".repeat(hash_count));
117
118            if let Some(raw_end) = line[raw_start..].find(&closing) {
119                let raw_end_pos = raw_start + raw_end + closing.len();
120                if pattern_pos < raw_end_pos {
121                    return true;
122                }
123            }
124        }
125
126        // Check for simple raw string r"..."
127        if let Some(raw_start) = line.find("r\"")
128            && raw_start <= pattern_pos
129            && let Some(raw_end) = line[raw_start + 2..].find('"')
130        {
131            let raw_end_pos = raw_start + 2 + raw_end + 1;
132            if pattern_pos < raw_end_pos {
133                return true;
134            }
135        }
136
137        // Check for regular strings "..." with escape handling
138        let mut in_string = false;
139        let mut escaped = false;
140
141        for (i, ch) in line.char_indices() {
142            if i >= pattern_pos {
143                return in_string;
144            }
145
146            if escaped {
147                escaped = false;
148                continue;
149            }
150
151            match ch {
152                '\\' if in_string => escaped = true,
153                '"' => in_string = !in_string,
154                _ => {}
155            }
156        }
157
158        false
159    }
160}
161
162// ============================================================================
163// C Line Filter
164// ============================================================================
165
166struct CLineFilter;
167
168impl LineFilter for CLineFilter {
169    fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
170        // Check for single-line comment: // before pattern
171        if let Some(comment_start) = line.find("//")
172            && comment_start <= pattern_pos
173        {
174            return true;
175        }
176
177        // Check for multi-line comment: /* ... */
178        if let Some(ml_start) = line.find("/*")
179            && ml_start <= pattern_pos
180        {
181            if let Some(ml_end) = line[ml_start..].find("*/") {
182                let ml_end_pos = ml_start + ml_end + 2;
183                if pattern_pos >= ml_end_pos {
184                    return false;
185                }
186            }
187            return true;
188        }
189
190        false
191    }
192
193    fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool {
194        // C strings: "..." with escape sequences
195        let mut in_string = false;
196        let mut escaped = false;
197
198        for (i, ch) in line.char_indices() {
199            if i >= pattern_pos {
200                return in_string;
201            }
202
203            if escaped {
204                escaped = false;
205                continue;
206            }
207
208            match ch {
209                '\\' if in_string => escaped = true,
210                '"' => in_string = !in_string,
211                _ => {}
212            }
213        }
214
215        false
216    }
217}
218
219// ============================================================================
220// C++ Line Filter (same as C)
221// ============================================================================
222
223struct CppLineFilter;
224
225impl LineFilter for CppLineFilter {
226    fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
227        CLineFilter.is_in_comment(line, pattern_pos)
228    }
229
230    fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool {
231        CLineFilter.is_in_string(line, pattern_pos)
232    }
233}
234
235// ============================================================================
236// Go Line Filter
237// ============================================================================
238
239struct GoLineFilter;
240
241impl LineFilter for GoLineFilter {
242    fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
243        // Go comments: // and /* */
244        if let Some(comment_start) = line.find("//")
245            && comment_start <= pattern_pos
246        {
247            return true;
248        }
249
250        if let Some(ml_start) = line.find("/*")
251            && ml_start <= pattern_pos
252        {
253            if let Some(ml_end) = line[ml_start..].find("*/") {
254                let ml_end_pos = ml_start + ml_end + 2;
255                if pattern_pos >= ml_end_pos {
256                    return false;
257                }
258            }
259            return true;
260        }
261
262        false
263    }
264
265    fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool {
266        // Go strings: "...", `...` (raw strings with backticks)
267
268        // Check for raw string literals first (backticks)
269        let mut in_raw_string = false;
270        for (i, ch) in line.char_indices() {
271            if i >= pattern_pos {
272                return in_raw_string;
273            }
274            if ch == '`' {
275                in_raw_string = !in_raw_string;
276            }
277        }
278
279        // Check for regular strings
280        let mut in_string = false;
281        let mut escaped = false;
282
283        for (i, ch) in line.char_indices() {
284            if i >= pattern_pos {
285                return in_string;
286            }
287
288            if escaped {
289                escaped = false;
290                continue;
291            }
292
293            match ch {
294                '\\' if in_string => escaped = true,
295                '"' => in_string = !in_string,
296                _ => {}
297            }
298        }
299
300        false
301    }
302}
303
304// ============================================================================
305// Java Line Filter
306// ============================================================================
307
308struct JavaLineFilter;
309
310impl LineFilter for JavaLineFilter {
311    fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
312        // Java comments: //, /* */, /** */ (Javadoc)
313        if let Some(comment_start) = line.find("//")
314            && comment_start <= pattern_pos
315        {
316            return true;
317        }
318
319        if let Some(ml_start) = line.find("/*")
320            && ml_start <= pattern_pos
321        {
322            if let Some(ml_end) = line[ml_start..].find("*/") {
323                let ml_end_pos = ml_start + ml_end + 2;
324                if pattern_pos >= ml_end_pos {
325                    return false;
326                }
327            }
328            return true;
329        }
330
331        false
332    }
333
334    fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool {
335        // Java strings: "..." with escape sequences
336        let mut in_string = false;
337        let mut escaped = false;
338
339        for (i, ch) in line.char_indices() {
340            if i >= pattern_pos {
341                return in_string;
342            }
343
344            if escaped {
345                escaped = false;
346                continue;
347            }
348
349            match ch {
350                '\\' if in_string => escaped = true,
351                '"' => in_string = !in_string,
352                _ => {}
353            }
354        }
355
356        false
357    }
358}
359
360// ============================================================================
361// JavaScript Line Filter
362// ============================================================================
363
364struct JavaScriptLineFilter;
365
366impl LineFilter for JavaScriptLineFilter {
367    fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
368        // JavaScript comments: //, /* */
369        if let Some(comment_start) = line.find("//")
370            && comment_start <= pattern_pos
371        {
372            return true;
373        }
374
375        if let Some(ml_start) = line.find("/*")
376            && ml_start <= pattern_pos
377        {
378            if let Some(ml_end) = line[ml_start..].find("*/") {
379                let ml_end_pos = ml_start + ml_end + 2;
380                if pattern_pos >= ml_end_pos {
381                    return false;
382                }
383            }
384            return true;
385        }
386
387        false
388    }
389
390    fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool {
391        // JavaScript strings: "...", '...', `...` (template literals)
392        let mut in_double_quote = false;
393        let mut in_single_quote = false;
394        let mut in_backtick = false;
395        let mut escaped = false;
396
397        for (i, ch) in line.char_indices() {
398            if i >= pattern_pos {
399                return in_double_quote || in_single_quote || in_backtick;
400            }
401
402            if escaped {
403                escaped = false;
404                continue;
405            }
406
407            match ch {
408                '\\' if (in_double_quote || in_single_quote || in_backtick) => escaped = true,
409                '"' if !in_single_quote && !in_backtick => in_double_quote = !in_double_quote,
410                '\'' if !in_double_quote && !in_backtick => in_single_quote = !in_single_quote,
411                '`' if !in_double_quote && !in_single_quote => in_backtick = !in_backtick,
412                _ => {}
413            }
414        }
415
416        false
417    }
418}
419
420// ============================================================================
421// TypeScript Line Filter (same as JavaScript)
422// ============================================================================
423
424struct TypeScriptLineFilter;
425
426impl LineFilter for TypeScriptLineFilter {
427    fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
428        JavaScriptLineFilter.is_in_comment(line, pattern_pos)
429    }
430
431    fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool {
432        JavaScriptLineFilter.is_in_string(line, pattern_pos)
433    }
434}
435
436// ============================================================================
437// Python Line Filter
438// ============================================================================
439
440struct PythonLineFilter;
441
442impl LineFilter for PythonLineFilter {
443    fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
444        // Python comments: # (single line only)
445        if let Some(comment_start) = line.find('#') {
446            // Make sure # is not inside a string
447            if comment_start <= pattern_pos {
448                // Conservative: assume it's a comment
449                // (We could check if # itself is in a string, but that's complex)
450                return true;
451            }
452        }
453
454        false
455    }
456
457    fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool {
458        // Python strings: "...", '...', """...""", '''...''', f"...", r"...", etc.
459
460        // Check for triple-quoted strings first
461        if let Some(triple_double) = line.find("\"\"\"")
462            && triple_double <= pattern_pos
463        {
464            // Look for closing triple quote
465            if let Some(close) = line[triple_double + 3..].find("\"\"\"") {
466                let close_pos = triple_double + 3 + close + 3;
467                if pattern_pos < close_pos {
468                    return true;
469                }
470            }
471        }
472
473        if let Some(triple_single) = line.find("'''")
474            && triple_single <= pattern_pos
475            && let Some(close) = line[triple_single + 3..].find("'''")
476        {
477            let close_pos = triple_single + 3 + close + 3;
478            if pattern_pos < close_pos {
479                return true;
480            }
481        }
482
483        // Check for single-line strings (with prefix support: f"...", r"...", b"...", etc.)
484        let mut in_double_quote = false;
485        let mut in_single_quote = false;
486        let mut escaped = false;
487
488        for (i, ch) in line.char_indices() {
489            if i >= pattern_pos {
490                return in_double_quote || in_single_quote;
491            }
492
493            if escaped {
494                escaped = false;
495                continue;
496            }
497
498            match ch {
499                '\\' if (in_double_quote || in_single_quote) => escaped = true,
500                '"' if !in_single_quote => in_double_quote = !in_double_quote,
501                '\'' if !in_double_quote => in_single_quote = !in_single_quote,
502                _ => {}
503            }
504        }
505
506        false
507    }
508}
509
510// ============================================================================
511// Ruby Line Filter
512// ============================================================================
513
514struct RubyLineFilter;
515
516impl LineFilter for RubyLineFilter {
517    fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
518        // Ruby comments: # (single line)
519        // Note: Ruby also has =begin...=end multi-line comments, but those are entire-line only
520        if let Some(comment_start) = line.find('#')
521            && comment_start <= pattern_pos
522        {
523            return true;
524        }
525
526        false
527    }
528
529    fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool {
530        // Ruby strings: "...", '...', %q{...}, %Q{...}, etc.
531        // For simplicity, we'll handle the common cases: "..." and '...'
532        let mut in_double_quote = false;
533        let mut in_single_quote = false;
534        let mut escaped = false;
535
536        for (i, ch) in line.char_indices() {
537            if i >= pattern_pos {
538                return in_double_quote || in_single_quote;
539            }
540
541            if escaped {
542                escaped = false;
543                continue;
544            }
545
546            match ch {
547                '\\' if (in_double_quote || in_single_quote) => escaped = true,
548                '"' if !in_single_quote => in_double_quote = !in_double_quote,
549                '\'' if !in_double_quote => in_single_quote = !in_single_quote,
550                _ => {}
551            }
552        }
553
554        false
555    }
556}
557
558// ============================================================================
559// PHP Line Filter
560// ============================================================================
561
562struct PHPLineFilter;
563
564impl LineFilter for PHPLineFilter {
565    fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
566        // PHP comments: //, #, /* */
567        // Check for // comment
568        if let Some(comment_start) = line.find("//")
569            && comment_start <= pattern_pos
570        {
571            return true;
572        }
573
574        // Check for # comment
575        if let Some(comment_start) = line.find('#')
576            && comment_start <= pattern_pos
577        {
578            return true;
579        }
580
581        // Check for /* */ comment
582        if let Some(ml_start) = line.find("/*")
583            && ml_start <= pattern_pos
584        {
585            if let Some(ml_end) = line[ml_start..].find("*/") {
586                let ml_end_pos = ml_start + ml_end + 2;
587                if pattern_pos >= ml_end_pos {
588                    return false;
589                }
590            }
591            return true;
592        }
593
594        false
595    }
596
597    fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool {
598        // PHP strings: "...", '...', with escape sequences
599        let mut in_double_quote = false;
600        let mut in_single_quote = false;
601        let mut escaped = false;
602
603        for (i, ch) in line.char_indices() {
604            if i >= pattern_pos {
605                return in_double_quote || in_single_quote;
606            }
607
608            if escaped {
609                escaped = false;
610                continue;
611            }
612
613            match ch {
614                '\\' if (in_double_quote || in_single_quote) => escaped = true,
615                '"' if !in_single_quote => in_double_quote = !in_double_quote,
616                '\'' if !in_double_quote => in_single_quote = !in_single_quote,
617                _ => {}
618            }
619        }
620
621        false
622    }
623}
624
625// ============================================================================
626// C# Line Filter
627// ============================================================================
628
629struct CSharpLineFilter;
630
631impl LineFilter for CSharpLineFilter {
632    fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
633        // C# comments: //, /* */, /// (XML doc comments)
634        if let Some(comment_start) = line.find("//")
635            && comment_start <= pattern_pos
636        {
637            return true;
638        }
639
640        if let Some(ml_start) = line.find("/*")
641            && ml_start <= pattern_pos
642        {
643            if let Some(ml_end) = line[ml_start..].find("*/") {
644                let ml_end_pos = ml_start + ml_end + 2;
645                if pattern_pos >= ml_end_pos {
646                    return false;
647                }
648            }
649            return true;
650        }
651
652        false
653    }
654
655    fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool {
656        // C# strings: "...", @"..." (verbatim strings)
657
658        // Check for verbatim strings @"..."
659        if let Some(verbatim_start) = line.find("@\"")
660            && verbatim_start <= pattern_pos
661        {
662            // In verbatim strings, "" escapes to single "
663            let mut pos = verbatim_start + 2;
664            let chars: Vec<char> = line.chars().collect();
665
666            while pos < chars.len() {
667                if chars[pos] == '"' {
668                    // Check if it's escaped (double quote)
669                    if pos + 1 < chars.len() && chars[pos + 1] == '"' {
670                        pos += 2;
671                        continue;
672                    }
673                    // End of verbatim string
674                    if pattern_pos <= pos {
675                        return true;
676                    }
677                    break;
678                }
679                pos += 1;
680            }
681        }
682
683        // Check for regular strings "..."
684        let mut in_string = false;
685        let mut escaped = false;
686
687        for (i, ch) in line.char_indices() {
688            if i >= pattern_pos {
689                return in_string;
690            }
691
692            if escaped {
693                escaped = false;
694                continue;
695            }
696
697            match ch {
698                '\\' if in_string => escaped = true,
699                '"' => in_string = !in_string,
700                _ => {}
701            }
702        }
703
704        false
705    }
706}
707
708// ============================================================================
709// Kotlin Line Filter
710// ============================================================================
711
712struct KotlinLineFilter;
713
714impl LineFilter for KotlinLineFilter {
715    fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
716        // Kotlin comments: //, /* */
717        if let Some(comment_start) = line.find("//")
718            && comment_start <= pattern_pos
719        {
720            return true;
721        }
722
723        if let Some(ml_start) = line.find("/*")
724            && ml_start <= pattern_pos
725        {
726            if let Some(ml_end) = line[ml_start..].find("*/") {
727                let ml_end_pos = ml_start + ml_end + 2;
728                if pattern_pos >= ml_end_pos {
729                    return false;
730                }
731            }
732            return true;
733        }
734
735        false
736    }
737
738    fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool {
739        // Kotlin strings: "...", """...""" (raw strings)
740
741        // Check for triple-quoted strings first
742        if let Some(triple_start) = line.find("\"\"\"")
743            && triple_start <= pattern_pos
744            && let Some(close) = line[triple_start + 3..].find("\"\"\"")
745        {
746            let close_pos = triple_start + 3 + close + 3;
747            if pattern_pos < close_pos {
748                return true;
749            }
750        }
751
752        // Check for regular strings "..."
753        let mut in_string = false;
754        let mut escaped = false;
755
756        for (i, ch) in line.char_indices() {
757            if i >= pattern_pos {
758                return in_string;
759            }
760
761            if escaped {
762                escaped = false;
763                continue;
764            }
765
766            match ch {
767                '\\' if in_string => escaped = true,
768                '"' => in_string = !in_string,
769                _ => {}
770            }
771        }
772
773        false
774    }
775}
776
777// ============================================================================
778// Zig Line Filter
779// ============================================================================
780
781struct ZigLineFilter;
782
783impl LineFilter for ZigLineFilter {
784    fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
785        // Zig comments: // and /// (doc comments)
786        if let Some(comment_start) = line.find("//")
787            && comment_start <= pattern_pos
788        {
789            return true;
790        }
791
792        false
793    }
794
795    fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool {
796        // Zig strings: "..." and \\ (multiline string literals)
797        // For simplicity, we'll handle regular strings here
798        let mut in_string = false;
799        let mut escaped = false;
800
801        for (i, ch) in line.char_indices() {
802            if i >= pattern_pos {
803                return in_string;
804            }
805
806            if escaped {
807                escaped = false;
808                continue;
809            }
810
811            match ch {
812                '\\' if in_string => escaped = true,
813                '"' => in_string = !in_string,
814                _ => {}
815            }
816        }
817
818        false
819    }
820}
821
822// ============================================================================
823// Vue Line Filter (use JavaScript/TypeScript for <script> sections)
824// ============================================================================
825
826struct VueLineFilter;
827
828impl LineFilter for VueLineFilter {
829    fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
830        // Vue uses JS/TS in <script> sections, HTML comments in <template>
831        // For simplicity, use JavaScript-style comments
832        JavaScriptLineFilter.is_in_comment(line, pattern_pos)
833    }
834
835    fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool {
836        JavaScriptLineFilter.is_in_string(line, pattern_pos)
837    }
838}
839
840// ============================================================================
841// Svelte Line Filter (use JavaScript/TypeScript)
842// ============================================================================
843
844struct SvelteLineFilter;
845
846impl LineFilter for SvelteLineFilter {
847    fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
848        JavaScriptLineFilter.is_in_comment(line, pattern_pos)
849    }
850
851    fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool {
852        JavaScriptLineFilter.is_in_string(line, pattern_pos)
853    }
854}
855
856#[cfg(test)]
857mod tests {
858    use super::*;
859
860    // ========================================================================
861    // Rust Tests
862    // ========================================================================
863
864    #[test]
865    fn test_rust_single_line_comment() {
866        let filter = RustLineFilter;
867        let line = "let x = 5; // extract_symbols here";
868        assert!(filter.is_in_comment(line, 15)); // "extract" is in comment
869        assert!(!filter.is_in_comment(line, 4)); // "x" is not in comment
870    }
871
872    #[test]
873    fn test_rust_multiline_comment() {
874        let filter = RustLineFilter;
875        let line = "let x = /* extract_symbols */ 5;";
876        assert!(filter.is_in_comment(line, 11)); // "extract" is in comment
877        assert!(!filter.is_in_comment(line, 30)); // "5" is not in comment
878    }
879
880    #[test]
881    fn test_rust_string_literal() {
882        let filter = RustLineFilter;
883        let line = r#"let s = "extract_symbols";"#;
884        assert!(filter.is_in_string(line, 9)); // "extract" is in string
885        assert!(!filter.is_in_string(line, 27)); // after string
886    }
887
888    #[test]
889    fn test_rust_raw_string() {
890        let filter = RustLineFilter;
891        let line = r#"let s = r"extract_symbols";"#;
892        assert!(filter.is_in_string(line, 10)); // "extract" is in raw string
893    }
894
895    #[test]
896    fn test_rust_raw_string_with_hashes() {
897        let filter = RustLineFilter;
898        let line = r###"let s = r#"extract_symbols"#;"###;
899        assert!(filter.is_in_string(line, 11)); // "extract" is in raw string
900    }
901
902    #[test]
903    fn test_rust_escaped_quote() {
904        let filter = RustLineFilter;
905        let line = r#"let s = "before \" extract_symbols after";"#;
906        assert!(filter.is_in_string(line, 15)); // "extract" is in string
907    }
908
909    // ========================================================================
910    // JavaScript Tests
911    // ========================================================================
912
913    #[test]
914    fn test_js_single_line_comment() {
915        let filter = JavaScriptLineFilter;
916        let line = "let x = 5; // extract_symbols here";
917        assert!(filter.is_in_comment(line, 15));
918        assert!(!filter.is_in_comment(line, 4));
919    }
920
921    #[test]
922    fn test_js_string_double_quote() {
923        let filter = JavaScriptLineFilter;
924        let line = r#"let s = "extract_symbols";"#;
925        assert!(filter.is_in_string(line, 9));
926        assert!(!filter.is_in_string(line, 27));
927    }
928
929    #[test]
930    fn test_js_string_single_quote() {
931        let filter = JavaScriptLineFilter;
932        let line = "let s = 'extract_symbols';";
933        assert!(filter.is_in_string(line, 9));
934    }
935
936    #[test]
937    fn test_js_template_literal() {
938        let filter = JavaScriptLineFilter;
939        let line = "let s = `extract_symbols`;";
940        assert!(filter.is_in_string(line, 9));
941    }
942
943    // ========================================================================
944    // Python Tests
945    // ========================================================================
946
947    #[test]
948    fn test_python_comment() {
949        let filter = PythonLineFilter;
950        let line = "x = 5  # extract_symbols here";
951        assert!(filter.is_in_comment(line, 9));
952        assert!(!filter.is_in_comment(line, 0));
953    }
954
955    #[test]
956    fn test_python_string() {
957        let filter = PythonLineFilter;
958        let line = r#"s = "extract_symbols""#;
959        assert!(filter.is_in_string(line, 5));
960    }
961
962    #[test]
963    fn test_python_triple_quote() {
964        let filter = PythonLineFilter;
965        let line = r#"s = """extract_symbols""""#;
966        assert!(filter.is_in_string(line, 7));
967    }
968
969    // ========================================================================
970    // Go Tests
971    // ========================================================================
972
973    #[test]
974    fn test_go_raw_string() {
975        let filter = GoLineFilter;
976        let line = "s := `extract_symbols`";
977        assert!(filter.is_in_string(line, 6));
978    }
979
980    // ========================================================================
981    // C# Tests
982    // ========================================================================
983
984    #[test]
985    fn test_csharp_verbatim_string() {
986        let filter = CSharpLineFilter;
987        let line = r#"string s = @"extract_symbols";"#;
988        assert!(filter.is_in_string(line, 13));
989    }
990
991    #[test]
992    fn test_csharp_verbatim_escaped_quote() {
993        let filter = CSharpLineFilter;
994        let line = r#"string s = @"before "" extract_symbols after";"#;
995        assert!(filter.is_in_string(line, 19));
996    }
997}