Skip to content

Commit 10241a1

Browse files
Reimplement soft access restriction for members prefixed with _
1 parent 1559ab3 commit 10241a1

File tree

11 files changed

+121
-1
lines changed

11 files changed

+121
-1
lines changed

doc/classes/ProjectSettings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,12 +530,18 @@
530530
Specifies the maximum number of log files allowed (used for rotation). Set to [code]1[/code] to disable log file rotation.
531531
If the [code]--log-file <file>[/code] [url=$DOCS_URL/tutorials/editor/command_line_tutorial.html]command line argument[/url] is used, log rotation is always disabled.
532532
</member>
533+
<member name="debug/gdscript/warnings/access_private_member" type="int" setter="" getter="" default="1">
534+
When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a script's private member (property, method, or signal) is accessed from an external script that does not inherit the original script. A member is considered private when its name begins with an underscore ([code]_[/code]).
535+
</member>
533536
<member name="debug/gdscript/warnings/assert_always_false" type="int" setter="" getter="" default="1">
534537
When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when an [code]assert[/code] call always evaluates to [code]false[/code].
535538
</member>
536539
<member name="debug/gdscript/warnings/assert_always_true" type="int" setter="" getter="" default="1">
537540
When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when an [code]assert[/code] call always evaluates to [code]true[/code].
538541
</member>
542+
<member name="debug/gdscript/warnings/call_private_method" type="int" setter="" getter="" default="1">
543+
When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a script's private method is called from an external script that does not inherit the original script. A method is considered private when its name begins with an underscore ([code]_[/code]).
544+
</member>
539545
<member name="debug/gdscript/warnings/confusable_capture_reassignment" type="int" setter="" getter="" default="1">
540546
When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when a local variable captured by a lambda is reassigned, since this does not modify the outer local variable.
541547
</member>

modules/gdscript/gdscript_analyzer.cpp

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2051,6 +2051,33 @@ void GDScriptAnalyzer::decide_suite_type(GDScriptParser::Node *p_suite, GDScript
20512051
}
20522052
}
20532053

2054+
#ifdef DEBUG_ENABLED
2055+
void GDScriptAnalyzer::check_access_private_member(GDScriptParser::IdentifierNode *p_identifier, const GDScriptParser::DataType &p_datatype, const bool p_is_call) {
2056+
if (p_identifier == nullptr) {
2057+
return;
2058+
}
2059+
if (!String(p_identifier->name).begins_with("_")) {
2060+
return;
2061+
}
2062+
if (p_datatype.kind != GDScriptParser::DataType::Kind::CLASS && p_datatype.kind != GDScriptParser::DataType::Kind::SCRIPT) {
2063+
return; // Accessing from self
2064+
}
2065+
if (parser->current_function && parser->current_function->body && parser->current_function->body->has_local(p_identifier->name)) {
2066+
return; // Accessing local variable
2067+
}
2068+
2069+
GDScriptParser::DataType target_resolved = type_from_metatype(p_datatype);
2070+
if (target_resolved.class_type != nullptr && !target_resolved.class_type->has_member(p_identifier->name)) {
2071+
return; // Accessing an inexisting method
2072+
}
2073+
if (is_type_compatible(target_resolved, type_from_metatype(parser->current_class->get_datatype()))) {
2074+
return; // Not accessing from the external places
2075+
}
2076+
2077+
parser->push_warning(p_identifier, p_is_call ? GDScriptWarning::CALL_PRIVATE_METHOD : GDScriptWarning::ACCESS_PRIVATE_MEMBER, p_identifier->name);
2078+
}
2079+
#endif // DEBUG_ENABLED
2080+
20542081
void GDScriptAnalyzer::resolve_suite(GDScriptParser::SuiteNode *p_suite) {
20552082
for (int i = 0; i < p_suite->statements.size(); i++) {
20562083
GDScriptParser::Node *stmt = p_suite->statements[i];
@@ -3560,10 +3587,19 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
35603587
if (p_call->callee == nullptr && current_lambda != nullptr) {
35613588
push_error("Cannot use `super()` inside a lambda.", p_call);
35623589
}
3590+
3591+
#ifdef DEBUG_ENABLED
3592+
GDScriptParser::IdentifierNode *identifier = static_cast<GDScriptParser::IdentifierNode *>(p_call->callee);
3593+
check_access_private_member(identifier, p_call->get_datatype(), true);
3594+
#endif
35633595
} else if (callee_type == GDScriptParser::Node::IDENTIFIER) {
35643596
base_type = parser->current_class->get_datatype();
35653597
base_type.is_meta_type = false;
35663598
is_self = true;
3599+
#ifdef DEBUG_ENABLED
3600+
GDScriptParser::IdentifierNode *identifier = static_cast<GDScriptParser::IdentifierNode *>(p_call->callee);
3601+
check_access_private_member(identifier, p_call->get_datatype(), true);
3602+
#endif
35673603
} else if (callee_type == GDScriptParser::Node::SUBSCRIPT) {
35683604
GDScriptParser::SubscriptNode *subscript = static_cast<GDScriptParser::SubscriptNode *>(p_call->callee);
35693605
if (subscript->base == nullptr) {
@@ -3597,6 +3633,9 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
35973633
base_type = subscript->base->get_datatype();
35983634
is_self = subscript->base->type == GDScriptParser::Node::SELF;
35993635
}
3636+
#ifdef DEBUG_ENABLED
3637+
check_access_private_member(subscript->attribute, subscript->base->get_datatype(), true);
3638+
#endif
36003639
} else {
36013640
// Invalid call. Error already sent in parser.
36023641
// TODO: Could check if Callable here too.
@@ -4536,6 +4575,10 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident
45364575
}
45374576
}
45384577

4578+
#ifdef DEBUG_ENABLED
4579+
check_access_private_member(p_identifier, p_identifier->get_datatype());
4580+
#endif
4581+
45394582
return;
45404583
}
45414584

@@ -4898,6 +4941,10 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri
48984941
}
48994942
result_type.kind = GDScriptParser::DataType::VARIANT;
49004943
}
4944+
4945+
#ifdef DEBUG_ENABLED
4946+
check_access_private_member(p_subscript->attribute, p_subscript->base->get_datatype());
4947+
#endif
49014948
} else {
49024949
if (p_subscript->index == nullptr) {
49034950
return;

modules/gdscript/gdscript_analyzer.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ class GDScriptAnalyzer {
7171

7272
void decide_suite_type(GDScriptParser::Node *p_suite, GDScriptParser::Node *p_statement);
7373

74+
#ifdef DEBUG_ENABLED
75+
void check_access_private_member(GDScriptParser::IdentifierNode *p_identifier, const GDScriptParser::DataType &p_datatype, const bool p_is_call = false);
76+
#endif
77+
7478
void resolve_annotation(GDScriptParser::AnnotationNode *p_annotation);
7579
void resolve_class_member(GDScriptParser::ClassNode *p_class, const StringName &p_name, const GDScriptParser::Node *p_source = nullptr);
7680
void resolve_class_member(GDScriptParser::ClassNode *p_class, int p_index, const GDScriptParser::Node *p_source = nullptr);

modules/gdscript/gdscript_warning.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,12 @@ String GDScriptWarning::get_message() const {
164164
return vformat(R"*(The default value uses "%s" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this.)*", symbols[0]);
165165
case ONREADY_WITH_EXPORT:
166166
return R"("@onready" will set the default value after "@export" takes effect and will override it.)";
167+
case ACCESS_PRIVATE_MEMBER:
168+
CHECK_SYMBOLS(1);
169+
return vformat(R"(The member "%s" is private. It should not be accessed from an external script.)", symbols[0]);
170+
case CALL_PRIVATE_METHOD:
171+
CHECK_SYMBOLS(1);
172+
return vformat(R"*(The method "%s()" is private. It should not be called from an external script.)*", symbols[0]);
167173
#ifndef DISABLE_DEPRECATED
168174
// Never produced. These warnings migrated from 3.x by mistake.
169175
case PROPERTY_USED_AS_FUNCTION: // There is already an error.
@@ -241,6 +247,8 @@ String GDScriptWarning::get_name_from_code(Code p_code) {
241247
PNAME("NATIVE_METHOD_OVERRIDE"),
242248
PNAME("GET_NODE_DEFAULT_WITHOUT_ONREADY"),
243249
PNAME("ONREADY_WITH_EXPORT"),
250+
PNAME("ACCESS_PRIVATE_MEMBER"),
251+
PNAME("CALL_PRIVATE_METHOD"),
244252
#ifndef DISABLE_DEPRECATED
245253
"PROPERTY_USED_AS_FUNCTION",
246254
"CONSTANT_USED_AS_FUNCTION",

modules/gdscript/gdscript_warning.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ class GDScriptWarning {
9090
NATIVE_METHOD_OVERRIDE, // The script method overrides a native one, this may not work as intended.
9191
GET_NODE_DEFAULT_WITHOUT_ONREADY, // A class variable uses `get_node()` (or the `$` notation) as its default value, but does not use the @onready annotation.
9292
ONREADY_WITH_EXPORT, // The `@onready` annotation will set the value after `@export` which is likely not intended.
93+
ACCESS_PRIVATE_MEMBER, // Accessing a private member from external places. E.g. accessing an `_`-prefixed member from other classes that are not derived from the class where the member is defined.
94+
CALL_PRIVATE_METHOD, // Calling a private method from external places. E.g. calling an `_`-prefixed method from other classes that are not derived from the class where the method is defined.
9395
#ifndef DISABLE_DEPRECATED
9496
PROPERTY_USED_AS_FUNCTION, // Function not found, but there's a property with the same name.
9597
CONSTANT_USED_AS_FUNCTION, // Function not found, but there's a constant with the same name.
@@ -148,6 +150,8 @@ class GDScriptWarning {
148150
ERROR, // NATIVE_METHOD_OVERRIDE // May not work as expected.
149151
ERROR, // GET_NODE_DEFAULT_WITHOUT_ONREADY // May not work as expected.
150152
ERROR, // ONREADY_WITH_EXPORT // May not work as expected.
153+
WARN, // ACCESS_PRIVATE_MEMBER
154+
WARN, // CALL_PRIVATE_METHOD
151155
#ifndef DISABLE_DEPRECATED
152156
WARN, // PROPERTY_USED_AS_FUNCTION
153157
WARN, // CONSTANT_USED_AS_FUNCTION

modules/gdscript/tests/scripts/analyzer/features/virtual_method_implemented.gd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class SuperMethodsRecognized extends BaseClass:
1515
return result
1616

1717
func test():
18+
@warning_ignore_start("call_private_method")
1819
var test1 = SuperClassMethodsRecognized.new()
1920
print(test1._get_property_list()) # Calls base class's method.
2021
var test2 = SuperMethodsRecognized.new()

modules/gdscript/tests/scripts/analyzer/features/warning_ignore_warnings.gd

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,30 @@ func test_unsafe_void_return() -> void:
151151
func get_class():
152152
pass
153153

154+
class PrivA:
155+
var _t = 1
156+
157+
func _priv_method():
158+
_t = 5
159+
160+
class PrivB:
161+
var a = PrivA.new()
162+
163+
func _foo():
164+
@warning_ignore("access_private_member")
165+
a._t = 2
166+
@warning_ignore("call_private_method")
167+
a._priv_method()
168+
169+
class PrivC extends PrivA:
170+
var a = PrivA.new()
171+
172+
func _foo():
173+
@warning_ignore("access_private_member")
174+
a._t = 2
175+
@warning_ignore("call_private_method")
176+
a._priv_method()
177+
154178
# We don't want to execute it because of errors, just analyze.
155179
func test():
156180
pass
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
class A:
2+
var _t = 1
3+
4+
func _priv_method():
5+
_t = 5
6+
7+
class B:
8+
var a = A.new()
9+
10+
func _foo():
11+
a._t = 2
12+
a._priv_method()
13+
14+
class C extends A:
15+
var a = A.new()
16+
17+
func _foo():
18+
a._t = 2
19+
a._priv_method()
20+
21+
func test():
22+
pass
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
GDTEST_OK
2+
~~ WARNING at line 11: (ACCESS_PRIVATE_MEMBER) The member "_t" is private. It should not be accessed from an external script.
3+
~~ WARNING at line 12: (CALL_PRIVATE_METHOD) The method "_priv_method()" is private. It should not be called from an external script.

modules/gdscript/tests/scripts/runtime/features/onready_base_before_subclass.gd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ class B extends A:
1414

1515
func test():
1616
var node := B.new()
17+
@warning_ignore("call_private_method")
1718
node._ready()
1819
node.free()

0 commit comments

Comments
 (0)