diff --git a/Core/GDCore/Events/Parsers/ExpressionParser2.h b/Core/GDCore/Events/Parsers/ExpressionParser2.h index 08e299964a04..bf08fedb4159 100644 --- a/Core/GDCore/Events/Parsers/ExpressionParser2.h +++ b/Core/GDCore/Events/Parsers/ExpressionParser2.h @@ -277,8 +277,11 @@ class GD_CORE_API ExpressionParser2 { std::unique_ptr Variable(const gd::String &name, gd::ExpressionParserLocation nameLocation) { auto variable = gd::make_unique(name); - variable->child = VariableAccessorOrVariableBracketAccessor(); - variable->child->parent = variable.get(); + + if (CheckIfChar(IsOpeningSquareBracket) || CheckIfChar(IsDot)) { + variable->child = VariableAccessorOrVariableBracketAccessor(); + variable->child->parent = variable.get(); + } variable->location = ExpressionParserLocation( nameLocation.GetStartPosition(), GetCurrentPosition()); @@ -302,8 +305,12 @@ class GD_CORE_API ExpressionParser2 { "bracket for each opening bracket.")); } SkipIfChar(IsClosingSquareBracket); - child->child = VariableAccessorOrVariableBracketAccessor(); - child->child->parent = child.get(); + + SkipAllWhitespaces(); + if (CheckIfChar(IsOpeningSquareBracket) || CheckIfChar(IsDot)) { + child->child = VariableAccessorOrVariableBracketAccessor(); + child->child->parent = child.get(); + } child->location = ExpressionParserLocation(childStartPosition, GetCurrentPosition()); @@ -315,8 +322,15 @@ class GD_CORE_API ExpressionParser2 { auto identifierAndLocation = ReadIdentifierName(/*allowDeprecatedSpacesInName=*/ false); auto child = gd::make_unique(identifierAndLocation.name); - child->child = VariableAccessorOrVariableBracketAccessor(); - child->child->parent = child.get(); + if (identifierAndLocation.name.empty()) { + child->diagnostic = RaiseSyntaxError(_("A name should be entered after the dot.")); + } + + SkipAllWhitespaces(); + if (CheckIfChar(IsOpeningSquareBracket) || CheckIfChar(IsDot)) { + child->child = VariableAccessorOrVariableBracketAccessor(); + child->child->parent = child.get(); + } child->nameLocation = identifierAndLocation.location; child->dotLocation = dotLocation; child->location = @@ -325,7 +339,11 @@ class GD_CORE_API ExpressionParser2 { return std::move(child); } - return std::move(gd::make_unique()); + // Should never happen, unless a node called this function without checking if the current character + // was a dot or an opening bracket - this means there is an error in the grammar. + auto unrecognisedNode = gd::make_unique(); + unrecognisedNode->diagnostic = RaiseSyntaxError(_("A dot or bracket was expected here.")); + return std::move(unrecognisedNode); } std::unique_ptr FreeFunction( @@ -361,18 +379,24 @@ class GD_CORE_API ExpressionParser2 { const auto &childIdentifierNameLocation = childIdentifierAndLocation.location; + std::unique_ptr emptyNameError = childIdentifierName.empty() ? + RaiseSyntaxError(_("A name should be entered after the dot.")) : nullptr; + SkipAllWhitespaces(); if (IsNamespaceSeparator()) { ExpressionParserLocation namespaceSeparatorLocation = SkipNamespaceSeparator(); SkipAllWhitespaces(); - return BehaviorFunction(parentIdentifier, + auto behaviorFunction = BehaviorFunction(parentIdentifier, childIdentifierName, parentIdentifierLocation, parentIdentifierDotLocation, childIdentifierNameLocation, namespaceSeparatorLocation); + + if (emptyNameError) behaviorFunction->diagnostic = std::move(emptyNameError); + return std::move(behaviorFunction); } else if (CheckIfChar(IsOpeningParenthesis)) { ExpressionParserLocation openingParenthesisLocation = SkipChar(); @@ -381,7 +405,7 @@ class GD_CORE_API ExpressionParser2 { childIdentifierName); auto parametersNode = Parameters(function.get(), parentIdentifier); function->parameters = std::move(parametersNode.parameters), - function->diagnostic = std::move(parametersNode.diagnostic); + function->diagnostic = emptyNameError ? std::move(emptyNameError) : std::move(parametersNode.diagnostic); function->location = ExpressionParserLocation( parentIdentifierLocation.GetStartPosition(), GetCurrentPosition()); @@ -394,6 +418,8 @@ class GD_CORE_API ExpressionParser2 { return std::move(function); } else if (CheckIfChar(IsDot) || CheckIfChar(IsOpeningSquareBracket)) { auto variable = gd::make_unique(parentIdentifier); + variable->diagnostic = std::move(emptyNameError); + auto child = gd::make_unique(childIdentifierName); child->child = VariableAccessorOrVariableBracketAccessor(); @@ -419,6 +445,7 @@ class GD_CORE_API ExpressionParser2 { node->identifierNameLocation = parentIdentifierLocation; node->identifierNameDotLocation = parentIdentifierDotLocation; node->childIdentifierNameLocation = childIdentifierNameLocation; + node->diagnostic = std::move(emptyNameError); return std::move(node); } diff --git a/Core/GDCore/IDE/Events/ExpressionCompletionFinder.h b/Core/GDCore/IDE/Events/ExpressionCompletionFinder.h index 396a45424607..d225adab3043 100644 --- a/Core/GDCore/IDE/Events/ExpressionCompletionFinder.h +++ b/Core/GDCore/IDE/Events/ExpressionCompletionFinder.h @@ -11,7 +11,9 @@ #include "GDCore/Events/Parsers/ExpressionParser2.h" #include "GDCore/Events/Parsers/ExpressionParser2Node.h" +#include "GDCore/Events/Parsers/ExpressionParser2NodePrinter.h" #include "GDCore/Events/Parsers/ExpressionParser2NodeWorker.h" +#include "GDCore/Events/Parsers/GrammarTerminals.h" #include "GDCore/Extensions/Metadata/ExpressionMetadata.h" #include "GDCore/Extensions/Metadata/InstructionMetadata.h" #include "GDCore/Extensions/Metadata/ValueTypeMetadata.h" @@ -487,6 +489,7 @@ class GD_CORE_API ExpressionCompletionFinder auto type = gd::ExpressionTypeFinder::GetType( platform, projectScopedContainers, rootType, node); + bool eagerlyCompleteIfExactMatch = node.child == nullptr; if (gd::ValueTypeMetadata::IsTypeLegacyPreScopedVariable(type)) { if (type == "globalvar" || type == "scenevar") { const auto* variablesContainer = @@ -496,28 +499,40 @@ class GD_CORE_API ExpressionCompletionFinder : projectScopedContainers.GetVariablesContainersList() .GetBottomMostVariablesContainer(); if (variablesContainer) { - AddCompletionsForVariablesMatchingSearch( - *variablesContainer, node.name, node.nameLocation); + AddCompletionsForVariablesMatchingSearch(*variablesContainer, + node.name, + node.nameLocation, + eagerlyCompleteIfExactMatch); } } else if (type == "objectvar") { auto objectName = gd::ExpressionVariableOwnerFinder::GetObjectName( platform, objectsContainersList, rootObjectName, node); AddCompletionsForObjectOrGroupVariablesMatchingSearch( - objectsContainersList, objectName, node.name, node.nameLocation); + objectsContainersList, + objectName, + node.name, + node.nameLocation, + eagerlyCompleteIfExactMatch); } } else { AddCompletionsForObjectsAndVariablesMatchingSearch( - node.name, type, node.nameLocation); + node.name, type, node.nameLocation, eagerlyCompleteIfExactMatch); } } void OnVisitVariableAccessorNode(VariableAccessorNode& node) override { - VariableOrVariablesContainer variableOrVariablesContainer = + VariableAndItsParent variableAndItsParent = gd::ExpressionVariableParentFinder::GetLastParentOfNode( platform, projectScopedContainers, node); - AddCompletionsForChildrenVariablesOf(variableOrVariablesContainer, - node.nameLocation); + // If no child, we're at the end of a variable (like `GrandChild` in + // `Something.Child.GrandChild`) so we can complete eagerly children if we + // can. + gd::String eagerlyCompleteForVariableName = + node.child == nullptr ? node.name : ""; + AddCompletionsForChildrenVariablesOf(variableAndItsParent, + node.nameLocation, + eagerlyCompleteForVariableName); } void OnVisitVariableBracketAccessorNode( VariableBracketAccessorNode& node) override {} @@ -545,14 +560,18 @@ class GD_CORE_API ExpressionCompletionFinder if (variablesContainer->Has(node.identifierName)) { AddCompletionsForChildrenVariablesOf( &variablesContainer->Get(node.identifierName), - node.childIdentifierNameLocation); + node.childIdentifierNameLocation, + node.childIdentifierName); } } else { // Complete a root variable of the scene or project: + bool eagerlyCompleteIfPossible = + !node.identifierNameDotLocation.IsValid(); AddCompletionsForVariablesMatchingSearch( *variablesContainer, node.identifierName, - node.identifierNameLocation); + node.identifierNameLocation, + eagerlyCompleteIfPossible); } } } else if (type == "objectvar") { @@ -569,23 +588,31 @@ class GD_CORE_API ExpressionCompletionFinder variablesContainer->Has(node.identifierName)) { AddCompletionsForChildrenVariablesOf( &variablesContainer->Get(node.identifierName), - node.childIdentifierNameLocation); + node.childIdentifierNameLocation, + node.childIdentifierName); } } else { // Complete a root variable of the object: + bool eagerlyCompleteIfPossible = + !node.identifierNameDotLocation.IsValid(); AddCompletionsForObjectOrGroupVariablesMatchingSearch( objectsContainersList, objectName, node.identifierName, - node.identifierNameLocation); + node.identifierNameLocation, + eagerlyCompleteIfPossible); } } } else { // Object function, behavior name, variable, object variable. if (IsCaretOn(node.identifierNameLocation)) { - // Is this the proper position? + bool eagerlyCompleteIfPossible = + !node.identifierNameDotLocation.IsValid(); AddCompletionsForAllIdentifiersMatchingSearch( - node.identifierName, type, node.identifierNameLocation); + node.identifierName, + type, + node.identifierNameLocation, + eagerlyCompleteIfPossible); if (!node.identifierNameDotLocation.IsValid()) { completions.push_back( ExpressionCompletionDescription::ForExpressionWithPrefix( @@ -608,7 +635,9 @@ class GD_CORE_API ExpressionCompletionFinder objectsContainersList, objectName, node.childIdentifierName, - node.childIdentifierNameLocation); + node.childIdentifierNameLocation, + true); + completions.push_back( ExpressionCompletionDescription::ForBehaviorWithPrefix( node.childIdentifierName, @@ -625,13 +654,14 @@ class GD_CORE_API ExpressionCompletionFinder }, [&]() { // This is a variable. - VariableOrVariablesContainer variableOrVariablesContainer = + VariableAndItsParent variableAndItsParent = gd::ExpressionVariableParentFinder::GetLastParentOfNode( platform, projectScopedContainers, node); AddCompletionsForChildrenVariablesOf( - variableOrVariablesContainer, - node.childIdentifierNameLocation); + variableAndItsParent, + node.childIdentifierNameLocation, + node.childIdentifierName); }, [&]() { // Ignore properties here. @@ -793,24 +823,46 @@ class GD_CORE_API ExpressionCompletionFinder (inclusive && searchedPosition <= location.GetEndPosition()))); } + /** + * A slightly less strict check than `gd::Project::IsNameSafe` as child variables can be completed + * even if they start with a number. + */ + bool IsIdentifierSafe(const gd::String& name) { + if (name.empty()) return false; + + for (auto character : name) { + if (!GrammarTerminals::IsAllowedInIdentifier(character)) { + return false; + } + } + + return true; + } + void AddCompletionsForChildrenVariablesOf( - VariableOrVariablesContainer variableOrVariablesContainer, - const ExpressionParserLocation& location) { - if (variableOrVariablesContainer.variable) { - return AddCompletionsForChildrenVariablesOf( - variableOrVariablesContainer.variable, location); - } else if (variableOrVariablesContainer.variablesContainer) { - return AddCompletionsForVariablesMatchingSearch( - *variableOrVariablesContainer.variablesContainer, "", location); + VariableAndItsParent variableAndItsParent, + const ExpressionParserLocation& location, + gd::String eagerlyCompleteForVariableName = "") { + if (variableAndItsParent.parentVariable) { + AddCompletionsForChildrenVariablesOf(variableAndItsParent.parentVariable, + location, + eagerlyCompleteForVariableName); + } else if (variableAndItsParent.parentVariablesContainer) { + AddCompletionsForVariablesMatchingSearch( + *variableAndItsParent.parentVariablesContainer, "", location); } } void AddCompletionsForChildrenVariablesOf( - const gd::Variable* variable, const ExpressionParserLocation& location) { + const gd::Variable* variable, + const ExpressionParserLocation& location, + gd::String eagerlyCompleteForVariableName = "") { if (!variable) return; if (variable->GetType() == gd::Variable::Structure) { for (const auto& name : variable->GetAllChildrenNames()) { + if (!IsIdentifierSafe(name)) continue; + const auto& childVariable = variable->GetChild(name); ExpressionCompletionDescription description( ExpressionCompletionDescription::Variable, @@ -819,6 +871,10 @@ class GD_CORE_API ExpressionCompletionFinder description.SetCompletion(name); description.SetVariableType(childVariable.GetType()); completions.push_back(description); + + if (name == eagerlyCompleteForVariableName) { + AddEagerCompletionForVariableChildren(childVariable, name, location); + } } } else { // TODO: we could do a "comment only completion" to indicate that nothing @@ -826,10 +882,32 @@ class GD_CORE_API ExpressionCompletionFinder } } + void AddEagerCompletionForVariableChildren( + const gd::Variable& variable, + const gd::String& variableName, + const ExpressionParserLocation& location) { + if (variable.GetType() == gd::Variable::Structure) { + gd::String prefix = variableName + "."; + for (const auto& name : variable.GetAllChildrenNames()) { + if (!IsIdentifierSafe(name)) continue; + + const auto& childVariable = variable.GetChild(name); + ExpressionCompletionDescription description( + ExpressionCompletionDescription::Variable, + location.GetStartPosition(), + location.GetEndPosition()); + description.SetCompletion(prefix + name); + description.SetVariableType(childVariable.GetType()); + completions.push_back(description); + } + } + } + void AddCompletionsForVariablesMatchingSearch( const gd::VariablesContainer& variablesContainer, const gd::String& search, - const ExpressionParserLocation& location) { + const ExpressionParserLocation& location, + bool eagerlyCompleteIfExactMatch = false) { variablesContainer.ForEachVariableMatchingSearch( search, [&](const gd::String& variableName, const gd::Variable& variable) { @@ -840,6 +918,11 @@ class GD_CORE_API ExpressionCompletionFinder description.SetCompletion(variableName); description.SetVariableType(variable.GetType()); completions.push_back(description); + + if (eagerlyCompleteIfExactMatch && variableName == search) { + AddEagerCompletionForVariableChildren( + variable, variableName, location); + } }); } @@ -847,7 +930,8 @@ class GD_CORE_API ExpressionCompletionFinder const gd::ObjectsContainersList& objectsContainersList, const gd::String& objectOrGroupName, const gd::String& search, - const ExpressionParserLocation& location) { + const ExpressionParserLocation& location, + bool eagerlyCompleteIfExactMatch) { objectsContainersList.ForEachObjectOrGroupVariableMatchingSearch( objectOrGroupName, search, @@ -859,6 +943,11 @@ class GD_CORE_API ExpressionCompletionFinder description.SetCompletion(variableName); description.SetVariableType(variable.GetType()); completions.push_back(description); + + if (eagerlyCompleteIfExactMatch && variableName == search) { + AddEagerCompletionForVariableChildren( + variable, variableName, location); + } }); } @@ -885,7 +974,8 @@ class GD_CORE_API ExpressionCompletionFinder void AddCompletionsForObjectsAndVariablesMatchingSearch( const gd::String& search, const gd::String& type, - const ExpressionParserLocation& location) { + const ExpressionParserLocation& location, + bool eagerlyCompleteIfExactMatch) { projectScopedContainers.ForEachIdentifierMatchingSearch( search, [&](const gd::String& objectName, @@ -907,6 +997,11 @@ class GD_CORE_API ExpressionCompletionFinder description.SetCompletion(variableName); description.SetVariableType(variable.GetType()); completions.push_back(description); + + if (eagerlyCompleteIfExactMatch && variableName == search) { + AddEagerCompletionForVariableChildren( + variable, variableName, location); + } }, [&](const gd::NamedPropertyDescriptor& property) { // Ignore properties here. @@ -927,7 +1022,8 @@ class GD_CORE_API ExpressionCompletionFinder void AddCompletionsForAllIdentifiersMatchingSearch( const gd::String& search, const gd::String& type, - const ExpressionParserLocation& location) { + const ExpressionParserLocation& location, + bool eagerlyCompleteIfExactMatch = false) { projectScopedContainers.ForEachIdentifierMatchingSearch( search, [&](const gd::String& objectName, @@ -949,6 +1045,11 @@ class GD_CORE_API ExpressionCompletionFinder description.SetCompletion(variableName); description.SetVariableType(variable.GetType()); completions.push_back(description); + + if (eagerlyCompleteIfExactMatch && variableName == search) { + AddEagerCompletionForVariableChildren( + variable, variableName, location); + } }, [&](const gd::NamedPropertyDescriptor& property) { ExpressionCompletionDescription description( diff --git a/Core/GDCore/IDE/Events/ExpressionValidator.cpp b/Core/GDCore/IDE/Events/ExpressionValidator.cpp index 6202ea0a3684..4abffc3a1319 100644 --- a/Core/GDCore/IDE/Events/ExpressionValidator.cpp +++ b/Core/GDCore/IDE/Events/ExpressionValidator.cpp @@ -263,11 +263,17 @@ ExpressionValidator::Type ExpressionValidator::ValidateFunction( } if (gd::MetadataProvider::IsBadExpressionMetadata(metadata)) { - RaiseError("invalid_function_name", + if (function.functionName.empty()) { + RaiseError("invalid_function_name", + _("Enter the name of the function to call."), + function.location); + } else { + RaiseError("invalid_function_name", _("Cannot find an expression with this name: ") + function.functionName + "\n" + _("Double check that you've not made any typo in the name."), function.location); + } return returnType; } diff --git a/Core/GDCore/IDE/Events/ExpressionVariableParentFinder.h b/Core/GDCore/IDE/Events/ExpressionVariableParentFinder.h index c19894ddea19..f0572ab406e0 100644 --- a/Core/GDCore/IDE/Events/ExpressionVariableParentFinder.h +++ b/Core/GDCore/IDE/Events/ExpressionVariableParentFinder.h @@ -33,9 +33,9 @@ namespace gd { * to refer to the parent of a variable (which can be a VariablesContainer * or another Variable). */ -struct VariableOrVariablesContainer { - const gd::VariablesContainer* variablesContainer; - const gd::Variable* variable; +struct VariableAndItsParent { + const gd::VariablesContainer* parentVariablesContainer; + const gd::Variable* parentVariable; }; /** @@ -49,14 +49,14 @@ class GD_CORE_API ExpressionVariableParentFinder : public ExpressionParser2NodeWorker { public: - static VariableOrVariablesContainer GetLastParentOfNode( + static VariableAndItsParent GetLastParentOfNode( const gd::Platform& platform, const gd::ProjectScopedContainers& projectScopedContainers, gd::ExpressionNode& node) { gd::ExpressionVariableParentFinder typeFinder( platform, projectScopedContainers); node.Visit(typeFinder); - return typeFinder.lastParentOfNode; + return typeFinder.variableAndItsParent; } virtual ~ExpressionVariableParentFinder(){}; @@ -69,6 +69,7 @@ class GD_CORE_API ExpressionVariableParentFinder projectScopedContainers(projectScopedContainers_), variableNode(nullptr), thisIsALegacyPrescopedVariable(false), + bailOutBecauseEmptyVariableName(false), legacyPrescopedVariablesContainer(nullptr){}; void OnVisitSubExpressionNode(SubExpressionNode& node) override {} @@ -92,7 +93,7 @@ class GD_CORE_API ExpressionVariableParentFinder // containing it was identified in the FunctionCallNode. childVariableNames.insert(childVariableNames.begin(), node.name); if (legacyPrescopedVariablesContainer) - lastParentOfNode = WalkUntilLastParent( + variableAndItsParent = WalkUntilLastParent( *legacyPrescopedVariablesContainer, childVariableNames); } else { // Otherwise, the identifier is to be interpreted as usual: @@ -106,14 +107,14 @@ class GD_CORE_API ExpressionVariableParentFinder projectScopedContainers.GetObjectsContainersList() .GetObjectOrGroupVariablesContainer(node.name); if (variablesContainer) - lastParentOfNode = + variableAndItsParent = WalkUntilLastParent(*variablesContainer, childVariableNames); }, [&]() { // This is a variable. if (projectScopedContainers.GetVariablesContainersList().Has( node.name)) { - lastParentOfNode = WalkUntilLastParent( + variableAndItsParent = WalkUntilLastParent( projectScopedContainers.GetVariablesContainersList().Get( node.name), childVariableNames); @@ -133,6 +134,13 @@ class GD_CORE_API ExpressionVariableParentFinder } } void OnVisitVariableAccessorNode(VariableAccessorNode& node) override { + if (node.name.empty() && node.child) { + // A variable accessor should always have a name if it has a child (i.e: another accessor). + // While the parser may have generated an empty name, + // flag this so we avoid finding a wrong parent (and so, run the risk of giving + // wrong autocompletions). + bailOutBecauseEmptyVariableName = true; + } childVariableNames.insert(childVariableNames.begin(), node.name); if (node.parent) node.parent->Visit(*this); } @@ -159,7 +167,7 @@ class GD_CORE_API ExpressionVariableParentFinder node.identifierName); if (legacyPrescopedVariablesContainer) - lastParentOfNode = WalkUntilLastParent( + variableAndItsParent = WalkUntilLastParent( *legacyPrescopedVariablesContainer, childVariableNames); } else { @@ -178,7 +186,7 @@ class GD_CORE_API ExpressionVariableParentFinder projectScopedContainers.GetObjectsContainersList() .GetObjectOrGroupVariablesContainer(node.identifierName); if (variablesContainer) - lastParentOfNode = + variableAndItsParent = WalkUntilLastParent(*variablesContainer, childVariableNames); }, [&]() { @@ -189,7 +197,7 @@ class GD_CORE_API ExpressionVariableParentFinder if (projectScopedContainers.GetVariablesContainersList().Has( node.identifierName)) { - lastParentOfNode = WalkUntilLastParent( + variableAndItsParent = WalkUntilLastParent( projectScopedContainers.GetVariablesContainersList().Get( node.identifierName), childVariableNames); @@ -287,10 +295,13 @@ class GD_CORE_API ExpressionVariableParentFinder } private: - static VariableOrVariablesContainer WalkUntilLastParent( + VariableAndItsParent WalkUntilLastParent( const gd::Variable& variable, const std::vector& childVariableNames, size_t startIndex = 0) { + if (bailOutBecauseEmptyVariableName) + return {}; // Do not even attempt to find the parent if we had an issue when visiting nodes. + const gd::Variable* currentVariable = &variable; // Walk until size - 1 as we want the last parent. @@ -323,33 +334,35 @@ class GD_CORE_API ExpressionVariableParentFinder // Return the last parent of the chain of variables (so not the last variable // but the one before it). - return {.variable = currentVariable}; + return {.parentVariable = currentVariable}; } - static VariableOrVariablesContainer WalkUntilLastParent( + VariableAndItsParent WalkUntilLastParent( const gd::VariablesContainer& variablesContainer, const std::vector& childVariableNames) { + if (bailOutBecauseEmptyVariableName) + return {}; // Do not even attempt to find the parent if we had an issue when visiting nodes. if (childVariableNames.empty()) return {}; // There is no "parent" to the variables container itself. - if (childVariableNames.size() == 1) - return {// Only one child: the parent is the variables container itself. - .variablesContainer = &variablesContainer}; const gd::String& firstChildName = *childVariableNames.begin(); - if (!variablesContainer.Has(firstChildName)) { - // The child does not exist - there is no "parent". - return {}; - } - return WalkUntilLastParent( - variablesContainer.Get(firstChildName), childVariableNames, 1); +// TODO: move again? + const gd::Variable* variable = variablesContainer.Has(firstChildName) ? + &variablesContainer.Get(firstChildName) : nullptr; + if (childVariableNames.size() == 1 || !variable) + return {// Only one child: the parent is the variables container itself. + .parentVariablesContainer = &variablesContainer}; + + return WalkUntilLastParent(*variable, childVariableNames, 1); } gd::ExpressionNode* variableNode; std::vector childVariableNames; bool thisIsALegacyPrescopedVariable; + bool bailOutBecauseEmptyVariableName; const gd::VariablesContainer* legacyPrescopedVariablesContainer; - VariableOrVariablesContainer lastParentOfNode; + VariableAndItsParent variableAndItsParent; const gd::Platform& platform; const gd::ProjectScopedContainers& projectScopedContainers; diff --git a/Core/tests/ExpressionParser2.cpp b/Core/tests/ExpressionParser2.cpp index faf1606dfb43..15459681a14a 100644 --- a/Core/tests/ExpressionParser2.cpp +++ b/Core/tests/ExpressionParser2.cpp @@ -1607,6 +1607,21 @@ TEST_CASE("ExpressionParser2", "[common][events]") { } } + SECTION("Invalid object variables (empty variable name, extra dot)") { + { + auto node = + parser.ParseExpression("MySpriteObjects.."); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 2); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "A name should be entered after the dot."); + REQUIRE(validator.GetFatalErrors()[1]->GetMessage() == + "A name should be entered after the dot."); + } + } + SECTION("Invalid object variables (object group, partially existing variable)") { { auto node = @@ -1691,6 +1706,42 @@ TEST_CASE("ExpressionParser2", "[common][events]") { "An object variable or expression should be entered."); } } + SECTION("Invalid object variables (extra dot)") { + { + auto node = + parser.ParseExpression("MySpriteObject.MyVariable."); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "A name should be entered after the dot."); + } + } + SECTION("Invalid object variables (extra dot after brackets)") { + { + auto node = + parser.ParseExpression("MySpriteObject.MyVariable[\"MyChild\"]."); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "A name should be entered after the dot."); + } + } + SECTION("Invalid object variables (extra dot before brackets)") { + { + auto node = + parser.ParseExpression("MySpriteObject.MyVariable.[\"MyChild\"]"); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "A name should be entered after the dot."); + } + } SECTION("Valid property") { gd::PropertiesContainer propertiesContainer(gd::EventsFunctionsContainer::Extension); @@ -2157,6 +2208,14 @@ TEST_CASE("ExpressionParser2", "[common][events]") { dynamic_cast(*node); REQUIRE(identifierNode.identifierName == "MyObject"); REQUIRE(identifierNode.childIdentifierName == ""); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 2); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "A name should be entered after the dot."); + REQUIRE(validator.GetFatalErrors()[1]->GetMessage() == + "An object variable or expression should be entered."); } SECTION("Unfinished object function name of type string with parentheses") { @@ -2168,6 +2227,14 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(objectFunctionCall.objectName == "MyObject"); REQUIRE(objectFunctionCall.functionName == ""); REQUIRE(type == "string"); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 2); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "A name should be entered after the dot."); + REQUIRE(validator.GetFatalErrors()[1]->GetMessage() == + "Enter the name of the function to call."); } SECTION("Unfinished object function name of type number with parentheses") { @@ -2194,6 +2261,19 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(type == "number|string"); } + SECTION("Unfinished object function/variable name with multiple dots") { + auto node = parser.ParseExpression("MyObject.."); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 2); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "A name should be entered after the dot."); + REQUIRE(validator.GetFatalErrors()[1]->GetMessage() == + "A name should be entered after the dot."); + } + SECTION("Unfinished object behavior name") { auto node = parser.ParseExpression("MyObject.MyBehavior::"); REQUIRE(node != nullptr); @@ -2202,6 +2282,12 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(objectFunctionName.objectName == "MyObject"); REQUIRE(objectFunctionName.objectFunctionOrBehaviorName == "MyBehavior"); REQUIRE(objectFunctionName.behaviorFunctionName == ""); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "An opening parenthesis was expected here to call a function."); } SECTION("Unfinished object behavior name of type string with parentheses") { @@ -2214,6 +2300,12 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(objectFunctionName.behaviorName == "MyBehavior"); REQUIRE(objectFunctionName.functionName == ""); REQUIRE(type == "string"); + + gd::ExpressionValidator validator(platform, projectScopedContainers, "number"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "Enter the name of the function to call."); } SECTION("Unfinished object behavior name of type number with parentheses") { @@ -2499,10 +2591,8 @@ TEST_CASE("ExpressionParser2", "[common][events]") { node->Visit(validator); REQUIRE(validator.GetFatalErrors().size() == 1); - // TODO: The error message could be improved REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == - "Cannot find an expression with this name: \nDouble " - "check that you've not made any typo in the name."); + "Enter the name of the function to call."); REQUIRE(validator.GetFatalErrors()[0]->GetStartPosition() == 0); REQUIRE(validator.GetFatalErrors()[0]->GetEndPosition() == 25); } @@ -2838,10 +2928,10 @@ TEST_CASE("ExpressionParser2", "[common][events]") { // Also check the ability to find the last parent of the variables: auto lastParentOfVariable1Node = gd::ExpressionVariableParentFinder::GetLastParentOfNode( platform, projectScopedContainers, variable1Node); - REQUIRE(lastParentOfVariable1Node.variablesContainer == &mySpriteObject.GetVariables()); + REQUIRE(lastParentOfVariable1Node.parentVariablesContainer == &mySpriteObject.GetVariables()); auto lastParentOfVariable2Node = gd::ExpressionVariableParentFinder::GetLastParentOfNode( platform, projectScopedContainers, variable2Node); - REQUIRE(lastParentOfVariable2Node.variablesContainer == &mySpriteObject2.GetVariables()); + REQUIRE(lastParentOfVariable2Node.parentVariablesContainer == &mySpriteObject2.GetVariables()); gd::ExpressionValidator validator(platform, projectScopedContainers, "string"); node->Visit(validator); @@ -2877,10 +2967,10 @@ TEST_CASE("ExpressionParser2", "[common][events]") { // Also check the ability to find the last parent of the variables: auto lastParentOfVariable1Node = gd::ExpressionVariableParentFinder::GetLastParentOfNode( platform, projectScopedContainers, variable1Node); - REQUIRE(lastParentOfVariable1Node.variablesContainer == &mySpriteObject.GetVariables()); + REQUIRE(lastParentOfVariable1Node.parentVariablesContainer == &mySpriteObject.GetVariables()); auto lastParentOfVariable2Node = gd::ExpressionVariableParentFinder::GetLastParentOfNode( platform, projectScopedContainers, variable2Node); - REQUIRE(lastParentOfVariable2Node.variablesContainer == &mySpriteObject.GetVariables()); + REQUIRE(lastParentOfVariable2Node.parentVariablesContainer == &mySpriteObject.GetVariables()); gd::ExpressionValidator validator(platform, projectScopedContainers, "string"); node->Visit(validator); @@ -2906,7 +2996,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { // Also check the ability to find the last parent of the variable: auto lastParentOfVariable1Node = gd::ExpressionVariableParentFinder::GetLastParentOfNode( platform, projectScopedContainers, variable1Node); - REQUIRE(lastParentOfVariable1Node.variablesContainer == &mySpriteObject.GetVariables()); + REQUIRE(lastParentOfVariable1Node.parentVariablesContainer == &mySpriteObject.GetVariables()); gd::ExpressionValidator validator(platform, projectScopedContainers, "number"); node->Visit(validator); @@ -2933,7 +3023,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { // Also check the ability to find the last parent of the variable: auto lastParentOfVariable1Node = gd::ExpressionVariableParentFinder::GetLastParentOfNode( platform, projectScopedContainers, variable1Node); - REQUIRE(lastParentOfVariable1Node.variable == &mySpriteObject.GetVariables().Get("MyVariable")); + REQUIRE(lastParentOfVariable1Node.parentVariable == &mySpriteObject.GetVariables().Get("MyVariable")); gd::ExpressionValidator validator(platform, projectScopedContainers, "number"); node->Visit(validator); diff --git a/GDevelop.js/__tests__/ExpressionCompletionFinder.spec.js b/GDevelop.js/__tests__/ExpressionCompletionFinder.spec.js index 7ddec8f943c5..048d7469a1c4 100644 --- a/GDevelop.js/__tests__/ExpressionCompletionFinder.spec.js +++ b/GDevelop.js/__tests__/ExpressionCompletionFinder.spec.js @@ -82,6 +82,9 @@ describe('gd.ExpressionCompletionFinder', function () { structureChild1.getChild('Child1StructureChild1'); structureChild1.getChild('Child1StructureChild2'); structureChild1.getChild('Child1StructureChild3'); + structureChild1.getChild( + 'Child1 Unsafe Because of Spaces so not listed' + ); // With a child array, containing 2 structures and a number. // Useful to test completion of the array children. @@ -178,6 +181,21 @@ describe('gd.ExpressionCompletionFinder', function () { `); }); + test('Root variables (eager completion of children)', () => { + expect( + testCompletions('number', 'MySpriteObject.MyObjectVariableStructure|') + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, MyObjectVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 3, no prefix, MyObjectVariableStructure.Child1Structure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 4, no prefix, MyObjectVariableStructure.Child2Array, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, MyObjectVariableStructure.Child3Boolean, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 1, no type, 1, MyObjectVariableStructure, no completion, MySpriteObject, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 2, number, 1, MyObjectVariableStructure, no completion, MySpriteObject, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + test('Root variables (objectvar parameter)', () => { expect(testCompletions('number', 'MySpriteObject.Variable(MyObject|')) .toMatchInlineSnapshot(` @@ -189,6 +207,22 @@ describe('gd.ExpressionCompletionFinder', function () { `); }); + test('Root variables (objectvar parameter, eager completion of children)', () => { + expect( + testCompletions( + 'number', + 'MySpriteObject.Variable(MyObjectVariableStructure|' + ) + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, MyObjectVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 3, no prefix, MyObjectVariableStructure.Child1Structure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 4, no prefix, MyObjectVariableStructure.Child2Array, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, MyObjectVariableStructure.Child3Boolean, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + test('Structure, 1 level', () => { // Completions of children: expect( @@ -229,6 +263,24 @@ describe('gd.ExpressionCompletionFinder', function () { `); }); + test('Structure, 1 level (eager completion of children)', () => { + expect( + testCompletions( + 'number', + 'MySpriteObject.MyObjectVariableStructure.Child1Structure|' + ) + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, Child1Structure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1Structure.Child1StructureChild1, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1Structure.Child1StructureChild2, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1Structure.Child1StructureChild3, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 4, no prefix, Child2Array, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child3Boolean, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + test('Structure, 1 level (objectvar parameter)', () => { expect( testCompletions( @@ -256,6 +308,24 @@ describe('gd.ExpressionCompletionFinder', function () { `); }); + test('Structure, 1 level (objectvar parameter, eager completion of children)', () => { + expect( + testCompletions( + 'number', + 'MySpriteObject.Variable(MyObjectVariableStructure.Child1Structure|' + ) + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, Child1Structure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1Structure.Child1StructureChild1, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1Structure.Child1StructureChild2, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1Structure.Child1StructureChild3, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 4, no prefix, Child2Array, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child3Boolean, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + test('Structure, 2 levels', () => { expect( testCompletions( @@ -490,7 +560,7 @@ describe('gd.ExpressionCompletionFinder', function () { `); }); - test('Structure, 1 level (scenevar parameter)', () => { + test('Structure, 1 level (objectvar parameter)', () => { // Completions of children: expect( testCompletions( @@ -690,13 +760,26 @@ describe('gd.ExpressionCompletionFinder', function () { describe('Scene variables completion', () => { test('Root variables', () => { expect(testCompletions('number', 'MyVariab|')).toMatchInlineSnapshot(` - [ - "{ 3, no type, 1, no prefix, MyVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", - "{ 3, no type, 1, no prefix, MyVariable2, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", - "{ 3, no type, 3, no prefix, MyVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", - "{ 2, number, 1, MyVariab, no completion, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", - ] - `); + [ + "{ 3, no type, 1, no prefix, MyVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, MyVariable2, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 3, no prefix, MyVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 2, number, 1, MyVariab, no completion, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + + test('Root variables (eager completion of children)', () => { + expect(testCompletions('number', 'MyVariableStructure|')) + .toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, MyVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 3, no prefix, MyVariableStructure.Child1Structure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 4, no prefix, MyVariableStructure.Child2Array, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, MyVariableStructure.Child3Boolean, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 2, number, 1, MyVariableStructure, no completion, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); }); test('Root variables (scenevar parameter)', () => { @@ -710,6 +793,18 @@ describe('gd.ExpressionCompletionFinder', function () { `); }); + test('Root variables (scenevar parameter, eager completion of children)', () => { + expect(testCompletions('number', 'Variable(MyVariableStructure|)')) + .toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, MyVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 3, no prefix, MyVariableStructure.Child1Structure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 4, no prefix, MyVariableStructure.Child2Array, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, MyVariableStructure.Child3Boolean, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + test('Structure, 1 level', () => { expect(testCompletions('number', 'MyVariableStructure.|')) .toMatchInlineSnapshot(` @@ -728,6 +823,21 @@ describe('gd.ExpressionCompletionFinder', function () { `); }); + test('Structure, 1 level (eager completion of children)', () => { + expect( + testCompletions('number', 'MyVariableStructure.Child1Structure|') + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, Child1Structure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1Structure.Child1StructureChild1, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1Structure.Child1StructureChild2, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1Structure.Child1StructureChild3, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 4, no prefix, Child2Array, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child3Boolean, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + test('Structure, 1 level (scenevar parameter)', () => { expect(testCompletions('number', 'Variable(MyVariableStructure.|')) .toMatchInlineSnapshot(` @@ -746,6 +856,24 @@ describe('gd.ExpressionCompletionFinder', function () { `); }); + test('Structure, 1 level (scenevar parameter, eager completion of children)', () => { + expect( + testCompletions( + 'number', + 'Variable(MyVariableStructure.Child1Structure|' + ) + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, Child1Structure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1Structure.Child1StructureChild1, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1Structure.Child1StructureChild2, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1Structure.Child1StructureChild3, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 4, no prefix, Child2Array, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child3Boolean, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + test('Structure, 2 levels', () => { expect( testCompletions('number', 'MyVariableStructure.Child1Structure.|')