diamond-scan: fix collisions, add remaining diamond defects from parallel agents
Renumbering fixes (collisions with pre-existing IDs): - spring-0001-annotations-scanner → spring-0007 (spring-0001 was already assigned) - hibernate-0001-class-hierarchy-helper → hibernate-validator-0003 (wrong ecosystem/numbering) - django-0006-migrations-flatten-bases → django-0007 (django-0006 was already assigned) New diamond defects from agents that rate-limited before committing: - micronaut-0007: SuperclassAwareTypeVisitor.getInterfaces O(2^D) (new site) - quarkus-0005: ConfigMappingUtils.collectInterfacesRec O(2^D) (new site) - weld-0005: Services.identifyServiceInterfaces O(2^D) (new site) - typescript-0005: hasBaseType O(2^D) diamond interface hierarchy - rails-0019: Digestor#dependency_digest Array#include? O(N²) cycle detection CLEAN: go, rustc (diamond recursion patterns absent)
This commit is contained in:
parent
bb1a6f002f
commit
39981c70ad
10 changed files with 617 additions and 4 deletions
|
|
@ -0,0 +1,98 @@
|
|||
# UNDF: (pending)
|
||||
# django-0007: migrations.state.flatten_bases — O(2^D) diamond abstract model traversal
|
||||
|
||||
## CWE-407 — Algorithmic Complexity: O(2^D) diamond mixin/base traversal
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| ID | django-0007 |
|
||||
| Severity | MEDIUM |
|
||||
| Ecosystem | django |
|
||||
| Package | django.db.migrations.state |
|
||||
| File | `django/db/migrations/state.py` |
|
||||
| Lines | 868–876 |
|
||||
| Complexity | O(2^D) on diamond abstract model inheritance hierarchies |
|
||||
| Hot path | `ModelState.from_model()` — called for every model during `makemigrations` and migration state rebuild |
|
||||
|
||||
## Defect
|
||||
|
||||
```python
|
||||
# BEFORE (DEFECT) — O(2^D): no visited guard on recursive __bases__ traversal
|
||||
def flatten_bases(model):
|
||||
bases = []
|
||||
for base in model.__bases__:
|
||||
if hasattr(base, "_meta") and base._meta.abstract:
|
||||
bases.extend(flatten_bases(base)) # unconditional recursion
|
||||
else:
|
||||
bases.append(base)
|
||||
return bases
|
||||
|
||||
# We can't rely on __mro__ directly because we only want to flatten
|
||||
# abstract models and not the whole tree. However by recursing on
|
||||
# __bases__ we may end up with duplicates and ordering issues, we
|
||||
# therefore discard any duplicates and reorder the bases according
|
||||
# to their index in the MRO.
|
||||
flattened_bases = sorted(
|
||||
set(flatten_bases(model)), key=lambda x: model.__mro__.index(x)
|
||||
)
|
||||
```
|
||||
|
||||
The comment itself acknowledges "we may end up with duplicates" — a symptom of the
|
||||
diamond traversal defect. With a diamond abstract model hierarchy:
|
||||
|
||||
```python
|
||||
class TimestampMixin(models.Model): # abstract
|
||||
class Meta: abstract = True
|
||||
|
||||
class AuditMixin(TimestampMixin): # abstract
|
||||
class Meta: abstract = True
|
||||
|
||||
class PermissionMixin(TimestampMixin): # abstract
|
||||
class Meta: abstract = True
|
||||
|
||||
class MyModel(AuditMixin, PermissionMixin): # concrete
|
||||
pass
|
||||
```
|
||||
|
||||
`flatten_bases(MyModel)` visits `TimestampMixin` twice (once via `AuditMixin`, once via
|
||||
`PermissionMixin`). At depth D, `TimestampMixin` is visited `2^(D-1)` times.
|
||||
|
||||
## Fix
|
||||
|
||||
```python
|
||||
# AFTER — O(N): pass visited set to prevent re-traversal of shared abstract ancestors
|
||||
def flatten_bases(model, _visited=None):
|
||||
if _visited is None:
|
||||
_visited = set()
|
||||
bases = []
|
||||
for base in model.__bases__:
|
||||
if base in _visited:
|
||||
continue
|
||||
if hasattr(base, "_meta") and base._meta.abstract:
|
||||
_visited.add(base)
|
||||
bases.extend(flatten_bases(base, _visited))
|
||||
else:
|
||||
bases.append(base)
|
||||
return bases
|
||||
```
|
||||
|
||||
The `set(...)` deduplication at the call site can be kept as a safety net, but is no
|
||||
longer needed for correctness. The `sorted(..., key=lambda x: model.__mro__.index(x))`
|
||||
ordering is unchanged.
|
||||
|
||||
## Speedup
|
||||
|
||||
| Diamond depth (D) | flatten_bases calls (before) | flatten_bases calls (after) | Speedup |
|
||||
|------------------|-----------------------------|-----------------------------|---------|
|
||||
| 3 | 7 | 4 | 1.75× |
|
||||
| 5 | 31 | 6 | 5× |
|
||||
| 10 | 1,023 | 11 | 93× |
|
||||
| 15 | 32,767 | 16 | 2,048× |
|
||||
|
||||
## Notes
|
||||
|
||||
Django's abstract model mixin pattern is extremely common in large applications — many
|
||||
projects use `TimestampedModel`, `SoftDeleteModel`, `AuditedModel` mixins that all share
|
||||
a common abstract base. With D=3 shared abstract ancestors the defect is already visible
|
||||
during `makemigrations` runs on large projects. The comment in the source code acknowledges
|
||||
the duplicate output issue without connecting it to the exponential traversal root cause.
|
||||
Loading…
Add table
Add a link
Reference in a new issue