Skip to content

Repository files navigation

A Static Analysis Tool for Detecting Code Smells in Django Projects

DOI

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


Abstract

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.


Repository Organization

.
├── 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).


Requirements

  • 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 in requirements.txt and evaluation/requirements.txt respectively.
  • git on PATH — used by evaluation/run_evaluation.py to clone candidate repositories (plain git, no GitHub API calls, no token needed).
  • GitHub personal access token — only needed to run evaluation/mine_repositories.py and mine a new candidate list; not needed to reproduce RQ2/RQ3 from the already-pinned candidates.json.

Hardware

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.


Installation

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.py

Verify the test suite passes:

pytest
# Expected: all tests pass (one smelly + one clean fixture per checker, plus CLI/scoping tests)

Using the Tool

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.


Reproducing the Paper's Results

RQ1 — Catalog: what Django-specific code smells are reported in grey literature?

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.

RQ2 — Prevalence: how often do these smells occur in real-world Django projects?

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.py

Outputs 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)

Full RQ2 pipeline re-run (optional)

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.py

To 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.csv

RQ3 — Precision: how reliably does the tool's detection align with manual inspection?

Primary 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.py

Outputs 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%

Re-drawing the sample / re-running manual review (optional)

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 Linux

review.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.


Running the Test Suite

pytest                        # all tests
ruff check src tests          # linting

tests/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.


Citing This Artifact

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.

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.


License

MIT — see LICENSE.

About

An open-source, AST-based static analysis tool that detects five Django-specific code smells spanning the Model, View, and Template layers, using Python's built-in ast module for Python code and a lightweight regex-based tokenizer for HTML templates.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages