Skip to content

Commit 0d3af2d

Browse files
committed
add action to unwrap a trivial anonymous function
1 parent 50cbf19 commit 0d3af2d

File tree

4 files changed

+194
-5
lines changed

4 files changed

+194
-5
lines changed

compiler-core/src/language_server/code_action.rs

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3518,7 +3518,7 @@ impl VariablesNames {
35183518
}
35193519
variables
35203520
}
3521-
3521+
35223522
fn from_expr(expr: &TypedExpr) -> Self {
35233523
let mut variables = Self {
35243524
names: HashSet::new(),
@@ -8288,3 +8288,107 @@ impl<'ast> ast::visit::Visit<'ast> for WrapInAnonymousFunction<'ast> {
82888288
}
82898289
}
82908290
}
8291+
8292+
pub struct UnwrapAnonymousFunction<'a> {
8293+
module: &'a Module,
8294+
line_numbers: &'a LineNumbers,
8295+
params: &'a CodeActionParams,
8296+
targets: Vec<FnToUnwrap>,
8297+
}
8298+
8299+
struct FnToUnwrap {
8300+
fn_location: SrcSpan,
8301+
inner_function_location: SrcSpan,
8302+
}
8303+
8304+
impl<'a> UnwrapAnonymousFunction<'a> {
8305+
pub fn new(
8306+
module: &'a Module,
8307+
line_numbers: &'a LineNumbers,
8308+
params: &'a CodeActionParams,
8309+
) -> Self {
8310+
Self {
8311+
module,
8312+
line_numbers,
8313+
params,
8314+
targets: vec![],
8315+
}
8316+
}
8317+
8318+
pub fn code_actions(mut self) -> Vec<CodeAction> {
8319+
self.visit_typed_module(&self.module.ast);
8320+
8321+
let mut actions = Vec::with_capacity(self.targets.len());
8322+
for target in self.targets {
8323+
let mut edits = TextEdits::new(self.line_numbers);
8324+
8325+
edits.delete(SrcSpan {
8326+
start: target.fn_location.start,
8327+
end: target.inner_function_location.start,
8328+
});
8329+
edits.delete(SrcSpan {
8330+
start: target.inner_function_location.end,
8331+
end: target.fn_location.end,
8332+
});
8333+
8334+
CodeActionBuilder::new("Unwrap anonymous function")
8335+
.kind(CodeActionKind::REFACTOR_REWRITE)
8336+
.changes(self.params.text_document.uri.clone(), edits.edits)
8337+
.push_to(&mut actions);
8338+
}
8339+
actions
8340+
}
8341+
}
8342+
8343+
impl<'ast> ast::visit::Visit<'ast> for UnwrapAnonymousFunction<'ast> {
8344+
fn visit_typed_expr_fn(
8345+
&mut self,
8346+
location: &'ast SrcSpan,
8347+
_type_: &'ast Arc<Type>,
8348+
kind: &'ast FunctionLiteralKind,
8349+
arguments: &'ast [TypedArg],
8350+
body: &'ast Vec1<TypedStatement>,
8351+
_return_annotation: &'ast Option<ast::TypeAst>,
8352+
) {
8353+
match kind {
8354+
FunctionLiteralKind::Anonymous { .. } => (),
8355+
_ => return,
8356+
}
8357+
8358+
// We need the existing argument list for the fn to be a 1:1 match for the args we pass
8359+
// to the called function. We figure out what the call-arg list needs to look like here,
8360+
// and bail out if any incoming args are discarded.
8361+
let mut expected_arguments = Vec::with_capacity(arguments.len());
8362+
for a in arguments {
8363+
match &a.names {
8364+
ArgNames::Named { name, .. } => expected_arguments.push(name),
8365+
ArgNames::NamedLabelled { name, .. } => expected_arguments.push(name),
8366+
ArgNames::Discard { .. } => return,
8367+
ArgNames::LabelledDiscard { .. } => return,
8368+
}
8369+
}
8370+
8371+
// match fn bodies with only a single function call
8372+
if let [stmt] = body.as_slice()
8373+
&& let TypedStatement::Expression(expr) = stmt
8374+
&& let TypedExpr::Call { fun, arguments, .. } = expr
8375+
{
8376+
let mut call_arguments = Vec::with_capacity(arguments.len());
8377+
for a in arguments {
8378+
match &a.value {
8379+
TypedExpr::Var { name, .. } => call_arguments.push(name),
8380+
_ => return,
8381+
}
8382+
}
8383+
8384+
if call_arguments != expected_arguments {
8385+
return;
8386+
}
8387+
8388+
self.targets.push(FnToUnwrap {
8389+
fn_location: *location,
8390+
inner_function_location: fun.location(),
8391+
})
8392+
}
8393+
}
8394+
}

compiler-core/src/language_server/engine.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ use super::{
4848
FillInMissingLabelledArgs, FillUnusedFields, FixBinaryOperation,
4949
FixTruncatedBitArraySegment, GenerateDynamicDecoder, GenerateFunction, GenerateJsonEncoder,
5050
GenerateVariant, InlineVariable, InterpolateString, LetAssertToCase, PatternMatchOnValue,
51-
RedundantTupleInCaseSubject, RemoveEchos, RemoveUnusedImports, UseLabelShorthandSyntax,
52-
WrapInAnonymousFunction, WrapInBlock, code_action_add_missing_patterns,
53-
code_action_convert_qualified_constructor_to_unqualified,
51+
RedundantTupleInCaseSubject, RemoveEchos, RemoveUnusedImports, UnwrapAnonymousFunction,
52+
UseLabelShorthandSyntax, WrapInAnonymousFunction, WrapInBlock,
53+
code_action_add_missing_patterns, code_action_convert_qualified_constructor_to_unqualified,
5454
code_action_convert_unqualified_constructor_to_qualified, code_action_import_module,
5555
code_action_inexhaustive_let_to_case,
5656
},
@@ -448,6 +448,7 @@ where
448448
actions.extend(RemoveBlock::new(module, &lines, &params).code_actions());
449449
actions.extend(RemovePrivateOpaque::new(module, &lines, &params).code_actions());
450450
actions.extend(WrapInAnonymousFunction::new(module, &lines, &params).code_actions());
451+
actions.extend(UnwrapAnonymousFunction::new(module, &lines, &params).code_actions());
451452
GenerateDynamicDecoder::new(module, &lines, &params, &mut actions).code_actions();
452453
GenerateJsonEncoder::new(
453454
module,

compiler-core/src/language_server/tests/action.rs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ const REMOVE_OPAQUE_FROM_PRIVATE_TYPE: &str = "Remove opaque from private type";
135135
const COLLAPSE_NESTED_CASE: &str = "Collapse nested case";
136136
const REMOVE_UNREACHABLE_BRANCHES: &str = "Remove unreachable branches";
137137
const WRAP_IN_ANONYMOUS_FUNCTION: &str = "Wrap in anonymous function";
138+
const UNWRAP_ANONYMOUS_FUNCTION: &str = "Unwrap anonymous function";
138139

139140
macro_rules! assert_code_action {
140141
($title:expr, $code:literal, $range:expr $(,)?) => {
@@ -9830,4 +9831,60 @@ fn op_factory(a: Int, b: Int, c: Int) -> fn(Int) -> Int {
98309831
",
98319832
find_position_of("op_factory").to_selection()
98329833
);
9833-
}
9834+
}
9835+
9836+
#[test]
9837+
fn unwrap_trivial_anonymous_function() {
9838+
assert_code_action!(
9839+
UNWRAP_ANONYMOUS_FUNCTION,
9840+
"import gleam/list
9841+
9842+
pub fn main() {
9843+
list.map([1, 2, 3], fn(int) { op(int) })
9844+
}
9845+
9846+
fn op(i: Int) -> Int {
9847+
todo
9848+
}
9849+
",
9850+
find_position_of("fn(int)").to_selection()
9851+
);
9852+
}
9853+
9854+
#[test]
9855+
fn unwrap_anonymous_function_unavailable_when_args_discarded() {
9856+
assert_no_code_actions!(
9857+
UNWRAP_ANONYMOUS_FUNCTION,
9858+
"import gleam/list
9859+
9860+
pub fn main() {
9861+
list.index_map([1, 2, 3], fn(_, int) { op(int) })
9862+
}
9863+
9864+
fn op(i: Int) -> Int {
9865+
todo
9866+
}
9867+
",
9868+
find_position_of("fn(_, int)").to_selection()
9869+
);
9870+
}
9871+
9872+
#[test]
9873+
fn unwrap_anonymous_function_unavailable_with_different_args() {
9874+
assert_no_code_actions!(
9875+
UNWRAP_ANONYMOUS_FUNCTION,
9876+
"import gleam/list
9877+
9878+
const another_int = 7
9879+
9880+
pub fn main() {
9881+
list.map([1, 2, 3], fn(int) { op(another_int) })
9882+
}
9883+
9884+
fn op(i: Int) -> Int {
9885+
todo
9886+
}
9887+
",
9888+
find_position_of("fn(int)").to_selection()
9889+
);
9890+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
source: compiler-core/src/language_server/tests/action.rs
3+
expression: "import gleam/list\n\npub fn main() {\n list.map([1, 2, 3], fn(int) { op(int) })\n}\n\nfn op(i: Int) -> Int {\n todo\n}\n"
4+
---
5+
----- BEFORE ACTION
6+
import gleam/list
7+
8+
pub fn main() {
9+
list.map([1, 2, 3], fn(int) { op(int) })
10+
11+
}
12+
13+
fn op(i: Int) -> Int {
14+
todo
15+
}
16+
17+
18+
----- AFTER ACTION
19+
import gleam/list
20+
21+
pub fn main() {
22+
list.map([1, 2, 3], op)
23+
}
24+
25+
fn op(i: Int) -> Int {
26+
todo
27+
}

0 commit comments

Comments
 (0)