Skip to main content

hermes_support/
manager.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8//! `SourceErrorManager`: owns source buffers, resolves locations, and
9//! dispatches diagnostics. This module covers buffer registration, names,
10//! virtual buffers, source-URL storage, and the central diagnostic emit
11//! pipeline: handler storage, message counts, error limit, warning
12//! categories, and message buffering/coalescing.
13//! Port of `hermes::SourceErrorManager`.
14
15use std::collections::HashMap;
16use std::rc::Rc;
17
18use crate::buffer::SourceBuffer;
19use crate::diag::{
20    CoordTranslator, DiagHandler, DiagKind, OutputOptions, ResolvedDiagnostic, Subsystem, Warning,
21};
22use crate::location::{SMLoc, SMRange, SourceCoords, SourceId};
23
24/// A single diagnostic payload awaiting flush while buffering is active, or
25/// held by an active collector. Port of the `MessageData` inner struct in
26/// `SourceErrorManager.cpp`.
27pub struct MessageData {
28    dk: DiagKind,
29    loc: Option<SMLoc>,
30    range: Option<SMRange>,
31    msg: String,
32}
33
34/// A buffered top-level message (non-note) plus the contiguous slice of its
35/// attached notes stored in `buffered_notes`.
36/// Port of `BufferedMessage` in `SourceErrorManager.cpp:26-84`.
37struct BufferedMessage {
38    data: MessageData,
39    /// Index into `buffered_notes` where this message's notes begin.
40    first_note: usize,
41    /// Number of notes attached to this message.
42    note_count: usize,
43}
44
45struct Entry {
46    buffer: Rc<SourceBuffer>,
47    is_virtual: bool,
48    source_url: Option<String>,
49    source_mapping_url: Option<String>,
50}
51
52/// A facade that owns source buffers and reports diagnostics against them.
53/// Rust port of `hermes::SourceErrorManager`.
54pub struct SourceErrorManager {
55    entries: Vec<Entry>,
56    by_name: HashMap<String, SourceId>,
57    /// Advisory output options for the installed diagnostic renderer.
58    /// The `DiagHandler` owns the actual rendering; this is the manager's copy
59    /// for callers that need to inspect or pass it along.
60    /// Port of `outputOptions_` in the C++ class.
61    output_options: OutputOptions,
62    /// Optional hook to translate (e.g. source-map) coordinates before display.
63    /// Port of `ICoordTranslator *translator_` in the C++ class.
64    translator: Option<Rc<dyn CoordTranslator>>,
65    /// Installed diagnostic sink. None means diagnostics are silently dropped.
66    handler: Option<Box<dyn DiagHandler>>,
67    /// Per-kind message counters: indexed by `DiagKind as usize` (Error=0,
68    /// Warning=1, Note=2). Port of `messageCount_[kMessageCount]`.
69    message_count: [u32; 3],
70    /// Maximum number of errors to emit before setting `error_limit_reached`.
71    /// Port of `errorLimit_`. Defaults to `u32::MAX` (unlimited).
72    error_limit: u32,
73    /// Set to `true` once the error count reaches `error_limit`.
74    /// Port of `errorLimitReached_`.
75    error_limit_reached: bool,
76    /// Tracks whether the immediately preceding message was suppressed, so that
77    /// follow-on Notes can be suppressed as well. Port of `lastMessageSuppressed_`.
78    last_message_suppressed: bool,
79    /// Per-category enabled flag. Indexed by `Warning::index()`.
80    /// Port of the `warningStatuses_` bitset (enabled side).
81    warning_enabled: Vec<bool>,
82    /// Per-category error-promotion flag. Indexed by `Warning::index()`.
83    /// Port of the `warningStatuses_` bitset (is-error side).
84    warning_as_error: Vec<bool>,
85    /// If `Some(s)`, messages from subsystem `s` are silently dropped.
86    /// If `Some(Subsystem::Unspecified)`, ALL messages are dropped.
87    /// Port of `SaveAndSuppressMessages` in C++.
88    ///
89    /// # Rust vs C++ design note
90    /// The C++ uses an RAII guard (`SaveAndSuppressMessages`) that holds a
91    /// pointer to the manager and restores the previous value on drop.  In safe
92    /// Rust that pattern would require the guard to hold a `&mut
93    /// SourceErrorManager`, which prevents the manager from also being borrowed
94    /// for emitting through the same scope.  Instead, callers (e.g. the lexer)
95    /// save the old value, set the new one, and restore it when done — an
96    /// explicit save/restore that is equivalent but borrow-checker-friendly.
97    suppressed_messages: Option<Subsystem>,
98    /// Reference count for buffering. While > 0, generated messages are stored
99    /// instead of dispatched; `disable_buffering` decrements and flushes when
100    /// it reaches 0. Port of `bufferingEnabled_` and the `enableBuffering` /
101    /// `disableBuffering` pair in `SourceErrorManager.cpp:26-84`.
102    ///
103    /// # Rust vs C++ design note
104    /// The C++ uses an RAII guard (`SaveAndBufferMessages`) for the same
105    /// reason as `SaveAndSuppressMessages` above: a `&mut`-holding guard
106    /// cannot coexist with emitting through the manager in safe Rust.
107    /// Callers use explicit `enable_buffering` / `disable_buffering` instead.
108    buffering_enabled: u32,
109    /// Top-level buffered messages (non-notes) in insertion order.
110    buffered_messages: Vec<BufferedMessage>,
111    /// Notes attached to buffered messages, stored in a single flat Vec;
112    /// each `BufferedMessage` indexes into this Vec via `first_note`/`note_count`.
113    buffered_notes: Vec<MessageData>,
114    /// Active message collector. While `Some`, filtered messages are captured
115    /// here instead of being counted or dispatched. Port of
116    /// `externalMessageBuffer_` in `SourceErrorManager.h`.
117    ///
118    /// # Rust vs C++ design note
119    /// The C++ uses an RAII guard (`CollectMessagesRAII`) that holds a pointer
120    /// to the manager and restores the previous collector on drop. For the
121    /// same borrow-checker reasons as `suppressed_messages`, callers use
122    /// explicit `begin_collecting` / `end_collecting` instead.
123    message_collector: Option<Vec<MessageData>>,
124}
125
126impl SourceErrorManager {
127    pub fn new() -> SourceErrorManager {
128        SourceErrorManager {
129            entries: Vec::new(),
130            by_name: HashMap::new(),
131            output_options: OutputOptions::default(),
132            translator: None,
133            handler: None,
134            message_count: [0; 3],
135            error_limit: u32::MAX,
136            error_limit_reached: false,
137            last_message_suppressed: false,
138            warning_enabled: vec![true; Warning::COUNT],
139            warning_as_error: vec![false; Warning::COUNT],
140            suppressed_messages: None,
141            buffering_enabled: 0,
142            buffered_messages: Vec::new(),
143            buffered_notes: Vec::new(),
144            message_collector: None,
145        }
146    }
147
148    /// Register a real source buffer and return its id.
149    pub fn add_buffer(&mut self, name: &str, contents: &str) -> SourceId {
150        self.push(SourceBuffer::from_str(name, contents), false)
151    }
152
153    /// Register a real source buffer from raw (possibly already NUL-terminated)
154    /// bytes.
155    #[allow(dead_code)]
156    pub fn add_buffer_bytes(&mut self, name: &str, contents: &[u8]) -> SourceId {
157        self.push(SourceBuffer::from_slice_check(name, contents), false)
158    }
159
160    /// Register a virtual buffer: a name with no contents, used for synthetic
161    /// locations. Port of `addNewVirtualSourceBuffer`.
162    pub fn add_virtual_buffer(&mut self, name: &str) -> SourceId {
163        self.push(SourceBuffer::from_str(name, ""), true)
164    }
165
166    fn push(&mut self, buffer: SourceBuffer, is_virtual: bool) -> SourceId {
167        let id = SourceId::from_index(self.entries.len() as u32);
168        let name = buffer.name().to_string();
169        self.entries.push(Entry {
170            buffer: Rc::new(buffer),
171            is_virtual,
172            source_url: None,
173            source_mapping_url: None,
174        });
175        self.by_name.entry(name).or_insert(id);
176        id
177    }
178
179    pub fn is_virtual(&self, id: SourceId) -> bool {
180        self.entries[id.index() as usize].is_virtual
181    }
182
183    pub fn buffer_file_name(&self, id: SourceId) -> &str {
184        self.entries[id.index() as usize].buffer.name()
185    }
186
187    /// Obtain a buffer by id (cloning the `Rc`, e.g. to hand to a lexer).
188    pub fn source_buffer(&self, id: SourceId) -> Rc<SourceBuffer> {
189        Rc::clone(&self.entries[id.index() as usize].buffer)
190    }
191
192    /// Obtain the buffer containing `loc` (cloning the `Rc`). The location
193    /// carries its buffer, so this is a direct lookup. Port of `findBufferForLoc`.
194    pub fn find_buffer_for_loc(&self, loc: SMLoc) -> Rc<SourceBuffer> {
195        self.source_buffer(loc.source)
196    }
197
198    pub fn lookup_name(&self, name: &str) -> Option<SourceId> {
199        self.by_name.get(name).copied()
200    }
201
202    pub fn set_source_url(&mut self, id: SourceId, url: &str) {
203        self.entries[id.index() as usize].source_url = Some(url.to_string());
204    }
205    pub fn source_url(&self, id: SourceId) -> Option<&str> {
206        self.entries[id.index() as usize].source_url.as_deref()
207    }
208    pub fn set_source_mapping_url(&mut self, id: SourceId, url: &str) {
209        self.entries[id.index() as usize].source_mapping_url = Some(url.to_string());
210    }
211    pub fn source_mapping_url(&self, id: SourceId) -> Option<&str> {
212        self.entries[id.index() as usize]
213            .source_mapping_url
214            .as_deref()
215    }
216}
217
218/// Port of `SourceErrorManager.cpp:256` `adjustSourceLocation`. If `offset`
219/// points at a '\r' or a UTF-8 continuation byte, walk backward (not past
220/// `line_start`) until a normal byte, so the column reflects the start of the
221/// character. A no-op for normal (token-start) locations.
222fn adjust_source_offset(bytes: &[u8], offset: u32, line_start: u32) -> u32 {
223    let mut i = offset as usize;
224    if i >= bytes.len() {
225        return offset;
226    }
227    let is_adjust = |b: u8| b == b'\r' || (b & 0b1100_0000) == 0b1000_0000;
228    if is_adjust(bytes[i]) {
229        while i as u32 > line_start && is_adjust(bytes[i]) {
230            i -= 1;
231        }
232    }
233    i as u32
234}
235
236impl SourceErrorManager {
237    /// Install (or clear) the coordinate translator applied during resolution.
238    pub fn set_translator(&mut self, translator: Option<Rc<dyn CoordTranslator>>) {
239        self.translator = translator;
240    }
241
242    /// The buffer containing `loc`. Trivial: the location carries its buffer.
243    /// Port of `findBufferIdForLoc`.
244    pub fn find_buffer_id(&self, loc: SMLoc) -> SourceId {
245        loc.source
246    }
247
248    /// Decode `loc` to 1-based (buffer, line, col), applying the coordinate
249    /// translator if one is installed. Port of `findBufferLineAndLoc`.
250    pub fn find_coords(&self, loc: SMLoc) -> SourceCoords {
251        let mut coords = self.find_untranslated_coords(loc);
252        if let Some(t) = &self.translator {
253            t.translate(&mut coords);
254        }
255        coords
256    }
257
258    /// Return the 1-based line span `[start, end)` for `line` in buffer `buf`
259    /// as an `SMRange`, after LF and CR trimming. Matches the line-span branch of
260    /// `findForCoordsImpl` (cpp:357-398). Returns `None` if `line` is out of range.
261    pub fn find_smrange_for_line(&self, buf: SourceId, line: u32) -> Option<SMRange> {
262        let entry = &self.entries[buf.index() as usize];
263        entry.buffer.with_line_index(|idx, bytes| {
264            if line < 1 || line > idx.line_count() {
265                return None;
266            }
267            let ls = idx.line_start(line);
268            let raw = idx.line_ref(bytes, line);
269            // `raw` may include a trailing '\n'; strip it.
270            let lf_trimmed = raw.len() as u32 - if raw.ends_with(b"\n") { 1 } else { 0 };
271            let mut start = ls;
272            let mut end = ls + lf_trimmed;
273            // Trim lone CR at start/end to handle \r, \r\n, \n\r line endings.
274            // Port of cpp:392-395.
275            if start < end && bytes[start as usize] == b'\r' {
276                start += 1;
277            }
278            if start < end && bytes[(end - 1) as usize] == b'\r' {
279                end -= 1;
280            }
281            Some(SMRange {
282                start: SMLoc {
283                    source: buf,
284                    offset: start,
285                },
286                end: SMLoc {
287                    source: buf,
288                    offset: end,
289                },
290            })
291        })
292    }
293
294    /// Resolve a `SourceCoords` (buffer + 1-based line + 1-based col) to an
295    /// `SMLoc`. Port of `findSMLocFromCoords` / `findForCoordsImpl` (cpp:397-439).
296    /// Returns `None` if the line is out of range or the column is past the line.
297    pub fn find_smloc_from_coords(&self, coords: SourceCoords) -> Option<SMLoc> {
298        let buf = coords.buf;
299        let line = coords.line;
300        let col = coords.col;
301        let entry = &self.entries[buf.index() as usize];
302        entry.buffer.with_line_index(|idx, bytes| {
303            if line < 1 || line > idx.line_count() {
304                return None;
305            }
306            let ls = idx.line_start(line);
307            let raw = idx.line_ref(bytes, line);
308            // Strip trailing '\n' (the LF itself is not part of the line content).
309            let lf_trimmed = raw.len() as u32 - if raw.ends_with(b"\n") { 1 } else { 0 };
310            let mut start = ls;
311            let mut end = ls + lf_trimmed;
312            // CR trim — port of cpp:392-395.
313            if start < end && bytes[start as usize] == b'\r' {
314                start += 1;
315            }
316            if start < end && bytes[(end - 1) as usize] == b'\r' {
317                end -= 1;
318            }
319            // Special case: empty line — port of cpp:402-407.
320            if start == end {
321                if col <= 1 {
322                    return Some(SMLoc {
323                        source: buf,
324                        offset: start,
325                    });
326                }
327                return None;
328            }
329            // Detect presence of any non-ASCII byte — port of cpp:409-416.
330            let has_non_ascii = bytes[start as usize..end as usize]
331                .iter()
332                .any(|&b| b & 0x80 != 0);
333            if !has_non_ascii {
334                // ASCII fast path — port of cpp:419-426.
335                if col > end - start {
336                    return None;
337                }
338                return Some(SMLoc {
339                    source: buf,
340                    offset: start + col - 1,
341                });
342            }
343            // UTF-8 path: scan code points, skipping continuation bytes.
344            // Port of cpp:429-437.
345            let mut column: u32 = 0;
346            let mut offset = start;
347            while offset < end {
348                let b = bytes[offset as usize];
349                // Skip UTF-8 continuation bytes (0b10xx_xxxx).
350                if (b & 0b1100_0000) != 0b1000_0000 {
351                    column += 1;
352                    if column == col {
353                        return Some(SMLoc {
354                            source: buf,
355                            offset,
356                        });
357                    }
358                }
359                offset += 1;
360            }
361            None
362        })
363    }
364
365    /// Return `(buf, 1-based-line, byte-range-of-line)` for the line containing
366    /// `loc`. Equivalent to `findBufferAndLine` (C++ header:428).
367    ///
368    /// # Lifetime note
369    /// The C++ returns a `LineCoord` with a borrow into the buffer's bytes.
370    /// Because `with_line_index` scopes the borrow to a closure, we cannot
371    /// return a `LineCoord<'_>` without lifetime gymnastics. Instead we return
372    /// the byte range as a `std::ops::Range<u32>` (owned, zero-copy). Callers
373    /// can retrieve the actual slice via `source_buffer(buf).bytes()[range]`.
374    pub fn find_buffer_and_line(&self, loc: SMLoc) -> (SourceId, u32, std::ops::Range<u32>) {
375        let entry = &self.entries[loc.source.index() as usize];
376        entry.buffer.with_line_index(|idx, bytes| {
377            let (line, _col) = idx.line_col(loc.offset);
378            let start = idx.line_start(line);
379            // end = start of next line (includes the '\n'), or end of bytes.
380            let line_ref = idx.line_ref(bytes, line);
381            let end = start + line_ref.len() as u32;
382            (loc.source, line, start..end)
383        })
384    }
385
386    /// Decode `loc` without applying the translator, including the
387    /// `adjustSourceLocation` correction for '\r'/UTF-8 continuation bytes.
388    /// Port of `findUntranslatedBufferLineAndLoc`.
389    pub fn find_untranslated_coords(&self, loc: SMLoc) -> SourceCoords {
390        let entry = &self.entries[loc.source.index() as usize];
391        let (line, col) = entry.buffer.with_line_index(|idx, bytes| {
392            // Find the line for the raw offset, then derive the line's start
393            // offset (col is 1-based byte distance from it).
394            let (line, raw_col) = idx.line_col(loc.offset);
395            let line_start = loc.offset - (raw_col - 1);
396            let adjusted = adjust_source_offset(bytes, loc.offset, line_start);
397            (line, adjusted - line_start + 1)
398        });
399        SourceCoords {
400            buf: loc.source,
401            line,
402            col,
403        }
404    }
405
406    // ---- Small helpers --------------------------------------------------------
407
408    /// Build the smallest `SMRange` covering both `a` and `b` (both must be in
409    /// the same buffer). Delegates to `SMRange::combine`.
410    /// Port of `combineIntoRange` (C++ header:601-607).
411    pub fn combine_into_range(&self, a: SMLoc, b: SMLoc) -> SMRange {
412        SMRange::combine(a, b)
413    }
414
415    /// Convert the exclusive end of `range` to an inclusive location by
416    /// subtracting one. If the range is empty (start == end), returns the start
417    /// unchanged. Port of `convertEndToLocation` (cpp:670-676).
418    pub fn convert_end_to_location(range: SMRange) -> SMLoc {
419        if range.start == range.end {
420            range.start
421        } else {
422            SMLoc {
423                source: range.end.source,
424                offset: range.end.offset - 1,
425            }
426        }
427    }
428
429    /// The display name of a buffer: its source URL if one was set (e.g. from a
430    /// `//# sourceURL=` comment or a source map), otherwise its file name.
431    /// Port of `getSourceUrl` (C++ header:415-421).
432    fn source_url_or_name(&self, id: SourceId) -> &str {
433        self.source_url(id)
434            .unwrap_or_else(|| self.buffer_file_name(id))
435    }
436
437    /// Format `coords` as `"name:line:col"`, where `name` prefers the buffer's
438    /// source URL over its file name (matching C++ `dumpCoords`, which uses
439    /// `getSourceUrl`). Port of `dumpCoords(OS, SourceCoords)` (cpp:109-117).
440    pub fn dump_coords(&self, coords: SourceCoords) -> String {
441        format!(
442            "{}:{}:{}",
443            self.source_url_or_name(coords.buf),
444            coords.line,
445            coords.col
446        )
447    }
448
449    /// Resolve `loc` to coordinates and format as `"filename:line:col"`.
450    /// Port of `dumpCoords(OS, SMLoc)` (cpp:119-123).
451    pub fn dump_coords_loc(&self, loc: SMLoc) -> String {
452        let coords = self.find_coords(loc);
453        self.dump_coords(coords)
454    }
455
456    // ---- Output options and translator accessors --------------------------------
457
458    /// Return the current advisory output options.
459    /// Port of `getOutputOptions` (C++ header:331).
460    pub fn output_options(&self) -> OutputOptions {
461        self.output_options
462    }
463
464    /// Replace the current advisory output options.
465    /// Port of `setOutputOptions` (C++ header:335).
466    pub fn set_output_options(&mut self, o: OutputOptions) {
467        self.output_options = o;
468    }
469
470    /// Clone the installed translator, if any.
471    /// Port of `getTranslator` (C++ header:355).
472    pub fn translator(&self) -> Option<Rc<dyn CoordTranslator>> {
473        self.translator.as_ref().map(Rc::clone)
474    }
475
476    // ---- Additional count accessors -------------------------------------------
477
478    /// Number of messages of kind `dk` emitted so far.
479    /// Port of `getMessageCount` (C++ header:586).
480    pub fn get_message_count(&self, dk: DiagKind) -> u32 {
481        self.message_count[dk as usize]
482    }
483
484    /// Convenience: number of notes emitted.
485    /// Port of `getNoteCount` (C++ header:597).
486    pub fn note_count(&self) -> u32 {
487        self.message_count[DiagKind::Note as usize]
488    }
489}
490
491impl Default for SourceErrorManager {
492    fn default() -> Self {
493        Self::new()
494    }
495}
496
497// ---------------------------------------------------------------------------
498// Diagnostic dispatch. Port of SourceErrorManager::message, countAndGenMessage,
499// doGenMessage / doPrintMessage in SourceErrorManager.cpp. Includes subsystem
500// suppression, message buffering/coalescing, and external message collection.
501// ---------------------------------------------------------------------------
502impl SourceErrorManager {
503    /// Install the diagnostic sink. Replaces any previously installed handler.
504    pub fn set_handler(&mut self, h: Box<dyn DiagHandler>) {
505        self.handler = Some(h);
506    }
507
508    /// Downcast the installed handler to a concrete type (for tests/inspection).
509    pub fn handler_as<T: 'static>(&self) -> Option<&T> {
510        self.handler.as_ref()?.as_any().downcast_ref::<T>()
511    }
512
513    /// Set the maximum number of errors before suppressing further messages.
514    pub fn set_error_limit(&mut self, limit: u32) {
515        self.error_limit = limit;
516    }
517
518    /// Return the current error limit. Port of `getErrorLimit` (C++ header:285).
519    pub fn get_error_limit(&self) -> u32 {
520        self.error_limit
521    }
522
523    /// Return `true` if the error limit has been reached.
524    pub fn is_error_limit_reached(&self) -> bool {
525        self.error_limit_reached
526    }
527
528    /// Clear the error-limit-reached flag AND reset the error counter, so a
529    /// fresh batch of errors can be emitted (e.g. to resume after recovery).
530    /// Port of `clearErrorLimitReached` (C++ header:295-298), which resets both.
531    pub fn clear_error_limit_reached(&mut self) {
532        self.error_limit_reached = false;
533        self.message_count[DiagKind::Error as usize] = 0;
534    }
535
536    /// Convenience: number of errors emitted.
537    pub fn error_count(&self) -> u32 {
538        self.message_count[DiagKind::Error as usize]
539    }
540
541    /// Convenience: number of warnings emitted (before any is-error promotion).
542    pub fn warning_count(&self) -> u32 {
543        self.message_count[DiagKind::Warning as usize]
544    }
545
546    // ---- Warning categories (Task 9) ----------------------------------------
547
548    /// Enable or disable a warning category.
549    /// Port of `setWarningStatus` / `disableAllWarnings`.
550    pub fn set_warning_status(&mut self, w: Warning, enabled: bool) {
551        self.warning_enabled[w.index()] = enabled;
552    }
553
554    /// Promote (or demote) a warning category to errors.
555    /// Port of `setWarningIsError`.
556    pub fn set_warning_is_error(&mut self, w: Warning, v: bool) {
557        self.warning_as_error[w.index()] = v;
558    }
559
560    /// Promote all warning categories to errors (equivalent to `-Werror`).
561    pub fn set_warnings_are_errors(&mut self, v: bool) {
562        for x in &mut self.warning_as_error {
563            *x = v;
564        }
565    }
566
567    /// Disable all warning categories.
568    pub fn disable_all_warnings(&mut self) {
569        for x in &mut self.warning_enabled {
570            *x = false;
571        }
572    }
573
574    /// Return `true` if the warning category is currently enabled.
575    pub fn is_warning_enabled(&self, w: Warning) -> bool {
576        self.warning_enabled[w.index()]
577    }
578
579    /// Return `true` if the warning category is promoted to error.
580    pub fn is_warning_an_error(&self, w: Warning) -> bool {
581        self.warning_as_error[w.index()]
582    }
583
584    // ---- Subsystem suppression ----------------------------------------------
585
586    /// Set (or clear) the subsystem whose messages are suppressed.
587    /// Pass `Some(Subsystem::Unspecified)` to suppress all messages.
588    /// Pass `None` to lift suppression.
589    ///
590    /// See the `suppressed_messages` field comment for the Rust vs C++ design
591    /// rationale.
592    pub fn set_suppressed_messages(&mut self, s: Option<Subsystem>) {
593        self.suppressed_messages = s;
594    }
595
596    /// Return the currently suppressed subsystem, if any.
597    pub fn suppressed_messages(&self) -> Option<Subsystem> {
598        self.suppressed_messages
599    }
600
601    // ---- Public reporting overloads -----------------------------------------
602
603    /// Emit an error at `loc` (subsystem: `Unspecified`).
604    pub fn error(&mut self, loc: SMLoc, msg: impl Into<String>) {
605        self.emit(
606            DiagKind::Error,
607            Warning::NoWarning,
608            Subsystem::Unspecified,
609            Some(loc),
610            None,
611            msg.into(),
612        );
613    }
614
615    /// Emit an error at the start of `range`, underscoring the full range
616    /// (subsystem: `Unspecified`).
617    pub fn error_range(&mut self, range: SMRange, msg: impl Into<String>) {
618        self.emit(
619            DiagKind::Error,
620            Warning::NoWarning,
621            Subsystem::Unspecified,
622            Some(range.start),
623            Some(range),
624            msg.into(),
625        );
626    }
627
628    /// Emit an error at `loc` with an optional `range` and explicit `subsystem`.
629    /// The lexer calls this form to allow per-subsystem suppression.
630    pub fn error_at(
631        &mut self,
632        loc: SMLoc,
633        range: Option<SMRange>,
634        msg: impl Into<String>,
635        subsystem: Subsystem,
636    ) {
637        self.emit(
638            DiagKind::Error,
639            Warning::NoWarning,
640            subsystem,
641            Some(loc),
642            range,
643            msg.into(),
644        );
645    }
646
647    /// Emit a note at `loc` (subsystem: `Unspecified`).
648    pub fn note(&mut self, loc: SMLoc, msg: impl Into<String>) {
649        self.emit(
650            DiagKind::Note,
651            Warning::NoWarning,
652            Subsystem::Unspecified,
653            Some(loc),
654            None,
655            msg.into(),
656        );
657    }
658
659    /// Emit a note over `range` (the caret sits at `range.start`).
660    pub fn note_range(&mut self, range: SMRange, msg: impl Into<String>, subsystem: Subsystem) {
661        self.emit(
662            DiagKind::Note,
663            Warning::NoWarning,
664            subsystem,
665            Some(range.start),
666            Some(range),
667            msg.into(),
668        );
669    }
670
671    /// Emit a note at `loc`, optionally underlining `range`, in `subsystem`.
672    pub fn note_at(
673        &mut self,
674        loc: SMLoc,
675        range: Option<SMRange>,
676        msg: impl Into<String>,
677        subsystem: Subsystem,
678    ) {
679        self.emit(
680            DiagKind::Note,
681            Warning::NoWarning,
682            subsystem,
683            Some(loc),
684            range,
685            msg.into(),
686        );
687    }
688
689    /// Emit a warning of the given category at `loc` (subsystem: `Unspecified`).
690    pub fn warning(&mut self, w: Warning, loc: SMLoc, msg: impl Into<String>) {
691        self.emit(
692            DiagKind::Warning,
693            w,
694            Subsystem::Unspecified,
695            Some(loc),
696            None,
697            msg.into(),
698        );
699    }
700
701    /// Emit a `Misc` warning at `loc` (subsystem: `Unspecified`).
702    pub fn warning_misc(&mut self, loc: SMLoc, msg: impl Into<String>) {
703        self.emit(
704            DiagKind::Warning,
705            Warning::Misc,
706            Subsystem::Unspecified,
707            Some(loc),
708            None,
709            msg.into(),
710        );
711    }
712
713    /// Emit a warning of the given category at the start of `range`,
714    /// underscoring the full range, with an explicit `subsystem`.
715    pub fn warning_range(
716        &mut self,
717        w: Warning,
718        range: SMRange,
719        msg: impl Into<String>,
720        subsystem: Subsystem,
721    ) {
722        self.emit(
723            DiagKind::Warning,
724            w,
725            subsystem,
726            Some(range.start),
727            Some(range),
728            msg.into(),
729        );
730    }
731
732    /// General-purpose overload: caller supplies all parameters.
733    /// Port of the `message(DiagKind, Warning, Subsystem, ...)` C++ overloads.
734    pub fn message(
735        &mut self,
736        dk: DiagKind,
737        w: Warning,
738        subsystem: Subsystem,
739        loc: Option<SMLoc>,
740        range: Option<SMRange>,
741        msg: impl Into<String>,
742    ) {
743        self.emit(dk, w, subsystem, loc, range, msg.into());
744    }
745
746    // ---- Central dispatch ---------------------------------------------------
747
748    /// Central dispatch. Port of `SourceErrorManager::message` +
749    /// `countAndGenMessage`. Subsystem suppression is checked first (port of
750    /// `cpp:181-188`), before the error-limit check, matching C++ ordering.
751    fn emit(
752        &mut self,
753        mut dk: DiagKind,
754        w: Warning,
755        subsystem: Subsystem,
756        loc: Option<SMLoc>,
757        range: Option<SMRange>,
758        msg: String,
759    ) {
760        // Suppress messages from the suppressed subsystem (or all, if
761        // Unspecified). Port of SourceErrorManager.cpp:181-188.
762        // Note: suppressed messages do NOT update `last_message_suppressed`,
763        // matching C++ behavior — they just return.
764        if let Some(s) = self.suppressed_messages {
765            if s == Subsystem::Unspecified || subsystem == s {
766                return;
767            }
768        }
769        // Suppress all messages once the error limit has been reached.
770        if self.error_limit_reached {
771            return;
772        }
773        if dk == DiagKind::Warning && !self.is_warning_enabled(w) {
774            self.last_message_suppressed = true;
775            return;
776        }
777        // Automatically suppress notes if the last message was suppressed.
778        if dk == DiagKind::Note && self.last_message_suppressed {
779            return;
780        }
781        self.last_message_suppressed = false;
782        // Optionally upgrade warnings into errors.
783        if dk == DiagKind::Warning && self.is_warning_an_error(w) {
784            dk = DiagKind::Error;
785        }
786        // If a collector is active, capture the (already-filtered) message
787        // instead of generating it. Port of the externalMessageBuffer_ check
788        // (cpp:207-210). Collected messages are NOT counted at collect time;
789        // they are replayed through count_and_gen in end_collecting.
790        if let Some(collector) = self.message_collector.as_mut() {
791            collector.push(MessageData {
792                dk,
793                loc,
794                range,
795                msg,
796            });
797            return;
798        }
799        self.count_and_gen(dk, loc, range, msg);
800    }
801
802    /// Port of `countAndGenMessage`.
803    fn count_and_gen(
804        &mut self,
805        dk: DiagKind,
806        loc: Option<SMLoc>,
807        range: Option<SMRange>,
808        msg: String,
809    ) {
810        self.message_count[dk as usize] += 1;
811        self.do_gen_message(dk, loc, range, msg);
812        // Check after calling do_gen_message so the original message is emitted
813        // (or buffered) first, then the "too many errors" sentinel.  Matches
814        // C++ behavior.
815        if dk == DiagKind::Error && self.message_count[DiagKind::Error as usize] == self.error_limit
816        {
817            self.error_limit_reached = true;
818            self.do_gen_message(
819                DiagKind::Error,
820                None,
821                None,
822                "too many errors emitted".to_string(),
823            );
824        }
825    }
826
827    /// Route a message either to the buffer (if buffering is active) or
828    /// directly to the handler.  Notes are attached to the last buffered
829    /// non-note message; if no such message exists yet, the note becomes a
830    /// standalone buffered message (edge case, matches C++).
831    /// Port of `doGenMessage` in `SourceErrorManager.cpp:125-156`.
832    fn do_gen_message(
833        &mut self,
834        dk: DiagKind,
835        loc: Option<SMLoc>,
836        range: Option<SMRange>,
837        msg: String,
838    ) {
839        if self.buffering_enabled > 0 {
840            if dk == DiagKind::Note && !self.buffered_messages.is_empty() {
841                // Attach note to the last buffered non-note message.
842                let note = MessageData {
843                    dk,
844                    loc,
845                    range,
846                    msg,
847                };
848                let first = self.buffered_notes.len();
849                self.buffered_notes.push(note);
850                let last = self.buffered_messages.last_mut().unwrap();
851                if last.note_count == 0 {
852                    last.first_note = first;
853                }
854                last.note_count += 1;
855            } else {
856                // Buffer as a standalone top-level message (includes the edge
857                // case of a Note when no top-level message is buffered yet).
858                self.buffered_messages.push(BufferedMessage {
859                    data: MessageData {
860                        dk,
861                        loc,
862                        range,
863                        msg,
864                    },
865                    first_note: 0,
866                    note_count: 0,
867                });
868            }
869        } else {
870            self.gen_message(dk, loc, range, msg);
871        }
872    }
873
874    // ---- Message buffering / coalescing (Phase 3) ---------------------------
875
876    /// Increment the buffering reference count.  While the count is > 0,
877    /// messages are queued rather than dispatched to the handler.
878    /// Port of `enableBuffering` in `SourceErrorManager.cpp`.
879    pub fn enable_buffering(&mut self) {
880        self.buffering_enabled += 1;
881    }
882
883    /// Decrement the buffering reference count.  When it reaches zero, flush
884    /// all buffered messages — stable-sorted by source position — to the
885    /// handler.  Port of `disableBuffering` in `SourceErrorManager.cpp`.
886    ///
887    /// # Flush ordering
888    /// Messages with a source location are emitted in (buffer-index, offset)
889    /// order.  The "too many errors emitted" sentinel (Error with no location)
890    /// is forced last.  No deduplication — matches C++ behavior.
891    /// Each top-level message is immediately followed by its attached notes in
892    /// insertion order.
893    pub fn disable_buffering(&mut self) {
894        debug_assert!(self.buffering_enabled > 0);
895        self.buffering_enabled -= 1;
896        if self.buffering_enabled > 0 {
897            return;
898        }
899        // Take ownership of both vecs so we can borrow `self` mutably for
900        // gen_message while iterating.
901        let msgs = std::mem::take(&mut self.buffered_messages);
902        let notes = std::mem::take(&mut self.buffered_notes);
903        // Build a sorted index.  Located messages sort by (source-index, offset);
904        // the sentinel (loc == None) sorts last via the leading 0/1 discriminant.
905        //
906        // `sort_by_key` is a STABLE sort, so two messages emitted at the same
907        // location keep their emission order.  This used to be a documented
908        // divergence: C++ sorted the buffered messages with `std::sort`, whose
909        // tie order is unspecified, so a same-location pair could come out
910        // either way (in practice depending on the total buffered count).
911        // Upstream `5f313a13a` ("Sort buffered diagnostics with a stable
912        // sort") changed it to `std::stable_sort`
913        // (`SourceErrorManager.cpp:60-74`), so both sides now break
914        // same-location ties in emission order and the divergence is retired.
915        let mut order: Vec<usize> = (0..msgs.len()).collect();
916        order.sort_by_key(|&i| match msgs[i].data.loc {
917            Some(l) => (0u8, l.source.index(), l.offset),
918            None => (1u8, u32::MAX, u32::MAX),
919        });
920        for &i in &order {
921            let m = &msgs[i];
922            self.gen_message(m.data.dk, m.data.loc, m.data.range, m.data.msg.clone());
923            for n in &notes[m.first_note..m.first_note + m.note_count] {
924                self.gen_message(n.dk, n.loc, n.range, n.msg.clone());
925            }
926        }
927        // Both vecs were moved out; nothing to clear.
928    }
929
930    // ---- External message collection (Phase 4) --------------------------------
931
932    /// Begin collecting messages. Returns the previous collector (if nested) to
933    /// be passed back to `end_collecting`. While active, filtered messages are
934    /// captured (not counted or dispatched). Also enables buffering so the
935    /// replayed messages are source-sorted on flush.
936    /// Port of `CollectMessagesRAII` constructor.
937    pub fn begin_collecting(&mut self) -> Option<Vec<MessageData>> {
938        self.enable_buffering();
939        self.message_collector.replace(Vec::new())
940    }
941
942    /// End collection. If `discard` is false, replay the collected messages
943    /// through the normal count+buffer path (counting them and flushing
944    /// source-sorted); otherwise drop them. Restores the previous collector.
945    /// Port of the `CollectMessagesRAII` destructor.
946    ///
947    /// Order matches C++: replay (counts + buffers) → disable_buffering
948    /// (flushes) → restore previous collector. During replay `message_collector`
949    /// is `None` (taken), so `count_and_gen` → `do_gen_message` buffers them
950    /// rather than re-collecting.
951    pub fn end_collecting(&mut self, previous: Option<Vec<MessageData>>, discard: bool) {
952        let collected = self.message_collector.take().unwrap_or_default();
953        if !discard {
954            for m in collected {
955                self.count_and_gen(m.dk, m.loc, m.range, m.msg);
956            }
957        }
958        self.disable_buffering();
959        self.message_collector = previous;
960    }
961
962    /// Resolve the location and hand a `ResolvedDiagnostic` to the handler.
963    /// Port of `doGenMessage` → `doPrintMessage`.
964    fn gen_message(
965        &mut self,
966        dk: DiagKind,
967        loc: Option<SMLoc>,
968        range: Option<SMRange>,
969        msg: String,
970    ) {
971        // Build the resolved struct first (immutable borrows of self.entries
972        // and self.translator complete and produce owned data), then hand it
973        // to self.handler (mutable borrow).  This ordering satisfies the
974        // borrow checker without any unsafe.
975        let resolved = match loc {
976            Some(loc) => {
977                // Use UNtranslated coordinates here: the rendered diagnostic and
978                // its source-line / caret-column lookups must be resolved against
979                // the original buffer, exactly as the C++ primary diagnostic does
980                // (`doPrintMessage` passes the original loc to PrintMessage; the
981                // translator only affects a separate annotation we don't render).
982                // Using translated coords would fetch the wrong source line and
983                // miscompute the caret column when a CoordTranslator is installed.
984                let coords = self.find_untranslated_coords(loc);
985                let file_name = self.buffer_file_name(loc.source).to_string();
986                // Clone the Rc so the immutable borrow on self.entries ends here.
987                let buf = self.source_buffer(loc.source);
988                // Pull the source line (without trailing EOL) for the caret.
989                let source_line = buf.with_line_index(|idx, bytes| {
990                    let raw = idx.line_ref(bytes, coords.line);
991                    let trimmed = strip_eol(raw);
992                    String::from_utf8_lossy(trimmed).into_owned()
993                });
994                // Compute range columns relative to the source line, if the
995                // range is in the same buffer as the location.
996                // Port of SourceErrorManager.cpp:158-171 (caret/tilde fill).
997                let range_cols = range.filter(|r| r.start.source == loc.source).map(|r| {
998                    // Byte offset of the first character of this line.
999                    let line_start = loc.offset - (coords.col - 1);
1000                    // Byte length of the EOL-stripped source line.
1001                    let source_line_byte_len = source_line.len() as u32;
1002                    let line_end = line_start + source_line_byte_len;
1003                    // Clamp both endpoints to the line so multi-line ranges
1004                    // stop at the line end, matching the C++ behavior
1005                    // (`min(range.second, caretLine.size())`).
1006                    let start = r.start.offset.saturating_sub(line_start);
1007                    // saturating_sub guards against an inverted/foreign range
1008                    // whose end precedes the line start.
1009                    let end = r.end.offset.min(line_end).saturating_sub(line_start);
1010                    (start, end)
1011                });
1012                ResolvedDiagnostic {
1013                    kind: dk,
1014                    file_name,
1015                    line: coords.line,
1016                    col: coords.col,
1017                    message: msg,
1018                    source_line: Some(source_line),
1019                    range_cols,
1020                }
1021            }
1022            // No location: `SourceMgr::GetMessage` (SourceMgr.cpp:238-298)
1023            // never touches the buffers, so the `SMDiagnostic` keeps its
1024            // `BufferID = "<unknown>"` default (:246) and its zero-initialized
1025            // `LineAndCol`, giving line 0 and (as `col - 1`) column -1. The
1026            // renderer's `col == 0` means exactly that -1, i.e. "print no
1027            // column". This is the shape of the `too many errors emitted`
1028            // sentinel, the only location-less message hermesc emits.
1029            None => ResolvedDiagnostic {
1030                kind: dk,
1031                file_name: "<unknown>".to_string(),
1032                line: 0,
1033                col: 0,
1034                message: msg,
1035                source_line: None,
1036                range_cols: None,
1037            },
1038        };
1039        if let Some(h) = self.handler.as_mut() {
1040            h.handle(&resolved);
1041        }
1042    }
1043}
1044
1045/// Strip a single trailing `\n` or `\r\n`/`\r` from a line slice.
1046/// Used to drop the EOL before handing the source line to the renderer.
1047fn strip_eol(line: &[u8]) -> &[u8] {
1048    let mut end = line.len();
1049    if end > 0 && line[end - 1] == b'\n' {
1050        end -= 1;
1051    }
1052    if end > 0 && line[end - 1] == b'\r' {
1053        end -= 1;
1054    }
1055    &line[..end]
1056}
1057
1058#[cfg(test)]
1059mod tests {
1060    use super::*;
1061
1062    #[test]
1063    fn resolves_loc_to_coords() {
1064        use crate::location::SMLoc;
1065        let mut sm = SourceErrorManager::new();
1066        let id = sm.add_buffer("a.js", "ab\ncde");
1067        let loc = SMLoc {
1068            source: id,
1069            offset: 4,
1070        }; // 'd' on line 2 col 2
1071        let coords = sm.find_coords(loc);
1072        assert_eq!((coords.buf, coords.line, coords.col), (id, 2, 2));
1073        assert_eq!(sm.find_buffer_id(loc), id);
1074    }
1075
1076    #[test]
1077    fn translator_is_applied() {
1078        use crate::location::{SMLoc, SourceCoords};
1079        use std::rc::Rc;
1080        struct Shift;
1081        impl crate::diag::CoordTranslator for Shift {
1082            fn translate(&self, c: &mut SourceCoords) {
1083                c.line += 100;
1084            }
1085        }
1086        let mut sm = SourceErrorManager::new();
1087        let id = sm.add_buffer("a.js", "ab\ncde");
1088        sm.set_translator(Some(Rc::new(Shift)));
1089        let coords = sm.find_coords(SMLoc {
1090            source: id,
1091            offset: 4,
1092        });
1093        assert_eq!(coords.line, 102);
1094    }
1095
1096    #[test]
1097    fn cr_before_lf_adjusts_back() {
1098        use crate::location::SMLoc;
1099        // "ab\r\ncd": a0 b1 \r2 \n3 c4 d5. Offset 2 is the '\r'; it adjusts back to
1100        // 'b' (line 1, col 2).
1101        let mut sm = SourceErrorManager::new();
1102        let id = sm.add_buffer("a.js", "ab\r\ncd");
1103        let coords = sm.find_coords(SMLoc {
1104            source: id,
1105            offset: 2,
1106        });
1107        assert_eq!((coords.line, coords.col), (1, 2));
1108    }
1109
1110    #[test]
1111    fn mid_utf8_byte_adjusts_to_char_start() {
1112        use crate::location::SMLoc;
1113        // "aé": a=0x61 at 0, 'é'=0xC3 0xA9 at offsets 1,2. Offset 2 is the
1114        // continuation byte; it adjusts back to the lead byte (line 1, col 2).
1115        let mut sm = SourceErrorManager::new();
1116        let id = sm.add_buffer("a.js", "aé");
1117        let coords = sm.find_coords(SMLoc {
1118            source: id,
1119            offset: 2,
1120        });
1121        assert_eq!((coords.line, coords.col), (1, 2));
1122    }
1123
1124    #[test]
1125    fn register_real_and_lookup() {
1126        let mut sm = SourceErrorManager::new();
1127        let id = sm.add_buffer("a.js", "let x = 1;");
1128        assert_eq!(sm.buffer_file_name(id), "a.js");
1129        assert_eq!(sm.lookup_name("a.js"), Some(id));
1130        assert!(!sm.is_virtual(id));
1131    }
1132
1133    #[test]
1134    fn virtual_buffer_is_tagged() {
1135        let mut sm = SourceErrorManager::new();
1136        let id = sm.add_virtual_buffer("<native>");
1137        assert!(sm.is_virtual(id));
1138        assert_eq!(sm.buffer_file_name(id), "<native>");
1139    }
1140
1141    #[test]
1142    fn source_urls_roundtrip() {
1143        let mut sm = SourceErrorManager::new();
1144        let id = sm.add_buffer("a.js", "x");
1145        sm.set_source_url(id, "https://example/a.js");
1146        sm.set_source_mapping_url(id, "a.js.map");
1147        assert_eq!(sm.source_url(id), Some("https://example/a.js"));
1148        assert_eq!(sm.source_mapping_url(id), Some("a.js.map"));
1149    }
1150
1151    #[test]
1152    fn error_count_and_limit() {
1153        use crate::diag::CollectingHandler;
1154        use crate::location::SMLoc;
1155        let mut sm = SourceErrorManager::new();
1156        let id = sm.add_buffer("a.js", "abc\ndef");
1157        sm.set_handler(Box::new(CollectingHandler::new()));
1158        sm.set_error_limit(1);
1159        sm.error(
1160            SMLoc {
1161                source: id,
1162                offset: 0,
1163            },
1164            "first",
1165        );
1166        assert!(sm.is_error_limit_reached());
1167        assert_eq!(sm.error_count(), 1);
1168        // After the limit, a "too many errors" message is emitted once and further
1169        // errors are suppressed (port of sTooManyErrors behavior).
1170        sm.error(
1171            SMLoc {
1172                source: id,
1173                offset: 1,
1174            },
1175            "second",
1176        );
1177        assert_eq!(sm.error_count(), 1);
1178        // clear_error_limit_reached resets BOTH the flag and the error count
1179        // (matching C++), so a fresh error can be emitted afterwards.
1180        sm.clear_error_limit_reached();
1181        assert!(!sm.is_error_limit_reached());
1182        assert_eq!(sm.error_count(), 0);
1183        sm.error(
1184            SMLoc {
1185                source: id,
1186                offset: 2,
1187            },
1188            "third",
1189        );
1190        assert_eq!(sm.error_count(), 1);
1191        assert!(sm.is_error_limit_reached());
1192    }
1193
1194    #[test]
1195    fn collecting_handler_receives_resolved() {
1196        use crate::diag::{CollectingHandler, DiagKind};
1197        use crate::location::SMLoc;
1198        let mut sm = SourceErrorManager::new();
1199        let id = sm.add_buffer("a.js", "abc\ndef");
1200        sm.set_handler(Box::new(CollectingHandler::new()));
1201        sm.warning_misc(
1202            SMLoc {
1203                source: id,
1204                offset: 4,
1205            },
1206            "watch out",
1207        );
1208        let h = sm.handler_as::<CollectingHandler>().unwrap();
1209        assert_eq!(h.messages().len(), 1);
1210        assert_eq!(h.messages()[0].kind, DiagKind::Warning);
1211        assert_eq!((h.messages()[0].line, h.messages()[0].col), (2, 1));
1212    }
1213
1214    #[test]
1215    fn disabled_warning_is_dropped() {
1216        use crate::diag::{CollectingHandler, Warning};
1217        use crate::location::SMLoc;
1218        let mut sm = SourceErrorManager::new();
1219        let id = sm.add_buffer("a.js", "abc");
1220        sm.set_handler(Box::new(CollectingHandler::new()));
1221        sm.set_warning_status(Warning::Misc, false);
1222        sm.warning(
1223            Warning::Misc,
1224            SMLoc {
1225                source: id,
1226                offset: 0,
1227            },
1228            "x",
1229        );
1230        assert_eq!(sm.warning_count(), 0);
1231        assert_eq!(
1232            sm.handler_as::<CollectingHandler>()
1233                .unwrap()
1234                .messages()
1235                .len(),
1236            0
1237        );
1238    }
1239
1240    #[test]
1241    fn error_range_threads_range() {
1242        use crate::diag::CollectingHandler;
1243        use crate::location::{SMLoc, SMRange};
1244        let mut sm = SourceErrorManager::new();
1245        let id = sm.add_buffer("t.js", "let x = 1;");
1246        sm.set_handler(Box::new(CollectingHandler::new()));
1247        sm.error_range(
1248            SMRange {
1249                start: SMLoc {
1250                    source: id,
1251                    offset: 4,
1252                },
1253                end: SMLoc {
1254                    source: id,
1255                    offset: 9,
1256                },
1257            },
1258            "m",
1259        );
1260        let h = sm.handler_as::<CollectingHandler>().unwrap();
1261        assert_eq!(h.messages().len(), 1);
1262        assert_eq!(h.messages()[0].range_cols, Some((4, 9)));
1263    }
1264
1265    #[test]
1266    fn warning_as_error_counts_as_error() {
1267        use crate::diag::{CollectingHandler, DiagKind, Warning};
1268        use crate::location::SMLoc;
1269        let mut sm = SourceErrorManager::new();
1270        let id = sm.add_buffer("a.js", "abc");
1271        sm.set_handler(Box::new(CollectingHandler::new()));
1272        sm.set_warning_is_error(Warning::Misc, true);
1273        sm.warning(
1274            Warning::Misc,
1275            SMLoc {
1276                source: id,
1277                offset: 0,
1278            },
1279            "x",
1280        );
1281        assert_eq!(sm.error_count(), 1);
1282        assert_eq!(
1283            sm.handler_as::<CollectingHandler>().unwrap().messages()[0].kind,
1284            DiagKind::Error
1285        );
1286    }
1287
1288    #[test]
1289    fn suppresses_matching_subsystem() {
1290        use crate::diag::{CollectingHandler, Subsystem};
1291        use crate::location::SMLoc;
1292        let mut sm = SourceErrorManager::new();
1293        let id = sm.add_buffer("a.js", "abc");
1294        sm.set_handler(Box::new(CollectingHandler::new()));
1295        sm.set_suppressed_messages(Some(Subsystem::Lexer));
1296        // A Lexer-subsystem error is dropped (no count, no handler message).
1297        sm.error_at(
1298            SMLoc {
1299                source: id,
1300                offset: 0,
1301            },
1302            None,
1303            "x",
1304            Subsystem::Lexer,
1305        );
1306        assert_eq!(sm.error_count(), 0);
1307        assert_eq!(
1308            sm.handler_as::<CollectingHandler>()
1309                .unwrap()
1310                .messages()
1311                .len(),
1312            0
1313        );
1314        // A Parser-subsystem error still passes when only Lexer is suppressed.
1315        sm.error_at(
1316            SMLoc {
1317                source: id,
1318                offset: 1,
1319            },
1320            None,
1321            "y",
1322            Subsystem::Parser,
1323        );
1324        assert_eq!(sm.error_count(), 1);
1325    }
1326
1327    #[test]
1328    fn suppress_unspecified_drops_everything() {
1329        use crate::diag::{CollectingHandler, Subsystem};
1330        use crate::location::SMLoc;
1331        let mut sm = SourceErrorManager::new();
1332        let id = sm.add_buffer("a.js", "abc");
1333        sm.set_handler(Box::new(CollectingHandler::new()));
1334        sm.set_suppressed_messages(Some(Subsystem::Unspecified));
1335        sm.error_at(
1336            SMLoc {
1337                source: id,
1338                offset: 0,
1339            },
1340            None,
1341            "x",
1342            Subsystem::Parser,
1343        );
1344        assert_eq!(sm.error_count(), 0);
1345    }
1346
1347    // ---- Message buffering / coalescing tests --------------------------------
1348
1349    #[test]
1350    fn buffering_sorts_by_source_order() {
1351        use crate::diag::CollectingHandler;
1352        use crate::location::SMLoc;
1353        let mut sm = SourceErrorManager::new();
1354        let id = sm.add_buffer("a.js", "abcdef");
1355        sm.set_handler(Box::new(CollectingHandler::new()));
1356        sm.enable_buffering();
1357        sm.error(
1358            SMLoc {
1359                source: id,
1360                offset: 4,
1361            },
1362            "second",
1363        ); // later in source
1364        sm.error(
1365            SMLoc {
1366                source: id,
1367                offset: 1,
1368            },
1369            "first",
1370        ); // earlier
1371           // Nothing emitted yet while buffering.
1372        assert_eq!(
1373            sm.handler_as::<CollectingHandler>()
1374                .unwrap()
1375                .messages()
1376                .len(),
1377            0
1378        );
1379        sm.disable_buffering();
1380        let h = sm.handler_as::<CollectingHandler>().unwrap();
1381        assert_eq!(h.messages().len(), 2);
1382        assert_eq!(h.messages()[0].message, "first"); // source order
1383        assert_eq!(h.messages()[1].message, "second");
1384        assert_eq!(sm.error_count(), 2); // counted when emitted, not at flush
1385    }
1386
1387    #[test]
1388    fn buffered_note_follows_its_message() {
1389        use crate::diag::{CollectingHandler, DiagKind};
1390        use crate::location::SMLoc;
1391        let mut sm = SourceErrorManager::new();
1392        let id = sm.add_buffer("a.js", "abcdef");
1393        sm.set_handler(Box::new(CollectingHandler::new()));
1394        sm.enable_buffering();
1395        sm.error(
1396            SMLoc {
1397                source: id,
1398                offset: 0,
1399            },
1400            "err",
1401        );
1402        sm.note(
1403            SMLoc {
1404                source: id,
1405                offset: 2,
1406            },
1407            "a note",
1408        );
1409        sm.disable_buffering();
1410        let h = sm.handler_as::<CollectingHandler>().unwrap();
1411        assert_eq!(h.messages().len(), 2);
1412        assert_eq!(h.messages()[0].kind, DiagKind::Error);
1413        assert_eq!(h.messages()[1].kind, DiagKind::Note);
1414        assert_eq!(h.messages()[1].message, "a note");
1415    }
1416
1417    #[test]
1418    fn nested_buffering_flushes_only_at_zero() {
1419        use crate::diag::CollectingHandler;
1420        use crate::location::SMLoc;
1421        let mut sm = SourceErrorManager::new();
1422        let id = sm.add_buffer("a.js", "abc");
1423        sm.set_handler(Box::new(CollectingHandler::new()));
1424        sm.enable_buffering();
1425        sm.enable_buffering();
1426        sm.error(
1427            SMLoc {
1428                source: id,
1429                offset: 0,
1430            },
1431            "x",
1432        );
1433        sm.disable_buffering(); // still buffering (count 1)
1434        assert_eq!(
1435            sm.handler_as::<CollectingHandler>()
1436                .unwrap()
1437                .messages()
1438                .len(),
1439            0
1440        );
1441        sm.disable_buffering(); // now flush
1442        assert_eq!(
1443            sm.handler_as::<CollectingHandler>()
1444                .unwrap()
1445                .messages()
1446                .len(),
1447            1
1448        );
1449    }
1450
1451    // ---- External message collection tests ----------------------------------
1452
1453    #[test]
1454    fn collect_then_replay() {
1455        use crate::diag::CollectingHandler;
1456        use crate::location::SMLoc;
1457        let mut sm = SourceErrorManager::new();
1458        let id = sm.add_buffer("a.js", "abcdef");
1459        sm.set_handler(Box::new(CollectingHandler::new()));
1460        let prev = sm.begin_collecting();
1461        sm.error(
1462            SMLoc {
1463                source: id,
1464                offset: 4,
1465            },
1466            "second",
1467        );
1468        sm.error(
1469            SMLoc {
1470                source: id,
1471                offset: 1,
1472            },
1473            "first",
1474        );
1475        // Collected, not counted, not dispatched.
1476        assert_eq!(sm.error_count(), 0);
1477        assert_eq!(
1478            sm.handler_as::<CollectingHandler>()
1479                .unwrap()
1480                .messages()
1481                .len(),
1482            0
1483        );
1484        sm.end_collecting(prev, false);
1485        // Replayed: counted and dispatched in source order.
1486        assert_eq!(sm.error_count(), 2);
1487        let h = sm.handler_as::<CollectingHandler>().unwrap();
1488        assert_eq!(h.messages().len(), 2);
1489        assert_eq!(h.messages()[0].message, "first");
1490        assert_eq!(h.messages()[1].message, "second");
1491    }
1492
1493    #[test]
1494    fn collect_then_discard() {
1495        use crate::diag::CollectingHandler;
1496        use crate::location::SMLoc;
1497        let mut sm = SourceErrorManager::new();
1498        let id = sm.add_buffer("a.js", "abc");
1499        sm.set_handler(Box::new(CollectingHandler::new()));
1500        let prev = sm.begin_collecting();
1501        sm.error(
1502            SMLoc {
1503                source: id,
1504                offset: 0,
1505            },
1506            "x",
1507        );
1508        sm.end_collecting(prev, true);
1509        assert_eq!(sm.error_count(), 0);
1510        assert_eq!(
1511            sm.handler_as::<CollectingHandler>()
1512                .unwrap()
1513                .messages()
1514                .len(),
1515            0
1516        );
1517    }
1518
1519    // ---- Phase 5: find/convert/dump helpers ---------------------------------
1520
1521    #[test]
1522    fn smloc_from_coords_roundtrip() {
1523        use crate::location::SourceCoords;
1524        let mut sm = SourceErrorManager::new();
1525        let id = sm.add_buffer("a.js", "ab\ncde\nf");
1526        // 'd' is line 2 col 2 -> offset 4.
1527        let loc = sm
1528            .find_smloc_from_coords(SourceCoords {
1529                buf: id,
1530                line: 2,
1531                col: 2,
1532            })
1533            .unwrap();
1534        assert_eq!(loc.offset, 4);
1535        // round-trips with find_coords
1536        let c = sm.find_coords(loc);
1537        assert_eq!((c.line, c.col), (2, 2));
1538    }
1539
1540    #[test]
1541    fn smloc_from_coords_utf8() {
1542        use crate::location::SourceCoords;
1543        let mut sm = SourceErrorManager::new();
1544        // "aé" : a(0), é=0xC3 0xA9 (1,2). col 2 is 'é' -> offset 1 (lead byte).
1545        let id = sm.add_buffer("a.js", "aé");
1546        let loc = sm
1547            .find_smloc_from_coords(SourceCoords {
1548                buf: id,
1549                line: 1,
1550                col: 2,
1551            })
1552            .unwrap();
1553        assert_eq!(loc.offset, 1);
1554    }
1555
1556    #[test]
1557    fn smrange_for_line_spans_content() {
1558        let mut sm = SourceErrorManager::new();
1559        let id = sm.add_buffer("a.js", "ab\r\ncde");
1560        // Line 1 is "ab" (CR + LF trimmed): offsets [0,2).
1561        let r = sm.find_smrange_for_line(id, 1).unwrap();
1562        assert_eq!((r.start.offset, r.end.offset), (0, 2));
1563    }
1564
1565    #[test]
1566    fn convert_end_to_location_subtracts_one() {
1567        use crate::location::{SMLoc, SMRange};
1568        let mut sm = SourceErrorManager::new();
1569        let id = sm.add_buffer("a.js", "abcdef");
1570        let r = SMRange {
1571            start: SMLoc {
1572                source: id,
1573                offset: 1,
1574            },
1575            end: SMLoc {
1576                source: id,
1577                offset: 4,
1578            },
1579        };
1580        assert_eq!(SourceErrorManager::convert_end_to_location(r).offset, 3);
1581        let empty = SMRange {
1582            start: SMLoc {
1583                source: id,
1584                offset: 2,
1585            },
1586            end: SMLoc {
1587                source: id,
1588                offset: 2,
1589            },
1590        };
1591        assert_eq!(SourceErrorManager::convert_end_to_location(empty).offset, 2);
1592    }
1593
1594    #[test]
1595    fn dump_coords_prefers_source_url() {
1596        use crate::location::SourceCoords;
1597        let mut sm = SourceErrorManager::new();
1598        let id = sm.add_buffer("a.js", "abc");
1599        // Without a source URL, the file name is used.
1600        assert_eq!(
1601            sm.dump_coords(SourceCoords {
1602                buf: id,
1603                line: 1,
1604                col: 1
1605            }),
1606            "a.js:1:1"
1607        );
1608        // With a source URL set, it is preferred (matches C++ getSourceUrl).
1609        sm.set_source_url(id, "orig.ts");
1610        assert_eq!(
1611            sm.dump_coords(SourceCoords {
1612                buf: id,
1613                line: 1,
1614                col: 1
1615            }),
1616            "orig.ts:1:1"
1617        );
1618    }
1619}