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 ¬es[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}