diff --git a/src/cli.rs b/src/cli.rs index 4a1c0d0..606ad16 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,15 +1,19 @@ +//! This module contains the command line interface for the tool + use { anyhow::{Context, Result}, clap::Parser, std::{fs, path}, }; +/// Command line arguments for the tool #[derive(Parser, Debug)] #[command( name = "rust test to DejaGnu", long_about = "A tool to convert rust tests into DejaGnu tests format" )] pub struct Arguments { + /// The rust source file to convert into `DejaGnu` format #[arg( short = 'f', long = "file", @@ -18,6 +22,7 @@ pub struct Arguments { )] pub source_file: path::PathBuf, + /// `optional argument`: The `stderr` file to extract rustc error codes, column numbers and convert them into `DejaGnu` format #[arg( short = 'e', long = "stderr", @@ -28,11 +33,21 @@ pub struct Arguments { pub stderr_file: Option, } +/// Parses the command line arguments and reads the input file. +/// +/// # Arguments +/// +/// * `args` - A reference to the `Arguments` struct containing the parsed command line arguments. +/// +/// # Returns +/// +/// * `Result<(String, Option)>` - Returns a tuple containing the source code and optionally the stderr file content if successful, otherwise returns an error. pub fn parse_arguments_and_read_file(args: &Arguments) -> Result<(String, Option)> { //TODO: maybe to use sanitization to prevent reading files outside the project directory let source_code = fs::read_to_string(&args.source_file) .with_context(|| format!("could not read sourcefile `{}`", args.source_file.display()))?; + // Read the stderr file if it exists let err_file = match &args.stderr_file { Some(stderr_file) => Some(fs::read_to_string(stderr_file).with_context(|| { @@ -44,6 +59,11 @@ pub fn parse_arguments_and_read_file(args: &Arguments) -> Result<(String, Option Ok((source_code, err_file)) } +/// Prints the source code to the standard output. +/// +/// # Arguments +/// +/// * `source_code` - A reference to the source code string to be printed. pub fn print_source_code(source_code: &str) { println!("{source_code}"); } @@ -52,6 +72,7 @@ pub fn print_source_code(source_code: &str) { mod tests { use super::*; + /// Tests the required argument `file`. #[test] fn test_required_argument_file() { let args = Arguments::parse_from(["test", "-f", "test.rs"]); @@ -59,6 +80,7 @@ mod tests { assert_eq!(args.stderr_file, None); } + /// Tests the optional argument `stderr_file`. #[test] fn test_optional_argument_file() { let args = Arguments::parse_from(["test", "-f", "test.rs", "-e", "test.stderr"]); @@ -66,6 +88,9 @@ mod tests { assert_eq!(args.stderr_file, Some(path::PathBuf::from("test.stderr"))); } + /// Tests the debug assertions for the command line arguments. + /// clap reports most development errors as `debug_assert!`s + /// See this for more details, [here](https://docs.rs/clap/4.5.15/clap/_derive/_tutorial/chapter_4/index.html) #[test] fn debug_args() { use clap::CommandFactory; diff --git a/src/errors.rs b/src/errors.rs index 93036b8..278cc9a 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,9 +1,34 @@ +//! This module contains the logic for parsing rustc error messages. + use { self::WhichLine::*, std::{fmt, str::FromStr}, }; -// https://docs.rs/once_cell/1.19.0/once_cell/#lazily-compiled-regex +/// A macro to lazily compile a regular expression. +/// +/// # Arguments +/// +/// * `$re` - A string literal representing the regular expression pattern. +/// +/// # Example +/// +/// This example: +/// ```rust +/// regex!(r"^E\d{4}$") +/// ``` +/// was expanded to +/// ```rust +/// { +/// static RE: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); +/// RE.get_or_init(|| regex::Regex::new(r"^E\d{4}$").unwrap()) +/// } +/// ``` +/// Another example: +/// ```rust +/// let re = regex!(r"^E\d{4}$"); +/// assert!(re.is_match("E1234")); +/// ``` #[macro_export] macro_rules! regex { ($re:literal $(,)?) => {{ @@ -11,7 +36,19 @@ macro_rules! regex { RE.get_or_init(|| regex::Regex::new($re).unwrap()) }}; } -// https://rustc-dev-guide.rust-lang.org/tests/ui.html#error-levels + +/// Represents the different kinds of Rustc compiler messages. +/// +/// This enum is used to categorize the types of messages that the Rust compiler can produce, +/// See [rustc dev guide](https://rustc-dev-guide.rust-lang.org/tests/ui.html#error-levels) +/// +/// # Variants +/// +/// * `Help` - Represents a help message. +/// * `Error` - Represents an error message. +/// * `Note` - Represents a note message. +/// * `Suggestion` - Represents a suggestion message. +/// * `Warning` - Represents a warning message. #[derive(Copy, Clone, Debug, PartialEq)] pub enum RustcErrorKind { Help, @@ -23,6 +60,25 @@ pub enum RustcErrorKind { impl FromStr for RustcErrorKind { type Err = (); + + /// Converts a string slice to a `RustcErrorKind`. + /// + /// # Arguments + /// + /// * `s` - A case-insensitive String slice representing the error kind. + /// + /// # Returns + /// + /// * `Result` - Returns the corresponding `RustcErrorKind` variant if successful, + /// otherwise returns an error. + /// + /// # Examples + /// + /// ```rust + /// use std::str::FromStr; + /// assert_eq!(RustcErrorKind::from_str("help").unwrap(), RustcErrorKind::Help); + /// assert_eq!(RustcErrorKind::from_str("error").unwrap(), RustcErrorKind::Error); + /// ``` fn from_str(s: &str) -> Result { let s = s.to_uppercase(); // Some RustcErrorKinds has this colon, so we need to split it @@ -44,6 +100,26 @@ impl FromStr for RustcErrorKind { } impl fmt::Display for RustcErrorKind { + /// Formats the `RustcErrorKind` for display. + /// + /// This method implements the `fmt` function from the `fmt::Display` trait, + /// allowing `RustcErrorKind` to be formatted according to rust compiletest. + /// + /// # Arguments + /// + /// * `f` - A mutable reference to a `fmt::Formatter` used to build the formatted string. + /// + /// # Returns + /// + /// * `fmt::Result` - Returns `fmt::Result` indicating whether the formatting was successful. + /// + /// # Examples + /// + /// ```rust + /// use std::fmt; + /// let kind = RustcErrorKind::Help; + /// assert_eq!(format!("{}", kind), "help message"); + /// ``` fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { RustcErrorKind::Help => write!(f, "help message"), @@ -55,8 +131,12 @@ impl fmt::Display for RustcErrorKind { } } +/// For representing an error in the Rustc source file. +/// +/// This struct was use to store information from rustc source file #[derive(Debug)] pub struct Error { + /// The line number where the error occurred. pub line_num: usize, /// We also need to take into account the relative line number. /// - `1` if the error is on the previous line @@ -67,11 +147,17 @@ pub struct Error { /// What kind of message we expect (e.g., warning, error, suggestion). /// `None` if not specified or unknown message kind. pub kind: Option, + + /// The error message. (if we are loading this from rustc source file, this might be incomplete) pub msg: String, + + /// An optional error code associated with the error. pub error_code: Option, } impl fmt::Display for Error { + /// Formats the `Error` for display according to `DejaGnu` format + /// See `DejaGnu` documentation [here](https://gcc.gnu.org/onlinedocs/gccint/testsuites/directives-used-within-dejagnu-tests/syntax-and-descriptions-of-test-directives.html) fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use RustcErrorKind::*; @@ -105,15 +191,46 @@ impl fmt::Display for Error { } } +/// Represents the line in the rustc source code where an error occurred. +/// +/// This enum is used to determine the relative position of the error line +/// in relation to the current line being processed. +/// +/// Luckily, rust compile test only stores error messages on and after the line where the error occurred. +/// But `DejaGnu` can process error messages on the previous line, the current line, or the next line. #[derive(PartialEq, Debug)] enum WhichLine { + /// The error is on the current line. ThisLine, + + /// The error follows the previous line. + /// + /// # Arguments + /// + /// * `usize` - The number of lines to follow. FollowPrevious(usize), + + /// The error is adjusted backward by a certain number of lines. + /// + /// # Arguments + /// + /// * `usize` - The number of lines to adjust backward. AdjustBackward(usize), } +/// The main function for loading errors from source file and from optional stderr file. +/// +/// # Arguments +/// +/// * `text_file` - A string slice containing rustc error messages. +/// * `stderr_file` - An optional string slice containing error codes. +/// +/// # Returns +/// +/// * `Vec` - A vector of `Error` structs containing the parsed error information. pub fn load_error(text_file: &str, stderr_file: Option<&str>) -> Vec { let mut last_unfollow_error = None; + // For storing the errors let mut errors = Vec::new(); for (line_num, line) in text_file.lines().enumerate() { @@ -126,12 +243,17 @@ pub fn load_error(text_file: &str, stderr_file: Option<&str>) -> Vec { } } + // If stderr file is not provided, return the errors if stderr_file.is_none() { return errors; } // TODO: improve this code incrementally + // parsing error related information from `.stderr` file let error_code_stderr = parse_error_code(stderr_file.expect("stderr file is not found")); + // TODO: We need to load error messages from `.stderr` instead of source file become sometimes source file contains incomplete error messages + // finding the error code w.r.t line number and error message + // TODO: sometimes, the error message might not be same but this doesn't matter as we are not comparing the row number for the message for error in errors.iter_mut() { for error_code in error_code_stderr.iter() { if error.line_num == error_code.line_number @@ -141,20 +263,46 @@ pub fn load_error(text_file: &str, stderr_file: Option<&str>) -> Vec { } } } + // return error detail with error code errors } +/// Represents the result of parsing an error from the stderr file. #[derive(Debug)] struct StderrResult { + /// The error code associated with the error. + /// We only consider error codes that match the pattern `E\d{4}`. error_code: String, + + /// The complete error message. error_message_detail: String, + + /// The line number in the source code where the error occurred. line_number: usize, } +/// Checks if the given string is a valid rustc error code. +/// +/// # Arguments +/// +/// * `s` - A string slice representing the error code to be checked. +/// +/// # Returns +/// +/// * `bool` - Returns `true` if the string matches the error code pattern, otherwise `false`. fn is_error_code(s: &str) -> bool { regex!(r"^E\d{4}$").is_match(s) } +/// Parses error codes from the stderr file. +/// +/// # Arguments +/// +/// * `stderr_content` - A string slice representing the content of the stderr file. +/// +/// # Returns +/// +/// * `Vec` - A vector of `StderrResult` structs containing the parsed error information. fn parse_error_code(stderr_content: &str) -> Vec { // Modified regex pattern with named capture groups let error_pattern = regex!( @@ -176,6 +324,7 @@ fn parse_error_code(stderr_content: &str) -> Vec { || "Line number not found".to_string(), |m| m.as_str().to_string(), ); + // We only consider error codes that match the pattern `E\d{4}` if !is_error_code(&error_code) { continue; } @@ -191,6 +340,17 @@ fn parse_error_code(stderr_content: &str) -> Vec { results } +/// Parses expected error comments from a source line. +/// +/// # Arguments +/// +/// * `last_nonfollow_error` - An optional `usize` representing the line number of the last non-follow error. +/// * `line_num` - A `usize` representing the current line number being processed. +/// * `line` - A string slice representing the content of the current line. +/// +/// # Returns +/// +/// * `Option<(WhichLine, Error)>` - Returns an `Option` containing a tuple with `WhichLine` and `Error` if a match is found, otherwise `None`. fn parse_expected( last_nonfollow_error: Option, line_num: usize, @@ -228,6 +388,7 @@ fn parse_expected( let msg = msg.trim().to_owned(); + // If we find `//~|` or `//~^`, we need to adjust the line number. let mut relative_line_num = line_num as i32; let (which, line_num) = if follow { assert_eq!(adjusts, 0, "use either //~| or //~^, not both."); @@ -264,6 +425,7 @@ fn parse_expected( mod tests { use super::*; + /// Test that `RustcErrorKind::from_str` correctly parses "help" and "help:" into `RustcErrorKind::Help`. #[test] fn from_str_help_returns_help() { assert_eq!( @@ -276,6 +438,7 @@ mod tests { ); } + /// Tests that `RustcErrorKind::from_str` correctly parses "error" into `RustcErrorKind::Error`. #[test] fn from_str_error_returns_error() { assert_eq!( @@ -284,6 +447,7 @@ mod tests { ); } + /// Tests that `RustcErrorKind::from_str` correctly parses "note" into `RustcErrorKind::Note`. #[test] fn from_str_note_returns_note() { assert_eq!( @@ -292,6 +456,7 @@ mod tests { ); } + /// Tests that `RustcErrorKind::from_str` correctly parses "suggestion" into `RustcErrorKind::Suggestion`. #[test] fn from_str_suggestion_returns_suggestion() { assert_eq!( @@ -300,6 +465,7 @@ mod tests { ); } + /// Tests that `RustcErrorKind::from_str` correctly parses "warning" into `RustcErrorKind::Warning`. #[test] fn from_str_warning_returns_warning() { assert_eq!( @@ -308,6 +474,7 @@ mod tests { ); } + /// Tests that `RustcErrorKind::from_str` correctly parses "warn" into `RustcErrorKind::Warning`. #[test] fn from_str_warn_returns_warning() { assert_eq!( @@ -316,37 +483,44 @@ mod tests { ); } + /// Tests that `RustcErrorKind::from_str` returns an error for unrecognized strings. #[test] fn from_str_unrecognized_returns_err() { assert!(RustcErrorKind::from_str("unrecognized").is_err()); } + /// Tests that `RustcErrorKind::from_str` returns an error for an empty string. #[test] fn from_str_empty_string_returns_err() { // split always returns at least one element assert!(RustcErrorKind::from_str("").is_err()); } + /// Tests that `RustcErrorKind::Help` is formatted correctly as "help message" according to rust compiletest tool #[test] fn display_help_outputs_correct_string() { assert_eq!(format!("{}", RustcErrorKind::Help), "help message"); } + /// Tests that `RustcErrorKind::Error` is formatted correctly as "error" according to rust compiletest tool #[test] fn display_error_outputs_correct_string() { assert_eq!(format!("{}", RustcErrorKind::Error), "error"); } + /// Tests that `RustcErrorKind::Note` is formatted correctly as "note" according to rust compiletest tool #[test] fn display_note_outputs_correct_string() { assert_eq!(format!("{}", RustcErrorKind::Note), "note"); } + /// Tests that `RustcErrorKind::Suggestion` is formatted correctly as "suggestion" according to rust compiletest tool #[test] fn display_suggestion_outputs_correct_string() { assert_eq!(format!("{}", RustcErrorKind::Suggestion), "suggestion"); } + /// Tests that `RustcErrorKind::Warning` is formatted correctly as "warning" according to rust compiletest tool #[test] fn display_warning_outputs_correct_string() { assert_eq!(format!("{}", RustcErrorKind::Warning), "warning"); diff --git a/src/main.rs b/src/main.rs index bbf45f0..fbf36d2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +//! The main entry point of the program. + use { anyhow::{Context, Result}, clap::Parser, @@ -7,10 +9,20 @@ mod cli; mod errors; mod transform; +/// The main function of the program. +/// +/// # Returns +/// +/// * `Result<()>` - Returns an `Ok(())` if the program runs successfully, otherwise returns an error. fn main() -> Result<()> { try_parse() } +/// Parses the command line arguments, reads the input file, transforms the code, and prints the transformed code. +/// +/// # Returns +/// +/// * `Result<()>` - Returns an `Ok(())` if the operations are successful, otherwise returns an error. fn try_parse() -> Result<()> { let args = cli::Arguments::parse(); diff --git a/src/transform.rs b/src/transform.rs index f1da343..b161f01 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -1,36 +1,57 @@ +//! This module contains the code transformation logic. + use { crate::{errors, regex}, anyhow::Result, }; -/// This function takes the rust code as input +/// This function takes the rust code and optional `stderr` files as input /// and returns the code with DejaGnu directive +/// +/// # Arguments +/// +/// * `code` - A reference to the Rust source code as a string slice. +/// * `stderr_file` - An optional reference to the `stderr` file content as a string slice. +/// +/// # Returns +/// +/// * `Result` - Returns the transformed code as a string if successful, otherwise returns an error. pub fn transform_code(code: &str, stderr_file: Option<&str>) -> Result { + // Load the rustc error messages, codes, lines and relative line numbers let errors = errors::load_error(code, stderr_file); + // For storing the transformed code let mut new_code = String::new(); let mut line_num = 1; + // finding the respective line number and adding the error code for line in code.lines() { let mut new_line = line.to_string(); // TODO: This is not the efficient way to find respective line number for error in errors.iter() { + // Checking the original line number if (error.line_num as i32 - error.relative_line_num) != line_num { continue; } // In rustc test suites, the error directive is - // on the same line or the next line not on the previous line + // on the same line or on the next line, but not on the previous line + // See this: https://rustc-dev-guide.rust-lang.org/tests/ui.html#error-annotations // For the error on the next line if error.relative_line_num != 0 { + // We simply add the error message, not to worry about the code + // The error was printed by our overloaded `Display` trait new_line = format!("{}", error); } else { - // For the error on the same line + // For the error on the same line, we need to add error message at the end of the line let captures = regex!(r"//(?:\[(?P[\w\-,]+)])?~(?P\||\^*)") .captures(line) .expect("Could not find the error directive"); // Get the part of comment before the sigil (e.g. `~^` or ~|) let whole_match = captures.get(0).unwrap(); + // Get the existing source code before the error directive //~ ERROR or similar to this let before_match = &line[..whole_match.start()]; + + // The error was printed by our overloaded `Display` trait new_line = format!("{}{}", before_match, error); } break; @@ -47,6 +68,7 @@ pub fn transform_code(code: &str, stderr_file: Option<&str>) -> Result { mod tests { use super::*; + /// Tests the `transform_code` function with a sample Rust error message. #[test] fn test_transform() { // as suggested by @CohenArthur, we only need to add error code in msg