| //! Parsing of Makefile syntax as found in `.d` files emitted by C compilers. |
| |
| use crate::scanner::{ParseResult, Scanner}; |
| |
| /// Dependency information for a single target. |
| #[derive(Debug)] |
| pub struct Deps<'a> { |
| /// Output name, as found in the `.d` input. |
| pub target: &'a str, |
| /// Input names, as found in the `.d` input. |
| pub deps: Vec<&'a str>, |
| } |
| |
| /// Skip spaces and backslashed newlines. |
| fn skip_spaces(scanner: &mut Scanner) -> ParseResult<()> { |
| loop { |
| match scanner.read() { |
| ' ' => {} |
| '\\' => match scanner.read() { |
| '\r' => scanner.expect('\n')?, |
| '\n' => {} |
| _ => return scanner.parse_error("invalid backslash escape"), |
| }, |
| _ => { |
| scanner.back(); |
| break; |
| } |
| } |
| } |
| Ok(()) |
| } |
| |
| /// Read one path from the input scanner. |
| /// Note: treats colon as a valid character in a path because of Windows-style |
| /// paths, but this means that the inital `output: ...` path will include the |
| /// trailing colon. |
| fn read_path<'a>(scanner: &mut Scanner<'a>) -> ParseResult<Option<&'a str>> { |
| skip_spaces(scanner)?; |
| let start = scanner.ofs; |
| loop { |
| match scanner.read() { |
| '\0' | ' ' | '\r' | '\n' => { |
| scanner.back(); |
| break; |
| } |
| '\\' => { |
| let peek = scanner.peek(); |
| if peek == '\n' || peek == '\r' { |
| scanner.back(); |
| break; |
| } |
| } |
| _ => {} |
| } |
| } |
| let end = scanner.ofs; |
| if end == start { |
| return Ok(None); |
| } |
| Ok(Some(scanner.slice(start, end))) |
| } |
| |
| /// Parse a `.d` file into `Deps`. |
| pub fn parse<'a>(scanner: &mut Scanner<'a>) -> ParseResult<Deps<'a>> { |
| let target = match read_path(scanner)? { |
| None => return scanner.parse_error("expected file"), |
| Some(o) => o, |
| }; |
| scanner.skip_spaces(); |
| let target = match target.strip_suffix(':') { |
| None => { |
| scanner.expect(':')?; |
| target |
| } |
| Some(target) => target, |
| }; |
| let mut deps = Vec::new(); |
| while let Some(p) = read_path(scanner)? { |
| deps.push(p); |
| } |
| scanner.skip('\r'); |
| scanner.skip('\n'); |
| scanner.skip_spaces(); |
| scanner.expect('\0')?; |
| |
| Ok(Deps { target, deps }) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use std::path::Path; |
| |
| fn try_parse(buf: &mut Vec<u8>) -> Result<Deps, String> { |
| buf.push(0); |
| let mut scanner = Scanner::new(buf); |
| parse(&mut scanner).map_err(|err| scanner.format_parse_error(Path::new("test"), err)) |
| } |
| |
| fn must_parse(buf: &mut Vec<u8>) -> Deps { |
| match try_parse(buf) { |
| Err(err) => { |
| println!("{}", err); |
| panic!("failed parse"); |
| } |
| Ok(d) => d, |
| } |
| } |
| |
| fn test_for_crlf(input: &str, test: fn(String)) { |
| let crlf = input.replace('\n', "\r\n"); |
| for test_case in [String::from(input), crlf] { |
| test(test_case); |
| } |
| } |
| |
| #[test] |
| fn test_parse() { |
| test_for_crlf( |
| "build/browse.o: src/browse.cc src/browse.h build/browse_py.h\n", |
| |text| { |
| let mut file = text.into_bytes(); |
| let deps = must_parse(&mut file); |
| assert_eq!(deps.target, "build/browse.o"); |
| assert_eq!(deps.deps.len(), 3); |
| }, |
| ); |
| } |
| |
| #[test] |
| fn test_parse_space_suffix() { |
| test_for_crlf("build/browse.o: src/browse.cc \n", |text| { |
| let mut file = text.into_bytes(); |
| let deps = must_parse(&mut file); |
| assert_eq!(deps.target, "build/browse.o"); |
| assert_eq!(deps.deps.len(), 1); |
| }); |
| } |
| |
| #[test] |
| fn test_parse_multiline() { |
| test_for_crlf( |
| "build/browse.o: src/browse.cc\\\n build/browse_py.h", |
| |text| { |
| let mut file = text.into_bytes(); |
| let deps = must_parse(&mut file); |
| assert_eq!(deps.target, "build/browse.o"); |
| assert_eq!(deps.deps.len(), 2); |
| }, |
| ); |
| } |
| |
| #[test] |
| fn test_parse_without_final_newline() { |
| let mut file = b"build/browse.o: src/browse.cc".to_vec(); |
| let deps = must_parse(&mut file); |
| assert_eq!(deps.target, "build/browse.o"); |
| assert_eq!(deps.deps.len(), 1); |
| } |
| |
| #[test] |
| fn test_parse_spaces_before_colon() { |
| let mut file = b"build/browse.o : src/browse.cc".to_vec(); |
| let deps = must_parse(&mut file); |
| assert_eq!(deps.target, "build/browse.o"); |
| assert_eq!(deps.deps.len(), 1); |
| } |
| |
| #[test] |
| fn test_parse_windows_dep_path() { |
| let mut file = b"odd/path.o: C:/odd\\path.c".to_vec(); |
| let deps = must_parse(&mut file); |
| assert_eq!(deps.target, "odd/path.o"); |
| assert_eq!(deps.deps[0], "C:/odd\\path.c"); |
| assert_eq!(deps.deps.len(), 1); |
| } |
| |
| #[test] |
| fn test_parse_missing_colon() { |
| let mut file = b"foo bar".to_vec(); |
| let err = try_parse(&mut file).unwrap_err(); |
| assert!( |
| err.starts_with("parse error: expected ':'"), |
| "expected parse error, got {:?}", |
| err |
| ); |
| } |
| } |