# GDScript Parser — Implementation Plan

## 1. Context and Objective

GodotIQ is an MCP server for AI-assisted Godot 4.x development. It already has a working TSCN parser pipeline (value_parser → tscn_parser → scene_resolver → project_index) with 127 passing tests. This plan adds a GDScript (.gd) parser as `src/godotiq/parsers/gd_parser.py` that extracts structural information from GDScript source files.

**Goal:** Parse .gd files into a rich structural representation covering class declarations, signals, functions, variables, enums, signal wiring, autoload references, and resource loading — enabling AI tools to reason about script behavior, dependencies, and architecture.

**Constraints:**
- Only modify `gd_parser.py` (expanding the existing stub)
- Create new test file `tests/test_parsers/test_gd_parser.py`
- Do NOT touch Sprint 0/1 tests, MCP tools, or project_index.py
- All 127 existing tests must continue to pass

**Breaking change from stub:** The existing stub defines `parse_gd(content: str)`. This plan changes the signature to `parse_gd(path: str, ...)` to match `parse_tscn(path: str)`. The stub's `parse_gd` is not imported or called anywhere outside the module (verified by grep). Existing Sprint 0 tests reference `parse_gd` but only test that it exists and raises `FileNotFoundError` for bad paths — they will work with the new path-based signature.

## 2. Architecture Overview

The parser follows the same patterns established by the existing TSCN parser: `@dataclass` models for output, pre-compiled regex for line detection, and a line-by-line state machine for context tracking.

```
┌──────────────┐    ┌─────────────────┐    ┌──────────────┐
│  .gd file    │ →  │ Line-by-line    │ →  │  GdScript    │
│  (raw text)  │    │ State Machine   │    │  dataclass   │
└──────────────┘    └─────────────────┘    └──────────────┘
                          │
                    ┌─────┴──────┐
                    │ Pre-compiled│
                    │   Regex    │
                    │  Patterns  │
                    └────────────┘
```

### Public API

```python
def parse_gd(path: str, known_autoloads: list[str] | None = None) -> GdScript:
    """Parse a .gd file into a structured GdScript representation.

    Args:
        path: Absolute or relative path to a .gd file.
        known_autoloads: Optional list of known autoload singleton names
                         for accurate autoload reference detection.

    Returns:
        Parsed GdScript dataclass with all extracted structural information.
        Never raises on malformed content — uses best-effort parsing.
    """
```

Internally, `parse_gd()` is a thin wrapper around `_parse_gd_content(content: str, known_autoloads)` which does all the work. This separation enables clean unit testing — tests can call `_parse_gd_content()` directly with inline strings without creating temp files.

The function reads the file, splits into lines, and runs the state machine. If the file cannot be read (FileNotFoundError), it raises — matching `parse_tscn` behavior. For any parse-level issues (malformed syntax), it silently skips the problematic line and continues.

**Logging:** The module uses `logger = logging.getLogger(__name__)` for consistency with existing parsers. Skipped/malformed lines are logged at DEBUG level.

## 3. Data Model

All dataclasses use the project convention: `@dataclass` with `field(default_factory=...)` for mutable defaults, type hints on every field, docstrings on each class.

### 3.1 GdSignal — Signal Declaration

```python
@dataclass
class GdSignal:
    """A signal declaration in a GDScript file."""
    name: str = ""
    args: list[tuple[str, str]] = field(default_factory=list)  # (arg_name, type_hint)
    line: int = 0
```

Captures `signal cash_changed(new_amount: float, delta: float)` as `GdSignal(name="cash_changed", args=[("new_amount", "float"), ("delta", "float")], line=12)`.

For parameterless signals like `signal health_depleted`, `args` is empty.

Type hints may include custom types: `Printer.State`, `ProductData`, `GameState` — stored as raw strings.

### 3.2 GdFunction — Function Declaration

```python
@dataclass
class GdFunction:
    """A function declaration in a GDScript file."""
    name: str = ""
    args: list[tuple[str, str, str]] = field(default_factory=list)  # (name, type, default)
    return_type: str = ""
    is_static: bool = False
    is_virtual: bool = False
    is_private: bool = False
    line: int = 0
    end_line: int = 0
```

**Argument triple semantics:**
- `(name, type_hint, default_value)` — all strings, empty if absent
- `func add_cash(amount: float, description: String = "")` → `[("amount", "float", ""), ("description", "String", '""')]`
- `func _ready()` → `[]`

**Depth-aware argument splitting:** Function arguments are split using depth-aware comma splitting (respecting nested parentheses), not naive `,` split. This correctly handles defaults like `Vector2(1, 2)`, `{key: value}`, and `[1, 2, 3]`.

**Virtual vs Private:**
- `is_virtual = True` when name starts with `_` (Godot convention for virtual callbacks like `_ready`, `_process`, `_physics_process`)
- `is_private = True` when name starts with `_` (same condition — all virtuals are private)
- This overlap is intentional: Godot uses `_` prefix for both meanings, and consumers can distinguish by checking known virtual names externally

**end_line tracking:**
- Determined by indentation: the last non-blank, non-comment line before dedent back to function indent level
- For one-liner functions (`func square(a): return a * a`), end_line equals line
- For the last function in a file, end_line is the last non-blank line

### 3.3 GdVariable — Variable/Constant Declaration

```python
@dataclass
class GdVariable:
    """A variable or constant declaration."""
    name: str = ""
    type_hint: str = ""
    default_value: str = ""
    is_const: bool = False
    is_static: bool = False
    annotation: str = ""
    scope: str = "class"  # "class" or "local"
    line: int = 0
```

**Annotation handling — same-line and separate-line:**
The most common pattern is annotation on the same line as the variable:
- `@export var speed: float = 5.0` → `annotation="@export"`
- `@export_range(0, 100, 1, "or_less") var hp: int` → `annotation='@export_range(0, 100, 1, "or_less")'`
- `@onready var sprite := $Sprite2D` → `annotation="@onready"`

The parser handles both:
1. **Same-line:** Detect annotation + var on one line, extract annotation and parse var in a single pass
2. **Separate-line (fallback):** If annotation is on its own line (rare), buffer it and apply to the next var line

Only `@export*` and `@onready` annotations are buffered. Other annotations (`@rpc`, `@warning_ignore`, `@tool`, `@icon`, `@abstract`, `@static_unload`) are recognized and discarded — they don't attach to variables.

Standalone grouping annotations (`@export_group`, `@export_subgroup`, `@export_category`) are recognized and discarded — they don't attach to a variable.

**Scope tracking:**
- `scope="class"` for declarations at indent 0 (class-level)
- `scope="local"` for declarations inside function bodies (indent > 0)
- Only class-scope variables are captured; local variables inside function bodies are not captured (they are transient implementation details not useful for structural analysis)

**Type inference via `:=`:**
- `var x := "hello"` → `type_hint=""`, `default_value='"hello"'` (type is inferred but not declared)
- The parser captures what's explicitly written, not what Godot infers

**Inline comment stripping:**
Before parsing, inline comments are stripped from the line: `var speed: float = 5.0  # movement speed` becomes `var speed: float = 5.0`. The stripping respects `#` inside string literals (simple approach: find `#` not preceded by odd number of quote characters).

**Multiline values (var and const):**
- `const LEVEL_XP: Array[int] = [0, 40, 100, ...]` spanning multiple lines
- `const UNLOCK_TABLE: Dictionary = { 1: {...}, ... }` spanning many lines
- `var session_stats: Dictionary = { "key": 0, ... }` spanning multiple lines
- The parser uses bracket-depth tracking (matching `[/]` and `{/}`) to accumulate multiline values into a single default_value string
- The IN_MULTILINE_VALUE state triggers for BOTH `var` and `const` with unclosed brackets

**Property getter/setter:**
Lines like `var health: int:` (trailing colon without `=`) indicate a property with getter/setter blocks. These are captured as variables (without default_value), and the getter/setter body is skipped (treated like a function body using indentation tracking).

**static var:**
Lines starting with `static var` are handled by stripping the `static ` prefix before applying the variable regex, and setting `is_static=True`.

### 3.4 GdEnum — Enum Definition

```python
@dataclass
class GdEnum:
    """An enum definition."""
    name: str = ""  # empty for anonymous enums
    values: dict[str, int | None] = field(default_factory=dict)
    line: int = 0
```

- Named: `enum GameState { PLAYING, PAUSED, DAILY_SUMMARY }` → `GdEnum(name="GameState", values={"PLAYING": None, "PAUSED": None, "DAILY_SUMMARY": None})`
- With explicit values: `enum Named { THING_1, THING_2 = 5 }` → values={"THING_1": None, "THING_2": 5}
- Negative values: `enum { A, B = -1 }` → values={"A": None, "B": -1}
- Anonymous: `enum { A, B, C }` → `GdEnum(name="", values={"A": None, "B": None, "C": None})`
- Multiline enums use the same bracket-depth accumulation as multiline values

### 3.5 GdSignalConnection — .connect() Call

```python
@dataclass
class GdSignalConnection:
    """A signal .connect() call found in code."""
    signal_source: str = ""
    target: str = ""
    has_bind: bool = False
    bind_args: str = ""
    line: int = 0
```

Patterns detected:
- `printer.print_started.connect(_on_print_started)` → signal_source="printer.print_started", target="_on_print_started"
- `timeout.connect(_handler.bind(param))` → target="_handler", has_bind=True, bind_args="param"
- `level_up.connect(func(_l, _u): fired[0] = true)` → target="func(_l, _u): fired[0] = true"
- `$Timer.timeout.connect(_on_timeout)` → signal_source="$Timer.timeout", target="_on_timeout"

**Depth-aware parsing:** The `.connect(` regex finds the position, then uses depth-aware parenthesis matching to find the correct closing `)`, avoiding over-matching when trailing code exists after the connect call (e.g., `if signal.connect(handler) == OK:`).

### 3.6 GdSignalEmission — .emit() Call

```python
@dataclass
class GdSignalEmission:
    """A signal .emit() call found in code."""
    signal_name: str = ""
    arg_count: int = 0
    line: int = 0
```

- `cash_changed.emit(cash, amount)` → signal_name="cash_changed", arg_count=2
- `health_depleted.emit()` → signal_name="health_depleted", arg_count=0
- `any_print_started.emit(printer_id, product)` → arg_count=2

The arg_count is determined by counting commas at depth 0 inside the emit parentheses (similar logic to the value_parser's `_split_at_depth_zero`). Empty args → 0.

### 3.7 GdAutoloadRef — Autoload Singleton Reference

```python
@dataclass
class GdAutoloadRef:
    """An autoload/singleton reference found in code."""
    autoload_name: str = ""
    access: str = ""
    pattern: str = ""  # "direct" | "has_node" | "get_node" | "get_node_or_null"
    line: int = 0
```

Detection strategy uses a **configurable list** of known autoload names passed to `parse_gd()`:

1. **Direct pattern:** `EconomyManager.spend_cash(...)` → When `EconomyManager` is in `known_autoloads`, any line matching `AutoloadName.something` is captured. `access` = everything after the dot on that line segment. A single alternation regex `\b(Name1|Name2|...)\.(\w+)` is compiled once for efficiency.

2. **has_node pattern:** `has_node("/root/ProgressionManager")` → The regex extracts the name from `/root/NAME`. Always detected (structurally unambiguous).

3. **get_node pattern:** `get_node("/root/ProgressionManager")` → Same as has_node but with `get_node`. Always detected.

4. **get_node_or_null pattern:** `get_tree().root.get_node_or_null("Main/Systems/PrinterSlotManager")` → Captures the path argument. Always detected.

When `known_autoloads` is None, direct-pattern detection is disabled (no way to distinguish autoloads from local variables). The has_node, get_node, and get_node_or_null patterns are always detected since they're structurally unambiguous.

### 3.8 GdPreloadRef — preload/load Reference

```python
@dataclass
class GdPreloadRef:
    """A preload() or load() reference found in code."""
    path: str = ""
    is_preload: bool = True
    line: int = 0
```

- `preload("res://scenes/player.tscn")` → GdPreloadRef(path="res://scenes/player.tscn", is_preload=True)
- `load("res://data/items.tres")` → GdPreloadRef(path="res://data/items.tres", is_preload=False)

**Detection scope:** preload/load are detected in ALL lines regardless of state — both at TOP_LEVEL (e.g., `const MyScene = preload(...)`) and inside function bodies. When a preload appears in a const declaration, BOTH a GdVariable and a GdPreloadRef are created for the same line.

### 3.9 GdScript — Top-Level Container

```python
@dataclass
class GdScript:
    """Parsed representation of a .gd file."""
    class_name: str = ""
    extends: str = ""
    signals: list[GdSignal] = field(default_factory=list)
    functions: list[GdFunction] = field(default_factory=list)
    variables: list[GdVariable] = field(default_factory=list)
    enums: list[GdEnum] = field(default_factory=list)
    inner_classes: list[str] = field(default_factory=list)
    signal_connections: list[GdSignalConnection] = field(default_factory=list)
    signal_emissions: list[GdSignalEmission] = field(default_factory=list)
    autoload_refs: list[GdAutoloadRef] = field(default_factory=list)
    preload_refs: list[GdPreloadRef] = field(default_factory=list)
    is_instance_valid_lines: list[int] = field(default_factory=list)
```

## 4. State Machine Design

### 4.1 States

```
TOP_LEVEL → IN_FUNCTION → TOP_LEVEL (on dedent)
TOP_LEVEL → IN_ENUM → TOP_LEVEL (on closing brace)
TOP_LEVEL → IN_MULTILINE_VALUE → TOP_LEVEL (on bracket balance)
TOP_LEVEL → IN_INNER_CLASS → TOP_LEVEL (on dedent to class level)
```

### 4.2 State Transitions

**TOP_LEVEL state** — processes class-level declarations:

| Line pattern | Action |
|-------------|--------|
| `class_name X` | Set `result.class_name` |
| `class_name X extends Y` | Set both `class_name` and `extends` |
| `extends X` | Set `result.extends` |
| `signal name(...)` | Parse and append GdSignal |
| `enum Name { ... }` (single line) | Parse and append GdEnum |
| `enum Name {` (multiline start) | Enter IN_ENUM, begin accumulating |
| `enum { ... }` (anonymous, single line) | Parse anonymous GdEnum |
| `@export* var` / `@onready var` (same line) | Parse annotation + var together |
| `@export*` / `@onready` (standalone line) | Buffer annotation for next var |
| `@export_group` / `@export_subgroup` / `@export_category` | Discard (standalone grouping) |
| `@rpc` / `@warning_ignore` / `@tool` / `@icon` / `@abstract` | Discard (not var annotations) |
| `static var name` at indent 0 | Parse GdVariable(is_static=True) |
| `var name` / `const name` at indent 0 | Parse GdVariable (apply buffered annotation if present) |
| `var name: Type:` (trailing colon, no `=`) | Parse as property, enter IN_FUNCTION to skip getter/setter body |
| `var/const name = [/{ ` (unclosed bracket) | Enter IN_MULTILINE_VALUE |
| `static func name(...)` | Parse GdFunction(is_static=True), enter IN_FUNCTION |
| `func name(...)` | Parse GdFunction, enter IN_FUNCTION |
| `class Name:` | Append to inner_classes, enter IN_INNER_CLASS |
| Comment / blank | Skip |

**All lines (regardless of state)** are also scanned for:
- `preload(...)` / `load(...)` → append GdPreloadRef
- `is_instance_valid(...)` → append line number

**IN_FUNCTION state** — scans function body for patterns:

| Line pattern | Action |
|-------------|--------|
| Dedent to indent ≤ func_indent | Exit to TOP_LEVEL, set function.end_line to last non-blank, non-comment line |
| `.connect(...)` | Parse GdSignalConnection (depth-aware) |
| `.emit(...)` | Parse GdSignalEmission |
| `AutoloadName.method(...)` | Parse GdAutoloadRef (if in known_autoloads) |
| `has_node("/root/...")` | Parse GdAutoloadRef(pattern="has_node") |
| `get_node("/root/...")` | Parse GdAutoloadRef(pattern="get_node") |
| `get_tree().root.get_node_or_null(...)` | Parse GdAutoloadRef(pattern="get_node_or_null") |
| Everything else | Skip |

**IN_ENUM state** — accumulates multiline enum:
- Append each line to buffer
- Track `{` / `}` depth
- When depth returns to 0, parse accumulated enum body and return to TOP_LEVEL

**IN_MULTILINE_VALUE state** — accumulates multiline var/const value:
- Append each line to buffer
- Track `[` / `]` and `{` / `}` depth
- When depth returns to 0, set the variable's default_value and return to TOP_LEVEL

**IN_INNER_CLASS state** — skips inner class body:
- Track indent level when class was declared
- Skip all lines with indent > class declaration indent
- On dedent back to class level, return to TOP_LEVEL
- Only captures the class name; does NOT parse inner class contents

### 4.3 Indentation Tracking

The state machine measures indentation as the count of leading whitespace characters (tabs or spaces). This handles both tab-indented (GDScript standard) and space-indented files. When in IN_FUNCTION, the function's start indent is recorded (always 0 for class-level functions). Any non-blank, non-comment line with indent ≤ start indent triggers function end.

Special case: blank lines and comment-only lines do not trigger function end or inner class end.

### 4.4 Annotation Handling

**Primary path (same-line):** When a line contains both an annotation and a `var` declaration (e.g., `@export var speed: float = 5.0`), the annotation is extracted and the var is parsed in a single pass.

**Fallback path (separate-line buffering):** When an `@export*` or `@onready` annotation appears on its own line, it is buffered and applied to the next `var` line encountered. The buffer is cleared after application.

**Non-var annotations:** `@rpc`, `@warning_ignore`, `@tool`, `@icon`, `@abstract`, `@static_unload` are recognized but NOT buffered. They are silently skipped to avoid corrupting the annotation buffer.

### 4.5 Comment Stripping

Before regex matching, each line has inline comments stripped: `var speed: float = 5.0  # movement speed` becomes `var speed: float = 5.0`. The stripping finds the first `#` that is not inside a string literal. Simple approach: scan left-to-right, track whether we're inside `"` or `'` delimiters, and strip from the first unquoted `#`.

This does NOT strip lines that are entirely comments (those are handled separately as skip conditions).

## 5. Regex Patterns

Pre-compiled at module level for performance (following value_parser.py's pattern):

### 5.1 Class-Level Declarations

- **Combined class_name + extends:** `^class_name\s+(\w+)\s+extends\s+(.+)$` — checked FIRST
- **class_name:** `^class_name\s+(\w+)` — captures the class name
- **extends:** `^extends\s+(.+)$` — captures everything after `extends` (can be `Node`, `"res://path.gd"`, `"file.gd".Inner`)

### 5.2 Signals

- `^signal\s+(\w+)(?:\(([^)]*)\))?` — captures signal name and optional arguments string
- Arguments string is then split and each arg parsed for `name: Type` pattern

### 5.3 Functions

- `^(?:static\s+)?func\s+(\w+)\s*\(` — detect function start, then use depth-aware paren matching to extract the full argument list and return type
- Function detection: find `func name(`, then scan forward with depth tracking to find the matching `)`, then check for `-> ReturnType :` after the closing paren
- Args string parsing: depth-aware comma splitting, then each arg parsed for `name: Type = default` pattern

**Known limitation:** Multi-line function signatures (parameters spanning multiple lines) are not supported. All fixtures use single-line signatures.

### 5.4 Variables

- `^(?:static\s+)?(var|const)\s+(\w+)\s*(?::\s*([^=:]+?))?\s*(?:(?::=|=)\s*(.+))?$` — captures optional static, var/const, name, optional type, optional initializer
- The `static` prefix is detected and stripped before applying the regex, setting `is_static=True`
- The type capture uses `[^=:]+?` to avoid consuming `=` or the trailing `:` of property getter/setter syntax

### 5.5 Enums

- Single-line: `^enum\s+(\w+)?\s*\{([^}]*)\}` — captures optional name and body
- Multiline start: `^enum\s+(\w+)?\s*\{` without closing `}`
- Enum value parsing: split body by `,`, each value parsed for `NAME = value` or just `NAME`; supports negative integer values

### 5.6 Body Patterns (IN_FUNCTION state)

- **connect:** Find `.connect(` in line, then use depth-aware parenthesis matching to extract the argument. Signal source is everything before `.connect(` (trimmed). Target is the first argument.
- **emit:** `(\w+)\.emit\(` — find position, then depth-aware paren matching for arg_count
- **autoload direct:** Single alternation regex `\b(Name1|Name2|...)\.(\w+)` compiled once from known_autoloads list
- **has_node:** `has_node\(\s*"(/root/[\w/]+)"\s*\)` — captures path
- **get_node:** `get_node\(\s*"(/root/[\w/]+)"\s*\)` — captures path (distinct from get_node_or_null)
- **get_node_or_null:** `get_node_or_null\(\s*"([^"]+)"\s*\)` — captures path argument
- **is_instance_valid:** `is_instance_valid\(` — just detect presence
- **preload/load:** `(?:preload|load)\(\s*"([^"]+)"\s*\)` — captures path

### 5.7 Other

- **inner class:** `^class\s+(\w+)\s*(?:extends\s+[\w.]+)?\s*:` — captures class name
- **annotation with var (same line):** `^(@\w+(?:\([^)]*\))?)\s+(var|const)\s+` — captures annotation and detects var/const on same line
- **standalone annotation:** `^@(\w+)(?:\(([^)]*)\))?` — for buffering or discarding

## 6. Implementation Strategy

### 6.1 Module Structure

The file is organized as:
1. **Imports** — `from __future__ import annotations`, `re`, `logging`, `dataclasses`
2. **Logger** — `logger = logging.getLogger(__name__)`
3. **Compiled regex patterns** — module-level `re.compile()` constants
4. **Dataclasses** — all 9 dataclasses in dependency order
5. **Helper functions** — `_strip_comment()`, `_split_args_depth_aware()`, `_parse_signal_args()`, `_parse_func_args()`, `_count_emit_args()`, `_parse_enum_body()`, `_find_matching_paren()`
6. **Internal function** — `_parse_gd_content(content: str, known_autoloads)` with the state machine
7. **Public function** — `parse_gd(path, known_autoloads)` as thin file-reading wrapper

### 6.2 Helper Functions

```python
def _strip_comment(line: str) -> str:
    """Strip inline comment from a line, respecting string literals."""

def _split_args_depth_aware(args_str: str) -> list[str]:
    """Split argument string by commas, respecting nested parens/brackets/braces."""

def _find_matching_paren(line: str, start: int) -> int:
    """Find the matching closing parenthesis from start position, respecting nesting."""

def _parse_signal_args(args_str: str) -> list[tuple[str, str]]:
    """Parse signal argument string into (name, type) tuples."""

def _parse_func_args(args_str: str) -> list[tuple[str, str, str]]:
    """Parse function argument string into (name, type, default) triples using depth-aware splitting."""

def _count_emit_args(args_str: str) -> int:
    """Count arguments in an emit() call, respecting nested parentheses."""

def _parse_enum_body(body: str) -> dict[str, int | None]:
    """Parse enum body string into {name: value} dict. Handles negative values."""
```

### 6.3 Best-Effort Error Handling

- File not found → raise `FileNotFoundError` (like parse_tscn)
- Malformed line → log at DEBUG, skip silently, continue parsing
- Unclosed multiline construct at EOF → close it with whatever was accumulated
- Empty file → return default GdScript() with all empty fields

## 7. Edge Cases

### 7.1 From Real Fixtures

| Edge case | Fixture | Handling |
|-----------|---------|----------|
| Signals with custom types: `Printer.State` | printer_manager.gd | Type stored as-is: "Printer.State" |
| Multiline const array (12 lines) | progression_manager.gd | IN_MULTILINE_VALUE with bracket tracking |
| Multiline const dict (11 lines) | progression_manager.gd | Same bracket tracking for `{}` |
| Multiline var dict (`session_stats`) | game_manager.gd | IN_MULTILINE_VALUE triggers for `var` too |
| Lambda in connect | progression_manager.gd | GdSignalConnection with lambda target |
| Multiple autoload refs per file | game_manager.gd | Each occurrence creates separate GdAutoloadRef |
| `is_instance_valid` in loop | printer_manager.gd | Each call records its line number |
| get_tree().root.get_node_or_null | printer_manager.gd | Captured as GdAutoloadRef(pattern="get_node_or_null") |
| get_node("/root/X") | game_manager.gd | Captured as GdAutoloadRef(pattern="get_node") |
| has_node("/root/X") | printer_manager.gd | Captured as GdAutoloadRef(pattern="has_node") |
| Minimal 1-line file: just `extends Node3D` | sample fixtures | Parses extends, everything else empty |
| Doc comments (`##`) | progression_manager.gd | Skipped (not parsed as code) |
| Inline comments after code | all fixtures | Stripped before regex matching |

### 7.2 Type Hint Complexity

The parser stores type hints as raw strings without decomposition:
- Simple: `int`, `String`, `float`, `bool`
- Generic: `Array[Printer]`, `Array[String]`, `Dictionary`
- Dotted: `Printer.State`, `Node.PROCESS_MODE_ALWAYS`
- These are all captured verbatim — no attempt to resolve or validate types

### 7.3 class_name + extends on Same Line

`class_name Item extends Node` — handled by checking for combined pattern before individual patterns. Both `class_name` and `extends` are set.

### 7.4 Known Limitations

- Multi-line function signatures are not supported (no fixtures use them)
- Legacy `emit_signal("name")` is not detected (Godot 3.x pattern, deprecated)
- `await signal` is not tracked (can be added in future iteration)
- Autoload boolean checks (`if EconomyManager:` without dot access) are not detected

## 8. Test Plan Summary

Tests go in `tests/test_parsers/test_gd_parser.py`, organized as class-based groups (matching project convention):

| Test class | Coverage |
|-----------|----------|
| `TestClassDeclarations` | class_name, extends, combined, minimal files |
| `TestSignalParsing` | signals with/without args, typed args, custom types |
| `TestFunctionParsing` | basic, typed, defaults with parens, static, virtual/private, end_line |
| `TestVariableParsing` | var, const, typed, @export (same-line), @onready, multiline const, multiline var, static var, property getter/setter |
| `TestEnumParsing` | named, anonymous, with explicit values, negative values, multiline |
| `TestSignalConnections` | direct, bind, lambda, $Node, depth-aware parsing |
| `TestSignalEmissions` | no args, multiple args, arg count |
| `TestAutoloadRefs` | direct, has_node, get_node, get_node_or_null |
| `TestPreloadLoad` | preload, load paths, preload in const |
| `TestIsInstanceValid` | detection and line tracking |
| `TestInnerClasses` | name detection, body skipping |
| `TestIntegration` | Full fixture parsing (economy_manager.gd, printer_manager.gd, game_manager.gd, progression_manager.gd) with assertions on expected counts and names |
| `TestEdgeCases` | empty file, minimal file, malformed input, missing file, comment stripping |

Tests use real fixture files from `tests/fixtures/` for integration tests. Unit tests call `_parse_gd_content()` directly with inline strings.

## 9. File Summary

| File | Action | Description |
|------|--------|-------------|
| `src/godotiq/parsers/gd_parser.py` | Expand | Replace stub with full parser (~500-600 lines) |
| `tests/test_parsers/test_gd_parser.py` | Create | Comprehensive test suite (~350-450 lines) |

No other files are modified.
