GDScript: Improve call analysis

* Add missing `UNSAFE_CALL_ARGUMENT` warning.
* Fix `Object` constructor.
* Display an error for non-existent static methods.
This commit is contained in:
Danil Alexeev 2023-09-21 12:42:55 +03:00
parent 59139df16e
commit e8696f9961
No known key found for this signature in database
GPG Key ID: 124453E157DA8DC7
25 changed files with 184 additions and 46 deletions

View File

@ -248,7 +248,7 @@ Error GDScriptAnalyzer::check_native_member_name_conflict(const StringName &p_me
return ERR_PARSE_ERROR;
}
if (GDScriptParser::get_builtin_type(p_member_name) != Variant::VARIANT_MAX) {
if (GDScriptParser::get_builtin_type(p_member_name) < Variant::VARIANT_MAX) {
push_error(vformat(R"(The member "%s" cannot have the same name as a builtin type.)", p_member_name), p_member_node);
return ERR_PARSE_ERROR;
}
@ -669,11 +669,6 @@ GDScriptParser::DataType GDScriptAnalyzer::resolve_datatype(GDScriptParser::Type
return bad_type;
}
result.kind = GDScriptParser::DataType::VARIANT;
} else if (first == SNAME("Object")) {
// Object is treated like a native type, not a built-in.
result.kind = GDScriptParser::DataType::NATIVE;
result.builtin_type = Variant::OBJECT;
result.native_type = SNAME("Object");
} else if (GDScriptParser::get_builtin_type(first) < Variant::VARIANT_MAX) {
// Built-in types.
if (p_type->type_chain.size() > 1) {
@ -1704,7 +1699,7 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
}
parent_signature += ") -> ";
const String return_type = parent_return_type.is_hard_type() ? parent_return_type.to_string() : "Variant";
const String return_type = parent_return_type.to_string_strict();
if (return_type == "null") {
parent_signature += "void";
} else {
@ -2888,19 +2883,20 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
if (!p_call->is_super && callee_type == GDScriptParser::Node::IDENTIFIER) {
// Call to name directly.
StringName function_name = p_call->function_name;
Variant::Type builtin_type = GDScriptParser::get_builtin_type(function_name);
if (function_name == SNAME("Object")) {
push_error(R"*(Invalid constructor "Object()", use "Object.new()" instead.)*", p_call);
p_call->set_datatype(call_type);
return;
}
Variant::Type builtin_type = GDScriptParser::get_builtin_type(function_name);
if (builtin_type < Variant::VARIANT_MAX) {
// Is a builtin constructor.
call_type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT;
call_type.kind = GDScriptParser::DataType::BUILTIN;
call_type.builtin_type = builtin_type;
if (builtin_type == Variant::OBJECT) {
call_type.kind = GDScriptParser::DataType::NATIVE;
call_type.native_type = function_name; // "Object".
}
bool safe_to_fold = true;
switch (builtin_type) {
// Those are stored by reference so not suited for compile-time construction.
@ -2936,7 +2932,7 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
switch (err.error) {
case Callable::CallError::CALL_ERROR_INVALID_ARGUMENT:
push_error(vformat(R"(Invalid argument for %s constructor: argument %d should be "%s" but is "%s".)", Variant::get_type_name(builtin_type), err.argument + 1,
push_error(vformat(R"*(Invalid argument for "%s()" constructor: argument %d should be "%s" but is "%s".)*", Variant::get_type_name(builtin_type), err.argument + 1,
Variant::get_type_name(Variant::Type(err.expected)), p_call->arguments[err.argument]->get_datatype().to_string()),
p_call->arguments[err.argument]);
break;
@ -2952,10 +2948,10 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
push_error(vformat(R"(No constructor of "%s" matches the signature "%s".)", Variant::get_type_name(builtin_type), signature), p_call->callee);
} break;
case Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS:
push_error(vformat(R"(Too many arguments for %s constructor. Received %d but expected %d.)", Variant::get_type_name(builtin_type), p_call->arguments.size(), err.expected), p_call);
push_error(vformat(R"*(Too many arguments for "%s()" constructor. Received %d but expected %d.)*", Variant::get_type_name(builtin_type), p_call->arguments.size(), err.expected), p_call);
break;
case Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS:
push_error(vformat(R"(Too few arguments for %s constructor. Received %d but expected %d.)", Variant::get_type_name(builtin_type), p_call->arguments.size(), err.expected), p_call);
push_error(vformat(R"*(Too few arguments for "%s()" constructor. Received %d but expected %d.)*", Variant::get_type_name(builtin_type), p_call->arguments.size(), err.expected), p_call);
break;
case Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL:
case Callable::CallError::CALL_ERROR_METHOD_NOT_CONST:
@ -2966,21 +2962,27 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
break;
}
} else {
// TODO: Check constructors without constants.
// If there's one argument, try to use copy constructor (those aren't explicitly defined).
if (p_call->arguments.size() == 1) {
GDScriptParser::DataType arg_type = p_call->arguments[0]->get_datatype();
if (arg_type.is_variant()) {
mark_node_unsafe(p_call->arguments[0]);
} else {
if (arg_type.is_hard_type() && !arg_type.is_variant()) {
if (arg_type.kind == GDScriptParser::DataType::BUILTIN && arg_type.builtin_type == builtin_type) {
// Okay.
p_call->set_datatype(call_type);
return;
}
} else {
#ifdef DEBUG_ENABLED
mark_node_unsafe(p_call);
// We don't know what type was expected since constructors support overloads.
// TODO: Improve this by checking for matching candidates?
parser->push_warning(p_call->arguments[0], GDScriptWarning::UNSAFE_CALL_ARGUMENT, "1", function_name, "<unknown type>", "Variant");
#endif
p_call->set_datatype(call_type);
return;
}
}
List<MethodInfo> constructors;
Variant::get_constructor_list(builtin_type, &constructors);
bool match = false;
@ -2997,14 +2999,14 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
for (int i = 0; i < p_call->arguments.size(); i++) {
GDScriptParser::DataType par_type = type_from_property(info.arguments[i], true);
if (!is_type_compatible(par_type, p_call->arguments[i]->get_datatype(), true)) {
GDScriptParser::DataType arg_type = p_call->arguments[i]->get_datatype();
if (!is_type_compatible(par_type, arg_type, true)) {
types_match = false;
break;
#ifdef DEBUG_ENABLED
} else {
if (par_type.builtin_type == Variant::INT && p_call->arguments[i]->get_datatype().builtin_type == Variant::FLOAT && builtin_type != Variant::INT) {
parser->push_warning(p_call, GDScriptWarning::NARROWING_CONVERSION, p_call->function_name);
if (par_type.builtin_type == Variant::INT && arg_type.builtin_type == Variant::FLOAT && builtin_type != Variant::INT) {
parser->push_warning(p_call, GDScriptWarning::NARROWING_CONVERSION, function_name);
}
#endif
}
@ -3012,9 +3014,19 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
if (types_match) {
for (int i = 0; i < p_call->arguments.size(); i++) {
GDScriptParser::DataType par_type = type_from_property(info.arguments[i], true);
if (p_call->arguments[i]->is_constant) {
update_const_expression_builtin_type(p_call->arguments[i], type_from_property(info.arguments[i], true), "pass");
update_const_expression_builtin_type(p_call->arguments[i], par_type, "pass");
}
#ifdef DEBUG_ENABLED
if (!(par_type.is_variant() && par_type.is_hard_type())) {
GDScriptParser::DataType arg_type = p_call->arguments[i]->get_datatype();
if (arg_type.is_variant() || !arg_type.is_hard_type() || !is_type_compatible(arg_type, par_type, true)) {
mark_node_unsafe(p_call);
parser->push_warning(p_call->arguments[i], GDScriptWarning::UNSAFE_CALL_ARGUMENT, itos(i + 1), function_name, par_type.to_string(), arg_type.to_string_strict());
}
}
#endif
}
match = true;
call_type = type_from_property(info.return_val);
@ -3314,8 +3326,8 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
#else
push_error(vformat(R"*(Function "%s()" not found in base %s.)*", p_call->function_name, base_name), p_call->is_super ? p_call : p_call->callee);
#endif // SUGGEST_GODOT4_RENAMES
} else if (!found && (!p_call->is_super && base_type.is_hard_type() && base_type.kind == GDScriptParser::DataType::NATIVE && base_type.is_meta_type)) {
push_error(vformat(R"*(Static function "%s()" not found in base "%s".)*", p_call->function_name, base_type.native_type), p_call);
} else if (!found && (!p_call->is_super && base_type.is_hard_type() && base_type.is_meta_type)) {
push_error(vformat(R"*(Static function "%s()" not found in base "%s".)*", p_call->function_name, base_type.to_string()), p_call);
}
}
@ -3803,6 +3815,7 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident
#endif
// Not a local, so check members.
if (!found_source) {
reduce_identifier_from_base(p_identifier);
if (p_identifier->source != GDScriptParser::IdentifierNode::UNDEFINED_SOURCE || p_identifier->get_datatype().is_set()) {
@ -3855,10 +3868,10 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident
StringName name = p_identifier->name;
p_identifier->source = GDScriptParser::IdentifierNode::UNDEFINED_SOURCE;
// Check globals. We make an exception for Variant::OBJECT because it's the base class for
// non-builtin types so we allow doing e.g. Object.new()
// Not a local or a member, so check globals.
Variant::Type builtin_type = GDScriptParser::get_builtin_type(name);
if (builtin_type != Variant::OBJECT && builtin_type < Variant::VARIANT_MAX) {
if (builtin_type < Variant::VARIANT_MAX) {
if (can_be_builtin) {
p_identifier->set_datatype(make_builtin_meta_type(builtin_type));
return;
@ -4986,21 +4999,28 @@ void GDScriptAnalyzer::validate_call_arg(const List<GDScriptParser::DataType> &p
GDScriptParser::DataType arg_type = p_call->arguments[i]->get_datatype();
if (arg_type.is_variant() || !arg_type.is_hard_type()) {
#ifdef DEBUG_ENABLED
// Argument can be anything, so this is unsafe (unless the parameter is a hard variant).
if (!(par_type.is_hard_type() && par_type.is_variant())) {
mark_node_unsafe(p_call->arguments[i]);
parser->push_warning(p_call->arguments[i], GDScriptWarning::UNSAFE_CALL_ARGUMENT, itos(i + 1), p_call->function_name, par_type.to_string(), arg_type.to_string_strict());
}
#endif
} else if (par_type.is_hard_type() && !is_type_compatible(par_type, arg_type, true)) {
// Supertypes are acceptable for dynamic compliance, but it's unsafe.
mark_node_unsafe(p_call);
if (!is_type_compatible(arg_type, par_type)) {
push_error(vformat(R"*(Invalid argument for "%s()" function: argument %d should be "%s" but is "%s".)*",
p_call->function_name, i + 1, par_type.to_string(), arg_type.to_string()),
p_call->arguments[i]);
#ifdef DEBUG_ENABLED
} else {
// Supertypes are acceptable for dynamic compliance, but it's unsafe.
mark_node_unsafe(p_call);
parser->push_warning(p_call->arguments[i], GDScriptWarning::UNSAFE_CALL_ARGUMENT, itos(i + 1), p_call->function_name, par_type.to_string(), arg_type.to_string_strict());
#endif
}
#ifdef DEBUG_ENABLED
} else if (par_type.kind == GDScriptParser::DataType::BUILTIN && par_type.builtin_type == Variant::INT && arg_type.kind == GDScriptParser::DataType::BUILTIN && arg_type.builtin_type == Variant::FLOAT) {
parser->push_warning(p_call, GDScriptWarning::NARROWING_CONVERSION, p_call->function_name);
parser->push_warning(p_call->arguments[i], GDScriptWarning::NARROWING_CONVERSION, p_call->function_name);
#endif
}
}
@ -5028,7 +5048,7 @@ void GDScriptAnalyzer::is_shadowing(GDScriptParser::IdentifierNode *p_identifier
} else if (ClassDB::class_exists(name)) {
parser->push_warning(p_identifier, GDScriptWarning::SHADOWED_GLOBAL_IDENTIFIER, p_context, name, "global class");
return;
} else if (GDScriptParser::get_builtin_type(name) != Variant::VARIANT_MAX) {
} else if (GDScriptParser::get_builtin_type(name) < Variant::VARIANT_MAX) {
parser->push_warning(p_identifier, GDScriptWarning::SHADOWED_GLOBAL_IDENTIFIER, p_context, name, "built-in type");
return;
}

View File

@ -601,11 +601,8 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
arguments.push_back(arg);
}
if (!call->is_super && call->callee->type == GDScriptParser::Node::IDENTIFIER && GDScriptParser::get_builtin_type(call->function_name) != Variant::VARIANT_MAX) {
// Construct a built-in type.
Variant::Type vtype = GDScriptParser::get_builtin_type(static_cast<GDScriptParser::IdentifierNode *>(call->callee)->name);
gen->write_construct(result, vtype, arguments);
if (!call->is_super && call->callee->type == GDScriptParser::Node::IDENTIFIER && GDScriptParser::get_builtin_type(call->function_name) < Variant::VARIANT_MAX) {
gen->write_construct(result, GDScriptParser::get_builtin_type(call->function_name), arguments);
} else if (!call->is_super && call->callee->type == GDScriptParser::Node::IDENTIFIER && Variant::has_utility_function(call->function_name)) {
// Variant utility function.
gen->write_call_utility(result, call->function_name, arguments);

View File

@ -52,11 +52,18 @@
#include "editor/editor_settings.h"
#endif
// This function is used to determine that a type is "built-in" as opposed to native
// and custom classes. So `Variant::NIL` and `Variant::OBJECT` are excluded:
// `Variant::NIL` - `null` is literal, not a type.
// `Variant::OBJECT` - `Object` should be treated as a class, not as a built-in type.
static HashMap<StringName, Variant::Type> builtin_types;
Variant::Type GDScriptParser::get_builtin_type(const StringName &p_type) {
if (builtin_types.is_empty()) {
for (int i = 1; i < Variant::VARIANT_MAX; i++) {
builtin_types[Variant::get_type_name((Variant::Type)i)] = (Variant::Type)i;
if (unlikely(builtin_types.is_empty())) {
for (int i = 0; i < Variant::VARIANT_MAX; i++) {
Variant::Type type = (Variant::Type)i;
if (type != Variant::NIL && type != Variant::OBJECT) {
builtin_types[Variant::get_type_name(type)] = type;
}
}
}

View File

@ -149,6 +149,7 @@ public:
_FORCE_INLINE_ bool is_hard_type() const { return type_source > INFERRED; }
String to_string() const;
_FORCE_INLINE_ String to_string_strict() const { return is_hard_type() ? to_string() : "Variant"; }
PropertyInfo to_property_info(const String &p_name) const;
_FORCE_INLINE_ void set_container_element_type(const DataType &p_type) {
@ -1530,7 +1531,7 @@ public:
bool is_tool() const { return _is_tool; }
ClassNode *find_class(const String &p_qualified_name) const;
bool has_class(const GDScriptParser::ClassNode *p_class) const;
static Variant::Type get_builtin_type(const StringName &p_type);
static Variant::Type get_builtin_type(const StringName &p_type); // Excluding `Variant::NIL` and `Variant::OBJECT`.
CompletionContext get_completion_context() const { return completion_context; }
CompletionCall get_completion_call() const { return completion_call; }

View File

@ -105,7 +105,7 @@ String GDScriptWarning::get_message() const {
return vformat(R"(The value is cast to "%s" but has an unknown type.)", symbols[0]);
case UNSAFE_CALL_ARGUMENT:
CHECK_SYMBOLS(4);
return vformat(R"*(The argument %s of the function "%s()" requires a the subtype "%s" but the supertype "%s" was provided.)*", symbols[0], symbols[1], symbols[2], symbols[3]);
return vformat(R"*(The argument %s of the function "%s()" requires the subtype "%s" but the supertype "%s" was provided.)*", symbols[0], symbols[1], symbols[2], symbols[3]);
case UNSAFE_VOID_RETURN:
CHECK_SYMBOLS(2);
return vformat(R"*(The method "%s()" returns "void" but it's trying to return a call to "%s()" that can't be ensured to also be "void".)*", symbols[0], symbols[1]);

View File

@ -0,0 +1,7 @@
# GH-73283
class MyClass:
pass
func test():
MyClass.not_existing_method()

View File

@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
Static function "not_existing_method()" not found in base "MyClass".

View File

@ -0,0 +1,4 @@
# GH-73213
func test():
print(Object())

View File

@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
Invalid constructor "Object()", use "Object.new()" instead.

View File

@ -23,6 +23,7 @@ func test() -> void:
typed = variant()
inferred = variant()
@warning_ignore("unsafe_call_argument") # TODO: Hard vs Weak vs Unknown.
param_weak(typed)
param_typed(typed)
param_inferred(typed)

View File

@ -6,10 +6,12 @@ var prop = null
func check_arg(arg = null) -> void:
if arg != null:
@warning_ignore("unsafe_call_argument")
print(check(arg))
func check_recur() -> void:
if recur != null:
@warning_ignore("unsafe_call_argument")
print(check(recur))
else:
recur = 1
@ -22,11 +24,13 @@ func test() -> void:
if prop == null:
set('prop', 1)
@warning_ignore("unsafe_call_argument")
print(check(prop))
set('prop', null)
var loop = null
while loop != 2:
if loop != null:
@warning_ignore("unsafe_call_argument")
print(check(loop))
loop = 1 if loop == null else 2

View File

@ -14,4 +14,5 @@ func test():
func do_add_node():
var node = Node.new()
node.name = "Node"
@warning_ignore("unsafe_call_argument")
add_child(node)

View File

@ -0,0 +1,37 @@
func variant_func(x: Variant) -> void:
print(x)
func int_func(x: int) -> void:
print(x)
func float_func(x: float) -> void:
print(x)
# We don't want to execute it because of errors, just analyze.
func no_exec_test():
var untyped_int = 42
var untyped_string = "abc"
var variant_int: Variant = 42
var variant_string: Variant = "abc"
var typed_int: int = 42
variant_func(untyped_int) # No warning.
variant_func(untyped_string) # No warning.
variant_func(variant_int) # No warning.
variant_func(variant_string) # No warning.
variant_func(typed_int) # No warning.
int_func(untyped_int)
int_func(untyped_string)
int_func(variant_int)
int_func(variant_string)
int_func(typed_int) # No warning.
float_func(untyped_int)
float_func(untyped_string)
float_func(variant_int)
float_func(variant_string)
float_func(typed_int) # No warning.
func test():
pass

View File

@ -0,0 +1,33 @@
GDTEST_OK
>> WARNING
>> Line: 24
>> UNSAFE_CALL_ARGUMENT
>> The argument 1 of the function "int_func()" requires the subtype "int" but the supertype "Variant" was provided.
>> WARNING
>> Line: 25
>> UNSAFE_CALL_ARGUMENT
>> The argument 1 of the function "int_func()" requires the subtype "int" but the supertype "Variant" was provided.
>> WARNING
>> Line: 26
>> UNSAFE_CALL_ARGUMENT
>> The argument 1 of the function "int_func()" requires the subtype "int" but the supertype "Variant" was provided.
>> WARNING
>> Line: 27
>> UNSAFE_CALL_ARGUMENT
>> The argument 1 of the function "int_func()" requires the subtype "int" but the supertype "Variant" was provided.
>> WARNING
>> Line: 30
>> UNSAFE_CALL_ARGUMENT
>> The argument 1 of the function "float_func()" requires the subtype "float" but the supertype "Variant" was provided.
>> WARNING
>> Line: 31
>> UNSAFE_CALL_ARGUMENT
>> The argument 1 of the function "float_func()" requires the subtype "float" but the supertype "Variant" was provided.
>> WARNING
>> Line: 32
>> UNSAFE_CALL_ARGUMENT
>> The argument 1 of the function "float_func()" requires the subtype "float" but the supertype "Variant" was provided.
>> WARNING
>> Line: 33
>> UNSAFE_CALL_ARGUMENT
>> The argument 1 of the function "float_func()" requires the subtype "float" but the supertype "Variant" was provided.

View File

@ -3,27 +3,32 @@ extends Node
func test():
var child = Node.new()
child.name = "Child"
@warning_ignore("unsafe_call_argument")
add_child(child)
child.owner = self
var hey = Node.new()
hey.name = "Hey"
@warning_ignore("unsafe_call_argument")
child.add_child(hey)
hey.owner = self
hey.unique_name_in_owner = true
var fake_hey = Node.new()
fake_hey.name = "Hey"
@warning_ignore("unsafe_call_argument")
add_child(fake_hey)
fake_hey.owner = self
var sub_child = Node.new()
sub_child.name = "SubChild"
@warning_ignore("unsafe_call_argument")
hey.add_child(sub_child)
sub_child.owner = self
var howdy = Node.new()
howdy.name = "Howdy"
@warning_ignore("unsafe_call_argument")
sub_child.add_child(howdy)
howdy.owner = self
howdy.unique_name_in_owner = true

View File

@ -5,9 +5,11 @@ func test():
# Create the required node structure.
var hello = Node.new()
hello.name = "Hello"
@warning_ignore("unsafe_call_argument")
add_child(hello)
var world = Node.new()
world.name = "World"
@warning_ignore("unsafe_call_argument")
hello.add_child(world)
# All the ways of writing node paths below with the `$` operator are valid.

View File

@ -26,6 +26,7 @@ func test():
if true: (v as Callable).call()
print()
@warning_ignore("unsafe_call_argument")
other(v)
print()

View File

@ -2,4 +2,5 @@ func foo(x):
return x + 1
func test():
@warning_ignore("unsafe_call_argument")
print(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(0)))))))))))))))))))))))))

View File

@ -36,6 +36,7 @@ class SayNothing extends Say:
print("howdy, see above")
func say(name):
@warning_ignore("unsafe_call_argument")
super(name + " super'd")
print(prefix, " say nothing... or not? ", name)

View File

@ -29,6 +29,7 @@ func test():
const d = 1.1
_process(d)
@warning_ignore("unsafe_call_argument")
print(is_equal_approx(, PI + (d * PI)))
func _process(Δ: float) -> void:

View File

@ -0,0 +1,6 @@
# GH-73213
func test():
var object := Object.new() # Not `Object()`.
print(object.get_class())
object.free()

View File

@ -0,0 +1,2 @@
GDTEST_OK
Object

View File

@ -12,6 +12,7 @@ func test():
print("end")
func test_construct(v, f):
@warning_ignore("unsafe_call_argument")
Vector2(v, v) # Built-in type construct.
assert(not f) # Test unary operator reading from `nil`.

View File

@ -44,6 +44,7 @@ func test():
@warning_ignore("unsafe_method_access")
var path = get_script().get_path().get_base_dir()
@warning_ignore("unsafe_call_argument")
var other = load(path + "/static_variables_load.gd")
prints("load.perm:", other.perm)

View File

@ -24,7 +24,8 @@ func test():
print(StringName("hello"))
print(NodePath("hello/world"))
var node := Node.new()
print(RID(node))
@warning_ignore("unsafe_call_argument")
print(RID(node)) # TODO: Why is the constructor (or implicit cast) not documented?
print(node.get_name)
print(node.property_list_changed)
node.free()