1use crate::models::Language;
22
23pub trait LineFilter {
25 fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool;
34
35 fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool;
44}
45
46pub 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
68struct RustLineFilter;
73
74impl LineFilter for RustLineFilter {
75 fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
76 if let Some(comment_start) = line.find("//")
78 && comment_start <= pattern_pos
79 {
80 return true;
81 }
82
83 if let Some(ml_start) = line.find("/*")
87 && ml_start <= pattern_pos
88 {
89 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 return false;
95 }
96 }
97 return true;
99 }
100
101 false
102 }
103
104 fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool {
105 if let Some(raw_start) = line.find("r#")
109 && raw_start <= pattern_pos
110 {
111 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 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 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
162struct CLineFilter;
167
168impl LineFilter for CLineFilter {
169 fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
170 if let Some(comment_start) = line.find("//")
172 && comment_start <= pattern_pos
173 {
174 return true;
175 }
176
177 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 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
219struct 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
235struct GoLineFilter;
240
241impl LineFilter for GoLineFilter {
242 fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
243 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 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 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
304struct JavaLineFilter;
309
310impl LineFilter for JavaLineFilter {
311 fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
312 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 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
360struct JavaScriptLineFilter;
365
366impl LineFilter for JavaScriptLineFilter {
367 fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
368 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 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
420struct 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
436struct PythonLineFilter;
441
442impl LineFilter for PythonLineFilter {
443 fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
444 if let Some(comment_start) = line.find('#') {
446 if comment_start <= pattern_pos {
448 return true;
451 }
452 }
453
454 false
455 }
456
457 fn is_in_string(&self, line: &str, pattern_pos: usize) -> bool {
458 if let Some(triple_double) = line.find("\"\"\"")
462 && triple_double <= pattern_pos
463 {
464 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 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
510struct RubyLineFilter;
515
516impl LineFilter for RubyLineFilter {
517 fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
518 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 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
558struct PHPLineFilter;
563
564impl LineFilter for PHPLineFilter {
565 fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
566 if let Some(comment_start) = line.find("//")
569 && comment_start <= pattern_pos
570 {
571 return true;
572 }
573
574 if let Some(comment_start) = line.find('#')
576 && comment_start <= pattern_pos
577 {
578 return true;
579 }
580
581 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 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
625struct CSharpLineFilter;
630
631impl LineFilter for CSharpLineFilter {
632 fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
633 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 if let Some(verbatim_start) = line.find("@\"")
660 && verbatim_start <= pattern_pos
661 {
662 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 if pos + 1 < chars.len() && chars[pos + 1] == '"' {
670 pos += 2;
671 continue;
672 }
673 if pattern_pos <= pos {
675 return true;
676 }
677 break;
678 }
679 pos += 1;
680 }
681 }
682
683 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
708struct KotlinLineFilter;
713
714impl LineFilter for KotlinLineFilter {
715 fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
716 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 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 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
777struct ZigLineFilter;
782
783impl LineFilter for ZigLineFilter {
784 fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
785 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 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
822struct VueLineFilter;
827
828impl LineFilter for VueLineFilter {
829 fn is_in_comment(&self, line: &str, pattern_pos: usize) -> bool {
830 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
840struct 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 #[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)); assert!(!filter.is_in_comment(line, 4)); }
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)); assert!(!filter.is_in_comment(line, 30)); }
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)); assert!(!filter.is_in_string(line, 27)); }
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)); }
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)); }
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)); }
908
909 #[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 #[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 #[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 #[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}