use super::makefile::MakefileItem;
use crate::lossless::{remove_with_preceding_comments, Error, ErrorInfo, Include, ParseError};
use crate::SyntaxKind::{EXPR, IDENTIFIER};
use rowan::ast::AstNode;
use rowan::{GreenNodeBuilder, SyntaxNode};
impl Include {
pub fn path(&self) -> Option<String> {
self.syntax()
.children()
.find(|it| it.kind() == EXPR)
.map(|it| it.text().to_string().trim().to_string())
}
pub fn path_range(&self) -> Option<rowan::TextRange> {
self.syntax()
.children()
.find(|it| it.kind() == EXPR)
.map(|it| it.text_range())
}
pub fn is_optional(&self) -> bool {
let text = self.syntax().text();
text.to_string().starts_with("-include") || text.to_string().starts_with("sinclude")
}
pub fn parent(&self) -> Option<MakefileItem> {
self.syntax().parent().and_then(MakefileItem::cast)
}
pub fn remove(&mut self) -> Result<(), Error> {
let Some(parent) = self.syntax().parent() else {
return Err(Error::Parse(ParseError {
errors: vec![ErrorInfo {
message: "Cannot remove include: no parent node".to_string(),
line: 1,
context: "include_remove".to_string(),
}],
}));
};
remove_with_preceding_comments(self.syntax(), &parent);
Ok(())
}
pub fn set_path(&mut self, new_path: &str) {
let expr_index = self
.syntax()
.children()
.find(|it| it.kind() == EXPR)
.map(|it| it.index());
if let Some(expr_idx) = expr_index {
let mut builder = GreenNodeBuilder::new();
builder.start_node(EXPR.into());
builder.token(IDENTIFIER.into(), new_path);
builder.finish_node();
let new_expr = SyntaxNode::new_root_mut(builder.finish());
self.syntax()
.splice_children(expr_idx..expr_idx + 1, vec![new_expr.into()]);
}
}
pub fn set_optional(&mut self, optional: bool) {
use crate::SyntaxKind::INCLUDE;
let keyword_token = self.syntax().children_with_tokens().find(|it| {
it.as_token()
.map(|t| t.kind() == IDENTIFIER)
.unwrap_or(false)
});
if let Some(token_element) = keyword_token {
let token = token_element.as_token().unwrap();
let current_text = token.text();
let new_keyword = if optional {
if current_text == "include" {
"-include"
} else if current_text == "sinclude" || current_text == "-include" {
return;
} else {
return;
}
} else {
if current_text == "-include" || current_text == "sinclude" {
"include"
} else if current_text == "include" {
return;
} else {
return;
}
};
let mut builder = GreenNodeBuilder::new();
builder.start_node(INCLUDE.into());
for child in self.syntax().children_with_tokens() {
match child {
rowan::NodeOrToken::Token(tok)
if tok.kind() == IDENTIFIER && tok.text() == current_text =>
{
builder.token(IDENTIFIER.into(), new_keyword);
}
rowan::NodeOrToken::Token(tok) => {
builder.token(tok.kind().into(), tok.text());
}
rowan::NodeOrToken::Node(node) => {
builder.start_node(node.kind().into());
for node_child in node.children_with_tokens() {
if let rowan::NodeOrToken::Token(tok) = node_child {
builder.token(tok.kind().into(), tok.text());
}
}
builder.finish_node();
}
}
}
builder.finish_node();
let new_include = SyntaxNode::new_root_mut(builder.finish());
let index = self.syntax().index();
if let Some(parent) = self.syntax().parent() {
parent.splice_children(index..index + 1, vec![new_include.clone().into()]);
*self = Include::cast(
parent
.children_with_tokens()
.nth(index)
.and_then(|it| it.into_node())
.unwrap(),
)
.unwrap();
}
}
}
}
#[cfg(test)]
mod tests {
use crate::lossless::Makefile;
#[test]
fn test_include_parent() {
let makefile: Makefile = "include common.mk\n".parse().unwrap();
let inc = makefile.includes().next().unwrap();
let parent = inc.parent();
assert!(parent.is_none());
}
#[test]
fn test_add_include() {
let mut makefile = Makefile::new();
makefile.add_include("config.mk");
let includes: Vec<_> = makefile.includes().collect();
assert_eq!(includes.len(), 1);
assert_eq!(includes[0].path(), Some("config.mk".to_string()));
let files: Vec<_> = makefile.included_files().collect();
assert_eq!(files, vec!["config.mk"]);
assert_eq!(makefile.to_string(), "include config.mk\n");
}
#[test]
fn test_add_include_to_existing() {
let mut makefile: Makefile = "VAR = value\nrule:\n\tcommand\n".parse().unwrap();
makefile.add_include("config.mk");
let files: Vec<_> = makefile.included_files().collect();
assert_eq!(files, vec!["config.mk"]);
let text = makefile.to_string();
assert!(text.starts_with("include config.mk\n"));
assert!(text.contains("VAR = value"));
}
#[test]
fn test_insert_include() {
let mut makefile: Makefile = "VAR = value\nrule:\n\tcommand\n".parse().unwrap();
makefile.insert_include(1, "config.mk").unwrap();
let items: Vec<_> = makefile.items().collect();
assert_eq!(items.len(), 3);
let files: Vec<_> = makefile.included_files().collect();
assert_eq!(files, vec!["config.mk"]);
}
#[test]
fn test_insert_include_at_beginning() {
let mut makefile: Makefile = "VAR = value\n".parse().unwrap();
makefile.insert_include(0, "config.mk").unwrap();
let text = makefile.to_string();
assert!(text.starts_with("include config.mk\n"));
}
#[test]
fn test_insert_include_at_end() {
let mut makefile: Makefile = "VAR = value\n".parse().unwrap();
let item_count = makefile.items().count();
makefile.insert_include(item_count, "config.mk").unwrap();
let text = makefile.to_string();
assert!(text.ends_with("include config.mk\n"));
}
#[test]
fn test_insert_include_out_of_bounds() {
let mut makefile: Makefile = "VAR = value\n".parse().unwrap();
let result = makefile.insert_include(100, "config.mk");
assert!(result.is_err());
}
#[test]
fn test_insert_include_after() {
let mut makefile: Makefile = "VAR1 = value1\nVAR2 = value2\n".parse().unwrap();
let first_var = makefile.items().next().unwrap();
makefile
.insert_include_after(&first_var, "config.mk")
.unwrap();
let files: Vec<_> = makefile.included_files().collect();
assert_eq!(files, vec!["config.mk"]);
let text = makefile.to_string();
let var1_pos = text.find("VAR1").unwrap();
let include_pos = text.find("include config.mk").unwrap();
assert!(include_pos > var1_pos);
}
#[test]
fn test_insert_include_after_with_rule() {
let mut makefile: Makefile = "rule1:\n\tcommand1\nrule2:\n\tcommand2\n".parse().unwrap();
let first_rule_item = makefile.items().next().unwrap();
makefile
.insert_include_after(&first_rule_item, "config.mk")
.unwrap();
let text = makefile.to_string();
let rule1_pos = text.find("rule1:").unwrap();
let include_pos = text.find("include config.mk").unwrap();
let rule2_pos = text.find("rule2:").unwrap();
assert!(include_pos > rule1_pos);
assert!(include_pos < rule2_pos);
}
#[test]
fn test_include_remove() {
let makefile: Makefile = "include config.mk\nVAR = value\n".parse().unwrap();
let mut inc = makefile.includes().next().unwrap();
inc.remove().unwrap();
assert_eq!(makefile.includes().count(), 0);
assert_eq!(makefile.to_string(), "VAR = value\n");
}
#[test]
fn test_include_remove_multiple() {
let makefile: Makefile = "include first.mk\ninclude second.mk\nVAR = value\n"
.parse()
.unwrap();
let mut inc = makefile.includes().next().unwrap();
inc.remove().unwrap();
assert_eq!(makefile.includes().count(), 1);
let remaining = makefile.includes().next().unwrap();
assert_eq!(remaining.path(), Some("second.mk".to_string()));
}
#[test]
fn test_include_set_path() {
let makefile: Makefile = "include old.mk\n".parse().unwrap();
let mut inc = makefile.includes().next().unwrap();
inc.set_path("new.mk");
assert_eq!(inc.path(), Some("new.mk".to_string()));
assert_eq!(makefile.to_string(), "include new.mk\n");
}
#[test]
fn test_include_set_path_preserves_optional() {
let makefile: Makefile = "-include old.mk\n".parse().unwrap();
let mut inc = makefile.includes().next().unwrap();
inc.set_path("new.mk");
assert_eq!(inc.path(), Some("new.mk".to_string()));
assert!(inc.is_optional());
assert_eq!(makefile.to_string(), "-include new.mk\n");
}
#[test]
fn test_include_set_optional_true() {
let makefile: Makefile = "include config.mk\n".parse().unwrap();
let mut inc = makefile.includes().next().unwrap();
inc.set_optional(true);
assert!(inc.is_optional());
assert_eq!(makefile.to_string(), "-include config.mk\n");
}
#[test]
fn test_include_set_optional_false() {
let makefile: Makefile = "-include config.mk\n".parse().unwrap();
let mut inc = makefile.includes().next().unwrap();
inc.set_optional(false);
assert!(!inc.is_optional());
assert_eq!(makefile.to_string(), "include config.mk\n");
}
#[test]
fn test_include_set_optional_from_sinclude() {
let makefile: Makefile = "sinclude config.mk\n".parse().unwrap();
let mut inc = makefile.includes().next().unwrap();
inc.set_optional(false);
assert!(!inc.is_optional());
assert_eq!(makefile.to_string(), "include config.mk\n");
}
#[test]
fn test_include_set_optional_already_optional() {
let makefile: Makefile = "-include config.mk\n".parse().unwrap();
let mut inc = makefile.includes().next().unwrap();
inc.set_optional(true);
assert!(inc.is_optional());
assert_eq!(makefile.to_string(), "-include config.mk\n");
}
#[test]
fn test_include_set_optional_already_non_optional() {
let makefile: Makefile = "include config.mk\n".parse().unwrap();
let mut inc = makefile.includes().next().unwrap();
inc.set_optional(false);
assert!(!inc.is_optional());
assert_eq!(makefile.to_string(), "include config.mk\n");
}
#[test]
fn test_include_combined_operations() {
let makefile: Makefile = "include old.mk\nVAR = value\n".parse().unwrap();
let mut inc = makefile.includes().next().unwrap();
inc.set_path("new.mk");
inc.set_optional(true);
assert_eq!(inc.path(), Some("new.mk".to_string()));
assert!(inc.is_optional());
assert_eq!(makefile.to_string(), "-include new.mk\nVAR = value\n");
}
#[test]
fn test_include_path_range() {
let makefile: Makefile = "include config.mk\n".parse().unwrap();
let inc = makefile.includes().next().unwrap();
let range = inc.path_range().unwrap();
assert_eq!(
&makefile.to_string()[std::ops::Range::from(range)],
"config.mk"
);
}
#[test]
fn test_include_path_range_optional() {
let makefile: Makefile = "-include optional.mk\n".parse().unwrap();
let inc = makefile.includes().next().unwrap();
let range = inc.path_range().unwrap();
assert_eq!(
&makefile.to_string()[std::ops::Range::from(range)],
"optional.mk"
);
}
#[test]
fn test_include_path_range_sinclude() {
let makefile: Makefile = "sinclude silent.mk\n".parse().unwrap();
let inc = makefile.includes().next().unwrap();
let range = inc.path_range().unwrap();
assert_eq!(
&makefile.to_string()[std::ops::Range::from(range)],
"silent.mk"
);
}
#[test]
fn test_include_with_comment() {
let makefile: Makefile = "# Comment\ninclude config.mk\n".parse().unwrap();
let mut inc = makefile.includes().next().unwrap();
inc.remove().unwrap();
assert_eq!(makefile.includes().count(), 0);
assert!(!makefile.to_string().contains("# Comment"));
}
}