Skip to content

Commit 71c33f1

Browse files
doorganmhanberg
andauthored
fix: handle missing metadata in indexer extractors (#390)
Fixes #379 The indexer was crashing because it encountered nodes with missing location metadata. I'm not sure what code exactly triggered that, I suspect it might be broken code so I added tests for that. I also looked at other places in the indexer where we would crash on missing metadata and added error handling there too. --------- Co-authored-by: Mitchell Hanberg <mitch@mitchellhanberg.com>
1 parent c98b870 commit 71c33f1

File tree

6 files changed

+276
-213
lines changed

6 files changed

+276
-213
lines changed

apps/engine/lib/engine/search/indexer/extractors/ex_unit.ex

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ defmodule Engine.Search.Indexer.Extractors.ExUnit do
99
alias Forge.Formats
1010
alias Forge.Search.Indexer.Entry
1111

12-
require Logger
13-
1412
# setup block i.e. setup do... or setup arg do...
1513
def extract({setup_fn, _, args} = setup, %Reducer{} = reducer)
1614
when setup_fn in [:setup, :setup_all] and length(args) > 0 do
@@ -19,16 +17,10 @@ defmodule Engine.Search.Indexer.Extractors.ExUnit do
1917
subject = Formats.mfa(module, setup_fn, arity)
2018
setup_type = :"ex_unit_#{setup_fn}"
2119

22-
entry =
23-
case Metadata.location(setup) do
24-
{:block, _, _, _} ->
25-
block_entry(reducer, setup, setup_type, subject)
26-
27-
{:expression, _} ->
28-
expression_entry(reducer, setup, setup_type, subject)
29-
end
30-
31-
{:ok, entry}
20+
case Metadata.location(setup) do
21+
{:block, _, _, _} -> block_entry(reducer, setup, setup_type, subject)
22+
{:expression, _} -> expression_entry(reducer, setup, setup_type, subject)
23+
end
3224
end
3325

3426
# Test block test "test name" do ... or test "test name", arg do
@@ -39,18 +31,10 @@ defmodule Engine.Search.Indexer.Extractors.ExUnit do
3931
module_name = Formats.module(module)
4032
subject = "#{module_name}.[\"#{test_name}\"]/#{arity}"
4133

42-
entry =
43-
case Metadata.location(test) do
44-
{:block, _, _, _} ->
45-
# a test with a body
46-
block_entry(reducer, test, :ex_unit_test, subject)
47-
48-
{:expression, _} ->
49-
# a pending test
50-
expression_entry(reducer, test, :ex_unit_test, subject)
51-
end
52-
53-
{:ok, entry}
34+
case Metadata.location(test) do
35+
{:block, _, _, _} -> block_entry(reducer, test, :ex_unit_test, subject)
36+
{:expression, _} -> expression_entry(reducer, test, :ex_unit_test, subject)
37+
end
5438
end
5539

5640
# describe blocks
@@ -60,9 +44,7 @@ defmodule Engine.Search.Indexer.Extractors.ExUnit do
6044
module_name = Formats.module(module)
6145
subject = "#{module_name}[\"#{describe_name}\"]/#{arity}"
6246

63-
entry = block_entry(reducer, test, :ex_unit_describe, subject)
64-
65-
{:ok, entry}
47+
block_entry(reducer, test, :ex_unit_describe, subject)
6648
end
6749

6850
def extract(_ign, _) do
@@ -75,9 +57,11 @@ defmodule Engine.Search.Indexer.Extractors.ExUnit do
7557

7658
{:ok, module} = Analyzer.current_module(reducer.analysis, Reducer.position(reducer))
7759
app = Application.get_application(module)
78-
detail_range = detail_range(reducer.analysis, ast)
7960

80-
Entry.definition(path, block, subject, type, detail_range, app)
61+
case detail_range(reducer.analysis, ast) do
62+
nil -> :ignored
63+
range -> {:ok, Entry.definition(path, block, subject, type, range, app)}
64+
end
8165
end
8266

8367
defp block_entry(%Reducer{} = reducer, ast, type, subject) do
@@ -86,9 +70,23 @@ defmodule Engine.Search.Indexer.Extractors.ExUnit do
8670

8771
{:ok, module} = Analyzer.current_module(reducer.analysis, Reducer.position(reducer))
8872
app = Application.get_application(module)
89-
detail_range = detail_range(reducer.analysis, ast)
90-
block_range = block_range(reducer.analysis, ast)
91-
Entry.block_definition(path, block, subject, type, block_range, detail_range, app)
73+
74+
case detail_range(reducer.analysis, ast) do
75+
nil ->
76+
:ignored
77+
78+
detail_range ->
79+
{:ok,
80+
Entry.block_definition(
81+
path,
82+
block,
83+
subject,
84+
type,
85+
block_range(reducer.analysis, ast),
86+
detail_range,
87+
app
88+
)}
89+
end
9290
end
9391

9492
defp block_range(%Analysis{} = analysis, ast) do
@@ -113,6 +111,9 @@ defmodule Engine.Search.Indexer.Extractors.ExUnit do
113111
Position.new(analysis.document, start_line, start_column),
114112
Position.new(analysis.document, end_line, end_column)
115113
)
114+
115+
_ ->
116+
nil
116117
end
117118
end
118119

apps/engine/lib/engine/search/indexer/extractors/function_reference.ex

Lines changed: 82 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ defmodule Engine.Search.Indexer.Extractors.FunctionReference do
2525
%Reducer{} = reducer
2626
)
2727
when is_list(arg_list) and is_atom(function_name) do
28-
entry = entry(reducer, apply_meta, apply_meta, module, function_name, arg_list)
29-
{:ok, entry, nil}
28+
reducer
29+
|> entry(apply_meta, apply_meta, module, function_name, arg_list)
30+
|> without_further_analysis()
3031
end
3132

3233
# Dynamic call via Kernel.apply Kernel.apply(Module, :function, [1, 2])
@@ -40,8 +41,9 @@ defmodule Engine.Search.Indexer.Extractors.FunctionReference do
4041
%Reducer{} = reducer
4142
)
4243
when is_list(arg_list) and is_atom(function_name) do
43-
entry = entry(reducer, start_metadata, apply_meta, module, function_name, arg_list)
44-
{:ok, entry, nil}
44+
reducer
45+
|> entry(start_metadata, apply_meta, module, function_name, arg_list)
46+
|> without_further_analysis()
4547
end
4648

4749
# remote function OtherModule.foo(:arg), OtherModule.foo() or OtherModule.foo
@@ -50,9 +52,7 @@ defmodule Engine.Search.Indexer.Extractors.FunctionReference do
5052
%Reducer{} = reducer
5153
)
5254
when is_atom(fn_name) do
53-
entry = entry(reducer, start_metadata, end_metadata, module, fn_name, args)
54-
55-
{:ok, entry}
55+
entry(reducer, start_metadata, end_metadata, module, fn_name, args)
5656
end
5757

5858
# local function capture &downcase/1
@@ -65,8 +65,9 @@ defmodule Engine.Search.Indexer.Extractors.FunctionReference do
6565
{module, _, _} =
6666
Engine.Analyzer.resolve_local_call(reducer.analysis, position, fn_name, arity)
6767

68-
entry = entry(reducer, end_metadata, arity_meta, module, fn_name, arity)
69-
{:ok, entry, nil}
68+
reducer
69+
|> entry(end_metadata, arity_meta, module, fn_name, arity)
70+
|> without_further_analysis()
7071
end
7172

7273
# Function capture with arity: &OtherModule.foo/3
@@ -81,13 +82,13 @@ defmodule Engine.Search.Indexer.Extractors.FunctionReference do
8182
]},
8283
%Reducer{} = reducer
8384
) do
84-
entry = entry(reducer, start_metadata, end_metadata, module, function_name, arity)
85-
85+
reducer
86+
|> entry(start_metadata, end_metadata, module, function_name, arity)
8687
# we return nil here to stop analysis from progressing down the syntax tree,
8788
# because if it did, the function head that deals with normal calls will pick
8889
# up the rest of the call and return a reference to MyModule.function/0, which
8990
# is incorrect
90-
{:ok, entry, nil}
91+
|> without_further_analysis()
9192
end
9293

9394
def extract({:|>, pipe_meta, [pipe_start, {fn_name, meta, args}]}, %Reducer{}) do
@@ -134,16 +135,17 @@ defmodule Engine.Search.Indexer.Extractors.FunctionReference do
134135
{module, _, _} =
135136
Engine.Analyzer.resolve_local_call(reducer.analysis, position, fn_name, arity)
136137

137-
entry = entry(reducer, meta, meta, [module], fn_name, args)
138-
139-
{:ok, entry}
138+
entry(reducer, meta, meta, [module], fn_name, args)
140139
end
141140
end
142141

143142
def extract(_ast, _reducer) do
144143
:ignored
145144
end
146145

146+
defp without_further_analysis(:ignored), do: :ignored
147+
defp without_further_analysis({:ok, entry}), do: {:ok, entry, nil}
148+
147149
defp entry(
148150
%Reducer{} = reducer,
149151
start_metadata,
@@ -156,64 +158,81 @@ defmodule Engine.Search.Indexer.Extractors.FunctionReference do
156158
block = Reducer.current_block(reducer)
157159

158160
range =
159-
get_reference_range(reducer.analysis.document, start_metadata, end_metadata, function_name)
160-
161-
case Engine.Analyzer.expand_alias(module, reducer.analysis, range.start) do
162-
{:ok, module} ->
163-
mfa = Subject.mfa(module, function_name, arity)
161+
get_reference_range(
162+
reducer.analysis.document,
163+
start_metadata,
164+
end_metadata,
165+
function_name
166+
)
164167

165-
Entry.reference(
166-
reducer.analysis.document.path,
167-
block,
168-
mfa,
169-
{:function, :usage},
170-
range,
171-
Application.get_application(module)
172-
)
168+
case range do
169+
nil ->
170+
:ignored
173171

174172
_ ->
175-
human_location = Reducer.human_location(reducer)
176-
177-
Logger.warning(
178-
"Could not expand #{inspect(module)} into an alias. Please report this. (at #{human_location})"
179-
)
180-
181-
nil
173+
case Engine.Analyzer.expand_alias(module, reducer.analysis, range.start) do
174+
{:ok, module} ->
175+
mfa = Subject.mfa(module, function_name, arity)
176+
177+
{:ok,
178+
Entry.reference(
179+
reducer.analysis.document.path,
180+
block,
181+
mfa,
182+
{:function, :usage},
183+
range,
184+
Application.get_application(module)
185+
)}
186+
187+
_ ->
188+
human_location = Reducer.human_location(reducer)
189+
190+
Logger.warning(
191+
"Could not expand #{inspect(module)} into an alias (at #{human_location}). Please open an issue!"
192+
)
193+
194+
:ignored
195+
end
182196
end
183197
end
184198

185199
defp get_reference_range(document, start_metadata, end_metadata, function_name) do
186-
{start_line, start_column} = start_position(start_metadata)
187-
start_position = Position.new(document, start_line, start_column)
188-
has_parens? = not Keyword.get(end_metadata, :no_parens, false)
189-
190-
{end_line, end_column} =
191-
case Metadata.position(end_metadata, :closing) do
192-
{line, column} ->
193-
if has_parens? do
194-
{line, column + 1}
195-
else
196-
{line, column}
197-
end
198-
199-
nil ->
200-
{line, column} = Metadata.position(end_metadata)
201-
202-
if has_parens? do
203-
{line, column + 1}
204-
else
205-
name_length = function_name |> Atom.to_string() |> String.length()
206-
# without parens, the metadata points to the beginning of the call, so
207-
# we need to add the length of the function name to be sure we have it
208-
# all
209-
{line, column + name_length}
210-
end
211-
end
200+
if valid_position_metadata?(start_metadata) and valid_position_metadata?(end_metadata) do
201+
{start_line, start_column} = start_position(start_metadata)
202+
start_pos = Position.new(document, start_line, start_column)
203+
has_parens? = not Keyword.get(end_metadata, :no_parens, false)
204+
205+
{end_line, end_column} =
206+
case Metadata.position(end_metadata, :closing) do
207+
{line, column} when has_parens? -> {line, column + 1}
208+
{line, column} -> {line, column}
209+
nil -> adjust_position_for_name(end_metadata, function_name, has_parens?)
210+
end
211+
212+
end_pos = Position.new(document, end_line, end_column)
213+
Range.new(start_pos, end_pos)
214+
else
215+
nil
216+
end
217+
end
212218

213-
end_position = Position.new(document, end_line, end_column)
214-
Range.new(start_position, end_position)
219+
defp adjust_position_for_name(metadata, function_name, has_parens?) do
220+
{line, column} = Metadata.position(metadata)
221+
222+
if has_parens? do
223+
{line, column + 1}
224+
else
225+
name_length = function_name |> Atom.to_string() |> String.length()
226+
{line, column + name_length}
227+
end
228+
end
229+
230+
defp valid_position_metadata?(metadata) when is_list(metadata) do
231+
Keyword.has_key?(metadata, :line) and Keyword.has_key?(metadata, :column)
215232
end
216233

234+
defp valid_position_metadata?(_), do: false
235+
217236
defp start_position(metadata) do
218237
Metadata.position(metadata)
219238
end

0 commit comments

Comments
 (0)