Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rust edition feature #12

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ edition = "2021"
[dependencies]
anyhow = "1.0.86"
clap = { version = "4.5.8", features = ["derive"] }
once_cell = "1.19.0"
regex = "1.10.5"
15 changes: 12 additions & 3 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
use std::{fs, path};
//! This module contains the command line interface for the tool

use anyhow::{Context, Result};
use clap::Parser;
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",
Expand All @@ -17,6 +22,7 @@ pub struct Arguments {
)]
pub source_file: path::PathBuf,

/// optional `stderr` file
#[arg(
short = 'e',
long = "stderr",
Expand All @@ -32,6 +38,7 @@ pub fn parse_arguments_and_read_file(args: &Arguments) -> Result<(String, Option
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(|| {
Expand Down Expand Up @@ -65,6 +72,8 @@ mod tests {
assert_eq!(args.stderr_file, Some(path::PathBuf::from("test.stderr")));
}

/// 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)
Comment on lines +75 to +76
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure a doc comment is required ? A regular comment might be enough

#[test]
fn debug_args() {
use clap::CommandFactory;
Expand Down
60 changes: 42 additions & 18 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
use std::{cell::OnceCell, fmt, str::FromStr};

use regex::Regex;

use self::WhichLine::*;
//! 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// https://docs.rs/once_cell/1.19.0/once_cell/#lazily-compiled-regex
/// Macro used to lazily create a new regex the first time it is invoked.
///
/// # Arguments
///
/// * `re` - The regex literal string used to build the automaton
///
/// # Example
///
/// ```rust
/// assert!(regex!(r"\w").is_match(" "));
/// ```
///
/// Taken from here https://docs.rs/once_cell/1.19.0/once_cell/#lazily-compiled-regex

Macros can be tricky to understand, I'd like this to be more thoroughly documented. A doctest example even provides a great insight (the example should compile & run when you launch cargo test).

#[macro_export]
macro_rules! regex {
($re:literal $(,)?) => {{
static RE: once_cell::sync::OnceCell<regex::Regex> = once_cell::sync::OnceCell::new();
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.
/// See [rustc dev guide](https://rustc-dev-guide.rust-lang.org/tests/ui.html#error-levels)
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum RustcErrorKind {
Help,
Expand Down Expand Up @@ -48,6 +59,7 @@ impl fmt::Display for RustcErrorKind {
}
}

/// To store information from rustc source file
#[derive(Debug)]
pub struct Error {
pub line_num: usize,
Expand All @@ -60,11 +72,14 @@ 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<RustcErrorKind>,
///Note: if we are loading this from rustc source file, this might be incomplete
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
///Note: if we are loading this from rustc source file, this might be incomplete
/// Note: if we are loading this from rustc source file, this might be incomplete

pub msg: String,
pub error_code: Option<String>,
}

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)
Comment on lines +81 to +82
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// 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)
/// Formats the `Error` for display according to `DejaGnu` format
/// See [`DejaGnu` documentation](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::*;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let error_type = match &self.kind {
            Some(Help) => "help",
            Some(Note) => "dg-note",
            Some(Suggestion) => "suggestion",
            Some(Warning) => "dg-warning",
            Some(Error) | None => "dg-error",
};

Expand Down Expand Up @@ -98,15 +113,20 @@ impl fmt::Display for Error {
}
}

/// Represents the line in the rustc source code where an error occurred.
/// 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 {
ThisLine,
FollowPrevious(usize),
AdjustBackward(usize),
}

/// The main function for loading errors from source file and from optional stderr file.
pub fn load_error(text_file: &str, stderr_file: Option<&str>) -> Vec<Error> {
let mut last_unfollow_error = None;
// For storing the errors
let mut errors = Vec::new();

for (line_num, line) in text_file.lines().enumerate() {
Expand All @@ -119,24 +139,31 @@ pub fn load_error(text_file: &str, stderr_file: Option<&str>) -> Vec<Error> {
}
}

// 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
&& error.msg == error_code.error_message_detail
|| error.msg == error_code.error_message_detail
{
error.error_code = Some(error_code.error_code.clone());
}
}
}
// return error detail with error codes
errors
}

/// To represent information from `stderr` file
#[derive(Debug)]
struct StderrResult {
error_code: String,
Expand All @@ -145,17 +172,15 @@ struct StderrResult {
}

fn is_error_code(s: &str) -> bool {
let re: OnceCell<Regex> = OnceCell::new();
let regex = re.get_or_init(|| Regex::new(r"^E\d{4}$").unwrap());
regex.is_match(s)
regex!(r"^E\d{4}$").is_match(s)
}

/// Parses error codes from the `stderr` file
fn parse_error_code(stderr_content: &str) -> Vec<StderrResult> {
// Modified regex pattern with named capture groups
let re: OnceCell<Regex> = OnceCell::new();
let error_pattern = re.get_or_init(|| {
Regex::new(r"error\[(?P<error_code>E\d{4})\]: (?P<error_message_detail>.+?)\n\s+-->.+:(?P<line_number>\d+):").unwrap()
});
let error_pattern = regex!(
r"error\[(?P<error_code>E\d{4})\]: (?P<error_message_detail>.+?)\n\s+-->.+:(?P<line_number>\d+):"
);

let mut results = Vec::new();

Expand Down Expand Up @@ -187,6 +212,7 @@ fn parse_error_code(stderr_content: &str) -> Vec<StderrResult> {
results
}

/// Parses error details from a source line.
fn parse_expected(
last_nonfollow_error: Option<usize>,
line_num: usize,
Expand All @@ -197,11 +223,8 @@ fn parse_expected(
// //~|
// //~^
// //~^^^^^
let re: OnceCell<Regex> = OnceCell::new();

let captures = re
.get_or_init(|| Regex::new(r"//(?:\[(?P<revs>[\w\-,]+)])?~(?P<adjust>\||\^*)").unwrap())
.captures(line)?;
let captures = regex!(r"//(?:\[(?P<revs>[\w\-,]+)])?~(?P<adjust>\||\^*)").captures(line)?;

let (follow, adjusts) = match &captures["adjust"] {
"|" => (true, 0),
Expand All @@ -227,6 +250,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.");
Expand Down
96 changes: 96 additions & 0 deletions src/header.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//! This module contains the logic for parsing rust test headers
//! See [rustc dev guide](https://rustc-dev-guide.rust-lang.org/tests/headers.html#test-headers)

#[derive(Debug)]
pub struct HeaderLine<'ln> {
pub line_number: usize,
/// The main part of the header directive, after removing the comment prefix
/// and the optional revision specifier.
pub _directive: &'ln str,
/// DejaGnu formatted header line
pub dejagnu_header: String,
}

pub fn parse_additional_options(code: &str) -> Vec<HeaderLine> {
let mut headers = Vec::new();

for (line_number, line) in code.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with("fn") || line.starts_with("mod") {
continue;
}
if is_header_line(line) {
if let Some(header_info) = add_additional_options(line, line_number) {
headers.push(header_info);
}
}
}
headers
}

pub fn is_header_line(line: &str) -> bool {
line.trim_start().starts_with("//@")
}

fn add_additional_options(code: &str, line_number: usize) -> Option<HeaderLine> {
//TODO: If we know the file extension, then update this to
// let comment = if testfile.extension().is_some_and(|e| e == "rs") { "//@" } else { "#" };
let comment = "//@";

if let Some((_header_revision, non_revisioned_directive_line)) = line_directive(comment, code) {
// The non_revisioned_directive_line is the directive without the "//@" prefix
let edition = parse_edition(non_revisioned_directive_line);
edition.as_ref()?;
Some(HeaderLine {
line_number: line_number + 1, // 1 based-indexed instead of zero based
_directive: "edition",
dejagnu_header: to_dejagnu_edition(edition.unwrap().as_str()),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can we unwrap edition safely without panicking here ?

})
} else {
None
}
}

fn line_directive<'line>(
comment: &str,
original_line: &'line str,
) -> Option<(Option<&'line str>, &'line str)> {
let after_comment = original_line
.trim_start()
.strip_prefix(comment)?
.trim_start();

if let Some(after_open_bracket) = after_comment.strip_prefix('[') {
let Some((line_revision, directive)) = after_open_bracket.split_once(']') else {
panic!(
"malformed condition directive: expected `{comment}[foo]`, found `{original_line}`"
)
Comment on lines +65 to +67
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not thrilled about this panic here

};

Some((Some(line_revision), directive.trim_start()))
} else {
Some((None, after_comment))
}
}

fn parse_edition(line: &str) -> Option<String> {
parse_name_value_directive(line, "edition")
}

fn parse_name_value_directive(line: &str, directive: &str) -> Option<String> {
let colon = directive.len();

if line.starts_with(directive) && line.as_bytes().get(colon) == Some(&b':') {
let value = line[(colon + 1)..].to_owned();
Some(value)
} else {
None
}
}

fn to_dejagnu_edition(edition: &str) -> String {
format!(
"// {{ dg-additional-options \"-frust-edition={}\" }}",
edition
)
}
Loading