Skip to content

Contributing

All contributions are welcome! Besides code contributions, this includes things like documentation improvements, bug reports, and feature requests.

You should first check if there is a GitHub issue already open or related to what you would like to contribute. If there is, please comment on that issue to let others know you are working on it. If there is not, please open a new issue to discuss your contribution.

Not all contributions need to start with an issue, such as typo fixes in documentation or version bumps to Python or Django that require no internal code changes, but generally, it is a good idea to open an issue first.

We adhere to a version of Django's Code of Conduct in all interactions and expect all contributors to do the same. Please read the Code of Conduct before contributing.

AI Policy

Someone is going to read your PR. Be considerate of that — make sure what you're submitting is something you'd want to review yourself.

AI tools are fine to use. How the code got written matters less than whether it's good. But you're the one submitting it, so you're the one responsible for it. If you can't explain a change, don't submit it. If you haven't tested it, don't submit it. If it doesn't fit the codebase, it's going to need rework.

Mentioning that you used AI is appreciated but not required. We'll assume good faith. That said, a pattern of sloppy submissions speaks for itself regardless of how the code was produced.

  • If you submit it, you own it. "The AI wrote it" is not an explanation.
  • Read the diff. Understand what it does and why.
  • Test your work. Don't submit code you haven't verified.
  • Make sure it fits — existing patterns, naming conventions, architecture.

The project includes an AGENTS.md file with guidelines for AI coding agents. If you're using an AI tool that supports it, point it there.

Before opening a PR, make sure the tests, clippy, formatting, and linting all pass.

Development

For a detailed look at how the codebase works — data flow, the Salsa database, the template pipeline — see ARCHITECTURE.md.

The project is written in Rust and uses static analysis to introspect Django projects. It uses a Cargo workspace with all crates under crates/. A few conventions to be aware of:

  • Dependency versions are centralized in [workspace.dependencies] in the root Cargo.toml. Individual crates reference them with dep.workspace = true and never specify versions directly.
  • Internal crates are listed before third-party crates in each crate's [dependencies], separated by a blank line. Both groups are kept in alphabetical order.
  • Lints are configured once in [workspace.lints] in the root Cargo.toml. Each crate opts in with [lints] workspace = true.
  • Versioning: Only the djls binary crate carries the release version. All library crates use version = "0.0.0".

Code contributions are welcome from developers of all backgrounds. Rust expertise is valuable for the LSP server and core components, but Python and Django developers should not be deterred by the Rust codebase — Django expertise is just as valuable. Understanding Django's internals and common development patterns helps inform what features would be most valuable.

So far it's all been built by a simple country CRUD web developer learning Rust along the way — send help!

First-time setup

Development requires Rustup, uv, and just. The checked-in Rust toolchain files select the required compiler and formatter versions.

Install the locked Python development dependencies without building the local Rust package, install the Git hooks, and prefetch the test corpus:

uv sync --frozen --no-install-project
uv tool install prek
prek install
just corpus sync

Install the prebuilt snapshot review tool used throughout the test suite:

curl --proto '=https' --tlsv1.2 -LsSf \
  https://github.com/mitsuhiko/insta/releases/download/1.48.0/cargo-insta-installer.sh | sh

The first test or lint run may still download a supported Python version, create Nox environments, compile the Rust workspace, and prepare hook environments. Subsequent runs reuse those artifacts. Amp orbs perform these setup steps automatically through .agents/setup.

Testing

Command Scope
cargo test -q Rust workspace tests using the currently discoverable Python environment
just test Rust workspace tests with the default Python 3.10 and Django 5.2 environment
just testall All supported Python and Django combinations
just e2e Python LSP end-to-end tests

just test and just testall create isolated Nox environments, install the selected Django version, synchronize the corpus, and then run Cargo. Use just testall for Python/Django support changes; the default just test is the normal local compatibility check.

Corpus

The corpus contains pinned source from real Django packages and projects under crates/djls-testing/.corpus. Tests synchronize it automatically, while just corpus sync can prefetch or repair it explicitly. The first sync downloads dozens of checksum-validated archives and can consume hundreds of megabytes; later syncs skip entries that already match crates/djls-testing/manifest.lock.

Snapshots

The test suite uses Insta snapshots extensively. After running the relevant tests, inspect pending changes interactively:

cargo insta review

To rerun snapshot tests, accept updates, and delete unreferenced snapshots in one noninteractive pass:

cargo insta test --accept --unreferenced delete

Always review snapshot changes before committing them.

Linting

Install the commit-time hooks with prek install. Run just lint for the all-files local gate; it formats the Justfiles and runs every configured hook, including Rustfmt and Clippy. CI runs the portable pre-commit hooks, Rustfmt, and Clippy as separate jobs.

Formatting

Formatting uses the dated nightly pinned in tools/rustfmt/rust-toolchain.toml because the repository enables unstable rustfmt options. Run just fmt so local formatting uses that toolchain. Update the pin deliberately when newer Rust syntax or rustfmt fixes require it, then review and commit any resulting formatting changes.

Visibility Audits

Hawk is an experimental Cargo lint from Astral that checks unnecessary public Rust visibility across a closed-world workspace. It is useful here because most crates are internal architecture layers behind the shipped djls binary.

Hawk is part of the local linting suite for keeping crate boundaries clean.

Setup

Install the Cargo subcommand Hawk expects. Rustup installs the compiler pinned for Hawk when the recipe runs.

curl --proto '=https' --tlsv1.2 -LsSf \
  https://github.com/astral-sh/hawk/releases/download/0.1.9/cargo-hawk-installer.sh | sh
Usage

Run Hawk through just rather than cargo hawk directly:

just hawk

The recipe uses the exact compiler pinned in tools/hawk/rust-toolchain.toml, as required by cargo-hawk 0.1.9, and isolates Hawk's instrumented builds to avoid astral-sh/hawk#74. Use it when changing public APIs, moving code across crates, or cleaning up visibility.

A Hawk run is more compile-intensive than normal linting. It checks the configured production binaries and workspace non-production targets, so a single run may perform multiple Cargo analysis passes. --fix can repeat analysis while visibility changes converge. That cost is expected: Hawk answers a different question than clippy, namely whether crate boundaries expose more API surface than the workspace needs.

The just hawk recipe keeps rustc dead-code and unused-import warnings quiet so the output stays focused on visibility. After applying Hawk fixes, run the normal lint and test checks; newly private code may expose cleanup work that belongs there.

Updating development tools

  • Update the primary compiler in rust-toolchain.toml.
  • Update the formatter nightly in tools/rustfmt/rust-toolchain.toml, then run just fmt and review any formatting changes.
  • Update cargo-hawk in .agents/setup and this guide together with its exact required compiler in tools/hawk/rust-toolchain.toml.
  • Keep the prebuilt cargo-insta version in .agents/setup and this guide aligned with the Insta version resolved in Cargo.lock.

Hawk uses compiler-private APIs, so even a patch-level compiler mismatch can make it fail before analysis.

Debug information

Development and test builds use line-table-only debug information to keep Rust build artifacts smaller while retaining file-and-line panic backtraces and source-level stepping. Compiler diagnostics and normal build and test behavior are unaffected, but native debuggers cannot inspect local variables and function arguments.

When full GDB or LLDB inspection is needed, override the relevant Cargo profile for that build:

CARGO_PROFILE_DEV_DEBUG=full cargo build
CARGO_PROFILE_TEST_DEBUG=full cargo test

Profiling

Setup

You'll need jq, rg, and the codspeed fork of valgrind (github.com/joshuadavidthomas/django-language-server/tree/main/not stock valgrind):

git clone --depth 1 https://github.com/CodSpeedHQ/valgrind-codspeed /tmp/valgrind-codspeed
cd /tmp/valgrind-codspeed
./autogen.sh
./configure --prefix=$HOME/.local
make -j$(https://github.com/joshuadavidthomas/django-language-server/tree/main/nproc)
make install

Make sure $HOME/.local/bin is on your PATH. Verify with:

valgrind --version  # should contain "codspeed"

Usage

The just dev profile command runs benchmarks under valgrind-codspeed, the same callgrind fork used in CI. It produces deterministic per-function instruction counts with call trees, and automatically strips harness overhead.

just dev profile <bench> [filter]

# Examples:
just dev profile diagnostics collect_diagnostics_realistic
just dev profile parser parse_template

Changelog

The project maintains a CHANGELOG.md following Keep a Changelog. All notable changes should be documented under the [Unreleased] heading in the appropriate section.

Sections (github.com/joshuadavidthomas/django-language-server/tree/main/use only those that apply):

  • Added — new features
  • Changed — changes in existing functionality
  • Deprecated — soon-to-be removed features
  • Removed — now removed features
  • Fixed — bug fixes
  • Security — vulnerability fixes

Writing entries:

  • Keep entries short and factual — describe what changed, not why
  • Use past tense verbs: "Added", "Fixed", "Removed", "Bumped", etc.
  • Wrap crate names, types, commands, and config keys in backticks
  • Prefix internal changes (github.com/joshuadavidthomas/django-language-server/tree/main/refactors, crate restructuring, CI) with **Internal**:
  • List user-facing entries before **Internal**: entries within each section

Examples:

### Added

- Added `diagnostics.severity` configuration option for configuring diagnostic severity levels.

### Changed

- Bumped Rust toolchain from 1.90 to 1.91.
- **Internal**: Extracted concrete Salsa database into new `djls-db` crate.

### Fixed

- Fixed false positive errors for quoted strings with spaces (https://github.com/joshuadavidthomas/django-language-server/tree/main/e.g., `{% translate "Contact the owner" %}`).

Version Updates

Python

The project uses noxfile.py as the single source of truth for supported Python versions. The PY_VERSIONS list in this file controls:

  • Auto-generated documentation: cogapp reads PY_VERSIONS to generate Python version classifiers in pyproject.toml and the supported versions list in README.md
  • CI/CD test matrix: GitHub Actions workflows call the gha_matrix nox session to generate the test matrix from PY_VERSIONS, so all supported Python versions are tested automatically
  • Local testing: The tests nox session uses PY_VERSIONS to parametrize test runs across all supported Python versions

Note

When possible, prefer submitting additions and removals in separate pull requests. This makes it easier to review changes and track the impact of each version update independently.

To update the list of supported Python versions:

  1. Update noxfile.py, adding or removing version constants as needed and updating the PY_VERSIONS list accordingly.

    For example, to add Python 3.14 and remove Python 3.9:

    -PY39 = "3.9"
     PY310 = "3.10"
     PY311 = "3.11"
     PY312 = "3.12"
     PY313 = "3.13"
    -PY_VERSIONS = [PY39, PY310, PY311, PY312, PY313]
    +PY314 = "3.14"
    +PY_VERSIONS = [PY310, PY311, PY312, PY313, PY314]
    
  2. Regenerate auto-generated content:

    just cog
    

    This updates:

  3. Update the lock file:

    uv lock
    
  4. Test the changes:

    just testall
    

    Use just testall rather than just test to ensure all Python versions are tested. The just test command only runs against the default versions (github.com/joshuadavidthomas/django-language-server/tree/main/the oldest supported Python and Django LTS) and won't catch issues with newly added versions.

    Alternatively, you can test only a specific Python version across all Django versions by nox directly:

    nox --python 3.14 --session tests
    
  5. Update CHANGELOG.md, adding entries for any versions added or removed.

Django

The project uses noxfile.py as the single source of truth for supported Django versions. The DJ_VERSIONS list in this file controls:

  • Auto-generated documentation: cogapp reads DJ_VERSIONS to generate Django version classifiers in pyproject.toml and the supported versions list in README.md
  • CI/CD test matrix: GitHub Actions workflows call the gha_matrix nox session to generate the test matrix from DJ_VERSIONS, so all supported Django versions are tested automatically
  • Local testing: The tests nox session uses DJ_VERSIONS to parametrize test runs across all supported Django versions

Note

When possible, prefer submitting additions and removals in separate pull requests. This makes it easier to review changes and track the impact of each version update independently.

To update the list of supported Django versions:

  1. Update noxfile.py, adding or removing version constants as needed and updating the DJ_VERSIONS list accordingly.

    For example, to add Django 6.1 and remove Django 4.2:

    -DJ42 = "4.2"
     DJ51 = "5.1"
     DJ52 = "5.2"
     DJ60 = "6.0"
    +DJ61 = "6.1"
     DJMAIN = "main"
    -DJ_VERSIONS = [DJ42, DJ51, DJ52, DJ60, DJMAIN]
    +DJ_VERSIONS = [DJ51, DJ52, DJ60, DJ61, DJMAIN]
    
  2. Update any Python version constraints in the should_skip() function if the new Django version has specific Python requirements.

  3. Regenerate auto-generated content:

    just cog
    

    This updates:

  4. Update the lock file:

    uv lock
    
  5. Test the changes:

    just testall
    

    Use just testall rather than just test to ensure all Django versions are tested. The just test command only runs against the default versions (github.com/joshuadavidthomas/django-language-server/tree/main/the oldest supported Python and Django LTS) and won't catch issues with newly added versions.

    Alternatively, you can test only a specific Django version across all Python versions by using nox directly:

    nox --session "tests(https://github.com/joshuadavidthomas/django-language-server/tree/main/django='6.1')"
    
  6. Update CHANGELOG.md, adding entries for any versions added or removed.

  7. For major Django releases: If adding support for a new major Django version (github.com/joshuadavidthomas/django-language-server/tree/main/e.g., Django 6.0), the language server version should be bumped to match per DjangoVer versioning. For example, when adding Django 6.0 support, bump the server from v5.x.x to v6.0.0.

Justfile

The repository includes a Justfile that provides all common development tasks with a consistent interface. Running just without arguments shows all available commands and their descriptions.

$ just
$ # just --list --list-submodules

Available recipes:
    bumpver *ARGS
    check *ARGS
    clean
    clippy *ARGS
    corpus *ARGS
    e2e *ARGS
    fixtures *ARGS
    fmt *ARGS
    hawk *ARGS
    lint *ARGS     # run pre-commit on all files
    run *ARGS
    test *ARGS
    testall *ARGS
    dev:
        debug                      # TODO: djls-tmux binary was removed in #214, this recipe needs updating
        explore FILENAME="djls.db"
        inspect
        profile bench filter=""    # Profile a bench with callgrind
        record FILENAME="djls.db"
    docs:
        build LOCATION="site" # Build documentation
        serve PORT="8000"     # Serve documentation locally