makefile-lossless-0.1.7/.cargo_vcs_info.json0000644000000001360000000000100144630ustar { "git": { "sha1": "bc130287569569b9882caa4eaecd853cd6b6b8ef" }, "path_in_vcs": "" }makefile-lossless-0.1.7/.github/CODEOWNERS000064400000000000000000000000121046102023000161770ustar 00000000000000* @jelmer makefile-lossless-0.1.7/.github/FUNDING.yml000064400000000000000000000000171046102023000164260ustar 00000000000000github: jelmer makefile-lossless-0.1.7/.github/dependabot.yml000064400000000000000000000006251046102023000174460ustar 00000000000000# Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" rebase-strategy: "disabled" - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly makefile-lossless-0.1.7/.github/workflows/rust.yml000064400000000000000000000004121046102023000203650ustar 00000000000000name: Rust on: push: pull_request: env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build run: cargo build --verbose - name: Run tests run: cargo test --verbose makefile-lossless-0.1.7/.gitignore000064400000000000000000000000261046102023000152410ustar 00000000000000Cargo.lock target/ *~ makefile-lossless-0.1.7/Cargo.toml0000644000000021470000000000100124650ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "makefile-lossless" version = "0.1.7" authors = ["Jelmer Vernooij "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Lossless Parser for Makefiles" homepage = "https://github.com/jelmer/makefile-lossless" documentation = "https://docs.rs/makefile-lossless" readme = "README.md" license = "Apache-2.0" repository = "https://github.com/jelmer/makefile-lossless" [lib] name = "makefile_lossless" path = "src/lib.rs" [dependencies.log] version = "0.4" [dependencies.rowan] version = "^0.16" [dev-dependencies.maplit] version = "1.0.2" makefile-lossless-0.1.7/Cargo.toml.orig000064400000000000000000000007161046102023000161460ustar 00000000000000[package] name = "makefile-lossless" repository = "https://github.com/jelmer/makefile-lossless" description = "Lossless Parser for Makefiles" version = "0.1.7" edition = "2021" license = "Apache-2.0" readme = "README.md" authors = [ "Jelmer Vernooij ",] documentation = "https://docs.rs/makefile-lossless" homepage = "https://github.com/jelmer/makefile-lossless" [dependencies] log = "0.4" rowan = "^0.16" [dev-dependencies] maplit = "1.0.2" makefile-lossless-0.1.7/README.md000064400000000000000000000004741046102023000145370ustar 00000000000000Lossless parser for Makefiles ============================= This crate provides a lossless parser for makefiles, creating a modifiable CST. Example: ```rust let mf = Makefile::read("Makefile").unwrap(); println!("Rules in the makefile: {:?}", mf.rules().map(|r| r.targets().join(" ")).collect::>()); ``` makefile-lossless-0.1.7/TODO000064400000000000000000000001671046102023000137470ustar 00000000000000- Handle split lines (https://www.gnu.org/software/make/manual/make.html#Splitting-Lines) - Support variables in rules makefile-lossless-0.1.7/disperse.toml000064400000000000000000000001011046102023000157560ustar 00000000000000tag-name = "v$VERSION" tarball-location = [] release-timeout = 5 makefile-lossless-0.1.7/src/lex.rs000064400000000000000000000253611046102023000152070ustar 00000000000000use crate::SyntaxKind; use std::iter::Peekable; use std::str::Chars; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum LineType { Recipe, Other, } pub struct Lexer<'a> { input: Peekable>, line_type: Option, } impl<'a> Lexer<'a> { pub fn new(input: &'a str) -> Self { Lexer { input: input.chars().peekable(), line_type: None, } } fn is_whitespace(c: char) -> bool { c == ' ' || c == '\t' } fn is_newline(c: char) -> bool { c == '\n' || c == '\r' } fn is_valid_identifier_char(c: char) -> bool { c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-' } fn read_while(&mut self, predicate: F) -> String where F: Fn(char) -> bool, { let mut result = String::new(); while let Some(&c) = self.input.peek() { if predicate(c) { result.push(c); self.input.next(); } else { break; } } result } fn next_token(&mut self) -> Option<(SyntaxKind, String)> { if let Some(&c) = self.input.peek() { match (c, self.line_type) { ('\t', None) => { self.input.next(); self.line_type = Some(LineType::Recipe); return Some((SyntaxKind::INDENT, "\t".to_string())); } (_, None) => { self.line_type = Some(LineType::Other); } (_, _) => {} } match c { c if Self::is_newline(c) => { self.line_type = None; return Some((SyntaxKind::NEWLINE, self.input.next()?.to_string())); } '#' => { return Some(( SyntaxKind::COMMENT, self.read_while(|c| !Self::is_newline(c)), )); } _ => {} } match self.line_type.unwrap() { LineType::Recipe => { Some((SyntaxKind::TEXT, self.read_while(|c| !Self::is_newline(c)))) } LineType::Other => match c { c if Self::is_whitespace(c) => { Some((SyntaxKind::WHITESPACE, self.read_while(Self::is_whitespace))) } c if Self::is_valid_identifier_char(c) => Some(( SyntaxKind::IDENTIFIER, self.read_while(Self::is_valid_identifier_char), )), ':' | '=' | '?' | '+' => { let text = self.input.next().unwrap().to_string() + self .read_while(|c| c == ':' || c == '=' || c == '?') .as_str(); Some((SyntaxKind::OPERATOR, text)) } '(' => { self.input.next(); Some((SyntaxKind::LPAREN, "(".to_string())) } ')' => { self.input.next(); Some((SyntaxKind::RPAREN, ")".to_string())) } '$' => { self.input.next(); Some((SyntaxKind::DOLLAR, "$".to_string())) } ',' => { self.input.next(); Some((SyntaxKind::COMMA, ",".to_string())) } '\\' => { self.input.next(); Some((SyntaxKind::BACKSLASH, "\\".to_string())) } '"' => { self.input.next(); Some((SyntaxKind::QUOTE, "\"".to_string())) } _ => { self.input.next(); Some((SyntaxKind::ERROR, c.to_string())) } }, } } else { None } } } impl Iterator for Lexer<'_> { type Item = (crate::SyntaxKind, String); fn next(&mut self) -> Option { self.next_token() } } pub(crate) fn lex(input: &str) -> Vec<(SyntaxKind, String)> { let mut lexer = Lexer::new(input); lexer.by_ref().collect::>() } #[cfg(test)] mod tests { use super::*; use crate::SyntaxKind::*; #[test] fn test_empty() { assert_eq!(lex(""), vec![]); } #[test] fn test_simple() { assert_eq!( lex(r#"VARIABLE = value rule: prerequisite recipe "#) .iter() .map(|(kind, text)| (*kind, text.as_str())) .collect::>(), vec![ (IDENTIFIER, "VARIABLE"), (WHITESPACE, " "), (OPERATOR, "="), (WHITESPACE, " "), (IDENTIFIER, "value"), (NEWLINE, "\n"), (NEWLINE, "\n"), (IDENTIFIER, "rule"), (OPERATOR, ":"), (WHITESPACE, " "), (IDENTIFIER, "prerequisite"), (NEWLINE, "\n"), (INDENT, "\t"), (TEXT, "recipe"), (NEWLINE, "\n"), ] ); } #[test] fn test_bare_export() { assert_eq!( lex(r#"export "#) .iter() .map(|(kind, text)| (*kind, text.as_str())) .collect::>(), vec![(IDENTIFIER, "export"), (NEWLINE, "\n"),] ); } #[test] fn test_export() { assert_eq!( lex(r#"export VARIABLE "#) .iter() .map(|(kind, text)| (*kind, text.as_str())) .collect::>(), vec![ (IDENTIFIER, "export"), (WHITESPACE, " "), (IDENTIFIER, "VARIABLE"), (NEWLINE, "\n"), ] ); } #[test] fn test_export_assignment() { assert_eq!( lex(r#"export VARIABLE := value "#) .iter() .map(|(kind, text)| (*kind, text.as_str())) .collect::>(), vec![ (IDENTIFIER, "export"), (WHITESPACE, " "), (IDENTIFIER, "VARIABLE"), (WHITESPACE, " "), (OPERATOR, ":="), (WHITESPACE, " "), (IDENTIFIER, "value"), (NEWLINE, "\n"), ] ); } #[test] fn test_multiple_prerequisites() { assert_eq!( lex(r#"rule: prerequisite1 prerequisite2 recipe "#) .iter() .map(|(kind, text)| (*kind, text.as_str())) .collect::>(), vec![ (IDENTIFIER, "rule"), (OPERATOR, ":"), (WHITESPACE, " "), (IDENTIFIER, "prerequisite1"), (WHITESPACE, " "), (IDENTIFIER, "prerequisite2"), (NEWLINE, "\n"), (INDENT, "\t"), (TEXT, "recipe"), (NEWLINE, "\n"), (NEWLINE, "\n"), ] ); } #[test] fn test_variable_question() { assert_eq!( lex("VARIABLE ?= value\n") .iter() .map(|(kind, text)| (*kind, text.as_str())) .collect::>(), vec![ (IDENTIFIER, "VARIABLE"), (WHITESPACE, " "), (OPERATOR, "?="), (WHITESPACE, " "), (IDENTIFIER, "value"), (NEWLINE, "\n"), ] ); } #[test] fn test_conditional() { assert_eq!( lex(r#"ifneq (a, b) endif "#) .iter() .map(|(kind, text)| (*kind, text.as_str())) .collect::>(), vec![ (IDENTIFIER, "ifneq"), (WHITESPACE, " "), (LPAREN, "("), (IDENTIFIER, "a"), (COMMA, ","), (WHITESPACE, " "), (IDENTIFIER, "b"), (RPAREN, ")"), (NEWLINE, "\n"), (IDENTIFIER, "endif"), (NEWLINE, "\n"), ] ); } #[test] fn test_variable_paren() { assert_eq!( lex("VARIABLE = $(value)\n") .iter() .map(|(kind, text)| (*kind, text.as_str())) .collect::>(), vec![ (IDENTIFIER, "VARIABLE"), (WHITESPACE, " "), (OPERATOR, "="), (WHITESPACE, " "), (DOLLAR, "$"), (LPAREN, "("), (IDENTIFIER, "value"), (RPAREN, ")"), (NEWLINE, "\n"), ] ); } #[test] fn test_variable_paren2() { assert_eq!( lex("VARIABLE = $(value)$(value2)\n") .iter() .map(|(kind, text)| (*kind, text.as_str())) .collect::>(), vec![ (IDENTIFIER, "VARIABLE"), (WHITESPACE, " "), (OPERATOR, "="), (WHITESPACE, " "), (DOLLAR, "$"), (LPAREN, "("), (IDENTIFIER, "value"), (RPAREN, ")"), (DOLLAR, "$"), (LPAREN, "("), (IDENTIFIER, "value2"), (RPAREN, ")"), (NEWLINE, "\n"), ] ); } #[test] fn test_oom() { let text = r#" #!/usr/bin/make -f # # debhelper-7 [debian/rules] for cups-pdf # # COPYRIGHT © 2003-2021 Martin-Éric Racine # # LICENSE # GPLv2+: GNU GPL version 2 or later # export CC := $(shell dpkg-architecture --query DEB_HOST_GNU_TYPE)-gcc export CPPFLAGS := $(shell dpkg-buildflags --get CPPFLAGS) export CFLAGS := $(shell dpkg-buildflags --get CFLAGS) export LDFLAGS := $(shell dpkg-buildflags --get LDFLAGS) #export DEB_BUILD_MAINT_OPTIONS = hardening=+all,-bindnow,-pie # Append flags for Long File Support (LFS) # LFS_CPPFLAGS does not exist export DEB_CFLAGS_MAINT_APPEND +=$(shell getconf LFS_CFLAGS) $(HARDENING_CFLAGS) export DEB_LDFLAGS_MAINT_APPEND +=$(shell getconf LFS_LDFLAGS) $(HARDENING_LDFLAGS) override_dh_auto_build-arch: $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -o src/cups-pdf src/cups-pdf.c -lcups override_dh_auto_clean: rm -f src/cups-pdf src/*.o %: dh $@ #EOF "#; let _lexed = lex(text); } } makefile-lossless-0.1.7/src/lib.rs000064400000000000000000000022411046102023000151550ustar 00000000000000#![allow(clippy::tabs_in_doc_comments)] // Makefile uses tabs #![deny(missing_docs)] //! A lossless parser for Makefiles //! //! Example: //! //! ```rust //! use std::io::Read; //! let contents = r#"PYTHON = python3 //! //! .PHONY: all //! //! all: build //! //! build: //! $(PYTHON) setup.py build //! "#; //! let makefile: makefile_lossless::Makefile = contents.parse().unwrap(); //! //! assert_eq!(makefile.rules().count(), 3); //! ``` mod lex; mod parse; pub use parse::{Identifier, Makefile, Rule, VariableDefinition, ParseError, Error}; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[allow(non_camel_case_types)] #[repr(u16)] #[allow(missing_docs)] pub enum SyntaxKind { IDENTIFIER = 0, INDENT, TEXT, WHITESPACE, NEWLINE, DOLLAR, LPAREN, RPAREN, QUOTE, BACKSLASH, COMMA, OPERATOR, COMMENT, ERROR, // composite nodes ROOT, // The entire file RULE, // A single rule RECIPE, VARIABLE, EXPR, } /// Convert our `SyntaxKind` into the rowan `SyntaxKind`. impl From for rowan::SyntaxKind { fn from(kind: SyntaxKind) -> Self { Self(kind as u16) } } makefile-lossless-0.1.7/src/parse.rs000064400000000000000000000604021046102023000155240ustar 00000000000000use crate::lex::lex; use crate::SyntaxKind; use crate::SyntaxKind::*; use rowan::ast::AstNode; use std::str::FromStr; #[derive(Debug)] /// An error that can occur when parsing a makefile pub enum Error { /// An I/O error occurred Io(std::io::Error), /// A parse error occurred Parse(ParseError), } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match &self { Error::Io(e) => write!(f, "IO error: {}", e), Error::Parse(e) => write!(f, "Parse error: {}", e), } } } impl From for Error { fn from(e: std::io::Error) -> Self { Error::Io(e) } } impl std::error::Error for Error {} #[derive(Debug, Clone, PartialEq, Eq, Hash)] /// An error that occurred while parsing a makefile pub struct ParseError(Vec); impl std::fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { for err in &self.0 { writeln!(f, "{}", err)?; } Ok(()) } } impl std::error::Error for ParseError {} impl From for Error { fn from(e: ParseError) -> Self { Error::Parse(e) } } /// Second, implementing the `Language` trait teaches rowan to convert between /// these two SyntaxKind types, allowing for a nicer SyntaxNode API where /// "kinds" are values from our `enum SyntaxKind`, instead of plain u16 values. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Lang {} impl rowan::Language for Lang { type Kind = SyntaxKind; fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind { unsafe { std::mem::transmute::(raw.0) } } fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind { kind.into() } } /// GreenNode is an immutable tree, which is cheap to change, /// but doesn't contain offsets and parent pointers. use rowan::GreenNode; /// You can construct GreenNodes by hand, but a builder /// is helpful for top-down parsers: it maintains a stack /// of currently in-progress nodes use rowan::GreenNodeBuilder; /// The parse results are stored as a "green tree". /// We'll discuss working with the results later #[derive(Debug)] struct Parse { green_node: GreenNode, #[allow(unused)] errors: Vec, } fn parse(text: &str) -> Parse { struct Parser { /// input tokens, including whitespace, /// in *reverse* order. tokens: Vec<(SyntaxKind, String)>, /// the in-progress tree. builder: GreenNodeBuilder<'static>, /// the list of syntax errors we've accumulated /// so far. errors: Vec, } impl Parser { fn error(&mut self, msg: String) { self.builder.start_node(ERROR.into()); if self.current().is_some() { self.bump(); } self.errors.push(msg); self.builder.finish_node(); } fn parse_expr(&mut self) { self.builder.start_node(EXPR.into()); loop { match self.current() { Some(NEWLINE) => { break; } Some(_t) => { self.bump(); } None => { break; } } } self.builder.finish_node(); } fn parse_recipe_line(&mut self) { self.builder.start_node(RECIPE.into()); self.expect(INDENT); self.expect(TEXT); self.expect_eol(); self.builder.finish_node(); } fn parse_rule(&mut self) { self.builder.start_node(RULE.into()); self.skip_ws(); self.expect(IDENTIFIER); self.skip_ws(); if self.tokens.pop() == Some((OPERATOR, ":".to_string())) { self.builder.token(OPERATOR.into(), ":"); } else { self.error("expected ':'".into()); } self.skip_ws(); self.parse_expr(); self.expect_eol(); loop { match self.current() { Some(INDENT) => { self.parse_recipe_line(); } Some(NEWLINE) => { self.bump(); break; } _ => { break; } } } self.builder.finish_node(); } fn parse_assignment(&mut self) { self.builder.start_node(VARIABLE.into()); self.skip_ws(); if self.tokens.last() == Some(&(IDENTIFIER, "export".to_string())) { self.expect(IDENTIFIER); self.skip_ws(); } self.expect(IDENTIFIER); self.skip_ws(); self.expect(OPERATOR); self.skip_ws(); self.parse_expr(); self.expect_eol(); self.builder.finish_node(); } fn parse(mut self) -> Parse { self.builder.start_node(ROOT.into()); loop { match self.find(|&&(k, _)| k == OPERATOR || k == NEWLINE || k == LPAREN) { Some((OPERATOR, ":")) => { self.parse_rule(); } Some((OPERATOR, "?=")) | Some((OPERATOR, "=")) | Some((OPERATOR, ":=")) | Some((OPERATOR, "::=")) | Some((OPERATOR, ":::=")) | Some((OPERATOR, "+=")) | Some((OPERATOR, "!=")) => { self.parse_assignment(); } Some((NEWLINE, _)) => { self.bump(); } Some(_) | None => { self.error(format!("unexpected token {:?}", self.current())); if self.current().is_some() { self.bump(); } } } if self.current().is_none() { break; } } // Close the root node. self.builder.finish_node(); // Turn the builder into a GreenNode Parse { green_node: self.builder.finish(), errors: self.errors, } } /// Advance one token, adding it to the current branch of the tree builder. fn bump(&mut self) { let (kind, text) = self.tokens.pop().unwrap(); self.builder.token(kind.into(), text.as_str()); } /// Peek at the first unprocessed token fn current(&self) -> Option { self.tokens.last().map(|(kind, _)| *kind) } fn find( &self, finder: impl FnMut(&&(SyntaxKind, String)) -> bool, ) -> Option<(SyntaxKind, &str)> { self.tokens .iter() .rev() .find(finder) .map(|(kind, text)| (*kind, text.as_str())) } fn expect_eol(&mut self) { match self.current() { Some(NEWLINE) => { self.bump(); } None => {} n => { self.error(format!("expected newline, got {:?}", n)); } } } fn expect(&mut self, expected: SyntaxKind) { if self.current() != Some(expected) { self.error(format!("expected {:?}, got {:?}", expected, self.current())); } else { self.bump(); } } fn skip_ws(&mut self) { while self.current() == Some(WHITESPACE) { self.bump() } } } let mut tokens = lex(text); tokens.reverse(); Parser { tokens, builder: GreenNodeBuilder::new(), errors: Vec::new(), } .parse() } /// To work with the parse results we need a view into the /// green tree - the Syntax tree. /// It is also immutable, like a GreenNode, /// but it contains parent pointers, offsets, and /// has identity semantics. type SyntaxNode = rowan::SyntaxNode; #[allow(unused)] type SyntaxToken = rowan::SyntaxToken; #[allow(unused)] type SyntaxElement = rowan::NodeOrToken; impl Parse { fn syntax(&self) -> SyntaxNode { SyntaxNode::new_root_mut(self.green_node.clone()) } fn root(&self) -> Makefile { Makefile::cast(self.syntax()).unwrap() } } macro_rules! ast_node { ($ast:ident, $kind:ident) => { #[derive(PartialEq, Eq, Hash)] #[repr(transparent)] /// An AST node for $ast pub struct $ast(SyntaxNode); impl AstNode for $ast { type Language = Lang; fn can_cast(kind: SyntaxKind) -> bool { kind == $kind } fn cast(syntax: SyntaxNode) -> Option { if Self::can_cast(syntax.kind()) { Some(Self(syntax)) } else { None } } fn syntax(&self) -> &SyntaxNode { &self.0 } } impl core::fmt::Display for $ast { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { write!(f, "{}", self.0.text()) } } }; } ast_node!(Makefile, ROOT); ast_node!(Rule, RULE); ast_node!(Identifier, IDENTIFIER); ast_node!(VariableDefinition, VARIABLE); impl VariableDefinition { /// Get the name of the variable definition pub fn name(&self) -> Option { self.syntax().children_with_tokens().find_map(|it| { it.as_token().and_then(|it| { if it.kind() == IDENTIFIER && it.text() != "export" { Some(it.text().to_string()) } else { None } }) }) } /// Get the raw value of the variable definition pub fn raw_value(&self) -> Option { self.syntax() .children() .find(|it| it.kind() == EXPR) .map(|it| it.text().to_string()) } } impl Makefile { /// Create a new empty makefile pub fn new() -> Makefile { let mut builder = GreenNodeBuilder::new(); builder.start_node(ROOT.into()); builder.finish_node(); let syntax = SyntaxNode::new_root_mut(builder.finish()); Makefile(syntax) } /// Read a changelog file from a reader pub fn read(mut r: R) -> Result { let mut buf = String::new(); r.read_to_string(&mut buf)?; Ok(buf.parse()?) } /// Read makefile from a reader, but allow syntax errors pub fn read_relaxed(mut r: R) -> Result { let mut buf = String::new(); r.read_to_string(&mut buf)?; let parsed = parse(&buf); Ok(parsed.root()) } /// Retrieve the rules in the makefile /// /// # Example /// ``` /// use makefile_lossless::Makefile; /// let makefile: Makefile = "rule: dependency\n\tcommand\n".parse().unwrap(); /// assert_eq!(makefile.rules().count(), 1); /// ``` pub fn rules(&self) -> impl Iterator { self.syntax().children().filter_map(Rule::cast) } /// Get all rules that have a specific target pub fn rules_by_target<'a>(&'a self, target: &'a str) -> impl Iterator + 'a { self.rules() .filter(move |rule| rule.targets().any(|t| t == target)) } /// Get all variable definitions in the makefile pub fn variable_definitions(&self) -> impl Iterator { self.syntax() .children() .filter_map(VariableDefinition::cast) } /// Add a new rule to the makefile /// /// # Example /// ``` /// use makefile_lossless::Makefile; /// let mut makefile = Makefile::new(); /// makefile.add_rule("rule"); /// assert_eq!(makefile.to_string(), "rule:\n"); /// ``` pub fn add_rule(&mut self, target: &str) -> Rule { let mut builder = GreenNodeBuilder::new(); builder.start_node(RULE.into()); builder.token(IDENTIFIER.into(), target); builder.token(OPERATOR.into(), ":"); builder.token(NEWLINE.into(), "\n"); builder.finish_node(); let syntax = SyntaxNode::new_root_mut(builder.finish()); let pos = self.0.children_with_tokens().count(); self.0.splice_children(pos..pos, vec![syntax.into()]); Rule(self.0.children().nth(pos).unwrap()) } /// Read the makefile pub fn from_reader(mut r: R) -> Result { let mut buf = String::new(); r.read_to_string(&mut buf)?; Ok(buf.parse()?) } } impl FromStr for Rule { type Err = ParseError; fn from_str(s: &str) -> Result { let parsed = parse(s); let rules = parsed.root().rules().collect::>(); if !parsed.errors.is_empty() { Err(ParseError(parsed.errors)) } else if rules.len() == 1 { Ok(rules.into_iter().next().unwrap()) } else { Err(ParseError(vec!["expected a single rule".to_string()])) } } } impl Rule { /// Targets of this rule /// /// # Example /// ``` /// use makefile_lossless::Rule; /// /// let rule: Rule = "rule: dependency\n\tcommand".parse().unwrap(); /// assert_eq!(rule.targets().collect::>(), vec!["rule"]); /// ``` pub fn targets(&self) -> impl Iterator { self.syntax() .children_with_tokens() .take_while(|it| it.as_token().map_or(true, |t| t.kind() != OPERATOR)) .filter_map(|it| it.as_token().map(|t| t.text().to_string())) } /// Get the prerequisites in the rule /// /// # Example /// ``` /// use makefile_lossless::Rule; /// let rule: Rule = "rule: dependency\n\tcommand".parse().unwrap(); /// assert_eq!(rule.prerequisites().collect::>(), vec!["dependency"]); /// ``` pub fn prerequisites(&self) -> impl Iterator { self.syntax() .children() .find(|it| it.kind() == EXPR) .into_iter() .flat_map(|it| { it.children_with_tokens().filter_map(|it| { it.as_token().and_then(|t| { if t.kind() == IDENTIFIER { Some(t.text().to_string()) } else { None } }) }) }) } /// Get the commands in the rule /// /// # Example /// ``` /// use makefile_lossless::Rule; /// let rule: Rule = "rule: dependency\n\tcommand".parse().unwrap(); /// assert_eq!(rule.recipes().collect::>(), vec!["command"]); /// ``` pub fn recipes(&self) -> impl Iterator { self.syntax() .children() .filter(|it| it.kind() == RECIPE) .flat_map(|it| { it.children_with_tokens().filter_map(|it| { it.as_token().and_then(|t| { if t.kind() == TEXT { Some(t.text().to_string()) } else { None } }) }) }) } /// Replace the command at index i with a new line /// /// # Example /// ``` /// use makefile_lossless::Rule; /// let rule: Rule = "rule: dependency\n\tcommand".parse().unwrap(); /// rule.replace_command(0, "new command"); /// assert_eq!(rule.recipes().collect::>(), vec!["new command"]); /// assert_eq!(rule.to_string(), "rule: dependency\n\tnew command\n"); /// ``` pub fn replace_command(&self, i: usize, line: &str) { // Find the RECIPE with index i, then replace the line in it let index = self .syntax() .children() .filter(|it| it.kind() == RECIPE) .nth(i) .expect("index out of bounds") .index(); let mut builder = GreenNodeBuilder::new(); builder.start_node(RECIPE.into()); builder.token(INDENT.into(), "\t"); builder.token(TEXT.into(), line); builder.token(NEWLINE.into(), "\n"); builder.finish_node(); let syntax = SyntaxNode::new_root_mut(builder.finish()); self.0 .splice_children(index..index + 1, vec![syntax.into()]); } /// Add a new command to the rule /// /// # Example /// ``` /// use makefile_lossless::Rule; /// let rule: Rule = "rule: dependency\n\tcommand".parse().unwrap(); /// rule.push_command("command2"); /// assert_eq!(rule.recipes().collect::>(), vec!["command", "command2"]); /// ``` pub fn push_command(&self, line: &str) { // Find the latest RECIPE entry, then append the new line after it. let index = self .0 .children_with_tokens() .filter(|it| it.kind() == RECIPE) .last(); let index = index.map_or_else( || self.0.children_with_tokens().count(), |it| it.index() + 1, ); let mut builder = GreenNodeBuilder::new(); builder.start_node(RECIPE.into()); builder.token(INDENT.into(), "\t"); builder.token(TEXT.into(), line); builder.token(NEWLINE.into(), "\n"); builder.finish_node(); let syntax = SyntaxNode::new_root_mut(builder.finish()); self.0.splice_children(index..index, vec![syntax.into()]); } } impl Default for Makefile { fn default() -> Self { Self::new() } } impl FromStr for Makefile { type Err = ParseError; fn from_str(s: &str) -> Result { let parsed = parse(s); if parsed.errors.is_empty() { Ok(parsed.root()) } else { Err(ParseError(parsed.errors)) } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_simple() { const SIMPLE: &str = r#"VARIABLE = value rule: dependency command "#; let parsed = parse(SIMPLE); assert_eq!(parsed.errors, Vec::::new()); let node = parsed.syntax(); assert_eq!( format!("{:#?}", node), r#"ROOT@0..44 VARIABLE@0..17 IDENTIFIER@0..8 "VARIABLE" WHITESPACE@8..9 " " OPERATOR@9..10 "=" WHITESPACE@10..11 " " EXPR@11..16 IDENTIFIER@11..16 "value" NEWLINE@16..17 "\n" NEWLINE@17..18 "\n" RULE@18..44 IDENTIFIER@18..22 "rule" OPERATOR@22..23 ":" WHITESPACE@23..24 " " EXPR@24..34 IDENTIFIER@24..34 "dependency" NEWLINE@34..35 "\n" RECIPE@35..44 INDENT@35..36 "\t" TEXT@36..43 "command" NEWLINE@43..44 "\n" "# ); let root = parsed.root(); let mut rules = root.rules().collect::>(); assert_eq!(rules.len(), 1); let rule = rules.pop().unwrap(); assert_eq!(rule.targets().collect::>(), vec!["rule"]); assert_eq!(rule.prerequisites().collect::>(), vec!["dependency"]); assert_eq!(rule.recipes().collect::>(), vec!["command"]); let mut variables = root.variable_definitions().collect::>(); assert_eq!(variables.len(), 1); let variable = variables.pop().unwrap(); assert_eq!(variable.name(), Some("VARIABLE".to_string())); assert_eq!(variable.raw_value(), Some("value".to_string())); } #[test] fn test_parse_export_assign() { const EXPORT: &str = r#"export VARIABLE := value "#; let parsed = parse(EXPORT); assert_eq!(parsed.errors, Vec::::new()); let node = parsed.syntax(); assert_eq!( format!("{:#?}", node), r#"ROOT@0..25 VARIABLE@0..25 IDENTIFIER@0..6 "export" WHITESPACE@6..7 " " IDENTIFIER@7..15 "VARIABLE" WHITESPACE@15..16 " " OPERATOR@16..18 ":=" WHITESPACE@18..19 " " EXPR@19..24 IDENTIFIER@19..24 "value" NEWLINE@24..25 "\n" "# ); let root = parsed.root(); let mut variables = root.variable_definitions().collect::>(); assert_eq!(variables.len(), 1); let variable = variables.pop().unwrap(); assert_eq!(variable.name(), Some("VARIABLE".to_string())); assert_eq!(variable.raw_value(), Some("value".to_string())); } #[test] fn test_parse_multiple_prerequisites() { const MULTIPLE_PREREQUISITES: &str = r#"rule: dependency1 dependency2 command "#; let parsed = parse(MULTIPLE_PREREQUISITES); assert_eq!(parsed.errors, Vec::::new()); let node = parsed.syntax(); assert_eq!( format!("{:#?}", node), r#"ROOT@0..40 RULE@0..40 IDENTIFIER@0..4 "rule" OPERATOR@4..5 ":" WHITESPACE@5..6 " " EXPR@6..29 IDENTIFIER@6..17 "dependency1" WHITESPACE@17..18 " " IDENTIFIER@18..29 "dependency2" NEWLINE@29..30 "\n" RECIPE@30..39 INDENT@30..31 "\t" TEXT@31..38 "command" NEWLINE@38..39 "\n" NEWLINE@39..40 "\n" "# ); let root = parsed.root(); let rule = root.rules().next().unwrap(); assert_eq!(rule.targets().collect::>(), vec!["rule"]); assert_eq!( rule.prerequisites().collect::>(), vec!["dependency1", "dependency2"] ); assert_eq!(rule.recipes().collect::>(), vec!["command"]); } #[test] fn test_add_rule() { let mut makefile = Makefile::new(); let rule = makefile.add_rule("rule"); assert_eq!(rule.targets().collect::>(), vec!["rule"]); assert_eq!( rule.prerequisites().collect::>(), Vec::::new() ); assert_eq!(makefile.to_string(), "rule:\n"); } #[test] fn test_push_command() { let mut makefile = Makefile::new(); let rule = makefile.add_rule("rule"); rule.push_command("command"); assert_eq!(rule.recipes().collect::>(), vec!["command"]); assert_eq!(makefile.to_string(), "rule:\n\tcommand\n"); rule.push_command("command2"); assert_eq!( rule.recipes().collect::>(), vec!["command", "command2"] ); assert_eq!(makefile.to_string(), "rule:\n\tcommand\n\tcommand2\n"); } #[test] fn test_replace_command() { let mut makefile = Makefile::new(); let rule = makefile.add_rule("rule"); rule.push_command("command"); rule.push_command("command2"); assert_eq!( rule.recipes().collect::>(), vec!["command", "command2"] ); rule.replace_command(0, "new command"); assert_eq!( rule.recipes().collect::>(), vec!["new command", "command2"] ); assert_eq!(makefile.to_string(), "rule:\n\tnew command\n\tcommand2\n"); } #[test] fn test_parse_rule_without_newline() { let rule = "rule: dependency\n\tcommand".parse::().unwrap(); assert_eq!(rule.targets().collect::>(), vec!["rule"]); assert_eq!(rule.recipes().collect::>(), vec!["command"]); let rule = "rule: dependency".parse::().unwrap(); assert_eq!(rule.targets().collect::>(), vec!["rule"]); assert_eq!(rule.recipes().collect::>(), Vec::::new()); } #[test] fn test_parse_makefile_without_newline() { let makefile = "rule: dependency\n\tcommand".parse::().unwrap(); assert_eq!(makefile.rules().count(), 1); let makefile = "rule: dependency".parse::().unwrap(); assert_eq!(makefile.rules().count(), 1); } #[test] fn test_from_reader() { let makefile = Makefile::from_reader("rule: dependency\n\tcommand".as_bytes()).unwrap(); assert_eq!(makefile.rules().count(), 1); } }