@@ -19,9 +19,13 @@ const SETTING_INDENT_SIZE = "indent_size"
1919const SETTING_REORDER_CODE = "reorder_code"
2020const SETTING_SAFE_MODE = "safe_mode"
2121const 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
2326const COMMAND_PALETTE_CATEGORY = "gdquest gdscript formatter/"
2427const COMMAND_PALETTE_FORMAT_SCRIPT = "Format GDScript"
28+ const COMMAND_PALETTE_LINT_SCRIPT = "Lint GDScript"
2529const COMMAND_PALETTE_INSTALL_UPDATE = "Install or Update Formatter"
2630const COMMAND_PALETTE_UNINSTALL = "Uninstall Formatter"
2731const 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+
3849var connection_list : Array [Resource ] = []
3950var installer : FormatterInstaller = null
4051var 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+
142177func update_shortcut () -> void :
143178 for obj : Resource in connection_list :
144179 obj .changed .disconnect (update_shortcut )
@@ -159,38 +194,52 @@ func update_shortcut() -> void:
159194func _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
196245func 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+
217283func 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 ("\t Exit 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
407572class CodeEditState :
408573 var caret_line : int
0 commit comments