Skip to content

Introduce traits to GDScript - #1314

Open
Arctis-Fireblight wants to merge 6 commits into
Redot-Engine:masterfrom
Arctis-Fireblight:traits
Open

Introduce traits to GDScript#1314
Arctis-Fireblight wants to merge 6 commits into
Redot-Engine:masterfrom
Arctis-Fireblight:traits

Conversation

@Arctis-Fireblight

@Arctis-Fireblight Arctis-Fireblight commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

This PR builds on the work of SeremTitus and integrates his work on traits into Redot.

Prerequisites from Godot 4.6

This PR merges the following:

Examples of trait usage:

# Damageable.gd
trait_name Damageable
signal died

const MAX_HEALTH := 100
var health: int = MAX_HEALTH

func take_damage(amount: int) -> void:
	health -= amount
	print("Took damage:", amount, "Health now:", health)
	if health <= 0:
		died.emit()

func is_dead() -> bool:
	return health <= 0

func on_death()

# Enemy.gd
trait_name Enemy
func foobar():
	print("foobar called");

# Creature.gd

class_name Creature

extends Node
uses Enemy, Damageable

# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	foobar();
	take_damage(0);

func on_death():
	print("I died");

# test.gd
extends Node

func _ready() -> void:
	var temp = Creature.new();
	if (temp is Damageable && temp is Enemy):
		temp.take_damage(10);
		print("Damage was taken");
	else:
		print("The creature was invincible!");

In this example, we define two traits: Damageable and Enemy.
Traits can be defined globally using the trait_name keyword, or locally with the trait keyword.
Traits can then be used by classes or other traits using the uses keyword.
Traits can also declare methods that must be implemented by an implementing class, like the on_death() method in this example.
Lastly, traits can be tested for and casted by using the is or as keywords.

Summary by CodeRabbit

  • New Features

    • Added full GDScript traits support, including trait_name, uses composition (including nested traits), trait inheritance, overrides (including static), signals/enums, and bodyless trait functions.
    • Enabled runtime trait type checks (is) and casts (as).
    • Updated the editor experience for traits (completion, navigation, syntax highlighting, docs, and API inspection).
  • Bug Fixes

    • Improved trait parsing, analysis, compilation, and VM execution so trait contexts behave correctly.
  • Documentation

    • Added project settings for experimental trait warnings, including a warning for overridden static members missing the static keyword.
  • Tests

    • Added/extended trait coverage for analyzer and runtime scenarios.

Serem Titus and others added 5 commits July 16, 2026 19:35
(cherry picked from commit a50fc5acd81e0aa7d69e23faa462ecb6bbdad165)
(cherry picked from commit 02a3ada50224e1cdc95077ee0d67d633ff904d7f)
@Arctis-Fireblight
Arctis-Fireblight requested review from a team July 18, 2026 17:37
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 32156090-e6e0-4857-b664-bdb85f3973d5

📥 Commits

Reviewing files that changed from the base of the PR and between 337c30e and ad51e4b.

📒 Files selected for processing (11)
  • core/config/project_settings.cpp
  • doc/classes/ProjectSettings.xml
  • modules/gdscript/gdscript.cpp
  • modules/gdscript/gdscript_analyzer.cpp
  • modules/gdscript/gdscript_analyzer.h
  • modules/gdscript/gdscript_disassembler.cpp
  • modules/gdscript/gdscript_tokenizer_buffer.h
  • modules/gdscript/gdscript_warning.cpp
  • modules/gdscript/gdscript_warning.h
  • modules/gdscript/tests/gdscript_test_runner.cpp
  • modules/gdscript/tests/gdscript_test_runner.h
🚧 Files skipped from review as they are similar to previous changes (9)
  • core/config/project_settings.cpp
  • modules/gdscript/gdscript_tokenizer_buffer.h
  • modules/gdscript/gdscript_disassembler.cpp
  • modules/gdscript/gdscript_analyzer.h
  • modules/gdscript/gdscript.cpp
  • modules/gdscript/gdscript_warning.cpp
  • modules/gdscript/gdscript_warning.h
  • doc/classes/ProjectSettings.xml
  • modules/gdscript/gdscript_analyzer.cpp

Walkthrough

Adds GDScript traits with trait_name and uses syntax, AST and analyzer support, trait-aware type checking, bytecode and VM operations for is/as, editor integration, warnings, documentation handling, tests, and a LocalVector-based class-list API migration.

Changes

GDScript trait language

Layer / File(s) Summary
Trait syntax and AST
modules/gdscript/gdscript_parser.*, modules/gdscript/gdscript_tokenizer.*
Adds trait and uses tokens, parser nodes, trait metadata, bodyless trait functions, and trait-aware type/property representations.
Trait resolution and analysis
modules/gdscript/gdscript_analyzer.*, modules/gdscript/gdscript_cache.*
Resolves trait composition, inherited traits, copied members, conflicts, compatibility, dependency states, and analysis ordering.
Trait compilation and runtime
modules/gdscript/gdscript_compiler.cpp, modules/gdscript/gdscript_byte_codegen.cpp, modules/gdscript/gdscript_vm.cpp, modules/gdscript/gdscript_function.h
Adds trait data types, bytecode operations, script metadata propagation, and runtime trait membership tests and casts.
Editor, documentation, warnings, and tests
modules/gdscript/editor/*, modules/gdscript/language_server/*, modules/gdscript/gdscript_warning.*, doc/classes/ProjectSettings.xml, modules/gdscript/tests/*
Adds trait completion, symbol/API handling, documentation behavior, static-override warnings, test-runner warning suppression, and trait analyzer/runtime test scripts.

Class-list container migration

Layer / File(s) Summary
LocalVector class-list APIs and callers
core/object/*, core/*, editor/*, modules/mono/*, tests/core/object/test_class_db.h
Changes ClassDB and ScriptServer list outputs to LocalVector<StringName>, sorts appended ranges, and updates engine, editor, debugger, API-generation, Mono, and test callers.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested labels: topic:core

Suggested reviewers: olddev78

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding trait support to GDScript.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
modules/gdscript/gdscript_editor.cpp (2)

1274-1281: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not expose constructor behavior on trait metatypes.

Treating traits exactly like classes makes completion suggest new, infer a constructed trait instance, and show constructor hints, while the analyzer explicitly forbids trait construction. Preserve trait instance-member inference, but gate all metatype constructor handling on kind != TRAIT.

  • modules/gdscript/gdscript_editor.cpp#L1274-L1281: suppress the generic new completion for trait metatypes.
  • modules/gdscript/gdscript_editor.cpp#L2824-L2853: do not infer Trait.new() as a trait instance.
  • modules/gdscript/gdscript_editor.cpp#L2980-L3011: do not produce constructor argument hints for traits.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/gdscript/gdscript_editor.cpp` around lines 1274 - 1281, Gate
constructor-specific behavior on DataType::kind != TRAIT across
modules/gdscript/gdscript_editor.cpp at 1274-1281, 2824-2853, and 2980-3011:
suppress generic new completion, prevent Trait.new() from inferring a trait
instance, and omit constructor argument hints for trait metatypes. Preserve
trait instance-member inference and existing class behavior.

4017-4025: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve the nested trait lookup result.

The following post-processing condition at Line 4046 excludes only CLASS. For TRAIT, it overwrites the resolved trait’s class_name with the containing type and treats the trait as a regular member. Exclude both CLASS and TRAIT there.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/gdscript/gdscript_editor.cpp` around lines 4017 - 4025, Update the
post-processing condition near the lookup handling after the TRAIT/CLASS switch
in the GDScript editor so it excludes both
GDScriptParser::ClassNode::Member::CLASS and
GDScriptParser::ClassNode::Member::TRAIT. Preserve the resolved nested trait
result and prevent its class_name from being overwritten with the containing
type.
modules/gdscript/gdscript_analyzer.cpp (1)

6818-6829: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use trait composition when checking trait-target compatibility.

This branch treats a trait like an inherited class and only walks base_type. A class that directly uses the target trait is therefore rejected when assigned, passed, or returned through that trait type. Check each source class’s traits_fqtn for the target trait before traversing its parent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/gdscript/gdscript_analyzer.cpp` around lines 6818 - 6829, Update the
TRAIT/CLASS compatibility branch to inspect each source class’s traits_fqtn for
p_target.class_type before traversing its parent. Preserve the existing direct
class/FQCN match, continue checking each ancestor’s composed traits, and return
true when the target trait is found.
modules/gdscript/language_server/gdscript_extend_parser.cpp (1)

469-473: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve trait identity in language-server output.

Routing traits through the class path labels outline symbols as class and serializes traits under sub_classes without a discriminator. Clients cannot distinguish trait declarations from classes.

  • modules/gdscript/language_server/gdscript_extend_parser.cpp#L469-L473: emit trait-specific symbol detail/kind based on p_class->type.
  • modules/gdscript/language_server/gdscript_extend_parser.cpp#L950-L953: serialize a trait discriminator or separate trait collection.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/gdscript/language_server/gdscript_extend_parser.cpp` around lines 469
- 473, Preserve trait identity in the language-server symbol output: update the
TRAIT/CLASS handling around parse_class_symbol to derive trait-specific detail
or kind from p_class->type instead of treating traits as classes; also update
the serialization logic at
modules/gdscript/language_server/gdscript_extend_parser.cpp#L950-L953 to emit a
trait discriminator or dedicated trait collection so clients can distinguish
traits from classes.
🧹 Nitpick comments (5)
editor/doc/doc_tools.cpp (1)

430-430: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use value semantics for transparent conversion.

classes[classes_idx] yields a StringName. Assigning it to a const String & relies on C++ lifetime extension to keep the implicitly constructed temporary String alive. It is clearer to use value semantics, making the copy/conversion explicit to readers.

♻️ Proposed refactor
-			const String &name = classes[classes_idx];
+			String name = classes[classes_idx];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@editor/doc/doc_tools.cpp` at line 430, Update the local declaration in the
surrounding class-processing code to store classes[classes_idx] as a String
value instead of a const String reference, making the StringName-to-String
conversion explicit and avoiding reliance on temporary lifetime extension.
core/object/class_db.cpp (1)

261-268: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use original_size for robust sort-range calculations.

In both functions, the starting index for sorting is calculated using size() - container.size(), which relies on the strict assumption that exactly container.size() elements were appended. If a filtering condition (e.g., continue) is ever added to these loops (as seen in ClassDB::get_extensions_class_list), this math will break and cause an out-of-bounds pointer or sort an incorrect range.

Track the original_size before the loop for mathematically robust offsetting.

  • core/object/class_db.cpp#L261-L268: Track uint32_t original_size = p_classes.size(); before the loop, use it for reserve, and pass &p_classes[original_size] and p_classes.size() - original_size to sorter.sort().
  • core/object/script_language.cpp#L525-L530: Track uint32_t original_size = r_global_classes.size(); before the loop, use it for reserve, and pass &r_global_classes[original_size] and r_global_classes.size() - original_size to sorter.sort().
♻️ Proposed refactor for ClassDB::get_class_list
-	p_classes.reserve(p_classes.size() + classes.size());
+	uint32_t original_size = p_classes.size();
+	p_classes.reserve(original_size + classes.size());
 	for (const KeyValue<StringName, ClassInfo> &cls : classes) {
 		p_classes.push_back(cls.key);
 	}
 
 	SortArray<StringName, StringName::AlphCompare> sorter;
-	sorter.sort(&p_classes[p_classes.size() - classes.size()], classes.size());
+	sorter.sort(&p_classes[original_size], p_classes.size() - original_size);
♻️ Proposed refactor for ScriptServer::get_global_class_list
-	r_global_classes.reserve(r_global_classes.size() + global_classes.size());
+	uint32_t original_size = r_global_classes.size();
+	r_global_classes.reserve(original_size + global_classes.size());
 	for (const KeyValue<StringName, GlobalScriptClass> &global_class : global_classes) {
 		r_global_classes.push_back(global_class.key);
 	}
 	SortArray<StringName, StringName::AlphCompare> sorter;
-	sorter.sort(&r_global_classes[r_global_classes.size() - global_classes.size()], global_classes.size());
+	sorter.sort(&r_global_classes[original_size], r_global_classes.size() - original_size);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/object/class_db.cpp` around lines 261 - 268, In ClassDB::get_class_list
(core/object/class_db.cpp, lines 261-268), record original_size before
iterating, use it for reserve sizing, and sort from p_classes[original_size]
across the appended count p_classes.size() - original_size. Apply the same
change in ScriptServer::get_global_class_list (core/object/script_language.cpp,
lines 525-530) using r_global_classes and its original_size.
tests/core/object/test_class_db.h (1)

521-539: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer a range-based for loop.

Since LocalVector supports modern iterators, you can simplify this by using a range-based for loop instead of managing indices manually.

Additionally, if the previous test list was explicitly sorted (as the AI summary noted), please double-check whether a deterministic sort is still required to prevent arbitrary test failures. If so, you may need to add class_list.sort_custom<StringName::AlphCompare>(); before the loop.

♻️ Proposed refactor
 	LocalVector<StringName> class_list;
 	ClassDB::get_class_list(class_list);
 
-	for (uint32_t class_list_idx = 0; class_list_idx < class_list.size(); class_list_idx++) {
-		StringName class_name = class_list[class_list_idx];
+	for (const StringName &class_name : class_list) {
 
 		ClassDB::APIType api_type = ClassDB::get_api_type(class_name);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/core/object/test_class_db.h` around lines 521 - 539, Update the
class-list iteration in the test to use a range-based for loop over class_list
instead of class_list_idx indexing. Verify whether deterministic ordering is
required by the prior test behavior, and if so, sort class_list with the
existing StringName alphabetical comparator before the loop.
editor/project_upgrade/project_converter_3_to_4.cpp (1)

1196-1198: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a const reference for iteration.

The classes_list elements are only read within the loop, so using const StringName & is safer and more idiomatic.

♻️ Proposed refactor
 		LocalVector<StringName> classes_list;
 		ClassDB::get_class_list(classes_list);
-		for (StringName &name_of_class : classes_list) {
+		for (const StringName &name_of_class : classes_list) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@editor/project_upgrade/project_converter_3_to_4.cpp` around lines 1196 -
1198, Update the range-based loop over classes_list in the project conversion
code to use a const StringName reference, since name_of_class is only read. Keep
the existing iteration logic unchanged.
modules/gdscript/gdscript_vm.cpp (1)

1742-1749: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Eliminate redundant virtual calls to get_script_instance().

Both trait opcodes retrieve the object's script instance twice unnecessarily. Instead of calling get_script_instance() again inside the condition blocks, utilize the pointer you've already retrieved.

  • modules/gdscript/gdscript_vm.cpp#L1742-L1749: Replace src->operator Object *()->get_script_instance()->get_script().ptr() with scr_inst->get_script().ptr().
  • modules/gdscript/gdscript_vm.cpp#L977-L980: Cache object->get_script_instance() in a local variable before checking it, and use that local variable to retrieve the script.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/gdscript/gdscript_vm.cpp` around lines 1742 - 1749, Eliminate
redundant get_script_instance() calls in both trait opcode paths: at
modules/gdscript/gdscript_vm.cpp lines 1742-1749, use the existing scr_inst
local when retrieving the script; at lines 977-980, cache
object->get_script_instance() before the null check and use that cached pointer
to retrieve the script.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@modules/gdscript/gdscript_analyzer.cpp`:
- Around line 7017-7022: Update the trait method validation around
p_source_function and p_target_function to reject a static class implementation
when the required trait method is non-static. Emit the appropriate analyzer
error and prevent the implementation from being accepted; preserve the existing
warning and promotion behavior for a non-static implementation overriding a
static trait method.
- Around line 6960-6968: Fix parameter signature formatting by inserting the
comma before each parameter type after the first in the signature-building logic
near modules/gdscript/gdscript_analyzer.cpp lines 6960-6968 and in the
missing-implementation diagnostic near lines 7079-7084. Update both affected
sites so generated signatures use separators such as “Node,Node2D” rather than
appending commas after types.
- Around line 1779-1793: In both uses-chain resolution sites, update the
CONSTANT path to assign the resolved trait from member_datatype.class_type
instead of reading member.m_class, while retaining member.m_class for
class/trait members. At modules/gdscript/gdscript_analyzer.cpp lines 1779-1793
and 1839-1857, validate that the selected class_type is non-null and represents
a trait before continuing; reject invalid constant results through the existing
error path.
- Around line 1978-1997: The inheritance conflict check around class_base_type
currently inspects only the immediate script parent; traverse each ancestor in
the full script inheritance chain and compare the trait members against every
ancestor’s members. Preserve the existing native-class checks, and update the
class_type branch so inherited grandparent members also set inheritance_match to
false.
- Around line 1888-1899: Update the trait-use resolution logic in the TRAIT
branch to reject a target trait when trait->resolving_uses is already true,
treating it as a cyclic use alongside the existing traits_fqtn membership check.
Route this case through the cyclic-use error path and preserve the resolved_uses
reset so the counterpart trait also reports the cycle, rather than allowing the
later !trait->resolved_uses branch to continue silently.
- Around line 4284-4297: Update the cast analysis around the trait compatibility
check at modules/gdscript/gdscript_analyzer.cpp:4284-4297 and the corresponding
test analysis at modules/gdscript/gdscript_analyzer.cpp:5716-5734 to reject only
when the class and trait inheritance constraints cannot overlap; otherwise emit
the unsafe runtime operation, including for hard base-typed classes that may
contain a compatible subclass. Apply the same overlap rule consistently at both
sites.
- Around line 2252-2254: Update the rest-parameter unused-warning condition near
the existing parameter check in the function analysis path to also exclude
bodyless methods via p_function->is_bodyless. Keep the current warning behavior
unchanged for non-bodyless methods.

In `@modules/gdscript/gdscript_byte_codegen.cpp`:
- Around line 1066-1072: Update the GDTRAIT branch of write_cast to pass
p_type.trait_type to OPCODE_CAST_TO_TRAIT instead of p_target.type.trait_type,
while preserving the existing opcode and source/target operand handling.

In `@modules/gdscript/gdscript_editor.cpp`:
- Around line 1120-1124: Update _list_available_types() at
modules/gdscript/gdscript_editor.cpp:1120-1124 to filter global entries
according to p_include_trait and inheritance-only semantics, excluding traits
when traits are not requested and excluding incompatible classes for inheritance
completion. Update the COMPLETION_USES_TYPE handling at
modules/gdscript/gdscript_editor.cpp:3554-3574 to add only local and global
traits, not ordinary classes.

In `@modules/gdscript/gdscript_function.h`:
- Around line 82-83: Update GDScriptFunction::is_type so the GDTRAIT case
performs runtime checking instead of breaking to return false: verify the
variant is an object, retrieve its script, and determine whether the associated
GDScript implements the requested trait. Move is_type implementation to the
source file if needed to access complete GDScript/GDScriptFunction definitions,
and expose or reuse a static trait-checking helper such as _is_class_using_trait
or GDScript::has_trait.

In `@modules/gdscript/gdscript_parser.cpp`:
- Around line 989-993: Update the path-only trait handling in the parser branch
after match(GDScriptTokenizer::Token::PERIOD) so it records and completes uses
without returning immediately; consume the comma and continue parsing subsequent
trait entries such as `B`. Preserve the existing return behavior only when no
further trait entries remain.

In `@modules/gdscript/gdscript_tokenizer.h`:
- Around line 136-137: Update TOKENIZER_VERSION in gdscript_tokenizer_buffer.h
to a new value reflecting the added TRAIT_NAME and USES token IDs, ensuring
serialized .gdc buffers use the correct token mapping.

In `@modules/gdscript/gdscript_vm.cpp`:
- Around line 1720-1728: Remove the unused GET_VARIANT_PTR(to_type, 2) call from
the OPCODE_CAST_TO_TRAIT handler, while retaining src, dst, and trait_type_idx
retrieval and validation.

In `@modules/gdscript/gdscript.cpp`:
- Around line 2772-2773: Add "trait_name" to the reserved-word list alongside
the existing "trait" and "uses" entries so the API matches the tokenizer's
recognized keywords.

---

Outside diff comments:
In `@modules/gdscript/gdscript_analyzer.cpp`:
- Around line 6818-6829: Update the TRAIT/CLASS compatibility branch to inspect
each source class’s traits_fqtn for p_target.class_type before traversing its
parent. Preserve the existing direct class/FQCN match, continue checking each
ancestor’s composed traits, and return true when the target trait is found.

In `@modules/gdscript/gdscript_editor.cpp`:
- Around line 1274-1281: Gate constructor-specific behavior on DataType::kind !=
TRAIT across modules/gdscript/gdscript_editor.cpp at 1274-1281, 2824-2853, and
2980-3011: suppress generic new completion, prevent Trait.new() from inferring a
trait instance, and omit constructor argument hints for trait metatypes.
Preserve trait instance-member inference and existing class behavior.
- Around line 4017-4025: Update the post-processing condition near the lookup
handling after the TRAIT/CLASS switch in the GDScript editor so it excludes both
GDScriptParser::ClassNode::Member::CLASS and
GDScriptParser::ClassNode::Member::TRAIT. Preserve the resolved nested trait
result and prevent its class_name from being overwritten with the containing
type.

In `@modules/gdscript/language_server/gdscript_extend_parser.cpp`:
- Around line 469-473: Preserve trait identity in the language-server symbol
output: update the TRAIT/CLASS handling around parse_class_symbol to derive
trait-specific detail or kind from p_class->type instead of treating traits as
classes; also update the serialization logic at
modules/gdscript/language_server/gdscript_extend_parser.cpp#L950-L953 to emit a
trait discriminator or dedicated trait collection so clients can distinguish
traits from classes.

---

Nitpick comments:
In `@core/object/class_db.cpp`:
- Around line 261-268: In ClassDB::get_class_list (core/object/class_db.cpp,
lines 261-268), record original_size before iterating, use it for reserve
sizing, and sort from p_classes[original_size] across the appended count
p_classes.size() - original_size. Apply the same change in
ScriptServer::get_global_class_list (core/object/script_language.cpp, lines
525-530) using r_global_classes and its original_size.

In `@editor/doc/doc_tools.cpp`:
- Line 430: Update the local declaration in the surrounding class-processing
code to store classes[classes_idx] as a String value instead of a const String
reference, making the StringName-to-String conversion explicit and avoiding
reliance on temporary lifetime extension.

In `@editor/project_upgrade/project_converter_3_to_4.cpp`:
- Around line 1196-1198: Update the range-based loop over classes_list in the
project conversion code to use a const StringName reference, since name_of_class
is only read. Keep the existing iteration logic unchanged.

In `@modules/gdscript/gdscript_vm.cpp`:
- Around line 1742-1749: Eliminate redundant get_script_instance() calls in both
trait opcode paths: at modules/gdscript/gdscript_vm.cpp lines 1742-1749, use the
existing scr_inst local when retrieving the script; at lines 977-980, cache
object->get_script_instance() before the null check and use that cached pointer
to retrieve the script.

In `@tests/core/object/test_class_db.h`:
- Around line 521-539: Update the class-list iteration in the test to use a
range-based for loop over class_list instead of class_list_idx indexing. Verify
whether deterministic ordering is required by the prior test behavior, and if
so, sort class_list with the existing StringName alphabetical comparator before
the loop.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e8193825-879b-4abb-991a-1efd163fec87

📥 Commits

Reviewing files that changed from the base of the PR and between d890d7d and fe8e7f5.

⛔ Files ignored due to path filters (7)
  • modules/gdscript/tests/scripts/Traits/analyzer/errors/invalid_test_and_cast.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/Traits/analyzer/errors/invalid_trait_function.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/Traits/analyzer/errors/invalid_trait_inheritance.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/Traits/analyzer/errors/invalid_trait_uses.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/Traits/analyzer/features/trait_cohesion.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/Traits/analyzer/features/trait_test_and_cast.out is excluded by !**/*.out
  • modules/gdscript/tests/scripts/Traits/analyzer/warnings/trait_static_function.out is excluded by !**/*.out
📒 Files selected for processing (50)
  • core/core_bind.cpp
  • core/debugger/remote_debugger.cpp
  • core/extension/extension_api_dump.cpp
  • core/io/resource_loader.cpp
  • core/io/resource_saver.cpp
  • core/object/class_db.cpp
  • core/object/class_db.h
  • core/object/script_language.cpp
  • core/object/script_language.h
  • doc/classes/ProjectSettings.xml
  • editor/doc/doc_tools.cpp
  • editor/editor_data.cpp
  • editor/file_system/editor_file_system.cpp
  • editor/gui/create_dialog.cpp
  • editor/project_upgrade/project_converter_3_to_4.cpp
  • editor/script/script_editor_plugin.cpp
  • editor/settings/editor_build_profile.cpp
  • editor/shader/visual_shader_editor_plugin.cpp
  • modules/gdscript/editor/gdscript_docgen.cpp
  • modules/gdscript/editor/gdscript_highlighter.cpp
  • modules/gdscript/editor/gdscript_translation_parser_plugin.cpp
  • modules/gdscript/gdscript.cpp
  • modules/gdscript/gdscript.h
  • modules/gdscript/gdscript_analyzer.cpp
  • modules/gdscript/gdscript_analyzer.h
  • modules/gdscript/gdscript_byte_codegen.cpp
  • modules/gdscript/gdscript_cache.cpp
  • modules/gdscript/gdscript_cache.h
  • modules/gdscript/gdscript_compiler.cpp
  • modules/gdscript/gdscript_editor.cpp
  • modules/gdscript/gdscript_function.h
  • modules/gdscript/gdscript_parser.cpp
  • modules/gdscript/gdscript_parser.h
  • modules/gdscript/gdscript_tokenizer.cpp
  • modules/gdscript/gdscript_tokenizer.h
  • modules/gdscript/gdscript_vm.cpp
  • modules/gdscript/gdscript_warning.cpp
  • modules/gdscript/gdscript_warning.h
  • modules/gdscript/language_server/gdscript_extend_parser.cpp
  • modules/gdscript/tests/scripts/Traits/analyzer/errors/invalid_test_and_cast.gd
  • modules/gdscript/tests/scripts/Traits/analyzer/errors/invalid_trait_function.gd
  • modules/gdscript/tests/scripts/Traits/analyzer/errors/invalid_trait_inheritance.gd
  • modules/gdscript/tests/scripts/Traits/analyzer/errors/invalid_trait_uses.gd
  • modules/gdscript/tests/scripts/Traits/analyzer/features/global_trait.notest.gd
  • modules/gdscript/tests/scripts/Traits/analyzer/features/trait_cohesion.gd
  • modules/gdscript/tests/scripts/Traits/analyzer/features/trait_test_and_cast.gd
  • modules/gdscript/tests/scripts/Traits/analyzer/warnings/trait_static_function.gd
  • modules/mono/class_db_api_json.cpp
  • modules/mono/editor/bindings_generator.cpp
  • tests/core/object/test_class_db.h

Comment on lines +1779 to +1793
case GDScriptParser::ClassNode::Member::CONSTANT:
if (member_datatype.kind != GDScriptParser::DataType::SCRIPT && member_datatype.kind != GDScriptParser::DataType::TRAIT) {
push_error(vformat(R"(Constant "%s" is not a preloaded script or trait.)", name), id);
err = ERR_PARSE_ERROR;
}
break;
default:
push_error(vformat(R"(Cannot use %s "%s" in uses chain.)", member.get_type_name(), name), id);
err = ERR_PARSE_ERROR;
}
if (err || parser->errors.size() > error_count) {
break;
}
trait = member.m_class;
found = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Do not read member.m_class from a constant union member.

Both paths accept CONSTANT, then unconditionally assign trait = member.m_class. For constants this union field contains a ConstantNode *, so subsequent ClassNode access is undefined behavior. Use the resolved datatype’s class_type for constants and reject a null/non-trait result.

  • modules/gdscript/gdscript_analyzer.cpp#L1779-L1793: select member_datatype.class_type for preloaded trait constants.
  • modules/gdscript/gdscript_analyzer.cpp#L1839-L1857: apply the same typed selection for nested uses chains.
📍 Affects 1 file
  • modules/gdscript/gdscript_analyzer.cpp#L1779-L1793 (this comment)
  • modules/gdscript/gdscript_analyzer.cpp#L1839-L1857
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/gdscript/gdscript_analyzer.cpp` around lines 1779 - 1793, In both
uses-chain resolution sites, update the CONSTANT path to assign the resolved
trait from member_datatype.class_type instead of reading member.m_class, while
retaining member.m_class for class/trait members. At
modules/gdscript/gdscript_analyzer.cpp lines 1779-1793 and 1839-1857, validate
that the selected class_type is non-null and represents a trait before
continuing; reject invalid constant results through the existing error path.

Comment on lines +1888 to +1899
// Do not allows cyclic use of traits: TraitA uses TraitB and TraitB uses TraitA.
if (p_class->type == GDScriptParser::Node::TRAIT && trait->traits_fqtn.has(p_class->fqcn)) {
push_error(vformat(R"(Cyclic use of trait. "%s" trait can not use trait that uses it.)", trait_name), trait_name_node);
trait->resolved_uses = false; // Trigger same error on the other trait.
continue;
}

if (!trait->resolved_uses) {
if (!trait->resolving_uses) {
push_error(vformat(R"(Could not resolve trait's uses from "%s".)", trait_name), trait_name_node);
}
continue;

@coderabbitai coderabbitai Bot Jul 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Detect traits that are already resolving as cycles.

For TraitA uses TraitB and TraitB uses TraitA, TraitA.traits_fqtn is not populated until Line 2019. The reverse-membership check therefore misses the cycle, while the resolving_uses branch silently continues and marks TraitB resolved with incomplete composition.

Reject trait->resolving_uses as a cyclic use instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/gdscript/gdscript_analyzer.cpp` around lines 1888 - 1899, Update the
trait-use resolution logic in the TRAIT branch to reject a target trait when
trait->resolving_uses is already true, treating it as a cyclic use alongside the
existing traits_fqtn membership check. Route this case through the cyclic-use
error path and preserve the resolved_uses reset so the counterpart trait also
reports the cycle, rather than allowing the later !trait->resolved_uses branch
to continue silently.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give me a quick reproduction example that I can use for verification?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Arctis-Fireblight, add this as a trait analyzer error-case script, for example modules/gdscript/tests/scripts/Traits/analyzer/errors/cyclic_trait_uses.gd:

trait TraitA:
	uses TraitB

trait TraitB:
	uses TraitA

func test():
	pass

Expected behavior after the fix: parsing/analyzing this script should fail with a cyclic-trait-use diagnostic (at least for TraitB uses TraitA; the existing resolved_uses = false behavior may also cause the counterpart trait to report it).

Current problematic behavior: while resolving TraitB, TraitA is still marked resolving_uses, but its traits_fqtn has not yet been populated. The existing cycle check misses this, so resolution can complete without reporting the cycle.

For a slightly less direct chain, this should fail as well:

trait TraitA:
	uses TraitB

trait TraitB:
	uses TraitC

trait TraitC:
	uses TraitA

func test():
	pass

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is 100% a problem. I'll work on a fix.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Arctis-Fireblight, thanks for confirming. The direct and three-trait-cycle cases should provide good regression coverage when you have the fix ready. 🐇

Comment on lines +1978 to +1997
} else if (p_class->extends_used) {
GDScriptParser::DataType class_base_type = p_class->base_type;
if (class_base_type.kind == GDScriptParser::DataType::NATIVE && class_base_type.native_type != SNAME("RefCounted")) {
for (const GDScriptParser::ClassNode::Member &member : trait->members) {
if (ClassDB::has_enum(class_base_type.native_type, member.get_name()) ||
ClassDB::has_signal(class_base_type.native_type, member.get_name()) ||
ClassDB::has_method(class_base_type.native_type, member.get_name()) ||
ClassDB::has_integer_constant(class_base_type.native_type, member.get_name())) {
inheritance_match = false;
break;
}
}
} else if (class_base_type.class_type != nullptr) {
for (const GDScriptParser::ClassNode::Member &member : trait->members) {
if (class_base_type.class_type->has_member(member.get_name())) {
inheritance_match = false;
break;
}
}
}

@coderabbitai coderabbitai Bot Jul 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Traverse the full script inheritance chain for conflicts.

When the trait has no explicit extends, this only checks the immediate parent’s own members. A trait member can therefore shadow a method inherited from a grandparent, bypassing the intended conflict enforcement.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/gdscript/gdscript_analyzer.cpp` around lines 1978 - 1997, The
inheritance conflict check around class_base_type currently inspects only the
immediate script parent; traverse each ancestor in the full script inheritance
chain and compare the trait members against every ancestor’s members. Preserve
the existing native-class checks, and update the class_type branch so inherited
grandparent members also set inheritance_match to false.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please give me a reproduction example for this one as well?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Arctis-Fireblight, here is a minimal reproduction:

trait AddsGreeting:
	func greet() -> void:
		print("trait greeting")

class Grandparent:
	func greet() -> void:
		print("grandparent greeting")

class Parent extends Grandparent:
	pass

class Child extends Parent:
	uses AddsGreeting

func test() -> void:
	Child.new().greet()

AddsGreeting has no explicit extends, while Child extends a GDScript class. At the current check, class_base_type.class_type is Parent, and Parent does not declare greet(), so the conflict is missed even though Grandparent does.

As a result, the trait’s greet() is copied into Child, shadowing/overriding the inherited Grandparent.greet() method. The expected behavior, based on the existing native-parent conflict handling, is to reject uses AddsGreeting because the method already exists somewhere in Child’s inherited script hierarchy.

🐇🔍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have also confirmed this one.
image

Comment on lines +2252 to 2254
if (!p_function->is_bodyless && p_function->parameters[i]->usages == 0 && !String(p_function->parameters[i]->identifier->name).begins_with("_") && !p_function->is_abstract) {
parser->push_warning(p_function->parameters[i]->identifier, GDScriptWarning::UNUSED_PARAMETER, function_visible_name, p_function->parameters[i]->identifier->name);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Suppress the warning for bodyless rest parameters too.

The normal parameter path now excludes bodyless trait methods, but the rest-parameter warning at Line 2291 does not. A required bodyless method using ...args will still report an impossible-to-fix unused-parameter warning.

Proposed fix
-		if (p_function->rest_parameter->usages == 0 && !String(p_function->rest_parameter->identifier->name).begins_with("_") && !p_function->is_abstract) {
+		if (!p_function->is_bodyless && p_function->rest_parameter->usages == 0 && !String(p_function->rest_parameter->identifier->name).begins_with("_") && !p_function->is_abstract) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/gdscript/gdscript_analyzer.cpp` around lines 2252 - 2254, Update the
rest-parameter unused-warning condition near the existing parameter check in the
function analysis path to also exclude bodyless methods via
p_function->is_bodyless. Keep the current warning behavior unchanged for
non-bodyless methods.

Comment on lines +4284 to +4297
if ((p_cast->operand->get_datatype().kind == GDScriptParser::DataType::CLASS || p_cast->operand->get_datatype().kind == GDScriptParser::DataType::TRAIT) && cast_type.kind == GDScriptParser::DataType::TRAIT) {
if (p_cast->operand->is_constant) {
push_error(vformat(R"(Invalid cast. Cannot convert from "%s" to "%s".)", p_cast->operand->get_datatype().to_string(), cast_type.to_string()), p_cast->cast_type);
} else {
GDScriptParser::DataType operand_type = p_cast->operand->get_datatype();
bool is_using_trait = false;
if (operand_type.class_type && cast_type.class_type) {
is_using_trait = operand_type.class_type->traits_fqtn.has(cast_type.class_type->fqcn);
}
if (operand_type.is_hard_type() && !is_using_trait && operand_type.kind != GDScriptParser::DataType::VARIANT) {
push_error(vformat(R"(Invalid cast. Cannot convert from "%s" to "%s".)", p_cast->operand->get_datatype().to_string(), cast_type.to_string()), p_cast->cast_type);
} else {
mark_node_unsafe(p_cast);
}

@coderabbitai coderabbitai Bot Jul 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Allow runtime trait checks for compatible base-typed objects.

A hard class type that does not itself use the trait may still hold a subclass that does. These branches reject such casts/tests statically instead of emitting the unsafe runtime operation, breaking patterns such as checking a base-typed Node against a Node-compatible trait.

  • modules/gdscript/gdscript_analyzer.cpp#L4284-L4297: reject only when the class and trait inheritance constraints cannot overlap.
  • modules/gdscript/gdscript_analyzer.cpp#L5716-L5734: use the same overlap rule before converting the test into a compile-time error.
📍 Affects 1 file
  • modules/gdscript/gdscript_analyzer.cpp#L4284-L4297 (this comment)
  • modules/gdscript/gdscript_analyzer.cpp#L5716-L5734
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/gdscript/gdscript_analyzer.cpp` around lines 4284 - 4297, Update the
cast analysis around the trait compatibility check at
modules/gdscript/gdscript_analyzer.cpp:4284-4297 and the corresponding test
analysis at modules/gdscript/gdscript_analyzer.cpp:5716-5734 to reject only when
the class and trait inheritance constraints cannot overlap; otherwise emit the
unsafe runtime operation, including for hard base-typed classes that may contain
a compatible subclass. Apply the same overlap rule consistently at both sites.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please give me a reproduction example?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Arctis-Fireblight, here is a minimal reproduction. The same issue applies to both as and is:

trait Damageable extends Node:
	func take_damage() -> void:
		pass

class Enemy extends Node:
	uses Damageable

func test() -> void:
	# The static type is Node, but the runtime value is an Enemy,
	# which implements Damageable.
	var value: Node = Enemy.new()

	var damageable := value as Damageable
	print(value is Damageable)

	damageable.take_damage()

value is only known to be a Node statically, so it does not have Damageable in its traits_fqtn. However, an Enemy is a valid runtime value for value, and it does use Damageable.

Currently, the analyzer rejects the cast/type test as if Node could never implement the trait:

Invalid cast. Cannot convert from "Node" to "Damageable".

The expected behavior is to allow the operation as a runtime-checked (unsafe) cast/test:

  • value as Damageable succeeds when value is an Enemy, otherwise evaluates to null.
  • value is Damageable evaluates to true for the Enemy instance.

The same applies with an intermediate script base class, e.g. var value: BaseEnemy = Enemy.new(), where only Enemy uses the trait.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly different error message, but I have confirmed this one as well.
image

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Arctis-Fireblight, the Comment image shows the expected reproducer: value is statically typed as Node, holds Enemy1, and Line 14 (value is Damageable1) is rejected with:

Expression is of type "Node" so it can't be of type "Damageable1".

Thanks for confirming. This validates the issue: the analyzer is treating the static base type as conclusive, despite a compatible subclass being able to implement the trait at runtime.

Comment on lines +82 to +83
case GDTRAIT:
break;

@coderabbitai coderabbitai Bot Jul 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift

Implement runtime type checking for traits.

is_type currently breaks and falls through to return false; for traits. This causes all runtime type checks (such as validating arguments passed to trait-typed function parameters or typed property assignments) to fail unconditionally.

You need to implement this case by checking if the variant is an object, retrieving its script, and checking if it implements the trait.

Note: Since checking traits_fqtn requires GDScript and GDScriptFunction implementations that are likely incomplete at this point in the header, you will likely need to move the implementation of is_type to a .cpp file and expose _is_class_using_trait as a static helper (or add a has_trait method to GDScript).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/gdscript/gdscript_function.h` around lines 82 - 83, Update
GDScriptFunction::is_type so the GDTRAIT case performs runtime checking instead
of breaking to return false: verify the variant is an object, retrieve its
script, and determine whether the associated GDScript implements the requested
trait. Move is_type implementation to the source file if needed to access
complete GDScript/GDScriptFunction definitions, and expose or reuse a static
trait-checking helper such as _is_class_using_trait or GDScript::has_trait.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please give me a quick reproduction script for this one as well?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 3 --glob '*.gd' \
  ':\s*[A-Za-z_][A-Za-z0-9_]*|as\s+[A-Za-z_][A-Za-z0-9_]*' \
  modules/gdscript/tests/scripts/Traits

printf '\n--- trait declarations and uses ---\n'
rg -n -C 2 --glob '*.gd' \
  '^\s*(trait|trait_name|uses)\b' \
  modules/gdscript/tests/scripts/Traits

Length of output: 20396


@Arctis-Fireblight, here is a minimal reproduction. Attach it to a Node and run the scene:

extends Node

trait Tagged:
	func marker() -> void:
		pass

class TaggedValue:
	uses Tagged

func accept_tagged(_value: Tagged) -> void:
	print("accepted")

func _ready() -> void:
	# This instance implements `Tagged`, so this should be accepted.
	accept_tagged(TaggedValue.new())

Expected: it prints accepted.

Current behavior: the trait-typed argument validation rejects TaggedValue.new() even though TaggedValue declares uses Tagged. The parameter check reaches GDScriptDataType::is_type(), where the GDTRAIT case currently falls through to the final return false.

A typed property assignment should trigger the same path:

var value: Tagged

func _ready() -> void:
	value = TaggedValue.new() # Incorrectly rejected currently.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tested, and it seems worse than you suggested as I dont even have to attach or run it for this to generate an error.
image

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Arctis-Fireblight, the Comment image shows the editor flagging Line 15, accept_tagged(TaggedValue.new()), with:

Invalid argument for "accept_tagged()" function: argument 1 should be "Tagged" but is "TaggedValue".

You’re right — this is worse than the runtime-only issue I described. The analyzer rejects the call statically, so the script cannot be run in the first place.

The GDTRAIT fallthrough in GDScriptDataType::is_type() remains a real runtime validation bug (e.g., dynamic calls, typed assignments checked at runtime), but this reproduction also exposes a separate/static compatibility gap: the analyzer must recognize that a class declaring uses Tagged satisfies a Tagged parameter type.

So the fix needs both:

  1. Analyzer/type compatibility: treat a class using a trait as compatible with that trait, preventing this compile-time error.
  2. Runtime validation: implement the GDTRAIT branch in GDScriptDataType::is_type() for values whose type is only known at runtime.

Thanks for catching that distinction — I’ll revise the finding’s scope accordingly.

🐇💭

Comment on lines +989 to +993
if (!match(GDScriptTokenizer::Token::PERIOD)) {
current_class->traits.push_back(uses);
complete_extents(uses);
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Continue parsing after a path-only trait entry.

This return leaves a following comma unconsumed, so valid composition such as uses "a.gd", B fails.

Proposed fix
 			if (!match(GDScriptTokenizer::Token::PERIOD)) {
 				current_class->traits.push_back(uses);
 				complete_extents(uses);
-				return;
+				if (!match(GDScriptTokenizer::Token::COMMA)) {
+					return;
+				}
+				continue;
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!match(GDScriptTokenizer::Token::PERIOD)) {
current_class->traits.push_back(uses);
complete_extents(uses);
return;
}
if (!match(GDScriptTokenizer::Token::PERIOD)) {
current_class->traits.push_back(uses);
complete_extents(uses);
if (!match(GDScriptTokenizer::Token::COMMA)) {
return;
}
continue;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/gdscript/gdscript_parser.cpp` around lines 989 - 993, Update the
path-only trait handling in the parser branch after
match(GDScriptTokenizer::Token::PERIOD) so it records and completes uses without
returning immediately; consume the comma and continue parsing subsequent trait
entries such as `B`. Preserve the existing return behavior only when no further
trait entries remain.

Comment thread modules/gdscript/gdscript_tokenizer.h
Comment on lines +1720 to +1728
OPCODE(OPCODE_CAST_TO_TRAIT) {
CHECK_SPACE(4);
GET_VARIANT_PTR(src, 0);
GET_VARIANT_PTR(dst, 1);
GET_VARIANT_PTR(to_type, 2);

int trait_type_idx = _code_ptr[ip + 3];
GD_ERR_BREAK(trait_type_idx < 0 || trait_type_idx >= _global_names_count);
const StringName trait_type = _global_names_ptr[trait_type_idx];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Remove invalid variant pointer retrieval.

GET_VARIANT_PTR(to_type, 2) attempts to read _code_ptr[ip + 3] as a variant stack address. However, for OPCODE_CAST_TO_TRAIT, the instruction length is only 4 words, and _code_ptr[ip + 3] stores trait_type_idx (an index into global names).

Treating this name index as a stack address triggers the GD_ERR_BREAK stack-bounds check within the macro, which will unconditionally abort the opcode execution if the index exceeds the current stack size. Since to_type is entirely unused here, simply remove this line.

🐛 Proposed fix
 			OPCODE(OPCODE_CAST_TO_TRAIT) {
 				CHECK_SPACE(4);
 				GET_VARIANT_PTR(src, 0);
 				GET_VARIANT_PTR(dst, 1);
-				GET_VARIANT_PTR(to_type, 2);
 
 				int trait_type_idx = _code_ptr[ip + 3];
 				GD_ERR_BREAK(trait_type_idx < 0 || trait_type_idx >= _global_names_count);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
OPCODE(OPCODE_CAST_TO_TRAIT) {
CHECK_SPACE(4);
GET_VARIANT_PTR(src, 0);
GET_VARIANT_PTR(dst, 1);
GET_VARIANT_PTR(to_type, 2);
int trait_type_idx = _code_ptr[ip + 3];
GD_ERR_BREAK(trait_type_idx < 0 || trait_type_idx >= _global_names_count);
const StringName trait_type = _global_names_ptr[trait_type_idx];
OPCODE(OPCODE_CAST_TO_TRAIT) {
CHECK_SPACE(4);
GET_VARIANT_PTR(src, 0);
GET_VARIANT_PTR(dst, 1);
int trait_type_idx = _code_ptr[ip + 3];
GD_ERR_BREAK(trait_type_idx < 0 || trait_type_idx >= _global_names_count);
const StringName trait_type = _global_names_ptr[trait_type_idx];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/gdscript/gdscript_vm.cpp` around lines 1720 - 1728, Remove the unused
GET_VARIANT_PTR(to_type, 2) call from the OPCODE_CAST_TO_TRAIT handler, while
retaining src, dst, and trait_type_idx retrieval and validation.

Comment thread modules/gdscript/gdscript.cpp
@Arctis-Fireblight
Arctis-Fireblight force-pushed the traits branch 2 times, most recently from f9e3476 to 337c30e Compare July 19, 2026 02:10

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@doc/classes/ProjectSettings.xml`:
- Around line 559-560: Update the descriptions for the experimental-trait
settings in ProjectSettings.xml: correct experimental_trait to explain that its
integer warning level selects warning versus error behavior, and revise
show_experimental_trait_warning to describe its separate boolean role in
suppressing emission entirely. Distinguish their relationship clearly and fix
the “When set” wording typo.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 932c03ba-52c4-4706-a9ae-8426c1674cc7

📥 Commits

Reviewing files that changed from the base of the PR and between f9e3476 and 337c30e.

📒 Files selected for processing (9)
  • core/config/project_settings.cpp
  • doc/classes/ProjectSettings.xml
  • modules/gdscript/gdscript.cpp
  • modules/gdscript/gdscript_analyzer.cpp
  • modules/gdscript/gdscript_analyzer.h
  • modules/gdscript/gdscript_disassembler.cpp
  • modules/gdscript/gdscript_tokenizer_buffer.h
  • modules/gdscript/gdscript_warning.cpp
  • modules/gdscript/gdscript_warning.h
🚧 Files skipped from review as they are similar to previous changes (8)
  • modules/gdscript/gdscript_tokenizer_buffer.h
  • modules/gdscript/gdscript_warning.cpp
  • modules/gdscript/gdscript_analyzer.h
  • modules/gdscript/gdscript_warning.h
  • core/config/project_settings.cpp
  • modules/gdscript/gdscript_disassembler.cpp
  • modules/gdscript/gdscript.cpp
  • modules/gdscript/gdscript_analyzer.cpp

Comment thread doc/classes/ProjectSettings.xml Outdated
- Added support for Traits OP Codes to the GDScript disassembler
- Fixed some implicit casts in gdscript_analyzer.cpp
- Fixed missing keyword highlighting for `trait_name`
- Added suppressable warning to project settings to notify users that traits are still experimental
- incremented `TOKENIZER_VERSION` to 102
- Fixed failing unit tests due to addition of the `experimental_trait_warning` by suppressing this warning during tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants