forge_fmt/
helpers.rs

1use crate::{
2    inline_config::{InlineConfig, InvalidInlineConfigItem},
3    Comments, Formatter, FormatterConfig, FormatterError, Visitable,
4};
5use ariadne::{Color, Fmt, Label, Report, ReportKind, Source};
6use itertools::Itertools;
7use solang_parser::{diagnostics::Diagnostic, pt::*};
8use std::{fmt::Write, path::Path};
9
10/// Result of parsing the source code
11#[derive(Debug)]
12pub struct Parsed<'a> {
13    /// The original source code.
14    pub src: &'a str,
15    /// The parse tree.
16    pub pt: SourceUnit,
17    /// Parsed comments.
18    pub comments: Comments,
19    /// Parsed inline config.
20    pub inline_config: InlineConfig,
21    /// Invalid inline config items parsed.
22    pub invalid_inline_config_items: Vec<(Loc, InvalidInlineConfigItem)>,
23}
24
25/// Parse source code.
26pub fn parse(src: &str) -> Result<Parsed<'_>, FormatterError> {
27    parse_raw(src).map_err(|diag| FormatterError::Parse(src.to_string(), None, diag))
28}
29
30/// Parse source code with a path for diagnostics.
31pub fn parse2<'s>(src: &'s str, path: Option<&Path>) -> Result<Parsed<'s>, FormatterError> {
32    parse_raw(src)
33        .map_err(|diag| FormatterError::Parse(src.to_string(), path.map(ToOwned::to_owned), diag))
34}
35
36/// Parse source code, returning a list of diagnostics on failure.
37pub fn parse_raw(src: &str) -> Result<Parsed<'_>, Vec<Diagnostic>> {
38    let (pt, comments) = solang_parser::parse(src, 0)?;
39    let comments = Comments::new(comments, src);
40    let (inline_config_items, invalid_inline_config_items): (Vec<_>, Vec<_>) =
41        comments.parse_inline_config_items().partition_result();
42    let inline_config = InlineConfig::new(inline_config_items, src);
43    Ok(Parsed { src, pt, comments, inline_config, invalid_inline_config_items })
44}
45
46/// Format parsed code
47pub fn format_to<W: Write>(
48    writer: W,
49    mut parsed: Parsed<'_>,
50    config: FormatterConfig,
51) -> Result<(), FormatterError> {
52    trace!(?parsed, ?config, "Formatting");
53    let mut formatter =
54        Formatter::new(writer, parsed.src, parsed.comments, parsed.inline_config, config);
55    parsed.pt.visit(&mut formatter)
56}
57
58/// Parse and format a string with default settings
59pub fn format(src: &str) -> Result<String, FormatterError> {
60    let parsed = parse(src)?;
61
62    let mut output = String::new();
63    format_to(&mut output, parsed, FormatterConfig::default())?;
64
65    Ok(output)
66}
67
68/// Converts the start offset of a `Loc` to `(line, col)`
69pub fn offset_to_line_column(content: &str, start: usize) -> (usize, usize) {
70    debug_assert!(content.len() > start);
71
72    // first line is `1`
73    let mut line_counter = 1;
74    for (offset, c) in content.chars().enumerate() {
75        if c == '\n' {
76            line_counter += 1;
77        }
78        if offset > start {
79            return (line_counter, offset - start)
80        }
81    }
82
83    unreachable!("content.len() > start")
84}
85
86/// Formats parser diagnostics
87pub fn format_diagnostics_report(
88    content: &str,
89    path: Option<&Path>,
90    diagnostics: &[Diagnostic],
91) -> String {
92    if diagnostics.is_empty() {
93        return String::new();
94    }
95
96    let filename =
97        path.map(|p| p.file_name().unwrap().to_string_lossy().to_string()).unwrap_or_default();
98    let mut s = Vec::new();
99    for diag in diagnostics {
100        let span = (filename.as_str(), diag.loc.start()..diag.loc.end());
101        let mut report = Report::build(ReportKind::Error, span.clone())
102            .with_message(format!("{:?}", diag.ty))
103            .with_label(
104                Label::new(span)
105                    .with_color(Color::Red)
106                    .with_message(diag.message.as_str().fg(Color::Red)),
107            );
108
109        for note in &diag.notes {
110            report = report.with_note(&note.message);
111        }
112
113        report.finish().write((filename.as_str(), Source::from(content)), &mut s).unwrap();
114    }
115    String::from_utf8(s).unwrap()
116}
117
118pub fn import_path_string(path: &ImportPath) -> String {
119    match path {
120        ImportPath::Filename(s) => s.string.clone(),
121        ImportPath::Path(p) => p.to_string(),
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    // <https://github.com/foundry-rs/foundry/issues/6816>
130    #[test]
131    fn test_interface_format() {
132        let s = "interface I {\n    function increment() external;\n    function number() external view returns (uint256);\n    function setNumber(uint256 newNumber) external;\n}";
133        let _formatted = format(s).unwrap();
134    }
135}