diff --git a/Cargo.lock b/Cargo.lock index 5663988..6c6ca21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,7 +32,7 @@ dependencies = [ [[package]] name = "heraclitus-compiler" -version = "1.6.1" +version = "1.6.2" dependencies = [ "capitalize", "colored", diff --git a/README.md b/README.md index c3d2e27..6556124 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,11 @@ let tokens = cc.tokenize()?; # Change log 🚀 +## Version 1.7.0 +### Feature: +- Tokens now contain information about their index of the first character in the source code +- Added `PositionInfo::from_between_tokens` method to select a region between two tokens in messages + ## Version 1.6.2 ### Fix: - Fixes escapes that were handled improperly diff --git a/src/compiling/failing/logger.rs b/src/compiling/failing/logger.rs index 7afb140..8ede4fc 100644 --- a/src/compiling/failing/logger.rs +++ b/src/compiling/failing/logger.rs @@ -5,6 +5,7 @@ use colored::{Colorize, Color}; use pad::PadStr; use crate::compiling::failing::position_info::PositionInfo; use crate::compiling::failing::message::MessageType; +use crate::prelude::Position; /// This is a logger that is used to log messages to the user /// The logger is being used internally by the Message struct @@ -64,13 +65,17 @@ impl Logger { /// Render location details with supplied coloring pub fn path(self) -> Self { + let get_row_col = |pos: &PositionInfo| match pos.position { + Position::Pos(row, col) => format!("{}:{}", row, col), + Position::EOF => " end of file".to_string() + }; let path = match self.trace.first() { - Some(det) => { + Some(pos) => { [ - format!("at {}:{}:{}", det.get_path(), det.row, det.col), + format!("at {}:{}", pos.get_path(), get_row_col(pos)), self.trace.iter() .skip(1) - .map(|det| format!("in {}:{}:{}", det.get_path(), det.row, det.col)) + .map(|pos| format!("in {}:{}", pos.get_path(), get_row_col(pos))) .collect::>() .join("\n") ].join("\n") @@ -83,29 +88,32 @@ impl Logger { self } - // Returns last row and column - fn get_row_col_len(&self) -> (usize, usize, usize) { + // Returns last row, column and it's length + fn get_row_col_len(&self) -> Option<(usize, usize, usize)> { match self.trace.first() { - Some(loc) => (loc.row, loc.col, loc.len), - None => (0, 0, 0) + Some(pos) => match pos.position { + Position::Pos(row, col) => Some((row, col, pos.len)), + Position::EOF => None + }, + None => None } } // Get max padding size (for line numbering) - fn get_max_pad_size(&self, length: usize) -> usize { - let (row, _, _) = self.get_row_col_len(); + fn get_max_pad_size(&self, length: usize) -> Option { + let (row, _, _) = self.get_row_col_len()?; if row < length - 1 { - format!("{}", row + 1).len() + Some(format!("{}", row + 1).len()) } else { - format!("{}", row).len() + Some(format!("{}", row).len()) } } // Returns chopped string where fisrt and third part are supposed // to be left as is but the second one is supposed to be highlighted - fn get_highlighted_part(&self, line: &str) -> [String;3] { - let (_row, col, len) = self.get_row_col_len(); + fn get_highlighted_part(&self, line: &str) -> Option<[String;3]> { + let (_row, col, len) = self.get_row_col_len()?; let begin = col - 1; let end = begin + len; let mut results: [String; 3] = Default::default(); @@ -120,23 +128,20 @@ impl Logger { results[1].push(letter); } } - results + Some(results) } // Return requested row with appropriate coloring - fn get_snippet_row(&self, code: &Vec, index: usize, offset: i8, overflow: &mut usize) -> String { - let (row, col, len) = self.get_row_col_len(); - let max_pad = self.get_max_pad_size(code.len()); + fn get_snippet_row(&self, code: &Vec, index: usize, offset: i8, overflow: &mut usize) -> Option { + let (row, col, len) = self.get_row_col_len()?; + let max_pad = self.get_max_pad_size(code.len())?; let index = index as i32 + offset as i32; - if code.get(index as usize).is_none() { - return String::new(); - } let row = row as i32 + offset as i32; - let code = code[index as usize].clone(); + let code = code.get(index as usize)?.clone(); let line = format!("{row}").pad_to_width(max_pad); // Case if we are in the same line as the error (or message) if offset == 0 { - let slices = self.get_highlighted_part(&code); + let slices = self.get_highlighted_part(&code)?; let formatted = format!("{}{}{}", slices[0], slices[1].color(self.kind_to_color()), slices[2]); // If we are at the end of the code snippet and there is still some if col - 1 + len > code.chars().count() { @@ -144,7 +149,7 @@ impl Logger { // and other 1 is the new line character that we do not display *overflow = col - 2 + len - code.chars().count(); } - format!("{line}| {formatted}") + Some(format!("{line}| {formatted}")) } // Case if we are in a different line than the error (or message) else { @@ -152,18 +157,18 @@ impl Logger { if *overflow > 0 { // Case if all line is highlighted if *overflow > code.chars().count() { - format!("{line}| {}", code.color(self.kind_to_color())).dimmed().to_string() + Some(format!("{line}| {}", code.color(self.kind_to_color())).dimmed().to_string()) } // Case if some line is highlighted else { let err = code.get(0..*overflow).unwrap().to_string().color(self.kind_to_color()); let rest = code.get(*overflow..).unwrap().to_string(); - format!("{line}| {err}{rest}").dimmed().to_string() + Some(format!("{line}| {err}{rest}").dimmed().to_string()) } } // Case if no overflow else { - format!("{line}| {code}").dimmed().to_string() + Some(format!("{line}| {code}").dimmed().to_string()) } } } @@ -183,23 +188,23 @@ impl Logger { } /// Render snippet of the code based on the code data - fn snippet_from_code(&self, code: String) { - let (row, _, _) = self.get_row_col_len(); + fn snippet_from_code(&self, code: String) -> Option<()> { + let (row, _, _) = self.get_row_col_len()?; let mut overflow = 0; let index = row - 1; let code = code.split('\n') - .map(|item| item.to_string()) + .map(|item| item.trim_end().to_string()) .collect::>(); eprintln!(); // Show additional code above the snippet - if index > 0 { - eprintln!("{}", self.get_snippet_row(&code, index, -1, &mut overflow)); + if let Some(line) = self.get_snippet_row(&code, index, -1, &mut overflow) { + eprintln!("{}", line); } - eprintln!("{}", self.get_snippet_row(&code, index, 0, &mut overflow)); + // Show the current line of code + eprintln!("{}", self.get_snippet_row(&code, index, 0, &mut overflow)?); // Show additional code below the snippet - if index < code.len() - 1 { - eprintln!("{}", self.get_snippet_row(&code, index, 1, &mut overflow)); - } + eprintln!("{}", self.get_snippet_row(&code, index, 1, &mut overflow)?); + Some(()) } } @@ -209,7 +214,7 @@ mod test { use std::time::Duration; use std::thread::sleep; - use crate::prelude::{PositionInfo, MessageType}; + use crate::prelude::{DefaultMetadata, Metadata, MessageType, PositionInfo, Token}; #[allow(unused_variables)] #[test] @@ -218,12 +223,12 @@ mod test { "let a = 12", "value = 'this", "is mutltiline", - "code" + "code'" ].join("\n"); // Uncomment to see the error message sleep(Duration::from_secs(1)); let trace = [ - PositionInfo::at_pos(Some("/path/to/bar".to_string()), (3, 25), 0), + PositionInfo::at_pos(Some("/path/to/bar".to_string()), (3, 4), 10), PositionInfo::at_pos(Some("/path/to/foo".to_string()), (2, 9), 24), ]; super::Logger::new(MessageType::Error, &trace) @@ -249,4 +254,24 @@ mod test { .path() .snippet(Some(code)); } + + #[test] + fn test_between_tokens() { + let code = vec![ + "foo(12 + 24)" + ].join("\n"); + // Uncomment to see the error message + sleep(Duration::from_secs(1)); + let begin = Token { word: "12".to_string(), pos: (1, 5), start: 4 }; + let end = Token { word: ")".to_string(), pos: (1, 12), start: 11 }; + let mut meta = DefaultMetadata::new(vec![], Some("/path/to/foo".to_string()), Some(code.clone())); + let trace = [ + PositionInfo::from_between_tokens(&mut meta, Some(begin), Some(end)) + ]; + super::Logger::new(MessageType::Error, &trace) + .header(MessageType::Error) + .text(Some(format!("Cannot call function \"foobar\" on a number"))) + .path() + .snippet(Some(code)); + } } diff --git a/src/compiling/failing/position_info.rs b/src/compiling/failing/position_info.rs index dfde4ac..178123f 100644 --- a/src/compiling/failing/position_info.rs +++ b/src/compiling/failing/position_info.rs @@ -1,5 +1,5 @@ //! Simple error structure -//! +//! //! This module defines `ErrorDetails` structure which is returned as //! an error by lexer and is used in parsing phase as well. @@ -23,10 +23,6 @@ pub struct PositionInfo { pub path: Option, /// Location of this error pub position: Position, - /// Row of the error - pub row: usize, - /// Column of the error - pub col: usize, /// Length of the token pub len: usize, /// Additional information @@ -39,8 +35,6 @@ impl PositionInfo { PositionInfo { position, path: meta.get_path(), - row: 0, - col: 0, len, data: None }.updated_pos(meta) @@ -51,8 +45,6 @@ impl PositionInfo { PositionInfo { path: meta.get_path(), position: Position::EOF, - row: 0, - col: 0, len: 0, data: None }.updated_pos(meta) @@ -63,15 +55,14 @@ impl PositionInfo { PositionInfo { path, position: Position::Pos(row, col), - row, - col, len, data: None } } fn updated_pos(mut self, meta: &impl Metadata) -> Self { - (self.row, self.col) = self.get_pos_by_file_or_code(meta.get_code()); + let (row, col) = self.get_pos_by_file_or_code(meta.get_code()); + self.position = Position::Pos(row, col); self } @@ -81,7 +72,7 @@ impl PositionInfo { } /// Create an error at current position of current token by metadata - /// + /// /// This function can become handy when parsing the AST. /// This takes the current index stored in metadata and uses it /// to retrieve token stored under it in metadata's expression. @@ -91,7 +82,7 @@ impl PositionInfo { } /// Create an error at position of the provided token - /// + /// /// This function gives you ability to store tokens /// and error once you finished parsing the entire expression pub fn from_token(meta: &impl Metadata, token_opt: Option) -> Self { @@ -101,6 +92,21 @@ impl PositionInfo { } } + /// Create an error at position between two tokens + /// + /// This function is used to create an error between two tokens + /// which can be used to express an error in a specific range + pub fn from_between_tokens(meta: &impl Metadata, begin: Option, end: Option) -> Self { + if let (Some(begin), Some(end)) = (begin, end) { + let (row, col) = begin.pos; + let len = end.start - begin.start; + PositionInfo::at_pos(meta.get_path(), (row, col), len) + } + else { + PositionInfo::from_metadata(meta) + } + } + /// Attach additional data in form of a string pub fn data>(mut self, data: T) -> Self { self.data = Some(data.as_ref().to_string()); @@ -153,3 +159,26 @@ impl PositionInfo { } } } + +#[cfg(test)] +mod test { + use crate::prelude::DefaultMetadata; + use super::*; + + #[test] + fn test_position_info() { + let pos = PositionInfo::at_pos(Some("test".to_string()), (1, 1), 1); + assert_eq!(pos.get_path(), "test"); + assert_eq!(pos.get_pos_by_code("test"), (1, 1)); + } + + #[test] + fn test_position_info_between_tokens() { + let begin = Token { word: "begin".to_string(), pos: (1, 1), start: 0 }; + let to = Token { word: "to".to_string(), pos: (1, 7), start: 6 }; + let end = Token { word: "end".to_string(), pos: (1, 10), start: 9 }; + let mut meta = DefaultMetadata::new(vec![begin.clone(), to.clone(), end.clone()], None, Some("begin to end".to_string())); + let pos = PositionInfo::from_between_tokens(&mut meta, Some(begin.clone()), Some(end.clone())); + assert_eq!(pos.len, end.start - begin.start); + } +} diff --git a/src/compiling/lexing/lexer.rs b/src/compiling/lexing/lexer.rs index 74bf4b9..d1a438f 100644 --- a/src/compiling/lexing/lexer.rs +++ b/src/compiling/lexing/lexer.rs @@ -21,7 +21,7 @@ pub enum LexerErrorType { pub type LexerError = (LexerErrorType, PositionInfo); /// The Lexer -/// +/// /// Lexer takes source code in a form of a string and translates it to a list of tokens. /// This particular implementation requires additional metadata such as like regions or symbols. /// These can be supplied by the `Compiler` in a one cohesive package. Hence the API requires to @@ -38,7 +38,8 @@ pub struct Lexer<'a> { separator_mode: SeparatorMode, scoping_mode: ScopingMode, is_escaped: bool, - position: (usize, usize) + position: (usize, usize), + index: usize } impl<'a> Lexer<'a> { @@ -57,6 +58,7 @@ impl<'a> Lexer<'a> { scoping_mode: cc.scoping_mode.clone(), is_escaped: false, position: (0, 0), + index: 0 } } @@ -70,7 +72,8 @@ impl<'a> Lexer<'a> { let (row, _col) = self.reader.get_position(); self.lexem.push(Token { word, - pos: (row, 1) + pos: (row, 1), + start: self.reader.get_index(), }); self.position = (0, 0); String::new() @@ -83,7 +86,8 @@ impl<'a> Lexer<'a> { if !word.is_empty() { self.lexem.push(Token { word, - pos: self.position + pos: self.position, + start: self.index }); self.position = (0, 0); String::new() @@ -97,7 +101,8 @@ impl<'a> Lexer<'a> { if !word.is_empty() { self.lexem.push(Token { word, - pos: self.position + pos: self.position, + start: self.index }); self.position = (0, 0); String::new() @@ -121,6 +126,7 @@ impl<'a> Lexer<'a> { word = self.add_word(word); word.push(letter); self.position = self.reader.get_position(); + self.index = self.reader.get_index(); self.add_word_inclusively(word) } @@ -142,7 +148,7 @@ impl<'a> Lexer<'a> { } /// Tokenize source code - /// + /// /// Run lexer and tokenize code. The result is stored in the lexem attribute pub fn run(&mut self) -> Result<(), LexerError> { let mut word = String::new(); @@ -515,4 +521,4 @@ mod test { } assert_eq!(expected, result); } -} \ No newline at end of file +} diff --git a/src/compiling/lexing/reader.rs b/src/compiling/lexing/reader.rs index 57a30e4..c87ac4d 100644 --- a/src/compiling/lexing/reader.rs +++ b/src/compiling/lexing/reader.rs @@ -196,4 +196,4 @@ mod test { assert_eq!(expected, result_history); assert_eq!(expected, result_future); } -} \ No newline at end of file +} diff --git a/src/compiling/parser/pattern.rs b/src/compiling/parser/pattern.rs index f1ed937..34a3d9c 100644 --- a/src/compiling/parser/pattern.rs +++ b/src/compiling/parser/pattern.rs @@ -3,7 +3,7 @@ use crate::compiling::failing::failure::Failure; use super::{ Metadata, SyntaxModule }; /// Matches one token with given word -/// +/// /// If token was matched succesfully - the word it contained is returned. /// Otherwise detailed information is returned about where this happened. /// # Example @@ -26,7 +26,7 @@ pub fn token>(meta: &mut impl Metadata, text: T) -> Result bool) -> Resul } /// Parses syntax module -/// +/// /// If syntax module was parsed succesfully - nothing is returned. /// Otherwise detailed information is returned about where this happened. /// # Example @@ -82,7 +82,7 @@ pub fn syntax(meta: &mut M, module: &mut impl SyntaxModule) -> R } /// Matches indentation -/// +/// /// If indentation was matched succesfully - the amount of spaces is returned. /// Otherwise detailed information is returned about where this happened. /// # Example @@ -103,7 +103,7 @@ pub fn indent(meta: &mut impl Metadata) -> Result { } /// Matches indentation with provided size -/// +/// /// If indentation was identified succesfully return the std::cmp::Ordering /// depending on whether the amount of spaces detected was smaller, equal or greater. /// Otherwise detailed information is returned about where this happened. @@ -138,7 +138,7 @@ mod test { #[test] fn indent_test() { - let expr = vec![Token {word: format!("\n "), pos: (0, 0)}]; + let expr = vec![Token {word: format!("\n "), pos: (0, 0), start: 0 }]; let mut meta = DefaultMetadata::new(expr, Some(format!("path/to/file")), None); let res = indent(&mut meta); assert!(res.is_ok()); @@ -147,9 +147,9 @@ mod test { #[test] fn indent_with_test() { - let expr = vec![Token { word: format!("\n "), pos: (0, 0) }]; + let expr = vec![Token { word: format!("\n "), pos: (0, 0), start: 0 }]; let mut meta = DefaultMetadata::new(expr, Some(format!("path/to/file")), None); let res = indent_with(&mut meta, 4); assert!(res.is_ok()); } -} \ No newline at end of file +} diff --git a/src/compiling/parser/syntax_module.rs b/src/compiling/parser/syntax_module.rs index 046fbde..cc3b60a 100644 --- a/src/compiling/parser/syntax_module.rs +++ b/src/compiling/parser/syntax_module.rs @@ -29,7 +29,7 @@ macro_rules! syntax_name { pub type SyntaxResult = Result<(), Failure>; /// Trait for parsing -/// +/// /// Trait that should be implemented in order to parse tokens with heraklit /// ``` /// # use heraclitus_compiler::prelude::*; @@ -37,17 +37,17 @@ pub type SyntaxResult = Result<(), Failure>; /// name: String /// // ... (you decide what you need to store) /// } -/// +/// /// impl SyntaxModule for MySyntax { /// syntax_name!("MySyntax"); -/// +/// /// fn new() -> MySyntax { /// MySyntax { /// name: format!(""), /// // Default initialization /// } /// } -/// +/// /// // Here you can parse the actual code /// fn parse(&mut self, meta: &mut DefaultMetadata) -> SyntaxResult { /// token(meta, "var")?; @@ -62,8 +62,8 @@ pub trait SyntaxModule { /// Name of this module fn name() -> &'static str; /// Parse and create AST - /// - /// This method is fundamental in creating a functional AST node that can determine + /// + /// This method is fundamental in creating a functional AST node that can determine /// if tokens provided by metadata can be consumed to create this particular AST node. fn parse(&mut self, meta: &mut M) -> SyntaxResult; /// Do not implement this function as this is a predefined function for debugging @@ -116,13 +116,15 @@ mod test { let dataset1 = vec![ Token { word: format!("let"), - pos: (0, 0) + pos: (0, 0), + start: 0 } ]; let dataset2 = vec![ Token { word: format!("tell"), - pos: (0, 0) + pos: (0, 0), + start: 0 } ]; let path = Some(format!("path/to/file")); @@ -153,15 +155,15 @@ mod test { let mut exp = Preset {}; let dataset = vec![ // Variable - Token { word: format!("_text"), pos: (0, 0) }, + Token { word: format!("_text"), pos: (0, 0), start: 0 }, // Numeric - Token { word: format!("12321"), pos: (0, 0) }, + Token { word: format!("12321"), pos: (0, 0), start: 0 }, // Number - Token { word: format!("-123.12"), pos: (0, 0) }, + Token { word: format!("-123.12"), pos: (0, 0), start: 0 }, // Integer - Token { word: format!("-12"), pos: (0, 0) }, + Token { word: format!("-12"), pos: (0, 0), start: 0 }, // Float - Token { word: format!("-.681"), pos: (0, 0)} + Token { word: format!("-.681"), pos: (0, 0), start: 0 } ]; let path = Some(format!("path/to/file")); let result = exp.parse(&mut DefaultMetadata::new(dataset, path, None)); @@ -180,7 +182,7 @@ mod test { if let Ok(_) = token(meta, "apple") {} else if let Ok(_) = token(meta, "orange") {} else if let Ok(_) = token(meta, "banana") {} - else { + else { if let Err(details) = token(meta, "banana") { return Err(details); } @@ -209,42 +211,42 @@ mod test { let mut exp = PatternModule {}; // Everything should pass let dataset1 = vec![ - Token { word: format!("orange"), pos: (0, 0) }, - Token { word: format!("optional"), pos: (0, 0) }, - Token { word: format!("let"), pos: (0, 0) }, - Token { word: format!("this"), pos: (0, 0) }, - Token { word: format!(","), pos: (0, 0) }, - Token { word: format!("this"), pos: (0, 0) }, - Token { word: format!("end"), pos: (0, 0) } + Token { word: format!("orange"), pos: (0, 0), start: 0 }, + Token { word: format!("optional"), pos: (0, 0), start: 0 }, + Token { word: format!("let"), pos: (0, 0), start: 0 }, + Token { word: format!("this"), pos: (0, 0), start: 0 }, + Token { word: format!(","), pos: (0, 0), start: 0 }, + Token { word: format!("this"), pos: (0, 0), start: 0 }, + Token { word: format!("end"), pos: (0, 0), start: 0 } ]; // Token should fail let dataset2 = vec![ - Token { word: format!("kiwi"), pos: (0, 0) }, - Token { word: format!("optional"), pos: (0, 0) }, - Token { word: format!("let"), pos: (0, 0) }, - Token { word: format!("this"), pos: (0, 0) }, - Token { word: format!(","), pos: (0, 0) }, - Token { word: format!("this"), pos: (0, 0) }, - Token { word: format!("end"), pos: (0, 0) } + Token { word: format!("kiwi"), pos: (0, 0), start: 0 }, + Token { word: format!("optional"), pos: (0, 0), start: 0 }, + Token { word: format!("let"), pos: (0, 0), start: 0 }, + Token { word: format!("this"), pos: (0, 0), start: 0 }, + Token { word: format!(","), pos: (0, 0), start: 0 }, + Token { word: format!("this"), pos: (0, 0), start: 0 }, + Token { word: format!("end"), pos: (0, 0), start: 0 } ]; // Syntax should fail let dataset3 = vec![ - Token { word: format!("orange"), pos: (0, 0) }, - Token { word: format!("tell"), pos: (0, 0) }, - Token { word: format!("this"), pos: (0, 0) }, - Token { word: format!(","), pos: (0, 0) }, - Token { word: format!("this"), pos: (0, 0) }, - Token { word: format!("end"), pos: (0, 0) } + Token { word: format!("orange"), pos: (0, 0), start: 0 }, + Token { word: format!("tell"), pos: (0, 0), start: 0 }, + Token { word: format!("this"), pos: (0, 0), start: 0 }, + Token { word: format!(","), pos: (0, 0), start: 0 }, + Token { word: format!("this"), pos: (0, 0), start: 0 }, + Token { word: format!("end"), pos: (0, 0), start: 0 } ]; // Token should fail because of repeat matching (this , this) , let dataset4 = vec![ - Token { word: format!("orange"), pos: (0, 0) }, - Token { word: format!("tell"), pos: (0, 0) }, - Token { word: format!("this"), pos: (0, 0) }, - Token { word: format!(","), pos: (0, 0) }, - Token { word: format!("this"), pos: (0, 0) }, - Token { word: format!("this"), pos: (0, 0) }, - Token { word: format!("end"), pos: (0, 0) } + Token { word: format!("orange"), pos: (0, 0), start: 0 }, + Token { word: format!("tell"), pos: (0, 0), start: 0 }, + Token { word: format!("this"), pos: (0, 0), start: 0 }, + Token { word: format!(","), pos: (0, 0), start: 0 }, + Token { word: format!("this"), pos: (0, 0), start: 0 }, + Token { word: format!("this"), pos: (0, 0), start: 0 }, + Token { word: format!("end"), pos: (0, 0), start: 0 } ]; let path = Some(format!("path/to/file")); let result1 = exp.parse(&mut DefaultMetadata::new(dataset1, path.clone(), None)); diff --git a/src/compiling/token.rs b/src/compiling/token.rs index 9fb7c92..df74162 100644 --- a/src/compiling/token.rs +++ b/src/compiling/token.rs @@ -7,6 +7,8 @@ pub struct Token { pub word: String, /// Position of the token (row, column) pub pos: (usize, usize), + /// Index of the character in the file that the token starts + pub start: usize, } impl Token { @@ -46,10 +48,11 @@ mod test { fn display_token() { let mut token = super::Token { word: String::from("keyword"), - pos: (1, 2) + pos: (1, 2), + start: 0 }; assert_eq!(format!("{}", token), String::from("Tok[keyword 1:2]")); token.word = String::from("["); assert_eq!(format!("{}", token), String::from("Tok[ 1:2]")); } -} \ No newline at end of file +} diff --git a/tests/cobra_modules/mod.rs b/tests/cobra_modules/mod.rs index ee215bf..eb39d33 100644 --- a/tests/cobra_modules/mod.rs +++ b/tests/cobra_modules/mod.rs @@ -4,4 +4,4 @@ mod block; mod if_statement; pub use expr::*; pub use block::*; -pub use if_statement::*; \ No newline at end of file +pub use if_statement::*;