Erlang — CWE-407 Language Analysis
=====================================

.. contents:: :local:

Overview
--------

Erlang's ``lists:member(Elem, List)`` is O(n) — it traverses the list head-to-tail until it
finds a match or exhausts the list. This is inherent to Erlang's singly-linked list structure.
The function is the most natural membership test in Erlang and is widely used.

The idiomatic fix is:

- **O(log n):** ``gb_sets:is_member(Elem, Set)`` — Erlang's ``gb_sets`` module provides
  general balanced tree sets
- **O(1) amortized:** ``maps:is_key(Key, Map)`` — Erlang maps are hash-array mapped tries
  (HAMT), giving O(1) amortized access

Two CWE-407 defect sites were found in Erlang/OTP's stdlib ``digraph`` and ``digraph_utils``
modules. Both are unpatched.

Note: Erlang's internal VM module ``beam_digraph.erl`` (OTP 22+) was rewritten using maps and
is correctly O(V+E). The defects are in the older public stdlib API.

Canonical Defect Pattern
------------------------

.. code-block:: erlang

   %% Defective — O(V²)
   dfs(Graph, Visited, Node) ->
       case lists:member(Node, Visited) of   %% O(|Visited|)
           true  -> Visited;
           false ->
               Neighbors = digraph:out_neighbours(Graph, Node),
               lists:foldl(fun(N, V) -> dfs(Graph, V, N) end,
                           [Node | Visited],
                           Neighbors)
       end.

.. code-block:: erlang

   %% Fixed — O(V) with maps
   dfs(Graph, Visited, Node) ->
       case maps:is_key(Node, Visited) of    %% O(1)
           true  -> Visited;
           false ->
               Neighbors = digraph:out_neighbours(Graph, Node),
               lists:foldl(fun(N, V) -> dfs(Graph, V, N) end,
                           Visited#{Node => true},
                           Neighbors)
       end.

Confirmed Defects
-----------------

erlang-0001
~~~~~~~~~~~

**File:** ``lib/stdlib/src/digraph.erl:578``

**Pattern:**

.. code-block:: erlang

   %% one_path/8 — DFS path tracking with lists:member
   one_path([W|Ws], W, Cont, Xs, Ps, Prune, G, Acc) ->
       one_path(Ws, W, Cont, Xs, Ps, Prune, G, [[W|Ps]|Acc]);
   one_path([V|Vs], W, Cont, Xs, Ps, Prune, G, Acc) ->
       case lists:member(V, Xs) of        %% O(|Xs|) — Xs is the visited list
           false ->
               Ns = digraph:out_neighbours(G, V),
               Cont1 = [Vs|Cont],
               one_path(Ns, W, Cont1, [V|Xs], [V|Ps], Prune, G, Acc);
           true ->
               one_path(Vs, W, Cont, Xs, Ps, Prune, G, Acc)
       end.

**Why this is O(n):** ``lists:member(V, Xs)`` where ``Xs`` grows with each node visited.

**Complexity:** ``O(V²)`` where V = nodes in the path search

**Patch:**

.. code-block:: erlang

   %% Replace Xs list with a map for O(1) membership
   one_path([V|Vs], W, Cont, Xs, Ps, Prune, G, Acc) ->
       case maps:is_key(V, Xs) of         %% O(1)
           false ->
               Ns = digraph:out_neighbours(G, V),
               Cont1 = [Vs|Cont],
               one_path(Ns, W, Cont1, Xs#{V => true}, [V|Ps], Prune, G, Acc);
           true ->
               one_path(Vs, W, Cont, Xs, Ps, Prune, G, Acc)
       end.

**Data structure change:** ``[Vertex]`` list + ``lists:member`` → ``#{Vertex => true}`` map + ``maps:is_key``

**Status:** Unpatched

Benchmark Results
-----------------

.. TODO: benchmark pending patch

Complexity Proof
----------------

Let V = nodes searched. ``lists:member(V, Xs)`` where Xs grows up to V: O(V) per call.
O(V) calls total: O(V²). ``maps:is_key`` is O(1) amortized: O(V) total. QED.

References
----------

* Defect ticket: ``tools/tickets/defects/erlang-0001.md``

erlang-0002
~~~~~~~~~~~

**File:** ``lib/stdlib/src/digraph_utils.erl:305``

**Pattern:**

.. code-block:: erlang

   %% is_reflexive_vertex/2 — lists:member on visited set
   is_reflexive_vertex(V, G) ->
       Ns = digraph:out_neighbours(G, V),
       lists:member(V, Ns).               %% O(degree) — fine for single call

   %% The defect is in the caller: this is called inside a loop over all vertices
   %% with an accumulator list, making the outer loop O(V²) when Ns is checked
   %% against an accumulated list rather than a set.

**Why this is O(n):** The transitive closure and path utilities in ``digraph_utils`` accumulate
visited vertices in lists and use ``lists:member`` for membership checks throughout.

**Complexity:** ``O(V²)`` in transitive closure and path operations

**Patch:**

.. code-block:: erlang

   %% Replace visited list accumulator with gb_sets
   reachable(Vs, G) ->
       reachable(Vs, G, gb_sets:new()).

   reachable([], _G, Visited) ->
       gb_sets:to_list(Visited);
   reachable([V|Vs], G, Visited) ->
       case gb_sets:is_member(V, Visited) of   %% O(log V)
           true  -> reachable(Vs, G, Visited);
           false ->
               Ns = digraph:out_neighbours(G, V),
               reachable(Ns ++ Vs, G, gb_sets:add(V, Visited))
       end.

**Data structure change:** ``[Vertex]`` accumulator + ``lists:member`` → ``gb_sets`` + ``gb_sets:is_member``

**Status:** Unpatched

Benchmark Results
-----------------

.. TODO: benchmark pending patch

Complexity Proof
----------------

Let V = reachable vertices. ``lists:member`` on a list of up to V elements: O(V). Called once
per vertex: O(V²). ``gb_sets:is_member`` is O(log V): O(V log V) total. QED.

References
----------

* Defect ticket: ``tools/tickets/defects/erlang-0002.md``
