diff --git a/src/parsing/checks/parser.rs b/src/parsing/checks/parser.rs index 2731693..a3f832f 100644 --- a/src/parsing/checks/parser.rs +++ b/src/parsing/checks/parser.rs @@ -2495,6 +2495,32 @@ fn parse_collecting_errors_basic() { // If this compiles, the method signature is correct } +#[test] +fn multiline_fence_interior_is_opaque() { + let mut input = Parser::new(); + + // The `http://` in the fenced shell once looked like a procedure + // declaration boundary and truncated the block before its closing `}`. + let source = r#" +% technique v1 + +check_proxy : + +1. Make a request. { exec(```bash +https_proxy=http://10.0.0.1:8888/ curl -f https://www.example.com/ + ```) } +"# + .trim_ascii(); + + input.initialize(source); + let result = input.parse_collecting_errors(); + assert!( + result.is_ok(), + "fenced shell content must not be read as structure: {:?}", + result.err() + ); +} + #[test] fn test_multiple_error_collection() { use std::path::Path; diff --git a/src/parsing/parser.rs b/src/parsing/parser.rs index ccebb7c..5a2e5a2 100644 --- a/src/parsing/parser.rs +++ b/src/parsing/parser.rs @@ -405,31 +405,7 @@ impl<'i> Parser<'i> { P1: Fn(&str) -> bool, P2: Fn(&str) -> bool, { - let mut i = 0; - let mut begun = false; - - for line in self - .source - .lines() - { - if !begun && start_predicate(line) { - begun = true; - i += line.len() + 1; - continue; - } else if begun && end_predicate(line) { - // don't include this line - break; - } - - i += line.len() + 1; - } - - if i > self - .source - .len() - { - i -= 1; - } + let i = locate_block_lines(self.source, start_predicate, end_predicate); // Extract the substring from start to the found position let block = &self.source[..i]; @@ -1064,31 +1040,11 @@ impl<'i> Parser<'i> { /// Parse a procedure with error recovery - collects multiple errors instead of stopping at the first one fn read_procedure(&mut self) -> Result, ParsingError> { // Find the procedure block boundaries - let mut i = 0; - let mut begun = false; - - for line in self - .source - .lines() - { - if !begun && is_procedure_declaration(line) { - begun = true; - i += line.len() + 1; - continue; - } else if begun && is_procedure_declaration(line) { - // don't include this line - break; - } - - i += line.len() + 1; - } - - if i > self - .source - .len() - { - i -= 1; - } + let i = locate_block_lines( + self.source, + is_procedure_declaration, + is_procedure_declaration, + ); // Extract the procedure block let block = &self.source[..i]; @@ -1304,31 +1260,7 @@ impl<'i> Parser<'i> { ParsingError, > { // Find declaration block boundaries - let mut i = 0; - let mut begun = false; - - for line in parser - .source - .lines() - { - if !begun && is_procedure_declaration(line) { - begun = true; - i += line.len() + 1; - continue; - } else if begun && is_procedure_body(line) { - // don't include this line - break; - } - - i += line.len() + 1; - } - - if i > parser - .source - .len() - { - i -= 1; - } + let i = locate_block_lines(parser.source, is_procedure_declaration, is_procedure_body); // Extract declaration block let block = &parser.source[..i]; @@ -3027,6 +2959,49 @@ fn is_genus(content: &str) -> bool { } } +/// Deal with corner cases (like fenced multi-line strings) which otherwise +/// would break the block detection logic. +fn locate_block_lines(source: &str, start: P1, end: P2) -> usize +where + P1: Fn(&str) -> bool, + P2: Fn(&str) -> bool, +{ + let mut i = 0; + let mut begun = false; + let mut in_fence = false; + + for line in source.lines() { + let opaque = in_fence; + if line + .matches("```") + .count() + % 2 + == 1 + { + in_fence = !in_fence; + } + if opaque { + i += line.len() + 1; + continue; + } + + if !begun && start(line) { + begun = true; + } else if begun && end(line) { + // don't include this line + break; + } + + i += line.len() + 1; + } + + if i > source.len() { + i -= 1; + } + + i +} + /// Correct declarations are of the form /// /// ```text