diff --git a/src/models/concrete_syntax.rs b/src/models/concrete_syntax.rs index 81323e32c..59e4836c8 100644 --- a/src/models/concrete_syntax.rs +++ b/src/models/concrete_syntax.rs @@ -65,13 +65,13 @@ pub(crate) fn get_all_matches_for_concrete_syntax( }; match_map.insert(replace_node_key, replace_node_match.clone()); - matches.push(Match { matched_string: replace_node_match.text, range: replace_node_match.range, matches: match_map.into_iter().map(|(k, v)| (k, v.text)).collect(), associated_comma: None, associated_comments: Vec::new(), + associated_leading_empty_lines: Vec::new(), }); } if recursive { diff --git a/src/models/matches.rs b/src/models/matches.rs index 47fbdeb96..596b8911c 100644 --- a/src/models/matches.rs +++ b/src/models/matches.rs @@ -51,6 +51,10 @@ pub struct Match { #[get_mut] #[serde(skip)] pub(crate) associated_comments: Vec, + #[get] + #[get_mut] + #[serde(skip)] + pub(crate) associated_leading_empty_lines: Vec, } #[pymethods] @@ -74,6 +78,7 @@ impl Match { matches, associated_comma: None, associated_comments: Vec::new(), + associated_leading_empty_lines: Vec::new(), } } @@ -85,6 +90,7 @@ impl Match { let associated_ranges = [ self.associated_comma().iter().collect_vec(), self.associated_comments().iter().collect_vec(), + self.associated_leading_empty_lines().iter().collect_vec(), ] .concat() .iter() @@ -117,8 +123,53 @@ impl Match { ) { self.get_associated_elements(node, code, piranha_arguments, true); self.get_associated_elements(node, code, piranha_arguments, false); + self.get_associated_leading_empty_lines(node, code); } + fn get_associated_leading_empty_lines(&mut self, matched_node: &Node, code: &String) { + if let Some(empty_range) = self.find_empty_line_range(code, matched_node) { + let skipped_range = Range { + start_byte: empty_range.start, + end_byte: empty_range.end, + start_point: position_for_offset(code.as_bytes(), empty_range.start), + end_point: position_for_offset(code.as_bytes(), empty_range.end), + }; + self + .associated_leading_empty_lines_mut() + .push(skipped_range); + } + } + + fn find_empty_line_range(&self, code: &str, node: &Node) -> Option> { + let mut end_byte = node.start_byte(); + let code_bytes = code.as_bytes(); + let mut found = false; + while end_byte > 0 { + let prev_char = code_bytes[end_byte - 1] as char; + if prev_char.is_whitespace() { + end_byte -= 1; + if prev_char == '\n' { + found = true; + break; + } + } else { + break; + } + } + + if found { + // Get the previous sibling node's end byte if it exists, in order to prevent overdeletions + let prev_sibling_end_byte = if let Some(prev_sibling) = node.prev_sibling() { + prev_sibling.end_byte() + } else { + 0 + }; + let max_end_byte = std::cmp::max(end_byte, prev_sibling_end_byte); + Some(max_end_byte..node.start_byte()) + } else { + None + } + } fn found_comma(&self) -> bool { self.associated_comma().is_some() } @@ -143,7 +194,6 @@ impl Match { current_node.prev_sibling() } { // Check if the sibling is a comma - if !self.found_comma() && self.is_comma_safe_to_delete(&sibling, code, trailing) { // Add the comma to the associated matches self.associated_comma = Some(sibling.range().into()); diff --git a/src/models/unit_tests/source_code_unit_test.rs b/src/models/unit_tests/source_code_unit_test.rs index 402fe6326..6c584300c 100644 --- a/src/models/unit_tests/source_code_unit_test.rs +++ b/src/models/unit_tests/source_code_unit_test.rs @@ -16,7 +16,7 @@ use tree_sitter::Parser; use crate::{ filter, models::{ - default_configs::{JAVA, UNUSED_CODE_PATH}, + default_configs::{JAVA, RUBY, SWIFT, UNUSED_CODE_PATH}, filter::Filter, language::PiranhaLanguage, matches::{Point, Range}, @@ -699,3 +699,158 @@ fn test_satisfies_outermost_enclosing_node() { assert!(source_code_unit.is_satisfied(*node, &rule_positive, &HashMap::new(), &mut rule_store,)); } + +#[test] +fn test_removes_blank_lines_after_inline_cleanup() { + let inline_cleanup_rule = piranha_rule! { + name= "inline_cleanup_rule", + query= " + ( + (if_modifier + body : ((_) @body) + [ + condition: (false) + condition: (parenthesized_statements (false)) + ] + )@if_modifier + ) + ", + replace_node = "if_modifier", + replace = "" + }; + + let inline_rule = InstantiatedRule::new(&inline_cleanup_rule, &HashMap::new()); + + let source_code = r#" + def method_name + do_something if false + end + "# + .trim(); + + let piranha_arguments = PiranhaArgumentsBuilder::default() + .paths_to_codebase(vec![UNUSED_CODE_PATH.to_string()]) + .language(PiranhaLanguage::from(RUBY)) + .build(); + + let mut rule_store = RuleStore::new(&piranha_arguments); + let mut parser = piranha_arguments.language().parser(); + + let source_code_unit = SourceCodeUnit::new( + &mut parser, + source_code.to_string(), + &HashMap::new(), + PathBuf::new().as_path(), + &piranha_arguments, + ); + let matches = source_code_unit.get_matches( + &inline_rule, + &mut rule_store, + source_code_unit.root_node(), + true, + ); + assert_eq!(matches.len(), 1); + assert_eq!( + matches + .first() + .unwrap() + .associated_leading_empty_lines + .len(), + 1 + ); + assert_eq!( + *matches + .first() + .unwrap() + .associated_leading_empty_lines + .first() + .unwrap(), + Range { + start_byte: 15, + end_byte: 24, + start_point: Point { row: 0, column: 15 }, + end_point: Point { row: 1, column: 8 } + } + ); +} + +#[test] +fn test_switch_entry_blank_lines() { + let inline_cleanup_rule = piranha_rule! { + name= "inline_cleanup_rule", + query= " + ( + (switch_entry + (switch_pattern) @p1 + (switch_pattern) @p2 + (switch_pattern) @p3 + (switch_pattern) @p4 + (switch_pattern) @p5 + (switch_pattern) @p6 + ) @custom_entry + (#eq? @p1 \".case_zeta_first\") + ) + ", + replace_node = "custom_entry", + replace = "" + }; + + let inline_rule = InstantiatedRule::new(&inline_cleanup_rule, &HashMap::new()); + + let source_code = r#" + public var namespace: ParameterNamespace { + switch self { + case .case_random_word_alpha, + .case_random_word_beta, + .case_random_word_gamma, + .case_random_word_delta: + return .namespace_group_alpha + case .case_zeta_first, + .case_zeta_second, + .case_zeta_third, + .case_zeta_fourth, + .case_zeta_fifth, + .case_zeta_sixth: + return .namespace_group_beta + case .case_random_word_omega: + return .namespace_group_gamma + } + } + "# + .trim(); + + let piranha_arguments = PiranhaArgumentsBuilder::default() + .paths_to_codebase(vec![UNUSED_CODE_PATH.to_string()]) + .language(PiranhaLanguage::from(SWIFT)) + .build(); + + let mut rule_store = RuleStore::new(&piranha_arguments); + let mut parser = piranha_arguments.language().parser(); + + let mut source_code_unit = SourceCodeUnit::new( + &mut parser, + source_code.to_string(), + &HashMap::new(), + PathBuf::new().as_path(), + &piranha_arguments, + ); + + source_code_unit.apply_rule(inline_rule, &mut rule_store, &mut parser, &None); + let transformed_code = source_code_unit.code(); + + let expected_code = r#" + public var namespace: ParameterNamespace { + switch self { + case .case_random_word_alpha, + .case_random_word_beta, + .case_random_word_gamma, + .case_random_word_delta: + return .namespace_group_alpha + case .case_random_word_omega: + return .namespace_group_gamma + } + } + "# + .trim(); + assert_eq!(transformed_code, expected_code); +}