· 3 min read

Measuring GDScript complexity without an AST

by · Project: gdmetrics

#godot #gdscript #tooling

Godot projects grow the same way other codebases do: a few functions quietly become the ones nobody wants to touch. gdmetrics is an editor plugin and command-line tool that measures every GDScript function and ranks what to refactor first. It only reads scripts; it never changes them.

No parser to borrow

Most complexity tools walk an abstract syntax tree. Godot doesn't expose one to plugins, so gdmetrics has its own tokenizer, written in GDScript. It reads files line by line without regular expressions and produces keywords, identifiers, operators, strings and comments.

On top of that sit a few small passes:

  • Functions start at func, static func or signal and end when indentation returns to their level.
  • Control flow is tracked with stacks for indentation, blocks and lambdas; nesting depth is the size of the indentation stack.
  • Cyclomatic complexity starts at 1 and adds a point for each if, elif, for, while, match, match arm and ternary, plus logical operators when enabled.
  • Cognitive complexity adds 1 plus the current nesting depth for most constructs, so the same if costs more the deeper it sits. Lambdas cost a flat point and their bodies don't count against the enclosing function.

The cognitive score is modeled on the published cognitive complexity idea but makes its own choices. For example, early return, break and continue inside control flow add a point.

The edge cases were most of the work

An indentation-based language without a real parser has sharp corners:

  • Ternary or statement? x if cond else y and a statement if use the same keyword. If any token comes before if on the line, it's a ternary.
  • Match arms have no keyword. An arm is a line at the first indentation level under match that has a top-level colon. Commas in patterns and when guards are counted only outside brackets, and text.match(...) is rejected because a dot comes before it.
  • Line continuations. A trailing backslash joins lines, including when blank or comment lines follow, which matches a known Godot parser behavior.
  • Strings. Multi-line strings, raw strings and the &"...", ^"..." and @"..." literal forms are handled, and a byte-order mark at the start of a file is stripped.
  • Mixed tabs and spaces make indentation ambiguous. Those lines are flagged instead of guessed.

Every edge case has a fixture file and an expected JSON result, and a headless test compares the two.

In the editor

The dock has one main button: Find what to fix. Results come in three lists: top fixes, the largest files, and everything. Each function gets a plain label (OK, Hard to read, Fix soon) based on configurable thresholds. Double-clicking a row opens the script at that line.

A few extras make the ranking more useful:

  • Files with recent git churn are marked, since complex code that changes often is the expensive kind.
  • Comment directives ignore a function, or pin it so it stays visible.
  • Functions over the failure threshold can be annotated in the script editor, turned on in the config file.

In CI

The same analysis runs headless from the command line. It writes JSON, CSV and HTML reports, appends a line to a history file on every run, and can compare against a baseline to catch regressions. Exit codes are simple:

  • 0: everything is within thresholds.
  • 1: a function crossed the failure threshold, or the diff regressed.
  • 2: the tool itself failed. It also refuses to write over protected files such as project.godot.

A GitHub Actions template sets up Godot, runs the check and uploads the reports.

Two editions, one core

Godot 3 and 4 differ enough that one plugin can't serve both. The analysis core lives in the Godot 4 edition and is copied into the Godot 3 edition by a sync script, while the tokenizer and UI stay per version. Godot 3 needed small workarounds: early 3.x releases can't parse quit(code), so the CLI sets the exit code another way.

Limits

This is a heuristic parser, not a compiler. The README states the expected accuracy (about 90–93% on Godot 4 and 85–90% on Godot 3), and each result carries a confidence value so low-confidence functions can be checked by hand. Planned additions include Halstead metrics and a maintainability index.

The Godot 4 edition is on itch.io, and both editions are on GitHub (Godot 4, Godot 3).