diff --git a/crates/turborepo-lib/src/commands/link.rs b/crates/turborepo-lib/src/commands/link.rs index 30e0e09b97cefa..81022e6ba20f4b 100644 --- a/crates/turborepo-lib/src/commands/link.rs +++ b/crates/turborepo-lib/src/commands/link.rs @@ -23,7 +23,7 @@ use crate::ui::CYAN; use crate::{ cli::LinkTarget, commands::CommandBase, - rewrite_json::{self, RewriteError}, + rewrite_json, ui::{BOLD, GREY, UNDERLINE}, }; diff --git a/crates/turborepo-lib/src/rewrite_json.rs b/crates/turborepo-lib/src/rewrite_json.rs index d5abbad99f6f2b..7a8c0d27373029 100644 --- a/crates/turborepo-lib/src/rewrite_json.rs +++ b/crates/turborepo-lib/src/rewrite_json.rs @@ -8,7 +8,9 @@ pub enum RewriteError { #[error("The JSON document contains no root object.")] NoRoot, } - +/** + * When generating replacement content it's one of two things. + */ enum GenerateType { Object, Path, @@ -95,7 +97,7 @@ pub fn set_path( let missing_path_segments = path[closest_path.len()..].to_vec(); let computed_object = match generate_type { GenerateType::Object => generate_object(missing_path_segments, json_value), - GenerateType::Path => generate_path(separator, missing_path_segments, json_value), + GenerateType::Path => generate_path(missing_path_segments, json_value, separator), }; // Generate a new document! @@ -104,6 +106,10 @@ pub fn set_path( return Ok(output); } +/** + * get_root returns the document root, or information on the error + * encountered with the input json_document_string. + */ fn get_root(json_document_string: &str) -> Result { let parse_result_result = parse_to_ast( json_document_string, @@ -120,37 +126,51 @@ fn get_root(json_document_string: &str) -> Result( current_node: &'a jsonc_parser::ast::Value<'a>, target_path: &Vec<&'a str>, current_path: Vec<&'a str>, ) -> (Vec<&'a str>, &'a jsonc_parser::ast::Value<'a>) { + // No target_path? We've arrived. if target_path.len() == 0 { return (current_path, current_node); } match current_node { + // Only objects can have key paths. jsonc_parser::ast::Value::Object(obj) => { let object_property = obj.properties.iter().rev().find(|property| { let current_property_name = property.name.as_str(); return target_path[0] == current_property_name; }); - if object_property.is_some() { - let kv_pair = &object_property.unwrap(); - let next_property_name = kv_pair.name.as_str(); - let mut next_path = current_path.clone(); - next_path.push(next_property_name); - let next_node = &object_property.unwrap().value; - return get_closest_node(next_node, &target_path[1..].to_vec(), next_path); - } else { - return (current_path, current_node); + // See if we found a matching key. If so, recurse. + match object_property { + Some(property) => { + let next_current_node = &property.value; + let next_property_name = property.name.as_str(); + let mut next_current_path = current_path.clone(); + next_current_path.push(next_property_name); + let next_target_path = &target_path[1..].to_vec(); + + // Tail call! + get_closest_node(next_current_node, next_target_path, next_current_path) + } + None => (current_path, current_node), } } + // All other node types are complete. _ => (current_path, current_node), } } +/** + * Given path segments, generate a JSON object. + */ fn generate_object(path_segments: Vec<&str>, value: &str) -> String { let mut output = String::new(); let length = path_segments.len(); @@ -170,7 +190,11 @@ fn generate_object(path_segments: Vec<&str>, value: &str) -> String { return output; } -fn generate_path(separator: &str, path_segments: Vec<&str>, value: &str) -> String { +/** + * Given path segments, generate a JSON object member with an optional + * trailing separator. + */ +fn generate_path(path_segments: Vec<&str>, value: &str, separator: &str) -> String { let mut output = String::new(); output.push_str("\""); output.push_str(path_segments[0]); @@ -182,37 +206,56 @@ fn generate_path(separator: &str, path_segments: Vec<&str>, value: &str) -> Stri return output; } +/** + * Given a JSONC document and an object traversal path, `unset_path` will + * return a minimally-mutated JSONC document with all occurrences of the + * specified path removed. + */ pub fn unset_path(json_document_string: &str, path: Vec<&str>) -> Result { let root = get_root(json_document_string)?; + // The key path can appear multiple times. This a vec that contains each time it + // occurs. let path_ranges = find_all_paths(&root, &path, vec![]); + // We mutate this as we go. let mut output: String = json_document_string.to_owned(); - let mut last_start = None; + // We could either join overlapping ranges, or just carry it over. + // This elects to carry it over. + let mut last_start_position = None; + + // We iterate in reverse since we're mutating the string. path_ranges.iter().rev().for_each(|range| { - let end = if last_start.is_some() { - if range.end > last_start.unwrap() { - last_start.unwrap() - } else { - range.end + let end = match last_start_position { + Some(last_start_position) => { + if range.end > last_start_position { + last_start_position + } else { + range.end + } } - } else { - range.end + None => range.end, }; - let replacement_char = if end != range.end { + let is_overlapping = end != range.end; + let replacement_char = if is_overlapping { "" } else { &range.replacement_char }; output.replace_range(range.start..end, replacement_char); - last_start = Some(range.start); + last_start_position = Some(range.start); }); + return Ok(output); } +/** + * find_all_paths returns the list of ranges which define the specified + * token. + */ fn find_all_paths<'a>( current_node: &'a jsonc_parser::ast::Value<'a>, target_path: &Vec<&'a str>, @@ -220,51 +263,65 @@ fn find_all_paths<'a>( ) -> Vec { let mut ranges: Vec = vec![]; - if target_path.len() > 0 { - match current_node { - jsonc_parser::ast::Value::Object(obj) => { - let properties: Vec<_> = obj.properties.iter().collect(); + // Early exit when it's impossible to have matching ranges. + if target_path.len() == 0 { + return ranges; + } - for index in 0..properties.len() { - let is_first = index == 0; - let is_last = index == properties.len() - 1; + match current_node { + // We can only find paths on objects. + jsonc_parser::ast::Value::Object(obj) => { + // We need a reference to the previous and next property to identify if we're + // looking at the first or last node which need special handling. + let properties: Vec<_> = obj.properties.iter().collect(); - let start = if is_first { - properties[index].range.start - } else { - properties[index - 1].range.end - }; + for index in 0..properties.len() { + let property = properties[index]; - let end = if is_last { - properties[index].range.end - } else { - properties[index + 1].range.start - }; - - let replacement_char = if is_first || is_last { "" } else { "," }; - - let property = properties[index]; - let current_property_name = property.name.as_str(); - if target_path[0] == current_property_name { - if target_path.len() == 1 { - ranges.push(Range { - start, - end, - replacement_char: replacement_char.to_owned(), - }); + let current_property_name = property.name.as_str(); + if target_path[0] == current_property_name { + // target_path == 1? We've arrived at a node to remove. + if target_path.len() == 1 { + let is_first = index == 0; + let is_last = index == properties.len() - 1; + + // We calculate the range based off the adjacent nodes. + // This is required to ensure that we capture things like commas. + let start = if is_first { + property.range.start + } else { + properties[index - 1].range.end + }; + + let end = if is_last { + property.range.end } else { - let mut next_path = current_path.clone(); - next_path.push(property.name.as_str()); - let next_node = &property.value; - let mut children_ranges = - find_all_paths(&next_node, &target_path[1..].to_vec(), next_path); - ranges.append(&mut children_ranges); - } + properties[index + 1].range.start + }; + + let replacement_char = if is_first || is_last { "" } else { "," }; + + ranges.push(Range { + start, + end, + replacement_char: replacement_char.to_owned(), + }); + } else { + // We must recurse. + let next_current_node = &property.value; + let next_property_name = property.name.as_str(); + let mut next_current_path = current_path.clone(); + next_current_path.push(next_property_name); + let next_target_path = &target_path[1..].to_vec(); + + let mut children_ranges = + find_all_paths(next_current_node, next_target_path, next_current_path); + ranges.append(&mut children_ranges); } } } - _ => {} } + _ => {} } return ranges;