Current section
Files
Jump to
Current section
Files
lib/reach.ex
defmodule Reach do
@moduledoc """
Program Dependence Graph for Elixir and Erlang.
Reach analyzes Elixir and Erlang source code and builds a graph that captures
**what depends on what**: which expressions produce values consumed
by others (data dependence), and which expressions control whether
others execute (control dependence).
## Building a graph
# Elixir (default)
{:ok, graph} = Reach.string_to_graph(\"""
def foo(x) do
y = x + 1
if y > 10, do: :big, else: :small
end
\""")
# Erlang
{:ok, graph} = Reach.string_to_graph(source, language: :erlang)
# Auto-detected from file extension
{:ok, graph} = Reach.file_to_graph("lib/my_module.ex")
{:ok, graph} = Reach.file_to_graph("src/my_module.erl")
## Querying
Reach.backward_slice(graph, node_id)
Reach.forward_slice(graph, node_id)
Reach.independent?(graph, id_a, id_b)
Reach.nodes(graph, type: :call, module: Enum)
Reach.nodes(graph, type: :call, module: Enum, function: :map, arity: 2)
Reach.data_flows?(graph, source_id, sink_id)
## Inspecting nodes
node = Reach.node(graph, some_id)
node.type #=> :call
node.meta #=> %{module: Enum, function: :map, arity: 2}
node.source_span #=> %{file: "lib/foo.ex", start_line: 5, ...}
Reach.pure?(node) #=> true
"""
alias Reach.{Effects, Frontend, SystemDependence}
alias Reach.IR.{Counter, Node}
@typedoc "A program dependence graph. Built by `string_to_graph/2`, `file_to_graph/2`, etc."
@opaque graph :: struct()
# --- Building ---
@doc """
Parses a source string and builds a program dependence graph.
Returns `{:ok, graph}` or `{:error, reason}`.
## Options
* `:language` — `:elixir` (default) or `:erlang`
* `:file` — filename for source locations (default: `"nofile"`)
* `:module` — module name for call graph resolution
"""
@spec string_to_graph(String.t(), keyword()) :: {:ok, graph()} | {:error, term()}
def string_to_graph(source, opts \\ []) do
{language, opts} = Keyword.pop(opts, :language, :elixir)
case parse_string(source, language, opts) do
{:ok, ir_nodes} -> {:ok, SystemDependence.build(ir_nodes, opts)}
{:error, _} = err -> err
end
end
@doc """
Same as `string_to_graph/2` but raises on parse error.
"""
@spec string_to_graph!(String.t(), keyword()) :: graph()
def string_to_graph!(source, opts \\ []) do
case string_to_graph(source, opts) do
{:ok, graph} -> graph
{:error, reason} -> raise "Reach parse error: #{inspect(reason)}"
end
end
@doc """
Reads a source file and builds a program dependence graph.
The language is auto-detected from the file extension (`.ex` / `.exs`
for Elixir, `.erl` / `.hrl` for Erlang), or can be set explicitly
via the `:language` option.
Returns `{:ok, graph}` or `{:error, reason}`.
"""
@spec file_to_graph(Path.t(), keyword()) :: {:ok, graph()} | {:error, term()}
def file_to_graph(path, opts \\ []) do
language = Keyword.get(opts, :language) || language_from_extension(path)
opts = Keyword.put(opts, :language, language)
case language do
:erlang ->
opts = Keyword.put_new(opts, :file, path)
case Frontend.Erlang.parse_file(path, opts) do
{:ok, nodes} -> {:ok, SystemDependence.build(nodes, opts)}
{:error, _} = err -> err
end
_elixir ->
case File.read(path) do
{:ok, source} ->
opts =
opts
|> Keyword.put_new(:file, path)
|> Keyword.put_new(:module, module_from_path(path))
parse_and_build(source, :elixir, opts)
{:error, reason} ->
{:error, {:file, reason}}
end
end
end
@doc """
Same as `file_to_graph/2` but raises on error.
"""
@spec file_to_graph!(Path.t(), keyword()) :: graph()
def file_to_graph!(path, opts \\ []) do
case file_to_graph(path, opts) do
{:ok, graph} -> graph
{:error, reason} -> raise "Reach error: #{inspect(reason)}"
end
end
@doc """
Builds a graph from a compiled module (loaded in the VM).
Analyzes the macro-expanded Erlang abstract forms from the BEAM bytecode.
This captures code injected by `use`, `defmacro`, and other macros that
the source-level frontend misses.
Requires the module to be compiled with debug info (the default).
"""
@spec module_to_graph(module(), keyword()) :: {:ok, graph()} | {:error, term()}
def module_to_graph(module, opts \\ []) do
case Frontend.BEAM.from_module(module, opts) do
{:ok, nodes} -> {:ok, SystemDependence.build(nodes, opts)}
{:error, _} = err -> err
end
end
@doc """
Compiles an Elixir source string and builds a graph from the expanded bytecode.
Unlike `string_to_graph/2`, this compiles the code first, so macro-expanded
constructs (try/rescue inside macros, `use` callbacks, etc.) are visible.
The source must define complete modules.
"""
@spec compiled_to_graph(String.t() | [{module(), binary()}], keyword()) ::
{:ok, graph()} | {:error, term()}
def compiled_to_graph(source_or_modules, opts \\ [])
def compiled_to_graph(source, opts) when is_binary(source) do
case Frontend.BEAM.from_compiled_string(source, opts) do
{:ok, nodes} -> {:ok, SystemDependence.build(nodes, opts)}
{:error, _} = err -> err
end
end
def compiled_to_graph(compiled, opts) when is_list(compiled) do
case Frontend.BEAM.from_compiled_modules(compiled, opts) do
{:ok, nodes} -> {:ok, SystemDependence.build(nodes, opts)}
end
end
@doc """
Builds a graph from an already-parsed Elixir AST.
Useful when you already have the AST (e.g. from Credo or ExDNA)
and don't want to re-parse source.
"""
@spec ast_to_graph(Macro.t(), keyword()) :: {:ok, graph()} | {:error, term()}
def ast_to_graph(ast, opts \\ []) do
counter = Counter.new()
file = Keyword.get(opts, :file, "nofile")
nodes = Frontend.Elixir.translate_ast(ast, counter, file)
{:ok, SystemDependence.build(List.wrap(nodes), opts)}
end
@doc """
Returns the children of a block in canonical order.
Independent sibling expressions are sorted by structural hash so
that reordered-but-equivalent blocks produce the same sequence.
Dependent expressions preserve their relative order.
Returns a list of `{node_id, ir_node}` pairs.
## Example
# These two blocks produce the same canonical order:
# a = 1; b = 2; c = a + b
# b = 2; a = 1; c = a + b
# Because a=1 and b=2 are independent, they get sorted,
# while c=a+b stays last (depends on both).
"""
@spec canonical_order(graph(), Reach.IR.Node.id()) :: [{Reach.IR.Node.id(), Reach.IR.Node.t()}]
def canonical_order(%SystemDependence{} = graph, block_node_id) do
block = node(graph, block_node_id)
case block do
%{type: type, children: children} when type in [:block, :clause, :function_def] ->
sort_preserving_deps(graph, children)
_ ->
case block do
%{id: id} = n -> [{id, n}]
nil -> []
end
end
end
defp sort_preserving_deps(graph, children) do
indexed = Enum.with_index(children)
# Build a dependency map: which children must come before which
must_precede =
for {a, i} <- indexed,
{b, j} <- indexed,
i < j,
not independent?(graph, a.id, b.id),
reduce: MapSet.new() do
acc -> MapSet.put(acc, {i, j})
end
# Topological sort respecting dependencies, breaking ties by structural hash
sorted_indices = topo_sort_with_hash(indexed, must_precede)
Enum.map(sorted_indices, fn i ->
{node, _} = Enum.at(indexed, i)
{node.id, node}
end)
end
defp topo_sort_with_hash(indexed, must_precede) do
n = length(indexed)
# Build adjacency + in-degree
{adj, in_deg} =
Enum.reduce(must_precede, {%{}, Map.new(0..(n - 1), &{&1, 0})}, fn {i, j}, {a, d} ->
{Map.update(a, i, [j], &[j | &1]), Map.update(d, j, 1, &(&1 + 1))}
end)
# Compute structural hash for each child (for deterministic tie-breaking)
hashes =
Map.new(indexed, fn {node, i} ->
{i, :erlang.phash2(node)}
end)
# Kahn's algorithm with hash-based priority
ready =
in_deg
|> Enum.filter(fn {_, d} -> d == 0 end)
|> Enum.map(&elem(&1, 0))
|> Enum.sort_by(&Map.get(hashes, &1))
do_topo_sort(ready, adj, in_deg, hashes, [])
end
defp do_topo_sort([], _adj, _in_deg, _hashes, acc), do: Enum.reverse(acc)
defp do_topo_sort([current | rest], adj, in_deg, hashes, acc) do
neighbors = Map.get(adj, current, [])
{new_ready, in_deg} =
Enum.reduce(neighbors, {[], in_deg}, fn neighbor, {ready, deg} ->
new_deg = Map.get(deg, neighbor, 0) - 1
deg = Map.put(deg, neighbor, new_deg)
if new_deg == 0, do: {[neighbor | ready], deg}, else: {ready, deg}
end)
next_ready =
(rest ++ new_ready)
|> Enum.sort_by(&Map.get(hashes, &1))
do_topo_sort(next_ready, adj, in_deg, hashes, [current | acc])
end
# --- Slicing ---
@doc """
Returns all node IDs that affect the given node (backward slice).
The backward slice answers: "what does this expression depend on?"
"""
def backward_slice(%SystemDependence{graph: g}, node_id) do
if Elixir.Graph.has_vertex?(g, node_id) do
Elixir.Graph.reaching(g, [node_id]) -- [node_id]
else
[]
end
end
@doc """
Returns all node IDs affected by the given node (forward slice).
The forward slice answers: "what does this expression influence?"
"""
def forward_slice(%SystemDependence{graph: g}, node_id) do
if Elixir.Graph.has_vertex?(g, node_id) do
Elixir.Graph.reachable(g, [node_id]) -- [node_id]
else
[]
end
end
@doc """
Returns node IDs on all paths from `source` to `sink`.
The chop answers: "how does A influence B?"
"""
def chop(graph, source, sink) do
fwd = forward_slice(graph, source) |> MapSet.new()
bwd = backward_slice(graph, sink) |> MapSet.new()
MapSet.intersection(fwd, bwd) |> MapSet.to_list()
end
# --- Independence ---
@doc """
Returns true if two expressions are independent.
Two expressions are independent when:
1. No data flows between them in either direction
2. They execute under the same conditions (same control dependencies)
3. Their side effects don't conflict
Independent expressions can be safely reordered.
"""
def independent?(%SystemDependence{graph: g, nodes: node_map} = sdg, id_x, id_y) do
data_only = build_data_graph(g)
ids_x = descendant_ids(node_map, id_x)
ids_y = descendant_ids(node_map, id_y)
not any_data_path?(data_only, ids_x, ids_y) and
not any_data_path?(data_only, ids_y, ids_x) and
same_control_deps?(sdg, id_x, id_y) and
not conflicting_effects?(node_map, id_x, id_y)
end
defp descendant_ids(node_map, id) do
case Map.get(node_map, id) do
nil -> [id]
node -> [id | Reach.IR.all_nodes(node) |> Enum.map(& &1.id)]
end
end
defp any_data_path?(data_only, from_ids, to_ids) do
Enum.any?(from_ids, fn from ->
Enum.any?(to_ids, fn to ->
from != to and data_path?(data_only, from, to)
end)
end)
end
# --- Querying nodes ---
@doc """
Returns all IR nodes, optionally filtered.
## Options
* `:type` — filter by node type (`:call`, `:match`, `:var`, etc.)
* `:module` — filter calls by module
* `:function` — filter calls by function name
* `:arity` — filter by arity
## Examples
Reach.nodes(graph, type: :call)
Reach.nodes(graph, type: :call, module: Enum)
Reach.nodes(graph, type: :call, module: Enum, function: :map, arity: 2)
"""
def nodes(graph, opts \\ [])
def nodes(%{nodes: node_map}, opts) do
node_map
|> Map.values()
|> filter_nodes(opts)
end
@doc """
Returns the IR node for a given ID, or `nil`.
"""
@spec node(graph(), Node.id()) :: Node.t() | nil
def node(%SystemDependence{nodes: nodes}, id), do: Map.get(nodes, id)
@doc """
Returns true if there's a data-dependence path from `source` to `sink`.
"""
def data_flows?(%SystemDependence{nodes: node_map} = graph, source_id, sink_id) do
source_ids = descendant_ids(node_map, source_id)
sink_ids = descendant_ids(node_map, sink_id) |> MapSet.new()
Enum.any?(source_ids, fn sid ->
forward_slice(graph, sid)
|> Enum.any?(&MapSet.member?(sink_ids, &1))
end)
end
@doc """
Returns true if `controller` has a control-dependence edge to `target`.
"""
def controls?(graph, controller_id, target_id) do
control_deps(graph, target_id)
|> Enum.any?(fn {id, _label} -> id == controller_id end)
end
@doc """
Returns true if there's any dependence path between two nodes.
"""
def depends?(graph, id_a, id_b) do
id_b in forward_slice(graph, id_a) or id_a in forward_slice(graph, id_b)
end
@doc """
Returns true if the node has data dependents (its value is used elsewhere).
"""
def has_dependents?(graph, node_id) do
forward_slice(graph, node_id) != []
end
@doc """
Returns true if the path from `source` to `sink` passes through
any node matching `predicate`.
Useful for taint analysis — check if sanitization occurs between
a source and sink.
"""
def passes_through?(graph, source_id, sink_id, predicate) do
chop(graph, source_id, sink_id)
|> Enum.any?(fn id ->
case node(graph, id) do
nil -> false
n -> predicate.(n)
end
end)
end
# --- Effects ---
@doc """
Returns true if the node is pure (no side effects).
"""
@spec pure?(Node.t()) :: boolean()
defdelegate pure?(node), to: Effects
@doc """
Returns the effect classification of a node.
Possible values: `:pure`, `:read`, `:write`, `:io`, `:send`,
`:receive`, `:exception`, `:nif`, `:unknown`.
"""
@spec classify_effect(Node.t()) :: Effects.effect()
defdelegate classify_effect(node), to: Effects, as: :classify
# --- Graph access ---
@doc """
Returns all dependence edges in the graph.
"""
@spec edges(graph()) :: [Elixir.Graph.Edge.t()]
def edges(%SystemDependence{graph: g}), do: Elixir.Graph.edges(g)
@doc """
Returns the control dependencies of a node.
Each entry is `{controller_node_id, label}`.
"""
@spec control_deps(graph(), Node.id()) :: [{Node.id(), term()}]
def control_deps(%SystemDependence{graph: g}, node_id) do
g
|> Elixir.Graph.in_edges(node_id)
|> Enum.filter(fn e -> match?({:control, _}, e.label) end)
|> Enum.map(fn e -> {e.v1, e.label} end)
end
@doc """
Returns the data dependencies of a node.
Each entry is `{source_node_id, variable_name}`.
"""
@spec data_deps(graph(), Node.id()) :: [{Node.id(), atom()}]
def data_deps(%SystemDependence{graph: g}, node_id) do
g
|> Elixir.Graph.in_edges(node_id)
|> Enum.filter(fn e -> match?({:data, _}, e.label) end)
|> Enum.map(fn e ->
{:data, var} = e.label
{e.v1, var}
end)
end
@doc """
Returns the per-function PDG for a `{module, function, arity}` tuple.
"""
@spec function_graph(graph(), {module() | nil, atom(), non_neg_integer()}) :: graph() | nil
defdelegate function_graph(graph, function_id), to: SystemDependence, as: :function_pdg
@doc """
Performs a context-sensitive backward slice through call boundaries.
Uses the Horwitz-Reps-Binkley two-phase algorithm to avoid
impossible paths through call sites.
"""
@spec context_sensitive_slice(graph(), Node.id()) :: [Node.id()]
defdelegate context_sensitive_slice(graph, node_id), to: SystemDependence
@doc """
Returns the call graph as a `Graph.t()`.
Vertices are `{module, function, arity}` tuples.
"""
@spec call_graph(graph()) :: Elixir.Graph.t()
def call_graph(%SystemDependence{call_graph: cg}), do: cg
@doc """
Exports the graph to DOT format for Graphviz visualization.
"""
@spec to_dot(graph()) :: {:ok, String.t()}
def to_dot(%SystemDependence{graph: g}), do: Elixir.Graph.to_dot(g)
@doc """
Returns the underlying `Graph.t()` (libgraph) for direct traversal.
Use this when you need graph operations that Reach doesn't expose —
path finding, subgraphs, BFS/DFS, topological sort, etc.
raw = Reach.to_graph(graph)
Graph.vertices(raw) |> length()
Graph.get_shortest_path(raw, id_a, id_b)
"""
@spec to_graph(graph()) :: Graph.t()
def to_graph(%SystemDependence{graph: g}), do: g
@doc """
Returns node IDs directly connected to `node_id`.
With no label filter, returns all neighbors (both incoming and outgoing).
With a label, returns only neighbors connected by edges with that label.
# All direct neighbors
Reach.neighbors(graph, node_id)
# Only nodes connected by :state_read edges
Reach.neighbors(graph, node_id, :state_read)
# Only data dependencies
Reach.neighbors(graph, node_id, {:data, :x})
"""
@spec neighbors(graph(), Node.id(), term() | nil) :: [Node.id()]
def neighbors(graph, node_id, label \\ nil)
def neighbors(%SystemDependence{graph: g}, node_id, nil) do
in_ids = g |> Elixir.Graph.in_neighbors(node_id)
out_ids = g |> Elixir.Graph.out_neighbors(node_id)
Enum.uniq(in_ids ++ out_ids)
end
def neighbors(%SystemDependence{graph: g}, node_id, label) do
in_edges = Elixir.Graph.in_edges(g, node_id)
out_edges = Elixir.Graph.out_edges(g, node_id)
(in_edges ++ out_edges)
|> Enum.filter(&match_label?(&1.label, label))
|> Enum.map(fn e -> if e.v1 == node_id, do: e.v2, else: e.v1 end)
|> Enum.uniq()
end
# --- Dead code ---
@doc """
Returns nodes whose values are never used and have no side effects.
A node is dead if:
1. It is pure (no side effects)
2. No observable output depends on it (return values or effectful calls)
"""
@spec dead_code(graph()) :: [Node.t()]
def dead_code(graph) do
observables = observable_nodes(graph)
observable_ids = MapSet.new(observables, & &1.id)
alive_ids =
observables
|> Enum.flat_map(&backward_slice(graph, &1.id))
|> MapSet.new()
|> MapSet.union(observable_ids)
all = nodes(graph)
# Also mark parents of alive nodes as alive
alive_ids = expand_alive_to_parents(all, alive_ids)
find_dead_nodes(all, alive_ids)
end
defp observable_nodes(graph) do
ret_ids = return_node_ids(graph)
nodes(graph)
|> Enum.filter(fn node ->
not pure?(node) or MapSet.member?(ret_ids, node.id)
end)
end
defp return_node_ids(graph) do
nodes(graph, type: :clause)
|> Enum.filter(&(&1.meta[:kind] == :function_clause))
|> Enum.map(fn clause ->
case List.last(clause.children) do
%{type: :block, children: children} -> List.last(children)
other -> other
end
end)
|> Enum.reject(&is_nil/1)
|> MapSet.new(& &1.id)
end
# --- Taint analysis ---
@doc """
Finds data flow paths from taint sources to dangerous sinks.
Returns a list of `%{source: node, sink: node, path: [node_id], sanitized: boolean}`
for each source→sink pair where data flows.
Sources, sinks, and sanitizers can be specified as keyword filters
(same format as `nodes/2`) or as predicate functions.
## Options
* `:sources` — keyword filter or predicate identifying taint sources
* `:sinks` — keyword filter or predicate identifying dangerous sinks
* `:sanitizers` — keyword filter or predicate identifying sanitization
points (optional)
## Examples
Reach.taint_analysis(graph,
sources: [type: :call, function: :get_param],
sinks: [type: :call, module: System, function: :cmd, arity: 2],
sanitizers: [type: :call, function: :sanitize]
)
# Predicates also work
Reach.taint_analysis(graph,
sources: &(&1.meta[:function] in [:params, :body_params]),
sinks: [type: :call, module: Ecto.Adapters.SQL]
)
"""
@spec taint_analysis(graph(), keyword()) :: [map()]
def taint_analysis(graph, opts) do
source_filter = Keyword.fetch!(opts, :sources)
sink_filter = Keyword.fetch!(opts, :sinks)
sanitizer_filter = Keyword.get(opts, :sanitizers)
all = nodes(graph)
sources = filter_by(all, source_filter)
sinks = filter_by(all, sink_filter)
sanitizer_pred = to_predicate(all, sanitizer_filter)
for source <- sources,
sink <- sinks,
data_flows?(graph, source.id, sink.id) do
path = chop(graph, source.id, sink.id)
sanitized =
sanitizer_pred != nil and
passes_through?(graph, source.id, sink.id, sanitizer_pred)
%{
source: source,
sink: sink,
path: path,
sanitized: sanitized
}
end
end
# --- Private ---
defp filter_by(nodes, filter) when is_list(filter), do: filter_nodes(nodes, filter)
defp filter_by(nodes, filter) when is_function(filter), do: Enum.filter(nodes, filter)
defp to_predicate(_all, nil), do: nil
defp to_predicate(_all, pred) when is_function(pred), do: pred
defp to_predicate(all, filter) when is_list(filter) do
matching_ids = filter_nodes(all, filter) |> MapSet.new(& &1.id)
fn node -> MapSet.member?(matching_ids, node.id) end
end
defp match_label?(label, label), do: true
defp match_label?({tag, _}, tag) when is_atom(tag), do: true
defp match_label?(_, _), do: false
defp expand_alive_to_parents(all_nodes, alive_ids) do
Enum.reduce(all_nodes, alive_ids, fn node, ids ->
child_ids = Enum.map(node.children, & &1.id)
if Enum.any?(child_ids, &MapSet.member?(ids, &1)) do
MapSet.put(ids, node.id)
else
ids
end
end)
end
defp find_dead_nodes(all_nodes, alive_ids) do
impure_ids = collect_impure_ids(all_nodes)
all_nodes
|> Enum.filter(&candidate_for_dead?/1)
|> Enum.reject(&(MapSet.member?(impure_ids, &1.id) or MapSet.member?(alive_ids, &1.id)))
end
defp collect_impure_ids(all_nodes) do
all_nodes
|> Enum.reject(&pure?/1)
|> Enum.flat_map(fn node -> [node.id | Enum.map(node.children, & &1.id)] end)
|> MapSet.new()
end
defp candidate_for_dead?(%Node{type: t} = node)
when t in [:call, :binary_op, :unary_op, :match] do
pure?(node) and not attribute_or_typespec?(node)
end
defp candidate_for_dead?(_), do: false
defp attribute_or_typespec?(%{type: :call, meta: %{function: f}})
when f in [:@, :__aliases__],
do: true
defp attribute_or_typespec?(_), do: false
defp filter_nodes(nodes, []), do: nodes
defp filter_nodes(nodes, [{:type, type} | rest]) do
nodes |> Enum.filter(&(&1.type == type)) |> filter_nodes(rest)
end
defp filter_nodes(nodes, [{:module, module} | rest]) do
nodes |> Enum.filter(&(&1.meta[:module] == module)) |> filter_nodes(rest)
end
defp filter_nodes(nodes, [{:function, function} | rest]) do
nodes |> Enum.filter(&(&1.meta[:function] == function)) |> filter_nodes(rest)
end
defp filter_nodes(nodes, [{:arity, arity} | rest]) do
nodes |> Enum.filter(&(&1.meta[:arity] == arity)) |> filter_nodes(rest)
end
defp filter_nodes(nodes, [_ | rest]) do
filter_nodes(nodes, rest)
end
defp build_data_graph(graph) do
graph
|> Elixir.Graph.edges()
|> Enum.filter(fn e ->
match?({:data, _}, e.label) or e.label in [:containment, :match_binding, :higher_order]
end)
|> then(&Elixir.Graph.add_edges(Elixir.Graph.new(), &1))
end
defp data_path?(data_graph, from, to) do
if Elixir.Graph.has_vertex?(data_graph, from) and Elixir.Graph.has_vertex?(data_graph, to) do
Elixir.Graph.get_shortest_path(data_graph, from, to) != nil
else
false
end
end
defp conflicting_effects?(node_map, id_x, id_y) do
case {Map.get(node_map, id_x), Map.get(node_map, id_y)} do
{%{} = x, %{} = y} ->
Effects.conflicting?(Effects.classify(x), Effects.classify(y))
_ ->
true
end
end
defp same_control_deps?(sdg, id_x, id_y) do
deps_x = control_deps(sdg, id_x) |> MapSet.new()
deps_y = control_deps(sdg, id_y) |> MapSet.new()
MapSet.equal?(deps_x, deps_y)
end
defp parse_string(source, :erlang, opts) do
Frontend.Erlang.parse_string(source, opts)
end
defp parse_string(source, _elixir, opts) do
Frontend.Elixir.parse(source, opts)
end
defp parse_and_build(source, language, opts) do
case parse_string(source, language, opts) do
{:ok, ir_nodes} -> {:ok, SystemDependence.build(ir_nodes, opts)}
{:error, _} = err -> err
end
end
defp language_from_extension(path) do
case Path.extname(path) do
ext when ext in [".erl", ".hrl"] -> :erlang
_ -> :elixir
end
end
defp module_from_path(path) do
path
|> Path.rootname()
|> Path.split()
|> Enum.drop_while(&(&1 != "lib"))
|> Enum.drop(1)
|> Enum.map_join(".", &Macro.camelize/1)
|> then(fn
"" -> nil
name -> Module.concat([name])
end)
end
end