Python — CWE-407 Language Analysis
====================================

.. contents:: :local:

Overview
--------

Python's list ``in`` operator (``x in list``) is O(n) — it delegates to ``list.__contains__``,
which performs a linear scan. This is the canonical CWE-407 pattern in Python. The fix is to
use a Python ``set`` (backed by a hash table): ``x in set`` is O(1) average.

The Python standard library itself includes both correct and defective implementations: the
``graphlib.TopologicalSorter`` (added in Python 3.9) is correctly implemented with set-backed
visited state, while the older ``sccutils.py`` in the PEG parser generator carried the defect
until it was patched.

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

.. code-block:: python

   # Defective — O(V²)
   path = []
   def dfs(node):
       if node in path:      # O(|path|) linear scan
           return            # cycle detected
       path.append(node)
       for neighbor in graph[node]:
           dfs(neighbor)
       path.pop()

.. code-block:: python

   # Fixed — O(V)
   on_path = set()
   def dfs(node):
       if node in on_path:   # O(1) hash lookup
           return
       on_path.add(node)
       for neighbor in graph[node]:
           dfs(neighbor)
       on_path.discard(node)

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

cpython-0001
~~~~~~~~~~~~

CPython PEG parser generator ``sccutils.py``. See :doc:`../compiler/cpython-peg` for full
analysis. **Status: Patched.**

distlib-0001
~~~~~~~~~~~~

**File:** ``distlib/util.py:1180,1204``

**Pattern:**

.. code-block:: python

   # Tarjan SCC in pip's dependency resolver — 'successor in stack' where stack is a list
   def strong_connections(graph):
       stack = []
       lowlinks = {}
       index = {}
       sccs = []

       def strongconnect(v):
           index[v] = lowlinks[v] = len(index)
           stack.append(v)
           for w in graph.get(v, []):
               if w not in index:
                   strongconnect(w)
                   lowlinks[v] = min(lowlinks[v], lowlinks[w])
               elif w in stack:   # O(V) — list membership
                   lowlinks[v] = min(lowlinks[v], index[w])
           if lowlinks[v] == index[v]:
               scc = []
               while True:
                   w = stack.pop()
                   scc.append(w)
                   if w == v:
                       break
               sccs.append(scc)

       for v in graph:
           if v not in index:
               strongconnect(v)
       return sccs

**Why this is O(n):** ``w in stack`` where ``stack`` is a list; same pattern as cpython-0001.
This code runs during ``pip install`` dependency resolution for every package with a dependency
graph large enough to form cycles (circular dependencies, sdist build deps).

**Complexity:** ``O(V²)`` where V = number of packages in the dependency graph

**Patch:**

.. code-block:: python

   def strong_connections(graph):
       stack = []
       on_stack = set()   # O(1) membership
       lowlinks = {}
       index = {}
       sccs = []

       def strongconnect(v):
           index[v] = lowlinks[v] = len(index)
           stack.append(v)
           on_stack.add(v)
           for w in graph.get(v, []):
               if w not in index:
                   strongconnect(w)
                   lowlinks[v] = min(lowlinks[v], lowlinks[w])
               elif w in on_stack:   # O(1)
                   lowlinks[v] = min(lowlinks[v], index[w])
           if lowlinks[v] == index[v]:
               scc = []
               while True:
                   w = stack.pop()
                   on_stack.discard(w)
                   scc.append(w)
                   if w == v:
                       break
               sccs.append(scc)

       for v in graph:
           if v not in index:
               strongconnect(v)
       return sccs

**Data structure change:** ``list`` stack + ``in`` → ``set`` on_stack + ``in``

**Status:** Unpatched

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

.. TODO: benchmark pending patch

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

Identical to cpython-0001. Let V = package nodes. ``w in stack`` costs O(V) per call.
O(V²) total with list; O(V) total with set. QED.

References
----------

* :doc:`../compiler/cpython-peg`
* :doc:`../tool-harness/pip-distlib`

Clean Python Implementations (Reference)
-----------------------------------------

- ``graphlib.TopologicalSorter`` (Python 3.9+): set-backed, correct O(V+E)
- ``ast`` cycle checking: set-backed throughout
