• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

erlang-ls / erlang_ls / 3017

11 Oct 2024 04:55PM UTC coverage: 67.395% (+0.06%) from 67.34%
3017

push

github

web-flow
Improvements to extract function (#1563)

* Ignore variables inside funs() and list comprehensions
* Don't suggest to extract function unless it contains more than one poi

29 of 30 new or added lines in 4 files covered. (96.67%)

1 existing line in 1 file now uncovered.

4752 of 7051 relevant lines covered (67.39%)

13132.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

51.55
/apps/els_lsp/src/els_execute_command_provider.erl
1
-module(els_execute_command_provider).
2

3
-behaviour(els_provider).
4

5
-export([
6
    options/0,
7
    handle_request/1
8
]).
9

10
%%==============================================================================
11
%% Includes
12
%%==============================================================================
13
-include("els_lsp.hrl").
14
-include_lib("kernel/include/logger.hrl").
15

16
%%==============================================================================
17
%% els_provider functions
18
%%==============================================================================
19
-spec options() -> map().
20
options() ->
21
    Commands = [
607✔
22
        <<"server-info">>,
23
        <<"ct-run-test">>,
24
        <<"show-behaviour-usages">>,
25
        <<"suggest-spec">>,
26
        <<"function-references">>,
27
        <<"refactor.extract">>,
28
        <<"add-behaviour-callbacks">>,
29
        <<"bump-variables">>
30
    ],
31
    #{
607✔
32
        commands => [
33
            els_command:with_prefix(Cmd)
4,856✔
34
         || Cmd <- Commands ++ wrangler_handler:enabled_commands()
607✔
35
        ]
36
    }.
37

38
-spec handle_request(any()) -> {response, any()}.
39
handle_request({workspace_executecommand, Params}) ->
40
    #{<<"command">> := PrefixedCommand} = Params,
7✔
41
    Arguments = maps:get(<<"arguments">>, Params, []),
7✔
42
    Result = execute_command(
7✔
43
        els_command:without_prefix(PrefixedCommand),
44
        Arguments
45
    ),
46
    {response, Result}.
7✔
47

48
%%==============================================================================
49
%% Internal Functions
50
%%==============================================================================
51

52
-spec execute_command(els_command:command_id(), [any()]) -> [map()].
53
execute_command(<<"server-info">>, _Arguments) ->
54
    {ok, Version} = application:get_key(?APP, vsn),
1✔
55
    BinVersion = list_to_binary(Version),
1✔
56
    Root = filename:basename(els_uri:path(els_config:get(root_uri))),
1✔
57
    ConfigPath =
1✔
58
        case els_config:get(config_path) of
59
            undefined -> <<"undefined">>;
×
60
            Path -> list_to_binary(Path)
1✔
61
        end,
62

63
    OtpPathConfig = list_to_binary(els_config:get(otp_path)),
1✔
64
    OtpRootDir = list_to_binary(code:root_dir()),
1✔
65
    OtpMessage =
1✔
66
        case OtpRootDir == OtpPathConfig of
67
            true ->
68
                <<", OTP root ", OtpRootDir/binary>>;
1✔
69
            false ->
70
                <<", OTP root(code):", OtpRootDir/binary, ", OTP root(config):",
×
71
                    OtpPathConfig/binary>>
72
        end,
73
    Message =
1✔
74
        <<"Erlang LS (in ", Root/binary, "), version: ", BinVersion/binary, ", config from ",
75
            ConfigPath/binary, OtpMessage/binary>>,
76
    els_server:send_notification(
1✔
77
        <<"window/showMessage">>,
78
        #{
79
            type => ?MESSAGE_TYPE_INFO,
80
            message => Message
81
        }
82
    ),
83
    [];
1✔
84
execute_command(<<"ct-run-test">>, [Params]) ->
85
    els_command_ct_run_test:execute(Params),
1✔
86
    [];
1✔
87
execute_command(<<"function-references">>, [_Params]) ->
88
    [];
×
89
execute_command(<<"show-behaviour-usages">>, [_Params]) ->
90
    [];
×
91
execute_command(<<"suggest-spec">>, []) ->
92
    [];
×
93
execute_command(<<"suggest-spec">>, [
94
    #{
95
        <<"uri">> := Uri,
96
        <<"line">> := Line,
97
        <<"spec">> := Spec
98
    }
99
]) ->
100
    Method = <<"workspace/applyEdit">>,
1✔
101
    {ok, #{text := Text}} = els_utils:lookup_document(Uri),
1✔
102
    LineText = els_text:line(Text, Line - 1),
1✔
103
    NewText = <<Spec/binary, "\n", LineText/binary, "\n">>,
1✔
104
    Params =
1✔
105
        #{
106
            edit =>
107
                els_text_edit:edit_replace_text(Uri, NewText, Line - 1, Line)
108
        },
109
    els_server:send_request(Method, Params),
1✔
110
    [];
1✔
111
execute_command(<<"refactor.extract">>, [
112
    #{
113
        <<"uri">> := Uri,
114
        <<"range">> := Range
115
    }
116
]) ->
117
    ok = extract_function(Uri, Range),
4✔
118
    [];
4✔
119
execute_command(<<"bump-variables">>, [
120
    #{
121
        <<"uri">> := Uri,
122
        <<"range">> := Range,
123
        <<"name">> := Name
124
    }
125
]) ->
126
    ok = bump_variables(Uri, Range, Name),
×
127
    [];
×
128
execute_command(<<"add-behaviour-callbacks">>, [
129
    #{
130
        <<"uri">> := Uri,
131
        <<"behaviour">> := Behaviour
132
    }
133
]) ->
134
    {ok, Document} = els_utils:lookup_document(Uri),
×
135
    case els_utils:find_module(binary_to_atom(Behaviour, utf8)) of
×
136
        {error, _} ->
137
            [];
×
138
        {ok, BeUri} ->
139
            %% Put exported callback functions after -behaviour() or -export()
140
            #{range := #{to := {ExportLine, _Col}}} =
×
141
                lists:last(
142
                    els_poi:sort(
143
                        els_dt_document:pois(
144
                            Document,
145
                            [behaviour, export]
146
                        )
147
                    )
148
                ),
149
            ExportPos = {ExportLine + 1, 1},
×
150

151
            %% Put callback functions after the last function
152
            CallbacksPos =
×
153
                case els_poi:sort(els_dt_document:pois(Document, [function])) of
154
                    [] ->
155
                        {ExportLine + 2, 1};
×
156
                    POIs ->
157
                        #{data := #{wrapping_range := #{to := Pos}}} = lists:last(POIs),
×
158
                        Pos
×
159
                end,
160
            {ok, BeDoc} = els_utils:lookup_document(BeUri),
×
161
            CallbackPOIs = els_poi:sort(els_dt_document:pois(BeDoc, [callback])),
×
162
            FunPOIs = els_dt_document:pois(Document, [function]),
×
163

164
            %% Only add missing callback functions, existing functions are kept.
165
            Funs = [Id || #{id := Id} <- FunPOIs],
×
166
            Callbacks = [
×
167
                Cb
×
168
             || #{id := Id} = Cb <- CallbackPOIs,
×
169
                not lists:member(Id, Funs)
×
170
            ],
171
            Comment = ["\n%%% ", Behaviour, " callbacks\n"],
×
172
            ExportText = Comment ++ [export_text(Id) || Id <- Callbacks],
×
173
            Text = Comment ++ [fun_text(Cb, BeDoc) || Cb <- Callbacks],
×
174
            Method = <<"workspace/applyEdit">>,
×
175
            Params =
×
176
                #{
177
                    edit =>
178
                        #{
179
                            changes => #{
180
                                Uri =>
181
                                    [
182
                                        #{
183
                                            newText => iolist_to_binary(ExportText),
184
                                            range => els_protocol:range(
185
                                                #{
186
                                                    from => ExportPos,
187
                                                    to => ExportPos
188
                                                }
189
                                            )
190
                                        },
191
                                        #{
192
                                            newText => iolist_to_binary(Text),
193
                                            range => els_protocol:range(
194
                                                #{
195
                                                    from => CallbacksPos,
196
                                                    to => CallbacksPos
197
                                                }
198
                                            )
199
                                        }
200
                                    ]
201
                            }
202
                        }
203
                },
204
            els_server:send_request(Method, Params),
×
205
            []
×
206
    end;
207
execute_command(Command, Arguments) ->
208
    case wrangler_handler:execute_command(Command, Arguments) of
×
209
        true ->
210
            ok;
×
211
        _ ->
212
            ?LOG_INFO(
×
213
                "Unsupported command: [Command=~p] [Arguments=~p]",
214
                [Command, Arguments]
×
215
            )
216
    end,
217
    [].
×
218

219
-spec bump_variables(uri(), range(), binary()) -> ok.
220
bump_variables(Uri, Range, VarName) ->
221
    {Name, Number} = split_variable(VarName),
×
222
    {ok, Document} = els_utils:lookup_document(Uri),
×
223
    VarPOIs = els_poi:sort(els_dt_document:pois(Document, [variable])),
×
224
    VarRange = els_range:to_poi_range(Range),
×
225
    ScopeRange = els_scope:variable_scope_range(VarRange, Document),
×
226
    Changes =
×
227
        [
228
            bump_variable_change(POI)
×
229
         || POI <- pois_in(VarPOIs, ScopeRange),
×
230
            should_bump_variable(POI, Name, Number)
×
231
        ],
232
    Method = <<"workspace/applyEdit">>,
×
233
    Params = #{edit => #{changes => #{Uri => Changes}}},
×
234
    els_server:send_request(Method, Params).
×
235

236
-spec should_bump_variable(els_poi:poi(), binary(), binary()) -> boolean().
237
should_bump_variable(#{id := Id}, Name, Number) ->
238
    case split_variable(Id) of
×
239
        {PName, PNumber} when PName == Name ->
240
            binary_to_integer(PNumber) >= binary_to_integer(Number);
×
241
        _ ->
242
            false
×
243
    end.
244

245
-spec bump_variable_change(els_poi:poi()) -> map().
246
bump_variable_change(#{id := Id, range := PoiRange}) ->
247
    {Name, Number} = split_variable(Id),
×
248
    NewNumber = integer_to_binary(binary_to_integer(Number) + 1),
×
249
    NewId = binary_to_atom(<<Name/binary, NewNumber/binary>>, utf8),
×
250
    #{
×
251
        newText => NewId,
252
        range => els_protocol:range(PoiRange)
253
    }.
254

255
-spec pois_in([els_poi:poi()], els_poi:poi_range()) ->
256
    [els_poi:poi()].
257
pois_in(POIs, Range) ->
258
    [POI || #{range := R} = POI <- POIs, els_range:in(R, Range)].
×
259

260
-spec split_variable(atom() | binary() | list()) -> {binary(), binary()} | error.
261
split_variable(Name) when is_atom(Name) ->
262
    split_variable(atom_to_list(Name));
×
263
split_variable(Name) when is_binary(Name) ->
264
    split_variable(unicode:characters_to_list(Name));
×
265
split_variable(Name) when is_list(Name) ->
266
    split_variable(lists:reverse(Name), []).
×
267

268
-spec split_variable(string(), string()) -> {binary(), binary()} | error.
269
split_variable([H | T], Acc) when $0 =< H, H =< $9 ->
270
    split_variable(T, [H | Acc]);
×
271
split_variable(_Name, []) ->
272
    error;
×
273
split_variable(Name, Acc) ->
274
    {list_to_binary(lists:reverse(Name)), list_to_binary(Acc)}.
×
275

276
-spec extract_function(uri(), range()) -> ok.
277
extract_function(Uri, Range) ->
278
    {ok, [#{text := Text} = Document]} = els_dt_document:lookup(Uri),
4✔
279
    ExtractRange = extract_range(Document, Range),
4✔
280
    #{from := {FromL, FromC} = From, to := {ToL, ToC}} = ExtractRange,
4✔
281
    ExtractString0 = els_text:range(Text, From, {ToL, ToC}),
4✔
282
    %% Trim whitespace
283
    ExtractString = string:trim(ExtractString0, both, " \n\r\t"),
4✔
284
    %% Trim trailing termination symbol
285
    ExtractStringTrimmed = string:trim(ExtractString, trailing, ",.;"),
4✔
286
    Method = <<"workspace/applyEdit">>,
4✔
287
    case els_dt_document:wrapping_functions(Document, FromL, FromC) of
4✔
288
        [WrappingFunPOI | _] when ExtractStringTrimmed /= <<>> ->
289
            %% WrappingFunPOI is the function that we are currently in
290
            #{
4✔
291
                data := #{
292
                    wrapping_range :=
293
                        #{
294
                            from := {FunBeginLine, _},
295
                            to := {FunEndLine, _}
296
                        }
297
                }
298
            } = WrappingFunPOI,
299
            %% Get args needed for the new function
300
            Args = get_args(ExtractRange, Document, FromL, FunBeginLine),
4✔
301
            ArgsBin = unicode:characters_to_binary(string:join(Args, ", ")),
4✔
302
            FunClause = <<"new_function(", ArgsBin/binary, ")">>,
4✔
303
            %% Place the new function after the current function
304
            EndSymbol = end_symbol(ExtractString),
4✔
305
            NewRange = els_protocol:range(
4✔
306
                #{from => {FunEndLine + 1, 1}, to => {FunEndLine + 1, 1}}
307
            ),
308
            FunBody = unicode:characters_to_list(
4✔
309
                <<FunClause/binary, " ->\n", ExtractStringTrimmed/binary, ".">>
310
            ),
311
            {ok, FunBodyFormatted, _} = erlfmt:format_string(FunBody, []),
4✔
312
            NewFun = unicode:characters_to_binary(FunBodyFormatted ++ "\n"),
4✔
313
            Changes = [
4✔
314
                #{
315
                    newText => <<FunClause/binary, EndSymbol/binary>>,
316
                    range => els_protocol:range(ExtractRange)
317
                },
318
                #{
319
                    newText => NewFun,
320
                    range => NewRange
321
                }
322
            ],
323
            Params = #{edit => #{changes => #{Uri => Changes}}},
4✔
324
            els_server:send_request(Method, Params);
4✔
325
        _ ->
326
            ?LOG_INFO("No wrapping function found"),
×
327
            ok
×
328
    end.
329

330
-spec end_symbol(binary()) -> binary().
331
end_symbol(ExtractString) ->
332
    case binary:last(ExtractString) of
4✔
333
        $. -> <<".">>;
×
334
        $, -> <<",">>;
1✔
335
        $; -> <<";">>;
×
336
        _ -> <<>>
3✔
337
    end.
338

339
%% @doc Find all variables defined in the function before the current.
340
%%      If they are used inside the selected range, they need to be
341
%%      sent in as arguments to the new function.
342
-spec get_args(
343
    els_poi:poi_range(),
344
    els_dt_document:item(),
345
    non_neg_integer(),
346
    non_neg_integer()
347
) -> [string()].
348
get_args(PoiRange, Document, FromL, FunBeginLine) ->
349
    BeforeRange = #{from => {FunBeginLine, 1}, to => {FromL, 1}},
4✔
350
    VarPOIsBefore = els_dt_document:pois_in_range(Document, [variable], BeforeRange),
4✔
351
    %% Remove all variables inside LCs or keyword expressions
352
    LCPOIs = els_dt_document:pois(Document, [list_comp]),
4✔
353
    FunExprPOIs = [
4✔
NEW
354
        POI
×
355
     || #{id := fun_expr} = POI <- els_dt_document:pois(Document, [keyword_expr])
4✔
356
    ],
357
    %% Only consider fun exprs that doesn't contain the selected range
358
    ExcludePOIs = [
4✔
359
        POI
7✔
360
     || #{range := R} = POI <- FunExprPOIs ++ LCPOIs, not els_range:in(PoiRange, R)
4✔
361
    ],
362
    VarsBefore = [
4✔
363
        Id
34✔
364
     || #{range := VarRange, id := Id} <- VarPOIsBefore,
4✔
365
        not_in_any_range(VarRange, ExcludePOIs)
46✔
366
    ],
367
    %% Find all variables defined before the current function that are used
368
    %% inside the selected range.
369
    VarPOIsInside = els_dt_document:pois_in_range(Document, [variable], PoiRange),
4✔
370
    els_utils:uniq([
4✔
371
        atom_to_list(Id)
10✔
372
     || #{id := Id} <- els_poi:sort(VarPOIsInside),
4✔
373
        lists:member(Id, VarsBefore)
13✔
374
    ]).
375

376
-spec not_in_any_range(els_poi:poi_range(), [els_poi:poi()]) -> boolean().
377
not_in_any_range(VarRange, POIs) ->
378
    not lists:any(
46✔
379
        fun(#{range := Range}) ->
380
            els_range:in(VarRange, Range)
72✔
381
        end,
382
        POIs
383
    ).
384

385
-spec extract_range(els_dt_document:item(), range()) -> els_poi:poi_range().
386
extract_range(#{text := Text} = Document, Range) ->
387
    PoiRange = els_range:to_poi_range(Range),
4✔
388
    #{from := {CurrL, CurrC} = From, to := To} = PoiRange,
4✔
389
    POIs = els_dt_document:get_element_at_pos(Document, CurrL, CurrC),
4✔
390
    MarkedText = els_text:range(Text, From, To),
4✔
391
    case els_text:is_keyword_expr(MarkedText) of
4✔
392
        true ->
393
            case sort_by_range_size([P || #{kind := keyword_expr} = P <- POIs]) of
1✔
394
                [] ->
395
                    PoiRange;
×
396
                [{_Size, #{range := SmallestRange}} | _] ->
397
                    SmallestRange
1✔
398
            end;
399
        false ->
400
            PoiRange
3✔
401
    end.
402

403
-spec sort_by_range_size(_) -> _.
404
sort_by_range_size(POIs) ->
405
    lists:sort([{range_size(P), P} || P <- POIs]).
1✔
406

407
-spec range_size(_) -> _.
408
range_size(#{range := #{from := {FromL, FromC}, to := {ToL, ToC}}}) ->
409
    {ToL - FromL, ToC - FromC}.
1✔
410

411
-spec spec_text(binary()) -> binary().
412
spec_text(<<"-callback", Rest/binary>>) ->
413
    <<"-spec", Rest/binary>>;
×
414
spec_text(Text) ->
415
    Text.
×
416

417
-spec fun_text(els_poi:poi(), els_dt_document:item()) -> iolist().
418
fun_text(#{id := {Name, Arity}, range := Range}, #{text := Text}) ->
419
    #{from := From, to := To} = Range,
×
420
    %% TODO: Assuming 2 space indentation
421
    CallbackText = els_text:range(Text, From, To),
×
422
    SpecText = spec_text(CallbackText),
×
423
    [
424
        io_lib:format("~s", [SpecText]),
×
425
        "\n",
426
        atom_to_binary(Name, utf8),
427
        "(",
428
        args_text(Arity, 1),
429
        ") ->\n",
430
        "  error(not_implemented).\n\n"
431
    ].
432

433
-spec export_text(els_poi:poi()) -> iolist().
434
export_text(#{id := {Name, Arity}}) ->
435
    [
436
        "-export([",
×
437
        atom_to_binary(Name, utf8),
438
        "/",
439
        integer_to_list(Arity),
440
        "]).\n"
441
    ].
442

443
-spec args_text(integer(), integer()) -> iolist().
444
args_text(0, 1) ->
445
    [];
×
446
args_text(Arity, Arity) ->
447
    ["_"];
×
448
args_text(Arity, N) ->
449
    ["_, " | args_text(Arity, N + 1)].
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc