1//! Parse a Rust source file into a `syn::File` and print out a debug
2//! representation of the syntax tree.
3//!
4//! Use the following command from this directory to test this program by
5//! running it on its own source code:
6//!
7//!     cargo run -- src/main.rs
8//!
9//! The output will begin with:
10//!
11//!     File {
12//!         shebang: None,
13//!         attrs: [
14//!             Attribute {
15//!                 pound_token: Pound,
16//!                 style: AttrStyle::Inner(
17//!         ...
18//!     }
19
20use colored::Colorize;
21use std::borrow::Cow;
22use std::env;
23use std::ffi::OsStr;
24use std::fmt::{self, Display};
25use std::fs;
26use std::io::{self, Write};
27use std::path::{Path, PathBuf};
28use std::process;
29
30enum Error {
31    IncorrectUsage,
32    ReadFile(io::Error),
33    ParseFile {
34        error: syn::Error,
35        filepath: PathBuf,
36        source_code: String,
37    },
38}
39
40impl Display for Error {
41    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
42        use self::Error::*;
43
44        match self {
45            IncorrectUsage => write!(f, "Usage: dump-syntax path/to/filename.rs"),
46            ReadFile(error) => write!(f, "Unable to read file: {}", error),
47            ParseFile {
48                error,
49                filepath,
50                source_code,
51            } => render_location(f, error, filepath, source_code),
52        }
53    }
54}
55
56fn main() {
57    if let Err(error) = try_main() {
58        let _ = writeln!(io::stderr(), "{}", error);
59        process::exit(1);
60    }
61}
62
63fn try_main() -> Result<(), Error> {
64    let mut args = env::args_os();
65    let _ = args.next(); // executable name
66
67    let filepath = match (args.next(), args.next()) {
68        (Some(arg), None) => PathBuf::from(arg),
69        _ => return Err(Error::IncorrectUsage),
70    };
71
72    let code = fs::read_to_string(&filepath).map_err(Error::ReadFile)?;
73    let syntax = syn::parse_file(&code).map_err({
74        |error| Error::ParseFile {
75            error,
76            filepath,
77            source_code: code,
78        }
79    })?;
80    println!("{:#?}", syntax);
81
82    Ok(())
83}
84
85// Render a rustc-style error message, including colors.
86//
87//     error: Syn unable to parse file
88//       --> main.rs:40:17
89//        |
90//     40 |     fn fmt(&self formatter: &mut fmt::Formatter) -> fmt::Result {
91//        |                  ^^^^^^^^^ expected `,`
92//
93fn render_location(
94    formatter: &mut fmt::Formatter,
95    err: &syn::Error,
96    filepath: &Path,
97    code: &str,
98) -> fmt::Result {
99    let start = err.span().start();
100    let mut end = err.span().end();
101
102    let code_line = match start.line.checked_sub(1).and_then(|n| code.lines().nth(n)) {
103        Some(line) => line,
104        None => return render_fallback(formatter, err),
105    };
106
107    if end.line > start.line {
108        end.line = start.line;
109        end.column = code_line.len();
110    }
111
112    let filename = filepath
113        .file_name()
114        .map(OsStr::to_string_lossy)
115        .unwrap_or(Cow::Borrowed("main.rs"));
116
117    write!(
118        formatter,
119        "\n\
120         {error}{header}\n\
121         {indent}{arrow} {filename}:{linenum}:{colnum}\n\
122         {indent} {pipe}\n\
123         {label} {pipe} {code}\n\
124         {indent} {pipe} {offset}{underline} {message}\n\
125         ",
126        error = "error".red().bold(),
127        header = ": Syn unable to parse file".bold(),
128        indent = " ".repeat(start.line.to_string().len()),
129        arrow = "-->".blue().bold(),
130        filename = filename,
131        linenum = start.line,
132        colnum = start.column,
133        pipe = "|".blue().bold(),
134        label = start.line.to_string().blue().bold(),
135        code = code_line.trim_end(),
136        offset = " ".repeat(start.column),
137        underline = "^"
138            .repeat(end.column.saturating_sub(start.column).max(1))
139            .red()
140            .bold(),
141        message = err.to_string().red(),
142    )
143}
144
145fn render_fallback(formatter: &mut fmt::Formatter, err: &syn::Error) -> fmt::Result {
146    write!(formatter, "Unable to parse file: {}", err)
147}
148