@gmloop/format

July 15, 2026 · View on GitHub

This directory contains the source code for the gml-modules/format package.

Test Tiering

  • Format fixture/unit tests validate formatter-owned layout behavior.
  • Project-aware behavior is validated in lint/CLI test suites; format tests do not validate semantic rewrite behavior.

Format Architecture

Constants (src/printer/constants.ts)

The format workspace uses a centralized constants file to define formatting defaults and thresholds. This ensures consistency across the codebase and makes it easy to understand and maintain default values.

Key constants include:

  • DEFAULT_PRINT_WIDTH (120): The default line width for code
  • DEFAULT_TAB_WIDTH (4): The default indentation width

These constants are used throughout the format workspace to ensure consistent behavior. Users can override formatting defaults through Prettier's standard options (e.g., printWidth, tabWidth).

Formatting Conventions

  • Prettier always formats functions with a space before the parameter list function (o) {}, but never for function calls fn(o). This behavior is fixed and not configurable. For a function name and its parameter list (e.g. function foo(x) {}) Prettier does NOT add a space. The formatter uses the same style convention.
  • Comments are never broken up or reflowed to fit the printWidth setting. This aligns with Prettier's default behavior for comments, preserving the developer's original line structure and preventing unintended corruption of commented-out code or manual formatting.
  • The formatter does not introduce additional line breaks or blank lines beyond what Prettier's core engine generates based on the document shape and printWidth. This means that struct literals, argument lists, and other constructs will wrap according to Prettier's standard rules without custom thresholds for the number of properties or parameters per line.
  • Like Prettier, this formatter does remove redundant parentheses, but it does not add new ones for readability. For example, expressions like a + (b * c) are formatted as a + b * c. Prettier is opinionated about layout and minimal syntax, but it avoids adding new structural elements like parentheses because that crosses from formatting into rewriting the code’s AST.
  • The formatter requires a valid parse; if parse fails, it should error and not change files. It should never produce partial or best-effort output on an invalid parse, and it should not attempt to salvage or reformat code when the input is syntactically incorrect

Deprecated And Removed Options

  • maxStructPropertiesPerLine was removed on February 7, 2026.
  • Why it was removed: struct layout is now fully opinionated and consistent with Prettier defaults, so struct wrapping is controlled by document shape and printWidth rather than a custom numeric threshold.
  • Migration: remove maxStructPropertiesPerLine from configuration files; no replacement option is provided.
  • maxParamsPerLine was removed on February 7, 2026.
  • Why it was removed: argument layout now follows Prettier-style default wrapping with printWidth and document shape, without numeric argument-count thresholds.
  • Migration: remove maxParamsPerLine from configuration files; no replacement option is provided.

Formatter/Linter/Refactor Ownership Boundaries

The format workspace owns formatting and parser-to-printer orchestration only (semicolons, whitespaces, line breaks, indentation, etc.).

  • @gmloop/format is formatter-only. The formatter never repairs invalid syntax and only formats valid AST.
  • Any file-scoped semantic/content rewrites belongs to @gmloop/lint rules and is applied via lint --write. Lint rule autofixes are responsible for fixing valid-but-forbidden syntax (e.g., style violations or deprecated patterns that are still syntactically valid). This includes fixing/generating function doc-comments and legacy annotation normalization.
  • Project-aware transformations and rewrites (e.g. globalvar rewrite, cross-file renames) are the responsibility of the @gmloop/refactor module, which is a 'codemod' module. Codemod/fixer commands are responsible for repairing non-parsable source text to restore parsability.
  • This Prettier formatter workspace must not depend directly on @gmloop/semantic, @gmloop/refactor, or @gmloop/lint.
  • Semantic/content rewrites and 'fixes' (fixing/generating function doc-comments, etc.) are lint auto-fix responsibilities in @gmloop/lint, provided they are single-file and do not require project-wide knowledge. Project-aware transformations (like the globalvar rewrite) always belong in @gmloop/refactor.

Formatter doc-comment boundary guarantees:

  • Legacy annotations such as /// @function ... are preserved by the formatter and are never replaced/normalized.
  • The formatter never synthesizes /// @description, /// @param, /// @returns, or other function doc-comment tags.
  • The formatter preserves explicitly-authored function parameter defaults, including = undefined, and never infers/removes optional defaults from doc-comment semantics.
  • The formatter does not remove default/placeholder comments (for example, GameMaker migration banners or editor placeholder comments); this is lint-owned via gml/remove-default-comments.

See the durable split contract and examples in docs/target-state.md.

TODO

  • BUG: The formatter is incorrectly adding double-blank lines in some cases, for example between gml_pragma("forceinline"); and // Check if are still on a valid... in this snippet:
        if (can_pathfind) {
          path = new PathHandler();
          last_known_pos = new Vector3(0, 0, 0); // last known position of AI owner - used to save position for reference in pathfinding time source
          ts_path = time_source_create(
              time_source_game,
              irandom_range(240, 260),
              time_source_units_frames,
              function () {
                  gml_pragma("forceinline");
    
    
                  // Check if are still on a valid path, if so we can skip generating a new one
                  var curr_sight_rad = sight_radius.get();
                  var curr_dist_to_pt = path.distance_to_next_point(last_known_pos.x, last_known_pos.y);
                  if (curr_dist_to_pt <= curr_sight_rad) { exit; }
    
                  var coords;
                  switch (state) {
                      case eAiState.WANDER_PATH:
                          // Pick random direction, then try to find free coordinates in that direction
                          var rand_dir_wander = irandom(359);
                          var curr_half_sght_rad = curr_sight_rad * 0.5;
                          coords = new Vector3(
                              last_known_pos.x + lengthdir_x(curr_half_sght_rad, rand_dir_wander), last_known_pos.y +
                              lengthdir_y(curr_half_sght_rad, rand_dir_wander), last_known_pos.z
                          );
                          break;
                      case eAiState.MOVE_TO_PATH:
                      case eAiState.FOLLOW_PATH:
                          // Path generation is the same for these states, difference is in steering
                          coords = targeting.get_last_known_position();
                          break;
                      default:
                          time_source_stop(ts_path);
                          exit;
                  }
    
                  if (is_undefined(coords)) {
                      LOG.warn("Failed to find a free position to pathfind to");
                  } else if (
                      !global.mp_controller.make_path(
                          last_known_pos.x,
                          last_known_pos.y,
                          last_known_pos.z,
                          coords.x,
                          coords.y,
                          coords.z,
                          path
                      )
                  ) {
                      LOG.warn("Failed to make a path to room position");
                  } else {
                      LOG.trace("Generated a path");
                  }
              },
              [],
              -1
          );
      }
    
  • BUG: With allowInlineControlFlowBlocks enabled, the formatter seems to be incorrectly breaking one-line brace statements into multi-line blocks, for example:
    -if (TRAILER_MODE) { exit; }
    +if (TRAILER_MODE) {
    +    exit;
    +}
    
    Is this expected behavior? Or a gap/miss/bug?