Replication package for the paper "Django-Specific Code Smells: Catalog, Prevalence, and Automated Detection," accepted as a Short Paper at VEM 2026 (14th Workshop on Software Visualization, Evolution and Maintenance), co-located with CBSoft 2026.
Authors: Leopoldo Teixeira (lmt@cin.ufpe.br), Eduardo Luiz Silva (els6@cin.ufpe.br) — Centro de Informática, Universidade Federal de Pernambuco, Brazil.
Paper PDF:
paper.pdf
Django is a widely used Python web framework for database-backed application
development. As with any actively used framework, Django projects can
accumulate code smells: patterns that do not break functionality but
signal design or implementation choices likely to hurt maintainability,
readability, or performance. Existing Django-aware static analysis tools
primarily target schema- and configuration-level issues, leaving
performance-oriented and behavioral smells largely unaddressed. This
repository contains an open-source static analysis tool, derived from a
review of Django-focused grey literature, that detects five Django-specific
smells spanning the Model, View, and Template layers. It uses Python's
built-in ast module for checks within Python code, and a lightweight
regex-based tokenizer over HTML templates. The accompanying paper
investigates three research questions concerning the catalog's provenance
(RQ1), its prevalence in real-world projects (RQ2), and the reliability of
automated detection (RQ3). The tool was applied to a sample of 33 public
Django repositories of varying size, where it flagged 4,157 occurrences
across the five smells; a manual inspection of 50 flagged occurrences (44 of
them genuine, 88% overall precision) indicates the tool's detections are
reasonably but not uniformly reliable across smells.
.
├── src/ The checker itself (django-smells-checker)
│ ├── main.py CLI entry point / scan_directory()
│ ├── models.py SmellOccurrence result type
│ ├── thresholds.py Every magic number the checkers use, in one place
│ ├── report.py text / csv / json output formatters
│ ├── god_view.py God View check
│ ├── loop_object_creation.py Object creation inside a loop check
│ ├── missing_prefetch.py Missing prefetch_related/select_related check
│ ├── fat_models.py Fat Models check
│ └── template_logic.py Complex template logic check
├── tests/ pytest suite (one smelly + one clean fixture per checker)
│ ├── fixtures/ Standalone Django-style code/template snippets
│ └── test_*.py
├── evaluation/ RQ2: prevalence across a 33-repository sample
│ ├── generate_paper_data.py Aggregates results.json into the paper's RQ2 tables
│ ├── mine_repositories.py Searches GitHub for candidate repositories
│ ├── requirements.txt
│ ├── run_evaluation.py Clones each candidate at a pinned commit and scans it
│ └── data/
│ ├── candidates.json / .csv The 33 mined repositories (owner, URL, pinned commit_sha, size class)
│ ├── mining_manifest.json Reproducibility metadata for the mining run
│ ├── scans/ Per-repository raw occurrence output
│ ├── results.json Aggregated per-repo occurrence counts (input to generate_paper_data.py)
│ └── rq3_manual_validation.csv Manually reviewed verdicts used by validation/compute_precision.py
├── validation/ RQ3: manual validation / precision of a 50-occurrence sample
│ ├── sample_occurrences.py Draws the stratified sample from evaluation/data/
│ ├── build_review_html.py Builds the self-contained review.html tool from sample.json
│ ├── compute_precision.py Computes overall/per-smell precision from the reviewed CSV
│ ├── sample.json The 50 sampled occurrences (code snippet + context, ready for review)
│ └── review.html Self-contained (no server, no build step) manual-review UI
├── requirements.txt Dev/test tooling for src/ (pytest, ruff — the tool itself has zero runtime deps)
├── pytest.ini
├── ruff.toml
├── .env.example Template for the GITHUB_TOKEN used by mine_repositories.py
├── LICENSE MIT
├── paper.pdf Camera-ready PDF
└── README.md (rename of this file in the standalone artifact repository)
evaluation/data/repos/ (the actual cloned checkouts used to produce
results.json) is not committed — it is fully reproducible from
evaluation/data/candidates.json's pinned commit_sha per repository via
evaluation/run_evaluation.py (see below).
- Python ≥ 3.10
- The tool itself (
src/) has zero runtime dependencies — it only uses the standard library (ast,re,argparse,os). - Dev/test tooling (
pytest,ruff) and evaluation tooling (requests,python-dotenv) are pinned inrequirements.txtandevaluation/requirements.txtrespectively. gitonPATH— used byevaluation/run_evaluation.pyto clone candidate repositories (plaingit, no GitHub API calls, no token needed).- GitHub personal access token — only needed to run
evaluation/mine_repositories.pyand mine a new candidate list; not needed to reproduce RQ2/RQ3 from the already-pinnedcandidates.json.
Everything here runs on a standard laptop with no GPU. Running the checker itself and the Python analysis/aggregation scripts against the already-mined data completes in well under a minute. Re-cloning and re-scanning all 33 repositories from scratch (the full RQ2 pipeline) is the slow path — expect it to take from several minutes up to tens of minutes depending on network speed and the size of the larger repositories in the sample.
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt # dev/test tooling for src/
pip install -r evaluation/requirements.txt # only needed for evaluation/mine_repositories.pyVerify the test suite passes:
pytest
# Expected: all tests pass (one smelly + one clean fixture per checker, plus CLI/scoping tests)The checker can be run standalone against any Django project directory:
python src/main.py <path-to-django-project> [--format {text,csv,json}] [--output FILE]It walks <path> for .py and .html files, skips migrations, test code,
static/media assets, and vendored dependencies, and reports occurrences of
five smells:
| Smell | Check | Default threshold |
|---|---|---|
| God View | Function in a view file with too many lines or too much nesting | ≥ 50 lines, or nesting depth ≥ 3 |
| Object Creation in a Loop | .save()/.create() called inside a for loop instead of bulk_create/bulk_update |
any occurrence |
Missing prefetch_related/select_related |
Related-queryset call on a for loop's target variable whose source iterable was never prefetched |
any occurrence |
| Fat Models | models.Model subclass with too many/too-long non-trivial methods |
≥ 4 non-trivial methods, or ≥ 40 total lines across them |
| Complex Template Logic | {% if/for %} nesting, or use of a non-allowlisted custom template filter |
nesting depth > 3 |
Every run also prints a scan manifest to stderr (scanned_at, Python
version, CLI invocation, target path, and the target's own git commit, when
it is a git working tree) — reproducibility metadata separate from the
per-occurrence results. When --output FILE is given, the manifest is
additionally written to FILE.manifest.json.
Exact thresholds for all five checks live in src/thresholds.py.
RQ1 is a manual grey-literature review (practitioner blog posts, official Django documentation guidance, and Stack Overflow/Reddit discussion threads), not a script — its search methodology and findings are described directly in the paper.
Primary data: evaluation/data/results.json (per-repository occurrence
counts, one entry per repository with size_class, pinned commit_sha, and
a counts dict per smell) and evaluation/data/candidates.json (the 33
mined repositories with owner, URL, pinned commit, and size classification).
To regenerate the paper's tables/macros from the already-collected data:
source .venv/bin/activate
python evaluation/generate_paper_data.pyOutputs written (to paper/data/, created automatically if it doesn't
exist yet — this repository does not include the paper's own LaTeX source,
only the regenerated data/table fragments the paper pulls its numbers from;
the LaTeX macros are only meaningful alongside that source, so the
easiest way to check the numbers here is aggregated.json):
| File | Contents |
|---|---|
paper/data/results_macros.tex |
\newcommand macros for every RQ2 number cited in the paper |
paper/data/table_results.tex |
Per-size-class occurrence table |
paper/data/table_repos.tex |
The 33-repository sample table |
paper/data/aggregated.json |
Same aggregation, as plain JSON |
Key numbers to verify:
| Metric | Expected value |
|---|---|
| Repositories sampled | 33 (13 Small, 7 Medium, 13 Large) |
| Total occurrences flagged | 4,157 |
| God View occurrences | 2,584 (28 repos with ≥ 1) |
| Object Creation in a Loop occurrences | 304 (22 repos with ≥ 1) |
| Fat Model occurrences | 99 (16 repos with ≥ 1) |
Missing prefetch_related/select_related occurrences |
376 (17 repos with ≥ 1) |
| Complex Template Logic occurrences | 794 (26 repos with ≥ 1) |
results.json and evaluation/data/scans/ were produced by cloning each of
the 33 candidates at its pinned commit and scanning it with src/main.py.
To reproduce that from scratch against the same pinned commits:
source .venv/bin/activate
python evaluation/run_evaluation.py
# Clones each repo in evaluation/data/candidates.json into evaluation/data/repos/
# (gitignored) at its pinned commit_sha, scans it, and writes
# evaluation/data/scans/<repo>.json + evaluation/data/results.json.
# Idempotent/resumable: only re-clones a checkout that doesn't already match
# its pinned commit_sha.
python evaluation/generate_paper_data.pyTo mine a new candidate list instead of reusing the pinned one (not needed to reproduce the paper's numbers — only relevant if you want to extend the sample):
export GITHUB_TOKEN=<your-personal-access-token> # or place it in .env, see .env.example
python evaluation/mine_repositories.py --dry-run # search-only; prints a time/call estimate first
python evaluation/mine_repositories.py --output evaluation/data/candidates.csvPrimary data: evaluation/data/rq3_manual_validation.csv (the
manually-reviewed verdict for each of the 50 sampled occurrences) and
validation/sample.json (the sampled occurrences themselves, with code
snippet and surrounding context).
source .venv/bin/activate
python validation/compute_precision.pyOutputs written (to paper/data/, same caveat as above — the LaTeX
macros are only meaningful alongside the paper's own source):
| File | Contents |
|---|---|
paper/data/validation_macros.tex |
\newcommand macros for every RQ3 number cited in the paper |
paper/data/table_validation.tex |
Per-smell precision table |
Key numbers to verify:
| Smell | Genuine / Inspected | Precision |
|---|---|---|
| God View | 8 / 10 | 80% |
| Object Creation in a Loop | 10 / 10 | 100% |
| Fat Models | 9 / 10 | 90% |
Missing prefetch_related/select_related |
7 / 10 | 70% |
| Complex Template Logic | 10 / 10 | 100% |
| Overall | 44 / 50 | 88% |
The 50-occurrence sample was drawn with a fixed seed
(validation/sample_occurrences.py, SEED = 20260701) so it is
reproducible from evaluation/data/results.json and
evaluation/data/scans/ without re-running any manual step:
python validation/sample_occurrences.py
# Writes validation/sample.json (requires evaluation/data/repos/ checkouts
# on disk, for source-code context — re-run evaluation/run_evaluation.py first
# if you deleted them)
python validation/build_review_html.py
# Regenerates validation/review.html from the current sample.json
open validation/review.html # macOS; use xdg-open on Linuxreview.html is a single self-contained file (no server, no external
dependencies, no build step) with a one-occurrence-at-a-time review UI
(Genuine / Not Genuine, optional note, autosave to localStorage, running
precision summary) and an Export CSV button that produces a file in the
same shape as evaluation/data/rq3_manual_validation.csv.
pytest # all tests
ruff check src tests # lintingtests/fixtures/ holds one smelly and one clean example per checker (several
mirror the code listings in the paper itself); tests/test_<checker>.py
exercises each detect_* function directly, tests/test_main_cli.py covers
the CLI end-to-end, and tests/test_scan_scoping.py covers the
exclusion/scoping/deduplication logic in scan_directory.
Citation metadata is in CITATION.cff (GitHub renders a
"Cite this repository" button from it). It includes both the software
itself and the accompanying paper as a preferred-citation — prefer citing
the paper unless you're specifically referencing this codebase/version.
This repository is connected to Zenodo: each tagged GitHub Release mints a new archival snapshot with its own DOI.
- Concept DOI (always resolves to the latest version): 10.5281/zenodo.21840280
The accompanying paper does not yet have its own DOI — the VEM 2026
proceedings are still being prepared. It will be added here and to
CITATION.cff's preferred-citation once available.
MIT — see LICENSE.