# GDScript Parser — Complete Specification

## 1. Overview

Implement a complete GDScript (.gd) parser for GodotIQ as `src/godotiq/parsers/gd_parser.py`. The parser extracts structural information from Godot 4.x GDScript files using regex + state machine approach, matching the architecture established by the existing TSCN parser.

**Target file:** `src/godotiq/parsers/gd_parser.py` (expand existing stub)
**Test fixtures:** Real .gd files in `tests/fixtures/` (6 files, 842 total lines)
**Package layout:** `src/godotiq/`
**Constraints:** Do NOT modify any file outside `gd_parser.py` and new test files. Do NOT touch Sprint 0/1 tests. Do NOT implement MCP tools. Do NOT update `project_index.py`.

## 2. Data Model

### 2.1 Dataclass Architecture

Follow the established pattern: `@dataclass` with `field(default_factory=...)` for mutable defaults, comprehensive type hints, docstrings.

### 2.2 Required Dataclasses

**GdSignal** — Signal declaration
- `name: str` — signal name
- `args: list[tuple[str, str]]` — (arg_name, type_hint) pairs; type_hint="" if untyped
- `line: int` — source line number

**GdFunction** — Function declaration
- `name: str` — function name
- `args: list[tuple[str, str, str]]` — (arg_name, type_hint, default_value) triples; empty strings if absent
- `return_type: str` — return type annotation ("" if absent)
- `is_static: bool` — `static func`
- `is_virtual: bool` — starts with `_` (Godot convention)
- `is_private: bool` — starts with `_` (naming convention; overlaps virtual)
- `line: int` — start line number
- `end_line: int` — end line number (indentation-based tracking)

**GdVariable** — Variable/constant declaration
- `name: str` — variable name
- `type_hint: str` — type annotation ("" if absent)
- `default_value: str` — raw default value text ("" if absent)
- `is_const: bool` — `const` vs `var`
- `is_static: bool` — `static var`
- `annotation: str` — full annotation text (e.g. `@export_range(0, 100)`, `@onready`); "" if none
- `scope: str` — "class" or "local" (indentation-based)
- `line: int` — source line number

**GdEnum** — Enum definition
- `name: str` — enum name ("" for anonymous)
- `values: dict[str, int | None]` — {member_name: explicit_value_or_None}
- `line: int` — source line number

**GdSignalConnection** — `.connect()` call in code
- `signal_source: str` — e.g. "printer.print_started", "$Timer.timeout"
- `target: str` — e.g. "_on_print_started", "func(): print('hi')"
- `has_bind: bool` — uses `.bind()`
- `bind_args: str` — raw bind arguments text
- `line: int`

**GdSignalEmission** — `.emit()` call in code
- `signal_name: str` — e.g. "cash_changed"
- `arg_count: int` — number of arguments passed
- `line: int`

**GdAutoloadRef** — Autoload singleton reference
- `autoload_name: str` — e.g. "EconomyManager"
- `access: str` — e.g. "add_cash()", "current_level"
- `pattern: str` — "direct" | "has_node" | "get_node_or_null"
- `line: int`

**GdPreloadRef** — preload/load reference
- `path: str` — resource path (e.g. "res://scenes/player.tscn")
- `is_preload: bool` — preload vs load
- `line: int`

**GdScript** — Top-level parsed representation
- `class_name: str` — from `class_name` declaration
- `extends: str` — from `extends` declaration
- `signals: list[GdSignal]`
- `functions: list[GdFunction]`
- `variables: list[GdVariable]` — @export, @onready, regular vars, consts at class level
- `enums: list[GdEnum]`
- `inner_classes: list[str]` — names only (no deep parsing)
- `signal_connections: list[GdSignalConnection]`
- `signal_emissions: list[GdSignalEmission]`
- `autoload_refs: list[GdAutoloadRef]`
- `preload_refs: list[GdPreloadRef]`
- `is_instance_valid_lines: list[int]` — line numbers of is_instance_valid() calls

### 2.3 Public API

```python
def parse_gd(path: str, known_autoloads: list[str] | None = None) -> GdScript:
```
- `path`: File path to .gd file (reads content internally)
- `known_autoloads`: Optional list of known autoload names for detection
- Returns: Parsed `GdScript` object
- Best-effort parsing: never raises on malformed content, skips unparseable lines

## 3. Parsing Strategy

### 3.1 Line-by-Line State Machine

Process file line by line with states:
- **TOP_LEVEL** — scanning class-level declarations
- **IN_FUNCTION** — inside a function body (tracking indentation for end_line)
- **IN_ENUM** — accumulating multiline enum definition
- **IN_MULTILINE_CONST** — accumulating multiline const value (arrays, dicts)

### 3.2 Indentation Tracking

- Track current indent level to determine:
  - Function body end (dedent back to class level)
  - Variable scope ("class" at indent 0, "local" inside function)
  - Inner class boundaries

### 3.3 Annotation Buffering

- When an `@export*` or `@onready` annotation line is encountered, buffer it
- Apply it to the next `var` declaration

### 3.4 Regex-Based Detection

Pre-compiled regex patterns for:
- `class_name`, `extends` declarations
- `signal` declarations with optional typed args
- `func` signatures with params, types, defaults, return type
- `var`/`const` declarations with types and defaults
- `enum` definitions (single-line and multiline)
- `.connect()`, `.emit()` calls
- Autoload access patterns (configurable names)
- `preload()`/`load()` calls
- `is_instance_valid()` calls
- Inner `class` declarations

### 3.5 Function Body Analysis

While in IN_FUNCTION state:
- Scan for `.connect()` calls
- Scan for `.emit()` calls
- Scan for autoload references
- Scan for `is_instance_valid()` calls
- Scan for `preload()`/`load()` calls
- Track local variables (scope="local")

## 4. Feature Coverage from Fixtures

From the 6 real .gd fixtures:

| Feature | Example from fixtures | Parser output |
|---------|----------------------|---------------|
| class_name | `class_name EconomyManagerClass` | GdScript.class_name = "EconomyManagerClass" |
| extends | `extends Node` | GdScript.extends = "Node" |
| signal w/ types | `signal cash_changed(new_amount: float, delta: float)` | GdSignal with typed args |
| @export var | `@export var show_debug: bool = false` | GdVariable with annotation |
| @onready var | `@onready var sprite := $Sprite2D` | GdVariable with @onready annotation |
| const | `const LEVEL_XP: Array[int] = [0, 40, ...]` | GdVariable(is_const=True) |
| enum | `enum GameState { PLAYING, PAUSED, DAILY_SUMMARY }` | GdEnum with values |
| func w/ types | `func add_cash(amount: float, desc: String = "") -> void:` | GdFunction with full sig |
| static func | `static func sum2(a, b):` | GdFunction(is_static=True) |
| signal.connect | `printer.print_started.connect(_on_print_started)` | GdSignalConnection |
| signal.emit | `cash_changed.emit(cash, delta)` | GdSignalEmission(arg_count=2) |
| .bind() | `connect(_handler.bind(param))` | GdSignalConnection(has_bind=True) |
| lambda connect | `level_up.connect(func(_l, _u): fired[0] = true)` | GdSignalConnection |
| autoload ref | `EconomyManager.add_cash(100)` | GdAutoloadRef(pattern="direct") |
| get_node_or_null | `get_tree().root.get_node_or_null("Main/...")` | GdAutoloadRef(pattern="get_node_or_null") |
| has_node | `has_node("/root/EconomyManager")` | GdAutoloadRef(pattern="has_node") |
| preload | `preload("res://scenes/player.tscn")` | GdPreloadRef(is_preload=True) |
| load | `load("res://data/items.tres")` | GdPreloadRef(is_preload=False) |
| is_instance_valid | `is_instance_valid(target)` | line number in list |
| inner class | `class Circle extends Shape:` | "Circle" in inner_classes |

## 5. Success Criteria

1. `pytest tests/ -v` → all tests pass (including Sprint 0 + Sprint 1 = 127 existing tests)
2. New tests for GDScript parser in `tests/test_parsers/test_gd_parser.py`
3. Comprehensive test coverage of all 15+ features listed above
4. Tests use real fixture files from `tests/fixtures/`
5. Best-effort parsing: no exceptions on any valid or malformed input
6. Consistent API pattern with `parse_tscn()` — accepts file path, returns dataclass
