1- use super :: { Comment , CommentOutcome , CommentStatus } ;
1+ use std:: collections:: { HashMap , HashSet } ;
2+ use std:: path:: Path ;
23
3- pub fn derive_comment_outcomes ( comment : & Comment , stale_review : bool ) -> Vec < CommentOutcome > {
4+ use crate :: core:: { diff_parser:: ChangeType , UnifiedDiff } ;
5+
6+ use super :: { Comment , CommentOutcome , CommentOutcomeContext , CommentStatus } ;
7+
8+ fn push_unique ( outcomes : & mut Vec < CommentOutcome > , outcome : CommentOutcome ) {
9+ if !outcomes. contains ( & outcome) {
10+ outcomes. push ( outcome) ;
11+ }
12+ }
13+
14+ fn normalize_comment_path ( path : & Path ) -> String {
15+ path. to_string_lossy ( )
16+ . replace ( '\\' , "/" )
17+ . trim_start_matches ( "./" )
18+ . to_string ( )
19+ }
20+
21+ pub fn infer_addressed_by_follow_up_comments (
22+ comments : & [ Comment ] ,
23+ follow_up_diffs : & [ UnifiedDiff ] ,
24+ ) -> HashSet < String > {
25+ let mut changed_old_lines_by_path: HashMap < String , HashSet < usize > > = HashMap :: new ( ) ;
26+
27+ for diff in follow_up_diffs {
28+ let changed_old_lines = changed_old_lines_by_path
29+ . entry ( normalize_comment_path ( diff. file_path . as_path ( ) ) )
30+ . or_default ( ) ;
31+
32+ for hunk in & diff. hunks {
33+ for line in & hunk. changes {
34+ if line. change_type != ChangeType :: Context {
35+ if let Some ( old_line_no) = line. old_line_no {
36+ changed_old_lines. insert ( old_line_no) ;
37+ }
38+ }
39+ }
40+ }
41+ }
42+
43+ comments
44+ . iter ( )
45+ . filter ( |comment| comment. status == CommentStatus :: Open )
46+ . filter_map ( |comment| {
47+ let path = normalize_comment_path ( comment. file_path . as_path ( ) ) ;
48+ changed_old_lines_by_path
49+ . get ( & path)
50+ . filter ( |changed_lines| changed_lines. contains ( & comment. line_number ) )
51+ . map ( |_| comment. id . clone ( ) )
52+ } )
53+ . collect ( )
54+ }
55+
56+ pub fn derive_comment_outcomes (
57+ comment : & Comment ,
58+ context : CommentOutcomeContext ,
59+ ) -> Vec < CommentOutcome > {
460 let mut outcomes = Vec :: new ( ) ;
561
662 match comment
@@ -10,31 +66,40 @@ pub fn derive_comment_outcomes(comment: &Comment, stale_review: bool) -> Vec<Com
1066 . map ( str:: to_ascii_lowercase)
1167 . as_deref ( )
1268 {
13- Some ( "accept" ) => outcomes . push ( CommentOutcome :: Accepted ) ,
14- Some ( "reject" ) => outcomes . push ( CommentOutcome :: Rejected ) ,
69+ Some ( "accept" ) => push_unique ( & mut outcomes , CommentOutcome :: Accepted ) ,
70+ Some ( "reject" ) => push_unique ( & mut outcomes , CommentOutcome :: Rejected ) ,
1571 _ => { }
1672 }
1773
18- if comment. status == CommentStatus :: Resolved {
19- outcomes. push ( CommentOutcome :: Addressed ) ;
74+ if comment. status == CommentStatus :: Resolved || context. addressed_by_follow_up {
75+ push_unique ( & mut outcomes, CommentOutcome :: Addressed ) ;
76+ }
77+
78+ if context. auto_fixed {
79+ push_unique ( & mut outcomes, CommentOutcome :: Addressed ) ;
80+ push_unique ( & mut outcomes, CommentOutcome :: AutoFixed ) ;
2081 }
2182
22- if comment. status == CommentStatus :: Open && stale_review {
23- outcomes . push ( CommentOutcome :: Stale ) ;
83+ if comment. status == CommentStatus :: Open && context . stale_review {
84+ push_unique ( & mut outcomes , CommentOutcome :: Stale ) ;
2485 }
2586
2687 if outcomes. is_empty ( ) && comment. status == CommentStatus :: Open {
27- outcomes . push ( CommentOutcome :: New ) ;
88+ push_unique ( & mut outcomes , CommentOutcome :: New ) ;
2889 }
2990
3091 outcomes
3192}
3293
3394#[ cfg( test) ]
3495mod tests {
96+ use std:: collections:: HashSet ;
3597 use std:: path:: PathBuf ;
3698
37- use crate :: core:: comment:: { Category , Comment , FixEffort , Severity } ;
99+ use crate :: core:: {
100+ comment:: { Category , Comment , CommentOutcomeContext , FixEffort , Severity } ,
101+ DiffParser ,
102+ } ;
38103
39104 use super :: * ;
40105
@@ -61,7 +126,7 @@ mod tests {
61126 #[ test]
62127 fn derives_new_for_open_comments_without_other_signals ( ) {
63128 assert_eq ! (
64- derive_comment_outcomes( & make_comment( ) , false ) ,
129+ derive_comment_outcomes( & make_comment( ) , CommentOutcomeContext :: default ( ) ) ,
65130 vec![ CommentOutcome :: New ]
66131 ) ;
67132 }
@@ -73,7 +138,7 @@ mod tests {
73138 comment. status = CommentStatus :: Resolved ;
74139
75140 assert_eq ! (
76- derive_comment_outcomes( & comment, false ) ,
141+ derive_comment_outcomes( & comment, CommentOutcomeContext :: default ( ) ) ,
77142 vec![ CommentOutcome :: Accepted , CommentOutcome :: Addressed ]
78143 ) ;
79144 }
@@ -84,24 +149,79 @@ mod tests {
84149 comment. feedback = Some ( "reject" . to_string ( ) ) ;
85150
86151 assert_eq ! (
87- derive_comment_outcomes( & comment, false ) ,
152+ derive_comment_outcomes( & comment, CommentOutcomeContext :: default ( ) ) ,
88153 vec![ CommentOutcome :: Rejected ]
89154 ) ;
90155 }
91156
92157 #[ test]
93158 fn derives_stale_for_open_comments_in_stale_reviews ( ) {
94159 assert_eq ! (
95- derive_comment_outcomes( & make_comment( ) , true ) ,
160+ derive_comment_outcomes(
161+ & make_comment( ) ,
162+ CommentOutcomeContext {
163+ stale_review: true ,
164+ ..CommentOutcomeContext :: default ( )
165+ }
166+ ) ,
96167 vec![ CommentOutcome :: Stale ]
97168 ) ;
98169 }
99170
171+ #[ test]
172+ fn derives_addressed_for_open_comments_touched_by_follow_up_commits ( ) {
173+ assert_eq ! (
174+ derive_comment_outcomes(
175+ & make_comment( ) ,
176+ CommentOutcomeContext {
177+ addressed_by_follow_up: true ,
178+ ..CommentOutcomeContext :: default ( )
179+ }
180+ ) ,
181+ vec![ CommentOutcome :: Addressed ]
182+ ) ;
183+ }
184+
100185 #[ test]
101186 fn dismissed_comments_keep_lifecycle_without_derived_outcomes ( ) {
102187 let mut comment = make_comment ( ) ;
103188 comment. status = CommentStatus :: Dismissed ;
104189
105- assert ! ( derive_comment_outcomes( & comment, false ) . is_empty( ) ) ;
190+ assert ! ( derive_comment_outcomes( & comment, CommentOutcomeContext :: default ( ) ) . is_empty( ) ) ;
191+ }
192+
193+ #[ test]
194+ fn infer_follow_up_marks_open_comments_as_addressed_when_old_line_changes ( ) {
195+ let diffs = DiffParser :: parse_text_diff (
196+ "first\n second\n third\n " ,
197+ "first\n updated second\n third\n " ,
198+ PathBuf :: from ( "src/lib.rs" ) ,
199+ )
200+ . unwrap ( ) ;
201+
202+ let mut comment = make_comment ( ) ;
203+ comment. line_number = 2 ;
204+
205+ assert_eq ! (
206+ infer_addressed_by_follow_up_comments( & [ comment] , & [ diffs] )
207+ . into_iter( )
208+ . collect:: <HashSet <_>>( ) ,
209+ HashSet :: from( [ "comment-1" . to_string( ) ] )
210+ ) ;
211+ }
212+
213+ #[ test]
214+ fn infer_follow_up_ignores_context_only_line_matches ( ) {
215+ let diffs = DiffParser :: parse_text_diff (
216+ "first\n second\n third\n " ,
217+ "first\n inserted\n second\n third\n " ,
218+ PathBuf :: from ( "src/lib.rs" ) ,
219+ )
220+ . unwrap ( ) ;
221+
222+ let mut comment = make_comment ( ) ;
223+ comment. line_number = 2 ;
224+
225+ assert ! ( infer_addressed_by_follow_up_comments( & [ comment] , & [ diffs] ) . is_empty( ) ) ;
106226 }
107227}
0 commit comments