22
33use oxc_allocator:: Dummy ;
44use oxc_diagnostics:: OxcDiagnostic ;
5+ use oxc_span:: Span ;
56
67use crate :: { ParserImpl , diagnostics, lexer:: Kind } ;
78
@@ -25,6 +26,15 @@ impl<'a> ParserImpl<'a> {
2526 self . set_fatal_error ( error) ;
2627 return ;
2728 }
29+
30+ // Check if this looks like a merge conflict marker
31+ if let Some ( start_span) = self . is_merge_conflict_marker ( ) {
32+ let ( middle_span, end_span) = self . find_merge_conflict_markers ( ) ;
33+ let error = diagnostics:: merge_conflict_marker ( start_span, middle_span, end_span) ;
34+ self . set_fatal_error ( error) ;
35+ return ;
36+ }
37+
2838 let error = diagnostics:: unexpected_token ( self . cur_token ( ) . span ( ) ) ;
2939 self . set_fatal_error ( error) ;
3040 }
@@ -71,3 +81,117 @@ impl<'a> ParserImpl<'a> {
7181 matches ! ( self . cur_kind( ) , Kind :: Eof | Kind :: Undetermined ) || self . fatal_error . is_some ( )
7282 }
7383}
84+
85+ // ==================== Merge Conflict Marker Detection ====================
86+ //
87+ // Git merge conflict markers detection and error recovery.
88+ //
89+ // This provides enhanced diagnostics when the parser encounters Git merge conflict markers
90+ // (e.g., `<<<<<<<`, `=======`, `>>>>>>>`). Instead of showing a generic "Unexpected token"
91+ // error, we detect these patterns and provide helpful guidance on how to resolve the conflict.
92+ //
93+ // Inspired by rust-lang/rust#106242
94+ impl ParserImpl < ' _ > {
95+ /// Check if the current position looks like a merge conflict marker.
96+ ///
97+ /// Detects the following Git conflict markers:
98+ /// - `<<<<<<<` - Start marker (ours)
99+ /// - `=======` - Middle separator
100+ /// - `>>>>>>>` - End marker (theirs)
101+ /// - `|||||||` - Diff3 format (common ancestor)
102+ ///
103+ /// Returns the span of the marker if detected, None otherwise.
104+ ///
105+ /// # False Positive Prevention
106+ ///
107+ /// Git conflict markers always appear at the start of a line. To prevent false positives
108+ /// from operator sequences in valid code (e.g., `a << << b`), we verify that the first
109+ /// token is on a new line using the `is_on_new_line()` flag from the lexer.
110+ ///
111+ /// The special case `span.start == 0` handles the beginning of the file, where
112+ /// `is_on_new_line()` may be false but a conflict marker is still valid.
113+ fn is_merge_conflict_marker ( & self ) -> Option < Span > {
114+ let token = self . cur_token ( ) ;
115+ let span = token. span ( ) ;
116+
117+ // Git conflict markers always appear at start of line.
118+ // This prevents false positives from operator sequences like `a << << b`.
119+ // At the start of the file (span.start == 0), we allow the check to proceed
120+ // even if is_on_new_line() is false, since there's no preceding line.
121+ if !token. is_on_new_line ( ) && span. start != 0 {
122+ return None ;
123+ }
124+
125+ // Get the remaining source text from the current position
126+ let remaining = & self . source_text [ span. start as usize ..] ;
127+
128+ // Check for each conflict marker pattern (all are exactly 7 ASCII characters)
129+ // Git conflict markers are always ASCII, so we can safely use byte slicing
130+ if remaining. starts_with ( "<<<<<<<" )
131+ || remaining. starts_with ( "=======" )
132+ || remaining. starts_with ( ">>>>>>>" )
133+ || remaining. starts_with ( "|||||||" )
134+ {
135+ // Marker length is 7 bytes (all ASCII characters)
136+ return Some ( Span :: new ( span. start , span. start + 7 ) ) ;
137+ }
138+
139+ None
140+ }
141+
142+ /// Scans forward to find the middle and end markers of a merge conflict.
143+ ///
144+ /// After detecting the start marker (`<<<<<<<`), this function scans forward to find:
145+ /// - The middle marker (`=======`)
146+ /// - The end marker (`>>>>>>>`)
147+ ///
148+ /// The diff3 marker (`|||||||`) is recognized but not returned, as it appears between
149+ /// the start and middle markers and doesn't need separate labeling in the diagnostic.
150+ ///
151+ /// Returns `(middle_span, end_span)` where:
152+ /// - `middle_span` is the location of `=======` (if found)
153+ /// - `end_span` is the location of `>>>>>>>` (if found)
154+ ///
155+ /// Uses a checkpoint to rewind the parser state after scanning, leaving the parser
156+ /// positioned at the start marker.
157+ ///
158+ /// # Nested Conflicts
159+ ///
160+ /// If nested conflict markers are encountered (e.g., a conflict within a conflict),
161+ /// this function returns the first complete set of markers found. The parser will
162+ /// stop with a fatal error at the first conflict, so nested conflicts won't be
163+ /// fully analyzed until the outer conflict is resolved.
164+ ///
165+ /// The diagnostic message includes a note about nested conflicts to guide users
166+ /// to resolve the outermost conflict first.
167+ fn find_merge_conflict_markers ( & mut self ) -> ( Option < Span > , Option < Span > ) {
168+ let checkpoint = self . checkpoint ( ) ;
169+ let mut middle_span = None ;
170+
171+ loop {
172+ self . bump_any ( ) ;
173+
174+ if self . cur_kind ( ) == Kind :: Eof {
175+ self . rewind ( checkpoint) ;
176+ return ( middle_span, None ) ;
177+ }
178+
179+ // Check if we've hit a conflict marker
180+ if let Some ( marker_span) = self . is_merge_conflict_marker ( ) {
181+ let span = self . cur_token ( ) . span ( ) ;
182+ let remaining = & self . source_text [ span. start as usize ..] ;
183+
184+ if remaining. starts_with ( "=======" ) && middle_span. is_none ( ) {
185+ // Found middle marker
186+ middle_span = Some ( marker_span) ;
187+ } else if remaining. starts_with ( ">>>>>>>" ) {
188+ // Found end marker
189+ let result = ( middle_span, Some ( marker_span) ) ;
190+ self . rewind ( checkpoint) ;
191+ return result;
192+ }
193+ // Skip other markers (like diff3 `|||||||` or nested start markers `<<<<<<<`)
194+ }
195+ }
196+ }
197+ }
0 commit comments