diff --git a/cli/src/properties.rs b/cli/src/properties.rs index 0b424938..55158c21 100644 --- a/cli/src/properties.rs +++ b/cli/src/properties.rs @@ -206,11 +206,29 @@ impl Builder { } } + // If there are multiple transitions that could *both* match (e.g. one based on a + // a node type and one based on a field name), then create an additional transition + // for the intersection of the two. + let mut i = 0; + let mut transition_list = transitions.into_iter().collect::>(); + while i < transition_list.len() { + for j in 0..i { + if let Some(intersection) = + self.intersect_transitions(&transition_list[j].0, &transition_list[i].0) + { + transition_list.push(( + intersection, + u32::max(transition_list[i].1, transition_list[j].1), + )); + } + } + i += 1; + } + // Ensure that for a given node type, more specific transitions are tried // first, and in the event of a tie, transitions corresponding to later rules - // in the cascade are tried first. - let mut transition_list: Vec<(PropertyTransitionJSON, u32)> = - transitions.into_iter().collect(); + // in the cascade are tried first. Also, sort the non-intersecting transitions + // by name to guarantee a deterministic order. transition_list.sort_by(|a, b| { (transition_specificity(&b.0).cmp(&transition_specificity(&a.0))) .then_with(|| b.1.cmp(&a.1)) @@ -290,6 +308,31 @@ impl Builder { .extend(transition_list.into_iter().map(|i| i.0)); } + fn intersect_transitions( + &self, + left: &PropertyTransitionJSON, + right: &PropertyTransitionJSON, + ) -> Option { + // If one transition is based on the node's type and the other + // is based on its field name, then create a combined transition + // wiht *both* criteria. + if let (Some(left_field), None) = (&left.field, &left.kind) { + if right.field.is_none() { + let mut result = right.clone(); + result.field = Some(left_field.clone()); + return Some(result); + } + } + if let (Some(right_field), None) = (&right.field, &right.kind) { + if left.field.is_none() { + let mut result = left.clone(); + result.field = Some(right_field.clone()); + return Some(result); + } + } + return None; + } + fn remove_duplicate_states(&mut self) { let mut state_replacements = BTreeMap::new(); let mut done = false; diff --git a/cli/src/tests/properties_test.rs b/cli/src/tests/properties_test.rs index f25b902c..1829b561 100644 --- a/cli/src/tests/properties_test.rs +++ b/cli/src/tests/properties_test.rs @@ -49,7 +49,13 @@ fn test_walk_with_properties_with_nth_child() { assert!(cursor.goto_first_child()); assert_eq!(cursor.node().kind(), "identifier"); - assert_eq!(*cursor.node_properties(), Properties { a: Some("z".to_string()), b: None }); + assert_eq!( + *cursor.node_properties(), + Properties { + a: Some("z".to_string()), + b: None + } + ); assert!(cursor.goto_next_sibling()); assert_eq!(cursor.node().kind(), "="); @@ -58,13 +64,25 @@ fn test_walk_with_properties_with_nth_child() { assert!(cursor.goto_first_child()); assert_eq!(cursor.node().kind(), "identifier"); - assert_eq!(*cursor.node_properties(), Properties { a: Some("y".to_string()), b: None }); + assert_eq!( + *cursor.node_properties(), + Properties { + a: Some("y".to_string()), + b: None + } + ); assert!(cursor.goto_next_sibling()); assert_eq!(cursor.node().kind(), "||"); assert!(cursor.goto_next_sibling()); assert_eq!(cursor.node().kind(), "identifier"); - assert_eq!(*cursor.node_properties(), Properties { a: Some("x".to_string()), b: None }); + assert_eq!( + *cursor.node_properties(), + Properties { + a: Some("x".to_string()), + b: None + } + ); } #[test] @@ -110,7 +128,13 @@ fn test_walk_with_properties_with_regexes() { // The later selector with a text regex overrides the earlier one. assert!(cursor.goto_first_child()); assert_eq!(cursor.node().kind(), "identifier"); - assert_eq!(*cursor.node_properties(), Properties { a: Some("z".to_string()), b: None }); + assert_eq!( + *cursor.node_properties(), + Properties { + a: Some("z".to_string()), + b: None + } + ); assert!(cursor.goto_next_sibling()); assert_eq!(cursor.node().kind(), "="); @@ -120,7 +144,13 @@ fn test_walk_with_properties_with_regexes() { // The selectors with text regexes override the selector without one. assert!(cursor.goto_first_child()); assert_eq!(cursor.node().kind(), "identifier"); - assert_eq!(*cursor.node_properties(), Properties { a: Some("y".to_string()), b: None }); + assert_eq!( + *cursor.node_properties(), + Properties { + a: Some("y".to_string()), + b: None + } + ); assert!(cursor.goto_next_sibling()); assert_eq!(cursor.node().kind(), "arguments"); @@ -130,5 +160,102 @@ fn test_walk_with_properties_with_regexes() { // This node doesn't match either of the regexes. assert!(cursor.goto_next_sibling()); assert_eq!(cursor.node().kind(), "identifier"); - assert_eq!(*cursor.node_properties(), Properties { a: Some("x".to_string()), b: None }); + assert_eq!( + *cursor.node_properties(), + Properties { + a: Some("x".to_string()), + b: None + } + ); +} + +#[test] +fn test_walk_with_properties_based_on_fields() { + let language = get_language("javascript"); + let property_sheet = PropertySheet::::new( + language, + &properties::generate_property_sheet_string( + "/some/path.css", + " + arrow_function > .parameter { + a: x; + } + + function_declaration { + & > .parameters > identifier { + a: y; + } + + & > .name { + b: z; + } + } + + identifier { + a: w; + } + ", + ) + .unwrap(), + ) + .unwrap(); + + let source_code = "function a(b) { return c => c + b; }"; + + let mut parser = Parser::new(); + parser.set_language(language).unwrap(); + let tree = parser.parse(source_code, None).unwrap(); + let mut cursor = tree.walk_with_properties(&property_sheet, source_code.as_bytes()); + + assert!(cursor.goto_first_child()); + assert_eq!(cursor.node().kind(), "function_declaration"); + assert!(cursor.goto_first_child()); + assert_eq!(cursor.node().kind(), "function"); + assert_eq!(*cursor.node_properties(), Properties::default()); + + assert!(cursor.goto_next_sibling()); + assert_eq!(cursor.node().kind(), "identifier"); + assert_eq!( + *cursor.node_properties(), + Properties { + a: None, + b: Some("z".to_string()) + } + ); + + assert!(cursor.goto_next_sibling()); + assert_eq!(cursor.node().kind(), "formal_parameters"); + assert_eq!(*cursor.node_properties(), Properties::default()); + + assert!(cursor.goto_first_child()); + assert_eq!(cursor.node().kind(), "("); + assert_eq!(*cursor.node_properties(), Properties::default()); + assert!(cursor.goto_next_sibling()); + assert_eq!(cursor.node().kind(), "identifier"); + assert_eq!( + *cursor.node_properties(), + Properties { + a: Some("y".to_string()), + b: None, + } + ); + + assert!(cursor.goto_parent()); + assert!(cursor.goto_next_sibling()); + assert_eq!(cursor.node().kind(), "statement_block"); + assert!(cursor.goto_first_child()); + assert!(cursor.goto_next_sibling()); + assert_eq!(cursor.node().kind(), "return_statement"); + assert!(cursor.goto_first_child()); + assert!(cursor.goto_next_sibling()); + assert_eq!(cursor.node().kind(), "arrow_function"); + assert!(cursor.goto_first_child()); + assert_eq!(cursor.node().kind(), "identifier"); + assert_eq!( + *cursor.node_properties(), + Properties { + a: Some("x".to_string()), + b: None, + } + ); }