Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ This file documents the changes made to the formatter with each release.
### Changed

- Force short lines that were manually wrapped on multiple lines with commas to merge back into one line when they fit within the max line length (this behavior was a leftover from the old formatter implementation)
- Changed editor settings for safe and reorder mode; they're now a single format mode
- Deprecated `--safe` flag, renamed into `--verify-structure`

### Fixed

- Fix type casts with type subscripts wrapping when placed at the end of long lines (e.g. `[long, array] as Array[SomeType]`)
- Fix icon annotation being reordered below class_name when extends was before class_name (#295)
- Godot addon: Fix error when running the formatter with both safe and reorder mode active; they're now mutually exclusive in the editor settings (#286)

## Release 0.22.2 (2026-07-22)

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ You can also pass multiple files or folders:
gdscript-formatter path/to/file.gd path/to/folder
```

Use the `--safe` flag to add a safety check that prevents overwriting files if the formatter makes unwanted changes (any change that would modify the code meaning, like removing a piece of functional code). This is most useful when formatting many files at once, running the formatter from a script or in continuous integration, or when you use a development version of the formatter:
Use the `--verify-structure` flag to reparse the formatted output and reject it if its structure differs from the input. This is an imperfect check, not a guarantee that formatting is safe or semantically equivalent. It is most useful when formatting many files at once, running the formatter from a script or in continuous integration, or when you do not regularly use version control:

```bash
gdscript-formatter --safe path/to/folder
gdscript-formatter --verify-structure path/to/folder
```

Format with check mode, to use in a build system (exit code 1 if changes needed):
Expand Down
111 changes: 106 additions & 5 deletions addons/GDQuest_GDScript_formatter/plugin.gd
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const SETTING_FORMAT_ON_SAVE = "format_on_save"
const SETTING_SHORTCUT = "shortcut"
const SETTING_USE_SPACES = "use_spaces"
const SETTING_INDENT_SIZE = "indent_size"
const SETTING_FORMAT_MODE = "format_mode"
const SETTING_REORDER_CODE = "reorder_code"
const SETTING_SAFE_MODE = "safe_mode"
const SETTING_FORMATTER_PATH = "formatter_path"
Expand All @@ -25,6 +26,30 @@ const SETTING_LINT_IGNORED_RULES = "lint_ignored_rules"
# Directories to ignore when Format on Save is enabled
const SETTING_IGNORED_DIRECTORIES = "format_on_save_ignored_directories"

## Represents the modes the user wants to use by default, notably when running
## the formatter on save.
enum FormatMode {
NORMAL,
## Reorder the code according to the official style guide every time the
## formatter runs. Note that without this option, you can still reorder any
## time from the format menu in the script editor.
REORDER_CODE,
## Reparse the formatted code and compare its structure with the original.
##
## [b]WARNING:[/b] this is an imperfect check. It does not guarantee that
## formatting is 100% safe or semantically equivalent. We always recommend
## using a version control system when running the formatter.
##
## This is an option used notably for development of the formatter, when
## testing it on new codebases, to quickly catch bugs on unsupported
## GDScript syntax.
##
## When using the formatter normally on individual scripts, you can always
## undo after formatting or use a version control system to track, review,
## and undo changes.
VERIFY_STRUCTURE,
}

const COMMAND_PALETTE_CATEGORY = "gdquest gdscript formatter/"
const COMMAND_PALETTE_FORMAT_SCRIPT = "Format GDScript"
const COMMAND_PALETTE_LINT_SCRIPT = "Lint GDScript"
Expand All @@ -36,8 +61,7 @@ var DEFAULT_SETTINGS = {
SETTING_FORMAT_ON_SAVE: false,
SETTING_USE_SPACES: false,
SETTING_INDENT_SIZE: 4,
SETTING_REORDER_CODE: false,
SETTING_SAFE_MODE: true,
SETTING_FORMAT_MODE: FormatMode.NORMAL,
SETTING_FORMATTER_PATH: "",
SETTING_LINT_ON_SAVE: false,
SETTING_LINT_LINE_LENGTH: 100,
Expand All @@ -56,6 +80,9 @@ var formatter_cache_dir: String
var menu: FormatterMenu = null
var _has_uninstall_command := false
var _has_formatter_command := false
var _has_format_command := false
var _has_lint_command := false
var _already_warned_about_reorder_on_save := false
# Used to auto detect changes to the project's .editorconfig file.
var _editorconfig_last_modified_time := -1
# Editorconfig allows setting rules per path glob. We track globs for the format
Expand All @@ -64,7 +91,14 @@ var _editorconfig_format_on_save_rules: Array[Dictionary] = []


func _init() -> void:
migrate_format_mode_setting()
if not has_editor_setting(SETTING_FORMAT_MODE):
set_editor_setting(SETTING_FORMAT_MODE, DEFAULT_SETTINGS[SETTING_FORMAT_MODE])
register_format_mode_setting()

for setting: String in DEFAULT_SETTINGS.keys():
if setting == SETTING_FORMAT_MODE:
continue
if not has_editor_setting(setting):
set_editor_setting(setting, DEFAULT_SETTINGS[setting])

Expand All @@ -83,6 +117,51 @@ func _init() -> void:
set_editor_setting(SETTING_SHORTCUT, shortcut)


func register_format_mode_setting() -> void:
var editor_settings := EditorInterface.get_editor_settings()
var setting_name := EDITOR_SETTINGS_CATEGORY + SETTING_FORMAT_MODE
editor_settings.add_property_info({
"name": setting_name,
"type": TYPE_INT,
"hint": PROPERTY_HINT_ENUM,
"hint_string": "Normal,Reorder code,Verify structure",
})
editor_settings.set_initial_value(setting_name, DEFAULT_SETTINGS[SETTING_FORMAT_MODE], false)


## Converts the old independent settings to the mutually exclusive format mode.
## The legacy safe mode takes priority because it is the least destructive option.
func migrate_format_mode_setting() -> void:
if has_editor_setting(SETTING_FORMAT_MODE):
return

# Inferring a version number from the old settings; editor settings does not
# give us a neat way to version our settings so we do it manually. It's just
# to keep track of migrations.
var version := -1
if has_editor_setting(SETTING_REORDER_CODE) or has_editor_setting(SETTING_SAFE_MODE):
version = 1

# Upgrade to version 2; that's when we merged safe mode and reorder code
# into a single format mode (because they're mutually exclusive).
if version == 1:
var format_mode := FormatMode.NORMAL
if has_editor_setting(SETTING_SAFE_MODE) and get_editor_setting(SETTING_SAFE_MODE) as bool:
format_mode = FormatMode.VERIFY_STRUCTURE
elif has_editor_setting(SETTING_REORDER_CODE) and get_editor_setting(SETTING_REORDER_CODE) as bool:
format_mode = FormatMode.REORDER_CODE

set_editor_setting(SETTING_FORMAT_MODE, format_mode)

# Remove the old settings so users do not see two conflicting configurations.
var editor_settings := EditorInterface.get_editor_settings()
for setting_name: String in [SETTING_REORDER_CODE, SETTING_SAFE_MODE]:
if has_editor_setting(setting_name):
editor_settings.erase(EDITOR_SETTINGS_CATEGORY + setting_name)

version = 2


func _enter_tree() -> void:
formatter_cache_dir = EditorInterface.get_editor_paths().get_cache_dir().path_join("gdquest")
installer = FormatterInstaller.new(formatter_cache_dir)
Expand Down Expand Up @@ -219,6 +298,9 @@ func _on_resource_saved(saved_resource: Resource) -> void:
if editorconfig_format_on_save != null:
do_format_on_save = editorconfig_format_on_save as bool
var lint_on_save := get_editor_setting(SETTING_LINT_ON_SAVE) as bool
if do_format_on_save and get_format_mode() == FormatMode.REORDER_CODE and not _already_warned_about_reorder_on_save:
push_warning("GDScript Formatter: Reorder code is enabled for format on save. It is usually better used manually.")
_already_warned_about_reorder_on_save = true

if not do_format_on_save and not lint_on_save:
return
Expand Down Expand Up @@ -286,6 +368,8 @@ func _on_resource_saved(saved_resource: Resource) -> void:


func add_format_command() -> void:
if _has_format_command:
return
var formatter_path := get_editor_setting(SETTING_FORMATTER_PATH) as String
if formatter_path.is_empty() or not _has_formatter_command:
if not formatter_path.is_empty():
Expand All @@ -302,27 +386,35 @@ func add_format_command() -> void:
format_current_script,
shortcut.get_as_text() if is_instance_valid(shortcut) else "None",
)
_has_format_command = true


func remove_format_command() -> void:
if not _has_format_command:
return
EditorInterface.get_command_palette().remove_command(COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_FORMAT_SCRIPT)
_has_format_command = false


func add_lint_command() -> void:
if not _has_formatter_command:
if not _has_formatter_command or _has_lint_command:
return

EditorInterface.get_command_palette().add_command(
COMMAND_PALETTE_LINT_SCRIPT,
COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_LINT_SCRIPT,
lint_current_script,
)
_has_lint_command = true


func remove_lint_command() -> void:
if not _has_lint_command:
return
EditorInterface.get_command_palette().remove_command(
COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_LINT_SCRIPT,
)
_has_lint_command = false


func add_install_update_command() -> void:
Expand Down Expand Up @@ -402,6 +494,7 @@ func uninstall_formatter() -> void:
_has_formatter_command = false

remove_format_command()
remove_lint_command()
add_format_command()
remove_uninstall_command()
add_uninstall_command()
Expand Down Expand Up @@ -617,12 +710,16 @@ func format_code(script: GDScript, force_reorder := false, source_content: Varia
formatter_arguments.push_back("--use-spaces")
formatter_arguments.push_back("--indent-size=%d" % get_editor_setting(SETTING_INDENT_SIZE))

var should_reorder := force_reorder or get_editor_setting(SETTING_REORDER_CODE) as bool
var format_mode := get_format_mode()
var should_reorder := force_reorder or format_mode == FormatMode.REORDER_CODE

if should_reorder:
formatter_arguments.push_back("--reorder-code")

if get_editor_setting(SETTING_SAFE_MODE):
if not force_reorder and format_mode == FormatMode.VERIFY_STRUCTURE:
# NB: this is a deprecated flag, replaced with --verify-structure, but
# we keep it here for users that have a previous version of the
# formatter installed.
formatter_arguments.push_back("--safe")

formatter_arguments.push_back(path_temporary_file)
Expand Down Expand Up @@ -657,6 +754,10 @@ func format_code(script: GDScript, force_reorder := false, source_content: Varia
return formatted_content


func get_format_mode() -> int:
return get_editor_setting(SETTING_FORMAT_MODE) as int


## Lints a GDScript file using the GDScript Formatter's linter,
## and returns an array of lint issues.
func lint_code(script: GDScript) -> Array:
Expand Down
24 changes: 16 additions & 8 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const HELP_FORMATTER: &str = "\

Options:
-c, --check Check if files are formatted, exit 1 if not
-s, --safe Safe mode: abort if formatting changes code meaning
--verify-structure Verify formatted output has the same structure as the input
--stdout Write to stdout instead of overwriting files
--use-spaces Use spaces instead of tabs for indentation
--indent-size <NUM> Spaces per indent level (default: 4)
Expand Down Expand Up @@ -79,9 +79,9 @@ pub enum Command {
use_spaces: Option<bool>,
/// Number of spaces to use for indentation.
indent_size: Option<usize>,
/// If true, the formatter will re-parse the formatted code to ensure it
/// matches the original code semantics before writing it to files.
use_safe_mode: bool,
/// If true, the formatter will re-parse the formatted code and verify it
/// has the same structure as the original before writing it to files.
use_verify_structure: bool,
/// If true, the formatter will reorder code to follow the official
/// GDScript style guide's recommended order of code elements.
do_reorder_code: bool,
Expand Down Expand Up @@ -128,7 +128,7 @@ pub fn parse_args() -> CliArguments {
let mut format_do_check_formatted_only = false;
let mut format_use_spaces: Option<bool> = None;
let mut format_indent_size: Option<usize> = None;
let mut format_use_safe_mode = false;
let mut format_use_verify_structure = false;
let mut format_do_reorder_code = false;
let mut format_max_line_length: Option<usize> = None;
let mut format_blank_lines_around_definitions: Option<u16> = None;
Expand Down Expand Up @@ -191,9 +191,17 @@ pub fn parse_args() -> CliArguments {
require_no_value(assigned_value, "--use-spaces");
format_use_spaces = Some(true);
}
"verify-structure" => {
require_no_value(assigned_value, "--verify-structure");
format_use_verify_structure = true;
}
"safe" => {
// DEPRECATED: We keep this flag for anyone who already
// used it as part of a CI or of their workflow. But
// it's now replaced by --verify-structure, which is a
// more accurate name for this mode.
require_no_value(assigned_value, "--safe");
format_use_safe_mode = true;
format_use_verify_structure = true;
}
"reorder-code" => {
require_no_value(assigned_value, "--reorder-code");
Expand Down Expand Up @@ -332,7 +340,7 @@ pub fn parse_args() -> CliArguments {
if matches!(active_command, ActiveCommand::Lint) {
print_error_invalid_argument("unexpected argument '-s'");
}
format_use_safe_mode = true;
format_use_verify_structure = true;
}
_ => print_error_invalid_argument(&format!(
"unexpected argument '-{}'",
Expand All @@ -354,7 +362,7 @@ pub fn parse_args() -> CliArguments {
do_check_formatted_only: format_do_check_formatted_only,
use_spaces: format_use_spaces,
indent_size: format_indent_size,
use_safe_mode: format_use_safe_mode,
use_verify_structure: format_use_verify_structure,
do_reorder_code: format_do_reorder_code,
max_line_length: format_max_line_length,
blank_lines_around_definitions: format_blank_lines_around_definitions,
Expand Down
12 changes: 8 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub mod node_kind;
pub mod parser;
pub mod renderer;
pub mod reorder;
pub mod safe_mode;
pub mod verify_structure;

pub use renderer::{PrinterConfiguration, RenderElement};

Expand Down Expand Up @@ -114,10 +114,14 @@ pub fn format_gdscript_with_buffers(

if config.safe {
let reparsed = parser::ParseInput::new(output, config)
.ok_or_else(|| "Safe mode: formatted output does not parse".to_string())?;
if !safe_mode::trees_structurally_equal(&parsed.tree, &reparsed.tree, parsed.kind_lookup) {
.ok_or_else(|| "Verify structure: formatted output does not parse".to_string())?;
if !verify_structure::trees_structurally_equal(
&parsed.tree,
&reparsed.tree,
parsed.kind_lookup,
) {
return Err(
"Safe mode: formatted output is structurally different from input. \
"Verify structure: formatted output is structurally different from input. \
Keeping original source."
.to_string(),
);
Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
do_check_formatted_only,
use_spaces,
indent_size,
use_safe_mode,
use_verify_structure,
do_reorder_code,
max_line_length,
blank_lines_around_definitions,
Expand All @@ -121,7 +121,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
};

let mut config = FormatterConfiguration {
safe: use_safe_mode,
safe: use_verify_structure,
reorder_code: do_reorder_code,
..Default::default()
};
Expand Down
Loading