Dependency analyzer for Godot/GDScript projects
Project description
gdcruiser
Static dependency analyzer for Godot projects, inspired by dependency-cruiser. Parses GDScript (.gd) and scene (.tscn) files to build a full dependency graph, detect circular dependencies, enforce architectural rules, and export the results as text, JSON, GraphViz DOT, or Mermaid diagrams. Designed to run locally or in CI pipelines.
Features
- Parses
.gd,.tscn, and.tresfiles —extends,preload(),load(),class_name, scene/resource scripts, and class-name expressions in code (var x: Foo,obj is Foo,Foo.STATIC_CONST) - Reads
[autoload]fromproject.godotand resolves singleton accesses (TurnManager.foo,EventBus.emit_signal(...)) to their backing scripts - Detects circular dependencies
- Resolves
class_namedeclarations to map symbolic inheritance - Configurable architectural rules (
forbidden,allowed,required,circular,orphan) via.gdcruiser.jsonorpyproject.toml - Multiple output formats: human-readable text, JSON, GraphViz DOT, and Mermaid
--excludeflag to filter paths from analysis- Non-zero exit code on cycles or rule violations (CI-friendly)
Installation
Requires Python 3.13+.
pip install gdcruiser
Or with uv:
uv tool install gdcruiser
Usage
gdcruiser [-h] [-f {text,json,dot,mermaid}] [-o FILE] [--no-cycles] [-v] [path]
| Option | Description |
|---|---|
path |
Godot project path (default: current directory) |
-f, --format |
Output format: text (default), json, dot, or mermaid |
-o, --output |
Write output to file instead of stdout |
--no-cycles |
Skip cycle detection |
--config FILE |
Path to config file (.gdcruiser.json or pyproject.toml) |
--validate-config |
Validate config file and exit |
--ignore-rules |
Skip rule evaluation |
--exclude PATTERN |
Regex pattern to exclude paths (can be repeated) |
-v, --verbose |
Verbose output |
Examples
Analyze the current directory:
gdcruiser .
Analyze a specific project and output JSON:
gdcruiser /path/to/godot/project -f json
Generate a GraphViz DOT file:
gdcruiser . -f dot -o deps.dot
dot -Tpng deps.dot -o deps.png
Generate a Mermaid diagram:
gdcruiser . -f mermaid
Output Formats
Text (default)
============================================================
GDScript Dependency Analysis
============================================================
Modules: 9
Dependencies: 8
----------------------------------------
CIRCULAR DEPENDENCIES (1 found)
----------------------------------------
Cycle 1:
-> res://cycle_b.gd
-> res://cycle_a.gd
-> res://cycle_b.gd (back to start)
----------------------------------------
MODULE DEPENDENCIES
----------------------------------------
res://player.gd
class_name: Player
extends_class: res://base_entity.gd:2
preload: res://inventory.gd:5
JSON
{
"graph": {
"modules": {
"res://player.gd": {
"path": "res://player.gd",
"class_name": "Player",
"dependencies": [
{
"target": "res://base_entity.gd",
"type": "extends_class",
"line": 2,
"resolved": true
}
]
}
},
"stats": {
"module_count": 9,
"dependency_count": 8
}
},
"cycles": [],
"symbols": {
"Player": "res://player.gd"
},
"errors": []
}
GraphViz DOT
digraph dependencies {
rankdir=LR;
node [shape=box, fontname="monospace"];
"res://player.gd" [label="Player\nplayer.gd"];
"res://base_entity.gd" [label="BaseEntity\nbase_entity.gd"];
"res://player.gd" -> "res://base_entity.gd" [label="extends"];
}
Nodes involved in cycles are highlighted in red.
Mermaid
Produces a Mermaid flowchart that renders natively on GitHub, GitLab, Notion, and other Markdown tools — no external tooling required.
gdcruiser tests/fixtures -f mermaid
graph LR
res_base_entity_gd["BaseEntity<br/>base_entity.gd"]
res_config_gd["config.gd"]
res_cycle_a_gd["CycleA<br/>cycle_a.gd"]
res_cycle_b_gd["CycleB<br/>cycle_b.gd"]
res_enemy_gd["Enemy<br/>enemy.gd"]
res_game_manager_gd["game_manager.gd"]
res_inventory_gd["Inventory<br/>inventory.gd"]
res_player_gd["Player<br/>player.gd"]
res_player_tscn["player.tscn"]
res_cycle_a_gd == preload ==> res_cycle_b_gd
res_cycle_b_gd == preload ==> res_cycle_a_gd
res_enemy_gd -- extends --> res_base_entity_gd
res_game_manager_gd -- preload --> res_player_tscn
res_game_manager_gd -- load --> res_config_gd
res_player_gd -- extends --> res_base_entity_gd
res_player_gd -- preload --> res_inventory_gd
res_player_tscn -- script --> res_player_gd
classDef cycle fill:#ffcccc,stroke:#cc0000
class res_cycle_a_gd,res_cycle_b_gd cycle
Cycle nodes are highlighted with a red fill, and cycle edges use thick arrows.
Supported Dependency Patterns
gdcruiser detects the following GDScript patterns:
| Pattern | Example |
|---|---|
extends (path) |
extends "res://path/to/script.gd" |
extends (class) |
extends ClassName |
class_name |
class_name MyClass |
preload() |
preload("res://path/to/file.gd") |
load() |
load("res://path/to/file.gd") |
| Typed annotation | var x: ClassName, func f(c: ClassName) -> ClassName: |
| Generic parameter | Array[ClassName], Dictionary[String, ClassName] |
| Type check / cast | obj is ClassName, obj as ClassName |
| Static / constant access | ClassName.method(), ClassName.CONSTANT |
Class-name references (the last four rows) are resolved against class_name declarations in the project. Built-in Godot types (Node, Vector2, Resource, …) and unresolved identifiers are filtered out so the graph stays clean.
For .tscn files, it detects scripts attached to nodes via [ext_resource].
For .tres files, it detects:
script_class="ClassName"in the[gd_resource]header (registers the resource under that class name in the symbol table).- Every
[ext_resource path="res://..."]reference — to scripts, sub-resources, or scenes — so architectural rules can constrain the data layer.
Autoload singletons
When a project.godot file is present at the project root, gdcruiser parses its [autoload] section:
[autoload]
TurnManager="*res://scripts/autoload/turn_manager.gd"
EventBus="*res://scripts/autoload/event_bus.gd"
Each identifier is registered as a global symbol pointing at its backing script (the leading * is stripped). Subsequent TurnManager.foo or EventBus.emit_signal(...) accesses anywhere in the project resolve to that script the same way class_name references do — so a rule like forbidden: { from: "scripts/skills/", to: "scripts/autoload/" } fires on its own without any extra configuration.
References inside string literals and comments are ignored.
Custom Rules
gdcruiser can enforce architectural rules on your dependency graph. Rules are defined in .gdcruiser.json (or pyproject.toml under [tool.gdcruiser]) and evaluated with every run unless --ignore-rules is passed.
Configuration file
gdcruiser looks for config files in this order:
--config <path>(explicit).gdcruiser.jsonin the project rootgdcruiser.jsonin the project root[tool.gdcruiser]section inpyproject.toml
Rule types
| Type | Meaning |
|---|---|
forbidden |
Dependency must not exist — a violation is raised when a matching edge is found |
allowed |
Only listed dependencies are permitted — anything else from a matching source is a violation |
required |
Dependency must exist — a violation is raised when a matching source has no edge to the target |
Rule fields
| Field | Type | Description |
|---|---|---|
name |
string | Rule identifier shown in violation messages |
severity |
"error" | "warn" | "info" | "ignore" |
Severity level (default: "error") |
comment |
string | Human-readable explanation |
from.path |
regex | Match source modules whose path matches |
from.pathNot |
regex | Exclude source modules whose path matches |
to.path |
regex | Match target modules whose path matches |
to.pathNot |
regex | Exclude target modules whose path matches |
circular |
bool | Flag the rule as a circular-dependency check (forbidden only) |
orphan |
bool | Flag the rule as an orphan-module check (forbidden only) |
Path patterns are regular expressions matched with re.search, so they can match any substring of the res:// path.
Examples
Forbid a dependency between two layers
Prevent UI scripts from importing game logic directly:
{
"forbidden": [
{
"name": "no-ui-to-core",
"comment": "UI must not depend on core game logic",
"from": { "path": "ui/" },
"to": { "path": "core/" }
}
]
}
Restrict allowed dependencies
Only allow player scripts to depend on modules under shared/ or components/:
{
"allowed": [
{
"name": "player-deps",
"from": { "path": "player/" },
"to": { "path": "(shared|components)/" }
}
]
}
Require a dependency
Ensure every autoload script preloads config.gd:
{
"required": [
{
"name": "autoloads-need-config",
"from": { "path": "autoloads/" },
"to": { "path": "config\\.gd$" }
}
]
}
Forbid circular dependencies in specific modules
{
"forbidden": [
{
"name": "no-cycles-in-core",
"circular": true,
"from": { "path": "core/" }
}
]
}
Detect orphan modules
Flag modules that have no dependencies and no dependents:
{
"forbidden": [
{
"name": "no-orphans",
"severity": "warn",
"orphan": true,
"from": { "pathNot": "autoloads/" }
}
]
}
Exclude paths from rule evaluation
{
"options": {
"exclude": ["addons/", "test/"]
},
"forbidden": [
{
"name": "no-core-to-ui",
"from": { "path": "core/" },
"to": { "path": "ui/" }
}
]
}
pyproject.toml equivalent
[tool.gdcruiser.options]
exclude = ["addons/"]
[[tool.gdcruiser.forbidden]]
name = "no-ui-to-core"
severity = "error"
comment = "UI must not depend on core game logic"
[tool.gdcruiser.forbidden.from]
path = "ui/"
[tool.gdcruiser.forbidden.to]
path = "core/"
Running with rules
gdcruiser . --config .gdcruiser.json # explicit config
gdcruiser . # auto-discovers config
gdcruiser . --ignore-rules # skip rule evaluation
gdcruiser . --config rules.json -f json # violations in JSON output
gdcruiser . --validate-config # validate config and exit
The exit code is non-zero when any error-severity violation is found.
Pre-commit Hook
gdcruiser can run as a pre-commit hook to catch dependency violations on every commit. Add the following to your Godot project's .pre-commit-config.yaml:
repos:
- repo: https://github.com/Thurbeen/gdcruiser
rev: v1.0.0 # use the latest release tag
hooks:
- id: gdcruiser
By default the hook runs gdcruiser . --no-cycles and only triggers when .gd or .tscn files are changed.
To customize the behavior, pass additional arguments via args:
hooks:
- id: gdcruiser
args: ["--config", "rules.json"]
hooks:
- id: gdcruiser
args: ["--exclude", "addons"]
If any rule violation is found the hook exits with a non-zero code and blocks the commit.
Development
Install dependencies:
uv sync
Set up pre-commit hooks:
uv run pre-commit install
This installs hooks for pre-commit, commit-msg, and post-checkout stages. On every commit the hooks will:
- Fix trailing whitespace and line endings
- Lint and format with Ruff
- Run the test suite with pytest
- Enforce Conventional Commits for commit messages
Run the CLI:
uv run gdcruiser
Run tests:
uv run pytest
Run linter:
uv run ruff check .
Format code:
uv run ruff format .
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gdcruiser-1.7.0.tar.gz.
File metadata
- Download URL: gdcruiser-1.7.0.tar.gz
- Upload date:
- Size: 22.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
751b4a854aae1b0a5ae39a10798e2f2e97be89ded0207b2f3d828e11753cae7e
|
|
| MD5 |
2e92f7ad2deff2a9c4e85b8f02808010
|
|
| BLAKE2b-256 |
59e3b065b046121625d00cc790faf9a08e19bdeaac9622d335657215c9003acb
|
Provenance
The following attestation bundles were made for gdcruiser-1.7.0.tar.gz:
Publisher:
cd.yaml on Thurbeen/gdcruiser
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gdcruiser-1.7.0.tar.gz -
Subject digest:
751b4a854aae1b0a5ae39a10798e2f2e97be89ded0207b2f3d828e11753cae7e - Sigstore transparency entry: 1402116836
- Sigstore integration time:
-
Permalink:
Thurbeen/gdcruiser@53ffa58bce5a97d7066b58cef9d9e2434141c222 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Thurbeen
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
cd.yaml@53ffa58bce5a97d7066b58cef9d9e2434141c222 -
Trigger Event:
push
-
Statement type:
File details
Details for the file gdcruiser-1.7.0-py3-none-any.whl.
File metadata
- Download URL: gdcruiser-1.7.0-py3-none-any.whl
- Upload date:
- Size: 33.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8b270694c5660f93f0c5daa03ece2a2c182378c388bab7de306544714a421e06
|
|
| MD5 |
8ced9391d3230d937d82261e1ed58e36
|
|
| BLAKE2b-256 |
46159a83e29468006f59897e934e8fc389118f20896deff5b8f0d443173706da
|
Provenance
The following attestation bundles were made for gdcruiser-1.7.0-py3-none-any.whl:
Publisher:
cd.yaml on Thurbeen/gdcruiser
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gdcruiser-1.7.0-py3-none-any.whl -
Subject digest:
8b270694c5660f93f0c5daa03ece2a2c182378c388bab7de306544714a421e06 - Sigstore transparency entry: 1402116945
- Sigstore integration time:
-
Permalink:
Thurbeen/gdcruiser@53ffa58bce5a97d7066b58cef9d9e2434141c222 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Thurbeen
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
cd.yaml@53ffa58bce5a97d7066b58cef9d9e2434141c222 -
Trigger Event:
push
-
Statement type: