# UNDF: UNDF-2026-000000006
# ans-0002: role _load_role_data() collections list O(C) membership tests per role load

**Severity:** LOW
**CWE:** CWE-407 (Algorithmic Complexity — linear membership test in hot loop)
**Speedup:** ~30x at C=50 collections (verified by unit test)
**Target:** Ansible (ansible/ansible)
**Files:**
- `lib/ansible/playbook/role/__init__.py:287` — `c not in self.collections` generator
- `lib/ansible/playbook/role/__init__.py:293` — two `not in self.collections` guards

## Description

`_load_role_data()` deduplicates collection names using a list:

```python
self.collections.extend((c for c in self._metadata.collections if c not in self.collections))
# ...
if 'ansible.builtin' not in self.collections and 'ansible.legacy' not in self.collections:
    self.collections.append(default_append_collection)
```

Each `not in self.collections` is O(C) where C = current list length.
Called once per role load; with C=50 collections this is ~155 list scans
instead of 3 hash lookups.

## Root Cause

`self.collections` is a list to preserve insertion order. The list is
queried for membership with O(C) `not in` tests.

Fix: maintain a parallel `_collections_set` (Python `set`) as a shadow
of `self.collections`. All membership tests become O(1). The list is
retained unchanged so that ordering semantics are preserved;
`_collections_set` is kept in sync at every mutation site.

## Patch

See `patch/ans-0002-role-collections-set.patch`

## Complexity Before

Per `not in self.collections` check: **O(C)**
Total per `_load_role_data()` call: **O(C)** (3 checks)

## Complexity After

Per `not in self._collections_set` check: **O(1)** average
Total: **O(1)**

## Reproduction

```
cd defects/ansible/unit && javac -d . AnsibleRoleTest.java && java -ea unit.AnsibleRoleTest
```

test3: C=50 candidates, defect=1585, fixed=52, ratio=30.5x
