unity-api-mcp 1.0.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.
- unity_api_mcp-1.0.0/.env.example +5 -0
- unity_api_mcp-1.0.0/.gitignore +8 -0
- unity_api_mcp-1.0.0/PKG-INFO +9 -0
- unity_api_mcp-1.0.0/README.md +233 -0
- unity_api_mcp-1.0.0/pyproject.toml +28 -0
- unity_api_mcp-1.0.0/src/unity_api_mcp/__init__.py +0 -0
- unity_api_mcp-1.0.0/src/unity_api_mcp/cs_doc_parser.py +306 -0
- unity_api_mcp-1.0.0/src/unity_api_mcp/data/unity_docs.db +0 -0
- unity_api_mcp-1.0.0/src/unity_api_mcp/data/unity_docs.db-shm +0 -0
- unity_api_mcp-1.0.0/src/unity_api_mcp/data/unity_docs.db-wal +0 -0
- unity_api_mcp-1.0.0/src/unity_api_mcp/db.py +219 -0
- unity_api_mcp-1.0.0/src/unity_api_mcp/ingest.py +107 -0
- unity_api_mcp-1.0.0/src/unity_api_mcp/server.py +342 -0
- unity_api_mcp-1.0.0/src/unity_api_mcp/unity_paths.py +148 -0
- unity_api_mcp-1.0.0/src/unity_api_mcp/xml_parser.py +132 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: unity-api-mcp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: MCP server providing ground-truth Unity 6 API documentation — prevents AI hallucination of signatures, namespaces, and deprecated APIs
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: mcp[cli]>=1.8.0
|
|
7
|
+
Requires-Dist: python-dotenv>=1.0
|
|
8
|
+
Provides-Extra: ingest
|
|
9
|
+
Requires-Dist: lxml>=5.0; extra == 'ingest'
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# unity-api-mcp
|
|
2
|
+
|
|
3
|
+
Local MCP server that gives AI agents accurate Unity 6 API documentation. Prevents hallucinated method signatures, wrong namespaces, and deprecated API usage.
|
|
4
|
+
|
|
5
|
+
Works with Claude Code, Cursor, Windsurf, or any MCP-compatible AI tool.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
5 tools your AI can call:
|
|
10
|
+
|
|
11
|
+
| Tool | Purpose | Example |
|
|
12
|
+
|------|---------|---------|
|
|
13
|
+
| `search_unity_api` | Find APIs by keyword | "Tilemap SetTile", "async load scene" |
|
|
14
|
+
| `get_method_signature` | Get exact signatures with all overloads | `UnityEngine.Physics.Raycast` |
|
|
15
|
+
| `get_namespace` | Resolve `using` directives | "SceneManager" → `using UnityEngine.SceneManagement;` |
|
|
16
|
+
| `get_class_reference` | Full class reference card (all members) | "InputAction" → 31 methods/fields/properties |
|
|
17
|
+
| `get_deprecation_warnings` | Check if an API is obsolete + get replacement | "WWW" → Use UnityWebRequest instead |
|
|
18
|
+
|
|
19
|
+
**Coverage:** All UnityEngine/UnityEditor modules (~75K records), Input System, Addressables. 78K total records ship pre-built — no ingestion step required.
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
- Python 3.10+
|
|
24
|
+
- That's it. The database ships pre-built. Unity does not need to be installed.
|
|
25
|
+
|
|
26
|
+
## Setup
|
|
27
|
+
|
|
28
|
+
### 1. Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install unity-api-mcp
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or install from source:
|
|
35
|
+
```bash
|
|
36
|
+
git clone <repo-url> unity-api-mcp
|
|
37
|
+
cd unity-api-mcp
|
|
38
|
+
pip install .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. Add to your AI tool
|
|
42
|
+
|
|
43
|
+
#### Claude Code
|
|
44
|
+
|
|
45
|
+
Add to `~/.claude/mcp.json` (global) or `<project>/.mcp.json` (per-project):
|
|
46
|
+
|
|
47
|
+
**macOS / Linux:**
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"unity-api": {
|
|
52
|
+
"command": "unity-api-mcp",
|
|
53
|
+
"args": []
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Windows:**
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"mcpServers": {
|
|
63
|
+
"unity-api": {
|
|
64
|
+
"command": "unity-api-mcp.exe",
|
|
65
|
+
"args": []
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If the command isn't found (not on PATH), use the full path to the script:
|
|
72
|
+
|
|
73
|
+
**macOS / Linux:**
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": {
|
|
77
|
+
"unity-api": {
|
|
78
|
+
"command": "/path/to/venv/bin/unity-api-mcp",
|
|
79
|
+
"args": []
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Windows:**
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"mcpServers": {
|
|
89
|
+
"unity-api": {
|
|
90
|
+
"command": "C:\\path\\to\\venv\\Scripts\\unity-api-mcp.exe",
|
|
91
|
+
"args": []
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### Cursor / Windsurf
|
|
98
|
+
|
|
99
|
+
Add the same config to your MCP settings file. The server uses stdio transport (default).
|
|
100
|
+
|
|
101
|
+
### 3. Verify it's working
|
|
102
|
+
|
|
103
|
+
Restart your AI tool (or run `/mcp` in Claude Code to reconnect), then ask:
|
|
104
|
+
|
|
105
|
+
*"What namespace does SceneManager belong to?"*
|
|
106
|
+
|
|
107
|
+
If everything is connected, it should call `get_namespace("SceneManager")` and answer `using UnityEngine.SceneManagement;`. If it guesses without calling a tool, the MCP server isn't connected — check the config paths and restart.
|
|
108
|
+
|
|
109
|
+
You can also try: *"Show me all methods on the Tilemap class"* — this should call `get_class_reference` and return 60+ methods.
|
|
110
|
+
|
|
111
|
+
### 4. Tell your AI to use it proactively
|
|
112
|
+
|
|
113
|
+
Add the following to your project's `CLAUDE.md` (or equivalent instructions file). **This step is important** — without it, the AI has the tools but won't know when to reach for them.
|
|
114
|
+
|
|
115
|
+
```markdown
|
|
116
|
+
## Unity API Lookup (unity-api MCP)
|
|
117
|
+
|
|
118
|
+
Use the `unity-api` MCP tools to verify Unity API usage instead of guessing. **Do not hallucinate signatures.**
|
|
119
|
+
|
|
120
|
+
| When | Tool | Example |
|
|
121
|
+
|------|------|---------|
|
|
122
|
+
| Unsure about a method's parameters or return type | `get_method_signature` | `get_method_signature("UnityEngine.Tilemaps.Tilemap.SetTile")` |
|
|
123
|
+
| Need the `using` directive for a type | `get_namespace` | `get_namespace("SceneManager")` |
|
|
124
|
+
| Want to see all members on a class | `get_class_reference` | `get_class_reference("InputAction")` |
|
|
125
|
+
| Searching for an API by keyword | `search_unity_api` | `search_unity_api("async load scene")` |
|
|
126
|
+
| Checking if an API is deprecated | `get_deprecation_warnings` | `get_deprecation_warnings("FindObjectOfType")` |
|
|
127
|
+
|
|
128
|
+
**Rules:**
|
|
129
|
+
- Before writing a Unity API call you haven't used in this conversation, verify the signature with `get_method_signature`
|
|
130
|
+
- Before adding a `using` directive, verify with `get_namespace` if unsure
|
|
131
|
+
- Covers: all UnityEngine/UnityEditor modules, Input System, Addressables
|
|
132
|
+
- Does NOT cover: DOTween, VContainer, Newtonsoft.Json (third-party — rely on project source)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## AI-Assisted Setup
|
|
136
|
+
|
|
137
|
+
If an AI agent (Claude Code, etc.) is setting this up for you, give it these instructions:
|
|
138
|
+
|
|
139
|
+
> Read the README at `<path>/unity-api-mcp/README.md` and set up the MCP server for my Unity project at `<project-path>`.
|
|
140
|
+
|
|
141
|
+
The agent should:
|
|
142
|
+
1. **Install** — `pip install unity-api-mcp` (or `pip install .` if cloned locally)
|
|
143
|
+
2. **Find the executable** — run `which unity-api-mcp` (macOS/Linux) or `where unity-api-mcp` (Windows) to get the full path
|
|
144
|
+
3. **Write MCP config** — add to `~/.claude/mcp.json` with `"command": "<full-path-to-unity-api-mcp>"`
|
|
145
|
+
4. **Add CLAUDE.md instructions** — append the "Unity API Lookup" snippet from Step 4 above to the project's `CLAUDE.md`
|
|
146
|
+
5. **Verify** — reconnect MCP (`/mcp` in Claude Code) and test: `get_namespace("SceneManager")` should return `using UnityEngine.SceneManagement;`
|
|
147
|
+
|
|
148
|
+
## Advanced: Rebuild or extend the database
|
|
149
|
+
|
|
150
|
+
The shipped database covers Unity 6 (6000.x) engine APIs + Input System + Addressables. If you need to:
|
|
151
|
+
- **Update for a newer Unity version** — re-run ingestion
|
|
152
|
+
- **Add more packages** (e.g. Cinemachine, TextMeshPro) — re-run with `--project`
|
|
153
|
+
- **Match your exact package versions** — re-run with `--project`
|
|
154
|
+
|
|
155
|
+
### Run ingestion
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# Activate venv first, then:
|
|
159
|
+
|
|
160
|
+
# Engine APIs only (requires Unity 6 installed)
|
|
161
|
+
python -m unity_api_mcp.ingest
|
|
162
|
+
|
|
163
|
+
# Engine APIs + packages from your project
|
|
164
|
+
python -m unity_api_mcp.ingest --project "/path/to/your/unity/project"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
If Unity isn't auto-detected, set the install path:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# Windows (Command Prompt)
|
|
171
|
+
set UNITY_INSTALL_PATH=C:\Program Files\Unity\Hub\Editor\6000.3.8f1
|
|
172
|
+
|
|
173
|
+
# Windows (PowerShell)
|
|
174
|
+
$env:UNITY_INSTALL_PATH = "C:\Program Files\Unity\Hub\Editor\6000.3.8f1"
|
|
175
|
+
|
|
176
|
+
# macOS/Linux
|
|
177
|
+
export UNITY_INSTALL_PATH=/Applications/Unity/Hub/Editor/6000.3.8f1
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
You can also add these to the MCP config `env` block so they're always available:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
"env": {
|
|
184
|
+
"PYTHONPATH": "...",
|
|
185
|
+
"UNITY_INSTALL_PATH": "...",
|
|
186
|
+
"UNITY_PROJECT_PATH": "..."
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### What ingestion parses
|
|
191
|
+
|
|
192
|
+
**XML IntelliSense files** — Ship with every Unity installation at `Editor/Data/Managed/`. 139 XML files covering all UnityEngine and UnityEditor modules.
|
|
193
|
+
|
|
194
|
+
**C# source doc comments** — Unity packages (Input System, Addressables, etc.) ship as source code in your project's `Library/PackageCache/`. The ingestion pipeline parses `///` XML doc comments from `.cs` files.
|
|
195
|
+
|
|
196
|
+
## Project structure
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
unity-api-mcp/
|
|
200
|
+
├── src/unity_api_mcp/
|
|
201
|
+
│ ├── server.py # MCP server — 5 tools
|
|
202
|
+
│ ├── db.py # SQLite + FTS5 database layer
|
|
203
|
+
│ ├── xml_parser.py # Parse Unity XML IntelliSense files
|
|
204
|
+
│ ├── cs_doc_parser.py # Parse C# doc comments from package source
|
|
205
|
+
│ ├── unity_paths.py # Locate Unity install + package dirs
|
|
206
|
+
│ ├── ingest.py # CLI ingestion pipeline
|
|
207
|
+
│ └── data/
|
|
208
|
+
│ └── unity_docs.db # Pre-built SQLite database (78K records, ships with package)
|
|
209
|
+
├── pyproject.toml
|
|
210
|
+
└── .env.example
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Troubleshooting
|
|
214
|
+
|
|
215
|
+
**"No results found" for a query**
|
|
216
|
+
- The pre-built database should be included in the package. If missing, reinstall: `pip install --force-reinstall unity-api-mcp`
|
|
217
|
+
- Or re-run ingestion to rebuild (see Advanced section)
|
|
218
|
+
|
|
219
|
+
**Server won't start**
|
|
220
|
+
- Check Python version: `python --version` (needs 3.10+)
|
|
221
|
+
- Check the command path: run `which unity-api-mcp` (macOS/Linux) or `where unity-api-mcp` (Windows)
|
|
222
|
+
- If not found, use the full path in your MCP config
|
|
223
|
+
|
|
224
|
+
**Third-party packages return no results**
|
|
225
|
+
- DOTween, VContainer, Newtonsoft.Json are not indexed (third-party, not Unity packages)
|
|
226
|
+
- Only Unity first-party packages are supported via ingestion with `--project`
|
|
227
|
+
|
|
228
|
+
**Want to add more packages**
|
|
229
|
+
- Run `python -m unity_api_mcp.ingest --project "/path/to/project"` to parse all Unity packages in your project's `Library/PackageCache/`
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "unity-api-mcp"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "MCP server providing ground-truth Unity 6 API documentation — prevents AI hallucination of signatures, namespaces, and deprecated APIs"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"mcp[cli]>=1.8.0",
|
|
12
|
+
"python-dotenv>=1.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
ingest = [
|
|
17
|
+
"lxml>=5.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
unity-api-mcp = "unity_api_mcp.server:main"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["src/unity_api_mcp"]
|
|
25
|
+
exclude = ["*.db-shm", "*.db-wal"]
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
28
|
+
"src/unity_api_mcp/data/unity_docs.db" = "unity_api_mcp/data/unity_docs.db"
|
|
File without changes
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Parse C# XML doc comments (///) from source files into structured records.
|
|
2
|
+
|
|
3
|
+
Used for Unity packages that ship as source code (Input System, Addressables)
|
|
4
|
+
rather than pre-built DLLs with XML IntelliSense files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Match /// comment blocks and the declaration that follows
|
|
12
|
+
_DOC_COMMENT_LINE = re.compile(r"^\s*///\s?(.*)")
|
|
13
|
+
_NAMESPACE_RE = re.compile(r"^\s*namespace\s+([\w.]+)")
|
|
14
|
+
_CLASS_RE = re.compile(
|
|
15
|
+
r"^\s*(?:public|internal|private|protected)?\s*(?:static\s+)?(?:abstract\s+)?(?:sealed\s+)?(?:partial\s+)?"
|
|
16
|
+
r"(?:class|struct|interface|enum)\s+(\w+)"
|
|
17
|
+
)
|
|
18
|
+
_METHOD_RE = re.compile(
|
|
19
|
+
r"^\s*(?:public|internal|protected)\s+(?:static\s+)?(?:virtual\s+)?(?:override\s+)?(?:abstract\s+)?(?:new\s+)?"
|
|
20
|
+
r"(?:async\s+)?(?:[\w<>\[\],\s?]+?)\s+(\w+)\s*(?:<[^>]+>)?\s*\("
|
|
21
|
+
)
|
|
22
|
+
_PROPERTY_RE = re.compile(
|
|
23
|
+
r"^\s*(?:public|internal|protected)\s+(?:static\s+)?(?:virtual\s+)?(?:override\s+)?(?:abstract\s+)?(?:new\s+)?"
|
|
24
|
+
r"([\w<>\[\],\s?]+?)\s+(\w+)\s*\{"
|
|
25
|
+
)
|
|
26
|
+
_FIELD_RE = re.compile(
|
|
27
|
+
r"^\s*(?:public|internal|protected)\s+(?:static\s+)?(?:readonly\s+)?(?:const\s+)?"
|
|
28
|
+
r"([\w<>\[\],\s?]+?)\s+(\w+)\s*[;=]"
|
|
29
|
+
)
|
|
30
|
+
_EVENT_RE = re.compile(
|
|
31
|
+
r"^\s*(?:public|internal|protected)\s+(?:static\s+)?event\s+"
|
|
32
|
+
r"([\w<>\[\],\s?]+?)\s+(\w+)\s*[;{]"
|
|
33
|
+
)
|
|
34
|
+
_PARAM_RE = re.compile(r"<param\s+name=[\"'](\w+)[\"']>(.*?)</param>")
|
|
35
|
+
_RETURNS_RE = re.compile(r"<returns>(.*?)</returns>")
|
|
36
|
+
_SUMMARY_RE = re.compile(r"<summary>(.*?)</summary>", re.DOTALL)
|
|
37
|
+
_SEE_CREF_RE = re.compile(r"<see\s+cref=[\"']([^\"']+)[\"']\s*/>")
|
|
38
|
+
_XML_TAG_RE = re.compile(r"<[^>]+>")
|
|
39
|
+
|
|
40
|
+
_DEPRECATED_PATTERNS = re.compile(
|
|
41
|
+
r"\b(obsolete|deprecated)\b|use\s+\S+\s+instead",
|
|
42
|
+
re.IGNORECASE,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_cs_directory(directory: Path) -> list[dict]:
|
|
47
|
+
"""Recursively parse all .cs files in a directory for XML doc comments."""
|
|
48
|
+
records = []
|
|
49
|
+
cs_files = list(directory.rglob("*.cs"))
|
|
50
|
+
|
|
51
|
+
for cs_file in cs_files:
|
|
52
|
+
# Skip test files, samples, editor-only
|
|
53
|
+
rel = str(cs_file.relative_to(directory))
|
|
54
|
+
if any(skip in rel.lower() for skip in ("test", "sample", "example", "doccode")):
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
file_records = _parse_cs_file(cs_file)
|
|
59
|
+
records.extend(file_records)
|
|
60
|
+
except Exception:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
return records
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _parse_cs_file(path: Path) -> list[dict]:
|
|
67
|
+
"""Parse a single .cs file and extract documented members."""
|
|
68
|
+
try:
|
|
69
|
+
text = path.read_text(encoding="utf-8-sig", errors="replace")
|
|
70
|
+
except (OSError, UnicodeDecodeError):
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
lines = text.splitlines()
|
|
74
|
+
records = []
|
|
75
|
+
current_namespace = ""
|
|
76
|
+
class_stack = [] # Stack of class names for nesting
|
|
77
|
+
|
|
78
|
+
i = 0
|
|
79
|
+
while i < len(lines):
|
|
80
|
+
line = lines[i]
|
|
81
|
+
|
|
82
|
+
# Track namespace
|
|
83
|
+
ns_match = _NAMESPACE_RE.match(line)
|
|
84
|
+
if ns_match:
|
|
85
|
+
current_namespace = ns_match.group(1)
|
|
86
|
+
i += 1
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
# Track class/struct/interface nesting
|
|
90
|
+
cls_match = _CLASS_RE.match(line)
|
|
91
|
+
if cls_match and not _is_doc_comment(line):
|
|
92
|
+
# Check if there's a doc comment block above
|
|
93
|
+
doc_block, params, returns_text = _extract_doc_block(lines, i)
|
|
94
|
+
class_name = cls_match.group(1)
|
|
95
|
+
class_stack.append(class_name)
|
|
96
|
+
|
|
97
|
+
if doc_block:
|
|
98
|
+
fqn = f"{current_namespace}.{class_name}" if current_namespace else class_name
|
|
99
|
+
summary = _clean_xml_text(doc_block)
|
|
100
|
+
deprecated = bool(_DEPRECATED_PATTERNS.search(summary))
|
|
101
|
+
records.append({
|
|
102
|
+
"fqn": fqn,
|
|
103
|
+
"namespace": current_namespace,
|
|
104
|
+
"class_name": class_name,
|
|
105
|
+
"member_name": "",
|
|
106
|
+
"member_type": "type",
|
|
107
|
+
"summary": summary,
|
|
108
|
+
"params_json": [],
|
|
109
|
+
"returns_text": "",
|
|
110
|
+
"deprecated": deprecated,
|
|
111
|
+
"deprecation_hint": _extract_deprecation_hint(summary) if deprecated else "",
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
i += 1
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
# Track scope for class stack (simplified brace counting)
|
|
118
|
+
if "}" in line and not "//" in line.split("}")[0]:
|
|
119
|
+
# This is a rough heuristic — good enough for top-level classes
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
# Check for doc comment block
|
|
123
|
+
if _is_doc_comment(line):
|
|
124
|
+
# Collect the full doc block
|
|
125
|
+
doc_start = i
|
|
126
|
+
while i < len(lines) and _is_doc_comment(lines[i]):
|
|
127
|
+
i += 1
|
|
128
|
+
|
|
129
|
+
# Now lines[i] should be the declaration
|
|
130
|
+
if i < len(lines):
|
|
131
|
+
decl_line = lines[i]
|
|
132
|
+
doc_text = "\n".join(
|
|
133
|
+
_DOC_COMMENT_LINE.match(lines[j]).group(1)
|
|
134
|
+
for j in range(doc_start, i)
|
|
135
|
+
if _DOC_COMMENT_LINE.match(lines[j])
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
record = _parse_declaration(
|
|
139
|
+
decl_line, doc_text, current_namespace,
|
|
140
|
+
class_stack[-1] if class_stack else ""
|
|
141
|
+
)
|
|
142
|
+
if record:
|
|
143
|
+
records.append(record)
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
i += 1
|
|
147
|
+
|
|
148
|
+
return records
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _is_doc_comment(line: str) -> bool:
|
|
152
|
+
stripped = line.strip()
|
|
153
|
+
return stripped.startswith("///")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _extract_doc_block(lines: list[str], decl_index: int) -> tuple[str, list[dict], str]:
|
|
157
|
+
"""Look backwards from a declaration to find its doc comment block."""
|
|
158
|
+
doc_lines = []
|
|
159
|
+
i = decl_index - 1
|
|
160
|
+
while i >= 0 and _is_doc_comment(lines[i]):
|
|
161
|
+
match = _DOC_COMMENT_LINE.match(lines[i])
|
|
162
|
+
if match:
|
|
163
|
+
doc_lines.insert(0, match.group(1))
|
|
164
|
+
i -= 1
|
|
165
|
+
|
|
166
|
+
if not doc_lines:
|
|
167
|
+
return "", [], ""
|
|
168
|
+
|
|
169
|
+
doc_text = "\n".join(doc_lines)
|
|
170
|
+
|
|
171
|
+
# Extract summary
|
|
172
|
+
summary_match = _SUMMARY_RE.search(doc_text)
|
|
173
|
+
summary = summary_match.group(1).strip() if summary_match else doc_text
|
|
174
|
+
|
|
175
|
+
# Extract params
|
|
176
|
+
params = [
|
|
177
|
+
{"name": m.group(1), "description": _clean_xml_text(m.group(2))}
|
|
178
|
+
for m in _PARAM_RE.finditer(doc_text)
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
# Extract returns
|
|
182
|
+
returns_match = _RETURNS_RE.search(doc_text)
|
|
183
|
+
returns_text = _clean_xml_text(returns_match.group(1)) if returns_match else ""
|
|
184
|
+
|
|
185
|
+
return summary, params, returns_text
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _parse_declaration(decl_line: str, doc_text: str,
|
|
189
|
+
namespace: str, class_name: str) -> dict | None:
|
|
190
|
+
"""Parse a C# declaration line and combine with doc text into a record."""
|
|
191
|
+
|
|
192
|
+
# Extract summary
|
|
193
|
+
summary_match = _SUMMARY_RE.search(doc_text)
|
|
194
|
+
summary = _clean_xml_text(summary_match.group(1).strip() if summary_match else doc_text)
|
|
195
|
+
|
|
196
|
+
if not summary or len(summary) < 3:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
# Extract params
|
|
200
|
+
params = [
|
|
201
|
+
{"name": m.group(1), "description": _clean_xml_text(m.group(2))}
|
|
202
|
+
for m in _PARAM_RE.finditer(doc_text)
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
# Extract returns
|
|
206
|
+
returns_match = _RETURNS_RE.search(doc_text)
|
|
207
|
+
returns_text = _clean_xml_text(returns_match.group(1)) if returns_match else ""
|
|
208
|
+
|
|
209
|
+
# Detect deprecation
|
|
210
|
+
deprecated = bool(_DEPRECATED_PATTERNS.search(summary))
|
|
211
|
+
deprecation_hint = _extract_deprecation_hint(summary) if deprecated else ""
|
|
212
|
+
|
|
213
|
+
# Try to match declaration type
|
|
214
|
+
# Check class/struct/interface first
|
|
215
|
+
cls_match = _CLASS_RE.match(decl_line)
|
|
216
|
+
if cls_match:
|
|
217
|
+
member_name_str = cls_match.group(1)
|
|
218
|
+
fqn = f"{namespace}.{member_name_str}" if namespace else member_name_str
|
|
219
|
+
return {
|
|
220
|
+
"fqn": fqn,
|
|
221
|
+
"namespace": namespace,
|
|
222
|
+
"class_name": member_name_str,
|
|
223
|
+
"member_name": "",
|
|
224
|
+
"member_type": "type",
|
|
225
|
+
"summary": summary,
|
|
226
|
+
"params_json": params,
|
|
227
|
+
"returns_text": returns_text,
|
|
228
|
+
"deprecated": deprecated,
|
|
229
|
+
"deprecation_hint": deprecation_hint,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if not class_name:
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
# Event (check before property — similar syntax)
|
|
236
|
+
event_match = _EVENT_RE.match(decl_line)
|
|
237
|
+
if event_match:
|
|
238
|
+
member_name_str = event_match.group(2)
|
|
239
|
+
fqn = f"{namespace}.{class_name}.{member_name_str}" if namespace else f"{class_name}.{member_name_str}"
|
|
240
|
+
return _build_record(fqn, namespace, class_name, member_name_str, "event",
|
|
241
|
+
summary, params, returns_text, deprecated, deprecation_hint)
|
|
242
|
+
|
|
243
|
+
# Method
|
|
244
|
+
method_match = _METHOD_RE.match(decl_line)
|
|
245
|
+
if method_match:
|
|
246
|
+
member_name_str = method_match.group(1)
|
|
247
|
+
# Skip property accessors
|
|
248
|
+
if member_name_str in ("get", "set", "add", "remove"):
|
|
249
|
+
return None
|
|
250
|
+
fqn = f"{namespace}.{class_name}.{member_name_str}" if namespace else f"{class_name}.{member_name_str}"
|
|
251
|
+
return _build_record(fqn, namespace, class_name, member_name_str, "method",
|
|
252
|
+
summary, params, returns_text, deprecated, deprecation_hint)
|
|
253
|
+
|
|
254
|
+
# Property
|
|
255
|
+
prop_match = _PROPERTY_RE.match(decl_line)
|
|
256
|
+
if prop_match:
|
|
257
|
+
member_name_str = prop_match.group(2)
|
|
258
|
+
# Skip common false positives
|
|
259
|
+
if member_name_str in ("class", "struct", "interface", "enum", "namespace",
|
|
260
|
+
"if", "else", "for", "while", "switch", "try", "catch"):
|
|
261
|
+
return None
|
|
262
|
+
fqn = f"{namespace}.{class_name}.{member_name_str}" if namespace else f"{class_name}.{member_name_str}"
|
|
263
|
+
return _build_record(fqn, namespace, class_name, member_name_str, "property",
|
|
264
|
+
summary, params, returns_text, deprecated, deprecation_hint)
|
|
265
|
+
|
|
266
|
+
# Field
|
|
267
|
+
field_match = _FIELD_RE.match(decl_line)
|
|
268
|
+
if field_match:
|
|
269
|
+
member_name_str = field_match.group(2)
|
|
270
|
+
fqn = f"{namespace}.{class_name}.{member_name_str}" if namespace else f"{class_name}.{member_name_str}"
|
|
271
|
+
return _build_record(fqn, namespace, class_name, member_name_str, "field",
|
|
272
|
+
summary, params, returns_text, deprecated, deprecation_hint)
|
|
273
|
+
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _build_record(fqn, namespace, class_name, member_name, member_type,
|
|
278
|
+
summary, params, returns_text, deprecated, deprecation_hint):
|
|
279
|
+
return {
|
|
280
|
+
"fqn": fqn,
|
|
281
|
+
"namespace": namespace,
|
|
282
|
+
"class_name": class_name,
|
|
283
|
+
"member_name": member_name,
|
|
284
|
+
"member_type": member_type,
|
|
285
|
+
"summary": summary,
|
|
286
|
+
"params_json": params,
|
|
287
|
+
"returns_text": returns_text,
|
|
288
|
+
"deprecated": deprecated,
|
|
289
|
+
"deprecation_hint": deprecation_hint,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _clean_xml_text(text: str) -> str:
|
|
294
|
+
"""Strip XML tags and normalize whitespace."""
|
|
295
|
+
# Replace <see cref="Foo"/> with Foo
|
|
296
|
+
text = _SEE_CREF_RE.sub(lambda m: m.group(1).split(".")[-1], text)
|
|
297
|
+
# Strip remaining XML tags
|
|
298
|
+
text = _XML_TAG_RE.sub("", text)
|
|
299
|
+
# Normalize whitespace
|
|
300
|
+
text = re.sub(r"\s+", " ", text).strip()
|
|
301
|
+
return text
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _extract_deprecation_hint(summary: str) -> str:
|
|
305
|
+
match = re.search(r"(?:use|Use)\s+(\S+?)[\s.]", summary)
|
|
306
|
+
return match.group(1) if match else ""
|
|
Binary file
|
|
Binary file
|
|
File without changes
|