diff --git a/kclvm/error/src/lib.rs b/kclvm/error/src/lib.rs index ce25667d0..36fabb3ce 100644 --- a/kclvm/error/src/lib.rs +++ b/kclvm/error/src/lib.rs @@ -337,9 +337,16 @@ pub enum ParseError { Message { message: String, span: Span, + fix_info: Option, }, } +#[derive(Debug, Clone)] +pub struct FixInfo { + pub suggestion: Option, + pub replacement: Option, +} + /// A single string error. pub struct StringError(pub String); @@ -357,13 +364,17 @@ impl ParseError { } /// New a message parse error with span. - pub fn message(message: String, span: Span) -> Self { - ParseError::Message { message, span } + pub fn message(message: String, span: Span, fix_info: Option) -> Self { + ParseError::Message { + message, + span, + fix_info, + } } } impl ParseError { - /// Convert a parse error into a error diagnostic. + /// Convert a parse error into an error diagnostic. pub fn into_diag(self, sess: &Session) -> Result { let span = match self { ParseError::UnexpectedToken { span, .. } => span, @@ -371,15 +382,122 @@ impl ParseError { }; let loc = sess.sm.lookup_char_pos(span.lo()); let pos: Position = loc.into(); + let suggestions = match self { + ParseError::Message { + fix_info: Some(ref info), + .. + } => Some(vec![ + info.suggestion + .clone() + .unwrap_or_else(|| "No suggestion available".to_string()), + info.replacement.clone().unwrap_or_else(|| " ".to_string()), + ]), + _ => None, + }; + + let (start_pos, end_pos) = self.generate_modified_range(&self.to_string(), &pos); + Ok(Diagnostic::new_with_code( Level::Error, &self.to_string(), None, - (pos.clone(), pos), + (start_pos, end_pos), Some(DiagnosticId::Error(ErrorKind::InvalidSyntax)), - None, + suggestions, )) } + + fn generate_modified_range(&self, msg: &str, pos: &Position) -> (Position, Position) { + match msg { + "invalid token '!', consider using 'not '" => { + let start_column = pos.column.unwrap_or(0); + let end_column = start_column + 1; + ( + Position { + column: Some(start_column), + ..pos.clone() + }, + Position { + column: Some(end_column), + ..pos.clone() + }, + ) + } + "'else if' here is invalid in KCL, consider using the 'elif' keyword" => { + let start_column = pos.column.map(|col| col.saturating_sub(5)).unwrap_or(0); + let end_column = pos.column.map(|col| col.saturating_add(2)).unwrap_or(0); + ( + Position { + column: Some(start_column), + ..pos.clone() + }, + Position { + column: Some(end_column), + ..pos.clone() + }, + ) + } + "error nesting on close paren" + | "mismatched closing delimiter" + | "error nesting on close brace" => { + let start_column = pos.column.unwrap_or(0); + let end_column = start_column + 1; + ( + Position { + column: Some(start_column), + ..pos.clone() + }, + Position { + column: Some(end_column), + ..pos.clone() + }, + ) + } + "unterminated string" => { + let start_column = pos.column.unwrap_or(0); + let end_column = start_column + 1; + ( + Position { + column: Some(start_column), + ..pos.clone() + }, + Position { + column: Some(end_column), + ..pos.clone() + }, + ) + } + "unexpected character after line continuation character" => { + let start_column = pos.column.unwrap_or(0); + let end_column = u32::MAX; + ( + Position { + column: Some(start_column), + ..pos.clone() + }, + Position { + column: Some(end_column.into()), + ..pos.clone() + }, + ) + } + "the semicolon ';' here is unnecessary, please remove it" => { + let start_column = pos.column.unwrap_or(0); + let end_column = start_column + 1; + ( + Position { + column: Some(start_column), + ..pos.clone() + }, + Position { + column: Some(end_column), + ..pos.clone() + }, + ) + } + _ => (pos.clone(), pos.clone()), + } + } } impl ToString for ParseError { @@ -405,7 +523,11 @@ impl SessionDiagnostic for ParseError { diag.append_component(Box::new(format!(" {}\n", self.to_string()))); Ok(diag) } - ParseError::Message { message, span } => { + ParseError::Message { + message, + span, + fix_info: _, + } => { let code_snippet = CodeSnippet::new(span, Arc::clone(&sess.sm)); diag.append_component(Box::new(code_snippet)); diag.append_component(Box::new(format!(" {message}\n"))); diff --git a/kclvm/parser/src/lexer/mod.rs b/kclvm/parser/src/lexer/mod.rs index 187c124b4..f175c7901 100644 --- a/kclvm/parser/src/lexer/mod.rs +++ b/kclvm/parser/src/lexer/mod.rs @@ -258,9 +258,11 @@ impl<'a> Lexer<'a> { // Unary op kclvm_lexer::TokenKind::Tilde => token::UnaryOp(token::UTilde), kclvm_lexer::TokenKind::Bang => { - self.sess.struct_span_error( + self.sess.struct_span_error_with_suggestions( "invalid token '!', consider using 'not'", self.span(start, self.pos), + Some("Replace '!' with 'not'".to_string()), + Some("not ".to_string()), ); token::UnaryOp(token::UNot) } @@ -324,17 +326,21 @@ impl<'a> Lexer<'a> { token::OpenDelim(token::Paren) => token::CloseDelim(token::Paren), // error recovery token::OpenDelim(token::Brace) => { - self.sess.struct_span_error( + self.sess.struct_span_error_with_suggestions( "error nesting on close paren", self.span(start, self.pos), + Some("Replace with '}'".to_string()), + Some("}".to_string()), ); token::CloseDelim(token::Brace) } // error recovery token::OpenDelim(token::Bracket) => { - self.sess.struct_span_error( + self.sess.struct_span_error_with_suggestions( "error nesting on close paren", self.span(start, self.pos), + Some("Replace with ']'".to_string()), + Some("]".to_string()), ); token::CloseDelim(token::Bracket) } @@ -343,9 +349,11 @@ impl<'a> Lexer<'a> { }, // error recovery None => { - self.sess.struct_span_error( + self.sess.struct_span_error_with_suggestions( "error nesting on close paren", self.span(start, self.pos), + Some("Insert ')'".to_string()), + Some(")".to_string()), ); token::CloseDelim(token::Paren) } @@ -361,17 +369,21 @@ impl<'a> Lexer<'a> { token::OpenDelim(token::Brace) => token::CloseDelim(token::Brace), // error recovery token::OpenDelim(token::Paren) => { - self.sess.struct_span_error( + self.sess.struct_span_error_with_suggestions( "error nesting on close brace", self.span(start, self.pos), + Some("Replace with ')'".to_string()), + Some(")".to_string()), ); token::CloseDelim(token::Paren) } // error recovery token::OpenDelim(token::Bracket) => { - self.sess.struct_span_error( + self.sess.struct_span_error_with_suggestions( "error nesting on close brace", self.span(start, self.pos), + Some("Replace with ']'".to_string()), + Some("]".to_string()), ); token::CloseDelim(token::Bracket) } @@ -380,9 +392,11 @@ impl<'a> Lexer<'a> { }, // error recovery None => { - self.sess.struct_span_error( + self.sess.struct_span_error_with_suggestions( "error nesting on close brace", self.span(start, self.pos), + Some("Insert '}'".to_string()), + Some("}".to_string()), ); token::CloseDelim(token::Brace) } @@ -400,17 +414,21 @@ impl<'a> Lexer<'a> { token::OpenDelim(token::Bracket) => token::CloseDelim(token::Bracket), // error recovery token::OpenDelim(token::Brace) => { - self.sess.struct_span_error( + self.sess.struct_span_error_with_suggestions( "mismatched closing delimiter", self.span(start, self.pos), + Some("Replace with '}'".to_string()), + Some("}".to_string()), ); token::CloseDelim(token::Brace) } // error recovery token::OpenDelim(token::Paren) => { - self.sess.struct_span_error( + self.sess.struct_span_error_with_suggestions( "mismatched closing delimiter", self.span(start, self.pos), + Some("Replace with ')'".to_string()), + Some(")".to_string()), ); token::CloseDelim(token::Paren) } @@ -419,9 +437,11 @@ impl<'a> Lexer<'a> { }, // error recovery None => { - self.sess.struct_span_error( + self.sess.struct_span_error_with_suggestions( "mismatched closing delimiter", self.span(start, self.pos), + Some("Insert ']'".to_string()), + Some("]".to_string()), ); token::CloseDelim(token::Bracket) } @@ -430,23 +450,31 @@ impl<'a> Lexer<'a> { kclvm_lexer::TokenKind::InvalidLineContinue => { // If we encounter an illegal line continuation character, // we will restore it to a normal line continuation character. - self.sess.struct_span_error( + self.sess.struct_span_error_with_suggestions( "unexpected character after line continuation character", self.span(start, self.pos), + Some("Replace with '\\'".to_string()), + Some("\\".to_string()), ); return None; } kclvm_lexer::TokenKind::Semi => { // If we encounter an illegal semi token ';', raise a friendly error. - self.sess.struct_span_error( + self.sess.struct_span_error_with_suggestions( "the semicolon ';' here is unnecessary, please remove it", self.span(start, self.pos), + Some("Remove ';'".to_string()), + Some(" ".to_string()), ); return None; } _ => { - self.sess - .struct_span_error("unknown start of token", self.span(start, self.pos)); + self.sess.struct_span_error_with_suggestions( + "unknown start of token", + self.span(start, self.pos), + Some("Remove unknown token".to_string()), + Some("".to_string()), + ); return None; } }) @@ -503,10 +531,12 @@ impl<'a> Lexer<'a> { _ => (false, start, start_char), }; if !terminated { - self.sess.struct_span_error( + self.sess.struct_span_error_with_suggestions( "unterminated string", self.span(quote_char_pos, self.pos), - ) + Some("Close the string with matching quote".to_string()), + Some("\"".to_string()), + ); } // Cut offset before validation. let offset: u32 = if triple_quoted { @@ -534,9 +564,11 @@ impl<'a> Lexer<'a> { let value = if content_start > content_end { // If get an error string from the eval process, // directly return an empty string. - self.sess.struct_span_error( + self.sess.struct_span_error_with_suggestions( "invalid string syntax", self.span(content_start, self.pos), + Some("Correct the string syntax".to_string()), + Some("\"\"".to_string()), ); "".to_string() } else { @@ -547,9 +579,11 @@ impl<'a> Lexer<'a> { None => { // If get an error string from the eval process, // directly return an empty string. - self.sess.struct_span_error( + self.sess.struct_span_error_with_suggestions( "invalid string syntax", self.span(content_start, self.pos), + Some("Correct the string syntax".to_string()), + Some("\"\"".to_string()), ); "".to_string() } diff --git a/kclvm/parser/src/parser/stmt.rs b/kclvm/parser/src/parser/stmt.rs index 377c4c2dd..434bb0ce4 100644 --- a/kclvm/parser/src/parser/stmt.rs +++ b/kclvm/parser/src/parser/stmt.rs @@ -599,9 +599,11 @@ impl<'a> Parser<'a> { // `else if -> elif` error recovery. if self.token.is_keyword(kw::If) { - self.sess.struct_span_error( + self.sess.struct_span_error_with_suggestions( "'else if' here is invalid in KCL, consider using the 'elif' keyword", self.token.span, + Some("Use 'elif' instead of 'else if'".to_string()), + Some("elif".to_string()), ); } else if self.token.kind != TokenKind::Colon { self.sess diff --git a/kclvm/parser/src/session/mod.rs b/kclvm/parser/src/session/mod.rs index 3d277b52d..872830b0f 100644 --- a/kclvm/parser/src/session/mod.rs +++ b/kclvm/parser/src/session/mod.rs @@ -3,7 +3,7 @@ use compiler_base_macros::bug; use compiler_base_session::Session; use indexmap::IndexSet; use kclvm_ast::token::Token; -use kclvm_error::{Diagnostic, Handler, ParseError}; +use kclvm_error::{Diagnostic, FixInfo, Handler, ParseError}; use kclvm_span::{BytePos, Loc, Span}; use std::{cell::RefCell, sync::Arc}; @@ -65,6 +65,27 @@ impl ParseSession { self.add_parse_err(ParseError::Message { message: msg.to_string(), span, + fix_info: None, + }); + } + + #[inline] + pub fn struct_span_error_with_suggestions( + &self, + msg: &str, + span: Span, + suggestion_text: Option, + replacement_text: Option, + ) { + let fix_info = Some(FixInfo { + suggestion: suggestion_text, + replacement: replacement_text, + }); + + self.add_parse_err(ParseError::Message { + message: msg.to_string(), + span, + fix_info, }); } diff --git a/kclvm/tools/src/LSP/src/quick_fix.rs b/kclvm/tools/src/LSP/src/quick_fix.rs index 7503e1d1f..3eda09184 100644 --- a/kclvm/tools/src/LSP/src/quick_fix.rs +++ b/kclvm/tools/src/LSP/src/quick_fix.rs @@ -46,6 +46,34 @@ pub(crate) fn quick_fix(uri: &Url, diags: &[Diagnostic]) -> Vec { + let replacement_texts = extract_suggested_replacements(&diag.data); + if replacement_texts.len() >= 2 { + let title = &replacement_texts[0]; + let replacement_text = &replacement_texts[1]; + + let mut changes = HashMap::new(); + + changes.insert( + uri.clone(), + vec![TextEdit { + range: diag.range, + new_text: replacement_text.clone(), + }], + ); + + code_actions.push(CodeActionOrCommand::CodeAction(CodeAction { + title: title.clone(), + kind: Some(CodeActionKind::QUICKFIX), + diagnostics: Some(vec![diag.clone()]), + edit: Some(lsp_types::WorkspaceEdit { + changes: Some(changes), + ..Default::default() + }), + ..Default::default() + })); + } + } _ => continue, }, DiagnosticId::Warning(warn) => match warn { @@ -123,6 +151,7 @@ pub(crate) fn convert_code_to_kcl_diag_id(code: &NumberOrString) -> Option Some(DiagnosticId::Warning(WarningKind::UnusedImportWarning)), "ReimportWarning" => Some(DiagnosticId::Warning(WarningKind::ReimportWarning)), "CompileError" => Some(DiagnosticId::Error(ErrorKind::CompileError)), + "InvalidSyntax" => Some(DiagnosticId::Error(ErrorKind::InvalidSyntax)), "ImportPositionWarning" => { Some(DiagnosticId::Warning(WarningKind::ImportPositionWarning)) }