@@ -246,12 +246,15 @@ fn parse_json_format(content: &str, file_path: &Path) -> Vec<core::comment::RawC
246246 return items
247247 . iter ( )
248248 . filter_map ( |item| {
249- let line = item
249+ let line_value = item
250250 . get ( "line" )
251251 . or_else ( || item. get ( "line_number" ) )
252- . or_else ( || item. get ( "lineNumber" ) )
253- . and_then ( |v| v. as_u64 ( ) )
254- . map ( |v| v as usize ) ?;
252+ . or_else ( || item. get ( "lineNumber" ) ) ;
253+ let line = line_value. and_then ( |v| {
254+ v. as_u64 ( )
255+ . map ( |n| n as usize )
256+ . or_else ( || v. as_str ( ) . and_then ( |s| s. trim ( ) . parse :: < usize > ( ) . ok ( ) ) )
257+ } ) ?;
255258 let text = item
256259 . get ( "issue" )
257260 . or_else ( || item. get ( "description" ) )
@@ -703,4 +706,188 @@ let data = &input;
703706 assert_eq ! ( comments. len( ) , 1 ) ;
704707 assert ! ( comments[ 0 ] . code_suggestion. is_some( ) ) ;
705708 }
709+
710+ // ── Mutation-testing gap fills ─────────────────────────────────────
711+
712+ #[ test]
713+ fn parse_primary_skips_code_fence_markers ( ) {
714+ // ``` markers themselves are skipped (not parsed as comments)
715+ let input = "```rust\n ```" ;
716+ let file_path = PathBuf :: from ( "src/lib.rs" ) ;
717+ let comments = parse_llm_response ( input, & file_path) . unwrap ( ) ;
718+ assert ! ( comments. is_empty( ) ) ;
719+ }
720+
721+ #[ test]
722+ fn parse_primary_skips_heading_lines ( ) {
723+ let input = "# Code Review\n Line 10: Real issue" ;
724+ let file_path = PathBuf :: from ( "src/lib.rs" ) ;
725+ let comments = parse_llm_response ( input, & file_path) . unwrap ( ) ;
726+ // Heading line skipped, but real Line 10 comment caught
727+ assert_eq ! ( comments. len( ) , 1 ) ;
728+ assert_eq ! ( comments[ 0 ] . line_number, 10 ) ;
729+ }
730+
731+ #[ test]
732+ fn parse_primary_skips_preamble_lines ( ) {
733+ let input = "Here are the issues I found:\n Line 5: Missing check" ;
734+ let file_path = PathBuf :: from ( "src/lib.rs" ) ;
735+ let comments = parse_llm_response ( input, & file_path) . unwrap ( ) ;
736+ assert_eq ! ( comments. len( ) , 1 ) ;
737+ assert_eq ! ( comments[ 0 ] . line_number, 5 ) ;
738+ }
739+
740+ #[ test]
741+ fn parse_json_in_code_block_strategy ( ) {
742+ // Test that extract_json_from_code_block specifically works
743+ let input = "Here are findings:\n ```json\n [{\" line\" : 7, \" issue\" : \" Off by one\" }]\n ```" ;
744+ let file_path = PathBuf :: from ( "src/lib.rs" ) ;
745+ let comments = parse_llm_response ( input, & file_path) . unwrap ( ) ;
746+ assert_eq ! ( comments. len( ) , 1 ) ;
747+ assert_eq ! ( comments[ 0 ] . line_number, 7 ) ;
748+ assert ! ( comments[ 0 ] . content. contains( "Off by one" ) ) ;
749+ }
750+
751+ #[ test]
752+ fn parse_json_bare_array_strategy ( ) {
753+ // Test find_json_array with text before/after
754+ let input = "Issues found: [{\" line\" : 3, \" issue\" : \" Bug\" }] end." ;
755+ let file_path = PathBuf :: from ( "src/lib.rs" ) ;
756+ let comments = parse_llm_response ( input, & file_path) . unwrap ( ) ;
757+ assert_eq ! ( comments. len( ) , 1 ) ;
758+ assert_eq ! ( comments[ 0 ] . line_number, 3 ) ;
759+ }
760+
761+ // ── Adversarial edge cases ──────────────────────────────────────────
762+
763+ #[ test]
764+ fn parse_line_zero_not_panicking ( ) {
765+ // Line 0 is technically invalid but should not panic
766+ let input = "Line 0: Edge case at line zero." ;
767+ let file_path = PathBuf :: from ( "src/lib.rs" ) ;
768+ let comments = parse_llm_response ( input, & file_path) . unwrap ( ) ;
769+ assert_eq ! ( comments. len( ) , 1 ) ;
770+ assert_eq ! ( comments[ 0 ] . line_number, 0 ) ;
771+ }
772+
773+ #[ test]
774+ fn parse_huge_line_number_no_overflow ( ) {
775+ let input = "Line 999999999999: Absurd line number." ;
776+ let file_path = PathBuf :: from ( "src/lib.rs" ) ;
777+ // Should either parse successfully or return empty, not panic
778+ let result = parse_llm_response ( input, & file_path) ;
779+ assert ! ( result. is_ok( ) || result. is_err( ) ) ;
780+ }
781+
782+ #[ test]
783+ fn parse_unicode_content_no_panic ( ) {
784+ let input = "Line 10: 漏洞 — SQL注入风险 🔴" ;
785+ let file_path = PathBuf :: from ( "src/lib.rs" ) ;
786+ let comments = parse_llm_response ( input, & file_path) . unwrap ( ) ;
787+ assert_eq ! ( comments. len( ) , 1 ) ;
788+ assert ! ( comments[ 0 ] . content. contains( "漏洞" ) ) ;
789+ }
790+
791+ #[ test]
792+ fn parse_numbered_list_with_no_line_number_in_path ( ) {
793+ // Numbered list where file path is missing the colon-number
794+ let input = "1. **src/lib.rs** - Missing null check" ;
795+ let file_path = PathBuf :: from ( "src/lib.rs" ) ;
796+ let comments = parse_llm_response ( input, & file_path) . unwrap ( ) ;
797+ // Should not extract a comment with a bogus line number
798+ for c in & comments {
799+ assert ! ( c. line_number > 0 || comments. is_empty( ) ) ;
800+ }
801+ }
802+
803+ #[ test]
804+ fn parse_json_with_nested_brackets ( ) {
805+ // JSON with nested arrays/objects should not confuse the bracket finder
806+ let input =
807+ r#"[{"line": 10, "issue": "Bug with [array] access", "details": {"nested": [1,2,3]}}]"# ;
808+ let file_path = PathBuf :: from ( "src/lib.rs" ) ;
809+ let comments = parse_llm_response ( input, & file_path) . unwrap ( ) ;
810+ assert_eq ! ( comments. len( ) , 1 ) ;
811+ assert_eq ! ( comments[ 0 ] . line_number, 10 ) ;
812+ }
813+
814+ #[ test]
815+ fn parse_json_with_line_number_as_string ( ) {
816+ // Some LLMs return line numbers as strings — should be handled
817+ let input = r#"[{"line": "42", "issue": "Bug"}]"# ;
818+ let file_path = PathBuf :: from ( "src/lib.rs" ) ;
819+ let comments = parse_llm_response ( input, & file_path) . unwrap ( ) ;
820+ assert_eq ! ( comments. len( ) , 1 ) ;
821+ assert_eq ! ( comments[ 0 ] . line_number, 42 ) ;
822+ }
823+
824+ #[ test]
825+ fn parse_malformed_json_no_panic ( ) {
826+ let input = r#"[{"line": 10, "issue": "unclosed string}]"# ;
827+ let file_path = PathBuf :: from ( "src/lib.rs" ) ;
828+ let comments = parse_llm_response ( input, & file_path) . unwrap ( ) ;
829+ assert ! ( comments. is_empty( ) ) ;
830+ }
831+
832+ #[ test]
833+ fn parse_mixed_strategies_first_wins ( ) {
834+ // Input matches both primary AND numbered list — primary should win
835+ let input = "Line 10: Primary format.\n 1. **src/lib.rs:20** - Numbered format." ;
836+ let file_path = PathBuf :: from ( "src/lib.rs" ) ;
837+ let comments = parse_llm_response ( input, & file_path) . unwrap ( ) ;
838+ // Primary parser should have caught Line 10, so we get that
839+ assert_eq ! ( comments[ 0 ] . line_number, 10 ) ;
840+ }
841+
842+ #[ test]
843+ fn parse_code_suggestion_without_preceding_comment ( ) {
844+ // <<<ORIGINAL block with no prior Line N: comment
845+ let input = "<<<ORIGINAL\n old code\n ===\n new code\n >>>SUGGESTED" ;
846+ let file_path = PathBuf :: from ( "src/lib.rs" ) ;
847+ let comments = parse_llm_response ( input, & file_path) . unwrap ( ) ;
848+ // Should not panic; suggestion just gets dropped since no comment to attach to
849+ assert ! ( comments. is_empty( ) ) ;
850+ }
851+
852+ #[ test]
853+ fn parse_unclosed_code_suggestion_block ( ) {
854+ // <<<ORIGINAL without >>>SUGGESTED
855+ let input = "Line 5: Issue here.\n <<<ORIGINAL\n old code\n ===\n new code" ;
856+ let file_path = PathBuf :: from ( "src/lib.rs" ) ;
857+ let comments = parse_llm_response ( input, & file_path) . unwrap ( ) ;
858+ assert_eq ! ( comments. len( ) , 1 ) ;
859+ // The code suggestion should be None since the block was never closed
860+ assert ! ( comments[ 0 ] . code_suggestion. is_none( ) ) ;
861+ }
862+
863+ #[ test]
864+ fn parse_only_whitespace_input ( ) {
865+ let input = " \n \n \t \n " ;
866+ let file_path = PathBuf :: from ( "src/lib.rs" ) ;
867+ let comments = parse_llm_response ( input, & file_path) . unwrap ( ) ;
868+ assert ! ( comments. is_empty( ) ) ;
869+ }
870+
871+ #[ test]
872+ fn parse_json_in_code_block_with_extra_text ( ) {
873+ let input = "Here are my findings:\n ```json\n [{\" line\" : 5, \" issue\" : \" Bug\" }]\n ```\n Let me know if you need more details." ;
874+ let file_path = PathBuf :: from ( "src/lib.rs" ) ;
875+ let comments = parse_llm_response ( input, & file_path) . unwrap ( ) ;
876+ assert_eq ! ( comments. len( ) , 1 ) ;
877+ assert_eq ! ( comments[ 0 ] . line_number, 5 ) ;
878+ }
879+
880+ #[ test]
881+ fn parse_file_line_format_does_not_match_urls ( ) {
882+ // URLs with port numbers like http://localhost:8080 should not be parsed as file:line
883+ let input = "Visit http://localhost:8080 for the dashboard." ;
884+ let file_path = PathBuf :: from ( "src/lib.rs" ) ;
885+ let comments = parse_llm_response ( input, & file_path) . unwrap ( ) ;
886+ // Should not extract port 8080 as a line number
887+ assert ! (
888+ comments. is_empty( ) ,
889+ "URL port should not be parsed as line number, got {:?}" ,
890+ comments. iter( ) . map( |c| c. line_number) . collect:: <Vec <_>>( )
891+ ) ;
892+ }
706893}
0 commit comments