From 246c34ddc2ca858e5427512919132b9a687d0fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Fri, 26 Jun 2026 19:49:09 +0200 Subject: [PATCH 01/10] Display an error on undeclared child of scene variables --- .../GDCore/IDE/Events/ExpressionValidator.cpp | 12 ++++-- Core/GDCore/IDE/Events/ExpressionValidator.h | 43 ++++++++++++++++--- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/Core/GDCore/IDE/Events/ExpressionValidator.cpp b/Core/GDCore/IDE/Events/ExpressionValidator.cpp index cacb35887fa4..672431985db1 100644 --- a/Core/GDCore/IDE/Events/ExpressionValidator.cpp +++ b/Core/GDCore/IDE/Events/ExpressionValidator.cpp @@ -67,15 +67,19 @@ size_t GetMaximumParametersNumber( } // namespace bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty( - const gd::IdentifierNode& identifier) { - return ValidateObjectVariableOrVariableOrProperty(identifier.identifierName, identifier.identifierNameLocation, identifier.childIdentifierName, identifier.childIdentifierNameLocation); + const gd::IdentifierNode &identifier) { + return ValidateObjectVariableOrVariableOrProperty( + identifier.identifierName, identifier.identifierNameLocation, + identifier.childIdentifierName, identifier.childIdentifierNameLocation, + true); } bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty( const gd::String &identifierName, const gd::ExpressionParserLocation identifierNameLocation, const gd::String &childIdentifierName, - const gd::ExpressionParserLocation childIdentifierNameLocation) { + const gd::ExpressionParserLocation childIdentifierNameLocation, + const bool isUndeclaredVariableFatal) { auto validateVariableTypeForExpression = [this, &identifierNameLocation](gd::Variable::Type type) { // Collections type can't be used directly in expressions, a child @@ -160,7 +164,7 @@ bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty( // A child variable is accessed, check it can be used in an expression. if (!variable.HasChild(childIdentifierName)) { RaiseTypeError(_("No child variable with this name found."), - childIdentifierNameLocation); + childIdentifierNameLocation, isUndeclaredVariableFatal); return true; // We should have found a variable. } diff --git a/Core/GDCore/IDE/Events/ExpressionValidator.h b/Core/GDCore/IDE/Events/ExpressionValidator.h index c46f4724ef90..27ada25ad259 100644 --- a/Core/GDCore/IDE/Events/ExpressionValidator.h +++ b/Core/GDCore/IDE/Events/ExpressionValidator.h @@ -210,15 +210,22 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { } void OnVisitVariableNode(VariableNode& node) override { ReportAnyError(node); + rootVariableName = ""; if (parentType == Type::Variable || parentType == Type::VariableOrProperty || parentType == Type::VariableOrPropertyOrParameter) { childType = parentType; - CheckVariableExistence(node.location, node.name, node.child != nullptr); + bool isRootVariableDeclared = CheckVariableExistence( + node.location, node.name, node.child != nullptr); if (node.child) { + if (isRootVariableDeclared) { + rootVariableName = node.name; + rootVariableLocation = node.nameLocation; + } node.child->Visit(*this); + rootVariableName = ""; } } else if (parentType == Type::LegacyVariable) { childType = parentType; @@ -246,6 +253,8 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { forbidsUsageOfBracketsBecauseParentIsObject = true; }, [&]() { // This is a variable. + rootVariableName = node.name; + rootVariableLocation = node.nameLocation; }, [&]() { // This is a property. // Being in this node implies that there is at least a child - which is not supported for properties. @@ -281,11 +290,16 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { ReportAnyError(node); // TODO Also check child-variables existence on a path with only VariableAccessor to raise non-fatal errors. if (!variableObjectName.empty()) { - ValidateObjectVariableOrVariableOrProperty(variableObjectName, - variableObjectNameLocation, - node.name, node.nameLocation); + ValidateObjectVariableOrVariableOrProperty( + variableObjectName, variableObjectNameLocation, node.name, + node.nameLocation, !node.child); variableObjectName = ""; + } else if (!rootVariableName.empty()) { + ValidateObjectVariableOrVariableOrProperty( + rootVariableName, rootVariableLocation, node.name, node.nameLocation, + !node.child); } + rootVariableName = ""; // In the case we accessed an object variable (`MyObject.MyVariable`), // brackets can now be used (`MyObject.MyVariable["MyChildVariable"]` is now valid). forbidsUsageOfBracketsBecauseParentIsObject = false; @@ -298,6 +312,7 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { VariableBracketAccessorNode& node) override { ReportAnyError(node); + rootVariableName = ""; variableObjectName = ""; if (forbidsUsageOfBracketsBecauseParentIsObject) { RaiseError(gd::ExpressionParserError::ErrorType::BracketsNotAllowedForObjects, @@ -350,7 +365,14 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { } else if (parentType == Type::Variable || parentType == Type::VariableOrProperty || parentType == Type::VariableOrPropertyOrParameter) { - CheckVariableExistence(node.location, node.identifierName, !node.childIdentifierName.empty()); + bool isRootVariableDeclared = + CheckVariableExistence(node.location, node.identifierName, + !node.childIdentifierName.empty()); + if (isRootVariableDeclared && !node.childIdentifierName.empty()) { + ValidateObjectVariableOrVariableOrProperty( + node.identifierName, node.identifierNameLocation, + node.childIdentifierName, node.childIdentifierNameLocation, false); + } } else if (parentType != Type::Object && parentType != Type::LegacyVariable) { // It can't happen. @@ -407,12 +429,14 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { const gd::String &identifierName, const gd::ExpressionParserLocation identifierNameLocation, const gd::String &childIdentifierName, - const gd::ExpressionParserLocation childIdentifierNameLocation); + const gd::ExpressionParserLocation childIdentifierNameLocation, + const bool hasChild); - void CheckVariableExistence(const ExpressionParserLocation &location, + bool CheckVariableExistence(const ExpressionParserLocation &location, const gd::String &name, bool hasChild) { if (!currentParameterExtraInfo || *currentParameterExtraInfo != "AllowUndeclaredVariable") { + bool isRootVariableDeclared = false; projectScopedContainers.MatchIdentifierWithName( name, [&]() { @@ -424,6 +448,7 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { }, [&]() { // This is a variable. + isRootVariableDeclared = true; }, [&]() { // This is a property. @@ -456,7 +481,9 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { _("No variable with this name found."), location, name); }); + return isRootVariableDeclared; } + return false; } void ReportAnyError(const ExpressionNode& node, bool isFatal = true) { @@ -569,6 +596,8 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { bool forbidsUsageOfBracketsBecauseParentIsObject; gd::String variableObjectName; gd::ExpressionParserLocation variableObjectNameLocation; + gd::String rootVariableName; + gd::ExpressionParserLocation rootVariableLocation; const gd::String *currentParameterExtraInfo; const gd::Platform &platform; const gd::ProjectScopedContainers &projectScopedContainers; From 667a4b11ae6a71f769e0c0ada66bf54772044631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Tue, 30 Jun 2026 12:32:50 +0200 Subject: [PATCH 02/10] Add a parameter for the object name --- .../ExpressionCodeGenerator.cpp | 1 + .../IDE/Events/EventsBehaviorRenamer.cpp | 1 - .../IDE/Events/EventsContextAnalyzer.cpp | 1 - .../IDE/Events/EventsParameterReplacer.cpp | 1 - .../IDE/Events/EventsPropertyReplacer.cpp | 1 - .../EventsVariableInstructionTypeSwitcher.cpp | 1 - .../IDE/Events/EventsVariableReplacer.cpp | 1 - .../GDCore/IDE/Events/ExpressionValidator.cpp | 7 ++++ Core/GDCore/IDE/Events/ExpressionValidator.h | 4 +++ .../IDE/Events/ExpressionsParameterMover.cpp | 1 - Core/GDCore/IDE/Events/ExpressionsRenamer.cpp | 1 - .../IDE/Events/ProjectElementRenamer.cpp | 1 - Core/GDCore/IDE/InstructionValidator.cpp | 34 +++++++------------ Core/tests/DummyPlatform.cpp | 1 - Core/tests/ExpressionParser2NaugtyStrings.cpp | 1 - Core/tests/ExpressionParser2NodePrinter.cpp | 1 - 16 files changed, 25 insertions(+), 33 deletions(-) diff --git a/Core/GDCore/Events/CodeGeneration/ExpressionCodeGenerator.cpp b/Core/GDCore/Events/CodeGeneration/ExpressionCodeGenerator.cpp index 86aca52b5fc7..e0d327b6eba0 100644 --- a/Core/GDCore/Events/CodeGeneration/ExpressionCodeGenerator.cpp +++ b/Core/GDCore/Events/CodeGeneration/ExpressionCodeGenerator.cpp @@ -55,6 +55,7 @@ gd::String ExpressionCodeGenerator::GenerateExpressionCode( gd::ExpressionValidator validator(codeGenerator.GetPlatform(), codeGenerator.GetProjectScopedContainers(), rootType, + rootObjectName, extraInfo); node->Visit(validator); if (!validator.GetFatalErrors().empty()) { diff --git a/Core/GDCore/IDE/Events/EventsBehaviorRenamer.cpp b/Core/GDCore/IDE/Events/EventsBehaviorRenamer.cpp index 6e97c89ea7ef..a450aa71acfb 100644 --- a/Core/GDCore/IDE/Events/EventsBehaviorRenamer.cpp +++ b/Core/GDCore/IDE/Events/EventsBehaviorRenamer.cpp @@ -15,7 +15,6 @@ #include "GDCore/Events/Parsers/ExpressionParser2NodeWorker.h" #include "GDCore/Extensions/Metadata/MetadataProvider.h" #include "GDCore/Extensions/Metadata/ParameterMetadataTools.h" -#include "GDCore/IDE/Events/ExpressionValidator.h" #include "GDCore/Project/Layout.h" #include "GDCore/Project/Project.h" #include "GDCore/String.h" diff --git a/Core/GDCore/IDE/Events/EventsContextAnalyzer.cpp b/Core/GDCore/IDE/Events/EventsContextAnalyzer.cpp index 58aac2059ce7..8c209dcbb1e9 100644 --- a/Core/GDCore/IDE/Events/EventsContextAnalyzer.cpp +++ b/Core/GDCore/IDE/Events/EventsContextAnalyzer.cpp @@ -15,7 +15,6 @@ #include "GDCore/Extensions/Metadata/InstructionMetadata.h" #include "GDCore/Extensions/Metadata/MetadataProvider.h" #include "GDCore/Extensions/Metadata/ParameterMetadataTools.h" -#include "GDCore/IDE/Events/ExpressionValidator.h" #include "GDCore/IDE/Events/ExpressionTypeFinder.h" #include "GDCore/Project/Layout.h" #include "GDCore/Project/Project.h" diff --git a/Core/GDCore/IDE/Events/EventsParameterReplacer.cpp b/Core/GDCore/IDE/Events/EventsParameterReplacer.cpp index 9a3e800d383e..b6cab334436f 100644 --- a/Core/GDCore/IDE/Events/EventsParameterReplacer.cpp +++ b/Core/GDCore/IDE/Events/EventsParameterReplacer.cpp @@ -18,7 +18,6 @@ #include "GDCore/Extensions/Metadata/MetadataProvider.h" #include "GDCore/Extensions/Metadata/ParameterMetadata.h" #include "GDCore/Extensions/Metadata/ParameterMetadataTools.h" -#include "GDCore/IDE/Events/ExpressionValidator.h" #include "GDCore/Project/Layout.h" #include "GDCore/Project/Project.h" #include "GDCore/Project/ProjectScopedContainers.h" diff --git a/Core/GDCore/IDE/Events/EventsPropertyReplacer.cpp b/Core/GDCore/IDE/Events/EventsPropertyReplacer.cpp index 7783f7d74a1f..ac5ce5ce7cca 100644 --- a/Core/GDCore/IDE/Events/EventsPropertyReplacer.cpp +++ b/Core/GDCore/IDE/Events/EventsPropertyReplacer.cpp @@ -18,7 +18,6 @@ #include "GDCore/Extensions/Metadata/MetadataProvider.h" #include "GDCore/Extensions/Metadata/ParameterMetadata.h" #include "GDCore/Extensions/Metadata/ParameterMetadataTools.h" -#include "GDCore/IDE/Events/ExpressionValidator.h" #include "GDCore/Project/Layout.h" #include "GDCore/Project/Project.h" #include "GDCore/Project/ProjectScopedContainers.h" diff --git a/Core/GDCore/IDE/Events/EventsVariableInstructionTypeSwitcher.cpp b/Core/GDCore/IDE/Events/EventsVariableInstructionTypeSwitcher.cpp index 974c3f3cc094..73eb2fff7992 100644 --- a/Core/GDCore/IDE/Events/EventsVariableInstructionTypeSwitcher.cpp +++ b/Core/GDCore/IDE/Events/EventsVariableInstructionTypeSwitcher.cpp @@ -18,7 +18,6 @@ #include "GDCore/Extensions/Metadata/MetadataProvider.h" #include "GDCore/Extensions/Metadata/ParameterMetadata.h" #include "GDCore/Extensions/Metadata/ParameterMetadataTools.h" -#include "GDCore/IDE/Events/ExpressionValidator.h" #include "GDCore/IDE/Events/ExpressionVariableOwnerFinder.h" #include "GDCore/IDE/Events/ExpressionVariableNameFinder.h" #include "GDCore/IDE/VariableInstructionSwitcher.h" diff --git a/Core/GDCore/IDE/Events/EventsVariableReplacer.cpp b/Core/GDCore/IDE/Events/EventsVariableReplacer.cpp index cc3dabb4bb8e..ab18332ffcbe 100644 --- a/Core/GDCore/IDE/Events/EventsVariableReplacer.cpp +++ b/Core/GDCore/IDE/Events/EventsVariableReplacer.cpp @@ -18,7 +18,6 @@ #include "GDCore/Extensions/Metadata/MetadataProvider.h" #include "GDCore/Extensions/Metadata/ParameterMetadata.h" #include "GDCore/Extensions/Metadata/ParameterMetadataTools.h" -#include "GDCore/IDE/Events/ExpressionValidator.h" #include "GDCore/IDE/Events/ExpressionVariableOwnerFinder.h" #include "GDCore/IDE/Events/ExpressionVariableNameFinder.h" #include "GDCore/IDE/VariableInstructionSwitcher.h" diff --git a/Core/GDCore/IDE/Events/ExpressionValidator.cpp b/Core/GDCore/IDE/Events/ExpressionValidator.cpp index 672431985db1..8de5525381e1 100644 --- a/Core/GDCore/IDE/Events/ExpressionValidator.cpp +++ b/Core/GDCore/IDE/Events/ExpressionValidator.cpp @@ -392,6 +392,12 @@ ExpressionValidator::Type ExpressionValidator::ValidateFunction( return returnType; } + const gd::String parentRootObjectName = rootObjectName; + // We don't check objectvar parameters since they are only used by the legacy + // functions like: `Object.Variable(MyVariable)` which allow undeclared + // variables. + rootObjectName = emptyParameterExtraInfo; + // TODO: reverse the order of diagnostic? size_t writtenParametersFirstIndex = ExpressionParser2::WrittenParametersFirstIndex(function.objectName, @@ -452,6 +458,7 @@ ExpressionValidator::Type ExpressionValidator::ValidateFunction( } metadataIndex++; } + rootObjectName = parentRootObjectName; return returnType; } diff --git a/Core/GDCore/IDE/Events/ExpressionValidator.h b/Core/GDCore/IDE/Events/ExpressionValidator.h index 27ada25ad259..869ff537f46f 100644 --- a/Core/GDCore/IDE/Events/ExpressionValidator.h +++ b/Core/GDCore/IDE/Events/ExpressionValidator.h @@ -40,10 +40,12 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { ExpressionValidator(const gd::Platform &platform_, const gd::ProjectScopedContainers & projectScopedContainers_, const gd::String &rootType_, + const gd::String &rootObjectName_ = emptyParameterExtraInfo, const gd::String &extraInfo_ = emptyParameterExtraInfo) : platform(platform_), projectScopedContainers(projectScopedContainers_), parentType(StringToType(gd::ValueTypeMetadata::GetExpressionPrimitiveValueType(rootType_))), + rootObjectName(rootObjectName_), childType(Type::Unknown), forbidsUsageOfBracketsBecauseParentIsObject(false), currentParameterExtraInfo(&extraInfo_), @@ -593,6 +595,8 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { std::vector> supplementalErrors; Type childType; ///< The type "discovered" down the tree and passed up. Type parentType; ///< The type "required" by the top of the tree. + /** The root object name of the expression or a function call. */ + gd::String rootObjectName; bool forbidsUsageOfBracketsBecauseParentIsObject; gd::String variableObjectName; gd::ExpressionParserLocation variableObjectNameLocation; diff --git a/Core/GDCore/IDE/Events/ExpressionsParameterMover.cpp b/Core/GDCore/IDE/Events/ExpressionsParameterMover.cpp index 7eb6e23c1cd3..bf2a6d848bb8 100644 --- a/Core/GDCore/IDE/Events/ExpressionsParameterMover.cpp +++ b/Core/GDCore/IDE/Events/ExpressionsParameterMover.cpp @@ -14,7 +14,6 @@ #include "GDCore/Events/Parsers/ExpressionParser2NodePrinter.h" #include "GDCore/Events/Parsers/ExpressionParser2NodeWorker.h" #include "GDCore/Extensions/Metadata/MetadataProvider.h" -#include "GDCore/IDE/Events/ExpressionValidator.h" #include "GDCore/Project/Layout.h" #include "GDCore/Project/Project.h" #include "GDCore/String.h" diff --git a/Core/GDCore/IDE/Events/ExpressionsRenamer.cpp b/Core/GDCore/IDE/Events/ExpressionsRenamer.cpp index b0382ad32287..e973d2589482 100644 --- a/Core/GDCore/IDE/Events/ExpressionsRenamer.cpp +++ b/Core/GDCore/IDE/Events/ExpressionsRenamer.cpp @@ -14,7 +14,6 @@ #include "GDCore/Events/Parsers/ExpressionParser2NodePrinter.h" #include "GDCore/Events/Parsers/ExpressionParser2NodeWorker.h" #include "GDCore/Extensions/Metadata/MetadataProvider.h" -#include "GDCore/IDE/Events/ExpressionValidator.h" #include "GDCore/Project/Layout.h" #include "GDCore/Project/Project.h" #include "GDCore/String.h" diff --git a/Core/GDCore/IDE/Events/ProjectElementRenamer.cpp b/Core/GDCore/IDE/Events/ProjectElementRenamer.cpp index a8fc2d4115b7..6374da7b17a0 100644 --- a/Core/GDCore/IDE/Events/ProjectElementRenamer.cpp +++ b/Core/GDCore/IDE/Events/ProjectElementRenamer.cpp @@ -15,7 +15,6 @@ #include "GDCore/Events/Parsers/ExpressionParser2NodeWorker.h" #include "GDCore/Extensions/Metadata/MetadataProvider.h" #include "GDCore/Extensions/Metadata/ParameterMetadataTools.h" -#include "GDCore/IDE/Events/ExpressionValidator.h" #include "GDCore/Project/Layout.h" #include "GDCore/Project/Project.h" #include "GDCore/String.h" diff --git a/Core/GDCore/IDE/InstructionValidator.cpp b/Core/GDCore/IDE/InstructionValidator.cpp index 5608df5af254..1f7c35446e0f 100644 --- a/Core/GDCore/IDE/InstructionValidator.cpp +++ b/Core/GDCore/IDE/InstructionValidator.cpp @@ -65,10 +65,23 @@ ParameterValidationResult InstructionValidator::ValidateParameter( if (gd::ParameterMetadata::IsExpression("number", parameterType) || gd::ParameterMetadata::IsExpression("string", parameterType) || gd::ParameterMetadata::IsExpression("variable", parameterType)) { + + // New object variable instructions require the variable to be + // declared while legacy ones don't. + // For legacy variable instruction, we pass an empty object name. + gd::String rootObjectName = ""; + if (parameterType == "objectvar" && + gd::VariableInstructionSwitcher::IsSwitchableVariableInstruction( + instruction.GetType())) { + const auto &objectsContainersList = + projectScopedContainers.GetObjectsContainersList(); + rootObjectName = instruction.GetParameter(0).GetPlainString(); + } auto &expressionNode = *instruction.GetParameter(parameterIndex).GetRootNode(); ExpressionValidator expressionValidator(platform, projectScopedContainers, parameterType, + rootObjectName, parameterMetadata.GetExtraInfo()); expressionNode.Visit(expressionValidator); @@ -78,27 +91,6 @@ ParameterValidationResult InstructionValidator::ValidateParameter( if (!expressionValidator.GetDeprecationWarnings().empty()) { result.hasDeprecationWarning = true; } - - // New object variable instructions require the variable to be - // declared while legacy ones don't. - // This is why it's done here instead of in the parser directly. - if (result.isValid && parameterType == "objectvar" && - gd::VariableInstructionSwitcher::IsSwitchableVariableInstruction( - instruction.GetType())) { - // Check at least the name of the root variable, it's the best we can - // do. - const auto &objectsContainersList = - projectScopedContainers.GetObjectsContainersList(); - const auto &objectName = instruction.GetParameter(0).GetPlainString(); - const auto &variableName = - instruction.GetParameter(parameterIndex).GetPlainString(); - if (objectsContainersList.HasObjectOrGroupWithVariableNamed( - objectName, - gd::InstructionValidator::GetRootVariableName(variableName)) == - gd::ObjectsContainersList::DoesNotExist) { - result.isValid = false; - } - } } else if (gd::ParameterMetadata::IsObject(parameterType)) { const auto &objectOrGroupName = instruction.GetParameter(parameterIndex).GetPlainString(); diff --git a/Core/tests/DummyPlatform.cpp b/Core/tests/DummyPlatform.cpp index 9e888804d4d7..56a0ee8e51d0 100644 --- a/Core/tests/DummyPlatform.cpp +++ b/Core/tests/DummyPlatform.cpp @@ -5,7 +5,6 @@ */ #include "GDCore/Extensions/Platform.h" #include "GDCore/Extensions/PlatformExtension.h" -#include "GDCore/IDE/Events/ExpressionValidator.h" #include "GDCore/Project/Behavior.h" #include "GDCore/Project/ObjectConfiguration.h" #include "GDCore/Extensions/Builtin/SpriteExtension/SpriteObject.h" diff --git a/Core/tests/ExpressionParser2NaugtyStrings.cpp b/Core/tests/ExpressionParser2NaugtyStrings.cpp index 3a44004e5e15..5fe6ab2ced49 100644 --- a/Core/tests/ExpressionParser2NaugtyStrings.cpp +++ b/Core/tests/ExpressionParser2NaugtyStrings.cpp @@ -8,7 +8,6 @@ #include "DummyPlatform.h" #include "GDCore/Extensions/Platform.h" #include "GDCore/Extensions/PlatformExtension.h" -#include "GDCore/IDE/Events/ExpressionValidator.h" #include "GDCore/Project/Layout.h" #include "GDCore/Project/Project.h" #include "catch.hpp" diff --git a/Core/tests/ExpressionParser2NodePrinter.cpp b/Core/tests/ExpressionParser2NodePrinter.cpp index 5638a7b38758..7fe9d0a9a8a2 100644 --- a/Core/tests/ExpressionParser2NodePrinter.cpp +++ b/Core/tests/ExpressionParser2NodePrinter.cpp @@ -8,7 +8,6 @@ #include "GDCore/Events/Parsers/ExpressionParser2.h" #include "GDCore/Extensions/Platform.h" #include "GDCore/Extensions/PlatformExtension.h" -#include "GDCore/IDE/Events/ExpressionValidator.h" #include "GDCore/Project/Layout.h" #include "GDCore/Project/Project.h" #include "catch.hpp" From 47625244c845934f5f90b10bab379954477b4eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Wed, 1 Jul 2026 21:59:03 +0200 Subject: [PATCH 03/10] Add errors for undeclared object variable (but not child) --- .../Extensions/Metadata/ValueTypeMetadata.h | 4 ++ .../GDCore/IDE/Events/ExpressionValidator.cpp | 24 +++++++----- Core/GDCore/IDE/Events/ExpressionValidator.h | 39 +++++++++++++++---- 3 files changed, 51 insertions(+), 16 deletions(-) diff --git a/Core/GDCore/Extensions/Metadata/ValueTypeMetadata.h b/Core/GDCore/Extensions/Metadata/ValueTypeMetadata.h index e5c3deb650bd..83ad3aad75a2 100644 --- a/Core/GDCore/Extensions/Metadata/ValueTypeMetadata.h +++ b/Core/GDCore/Extensions/Metadata/ValueTypeMetadata.h @@ -179,6 +179,10 @@ class GD_CORE_API ValueTypeMetadata { return type == "scenevar" || type == "globalvar" || type == "objectvar"; } + static bool IsTypeObjectVariable(const gd::String &type) { + return type == "objectvar"; + } + /** * \brief Return true if the type is representing one object * (or more, i.e: an object group). diff --git a/Core/GDCore/IDE/Events/ExpressionValidator.cpp b/Core/GDCore/IDE/Events/ExpressionValidator.cpp index 8de5525381e1..d942a7e47148 100644 --- a/Core/GDCore/IDE/Events/ExpressionValidator.cpp +++ b/Core/GDCore/IDE/Events/ExpressionValidator.cpp @@ -122,18 +122,21 @@ bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty( identifierName, childIdentifierName); if (variableExistence == gd::ObjectsContainersList::DoesNotExist) { - RaiseUndeclaredVariableError(_("This variable does not exist on this object or group."), - childIdentifierNameLocation, childIdentifierName, identifierName); + RaiseUndeclaredVariableError( + _("This variable does not exist on this object or group."), + childIdentifierNameLocation, childIdentifierName, identifierName, + isUndeclaredVariableFatal); return true; // We should have found a variable. - } - else if (variableExistence == gd::ObjectsContainersList::ExistsOnlyOnSomeObjectsOfTheGroup) { - RaiseUndeclaredVariableError(_("This variable only exists on some objects of the group. It must be declared for all objects."), - childIdentifierNameLocation, childIdentifierName, identifierName); + } else if (variableExistence == + gd::ObjectsContainersList::ExistsOnlyOnSomeObjectsOfTheGroup) { + RaiseUndeclaredVariableError( + _("This variable only exists on some objects of the group. It must be declared for all objects."), + childIdentifierNameLocation, childIdentifierName, identifierName, + isUndeclaredVariableFatal); return true; // We should have found a variable. - } - else if (variableExistence == gd::ObjectsContainersList::GroupIsEmpty) { + } else if (variableExistence == gd::ObjectsContainersList::GroupIsEmpty) { RaiseUndeclaredVariableError(_("This group is empty. Add an object to this group first."), identifierNameLocation, childIdentifierName, identifierName); @@ -489,6 +492,7 @@ const gd::String& ExpressionValidator::TypeToString(Type type) { // if it allows properties and parameters. case Type::VariableOrProperty: case Type::VariableOrPropertyOrParameter: + case Type::ObjectVariable: case Type::LegacyVariable: return variableTypeString; case Type::Object: @@ -517,7 +521,9 @@ ExpressionValidator::Type ExpressionValidator::StringToType( if (type == ExpressionValidator::variableTypeString || gd::ParameterMetadata::IsExpression( ExpressionValidator::variableTypeString, type)) { - if (gd::ValueTypeMetadata::IsTypeLegacyPreScopedVariable(type)) { + if (gd::ValueTypeMetadata::IsTypeObjectVariable(type)) { + return Type::ObjectVariable; + } else if (gd::ValueTypeMetadata::IsTypeLegacyPreScopedVariable(type)) { return Type::LegacyVariable; } else if (type == "variableOrProperty") { return Type::VariableOrProperty; diff --git a/Core/GDCore/IDE/Events/ExpressionValidator.h b/Core/GDCore/IDE/Events/ExpressionValidator.h index 869ff537f46f..5dd31abcc895 100644 --- a/Core/GDCore/IDE/Events/ExpressionValidator.h +++ b/Core/GDCore/IDE/Events/ExpressionValidator.h @@ -129,11 +129,12 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { _("Operators (+, -, /, *) can't be used with an object name. Remove " "the operator."), node.rightHandSide->location); - } else if (leftType == Type::Variable || leftType == Type::LegacyVariable) { + } else if (leftType == Type::Variable || leftType == Type::ObjectVariable || + leftType == Type::LegacyVariable) { RaiseOperatorError( _("Operators (+, -, /, *) can't be used in variable names. Remove " "the operator from the variable name."), - node.rightHandSide->location); + node.rightHandSide->location); } // The "required" type ("parentType") of the second operator is decided by: @@ -176,7 +177,9 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { _("Operators (+, -) can't be used with an object name. Remove the " "operator."), node.location); - } else if (rightType == Type::Variable || rightType == Type::LegacyVariable) { + } else if (rightType == Type::Variable || + rightType == Type::ObjectVariable || + rightType == Type::LegacyVariable) { RaiseTypeError( _("Operators (+, -) can't be used in variable names. Remove " "the operator from the variable name."), @@ -229,6 +232,17 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { node.child->Visit(*this); rootVariableName = ""; } + } else if (parentType == Type::ObjectVariable) { + childType = parentType; + + if (!rootObjectName.empty()) { + ValidateObjectVariableOrVariableOrProperty(rootObjectName, + node.nameLocation, node.name, + node.nameLocation, false); + } + if (node.child) { + node.child->Visit(*this); + } } else if (parentType == Type::LegacyVariable) { childType = parentType; @@ -294,7 +308,7 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { if (!variableObjectName.empty()) { ValidateObjectVariableOrVariableOrProperty( variableObjectName, variableObjectNameLocation, node.name, - node.nameLocation, !node.child); + node.nameLocation, true); variableObjectName = ""; } else if (!rootVariableName.empty()) { ValidateObjectVariableOrVariableOrProperty( @@ -375,6 +389,13 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { node.identifierName, node.identifierNameLocation, node.childIdentifierName, node.childIdentifierNameLocation, false); } + } else if (parentType == Type::ObjectVariable) { + childType = parentType; + if (!rootObjectName.empty()) { + ValidateObjectVariableOrVariableOrProperty( + rootObjectName, node.identifierNameLocation, + node.identifierName, node.identifierNameLocation, false); + } } else if (parentType != Type::Object && parentType != Type::LegacyVariable) { // It can't happen. @@ -400,7 +421,9 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { } else if (parentType == Type::String) { message = _( "You must enter a text (between quotes) or a valid expression call."); - } else if (parentType == Type::Variable || parentType == Type::LegacyVariable) { + } else if (parentType == Type::Variable || + parentType == Type::ObjectVariable || + parentType == Type::LegacyVariable) { message = _("You must enter a variable name."); } else if (parentType == Type::Object) { message = _("You must enter a valid object name."); @@ -419,6 +442,7 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { String, NumberOrString, Variable, + ObjectVariable, LegacyVariable, Object, Empty, @@ -526,9 +550,10 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { void RaiseUndeclaredVariableError(const gd::String &message, const ExpressionParserLocation &location, const gd::String &variableName, - const gd::String &objectName = "") { + const gd::String &objectName = "", + const bool isUndeclaredVariableFatal = true) { RaiseError(gd::ExpressionParserError::ErrorType::UndeclaredVariable, - message, location, true, variableName, objectName); + message, location, isUndeclaredVariableFatal, variableName, objectName); } void RaiseVariableNameCollisionError(const gd::String &message, From 2beaa38b401af69a1bddc32f10fbdbe7cb1ad53b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Thu, 2 Jul 2026 16:27:35 +0200 Subject: [PATCH 04/10] Generalize child-variable check --- .../GDCore/IDE/Events/ExpressionValidator.cpp | 71 +++++++++------- Core/GDCore/IDE/Events/ExpressionValidator.h | 83 +++++++++++++++---- 2 files changed, 109 insertions(+), 45 deletions(-) diff --git a/Core/GDCore/IDE/Events/ExpressionValidator.cpp b/Core/GDCore/IDE/Events/ExpressionValidator.cpp index d942a7e47148..1a04123fd14e 100644 --- a/Core/GDCore/IDE/Events/ExpressionValidator.cpp +++ b/Core/GDCore/IDE/Events/ExpressionValidator.cpp @@ -66,6 +66,39 @@ size_t GetMaximumParametersNumber( } // namespace +bool ExpressionValidator::ValidateChildVariable( + const gd::Variable &parentVariable, const gd::String &childVariableName, + const gd::ExpressionParserLocation childNameLocation, + const bool isUndeclaredVariableFatal) { + // A child variable is accessed, check it can be used in an expression. + if (!parentVariable.HasChild(childVariableName)) { + RaiseTypeError(_("No child variable with this name found."), + childNameLocation, isUndeclaredVariableFatal); + return false; + } + return true; +} + +void ExpressionValidator::ValidateLastChildVariable( + const gd::Variable &lastChildVariable, + const gd::ExpressionParserLocation childNameLocation) { + const auto type = lastChildVariable.GetType(); + // Collections type can't be used directly in expressions, a child + // must be accessed. + if (type == Variable::Structure) { + RaiseTypeError(_("You need to specify the name of the child variable " + "to access. For example: `MyVariable.child`."), + childNameLocation); + } else if (type == Variable::Array) { + RaiseTypeError(_("You need to specify the name of the child variable " + "to access. For example: `MyVariable[0]`."), + childNameLocation); + } else { + // Number, string or boolean variables can be used in expressions. + ReadChildTypeFromVariable(type); + } +} + bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty( const gd::IdentifierNode &identifier) { return ValidateObjectVariableOrVariableOrProperty( @@ -80,24 +113,6 @@ bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty( const gd::String &childIdentifierName, const gd::ExpressionParserLocation childIdentifierNameLocation, const bool isUndeclaredVariableFatal) { - auto validateVariableTypeForExpression = - [this, &identifierNameLocation](gd::Variable::Type type) { - // Collections type can't be used directly in expressions, a child - // must be accessed. - if (type == Variable::Structure) { - RaiseTypeError(_("You need to specify the name of the child variable " - "to access. For example: `MyVariable.child`."), - identifierNameLocation); - } else if (type == Variable::Array) { - RaiseTypeError(_("You need to specify the name of the child variable " - "to access. For example: `MyVariable[0]`."), - identifierNameLocation); - } else { - // Number, string or boolean variables can be used in expressions. - return; - } - }; - const auto& variablesContainersList = projectScopedContainers.GetVariablesContainersList(); const auto& objectsContainersList = projectScopedContainers.GetObjectsContainersList(); const auto& propertiesContainersList = projectScopedContainers.GetPropertiesContainersList(); @@ -159,22 +174,16 @@ bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty( if (childIdentifierName.empty()) { // Just the root variable is accessed, check it can be used in an // expression. - validateVariableTypeForExpression(variable.GetType()); - ReadChildTypeFromVariable(variable.GetType()); - + ValidateLastChildVariable(variable, identifierNameLocation); return true; // We found a variable. } else { - // A child variable is accessed, check it can be used in an expression. - if (!variable.HasChild(childIdentifierName)) { - RaiseTypeError(_("No child variable with this name found."), - childIdentifierNameLocation, isUndeclaredVariableFatal); - - return true; // We should have found a variable. + ValidateChildVariable(variable, childIdentifierName, + childIdentifierNameLocation, + isUndeclaredVariableFatal); + if (variable.HasChild(childIdentifierName)) { + ValidateLastChildVariable(variable.GetChild(childIdentifierName), + identifierNameLocation); } - - const gd::Variable& childVariable = - variable.GetChild(childIdentifierName); - ReadChildTypeFromVariable(childVariable.GetType()); return true; // We found a variable. } }, [&]() { diff --git a/Core/GDCore/IDE/Events/ExpressionValidator.h b/Core/GDCore/IDE/Events/ExpressionValidator.h index 5dd31abcc895..0df666299511 100644 --- a/Core/GDCore/IDE/Events/ExpressionValidator.h +++ b/Core/GDCore/IDE/Events/ExpressionValidator.h @@ -215,7 +215,8 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { } void OnVisitVariableNode(VariableNode& node) override { ReportAnyError(node); - rootVariableName = ""; + parentVariable = nullptr; + variableChildDepth = 0; if (parentType == Type::Variable || parentType == Type::VariableOrProperty || @@ -226,11 +227,16 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { node.location, node.name, node.child != nullptr); if (node.child) { if (isRootVariableDeclared) { - rootVariableName = node.name; - rootVariableLocation = node.nameLocation; + const auto &variable = + projectScopedContainers.GetVariablesContainersList().Get( + node.name); + if (node.child) { + parentVariable = &variable; + } else { + ValidateLastChildVariable(variable, node.location); + } } node.child->Visit(*this); - rootVariableName = ""; } } else if (parentType == Type::ObjectVariable) { childType = parentType; @@ -269,8 +275,14 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { forbidsUsageOfBracketsBecauseParentIsObject = true; }, [&]() { // This is a variable. - rootVariableName = node.name; - rootVariableLocation = node.nameLocation; + const auto &variable = + projectScopedContainers.GetVariablesContainersList().Get( + node.name); + if (node.child) { + parentVariable = &variable; + } else { + ValidateLastChildVariable(variable, node.location); + } }, [&]() { // This is a property. // Being in this node implies that there is at least a child - which is not supported for properties. @@ -309,13 +321,48 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { ValidateObjectVariableOrVariableOrProperty( variableObjectName, variableObjectNameLocation, node.name, node.nameLocation, true); + + const auto &objectsContainersList = + projectScopedContainers.GetObjectsContainersList(); + auto variableExistence = + objectsContainersList.HasObjectOrGroupWithVariableNamed( + variableObjectName, node.name); + if (variableExistence == gd::ObjectsContainersList::Exists) { + const auto &childVariable = + objectsContainersList + .GetObjectOrGroupVariablesContainer(variableObjectName) + ->Get(node.name); + if (node.child) { + parentVariable = &childVariable; + } else { + ValidateLastChildVariable(childVariable, node.nameLocation); + parentVariable = nullptr; + variableChildDepth = 0; + } + } else { + parentVariable = nullptr; + variableChildDepth = 0; + } variableObjectName = ""; - } else if (!rootVariableName.empty()) { - ValidateObjectVariableOrVariableOrProperty( - rootVariableName, rootVariableLocation, node.name, node.nameLocation, - !node.child); + } else if (parentVariable) { + const bool isChildVariableDeclared = ValidateChildVariable( + *parentVariable, node.name, node.nameLocation, false); + if (isChildVariableDeclared) { + const auto &childVariable = parentVariable->GetChild(node.name); + if (node.child) { + parentVariable = &childVariable; + variableChildDepth++; + } else { + ValidateLastChildVariable(childVariable, node.nameLocation); + parentVariable = nullptr; + variableChildDepth = 0; + } + } + else { + parentVariable = nullptr; + variableChildDepth = 0; + } } - rootVariableName = ""; // In the case we accessed an object variable (`MyObject.MyVariable`), // brackets can now be used (`MyObject.MyVariable["MyChildVariable"]` is now valid). forbidsUsageOfBracketsBecauseParentIsObject = false; @@ -328,8 +375,9 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { VariableBracketAccessorNode& node) override { ReportAnyError(node); - rootVariableName = ""; variableObjectName = ""; + parentVariable = nullptr; + variableChildDepth = 0; if (forbidsUsageOfBracketsBecauseParentIsObject) { RaiseError(gd::ExpressionParserError::ErrorType::BracketsNotAllowedForObjects, _("You can't use the brackets to access an object variable. " @@ -457,6 +505,13 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { const gd::String &childIdentifierName, const gd::ExpressionParserLocation childIdentifierNameLocation, const bool hasChild); + bool ValidateChildVariable( + const gd::Variable &parentVariable, const gd::String &childVariableName, + const gd::ExpressionParserLocation childNameLocation, + const bool isUndeclaredVariableFatal); + void ValidateLastChildVariable( + const gd::Variable &lastChildVariable, + const gd::ExpressionParserLocation childNameLocation); bool CheckVariableExistence(const ExpressionParserLocation &location, const gd::String &name, bool hasChild) { @@ -625,8 +680,8 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { bool forbidsUsageOfBracketsBecauseParentIsObject; gd::String variableObjectName; gd::ExpressionParserLocation variableObjectNameLocation; - gd::String rootVariableName; - gd::ExpressionParserLocation rootVariableLocation; + const gd::Variable *parentVariable = nullptr; + size_t variableChildDepth = 0; const gd::String *currentParameterExtraInfo; const gd::Platform &platform; const gd::ProjectScopedContainers &projectScopedContainers; From 6b03a63f3c51488b2ba26c7defb9b78254a25082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Thu, 2 Jul 2026 17:12:22 +0200 Subject: [PATCH 05/10] Fix missing undeclared object variable errors --- Core/GDCore/IDE/Events/ExpressionValidator.h | 51 ++++++++++++++++++-- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/Core/GDCore/IDE/Events/ExpressionValidator.h b/Core/GDCore/IDE/Events/ExpressionValidator.h index 0df666299511..ebcff02c8f00 100644 --- a/Core/GDCore/IDE/Events/ExpressionValidator.h +++ b/Core/GDCore/IDE/Events/ExpressionValidator.h @@ -245,6 +245,24 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { ValidateObjectVariableOrVariableOrProperty(rootObjectName, node.nameLocation, node.name, node.nameLocation, false); + + const auto &objectsContainersList = + projectScopedContainers.GetObjectsContainersList(); + auto variableExistence = + objectsContainersList.HasObjectOrGroupWithVariableNamed( + rootObjectName, node.name); + if (variableExistence == gd::ObjectsContainersList::Exists) { + const auto &objectVariable = + objectsContainersList + .GetObjectOrGroupVariablesContainer(rootObjectName) + ->Get(node.name); + if (node.child) { + parentVariable = &objectVariable; + } else { + ValidateLastChildVariable(objectVariable, node.nameLocation); + } + } + rootObjectName = ""; } if (node.child) { node.child->Visit(*this); @@ -328,21 +346,20 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { objectsContainersList.HasObjectOrGroupWithVariableNamed( variableObjectName, node.name); if (variableExistence == gd::ObjectsContainersList::Exists) { - const auto &childVariable = + const auto &objectVariable = objectsContainersList .GetObjectOrGroupVariablesContainer(variableObjectName) ->Get(node.name); if (node.child) { - parentVariable = &childVariable; + parentVariable = &objectVariable; } else { - ValidateLastChildVariable(childVariable, node.nameLocation); + ValidateLastChildVariable(objectVariable, node.nameLocation); parentVariable = nullptr; - variableChildDepth = 0; } } else { parentVariable = nullptr; - variableChildDepth = 0; } + variableChildDepth = 0; variableObjectName = ""; } else if (parentVariable) { const bool isChildVariableDeclared = ValidateChildVariable( @@ -443,6 +460,30 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { ValidateObjectVariableOrVariableOrProperty( rootObjectName, node.identifierNameLocation, node.identifierName, node.identifierNameLocation, false); + + const auto &objectsContainersList = + projectScopedContainers.GetObjectsContainersList(); + auto variableExistence = + objectsContainersList.HasObjectOrGroupWithVariableNamed( + rootObjectName, node.identifierName); + if (variableExistence == gd::ObjectsContainersList::Exists) { + const auto &objectVariable = + objectsContainersList + .GetObjectOrGroupVariablesContainer(rootObjectName) + ->Get(node.identifierName); + if (!node.childIdentifierName.empty()) { + const bool isChildVariableDeclared = + ValidateChildVariable(objectVariable, node.childIdentifierName, + node.childIdentifierNameLocation, false); + if (isChildVariableDeclared) { + const auto &childVariable = + parentVariable->GetChild(node.childIdentifierName); + ValidateLastChildVariable(childVariable, + node.childIdentifierNameLocation); + } + } + } + rootObjectName = ""; } } else if (parentType != Type::Object && parentType != Type::LegacyVariable) { From 1b888e43adf550a04635f8fd3b25e928e63beca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Thu, 2 Jul 2026 19:29:59 +0200 Subject: [PATCH 06/10] Fix variable type check --- Core/GDCore/IDE/Events/ExpressionValidator.cpp | 13 +++++++++---- Core/GDCore/IDE/Events/ExpressionValidator.h | 17 +++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Core/GDCore/IDE/Events/ExpressionValidator.cpp b/Core/GDCore/IDE/Events/ExpressionValidator.cpp index 1a04123fd14e..f312b0ba06ac 100644 --- a/Core/GDCore/IDE/Events/ExpressionValidator.cpp +++ b/Core/GDCore/IDE/Events/ExpressionValidator.cpp @@ -112,7 +112,8 @@ bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty( const gd::ExpressionParserLocation identifierNameLocation, const gd::String &childIdentifierName, const gd::ExpressionParserLocation childIdentifierNameLocation, - const bool isUndeclaredVariableFatal) { + const bool isUndeclaredVariableFatal, + const bool hasMoreChildren) { const auto& variablesContainersList = projectScopedContainers.GetVariablesContainersList(); const auto& objectsContainersList = projectScopedContainers.GetObjectsContainersList(); const auto& propertiesContainersList = projectScopedContainers.GetPropertiesContainersList(); @@ -158,9 +159,13 @@ bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty( return true; // We should have found a variable. } - auto variableType = objectsContainersList.GetTypeOfObjectOrGroupVariable( - identifierName, childIdentifierName); - ReadChildTypeFromVariable(variableType); + if (!hasMoreChildren) { + const auto &objectVariable = + objectsContainersList + .GetObjectOrGroupVariablesContainer(identifierName) + ->Get(childIdentifierName); + ValidateLastChildVariable(objectVariable, childIdentifierNameLocation); + } return true; // We found a variable. }, [&]() { diff --git a/Core/GDCore/IDE/Events/ExpressionValidator.h b/Core/GDCore/IDE/Events/ExpressionValidator.h index ebcff02c8f00..0974791b9b19 100644 --- a/Core/GDCore/IDE/Events/ExpressionValidator.h +++ b/Core/GDCore/IDE/Events/ExpressionValidator.h @@ -242,9 +242,9 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { childType = parentType; if (!rootObjectName.empty()) { - ValidateObjectVariableOrVariableOrProperty(rootObjectName, - node.nameLocation, node.name, - node.nameLocation, false); + ValidateObjectVariableOrVariableOrProperty( + rootObjectName, node.nameLocation, node.name, node.nameLocation, + false, !!node.child); const auto &objectsContainersList = projectScopedContainers.GetObjectsContainersList(); @@ -338,7 +338,7 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { if (!variableObjectName.empty()) { ValidateObjectVariableOrVariableOrProperty( variableObjectName, variableObjectNameLocation, node.name, - node.nameLocation, true); + node.nameLocation, true, !!node.child); const auto &objectsContainersList = projectScopedContainers.GetObjectsContainersList(); @@ -353,7 +353,6 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { if (node.child) { parentVariable = &objectVariable; } else { - ValidateLastChildVariable(objectVariable, node.nameLocation); parentVariable = nullptr; } } else { @@ -458,8 +457,9 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { childType = parentType; if (!rootObjectName.empty()) { ValidateObjectVariableOrVariableOrProperty( - rootObjectName, node.identifierNameLocation, - node.identifierName, node.identifierNameLocation, false); + rootObjectName, node.identifierNameLocation, node.identifierName, + node.identifierNameLocation, false, + !node.childIdentifierName.empty()); const auto &objectsContainersList = projectScopedContainers.GetObjectsContainersList(); @@ -545,7 +545,8 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { const gd::ExpressionParserLocation identifierNameLocation, const gd::String &childIdentifierName, const gd::ExpressionParserLocation childIdentifierNameLocation, - const bool hasChild); + const bool isUndeclaredVariableFatal, + const bool hasMoreChildren = false); bool ValidateChildVariable( const gd::Variable &parentVariable, const gd::String &childVariableName, const gd::ExpressionParserLocation childNameLocation, From db035d04748092d7262e33750a6c471a22a2ac90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Fri, 3 Jul 2026 17:50:16 +0200 Subject: [PATCH 07/10] Add tests on variable declaration --- Core/tests/ExpressionParser2.cpp | 93 ++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 39 deletions(-) diff --git a/Core/tests/ExpressionParser2.cpp b/Core/tests/ExpressionParser2.cpp index f8267f2556ea..a6701fb0fd7c 100644 --- a/Core/tests/ExpressionParser2.cpp +++ b/Core/tests/ExpressionParser2.cpp @@ -81,6 +81,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") { .InsertNew("MyObjectStructureVariable") .GetChild("MyChildStructure") .GetChild("MyChild"); + myObject.GetVariables() + .Get("MyObjectStructureVariable") + .GetChild("MyChild"); auto &myGroup = layout1.GetObjects().GetObjectGroups().InsertNew("MyGroup"); @@ -2099,7 +2102,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { } SECTION("in object variable parameter") { auto node = - parser.ParseExpression("MySceneStructureVariable.MyChild"); + parser.ParseExpression("MyObjectStructureVariable.MyChild"); gd::ExpressionValidator validator(platform, projectScopedContainers, "objectvar", "MyObject"); @@ -2238,7 +2241,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); node->Visit(validator); RequireNoFatalError(validator); - // TODO Add a non-fatal error + RequireAllErrorsCount(validator, 1); + REQUIRE(validator.GetAllErrors()[0]->GetMessage() == + "No child variable with this name found."); } SECTION("Invalid scene variables (2 levels, child does not exist)") { @@ -2248,7 +2253,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "variable"); node->Visit(validator); RequireNoFatalError(validator); - // TODO Add a non-fatal error + RequireAllErrorsCount(validator, 1); + REQUIRE(validator.GetAllErrors()[0]->GetMessage() == + "No child variable with this name found."); } SECTION("Invalid scene variables (3 levels, child does not exist)") { @@ -2259,7 +2266,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "variable"); node->Visit(validator); RequireNoFatalError(validator); - // TODO Add a non-fatal error + RequireAllErrorsCount(validator, 1); + REQUIRE(validator.GetAllErrors()[0]->GetMessage() == + "No child variable with this name found."); } SECTION("Undeclared legacy scene variables (2 levels, child does not exist)") { @@ -2308,7 +2317,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") { "objectvar", "MyObject"); node->Visit(validator); RequireNoFatalError(validator); - // TODO Add a non-fatal error + RequireAllErrorsCount(validator, 1); + REQUIRE(validator.GetAllErrors()[0]->GetMessage() == + "No child variable with this name found."); } SECTION("Undeclared object variables (3 levels, child does not exist)") { @@ -2319,7 +2330,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") { "objectvar", "MyObject"); node->Visit(validator); RequireNoFatalError(validator); - // TODO Add a non-fatal error + RequireAllErrorsCount(validator, 1); + REQUIRE(validator.GetAllErrors()[0]->GetMessage() == + "No child variable with this name found."); } SECTION("Undeclared object variables in expressions (2 levels, child does not exist)") { @@ -2330,7 +2343,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") { "number|string"); node->Visit(validator); RequireNoFatalError(validator); - // TODO Add a non-fatal error + RequireAllErrorsCount(validator, 1); + REQUIRE(validator.GetAllErrors()[0]->GetMessage() == + "No child variable with this name found."); } SECTION("Undeclared object variables in expressions (3 levels, child does not exist)") { @@ -2342,7 +2357,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") { "number|string"); node->Visit(validator); RequireNoFatalError(validator); - // TODO Add a non-fatal error + RequireAllErrorsCount(validator, 1); + REQUIRE(validator.GetAllErrors()[0]->GetMessage() == + "No child variable with this name found."); } SECTION("Invalid scene variables (2 levels, variable and child do not exist)") { @@ -2381,8 +2398,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); node->Visit(validator); - RequireNoError(validator); - // TODO Add a fatal error + RequireFatalErrorsCount(validator, 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You need to specify the name of the child variable to access. For example: `MyVariable.child`."); } SECTION("Invalid scene variables type in expression (3 levels)") { @@ -2392,8 +2410,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); node->Visit(validator); - RequireNoError(validator); - // TODO Add a fatal error + RequireFatalErrorsCount(validator, 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You need to specify the name of the child variable to access. For example: `MyVariable.child`."); } SECTION("Invalid object variables type in expression (1 level)") { @@ -2403,8 +2422,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); node->Visit(validator); - RequireNoError(validator); - // TODO Add a fatal error + RequireFatalErrorsCount(validator, 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You need to specify the name of the child variable to access. For example: `MyVariable.child`."); } SECTION("Invalid object variables type in expression (2 levels)") { @@ -2414,8 +2434,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); node->Visit(validator); - RequireNoError(validator); - // TODO Add a fatal error + RequireFatalErrorsCount(validator, 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You need to specify the name of the child variable to access. For example: `MyVariable.child`."); } SECTION("Valid object variables (1 level)") { @@ -2459,7 +2480,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { SECTION("Valid object variables (2 levels)") { { auto node = - parser.ParseExpression("MySpriteObject.MyVariable.MyChild"); + parser.ParseExpression("MyObject.MyObjectStructureVariable.MyChild"); gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); node->Visit(validator); @@ -2467,7 +2488,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { } { auto node = - parser.ParseExpression("MySpriteObject.MyVariable.MyChild + MySpriteObject.MyVariable2"); + parser.ParseExpression("MyObject.MyObjectStructureVariable.MyChild + MySpriteObject.MyVariable"); gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); node->Visit(validator); @@ -2475,7 +2496,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { } { auto node = - parser.ParseExpression("MySpriteObject.MyVariable[\"MyChild\"] + MySpriteObject.MyVariable2"); + parser.ParseExpression("MyObject.MyObjectStructureVariable[\"MyChild\"] + MySpriteObject.MyVariable"); gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); node->Visit(validator); @@ -2499,8 +2520,10 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); node->Visit(validator); - RequireNoError(validator); - // TODO Add a non-fatal error + RequireNoFatalError(validator); + RequireAllErrorsCount(validator, 1); + REQUIRE(validator.GetAllErrors()[0]->GetMessage() == + "No child variable with this name found."); } SECTION("Invalid object variables (1 level, non existing object)") { @@ -3929,7 +3952,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") { "objectvar", "MyObject"); node->Visit(validator); RequireNoFatalError(validator); - // TODO Add a non-fatal error + RequireAllErrorsCount(validator, 1); + REQUIRE(validator.GetAllErrors()[0]->GetMessage() == + "This variable does not exist on this object or group."); } SECTION("Undeclared object variable with children") { @@ -3941,7 +3966,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") { "objectvar", "MyObject"); node->Visit(validator); RequireNoFatalError(validator); - // TODO Add a non-fatal error + RequireAllErrorsCount(validator, 1); + REQUIRE(validator.GetAllErrors()[0]->GetMessage() == + "This variable does not exist on this object or group."); } SECTION("Undeclared object variable in expression") { @@ -4324,7 +4351,6 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "string"); node->Visit(validator); - RequireNoError(validator); } // A string concatenated with a number variable (will have to be casted to a string in code generation) @@ -4335,7 +4361,6 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "string"); node->Visit(validator); - RequireNoError(validator); } // A string concatenated with an unknown variable (will have to be casted to a string in code generation) @@ -4346,8 +4371,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "string"); node->Visit(validator); - - RequireNoError(validator); + RequireNoFatalError(validator); } } SECTION("Expression/parent type is 'number'") { @@ -4367,7 +4391,6 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "number"); node->Visit(validator); - RequireNoError(validator); } // A number concatenated with a string variable (will have to be casted to a number in code generation) @@ -4378,7 +4401,6 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "number"); node->Visit(validator); - RequireNoError(validator); } // A number concatenated with an unknown variable (will have to be casted to a number in code generation) @@ -4389,8 +4411,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "number"); node->Visit(validator); - - RequireNoError(validator); + RequireNoFatalError(validator); } } SECTION("Expression/parent type is 'number|string'") { @@ -4411,7 +4432,6 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); node->Visit(validator); - RequireNoError(validator); } // A string concatenated with a number variable (will have to be casted to a string in code generation) @@ -4422,7 +4442,6 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); node->Visit(validator); - RequireNoError(validator); } // A string concatenated with an unknown variable (will have to be casted to a string in code generation) @@ -4433,8 +4452,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); node->Visit(validator); - - RequireNoError(validator); + RequireNoFatalError(validator); } } SECTION("Expression/parent inferred type is 'number'") { @@ -4454,7 +4472,6 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); node->Visit(validator); - RequireNoError(validator); } // A number concatenated with a string variable (will have to be casted to a number in code generation) @@ -4465,7 +4482,6 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); node->Visit(validator); - RequireNoError(validator); } // A number concatenated with an unknown variable (will have to be casted to a number in code generation) @@ -4476,8 +4492,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); node->Visit(validator); - - RequireNoError(validator); + RequireNoFatalError(validator); } } } From 8c7c248fa5de71bb6b24ace21694d7e891d1f28a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Fri, 3 Jul 2026 20:20:22 +0200 Subject: [PATCH 08/10] Fix a NPE --- Core/GDCore/IDE/Events/ExpressionValidator.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/GDCore/IDE/Events/ExpressionValidator.h b/Core/GDCore/IDE/Events/ExpressionValidator.h index 0974791b9b19..0b07e24bbbc4 100644 --- a/Core/GDCore/IDE/Events/ExpressionValidator.h +++ b/Core/GDCore/IDE/Events/ExpressionValidator.h @@ -477,7 +477,7 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker { node.childIdentifierNameLocation, false); if (isChildVariableDeclared) { const auto &childVariable = - parentVariable->GetChild(node.childIdentifierName); + objectVariable.GetChild(node.childIdentifierName); ValidateLastChildVariable(childVariable, node.childIdentifierNameLocation); } From 36d641f9ba662d89d33fb4eb9be1a08ba5fd6417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sat, 4 Jul 2026 11:26:29 +0200 Subject: [PATCH 09/10] Add tests on leaf structure variable --- Core/tests/ExpressionParser2.cpp | 93 +++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 14 deletions(-) diff --git a/Core/tests/ExpressionParser2.cpp b/Core/tests/ExpressionParser2.cpp index a6701fb0fd7c..da8784d56ca5 100644 --- a/Core/tests/ExpressionParser2.cpp +++ b/Core/tests/ExpressionParser2.cpp @@ -77,13 +77,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") { layout1.GetObjects().InsertNewObject(project, "", "MyObject", 0); myObject.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior"); myObject.GetVariables().InsertNew("MyObjectVariable"); - myObject.GetVariables() - .InsertNew("MyObjectStructureVariable") - .GetChild("MyChildStructure") - .GetChild("MyChild"); - myObject.GetVariables() - .Get("MyObjectStructureVariable") - .GetChild("MyChild"); + myObject.GetVariables().InsertNew("MyObjectStructureVariable").GetChild("MyChild"); + myObject.GetVariables().Get("MyObjectStructureVariable").GetChild("MyChildStructure").GetChild("MyChild"); + myObject.GetVariables().Get("MyObjectStructureVariable").GetChild("MyChildStructure").GetChild("MyChildStructure2").GetChild("MyChild"); auto &myGroup = layout1.GetObjects().GetObjectGroups().InsertNew("MyGroup"); @@ -2379,9 +2375,8 @@ TEST_CASE("ExpressionParser2", "[common][events]") { "You must enter a number or a text, wrapped inside double quotes (example: \"Hello world\"), or a variable name."); } - SECTION("Invalid scene variables type in expression (1 level)") { - auto node = - parser.ParseExpression("MySceneStructureVariable"); + SECTION("Invalid scene variables structure in expression (1 level)") { + auto node = parser.ParseExpression("MySceneStructureVariable"); gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); @@ -2391,7 +2386,16 @@ TEST_CASE("ExpressionParser2", "[common][events]") { "You need to specify the name of the child variable to access. For example: `MyVariable.child`."); } - SECTION("Invalid scene variables type in expression (2 levels)") { + SECTION("Valid scene variable structure in variable parameter (1 level)") { + auto node = parser.ParseExpression("MySceneStructureVariable"); + + gd::ExpressionValidator validator(platform, projectScopedContainers, + "variable"); + node->Visit(validator); + RequireNoError(validator); + } + + SECTION("Invalid scene variables structure in expression (2 levels)") { auto node = parser.ParseExpression("MySceneStructureVariable.MyChildStructure"); @@ -2403,7 +2407,17 @@ TEST_CASE("ExpressionParser2", "[common][events]") { "You need to specify the name of the child variable to access. For example: `MyVariable.child`."); } - SECTION("Invalid scene variables type in expression (3 levels)") { + SECTION("Valid scene variables structure in variable parameter (2 levels)") { + auto node = + parser.ParseExpression("MySceneStructureVariable.MyChildStructure"); + + gd::ExpressionValidator validator(platform, projectScopedContainers, + "variable"); + node->Visit(validator); + RequireNoError(validator); + } + + SECTION("Invalid scene variables structure in expression (3 levels)") { auto node = parser.ParseExpression("MySceneStructureVariable.MyChildStructure.MyChildStructure2"); @@ -2415,7 +2429,17 @@ TEST_CASE("ExpressionParser2", "[common][events]") { "You need to specify the name of the child variable to access. For example: `MyVariable.child`."); } - SECTION("Invalid object variables type in expression (1 level)") { + SECTION("Valid scene variables structure in variable parameter (3 levels)") { + auto node = + parser.ParseExpression("MySceneStructureVariable.MyChildStructure.MyChildStructure2"); + + gd::ExpressionValidator validator(platform, projectScopedContainers, + "variable"); + node->Visit(validator); + RequireNoError(validator); + } + + SECTION("Invalid object variables structure in expression (1 level)") { auto node = parser.ParseExpression("MyObject.MyObjectStructureVariable"); @@ -2427,7 +2451,16 @@ TEST_CASE("ExpressionParser2", "[common][events]") { "You need to specify the name of the child variable to access. For example: `MyVariable.child`."); } - SECTION("Invalid object variables type in expression (2 levels)") { + SECTION("Valid object variables structure in variable parameter (1 level)") { + auto node = parser.ParseExpression("MyObjectStructureVariable"); + + gd::ExpressionValidator validator(platform, projectScopedContainers, + "objectvar"); + node->Visit(validator); + RequireNoError(validator); + } + + SECTION("Invalid object variables structure in expression (2 levels)") { auto node = parser.ParseExpression("MyObject.MyObjectStructureVariable.MyChildStructure"); @@ -2439,6 +2472,38 @@ TEST_CASE("ExpressionParser2", "[common][events]") { "You need to specify the name of the child variable to access. For example: `MyVariable.child`."); } + SECTION("Valid object variables structure in variable parameter (2 levels)") { + auto node = + parser.ParseExpression("MyObjectStructureVariable.MyChildStructure"); + + gd::ExpressionValidator validator(platform, projectScopedContainers, + "objectvar"); + node->Visit(validator); + RequireNoError(validator); + } + + SECTION("Invalid object variables structure in expression (3 levels)") { + auto node = + parser.ParseExpression("MyObject.MyObjectStructureVariable.MyChildStructure.MyChildStructure2"); + + gd::ExpressionValidator validator(platform, projectScopedContainers, + "number|string"); + node->Visit(validator); + RequireFatalErrorsCount(validator, 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You need to specify the name of the child variable to access. For example: `MyVariable.child`."); + } + + SECTION("Valid object variables structure in variable parameter (3 levels)") { + auto node = + parser.ParseExpression("MyObjectStructureVariable.MyChildStructure.MyChildStructure2"); + + gd::ExpressionValidator validator(platform, projectScopedContainers, + "objectvar"); + node->Visit(validator); + RequireNoError(validator); + } + SECTION("Valid object variables (1 level)") { { auto node = From 3981e55e7e9c5e50802f6935eafbb4b8d7933c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sat, 4 Jul 2026 11:38:21 +0200 Subject: [PATCH 10/10] Fix leaf structure error in variable parameters --- .../GDCore/IDE/Events/ExpressionValidator.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Core/GDCore/IDE/Events/ExpressionValidator.cpp b/Core/GDCore/IDE/Events/ExpressionValidator.cpp index f312b0ba06ac..7206d0251b51 100644 --- a/Core/GDCore/IDE/Events/ExpressionValidator.cpp +++ b/Core/GDCore/IDE/Events/ExpressionValidator.cpp @@ -82,21 +82,24 @@ bool ExpressionValidator::ValidateChildVariable( void ExpressionValidator::ValidateLastChildVariable( const gd::Variable &lastChildVariable, const gd::ExpressionParserLocation childNameLocation) { - const auto type = lastChildVariable.GetType(); + const auto type = lastChildVariable.GetType(); + + if (parentType == Type::String || parentType == Type::Number || + parentType == Type::NumberOrString) { // Collections type can't be used directly in expressions, a child // must be accessed. if (type == Variable::Structure) { RaiseTypeError(_("You need to specify the name of the child variable " - "to access. For example: `MyVariable.child`."), - childNameLocation); + "to access. For example: `MyVariable.child`."), + childNameLocation); } else if (type == Variable::Array) { RaiseTypeError(_("You need to specify the name of the child variable " - "to access. For example: `MyVariable[0]`."), - childNameLocation); - } else { - // Number, string or boolean variables can be used in expressions. - ReadChildTypeFromVariable(type); + "to access. For example: `MyVariable[0]`."), + childNameLocation); } + } + // Number, string or boolean variables can be used in expressions. + ReadChildTypeFromVariable(type); } bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty(