gdcruiser 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gdcruiser-1.1.0/PKG-INFO +207 -0
- gdcruiser-1.1.0/README.md +198 -0
- gdcruiser-1.1.0/pyproject.toml +37 -0
- gdcruiser-1.1.0/src/gdcruiser/__init__.py +3 -0
- gdcruiser-1.1.0/src/gdcruiser/analyzer.py +86 -0
- gdcruiser-1.1.0/src/gdcruiser/cli.py +186 -0
- gdcruiser-1.1.0/src/gdcruiser/config/__init__.py +17 -0
- gdcruiser-1.1.0/src/gdcruiser/config/loader.py +140 -0
- gdcruiser-1.1.0/src/gdcruiser/config/models.py +72 -0
- gdcruiser-1.1.0/src/gdcruiser/config/validator.py +102 -0
- gdcruiser-1.1.0/src/gdcruiser/graph/__init__.py +5 -0
- gdcruiser-1.1.0/src/gdcruiser/graph/cycles.py +58 -0
- gdcruiser-1.1.0/src/gdcruiser/graph/dependency.py +56 -0
- gdcruiser-1.1.0/src/gdcruiser/graph/node.py +46 -0
- gdcruiser-1.1.0/src/gdcruiser/output/__init__.py +5 -0
- gdcruiser-1.1.0/src/gdcruiser/output/dot.py +83 -0
- gdcruiser-1.1.0/src/gdcruiser/output/json.py +19 -0
- gdcruiser-1.1.0/src/gdcruiser/output/text.py +76 -0
- gdcruiser-1.1.0/src/gdcruiser/output/violations.py +60 -0
- gdcruiser-1.1.0/src/gdcruiser/parser/__init__.py +5 -0
- gdcruiser-1.1.0/src/gdcruiser/parser/gdscript.py +204 -0
- gdcruiser-1.1.0/src/gdcruiser/parser/patterns.py +34 -0
- gdcruiser-1.1.0/src/gdcruiser/parser/tscn.py +44 -0
- gdcruiser-1.1.0/src/gdcruiser/rules/__init__.py +12 -0
- gdcruiser-1.1.0/src/gdcruiser/rules/engine.py +207 -0
- gdcruiser-1.1.0/src/gdcruiser/rules/matcher.py +35 -0
- gdcruiser-1.1.0/src/gdcruiser/rules/models.py +81 -0
- gdcruiser-1.1.0/src/gdcruiser/scanner.py +29 -0
- gdcruiser-1.1.0/src/gdcruiser/symbols/__init__.py +3 -0
- gdcruiser-1.1.0/src/gdcruiser/symbols/table.py +27 -0
gdcruiser-1.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: gdcruiser
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Dependency analyzer for Godot/GDScript projects
|
|
5
|
+
Author: LeTuR
|
|
6
|
+
Author-email: LeTuR <magicletur@protonmail.com>
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# gdcruiser
|
|
11
|
+
|
|
12
|
+
A dependency analyzer for Godot/GDScript projects. Scans your project to build a dependency graph, detect circular dependencies, and export visualizations.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- Analyzes `.gd` (GDScript) and `.tscn` (scene) files
|
|
17
|
+
- Detects circular dependencies
|
|
18
|
+
- Resolves `class_name` declarations to map symbolic inheritance
|
|
19
|
+
- Multiple output formats: human-readable text, JSON, and GraphViz DOT
|
|
20
|
+
- Returns non-zero exit code when cycles are detected (CI-friendly)
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
Requires Python 3.13+.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install gdcruiser
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv tool install gdcruiser
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
gdcruiser [-h] [-f {text,json,dot}] [-o FILE] [--no-cycles] [-v] [path]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
| Option | Description |
|
|
43
|
+
|--------|-------------|
|
|
44
|
+
| `path` | Godot project path (default: current directory) |
|
|
45
|
+
| `-f, --format` | Output format: `text` (default), `json`, or `dot` |
|
|
46
|
+
| `-o, --output` | Write output to file instead of stdout |
|
|
47
|
+
| `--no-cycles` | Skip cycle detection |
|
|
48
|
+
| `-v, --verbose` | Verbose output |
|
|
49
|
+
|
|
50
|
+
### Examples
|
|
51
|
+
|
|
52
|
+
Analyze the current directory:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
gdcruiser .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Analyze a specific project and output JSON:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
gdcruiser /path/to/godot/project -f json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Generate a GraphViz DOT file:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
gdcruiser . -f dot -o deps.dot
|
|
68
|
+
dot -Tpng deps.dot -o deps.png
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Output Formats
|
|
72
|
+
|
|
73
|
+
### Text (default)
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
============================================================
|
|
77
|
+
GDScript Dependency Analysis
|
|
78
|
+
============================================================
|
|
79
|
+
|
|
80
|
+
Modules: 9
|
|
81
|
+
Dependencies: 8
|
|
82
|
+
|
|
83
|
+
----------------------------------------
|
|
84
|
+
CIRCULAR DEPENDENCIES (1 found)
|
|
85
|
+
----------------------------------------
|
|
86
|
+
|
|
87
|
+
Cycle 1:
|
|
88
|
+
-> res://cycle_b.gd
|
|
89
|
+
-> res://cycle_a.gd
|
|
90
|
+
-> res://cycle_b.gd (back to start)
|
|
91
|
+
|
|
92
|
+
----------------------------------------
|
|
93
|
+
MODULE DEPENDENCIES
|
|
94
|
+
----------------------------------------
|
|
95
|
+
|
|
96
|
+
res://player.gd
|
|
97
|
+
class_name: Player
|
|
98
|
+
extends_class: res://base_entity.gd:2
|
|
99
|
+
preload: res://inventory.gd:5
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### JSON
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"graph": {
|
|
107
|
+
"modules": {
|
|
108
|
+
"res://player.gd": {
|
|
109
|
+
"path": "res://player.gd",
|
|
110
|
+
"class_name": "Player",
|
|
111
|
+
"dependencies": [
|
|
112
|
+
{
|
|
113
|
+
"target": "res://base_entity.gd",
|
|
114
|
+
"type": "extends_class",
|
|
115
|
+
"line": 2,
|
|
116
|
+
"resolved": true
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"stats": {
|
|
122
|
+
"module_count": 9,
|
|
123
|
+
"dependency_count": 8
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"cycles": [],
|
|
127
|
+
"symbols": {
|
|
128
|
+
"Player": "res://player.gd"
|
|
129
|
+
},
|
|
130
|
+
"errors": []
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### GraphViz DOT
|
|
135
|
+
|
|
136
|
+
```dot
|
|
137
|
+
digraph dependencies {
|
|
138
|
+
rankdir=LR;
|
|
139
|
+
node [shape=box, fontname="monospace"];
|
|
140
|
+
|
|
141
|
+
"res://player.gd" [label="Player\nplayer.gd"];
|
|
142
|
+
"res://base_entity.gd" [label="BaseEntity\nbase_entity.gd"];
|
|
143
|
+
|
|
144
|
+
"res://player.gd" -> "res://base_entity.gd" [label="extends"];
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Nodes involved in cycles are highlighted in red.
|
|
149
|
+
|
|
150
|
+
## Supported Dependency Patterns
|
|
151
|
+
|
|
152
|
+
gdcruiser detects the following GDScript patterns:
|
|
153
|
+
|
|
154
|
+
| Pattern | Example |
|
|
155
|
+
|---------|---------|
|
|
156
|
+
| `extends` (path) | `extends "res://path/to/script.gd"` |
|
|
157
|
+
| `extends` (class) | `extends ClassName` |
|
|
158
|
+
| `class_name` | `class_name MyClass` |
|
|
159
|
+
| `preload()` | `preload("res://path/to/file.gd")` |
|
|
160
|
+
| `load()` | `load("res://path/to/file.gd")` |
|
|
161
|
+
|
|
162
|
+
For `.tscn` files, it detects scripts attached to nodes via `[ext_resource]`.
|
|
163
|
+
|
|
164
|
+
## Development
|
|
165
|
+
|
|
166
|
+
Install dependencies:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
uv sync
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Set up pre-commit hooks:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
uv run pre-commit install
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
This installs hooks for `pre-commit`, `commit-msg`, and `post-checkout` stages. On every commit the hooks will:
|
|
179
|
+
|
|
180
|
+
- Fix trailing whitespace and line endings
|
|
181
|
+
- Lint and format with [Ruff](https://docs.astral.sh/ruff/)
|
|
182
|
+
- Run the test suite with pytest
|
|
183
|
+
- Enforce [Conventional Commits](https://www.conventionalcommits.org/) for commit messages
|
|
184
|
+
|
|
185
|
+
Run the CLI:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
uv run gdcruiser
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Run tests:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
uv run pytest
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Run linter:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
uv run ruff check .
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Format code:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
uv run ruff format .
|
|
207
|
+
```
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# gdcruiser
|
|
2
|
+
|
|
3
|
+
A dependency analyzer for Godot/GDScript projects. Scans your project to build a dependency graph, detect circular dependencies, and export visualizations.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Analyzes `.gd` (GDScript) and `.tscn` (scene) files
|
|
8
|
+
- Detects circular dependencies
|
|
9
|
+
- Resolves `class_name` declarations to map symbolic inheritance
|
|
10
|
+
- Multiple output formats: human-readable text, JSON, and GraphViz DOT
|
|
11
|
+
- Returns non-zero exit code when cycles are detected (CI-friendly)
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Requires Python 3.13+.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install gdcruiser
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv tool install gdcruiser
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
gdcruiser [-h] [-f {text,json,dot}] [-o FILE] [--no-cycles] [-v] [path]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| Option | Description |
|
|
34
|
+
|--------|-------------|
|
|
35
|
+
| `path` | Godot project path (default: current directory) |
|
|
36
|
+
| `-f, --format` | Output format: `text` (default), `json`, or `dot` |
|
|
37
|
+
| `-o, --output` | Write output to file instead of stdout |
|
|
38
|
+
| `--no-cycles` | Skip cycle detection |
|
|
39
|
+
| `-v, --verbose` | Verbose output |
|
|
40
|
+
|
|
41
|
+
### Examples
|
|
42
|
+
|
|
43
|
+
Analyze the current directory:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
gdcruiser .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Analyze a specific project and output JSON:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
gdcruiser /path/to/godot/project -f json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Generate a GraphViz DOT file:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
gdcruiser . -f dot -o deps.dot
|
|
59
|
+
dot -Tpng deps.dot -o deps.png
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Output Formats
|
|
63
|
+
|
|
64
|
+
### Text (default)
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
============================================================
|
|
68
|
+
GDScript Dependency Analysis
|
|
69
|
+
============================================================
|
|
70
|
+
|
|
71
|
+
Modules: 9
|
|
72
|
+
Dependencies: 8
|
|
73
|
+
|
|
74
|
+
----------------------------------------
|
|
75
|
+
CIRCULAR DEPENDENCIES (1 found)
|
|
76
|
+
----------------------------------------
|
|
77
|
+
|
|
78
|
+
Cycle 1:
|
|
79
|
+
-> res://cycle_b.gd
|
|
80
|
+
-> res://cycle_a.gd
|
|
81
|
+
-> res://cycle_b.gd (back to start)
|
|
82
|
+
|
|
83
|
+
----------------------------------------
|
|
84
|
+
MODULE DEPENDENCIES
|
|
85
|
+
----------------------------------------
|
|
86
|
+
|
|
87
|
+
res://player.gd
|
|
88
|
+
class_name: Player
|
|
89
|
+
extends_class: res://base_entity.gd:2
|
|
90
|
+
preload: res://inventory.gd:5
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### JSON
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"graph": {
|
|
98
|
+
"modules": {
|
|
99
|
+
"res://player.gd": {
|
|
100
|
+
"path": "res://player.gd",
|
|
101
|
+
"class_name": "Player",
|
|
102
|
+
"dependencies": [
|
|
103
|
+
{
|
|
104
|
+
"target": "res://base_entity.gd",
|
|
105
|
+
"type": "extends_class",
|
|
106
|
+
"line": 2,
|
|
107
|
+
"resolved": true
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"stats": {
|
|
113
|
+
"module_count": 9,
|
|
114
|
+
"dependency_count": 8
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"cycles": [],
|
|
118
|
+
"symbols": {
|
|
119
|
+
"Player": "res://player.gd"
|
|
120
|
+
},
|
|
121
|
+
"errors": []
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### GraphViz DOT
|
|
126
|
+
|
|
127
|
+
```dot
|
|
128
|
+
digraph dependencies {
|
|
129
|
+
rankdir=LR;
|
|
130
|
+
node [shape=box, fontname="monospace"];
|
|
131
|
+
|
|
132
|
+
"res://player.gd" [label="Player\nplayer.gd"];
|
|
133
|
+
"res://base_entity.gd" [label="BaseEntity\nbase_entity.gd"];
|
|
134
|
+
|
|
135
|
+
"res://player.gd" -> "res://base_entity.gd" [label="extends"];
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Nodes involved in cycles are highlighted in red.
|
|
140
|
+
|
|
141
|
+
## Supported Dependency Patterns
|
|
142
|
+
|
|
143
|
+
gdcruiser detects the following GDScript patterns:
|
|
144
|
+
|
|
145
|
+
| Pattern | Example |
|
|
146
|
+
|---------|---------|
|
|
147
|
+
| `extends` (path) | `extends "res://path/to/script.gd"` |
|
|
148
|
+
| `extends` (class) | `extends ClassName` |
|
|
149
|
+
| `class_name` | `class_name MyClass` |
|
|
150
|
+
| `preload()` | `preload("res://path/to/file.gd")` |
|
|
151
|
+
| `load()` | `load("res://path/to/file.gd")` |
|
|
152
|
+
|
|
153
|
+
For `.tscn` files, it detects scripts attached to nodes via `[ext_resource]`.
|
|
154
|
+
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
Install dependencies:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
uv sync
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Set up pre-commit hooks:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
uv run pre-commit install
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
This installs hooks for `pre-commit`, `commit-msg`, and `post-checkout` stages. On every commit the hooks will:
|
|
170
|
+
|
|
171
|
+
- Fix trailing whitespace and line endings
|
|
172
|
+
- Lint and format with [Ruff](https://docs.astral.sh/ruff/)
|
|
173
|
+
- Run the test suite with pytest
|
|
174
|
+
- Enforce [Conventional Commits](https://www.conventionalcommits.org/) for commit messages
|
|
175
|
+
|
|
176
|
+
Run the CLI:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
uv run gdcruiser
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Run tests:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
uv run pytest
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Run linter:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
uv run ruff check .
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Format code:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
uv run ruff format .
|
|
198
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "gdcruiser"
|
|
3
|
+
version = "1.1.0"
|
|
4
|
+
description = "Dependency analyzer for Godot/GDScript projects"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "LeTuR", email = "magicletur@protonmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
dependencies = []
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
gdcruiser = "gdcruiser:main"
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["uv_build>=0.9.13,<0.10.0"]
|
|
17
|
+
build-backend = "uv_build"
|
|
18
|
+
|
|
19
|
+
[dependency-groups]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=9.0.2",
|
|
22
|
+
"ruff>=0.15.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[tool.semantic_release]
|
|
26
|
+
version_toml = []
|
|
27
|
+
branch = "main"
|
|
28
|
+
tag_format = "v{version}"
|
|
29
|
+
commit_message = ""
|
|
30
|
+
|
|
31
|
+
[tool.semantic_release.commit_parser_options]
|
|
32
|
+
allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "style", "test"]
|
|
33
|
+
minor_tags = ["feat"]
|
|
34
|
+
patch_tags = ["fix", "perf"]
|
|
35
|
+
|
|
36
|
+
[tool.semantic_release.remote]
|
|
37
|
+
type = "github"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from .scanner import Scanner
|
|
5
|
+
from .parser.gdscript import GDScriptParser
|
|
6
|
+
from .parser.tscn import TscnParser
|
|
7
|
+
from .graph.dependency import DependencyGraph
|
|
8
|
+
from .graph.cycles import CycleDetector
|
|
9
|
+
from .symbols.table import SymbolTable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class AnalysisResult:
|
|
14
|
+
"""Result of analyzing a Godot project."""
|
|
15
|
+
|
|
16
|
+
graph: DependencyGraph
|
|
17
|
+
cycles: list[list[str]] = field(default_factory=list)
|
|
18
|
+
symbol_table: SymbolTable = field(default_factory=SymbolTable)
|
|
19
|
+
errors: list[str] = field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
def to_dict(self) -> dict:
|
|
22
|
+
return {
|
|
23
|
+
"graph": self.graph.to_dict(),
|
|
24
|
+
"cycles": self.cycles,
|
|
25
|
+
"symbols": self.symbol_table.all_classes(),
|
|
26
|
+
"errors": self.errors,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Analyzer:
|
|
31
|
+
"""Orchestrates parsing and graph building for a Godot project."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, project_path: Path, verbose: bool = False) -> None:
|
|
34
|
+
self._scanner = Scanner(project_path)
|
|
35
|
+
self._symbol_table = SymbolTable()
|
|
36
|
+
self._gd_parser = GDScriptParser(self._symbol_table)
|
|
37
|
+
self._tscn_parser = TscnParser()
|
|
38
|
+
self._graph = DependencyGraph()
|
|
39
|
+
self._verbose = verbose
|
|
40
|
+
self._errors: list[str] = []
|
|
41
|
+
|
|
42
|
+
def analyze(self, detect_cycles: bool = True) -> AnalysisResult:
|
|
43
|
+
"""Analyze the project and return results."""
|
|
44
|
+
gd_files, tscn_files = self._scanner.find_all_files()
|
|
45
|
+
root = self._scanner.root
|
|
46
|
+
|
|
47
|
+
if self._verbose:
|
|
48
|
+
print(f"Found {len(gd_files)} GDScript files")
|
|
49
|
+
print(f"Found {len(tscn_files)} scene files")
|
|
50
|
+
|
|
51
|
+
# First pass: parse all GDScript files to build symbol table
|
|
52
|
+
modules = []
|
|
53
|
+
for gd_file in gd_files:
|
|
54
|
+
try:
|
|
55
|
+
module = self._gd_parser.parse(gd_file, root)
|
|
56
|
+
modules.append(module)
|
|
57
|
+
self._graph.add_module(module)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
self._errors.append(f"Error parsing {gd_file}: {e}")
|
|
60
|
+
|
|
61
|
+
# Second pass: resolve class name dependencies
|
|
62
|
+
for module in modules:
|
|
63
|
+
self._gd_parser.resolve_class_dependencies(module)
|
|
64
|
+
|
|
65
|
+
# Parse scene files
|
|
66
|
+
for tscn_file in tscn_files:
|
|
67
|
+
try:
|
|
68
|
+
module = self._tscn_parser.parse(tscn_file, root)
|
|
69
|
+
self._graph.add_module(module)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
self._errors.append(f"Error parsing {tscn_file}: {e}")
|
|
72
|
+
|
|
73
|
+
# Detect cycles
|
|
74
|
+
cycles: list[list[str]] = []
|
|
75
|
+
if detect_cycles:
|
|
76
|
+
detector = CycleDetector(self._graph)
|
|
77
|
+
cycles = detector.find_cycles()
|
|
78
|
+
if self._verbose:
|
|
79
|
+
print(f"Found {len(cycles)} cycles")
|
|
80
|
+
|
|
81
|
+
return AnalysisResult(
|
|
82
|
+
graph=self._graph,
|
|
83
|
+
cycles=cycles,
|
|
84
|
+
symbol_table=self._symbol_table,
|
|
85
|
+
errors=self._errors,
|
|
86
|
+
)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .analyzer import Analyzer
|
|
6
|
+
from .config import ConfigError, ConfigLoader, ConfigValidator
|
|
7
|
+
from .output.dot import DotFormatter
|
|
8
|
+
from .output.json import JsonFormatter
|
|
9
|
+
from .output.text import TextFormatter
|
|
10
|
+
from .rules import RuleEngine
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog="gdcruiser",
|
|
16
|
+
description="Dependency analyzer for Godot/GDScript projects",
|
|
17
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
18
|
+
epilog="""
|
|
19
|
+
Examples:
|
|
20
|
+
gdcruiser . Analyze current directory
|
|
21
|
+
gdcruiser /path/to/project Analyze specific project
|
|
22
|
+
gdcruiser . -f json Output as JSON
|
|
23
|
+
gdcruiser . -f dot -o deps.dot Output DOT file for GraphViz
|
|
24
|
+
gdcruiser . --no-cycles Skip cycle detection
|
|
25
|
+
gdcruiser . --config rules.json Use custom config file
|
|
26
|
+
gdcruiser . --validate-config Validate config without analyzing
|
|
27
|
+
""",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"path",
|
|
32
|
+
nargs="?",
|
|
33
|
+
default=".",
|
|
34
|
+
help="Godot project path (default: current directory)",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"-f",
|
|
39
|
+
"--format",
|
|
40
|
+
choices=["text", "json", "dot"],
|
|
41
|
+
default="text",
|
|
42
|
+
help="Output format (default: text)",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"-o",
|
|
47
|
+
"--output",
|
|
48
|
+
metavar="FILE",
|
|
49
|
+
help="Output file (default: stdout)",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--no-cycles",
|
|
54
|
+
action="store_true",
|
|
55
|
+
help="Skip cycle detection",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"-v",
|
|
60
|
+
"--verbose",
|
|
61
|
+
action="store_true",
|
|
62
|
+
help="Verbose output",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--config",
|
|
67
|
+
metavar="FILE",
|
|
68
|
+
help="Path to config file (.gdcruiser.json or pyproject.toml)",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--validate-config",
|
|
73
|
+
action="store_true",
|
|
74
|
+
help="Validate config file and exit",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--ignore-rules",
|
|
79
|
+
action="store_true",
|
|
80
|
+
help="Skip rule evaluation",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return parser
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def run(args: argparse.Namespace) -> int:
|
|
87
|
+
project_path = Path(args.path).resolve()
|
|
88
|
+
|
|
89
|
+
if not project_path.exists():
|
|
90
|
+
print(f"Error: Path does not exist: {project_path}", file=sys.stderr)
|
|
91
|
+
return 1
|
|
92
|
+
|
|
93
|
+
if not project_path.is_dir():
|
|
94
|
+
print(f"Error: Path is not a directory: {project_path}", file=sys.stderr)
|
|
95
|
+
return 1
|
|
96
|
+
|
|
97
|
+
# Load configuration
|
|
98
|
+
config_path = Path(args.config) if args.config else None
|
|
99
|
+
loader = ConfigLoader(project_path)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
config = loader.load(config_path)
|
|
103
|
+
except ConfigError as e:
|
|
104
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
105
|
+
return 1
|
|
106
|
+
|
|
107
|
+
# Validate config
|
|
108
|
+
if config.has_rules() or args.validate_config:
|
|
109
|
+
validator = ConfigValidator()
|
|
110
|
+
validation = validator.validate(config)
|
|
111
|
+
|
|
112
|
+
if args.verbose or args.validate_config:
|
|
113
|
+
if validation.warnings:
|
|
114
|
+
for w in validation.warnings:
|
|
115
|
+
print(f"Warning: {w.path}: {w.message}", file=sys.stderr)
|
|
116
|
+
|
|
117
|
+
if not validation.is_valid():
|
|
118
|
+
for e in validation.errors:
|
|
119
|
+
print(f"Error: {e.path}: {e.message}", file=sys.stderr)
|
|
120
|
+
return 1
|
|
121
|
+
|
|
122
|
+
if args.validate_config:
|
|
123
|
+
config_file = config_path or loader.discover()
|
|
124
|
+
if config_file:
|
|
125
|
+
print(f"Config valid: {config_file}")
|
|
126
|
+
print(f" Forbidden rules: {len(config.forbidden)}")
|
|
127
|
+
print(f" Allowed rules: {len(config.allowed)}")
|
|
128
|
+
print(f" Required rules: {len(config.required)}")
|
|
129
|
+
else:
|
|
130
|
+
print("No config file found")
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
if args.verbose:
|
|
134
|
+
print(f"Analyzing: {project_path}")
|
|
135
|
+
if config.has_rules():
|
|
136
|
+
print(f"Rules loaded: {len(config.all_rules())}")
|
|
137
|
+
|
|
138
|
+
analyzer = Analyzer(project_path, verbose=args.verbose)
|
|
139
|
+
result = analyzer.analyze(detect_cycles=not args.no_cycles)
|
|
140
|
+
|
|
141
|
+
# Evaluate rules
|
|
142
|
+
rule_result = None
|
|
143
|
+
if config.has_rules() and not args.ignore_rules:
|
|
144
|
+
engine = RuleEngine(config, result.graph)
|
|
145
|
+
rule_result = engine.check_all(cycles=result.cycles)
|
|
146
|
+
|
|
147
|
+
if args.verbose:
|
|
148
|
+
print(
|
|
149
|
+
f"Rule violations: {rule_result.error_count()} errors, "
|
|
150
|
+
f"{rule_result.warning_count()} warnings"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Format output
|
|
154
|
+
if args.format == "json":
|
|
155
|
+
formatter = JsonFormatter()
|
|
156
|
+
output = formatter.format(result, rule_result)
|
|
157
|
+
elif args.format == "dot":
|
|
158
|
+
formatter = DotFormatter()
|
|
159
|
+
output = formatter.format(result)
|
|
160
|
+
else:
|
|
161
|
+
formatter = TextFormatter()
|
|
162
|
+
output = formatter.format(result, rule_result)
|
|
163
|
+
|
|
164
|
+
# Write output
|
|
165
|
+
if args.output:
|
|
166
|
+
output_path = Path(args.output)
|
|
167
|
+
output_path.write_text(output, encoding="utf-8")
|
|
168
|
+
if args.verbose:
|
|
169
|
+
print(f"Output written to: {output_path}")
|
|
170
|
+
else:
|
|
171
|
+
print(output)
|
|
172
|
+
|
|
173
|
+
# Return non-zero if rule errors or cycles found
|
|
174
|
+
if rule_result and rule_result.has_errors():
|
|
175
|
+
return 1
|
|
176
|
+
|
|
177
|
+
if result.cycles and not args.no_cycles:
|
|
178
|
+
return 1
|
|
179
|
+
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def main() -> int:
|
|
184
|
+
parser = create_parser()
|
|
185
|
+
args = parser.parse_args()
|
|
186
|
+
return run(args)
|