diff --git a/kclvm/cmd/src/lib.rs b/kclvm/cmd/src/lib.rs index af51d8ab1..1bc06e22f 100644 --- a/kclvm/cmd/src/lib.rs +++ b/kclvm/cmd/src/lib.rs @@ -79,7 +79,8 @@ pub fn app() -> Command { .arg(arg!(path_selector: -S --path_selector ... "Specify the path selector").num_args(1..)) .arg(arg!(overrides: -O --overrides ... "Specify the configuration override path and value").num_args(1..)) .arg(arg!(target: --target "Specify the target type")) - .arg(arg!(package_map: -E --external ... "Mapping of package name and path where the package is located").num_args(1..)), + .arg(arg!(package_map: -E --external ... "Mapping of package name and path where the package is located").num_args(1..)) + .arg(arg!(fix: -f --fix "Auto fix")), ) .subcommand( Command::new("fmt") diff --git a/kclvm/cmd/src/lint.rs b/kclvm/cmd/src/lint.rs index 044007c63..f6740cfcb 100644 --- a/kclvm/cmd/src/lint.rs +++ b/kclvm/cmd/src/lint.rs @@ -3,7 +3,7 @@ use anyhow::Result; use clap::ArgMatches; use kclvm_error::Handler; use kclvm_runner::ExecProgramArgs; -use kclvm_tools::lint::lint_files; +use kclvm_tools::{fix, lint::lint_files}; use crate::settings::must_build_settings; @@ -28,6 +28,13 @@ pub fn lint_command(matches: &ArgMatches) -> Result<()> { if bool_from_matches(matches, "emit_warning").unwrap_or_default() { warning_handler.emit()?; } + + if bool_from_matches(matches, "fix").unwrap_or_default() { + let mut diags = vec![]; + diags.extend(err_handler.diagnostics.clone()); + diags.extend(warning_handler.diagnostics); + fix::fix(diags).unwrap(); + } err_handler.abort_if_any_errors(); Ok(()) } diff --git a/kclvm/error/src/diagnostic.rs b/kclvm/error/src/diagnostic.rs index 6e7f96df5..71afa8fc2 100644 --- a/kclvm/error/src/diagnostic.rs +++ b/kclvm/error/src/diagnostic.rs @@ -96,7 +96,7 @@ impl From for Position { impl Diagnostic { pub fn new(level: Level, message: &str, range: Range) -> Self { - Diagnostic::new_with_code(level, message, None, range, None) + Diagnostic::new_with_code(level, message, None, range, None, None) } /// New a diagnostic with error code. @@ -106,6 +106,7 @@ impl Diagnostic { note: Option<&str>, range: Range, code: Option, + suggested_replacement: Option, ) -> Self { Diagnostic { level, @@ -114,6 +115,7 @@ impl Diagnostic { style: Style::LineAndColumn, message: message.to_string(), note: note.map(|s| s.to_string()), + suggested_replacement, }], code, } @@ -133,6 +135,7 @@ pub struct Message { pub style: Style, pub message: String, pub note: Option, + pub suggested_replacement: Option, } #[derive(Clone, Debug, PartialEq, Eq, Hash)] diff --git a/kclvm/error/src/lib.rs b/kclvm/error/src/lib.rs index 7a736cb20..033dd2242 100644 --- a/kclvm/error/src/lib.rs +++ b/kclvm/error/src/lib.rs @@ -100,6 +100,7 @@ impl Handler { None, range, Some(DiagnosticId::Error(E1001.kind)), + None, ); self.add_diagnostic(diag); @@ -114,6 +115,7 @@ impl Handler { None, range, Some(DiagnosticId::Error(E2G22.kind)), + None, ); self.add_diagnostic(diag); @@ -128,6 +130,7 @@ impl Handler { None, range, Some(DiagnosticId::Error(E2L23.kind)), + None, ); self.add_diagnostic(diag); @@ -151,6 +154,7 @@ impl Handler { /// style: Style::LineAndColumn, /// message: "Invalid syntax: expected '+', got '-'".to_string(), /// note: None, + /// suggested_replacement: Some("".to_string()), /// } /// ]); /// ``` @@ -175,6 +179,7 @@ impl Handler { /// style: Style::LineAndColumn, /// message: "Module 'a' imported but unused.".to_string(), /// note: None, + /// suggested_replacement: Some("".to_string()), /// }], /// ); /// ``` @@ -235,7 +240,14 @@ impl From for Diagnostic { line: panic_info.kcl_line as u64, column: None, }; - Diagnostic::new_with_code(Level::Error, &panic_msg, None, (pos.clone(), pos), None) + Diagnostic::new_with_code( + Level::Error, + &panic_msg, + None, + (pos.clone(), pos), + None, + None, + ) } else { let mut backtrace_msg = "backtrace:\n".to_string(); let mut backtrace = panic_info.backtrace.clone(); @@ -261,6 +273,7 @@ impl From for Diagnostic { Some(&backtrace_msg), (pos.clone(), pos), None, + None, ) }; @@ -278,6 +291,7 @@ impl From for Diagnostic { None, (pos.clone(), pos), None, + None, ); config_meta_diag.messages.append(&mut diag.messages); config_meta_diag @@ -334,6 +348,7 @@ impl ParseError { None, (pos.clone(), pos), Some(DiagnosticId::Error(ErrorKind::InvalidSyntax)), + None, )) } } diff --git a/kclvm/parser/src/lib.rs b/kclvm/parser/src/lib.rs index 0cd42ecb8..eed2eb690 100644 --- a/kclvm/parser/src/lib.rs +++ b/kclvm/parser/src/lib.rs @@ -386,6 +386,7 @@ impl Loader { pkg_path ), note: None, + suggested_replacement: None, }], ); return Ok(None); @@ -402,6 +403,7 @@ impl Loader { style: Style::Line, message: format!("pkgpath {} not found in the program", pkg_path), note: None, + suggested_replacement: None, }], ); return Ok(None); @@ -498,6 +500,7 @@ impl Loader { style: Style::Line, message: format!("the plugin package `{}` is not found, please confirm if plugin mode is enabled", pkgpath), note: None, + suggested_replacement: None, }], ); } diff --git a/kclvm/sema/src/lint/lints_def.rs b/kclvm/sema/src/lint/lints_def.rs index d2d778d6f..422c17b97 100644 --- a/kclvm/sema/src/lint/lints_def.rs +++ b/kclvm/sema/src/lint/lints_def.rs @@ -62,6 +62,7 @@ impl LintPass for ImportPosition { note: Some( "Consider moving tihs statement to the top of the file".to_string(), ), + suggested_replacement: None, }], ); } @@ -108,6 +109,7 @@ impl LintPass for UnusedImport { style: Style::Line, message: format!("Module '{}' imported but unused", scope_obj.name), note: Some("Consider removing this statement".to_string()), + suggested_replacement: Some("".to_string()), }], ); } @@ -161,6 +163,7 @@ impl LintPass for ReImport { &import_stmt.name ), note: Some("Consider removing this statement".to_string()), + suggested_replacement: Some("".to_string()), }], ); } else { diff --git a/kclvm/sema/src/resolver/attr.rs b/kclvm/sema/src/resolver/attr.rs index 3e22f4d3c..bd6d0c45f 100644 --- a/kclvm/sema/src/resolver/attr.rs +++ b/kclvm/sema/src/resolver/attr.rs @@ -22,6 +22,7 @@ impl<'ctx> Resolver<'ctx> { attr_ty.ty_str() ), note: None, + suggested_replacement: None, }], ); } diff --git a/kclvm/sema/src/resolver/config.rs b/kclvm/sema/src/resolver/config.rs index 14a99f94c..1077dd42c 100644 --- a/kclvm/sema/src/resolver/config.rs +++ b/kclvm/sema/src/resolver/config.rs @@ -548,6 +548,7 @@ impl<'ctx> Resolver<'ctx> { val_ty.ty_str() ), note: None, + suggested_replacement: None, }], ); } diff --git a/kclvm/sema/src/resolver/global.rs b/kclvm/sema/src/resolver/global.rs index eb9d5d374..0dc484031 100644 --- a/kclvm/sema/src/resolver/global.rs +++ b/kclvm/sema/src/resolver/global.rs @@ -58,6 +58,7 @@ impl<'ctx> Resolver<'ctx> { style: Style::LineAndColumn, message: format!("unique key error name '{}'", name), note: None, + suggested_replacement: None, }], ); continue; @@ -184,6 +185,7 @@ impl<'ctx> Resolver<'ctx> { style: Style::Line, message: format!("pkgpath {} not found in the program", self.ctx.pkgpath), note: None, + suggested_replacement: None, }], ); } @@ -239,6 +241,7 @@ impl<'ctx> Resolver<'ctx> { name ), note: None, + suggested_replacement: None, }, Message { range: self @@ -255,6 +258,7 @@ impl<'ctx> Resolver<'ctx> { "change the variable name to '_{}' to make it mutable", name )), + suggested_replacement: None, }, ], ); @@ -278,12 +282,14 @@ impl<'ctx> Resolver<'ctx> { obj.ty.ty_str() ), note: None, + suggested_replacement: None, }, Message { range: obj.get_span_pos(), style: Style::LineAndColumn, message: format!("expected {}", obj.ty.ty_str()), note: None, + suggested_replacement: None, }, ], ); @@ -333,6 +339,7 @@ impl<'ctx> Resolver<'ctx> { name ), note: None, + suggested_replacement: None, }, Message { range: self @@ -349,6 +356,7 @@ impl<'ctx> Resolver<'ctx> { "change the variable name to '_{}' to make it mutable", name )), + suggested_replacement: None, }, ], ); @@ -390,6 +398,7 @@ impl<'ctx> Resolver<'ctx> { ty.ty_str() ), note: None, + suggested_replacement: None, }], ); None @@ -413,6 +422,7 @@ impl<'ctx> Resolver<'ctx> { style: Style::LineAndColumn, message: "only schema mixin can inherit from protocol".to_string(), note: None, + suggested_replacement: None, }], ); return None; @@ -434,6 +444,7 @@ impl<'ctx> Resolver<'ctx> { ty.ty_str() ), note: None, + suggested_replacement: None, }], ); None @@ -467,6 +478,7 @@ impl<'ctx> Resolver<'ctx> { ty.ty_str() ), note: None, + suggested_replacement: None, }], ); None @@ -503,6 +515,7 @@ impl<'ctx> Resolver<'ctx> { style: Style::LineAndColumn, message: format!("schema protocol name must end with '{}'", PROTOCOL_SUFFIX), note: None, + suggested_replacement: None, }], ); } @@ -523,6 +536,7 @@ impl<'ctx> Resolver<'ctx> { style: Style::LineAndColumn, message: format!("mixin inheritance {} is prohibited", parent_name), note: None, + suggested_replacement: None, }], ); } @@ -541,6 +555,7 @@ impl<'ctx> Resolver<'ctx> { style: Style::LineAndColumn, message: format!("index signature attribute name '{}' cannot have the same name as schema attributes", index_sign_name), note: None, + suggested_replacement: None, }], ); } @@ -565,6 +580,7 @@ impl<'ctx> Resolver<'ctx> { style: Style::LineAndColumn, message: format!("invalid index signature key type: '{}'", key_ty.ty_str()), note: None, + suggested_replacement: None, }], ); } @@ -707,6 +723,7 @@ impl<'ctx> Resolver<'ctx> { style: Style::LineAndColumn, message: format!("the type '{}' of schema attribute '{}' does not meet the index signature definition {}", ty.ty_str(), name, index_signature_obj.ty_str()), note: None, + suggested_replacement: None, }], ); } @@ -727,6 +744,7 @@ impl<'ctx> Resolver<'ctx> { mixin_names[mixin_names.len() - 1] ), note: None, + suggested_replacement: None, }], ); } @@ -748,6 +766,7 @@ impl<'ctx> Resolver<'ctx> { ty.ty_str() ), note: None, + suggested_replacement: None, }], ); None @@ -873,6 +892,7 @@ impl<'ctx> Resolver<'ctx> { style: Style::LineAndColumn, message: format!("illegal rule type '{}'", ty.ty_str()), note: None, + suggested_replacement: None, }], ); None diff --git a/kclvm/sema/src/resolver/import.rs b/kclvm/sema/src/resolver/import.rs index 1927d64b4..a79888439 100644 --- a/kclvm/sema/src/resolver/import.rs +++ b/kclvm/sema/src/resolver/import.rs @@ -45,6 +45,7 @@ impl<'ctx> Resolver<'ctx> { real_path.to_str().unwrap() ), note: None, + suggested_replacement: None, }], ); } else { @@ -60,6 +61,7 @@ impl<'ctx> Resolver<'ctx> { file ), note: None, + suggested_replacement: None, }], ); } diff --git a/kclvm/sema/src/resolver/node.rs b/kclvm/sema/src/resolver/node.rs index 555a90fe1..7542f4be4 100644 --- a/kclvm/sema/src/resolver/node.rs +++ b/kclvm/sema/src/resolver/node.rs @@ -215,6 +215,7 @@ impl<'ctx> MutSelfTypedResultWalker<'ctx> for Resolver<'ctx> { style: Style::LineAndColumn, message: format!("Immutable variable '{}' is modified during compiling", name), note: None, + suggested_replacement: None, }]; if let Some(pos) = self.get_global_name_pos(name) { msgs.push(Message { @@ -225,6 +226,7 @@ impl<'ctx> MutSelfTypedResultWalker<'ctx> for Resolver<'ctx> { "change the variable name to '_{}' to make it mutable", name )), + suggested_replacement: None, }) } self.handler.add_error(ErrorKind::ImmutableError, &msgs); diff --git a/kclvm/sema/src/resolver/para.rs b/kclvm/sema/src/resolver/para.rs index 6a213d8f3..61c79d055 100644 --- a/kclvm/sema/src/resolver/para.rs +++ b/kclvm/sema/src/resolver/para.rs @@ -22,6 +22,7 @@ impl<'ctx> Resolver<'ctx> { message: "non-default argument follows default argument" .to_string(), note: Some("A default argument".to_string()), + suggested_replacement: None, }], ); } diff --git a/kclvm/sema/src/resolver/schema.rs b/kclvm/sema/src/resolver/schema.rs index ae8050c5b..f89ec807f 100644 --- a/kclvm/sema/src/resolver/schema.rs +++ b/kclvm/sema/src/resolver/schema.rs @@ -32,6 +32,7 @@ impl<'ctx> Resolver<'ctx> { style: Style::LineAndColumn, message: format!("expected schema type, got {}", ty.ty_str()), note: None, + suggested_replacement: None, }], ); return ty; @@ -123,6 +124,7 @@ impl<'ctx> Resolver<'ctx> { style: Style::LineAndColumn, message: format!("expected rule type, got {}", ty.ty_str()), note: None, + suggested_replacement: None, }], ); return ty; diff --git a/kclvm/sema/src/resolver/tests.rs b/kclvm/sema/src/resolver/tests.rs index 0bf7678ec..5efbaf7b0 100644 --- a/kclvm/sema/src/resolver/tests.rs +++ b/kclvm/sema/src/resolver/tests.rs @@ -286,6 +286,7 @@ fn test_lint() { style: Style::Line, message: format!("Importstmt should be placed at the top of the module"), note: Some("Consider moving tihs statement to the top of the file".to_string()), + suggested_replacement: None, }], ); handler.add_warning( @@ -306,6 +307,7 @@ fn test_lint() { style: Style::Line, message: format!("Module 'a' is reimported multiple times"), note: Some("Consider removing this statement".to_string()), + suggested_replacement: Some("".to_string()), }], ); handler.add_warning( @@ -326,6 +328,7 @@ fn test_lint() { style: Style::Line, message: format!("Module 'import_test.a' imported but unused"), note: Some("Consider removing this statement".to_string()), + suggested_replacement: Some("".to_string()), }], ); for (d1, d2) in resolver diff --git a/kclvm/sema/src/resolver/ty.rs b/kclvm/sema/src/resolver/ty.rs index c7504d6ab..bc2b1956a 100644 --- a/kclvm/sema/src/resolver/ty.rs +++ b/kclvm/sema/src/resolver/ty.rs @@ -98,6 +98,7 @@ impl<'ctx> Resolver<'ctx> { style: Style::LineAndColumn, message: format!("expected {}, got {}", expected_ty.ty_str(), ty.ty_str(),), note: None, + suggested_replacement: None, }]; if let Some(expected_pos) = expected_pos { @@ -110,6 +111,7 @@ impl<'ctx> Resolver<'ctx> { ty.ty_str(), ), note: None, + suggested_replacement: None, }); } self.handler.add_error(ErrorKind::TypeError, &msgs); diff --git a/kclvm/sema/src/resolver/var.rs b/kclvm/sema/src/resolver/var.rs index 028ace36c..8cd943f11 100644 --- a/kclvm/sema/src/resolver/var.rs +++ b/kclvm/sema/src/resolver/var.rs @@ -127,6 +127,7 @@ impl<'ctx> Resolver<'ctx> { style: Style::LineAndColumn, message: format!("Unique key error name '{}'", name), note: None, + suggested_replacement: None, }]; if let Some(pos) = self.get_global_name_pos(name) { msgs.push(Message { @@ -134,6 +135,7 @@ impl<'ctx> Resolver<'ctx> { style: Style::LineAndColumn, message: format!("The variable '{}' is declared here", name), note: None, + suggested_replacement: None, }); } self.handler.add_error(ErrorKind::UniqueKeyError, &msgs); diff --git a/kclvm/tools/src/fix/mod.rs b/kclvm/tools/src/fix/mod.rs new file mode 100644 index 000000000..a9ce3ee75 --- /dev/null +++ b/kclvm/tools/src/fix/mod.rs @@ -0,0 +1,134 @@ +mod replace; +use anyhow::Error; +use kclvm_error::{diagnostic::Range as KCLRange, Diagnostic}; +use std::collections::HashMap; +use std::fs; +use std::ops::Range; + +pub struct CodeFix { + data: replace::Data, +} + +impl CodeFix { + pub fn new(s: &str) -> CodeFix { + CodeFix { + data: replace::Data::new(s.as_bytes()), + } + } + + pub fn apply(&mut self, suggestion: &Suggestion) -> Result<(), Error> { + let snippet = &suggestion.replacement.snippet; + self.data.replace_range( + snippet.range.start, + snippet.range.end.saturating_sub(1), + suggestion.replacement.replacement.as_bytes(), + )?; + Ok(()) + } + + pub fn finish(&self) -> Result { + Ok(String::from_utf8(self.data.to_vec())?) + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +/// An error/warning and possible solutions for fixing it +pub struct Suggestion { + pub message: String, + pub replacement: Replacement, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Replacement { + pub snippet: Snippet, + pub replacement: String, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Snippet { + pub file_name: String, + pub range: Range, +} + +pub fn diag_to_suggestion( + diag: Diagnostic, + files: &mut HashMap, +) -> Vec { + let mut suggestions = vec![]; + + for msg in &diag.messages { + if let Some(replace) = &msg.suggested_replacement { + let file_name = msg.range.0.filename.clone(); + let src = match files.get(&file_name) { + Some(src) => src.clone(), + None => { + let src = fs::read_to_string(&file_name).unwrap(); + files.insert(file_name, src.clone()); + src + } + }; + suggestions.push(Suggestion { + message: msg.message.clone(), + replacement: Replacement { + snippet: Snippet { + file_name: msg.range.0.filename.clone(), + range: text_range(src.as_str(), &msg.range), + }, + replacement: replace.clone(), + }, + }); + } + } + suggestions +} + +/// Converts the given lsp range to `Range` +pub(crate) fn text_range(text: &str, range: &KCLRange) -> Range { + let mut lines_length = vec![]; + let lines_text: Vec<&str> = text.split('\n').collect(); + let mut pre_total_length = 0; + + for line in &lines_text { + lines_length.push(pre_total_length); + pre_total_length += line.len() + "\n".len(); + } + + let start = + lines_length.get(range.0.line as usize - 1).unwrap() + range.0.column.unwrap_or(0) as usize; + let mut end = + lines_length.get(range.1.line as usize - 1).unwrap() + range.1.column.unwrap_or(0) as usize; + if let Some(ch) = text.chars().nth(end) { + if ch == '\n' { + end += 1; + } + } + Range { start, end } +} + +pub fn fix(diags: Vec) -> Result<(), Error> { + let mut suggestions = vec![]; + let mut source_code = HashMap::new(); + for diag in diags { + suggestions.extend(diag_to_suggestion(diag, &mut source_code)) + } + + let mut files = HashMap::new(); + for suggestion in suggestions { + let file = suggestion.replacement.snippet.file_name.clone(); + files.entry(file).or_insert_with(Vec::new).push(suggestion); + } + + for (source_file, suggestions) in &files { + println!("fix file: {:?}", source_file); + let source = fs::read_to_string(source_file)?; + let mut fix = CodeFix::new(&source); + for suggestion in suggestions.iter() { + if let Err(e) = fix.apply(suggestion) { + eprintln!("Failed to apply suggestion to {}: {}", source_file, e); + } + } + let fixes = fix.finish()?; + fs::write(source_file, fixes)?; + } + Ok(()) +} diff --git a/kclvm/tools/src/fix/replace.rs b/kclvm/tools/src/fix/replace.rs new file mode 100644 index 000000000..9bd1f0031 --- /dev/null +++ b/kclvm/tools/src/fix/replace.rs @@ -0,0 +1,271 @@ +//! A small module giving you a simple container that allows easy and cheap +//! replacement of parts of its content, with the ability to prevent changing +//! the same parts multiple times. + +use anyhow::{anyhow, ensure, Error}; +use std::rc::Rc; + +#[derive(Debug, Clone, PartialEq, Eq)] +enum State { + Initial, + Replaced(Rc<[u8]>), + Inserted(Rc<[u8]>), +} + +impl State { + fn is_inserted(&self) -> bool { + matches!(*self, State::Inserted(..)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct Span { + /// Start of this span in parent data + start: usize, + /// up to end including + end: usize, + data: State, +} + +/// A container that allows easily replacing chunks of its data +#[derive(Debug, Clone, Default)] +pub struct Data { + original: Vec, + parts: Vec, +} + +impl Data { + /// Create a new data container from a slice of bytes + pub fn new(data: &[u8]) -> Self { + Data { + original: data.into(), + parts: vec![Span { + data: State::Initial, + start: 0, + end: data.len().saturating_sub(1), + }], + } + } + + /// Render this data as a vector of bytes + pub fn to_vec(&self) -> Vec { + if self.original.is_empty() { + return Vec::new(); + } + + self.parts.iter().fold(Vec::new(), |mut acc, d| { + match d.data { + State::Initial => acc.extend_from_slice(&self.original[d.start..=d.end]), + State::Replaced(ref d) | State::Inserted(ref d) => acc.extend_from_slice(d), + }; + acc + }) + } + + /// Replace a chunk of data with the given slice, erroring when this part + /// was already changed previously. + pub fn replace_range( + &mut self, + from: usize, + up_to_and_including: usize, + data: &[u8], + ) -> Result<(), Error> { + let exclusive_end = up_to_and_including + 1; + + ensure!( + from <= exclusive_end, + "Invalid range {}...{}, start is larger than end", + from, + up_to_and_including + ); + + ensure!( + up_to_and_including <= self.original.len(), + "Invalid range {}...{} given, original data is only {} byte long", + from, + up_to_and_including, + self.original.len() + ); + + let insert_only = from == exclusive_end; + + // Since we error out when replacing an already replaced chunk of data, + // we can take some shortcuts here. For example, there can be no + // overlapping replacements -- we _always_ split a chunk of 'initial' + // data into three[^empty] parts, and there can't ever be two 'initial' + // parts touching. + // + // [^empty]: Leading and trailing ones might be empty if we replace + // the whole chunk. As an optimization and without loss of generality we + // don't add empty parts. + let new_parts = { + let index_of_part_to_split = self + .parts + .iter() + .position(|p| { + !p.data.is_inserted() && p.start <= from && p.end >= up_to_and_including + }) + .ok_or_else(|| { + anyhow!( + "Could not replace range {}...{} in file \ + -- maybe parts of it were already replaced?", + from, + up_to_and_including + ) + })?; + + let part_to_split = &self.parts[index_of_part_to_split]; + + // If this replacement matches exactly the part that we would + // otherwise split then we ignore this for now. This means that you + // can replace the exact same range with the exact same content + // multiple times and we'll process and allow it. + if part_to_split.start == from && part_to_split.end == up_to_and_including { + if let State::Replaced(ref replacement) = part_to_split.data { + if &**replacement == data { + return Ok(()); + } + } + } + + ensure!( + part_to_split.data == State::Initial, + "Cannot replace slice of data that was already replaced" + ); + + let mut new_parts = Vec::with_capacity(self.parts.len() + 2); + + // Previous parts + if let Some(ps) = self.parts.get(..index_of_part_to_split) { + new_parts.extend_from_slice(ps); + } + + // Keep initial data on left side of part + if from > part_to_split.start { + new_parts.push(Span { + start: part_to_split.start, + end: from.saturating_sub(1), + data: State::Initial, + }); + } + + // New part + new_parts.push(Span { + start: from, + end: up_to_and_including, + data: if insert_only { + State::Inserted(data.into()) + } else { + State::Replaced(data.into()) + }, + }); + + // Keep initial data on right side of part + if up_to_and_including < part_to_split.end { + new_parts.push(Span { + start: up_to_and_including + 1, + end: part_to_split.end, + data: State::Initial, + }); + } + + // Following parts + if let Some(ps) = self.parts.get(index_of_part_to_split + 1..) { + new_parts.extend_from_slice(ps); + } + + new_parts + }; + + self.parts = new_parts; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + fn str(i: &[u8]) -> &str { + ::std::str::from_utf8(i).unwrap() + } + + #[test] + fn replace_some_stuff() { + let mut d = Data::new(b"foo bar baz"); + d.replace_range(4, 6, b"lol").unwrap(); + assert_eq!("foo lol baz", str(&d.to_vec())); + } + + #[test] + fn replace_a_single_char() { + let mut d = Data::new(b"let y = true;"); + d.replace_range(4, 4, b"mut y").unwrap(); + assert_eq!("let mut y = true;", str(&d.to_vec())); + } + + #[test] + fn replace_multiple_lines() { + let mut d = Data::new(b"lorem\nipsum\ndolor"); + + d.replace_range(6, 10, b"lol").unwrap(); + assert_eq!("lorem\nlol\ndolor", str(&d.to_vec())); + + d.replace_range(12, 16, b"lol").unwrap(); + assert_eq!("lorem\nlol\nlol", str(&d.to_vec())); + } + + #[test] + fn replace_multiple_lines_with_insert_only() { + let mut d = Data::new(b"foo!"); + + d.replace_range(3, 2, b"bar").unwrap(); + assert_eq!("foobar!", str(&d.to_vec())); + + d.replace_range(0, 2, b"baz").unwrap(); + assert_eq!("bazbar!", str(&d.to_vec())); + + d.replace_range(3, 3, b"?").unwrap(); + assert_eq!("bazbar?", str(&d.to_vec())); + } + + #[test] + fn replace_invalid_range() { + let mut d = Data::new(b"foo!"); + + assert!(d.replace_range(2, 0, b"bar").is_err()); + assert!(d.replace_range(0, 2, b"bar").is_ok()); + } + + #[test] + fn empty_to_vec_roundtrip() { + let s = ""; + assert_eq!(s.as_bytes(), Data::new(s.as_bytes()).to_vec().as_slice()); + } + + #[test] + #[should_panic(expected = "Cannot replace slice of data that was already replaced")] + fn replace_overlapping_stuff_errs() { + let mut d = Data::new(b"foo bar baz"); + + d.replace_range(4, 6, b"lol").unwrap(); + assert_eq!("foo lol baz", str(&d.to_vec())); + + d.replace_range(4, 6, b"lol2").unwrap(); + } + + #[test] + #[should_panic(expected = "original data is only 3 byte long")] + fn broken_replacements() { + let mut d = Data::new(b"foo"); + d.replace_range(4, 7, b"lol").unwrap(); + } + + #[test] + fn replace_same_twice() { + let mut d = Data::new(b"foo"); + d.replace_range(0, 0, b"b").unwrap(); + d.replace_range(0, 0, b"b").unwrap(); + assert_eq!("boo", str(&d.to_vec())); + } +} diff --git a/kclvm/tools/src/lib.rs b/kclvm/tools/src/lib.rs index 6ec98222f..fc86da6cc 100644 --- a/kclvm/tools/src/lib.rs +++ b/kclvm/tools/src/lib.rs @@ -1,3 +1,4 @@ +pub mod fix; pub mod format; pub mod lint; pub mod util;