tiger_lib/report/
writer.rs

1use std::borrow::Cow;
2use std::io::Write;
3
4use ansiterm::{ANSIString, ANSIStrings};
5use itertools::Itertools;
6use strum::{EnumCount as _, IntoEnumIterator};
7use unicode_width::UnicodeWidthChar;
8
9use crate::fileset::{FileKind, FileStage};
10use crate::game::Game;
11use crate::report::errors::Errors;
12use crate::report::output_style::Styled;
13use crate::report::report_struct::pointer_indentation;
14use crate::report::{LogReportMetadata, LogReportPointers, OutputStyle, PointedMessage, Severity};
15
16/// Source lines printed in the output have leading tab characters replaced by this number of spaces.
17const SPACES_PER_TAB: usize = 4;
18/// Source lines that have more than this amount of leading whitespace (after tab replacement) have their whitespace truncated.
19const MAX_IDLE_SPACE: usize = 16;
20
21/// Log the report.
22pub fn log_report<O: Write + Send>(
23    errors: &Errors,
24    output: &mut O,
25    report: &LogReportMetadata,
26    pointers: &LogReportPointers,
27    additional: usize,
28) {
29    let indentation = pointer_indentation(pointers);
30    // Log error lvl and message:
31    log_line_title(errors, output, report);
32
33    // Log the pointers:
34    let iterator = pointers.iter();
35    let mut previous = None;
36    for pointer in iterator {
37        log_pointer(errors, output, previous, pointer, indentation, report.severity);
38        previous = Some(pointer);
39    }
40
41    // Log the additional count, if it's more than zero
42    if additional > 0 {
43        log_count(errors, output, indentation, additional);
44    }
45
46    // Log the info line, if one exists.
47    if let Some(info) = &report.info {
48        log_line_info(errors, output, indentation, info);
49    }
50
51    // Log the wiki link line, if one exists.
52    if let Some(wiki) = &report.wiki {
53        log_line_wiki(errors, output, indentation, wiki);
54    }
55
56    // Write a blank line to visually separate reports:
57    _ = writeln!(output);
58}
59
60pub fn log_summary<O: Write + Send>(
61    output: &mut O,
62    styles: &OutputStyle,
63    reports: &Vec<(&LogReportMetadata, Cow<'_, LogReportPointers>, usize)>,
64) {
65    let mut counts = [0usize; Severity::COUNT];
66    for (metadata, _, additional) in reports {
67        counts[metadata.severity as usize] += 1 + additional;
68    }
69
70    let line = Severity::iter()
71        .rev()
72        .flat_map(|sev| {
73            [
74                styles.style(Styled::Tag(sev, true)).paint(format!("{sev}")),
75                styles.style(Styled::Tag(sev, false)).paint(format!(": {}", counts[sev as usize])),
76                styles.style(Styled::Default).paint(", "),
77            ]
78        })
79        .collect_vec();
80    _ = writeln!(output, "{}", ANSIStrings(&line[..line.len() - 1]));
81}
82
83fn log_pointer<O: Write + Send>(
84    errors: &Errors,
85    output: &mut O,
86    previous: Option<&PointedMessage>,
87    pointer: &PointedMessage,
88    indentation: usize,
89    severity: Severity,
90) {
91    if previous.is_none() || !previous.unwrap().loc.same_file(pointer.loc) {
92        // This pointer is not the same as the previous pointer. Print file location as well:
93        log_line_file_location(errors, output, pointer, indentation);
94    }
95    if pointer.loc.line == 0 {
96        // Line being zero means the location is an entire file,
97        // not any particular location within the file.
98        return;
99    }
100    if let Some(line) = errors.cache.get_line(pointer.loc) {
101        let (line, removed, spaces) = line_spacing(line);
102        log_line_from_source(errors, output, pointer, indentation, line, spaces);
103        log_line_carets(errors, output, pointer, indentation, line, removed, spaces, severity);
104    }
105}
106
107/// Log the first line of a report, containing the severity level and the error message.
108fn log_line_title<O: Write + Send>(errors: &Errors, output: &mut O, report: &LogReportMetadata) {
109    let line: &[ANSIString<'static>] = &[
110        errors
111            .styles
112            .style(Styled::Tag(report.severity, true))
113            .paint(format!("{}", report.severity)),
114        errors.styles.style(Styled::Tag(report.severity, false)).paint(format!("({})", report.key)),
115        errors.styles.style(Styled::Default).paint(": "),
116        errors.styles.style(Styled::ErrorMessage).paint(report.msg.clone()),
117    ];
118    _ = writeln!(output, "{}", ANSIStrings(line));
119}
120
121/// Log the optional info line that is part of the overall report.
122fn log_line_info<O: Write + Send>(errors: &Errors, output: &mut O, indentation: usize, info: &str) {
123    let line_info: &[ANSIString<'static>] = &[
124        errors.styles.style(Styled::Default).paint(format!("{:width$}", "", width = indentation)),
125        errors.styles.style(Styled::Default).paint(" "),
126        errors.styles.style(Styled::Location).paint("="),
127        errors.styles.style(Styled::Default).paint(" "),
128        errors.styles.style(Styled::InfoTag).paint("Info:"),
129        errors.styles.style(Styled::Default).paint(" "),
130        errors.styles.style(Styled::Info).paint(info.to_string()),
131    ];
132    _ = writeln!(output, "{}", ANSIStrings(line_info));
133}
134
135/// Log the optional wiki link line that is part of the overall report.
136fn log_line_wiki<O: Write + Send>(errors: &Errors, output: &mut O, indentation: usize, wiki: &str) {
137    let line_info: &[ANSIString<'static>] = &[
138        errors.styles.style(Styled::Default).paint(format!("{:width$}", "", width = indentation)),
139        errors.styles.style(Styled::Default).paint(" "),
140        errors.styles.style(Styled::Location).paint("="),
141        errors.styles.style(Styled::Default).paint(" "),
142        errors.styles.style(Styled::InfoTag).paint("Wiki:"),
143        errors.styles.style(Styled::Default).paint(" "),
144        errors.styles.style(Styled::Info).paint(wiki.to_string()),
145    ];
146    _ = writeln!(output, "{}", ANSIStrings(line_info));
147}
148
149/// Log the additional number of this error that were found in other locations
150fn log_count<O: Write + Send>(errors: &Errors, output: &mut O, indentation: usize, count: usize) {
151    let line_count: &[ANSIString<'static>] = &[
152        errors.styles.style(Styled::Default).paint(format!("{:width$}", "", width = indentation)),
153        errors.styles.style(Styled::Location).paint("-->"),
154        errors.styles.style(Styled::Default).paint(" "),
155        errors.styles.style(Styled::Location).paint(format!("and {count} other locations")),
156    ];
157    _ = writeln!(output, "{}", ANSIStrings(line_count));
158}
159
160/// Log the line containing the location's mod name and filename.
161fn log_line_file_location<O: Write + Send>(
162    errors: &Errors,
163    output: &mut O,
164    pointer: &PointedMessage,
165    indentation: usize,
166) {
167    let line_filename: &[ANSIString<'static>] = &[
168        errors.styles.style(Styled::Default).paint(format!("{:width$}", "", width = indentation)),
169        errors.styles.style(Styled::Location).paint("-->"),
170        errors.styles.style(Styled::Default).paint(" "),
171        errors.styles.style(Styled::Location).paint(format!(
172            "[{}{}]",
173            kind_tag(errors, pointer.loc.kind),
174            stage_tag(pointer.loc.stage)
175        )),
176        errors.styles.style(Styled::Default).paint(" "),
177        errors
178            .styles
179            .style(Styled::Location)
180            .paint(format!("{}", pointer.loc.pathname().display())),
181    ];
182    _ = writeln!(output, "{}", ANSIStrings(line_filename));
183}
184
185/// Print a line from the source file.
186fn log_line_from_source<O: Write + Send>(
187    errors: &Errors,
188    output: &mut O,
189    pointer: &PointedMessage,
190    indentation: usize,
191    line: &str,
192    spaces: usize,
193) {
194    let line_from_source: &[ANSIString<'static>] = &[
195        errors.styles.style(Styled::Location).paint(format!("{:indentation$}", pointer.loc.line,)),
196        errors.styles.style(Styled::Default).paint(" "),
197        errors.styles.style(Styled::Location).paint("|"),
198        errors.styles.style(Styled::Default).paint(" "),
199        errors.styles.style(Styled::SourceText).paint(format!("{:spaces$}{line}", "")),
200    ];
201    _ = writeln!(output, "{}", ANSIStrings(line_from_source));
202}
203
204#[allow(clippy::too_many_arguments)]
205fn log_line_carets<O: Write + Send>(
206    errors: &Errors,
207    output: &mut O,
208    pointer: &PointedMessage,
209    indentation: usize,
210    line: &str,
211    removed: usize,
212    spaces: usize,
213    severity: Severity,
214) {
215    if pointer.length == 0 {
216        return;
217    }
218
219    let mut spacing = String::new();
220    for c in line.chars().take((pointer.loc.column as usize).saturating_sub(removed + 1)) {
221        // There might still be tabs in the non-leading space
222        if c == '\t' {
223            spacing.push('\t');
224        } else {
225            for _ in 0..c.width().unwrap_or(0) {
226                spacing.push(' ');
227            }
228        }
229    }
230
231    // A line containing the carets that point upwards at the source line.
232    let line_carets: &[ANSIString] = &[
233        errors.styles.style(Styled::Default).paint(format!("{:indentation$}", "")),
234        errors.styles.style(Styled::Default).paint(" "),
235        errors.styles.style(Styled::Location).paint("|"),
236        errors.styles.style(Styled::Default).paint(format!(
237            "{:width$}{spacing}",
238            "",
239            width = spaces + 1
240        )),
241        errors.styles.style(Styled::Tag(severity, true)).paint(format!(
242            "{:^^width$}",
243            "",
244            width = pointer.length
245        )),
246        errors.styles.style(Styled::Default).paint(" "),
247        errors
248            .styles
249            .style(Styled::Tag(severity, true))
250            .paint(pointer.msg.as_deref().map_or("", |_| "<-- ")),
251        errors
252            .styles
253            .style(Styled::Tag(severity, true))
254            .paint(pointer.msg.as_deref().unwrap_or("")),
255    ];
256    _ = writeln!(output, "{}", ANSIStrings(line_carets));
257}
258
259pub(crate) fn kind_tag<'a>(errors: &'a Errors<'a>, kind: FileKind) -> &'a str {
260    match kind {
261        FileKind::Internal => "Internal",
262        FileKind::Clausewitz => "Clausewitz",
263        FileKind::Jomini => "Jomini",
264        FileKind::Vanilla => match Game::game() {
265            #[cfg(feature = "ck3")]
266            Game::Ck3 => "CK3",
267            #[cfg(feature = "vic3")]
268            Game::Vic3 => "Vic3",
269            #[cfg(feature = "imperator")]
270            Game::Imperator => "Imperator",
271            #[cfg(feature = "eu5")]
272            Game::Eu5 => "EU5",
273            #[cfg(feature = "hoi4")]
274            Game::Hoi4 => "Hoi4",
275        },
276        FileKind::Dlc(idx) => &errors.loaded_dlcs_labels[idx as usize],
277        FileKind::LoadedMod(idx) => &errors.loaded_mods_labels[idx as usize],
278        FileKind::Mod => "MOD",
279    }
280}
281
282fn stage_tag(stage: FileStage) -> &'static str {
283    match stage {
284        #[cfg(feature = "eu5")]
285        FileStage::LoadingScreen => "(loadscreen)",
286        #[cfg(feature = "eu5")]
287        FileStage::MainMenu => "(menu)",
288        #[cfg(feature = "eu5")]
289        FileStage::InGame => "",
290        FileStage::NoStage => "",
291    }
292}
293
294/// Removes the leading spaces and tabs from `line` and returns it,
295/// together with how many character positions were removed and how many spaces should be substituted.
296fn line_spacing(line: &str) -> (&str, usize, usize) {
297    let mut remove = 0;
298    let mut spaces = 0;
299    for c in line.chars() {
300        if c == ' ' {
301            spaces += 1;
302        } else if c == '\t' {
303            spaces += SPACES_PER_TAB;
304        } else {
305            break;
306        }
307        remove += 1;
308    }
309    spaces = spaces.min(MAX_IDLE_SPACE);
310    (&line[remove..], remove, spaces)
311}