diff --git a/source/language_service/src/code_action.rs b/source/language_service/src/code_action.rs index 8d6f6f241d..9ca47a4aa5 100644 --- a/source/language_service/src/code_action.rs +++ b/source/language_service/src/code_action.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. mod auto_import; +mod int_to_double; mod wrap_in_array; mod wrapper_refactor; @@ -48,6 +49,12 @@ pub(crate) fn get_code_actions( span, position_encoding, )); + actions.extend(int_to_double::int_to_double_fixes( + compilation, + source_name, + span, + position_encoding, + )); actions } diff --git a/source/language_service/src/code_action/int_to_double.rs b/source/language_service/src/code_action/int_to_double.rs new file mode 100644 index 0000000000..8d6e6a6f98 --- /dev/null +++ b/source/language_service/src/code_action/int_to_double.rs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Code action: "Convert integer literal to double" +//! Detects when an integer literal is passed where a double is expected and +//! offers to add a trailing `.` to make it into a double literal. + +#[cfg(test)] +mod tests; + +use qsc::{ + Span, + ast::{self, Expr, ExprKind, UnOp}, + compile::{ErrorKind, TyInfoKind}, + hir::ty::Prim, + line_column::Encoding, +}; + +use super::is_error_relevant; +use crate::{ + compilation::Compilation, + protocol::{CodeAction, CodeActionKind, TextEdit, WorkspaceEdit}, + qsc_utils::into_range, +}; + +pub(crate) fn int_to_double_fixes( + compilation: &Compilation, + source_name: &str, + span: Span, + encoding: Encoding, +) -> Vec { + let mut code_actions = Vec::new(); + + let unit = compilation.user_unit(); + let package = &unit.ast.package; + let source_map = &unit.sources; + + let ty_mismatches = compilation + .compile_errors + .iter() + .filter(|error| is_error_relevant(error, span)) + .filter_map(|error| match error.error() { + ErrorKind::Frontend(frontend_error) => frontend_error.ty_mismatch(), + _ => None, + }); + + for (expected, actual, error_span) in ty_mismatches { + // Check if expected is Double and actual is Int. + if matches!(&expected.kind, TyInfoKind::Prim(Prim::Double)) + && matches!(&actual.kind, TyInfoKind::Prim(Prim::Int)) + { + // Confirm that it's a literal and not just some expression of type int + let Some(mut expr) = find_expr_at(package, error_span) else { + continue; + }; + + // Strip off any + or - unary operators + while let ExprKind::UnOp(UnOp::Pos | UnOp::Neg, inner) = expr.kind.as_ref() { + expr = inner; + } + + if !matches!(expr.kind.as_ref(), ExprKind::Lit(_)) { + continue; + } + + // Generate the fix: add a trailing `.` + // Note that this depends on the error span excluding surrounding parens + // so we don't end up with something like `(q).`. + let dot_range = into_range( + encoding, + Span { + lo: error_span.hi, + hi: error_span.hi, + }, + source_map, + ); + + code_actions.push(CodeAction { + title: "Convert to double literal".to_string(), + edit: Some(WorkspaceEdit { + changes: vec![( + source_name.to_string(), + vec![TextEdit { + new_text: ".".to_string(), + range: dot_range, + }], + )], + }), + kind: Some(CodeActionKind::QuickFix), + is_preferred: Some(true), + }); + } + } + + code_actions +} + +/// Finds the AST expression whose span exactly matches `target`. +fn find_expr_at(package: &ast::Package, target: Span) -> Option<&ast::Expr> { + let mut finder = ExprSpanFinder { + target, + found: None, + }; + ast::visit::Visitor::visit_package(&mut finder, package); + finder.found +} + +struct ExprSpanFinder<'a> { + target: Span, + found: Option<&'a ast::Expr>, +} + +impl<'a> ast::visit::Visitor<'a> for ExprSpanFinder<'a> { + fn visit_expr(&mut self, expr: &'a Expr) { + if expr.span == self.target { + self.found = Some(expr); + } else if self.target.intersection(&expr.span).is_some() { + ast::visit::walk_expr(self, expr); + } + } +} diff --git a/source/language_service/src/code_action/int_to_double/tests.rs b/source/language_service/src/code_action/int_to_double/tests.rs new file mode 100644 index 0000000000..7225404f26 --- /dev/null +++ b/source/language_service/src/code_action/int_to_double/tests.rs @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::{ + code_action, + test_utils::{compile_project_with_markers_no_cursor, whole_document_range}, +}; +use qsc::{line_column::Encoding, location::Location}; + +fn expect_single(items: &[T]) -> &T { + let [item] = items else { + panic!("expected a single item, got: {items:?}"); + }; + item +} + +fn get_int_to_double_actions(source: &str) -> (Vec, Vec) { + let (compilation, targets) = + compile_project_with_markers_no_cursor(&[("", source)], false); + let range = whole_document_range(source); + let actions = code_action::get_code_actions(&compilation, "", range, Encoding::Utf8); + ( + targets, + actions + .into_iter() + .filter(|a| a.title == "Convert to double literal") + .collect(), + ) +} + +#[test] +fn int_literal_to_double() { + let source = "namespace A { + function Foo(d: Double) : Unit { + Foo(1◉◉); + } +} +"; + let (locations, actions) = get_int_to_double_actions(source); + let action = expect_single(&actions); + let edit = action.edit.as_ref().expect("expected edit"); + let (_, text_edits) = expect_single(&edit.changes); + let text_edit = expect_single(text_edits); + let location = expect_single(&locations); + assert_eq!(text_edit.range, location.range); + assert_eq!(text_edit.new_text, "."); +} + +#[test] +fn int_literal_to_double_with_parens() { + let source = "namespace A { + function Foo(d: Double) : Unit { + Foo((1◉◉)); + } +} +"; + let (locations, actions) = get_int_to_double_actions(source); + let action = expect_single(&actions); + let edit = action.edit.as_ref().expect("expected edit"); + let (_, text_edits) = expect_single(&edit.changes); + let text_edit = expect_single(text_edits); + let location = expect_single(&locations); + assert_eq!(text_edit.range, location.range); + assert_eq!(text_edit.new_text, "."); +} + +#[test] +fn int_literal_to_double_with_pos() { + let source = "namespace A { + function Foo(d: Double) : Unit { + Foo((+1◉◉)); + } +} +"; + let (locations, actions) = get_int_to_double_actions(source); + let action = expect_single(&actions); + let edit = action.edit.as_ref().expect("expected edit"); + let (_, text_edits) = expect_single(&edit.changes); + let text_edit = expect_single(text_edits); + let location = expect_single(&locations); + assert_eq!(text_edit.range, location.range); + assert_eq!(text_edit.new_text, "."); +} + +#[test] +fn int_literal_to_double_with_neg() { + let source = "namespace A { + function Foo(d: Double) : Unit { + Foo((-1◉◉)); + } +} +"; + let (locations, actions) = get_int_to_double_actions(source); + let action = expect_single(&actions); + let edit = action.edit.as_ref().expect("expected edit"); + let (_, text_edits) = expect_single(&edit.changes); + let text_edit = expect_single(text_edits); + let location = expect_single(&locations); + assert_eq!(text_edit.range, location.range); + assert_eq!(text_edit.new_text, "."); +} + +#[test] +fn int_literal_to_double_with_neg_neg() { + let source = "namespace A { + function Foo(d: Double) : Unit { + Foo((--1◉◉)); + } +} +"; + let (locations, actions) = get_int_to_double_actions(source); + let action = expect_single(&actions); + let edit = action.edit.as_ref().expect("expected edit"); + let (_, text_edits) = expect_single(&edit.changes); + let text_edit = expect_single(text_edits); + let location = expect_single(&locations); + assert_eq!(text_edit.range, location.range); + assert_eq!(text_edit.new_text, "."); +} + +#[test] +fn int_literal_to_double_with_notb() { + let source = "namespace A { + function Foo(d: Double) : Unit { + Foo((~~~1)); + } +} +"; + let (_, actions) = get_int_to_double_actions(source); + assert_eq!(actions.len(), 0, "Expected 0 actions, got: {actions:?}"); +} + +#[test] +fn int_local_to_double() { + let source = "namespace A { + function Foo(d: Double) : Unit { + let x = 1; + Foo((x)); + } +} +"; + let (_, actions) = get_int_to_double_actions(source); + assert_eq!(actions.len(), 0, "Expected 0 actions, got: {actions:?}"); +} + +// We'd like this to work but the TyMismatch flags the whole array, not the elements +#[test] +fn int_array_to_double() { + let source = "namespace A { + function Foo(d: Double[]) : Unit { + Foo([1,2,3]); + } +} +"; + let (_, actions) = get_int_to_double_actions(source); + assert_eq!(actions.len(), 0, "Expected 0 actions, got: {actions:?}"); +} diff --git a/source/language_service/src/code_action/wrap_in_array/tests.rs b/source/language_service/src/code_action/wrap_in_array/tests.rs index fe94a878aa..72dfb6db1b 100644 --- a/source/language_service/src/code_action/wrap_in_array/tests.rs +++ b/source/language_service/src/code_action/wrap_in_array/tests.rs @@ -5,17 +5,20 @@ use crate::{ code_action, test_utils::{compile_project_with_markers_no_cursor, whole_document_range}, }; -use qsc::line_column::Encoding; +use qsc::{line_column::Encoding, location::Location}; -fn get_wrap_in_array_actions(source: &str) -> Vec { - let (compilation, _targets) = +fn get_wrap_in_array_actions(source: &str) -> (Vec, Vec) { + let (compilation, targets) = compile_project_with_markers_no_cursor(&[("", source)], false); let range = whole_document_range(source); let actions = code_action::get_code_actions(&compilation, "", range, Encoding::Utf8); - actions - .into_iter() - .filter(|a| a.title == "Convert to single-element array") - .collect() + ( + targets, + actions + .into_iter() + .filter(|a| a.title == "Convert to single-element array") + .collect(), + ) } #[test] @@ -23,18 +26,20 @@ fn single_arg_qubit_to_qubit_array() { let source = "namespace A { operation Foo(qs: Qubit[]) : Unit is Adj { use q = Qubit(); - Foo(q); + Foo(◉◉q◉◉); } } "; - let actions = get_wrap_in_array_actions(source); + let (locations, actions) = get_wrap_in_array_actions(source); assert_eq!(actions.len(), 1, "Expected 1 action, got: {actions:?}"); let action = &actions[0]; let edit = action.edit.as_ref().expect("expected edit"); let (_, text_edits) = &edit.changes[0]; - assert_eq!(text_edits.len(), 2); + assert_eq!(text_edits.len(), locations.len()); assert_eq!(text_edits[0].new_text, "["); + assert_eq!(text_edits[0].range, locations[0].range); assert_eq!(text_edits[1].new_text, "]"); + assert_eq!(text_edits[1].range, locations[1].range); } #[test] @@ -42,18 +47,20 @@ fn multi_arg_second_param_is_array() { let source = "namespace A { operation Bar(x: Int, qs: Qubit[]) : Unit { use q = Qubit(); - Bar(1, q); + Bar(1, ◉◉q◉◉); } } "; - let actions = get_wrap_in_array_actions(source); + let (locations, actions) = get_wrap_in_array_actions(source); assert_eq!(actions.len(), 1, "Expected 1 action, got: {actions:?}"); let action = &actions[0]; let edit = action.edit.as_ref().expect("expected edit"); let (_, text_edits) = &edit.changes[0]; - assert_eq!(text_edits.len(), 2); + assert_eq!(text_edits.len(), locations.len()); assert_eq!(text_edits[0].new_text, "["); + assert_eq!(text_edits[0].range, locations[0].range); assert_eq!(text_edits[1].new_text, "]"); + assert_eq!(text_edits[1].range, locations[1].range); } #[test] @@ -65,7 +72,7 @@ fn no_action_when_types_already_match() { } } "; - let actions = get_wrap_in_array_actions(source); + let (_, actions) = get_wrap_in_array_actions(source); assert!(actions.is_empty(), "Expected no actions, got: {actions:?}"); } @@ -79,7 +86,7 @@ fn no_action_for_unrelated_mismatch() { } } "; - let actions = get_wrap_in_array_actions(source); + let (_, actions) = get_wrap_in_array_actions(source); assert!(actions.is_empty(), "Expected no actions, got: {actions:?}"); } @@ -94,7 +101,7 @@ fn no_action_for_tuple_to_tuple_array() { } } "; - let actions = get_wrap_in_array_actions(source); + let (_, actions) = get_wrap_in_array_actions(source); assert!(actions.is_empty(), "Expected no actions, got: {actions:?}"); } @@ -109,7 +116,7 @@ fn no_action_for_array_to_nested_array() { } } "; - let actions = get_wrap_in_array_actions(source); + let (_, actions) = get_wrap_in_array_actions(source); assert!(actions.is_empty(), "Expected no actions, got: {actions:?}"); } @@ -124,7 +131,7 @@ fn no_action_for_arrow_to_arrow_array() { } } "; - let actions = get_wrap_in_array_actions(source); + let (_, actions) = get_wrap_in_array_actions(source); assert!(actions.is_empty(), "Expected no actions, got: {actions:?}"); } @@ -138,7 +145,7 @@ fn no_action_for_param_to_param_array() { } } "; - let actions = get_wrap_in_array_actions(source); + let (_, actions) = get_wrap_in_array_actions(source); assert!(actions.is_empty(), "Expected no actions, got: {actions:?}"); } @@ -153,6 +160,6 @@ fn no_action_for_udt_to_udt_array() { } } "; - let actions = get_wrap_in_array_actions(source); + let (_, actions) = get_wrap_in_array_actions(source); assert!(actions.is_empty(), "Expected no actions, got: {actions:?}"); }