Skip to content

Commit 5f5cfaf

Browse files
Add linting support to the Godot addon
* Add lint command to editor addon * Unused variables
1 parent 4e487e7 commit 5f5cfaf

File tree

2 files changed

+197
-26
lines changed

2 files changed

+197
-26
lines changed

addons/GDQuest_GDScript_formatter/menu.gd

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ signal menu_item_selected(command: String)
88
const MENU_TEXT = "Format"
99
const MENU_ITEMS = {
1010
"format_script": "Format Current Script",
11+
"lint_script": "Lint Current Script",
1112
"reorder_code": "Reorder Code",
1213
"install_update": "Install or Update Formatter",
1314
"uninstall": "Uninstall Formatter",
@@ -101,6 +102,11 @@ func _populate_menu(show_uninstall: bool = true) -> void:
101102
popup_menu.set_item_tooltip(current_item_index, "Run the GDScript Formatter over the current script")
102103
current_item_index += 1
103104

105+
popup_menu.add_item(MENU_ITEMS["lint_script"], current_item_index)
106+
popup_menu.set_item_metadata(current_item_index, "lint_script")
107+
popup_menu.set_item_tooltip(current_item_index, "Check the current script for linting issues")
108+
current_item_index += 1
109+
104110
popup_menu.add_item(MENU_ITEMS["reorder_code"], current_item_index)
105111
popup_menu.set_item_metadata(current_item_index, "reorder_code")
106112
popup_menu.set_item_tooltip(current_item_index, "Reorder the code elements in the current script according to the GDScript Style Guide")

addons/GDQuest_GDScript_formatter/plugin.gd

Lines changed: 191 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ const SETTING_INDENT_SIZE = "indent_size"
1919
const SETTING_REORDER_CODE = "reorder_code"
2020
const SETTING_SAFE_MODE = "safe_mode"
2121
const SETTING_FORMATTER_PATH = "formatter_path"
22+
const SETTING_LINT_ON_SAVE = "lint_on_save"
23+
const SETTING_LINT_LINE_LENGTH = "lint_line_length"
24+
const SETTING_LINT_IGNORED_RULES = "lint_ignored_rules"
2225

2326
const COMMAND_PALETTE_CATEGORY = "gdquest gdscript formatter/"
2427
const COMMAND_PALETTE_FORMAT_SCRIPT = "Format GDScript"
28+
const COMMAND_PALETTE_LINT_SCRIPT = "Lint GDScript"
2529
const COMMAND_PALETTE_INSTALL_UPDATE = "Install or Update Formatter"
2630
const COMMAND_PALETTE_UNINSTALL = "Uninstall Formatter"
2731
const COMMAND_PALETTE_REPORT_ISSUE = "Report Issue"
@@ -32,9 +36,16 @@ const DEFAULT_SETTINGS = {
3236
SETTING_INDENT_SIZE: 4,
3337
SETTING_REORDER_CODE: false,
3438
SETTING_SAFE_MODE: false,
35-
SETTING_FORMATTER_PATH: "gdscript-formatter",
39+
SETTING_FORMATTER_PATH: "",
40+
SETTING_LINT_ON_SAVE: false,
41+
SETTING_LINT_LINE_LENGTH: 100,
42+
SETTING_LINT_IGNORED_RULES: "",
3643
}
3744

45+
## Which gutter lint icons are shown in.
46+
## By default, gutter 0 is for breakpoints and 1 is for things like overrides.
47+
const LINT_ICON_GUTTER := 2
48+
3849
var connection_list: Array[Resource] = []
3950
var installer: FormatterInstaller = null
4051
var formatter_cache_dir: String
@@ -81,6 +92,7 @@ func _enter_tree() -> void:
8192
)
8293

8394
add_format_command()
95+
add_lint_command()
8496
add_install_update_command()
8597
add_uninstall_command()
8698
add_report_issue_command()
@@ -98,6 +110,7 @@ func _exit_tree() -> void:
98110
resource_saved.disconnect(_on_resource_saved)
99111

100112
remove_format_command()
113+
remove_lint_command()
101114
remove_install_update_command()
102115
remove_uninstall_command()
103116
remove_report_issue_command()
@@ -139,6 +152,28 @@ func format_current_script() -> bool:
139152
return true
140153

141154

155+
func lint_current_script() -> bool:
156+
if not EditorInterface.get_script_editor().is_visible_in_tree():
157+
return false
158+
159+
var current_script := EditorInterface.get_script_editor().get_current_script()
160+
if not is_instance_valid(current_script) or not current_script is GDScript:
161+
return false
162+
163+
var code_edit: CodeEdit = EditorInterface.get_script_editor().get_current_editor().get_base_editor()
164+
165+
var lint_issues := lint_code(current_script)
166+
if lint_issues.is_empty():
167+
print("No linting issues found.")
168+
clear_lint_highlights(code_edit)
169+
return true
170+
171+
apply_lint_highlights(code_edit, lint_issues)
172+
print_lint_summary(lint_issues, current_script.resource_path)
173+
174+
return true
175+
176+
142177
func update_shortcut() -> void:
143178
for obj: Resource in connection_list:
144179
obj.changed.disconnect(update_shortcut)
@@ -159,38 +194,52 @@ func update_shortcut() -> void:
159194
func _on_resource_saved(saved_resource: Resource) -> void:
160195
if saved_resource is not GDScript:
161196
return
162-
if not get_editor_setting(SETTING_FORMAT_ON_SAVE):
163-
return
164197

165-
var script := saved_resource as GDScript
198+
var format_on_save := get_editor_setting(SETTING_FORMAT_ON_SAVE) as bool
199+
var lint_on_save := get_editor_setting(SETTING_LINT_ON_SAVE) as bool
166200

167-
if not has_command(get_editor_setting(SETTING_FORMATTER_PATH)) or not is_instance_valid(script):
201+
if not format_on_save and not lint_on_save:
168202
return
169203

170-
var formatted_code := format_code(script, false)
171-
if formatted_code.is_empty():
172-
return
173-
174-
script.source_code = formatted_code
175-
ResourceSaver.save(script)
176-
script.reload()
177-
178-
var script_editor := EditorInterface.get_script_editor()
179-
var open_script_editors := script_editor.get_open_script_editors()
180-
var open_scripts := script_editor.get_open_scripts()
204+
var script := saved_resource as GDScript
181205

182-
if not open_scripts.has(script):
206+
if not has_command(get_editor_setting(SETTING_FORMATTER_PATH)) or not is_instance_valid(script):
183207
return
184208

185-
if script_editor.get_current_script() == script:
186-
reload_code_edit(script_editor.get_current_editor().get_base_editor(), formatted_code, true)
187-
elif open_scripts.size() == open_script_editors.size():
188-
for i: int in range(open_scripts.size()):
189-
if open_scripts[i] == script:
190-
reload_code_edit(open_script_editors[i].get_base_editor(), formatted_code, true)
191-
return
192-
else:
193-
push_error("GDScript Formatter error: Unknown situation, can't reload code editor in Editor. Please report this issue.")
209+
if format_on_save:
210+
var formatted_code := format_code(script, false)
211+
if formatted_code.is_empty():
212+
return
213+
214+
script.source_code = formatted_code
215+
ResourceSaver.save(script)
216+
script.reload()
217+
218+
var script_editor := EditorInterface.get_script_editor()
219+
var open_script_editors := script_editor.get_open_script_editors()
220+
var open_scripts := script_editor.get_open_scripts()
221+
222+
if not open_scripts.has(script):
223+
return
224+
225+
if script_editor.get_current_script() == script:
226+
reload_code_edit(script_editor.get_current_editor().get_base_editor(), formatted_code, true)
227+
elif open_scripts.size() == open_script_editors.size():
228+
for i: int in range(open_scripts.size()):
229+
if open_scripts[i] == script:
230+
reload_code_edit(open_script_editors[i].get_base_editor(), formatted_code, true)
231+
return
232+
else:
233+
push_error("GDScript Formatter error: Unknown situation, can't reload code editor in Editor. Please report this issue.")
234+
235+
if lint_on_save:
236+
var code_edit: CodeEdit = EditorInterface.get_script_editor().get_current_editor().get_base_editor()
237+
var lint_issues := lint_code(script)
238+
if lint_issues.is_empty():
239+
clear_lint_highlights(code_edit)
240+
else:
241+
apply_lint_highlights(code_edit, lint_issues)
242+
print_lint_summary(lint_issues, script.resource_path)
194243

195244

196245
func add_format_command() -> void:
@@ -214,6 +263,23 @@ func remove_format_command() -> void:
214263
EditorInterface.get_command_palette().remove_command(COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_FORMAT_SCRIPT)
215264

216265

266+
func add_lint_command() -> void:
267+
if not has_command(get_editor_setting(SETTING_FORMATTER_PATH)):
268+
return
269+
270+
EditorInterface.get_command_palette().add_command(
271+
COMMAND_PALETTE_LINT_SCRIPT,
272+
COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_LINT_SCRIPT,
273+
lint_current_script,
274+
)
275+
276+
277+
func remove_lint_command() -> void:
278+
EditorInterface.get_command_palette().remove_command(
279+
COMMAND_PALETTE_CATEGORY + COMMAND_PALETTE_LINT_SCRIPT,
280+
)
281+
282+
217283
func add_install_update_command() -> void:
218284
EditorInterface.get_command_palette().add_command(
219285
COMMAND_PALETTE_INSTALL_UPDATE,
@@ -308,6 +374,8 @@ func _on_menu_item_selected(command: String) -> void:
308374
match command:
309375
"format_script":
310376
format_current_script()
377+
"lint_script":
378+
lint_current_script()
311379
"reorder_code":
312380
reorder_code()
313381
"install_update":
@@ -403,6 +471,103 @@ func format_code(script: GDScript, force_reorder := false) -> String:
403471
return ""
404472

405473

474+
## Lints a GDScript file using the GDScript Formatter's linter,
475+
## and returns an array of lint issues.
476+
func lint_code(script: GDScript) -> Array:
477+
var script_path := script.resource_path
478+
var output: Array = []
479+
var formatter_arguments: Array = ["lint", ProjectSettings.globalize_path(script_path)]
480+
481+
var exit_code := OS.execute(get_editor_setting(SETTING_FORMATTER_PATH), formatter_arguments, output)
482+
if exit_code == OK:
483+
return [] # No issues found
484+
485+
if exit_code == 1:
486+
# Parse lint output - the output is a single string with multiple lines
487+
var issues = []
488+
for output_item in output:
489+
var lines = output_item.split("\n")
490+
for line in lines:
491+
var trimmed_line = line.strip_edges()
492+
if trimmed_line.is_empty():
493+
continue
494+
var issue = parse_lint_issue(trimmed_line)
495+
if issue != null and not issue.is_empty():
496+
issues.push_back(issue)
497+
return issues
498+
499+
push_error("Lint GDScript failed: " + script_path)
500+
push_error("\tExit code: " + str(exit_code) + " Output: " + (output.front().strip_edges() if output.size() > 0 else "No output"))
501+
return []
502+
503+
504+
## Parses a lint issue line and returns a dictionary with issue information
505+
func parse_lint_issue(line: String) -> Dictionary:
506+
# Expected format: filename:line:rule:severity: message
507+
var parts = line.split(":", 4)
508+
if parts.size() < 5:
509+
return { }
510+
511+
return {
512+
"line": int(parts[1]) - 1, # Convert to 0-based indexing
513+
"rule": parts[2],
514+
"severity": parts[3],
515+
"message": parts[4].strip_edges(),
516+
}
517+
518+
519+
## Applies lint highlighting to the code editor
520+
func apply_lint_highlights(code_edit: CodeEdit, issues: Array) -> void:
521+
clear_lint_highlights(code_edit)
522+
523+
for issue in issues:
524+
var line_number: int = issue.line
525+
var severity: String = issue.severity
526+
527+
# Set line background color based on severity
528+
var color: Color
529+
if severity == "error":
530+
color = Color(1, 0, 0, 0.1)
531+
else: # warning
532+
color = Color(1, 1, 0, 0.1)
533+
534+
code_edit.set_line_background_color(line_number, color)
535+
536+
# Add gutter icon for severity
537+
var icon_name = "StatusError" if severity == "error" else "StatusWarning"
538+
var icon = EditorInterface.get_editor_theme().get_icon(icon_name, "EditorIcons")
539+
code_edit.set_gutter_type(LINT_ICON_GUTTER, CodeEdit.GutterType.GUTTER_TYPE_ICON)
540+
code_edit.set_line_gutter_icon(line_number, LINT_ICON_GUTTER, icon)
541+
542+
543+
## Prints a detailed summary of lint issues to the output
544+
func print_lint_summary(issues: Array, script_path: String) -> void:
545+
print_rich("\n[b]=== Linting Results for %s ===[/b]\n" % script_path)
546+
print_rich("[b]Found [i]%s[/i] issue(s)\n[/b]" % issues.size())
547+
548+
for issue in issues:
549+
var line_display = str(issue.line + 1) # Convert back to 1-based for display
550+
var severity_label = issue.severity.to_upper()
551+
print_rich(
552+
"[color=%s]%s[/color] on line [color=cyan]%s[/color] ([i]%s[/i])" % [
553+
"red" if severity_label == "ERROR" else "yellow",
554+
severity_label,
555+
line_display,
556+
issue.rule,
557+
],
558+
)
559+
print_rich("[i]%s[/i]\n" % [issue.message])
560+
561+
print_rich("[b]=== End Linting Results ===[/b]\n")
562+
563+
564+
## Clears all lint highlighting from the code editor
565+
func clear_lint_highlights(code_edit: CodeEdit) -> void:
566+
for line in range(code_edit.get_line_count()):
567+
code_edit.set_line_background_color(line, Color(0, 0, 0, 0))
568+
code_edit.set_line_gutter_icon(line, LINT_ICON_GUTTER, null)
569+
570+
406571
## Data structure to hold code editor state information
407572
class CodeEditState:
408573
var caret_line: int

0 commit comments

Comments
 (0)