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

nshkrdotcom / ElixirScope / 841913e604b0e848ab19c084aca789cc2444a9a8

29 May 2025 05:21PM UTC coverage: 58.453% (-0.06%) from 58.515%
841913e604b0e848ab19c084aca789cc2444a9a8

push

github

NSHkr
redid refactor of CGP Gen. 1087 tests, 0 failures, 76 excluded

316 of 511 new or added lines in 9 files covered. (61.84%)

3 existing lines in 3 files now uncovered.

6085 of 10410 relevant lines covered (58.45%)

3231.97 hits per line

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

73.63
/lib/elixir_scope/ast_repository/enhanced/cfg_generator/ast_processor.ex
1
defmodule ElixirScope.ASTRepository.Enhanced.CFGGenerator.ASTProcessor do
2
  @moduledoc """
3
  Main AST processing functions for the CFG generator.
4
  """
5

6
  alias ElixirScope.ASTRepository.Enhanced.{CFGNode, CFGEdge, ScopeInfo}
7
  alias ElixirScope.ASTRepository.Enhanced.CFGGenerator.{
8
    StateManager, ASTUtilities, ControlFlowProcessors, ExpressionProcessors
9
  }
10

11
  @doc """
12
  Processes a function body AST and returns CFG components.
13
  """
14
  def process_function_body({:def, meta, [head, [do: body]]}, state) do
15
    process_function_body({:defp, meta, [head, [do: body]]}, state)
996✔
16
  end
17

18
  def process_function_body({:defp, meta, [head, [do: body]]}, state) do
19
    line = ASTUtilities.get_line_number(meta)
1,815✔
20

21
    # Extract function parameters and check for guards
22
    {function_params, guard_ast} = case head do
1,815✔
23
      {:when, _, [func_head, guard]} ->
1✔
24
        # Function has a guard
25
        {ASTUtilities.extract_function_parameters(func_head), guard}
26
      func_head ->
1,814✔
27
        # No guard
28
        {ASTUtilities.extract_function_parameters(func_head), nil}
29
    end
30

31
    # Create function scope
32
    function_scope = %ScopeInfo{
1,815✔
33
      id: state.current_scope,
1,815✔
34
      type: :function,
35
      parent_scope: nil,
36
      child_scopes: [],
37
      variables: function_params,
38
      ast_node_id: ASTUtilities.get_ast_node_id(meta),
39
      entry_points: [state.entry_node],
1,815✔
40
      exit_points: [],
41
      metadata: %{function_head: head, guard: guard_ast}
42
    }
43

44
    # Create entry node
45
    entry_node = %CFGNode{
1,815✔
46
      id: state.entry_node,
1,815✔
47
      type: :entry,
48
      ast_node_id: ASTUtilities.get_ast_node_id(meta),
49
      line: line,
50
      scope_id: state.current_scope,
1,815✔
51
      expression: head,
52
      predecessors: [],
53
      successors: [],
54
      metadata: %{function_head: head, guard: guard_ast}
55
    }
56

57
    initial_state = %{state | nodes: %{state.entry_node => entry_node}}
1,815✔
58

59
    # Process guard if present
60
    {guard_nodes, guard_edges, guard_exits, guard_scopes, guard_state} =
1,815✔
61
      if guard_ast do
62
        process_ast_node(guard_ast, initial_state)
1✔
63
      else
64
        {%{}, [], [state.entry_node], %{}, initial_state}
1,814✔
65
      end
66

67
    # Process function body
68
    {body_nodes, body_edges, body_exits, body_scopes, updated_state} =
1,815✔
69
      process_ast_node(body, guard_state)
70

71
    # Connect entry to guard (if present) or directly to body
72
    entry_connections = build_entry_connections(state.entry_node, guard_ast, guard_nodes, body_nodes)
1,815✔
73

74
    # Connect guard to body (if guard exists)
75
    guard_to_body_edges = build_guard_to_body_connections(guard_ast, guard_exits, body_nodes)
1,815✔
76

77
    # Create exit node
78
    {exit_node_id, final_state} = StateManager.generate_node_id("exit", updated_state)
1,815✔
79
    exit_node = %CFGNode{
1,815✔
80
      id: exit_node_id,
81
      type: :exit,
82
      ast_node_id: nil,
83
      line: line,
84
      scope_id: state.current_scope,
1,815✔
85
      expression: nil,
86
      predecessors: body_exits,
87
      successors: [],
88
      metadata: %{}
89
    }
90

91
    # Connect body exits to function exit
92
    exit_edges = build_exit_connections(body_exits, exit_node_id)
1,815✔
93

94
    # Handle empty function body case
95
    direct_entry_to_exit_edges = build_direct_entry_to_exit_connections(
1,815✔
96
      body_exits, guard_exits, state.entry_node, exit_node_id
1,815✔
97
    )
98

99
    all_nodes = guard_nodes
1,815✔
100
    |> Map.merge(body_nodes)
101
    |> Map.put(state.entry_node, entry_node)
1,815✔
102
    |> Map.put(exit_node_id, exit_node)
103

104
    all_edges = entry_connections ++ guard_edges ++ guard_to_body_edges ++
1,815✔
105
                body_edges ++ exit_edges ++ direct_entry_to_exit_edges
106
    all_scopes = Map.merge(guard_scopes, body_scopes)
1,815✔
107
    |> Map.put(state.current_scope, function_scope)
1,815✔
108

109
    {all_nodes, all_edges, [exit_node_id], all_scopes, final_state}
1,815✔
110
  end
111

NEW
112
  def process_function_body(malformed_ast, _state) do
×
113
    # Handle any malformed or unexpected AST structure
114
    {:error, {:cfg_generation_failed, "Invalid AST structure: #{inspect(malformed_ast)}"}}
115
  end
116

117
  @doc """
118
  Main AST node processing dispatcher.
119
  """
120
  def process_ast_node(ast, state) do
121
    case ast do
6,114✔
122
      # Block of statements - put this FIRST to ensure it matches before function call pattern
123
      {:__block__, meta, statements} ->
124
        ExpressionProcessors.process_statement_sequence(statements, meta, state)
100✔
125

126
      # Assignment with pattern matching - put this early to ensure it matches
127
      {:=, meta, [pattern, expression]} ->
128
        ExpressionProcessors.process_assignment(pattern, expression, meta, state)
200✔
129

130
      # Comprehensions - put this FIRST to ensure it matches
131
      {:for, meta, clauses} ->
132
        ExpressionProcessors.process_comprehension(clauses, meta, state)
4✔
133

134
      # Case statement - Elixir's primary pattern matching construct
135
      {:case, meta, [condition, [do: clauses]]} ->
136
        ControlFlowProcessors.process_case_statement(condition, clauses, meta, state)
219✔
137

138
      # If statement with optional else
139
      {:if, meta, [condition, clauses]} when is_list(clauses) ->
140
        then_branch = Keyword.get(clauses, :do)
175✔
141
        else_clause = case Keyword.get(clauses, :else) do
175✔
142
          nil -> []
×
143
          else_branch -> [else: else_branch]
175✔
144
        end
145
        ControlFlowProcessors.process_if_statement(condition, then_branch, else_clause, meta, state)
175✔
146

147
      # Cond statement - multiple conditions
148
      {:cond, meta, [[do: clauses]]} ->
NEW
149
        ControlFlowProcessors.process_cond_statement(clauses, meta, state)
×
150

151
      # Try-catch-rescue-after
152
      {:try, meta, blocks} ->
153
        ControlFlowProcessors.process_try_statement(blocks, meta, state)
202✔
154

155
      # With statement - error handling pipeline
156
      {:with, meta, clauses} ->
157
        ControlFlowProcessors.process_with_statement(clauses, meta, state)
1✔
158

159
      # Pipe operation - data transformation pipeline
160
      {:|>, meta, [left, right]} ->
161
        ExpressionProcessors.process_pipe_operation(left, right, meta, state)
1,202✔
162

163
      # Function call with module
164
      {{:., meta1, [module, func_name]}, meta2, args} ->
165
        ExpressionProcessors.process_module_function_call(module, func_name, args, meta1, meta2, state)
1,416✔
166

167
      # Function call
168
      {func_name, meta, args} when is_atom(func_name) ->
169
        ExpressionProcessors.process_function_call(func_name, args, meta, state)
1,402✔
170

171
      # Receive statement
172
      {:receive, meta, clauses} ->
NEW
173
        ControlFlowProcessors.process_receive_statement(clauses, meta, state)
×
174

175
      # Unless statement (negative conditional)
176
      {:unless, meta, [condition, clauses]} when is_list(clauses) ->
NEW
177
        ControlFlowProcessors.process_unless_statement(condition, clauses, meta, state)
×
178

179
      # When guard expressions
180
      {:when, meta, [expr, guard]} ->
NEW
181
        ExpressionProcessors.process_when_guard(expr, guard, meta, state)
×
182

183
      # Anonymous function
184
      {:fn, meta, clauses} ->
NEW
185
        ExpressionProcessors.process_anonymous_function(clauses, meta, state)
×
186

187
      # Exception handling
188
      {:raise, meta, args} ->
NEW
189
        ExpressionProcessors.process_raise_statement(args, meta, state)
×
190

191
      {:throw, meta, [value]} ->
NEW
192
        ExpressionProcessors.process_throw_statement(value, meta, state)
×
193

194
      {:exit, meta, [reason]} ->
NEW
195
        ExpressionProcessors.process_exit_statement(reason, meta, state)
×
196

197
      # Concurrency
198
      {:spawn, meta, args} ->
NEW
199
        ExpressionProcessors.process_spawn_statement(args, meta, state)
×
200

201
      {:send, meta, [pid, message]} ->
NEW
202
        ExpressionProcessors.process_send_statement(pid, message, meta, state)
×
203

204
      # Binary operations
205
      {op, meta, [left, right]} when op in [:+, :-, :*, :/, :==, :!=, :<, :>, :<=, :>=, :and, :or, :&&, :||] ->
NEW
206
        ExpressionProcessors.process_binary_operation(op, left, right, meta, state)
×
207

208
      # Unary operations
209
      {op, meta, [operand]} when op in [:not, :!, :+, :-] ->
NEW
210
        ExpressionProcessors.process_unary_operation(op, operand, meta, state)
×
211

212
      # Variable reference
213
      {var_name, meta, nil} when is_atom(var_name) ->
NEW
214
        ExpressionProcessors.process_variable_reference(var_name, meta, state)
×
215

216
      # Literal values
217
      literal when is_atom(literal) or is_number(literal) or is_binary(literal) or is_list(literal) ->
218
        ExpressionProcessors.process_literal_value(literal, state)
1,158✔
219

220
      # Handle nil (empty function body)
221
      nil ->
222
        # Empty function body - return empty results
NEW
223
        {%{}, [], [], %{}, state}
×
224

225
      # Data structures
226
      {:{}, meta, elements} ->
NEW
227
        ExpressionProcessors.process_tuple_construction(elements, meta, state)
×
228

229
      list when is_list(list) ->
NEW
230
        ExpressionProcessors.process_list_construction(list, state)
×
231

232
      {:%{}, meta, pairs} ->
NEW
233
        ExpressionProcessors.process_map_construction(pairs, meta, state)
×
234

235
      {:%{}, meta, [map | updates]} ->
NEW
236
        ExpressionProcessors.process_map_update(map, updates, meta, state)
×
237

238
      {:%, meta, [struct_name, fields]} ->
NEW
239
        ExpressionProcessors.process_struct_construction(struct_name, fields, meta, state)
×
240

241
      # Access operation
242
      {{:., meta1, [Access, :get]}, meta2, [container, key]} ->
NEW
243
        ExpressionProcessors.process_access_operation(container, key, meta1, meta2, state)
×
244

245
      # Attribute access
246
      {:@, meta, [attr]} ->
NEW
247
        ExpressionProcessors.process_attribute_access(attr, meta, state)
×
248

249
      # Simple expression fallback
250
      _ ->
251
        ExpressionProcessors.process_simple_expression(ast, state)
35✔
252
    end
253
  end
254

255
  # Helper functions for building connections
256

257
  defp build_entry_connections(entry_node_id, guard_ast, guard_nodes, body_nodes) do
258
    if guard_ast do
1,815✔
259
      # Connect entry to guard
260
      guard_entry_nodes = get_entry_nodes(guard_nodes)
1✔
261
      Enum.map(guard_entry_nodes, fn node_id ->
1✔
262
        %CFGEdge{
1✔
263
          from_node_id: entry_node_id,
264
          to_node_id: node_id,
265
          type: :sequential,
266
          condition: nil,
267
          probability: 1.0,
268
          metadata: %{connection: :entry_to_guard}
269
        }
270
      end)
271
    else
272
      # Connect entry directly to body
273
      body_entry_nodes = get_entry_nodes(body_nodes)
1,814✔
274
      if body_entry_nodes == [] do
1,814✔
275
        # Empty function body - no body nodes to connect to
276
        []
277
      else
278
        Enum.map(body_entry_nodes, fn node_id ->
1,812✔
279
          %CFGEdge{
4,804✔
280
            from_node_id: entry_node_id,
281
            to_node_id: node_id,
282
            type: :sequential,
283
            condition: nil,
284
            probability: 1.0,
285
            metadata: %{connection: :entry_to_body}
286
          }
287
        end)
288
      end
289
    end
290
  end
291

292
  defp build_guard_to_body_connections(guard_ast, guard_exits, body_nodes) do
293
    if guard_ast do
1,815✔
294
      body_entry_nodes = get_entry_nodes(body_nodes)
1✔
295
      Enum.flat_map(guard_exits, fn guard_exit ->
1✔
296
        Enum.map(body_entry_nodes, fn body_entry ->
1✔
297
          %CFGEdge{
6✔
298
            from_node_id: guard_exit,
299
            to_node_id: body_entry,
300
            type: :sequential,
301
            condition: nil,
302
            probability: 1.0,
303
            metadata: %{connection: :guard_to_body}
304
          }
305
        end)
306
      end)
307
    else
308
      []
309
    end
310
  end
311

312
  defp build_exit_connections(body_exits, exit_node_id) do
313
    Enum.map(body_exits, fn exit_id ->
1,815✔
314
      %CFGEdge{
2,216✔
315
        from_node_id: exit_id,
316
        to_node_id: exit_node_id,
317
        type: :sequential,
318
        condition: nil,
319
        probability: 1.0,
320
        metadata: %{}
321
      }
322
    end)
323
  end
324

325
  defp build_direct_entry_to_exit_connections(body_exits, guard_exits, entry_node_id, exit_node_id) do
326
    if body_exits == [] and guard_exits == [entry_node_id] do
1,815✔
327
      # Empty function body with no guard, or guard that doesn't produce nodes
328
      [%CFGEdge{
329
        from_node_id: entry_node_id,
330
        to_node_id: exit_node_id,
331
        type: :sequential,
332
        condition: nil,
333
        probability: 1.0,
334
        metadata: %{connection: :entry_to_exit_direct}
335
      }]
336
    else
337
      []
338
    end
339
  end
340

341
  defp get_entry_nodes(nodes) when map_size(nodes) == 0, do: []
2✔
342
  defp get_entry_nodes(nodes) do
343
    # Find nodes with no predecessors
344
    nodes
345
    |> Map.values()
346
    |> Enum.filter(fn node -> length(node.predecessors) == 0 end)
6,663✔
347
    |> Enum.map(& &1.id)
4,811✔
348
    |> case do
1,814✔
NEW
349
      [] -> [nodes |> Map.keys() |> List.first()]  # Fallback to first node
×
350
      entry_nodes -> entry_nodes
1,814✔
351
    end
352
  end
353
end
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

© 2026 Coveralls, Inc