/* * GDevelop Core * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights * reserved. This project is released under the MIT License. */ #include "GDCore/IDE/Events/EventsVariableReplacer.h" #include #include #include #include #include #include "GDCore/Events/Event.h" #include "GDCore/Events/EventsList.h" #include "GDCore/Events/Parsers/ExpressionParser2NodePrinter.h" #include "GDCore/Events/Parsers/ExpressionParser2NodeWorker.h" #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" #include "GDCore/IDE/WholeProjectRefactorer.h" #include "GDCore/Project/Layout.h" #include "GDCore/Project/Project.h" #include "GDCore/Project/ProjectScopedContainers.h" #include "GDCore/Project/VariablesContainer.h" #include "GDCore/String.h" #include "GDCore/Tools/Log.h" namespace gd { VariablesContainer EventsVariableReplacer::nullVariablesContainer; /** * \brief Go through the nodes and rename variables, * or signal if the instruction must be renamed if a removed variable is used. * * \see gd::ExpressionParser2 */ class GD_CORE_API ExpressionVariableReplacer : public ExpressionParser2NodeWorker { public: ExpressionVariableReplacer( const gd::Platform& platform_, const gd::ProjectScopedContainers& projectScopedContainers_, const VariablesRenamingChangesetNode& variablesRenamingChangesetRoot_, const std::unordered_set& removedVariableNames_, const gd::VariablesContainer& targetVariablesContainer_, const gd::String &groupName_, const gd::String &forcedInitialObjectName) : hasDoneRenaming(false), removedVariableUsed(false), platform(platform_), projectScopedContainers(projectScopedContainers_), forcedVariablesContainer(nullptr), forcedObjectName(forcedInitialObjectName), variablesRenamingChangesetRoot(variablesRenamingChangesetRoot_), removedVariableNames(removedVariableNames_), targetVariablesContainer(targetVariablesContainer_), targetGroupName(groupName_){}; virtual ~ExpressionVariableReplacer(){}; void SetForcedInitialVariablesContainer( const gd::VariablesContainer* forcedInitialVariablesContainer_) { forcedVariablesContainer = forcedInitialVariablesContainer_; } bool HasDoneRenaming() const { return hasDoneRenaming; } bool IsRemovedVariableUsed() const { return removedVariableUsed; } protected: void OnVisitSubExpressionNode(SubExpressionNode& node) override { node.expression->Visit(*this); } void OnVisitOperatorNode(OperatorNode& node) override { node.leftHandSide->Visit(*this); node.rightHandSide->Visit(*this); } void OnVisitUnaryOperatorNode(UnaryOperatorNode& node) override { node.factor->Visit(*this); } void OnVisitNumberNode(NumberNode& node) override {} void OnVisitTextNode(TextNode& node) override {} void OnVisitVariableNode(VariableNode& node) override { // The node represents a variable or an object name on which a variable // will be accessed. if (forcedVariablesContainer) { const gd::String oldVariableName = node.name; PushVariablesRenamingChangesetRoot(); // A scope was forced. Honor it: it means this node represents a variable // of the forced variables container. if (forcedVariablesContainer == &targetVariablesContainer || IsTargetingObjectGroup(forcedObjectName)) { RenameOrRemoveVariableOfTargetVariableContainer(node.name); } if (node.child) { bool hasBeenPushed = PushVariablesRenamingChangesetNodeForVariable(oldVariableName); node.child->Visit(*this); PopVariablesRenamingChangesetNode(hasBeenPushed); } PopVariablesRenamingChangesetNode(true); return; } // Match the potential *new* name of the variable, because refactorings are // done after changes in the variables container. projectScopedContainers.MatchIdentifierWithName( node.name, [&]() { // This represents an object. // Remember the object name. objectNameToUseForVariableAccessor = node.name; if (node.child) node.child->Visit(*this); objectNameToUseForVariableAccessor = ""; }, [&]() { // This is a variable. if (&projectScopedContainers.GetVariablesContainersList() .GetVariablesContainerFromVariableOrPropertyOrParameterName(node.name) == &targetVariablesContainer) { // The node represents a variable, that can come from the target // (because the target is in the scope), replace or remove it: PushVariablesRenamingChangesetRoot(); RenameVariableAndVisitChild(node.name, node.child.get()); PopVariablesRenamingChangesetNode(true); } else { if (node.child) { PushVariablesRenamingChangesetNodeForIgnoredVariables(); node.child->Visit(*this); PopVariablesRenamingChangesetNode(true); } } }, [&]() { // This is a property. if (node.child) node.child->Visit(*this); }, [&]() { // This is a parameter. if (node.child) node.child->Visit(*this); }, [&]() { // This is something else. if (node.child) node.child->Visit(*this); }); } void OnVisitVariableAccessorNode(VariableAccessorNode& node) override { auto& objectsContainersList = projectScopedContainers.GetObjectsContainersList(); if (!objectNameToUseForVariableAccessor.empty()) { // This is always true because MatchIdentifierWithName is used to get // objectNameToUseForVariableAccessor. if (objectsContainersList.HasObjectOrGroupVariablesContainer( objectNameToUseForVariableAccessor, targetVariablesContainer) || IsTargetingObjectGroup(objectNameToUseForVariableAccessor)) { objectNameToUseForVariableAccessor = ""; // The node represents an object variable, and this object variables are // the target. Do the replacement or removals: PushVariablesRenamingChangesetRoot(); RenameVariableAndVisitChild(node.name, node.child.get()); PopVariablesRenamingChangesetNode(true); } } else { RenameVariableAndVisitChild(node.name, node.child.get()); } } void OnVisitVariableBracketAccessorNode( VariableBracketAccessorNode& node) override { objectNameToUseForVariableAccessor = ""; // TODO Literal expressions could be checked to handle renaming in // `expression` and in `child`. node.expression->Visit(*this); if (node.child) { PushVariablesRenamingChangesetNodeForIgnoredVariables(); node.child->Visit(*this); PopVariablesRenamingChangesetNode(true); } } void OnVisitIdentifierNode(IdentifierNode &node) override { auto renameVariableAndChild = [this, &node]() { PushVariablesRenamingChangesetRoot(); const gd::String oldVariableName = node.identifierName; RenameOrRemoveVariableOfTargetVariableContainer(node.identifierName); if (!node.childIdentifierName.empty()) { bool hasBeenPushed = PushVariablesRenamingChangesetNodeForVariable(oldVariableName); RenameOrRemoveVariableOfTargetVariableContainer( node.childIdentifierName); PopVariablesRenamingChangesetNode(hasBeenPushed); } PopVariablesRenamingChangesetNode(true); }; auto& objectsContainersList = projectScopedContainers.GetObjectsContainersList(); // The node represents a variable or an object variable in an expression // (and if it's a variable reference or a value does not have any importance // here). if (forcedVariablesContainer) { // A scope was forced. Honor it: it means this node represents a variable // of the forced variables container. if (forcedVariablesContainer == &targetVariablesContainer || IsTargetingObjectGroup(forcedObjectName)) { renameVariableAndChild(); } return; } // Match the potential *new* name of the variable, because refactorings are // done after changes in the variables container. projectScopedContainers.MatchIdentifierWithName( node.identifierName, [&]() { // This represents an object. if (objectsContainersList.HasObjectOrGroupVariablesContainer( node.identifierName, targetVariablesContainer) || IsTargetingObjectGroup(node.identifierName)) { // The node represents an object variable, and this object variables // are the target. Do the replacement or removals: PushVariablesRenamingChangesetRoot(); RenameOrRemoveVariableOfTargetVariableContainer( node.childIdentifierName); PopVariablesRenamingChangesetNode(true); } }, [&]() { // This is a variable. if (&projectScopedContainers.GetVariablesContainersList() .GetVariablesContainerFromVariableOrPropertyOrParameterName( node.identifierName) == &targetVariablesContainer) { // The node represents a variable, that can come from the target // (because the target is in the scope), replace or remove it: renameVariableAndChild(); } }, [&]() { // This is a property. }, [&]() { // This is a parameter. }, [&]() { // This is something else. }); } void OnVisitObjectFunctionNameNode(ObjectFunctionNameNode& node) override {} void OnVisitFunctionCallNode(FunctionCallNode& node) override { const gd::ExpressionMetadata& metadata = MetadataProvider::GetFunctionCallMetadata( platform, projectScopedContainers.GetObjectsContainersList(), node); for (size_t parameterIndex = 0; parameterIndex < node.parameters.size(); ++parameterIndex) { const gd::ParameterMetadata* parameterMetadata = MetadataProvider::GetFunctionCallParameterMetadata( platform, projectScopedContainers.GetObjectsContainersList(), node, parameterIndex); // Handle legacy pre-scoped variable parameters: in this case, we // force the "scope" at which starts the evalution of variables. if (parameterMetadata && parameterMetadata->GetValueTypeMetadata() .IsLegacyPreScopedVariable()) { const gd::VariablesContainer *oldForcedVariablesContainer = forcedVariablesContainer; const gd::String &oldForcedObjectName = forcedObjectName; forcedVariablesContainer = nullptr; forcedObjectName = ""; if (parameterMetadata->GetType() == "globalvar") { forcedVariablesContainer = projectScopedContainers.GetVariablesContainersList() .GetTopMostVariablesContainer(); } else if (parameterMetadata->GetType() == "scenevar") { forcedVariablesContainer = projectScopedContainers.GetVariablesContainersList() .GetBottomMostVariablesContainer(); } else if (parameterMetadata->GetType() == "objectvar") { auto objectName = gd::ExpressionVariableOwnerFinder::GetObjectName( platform, projectScopedContainers.GetObjectsContainersList(), node.objectName, *node.parameters[parameterIndex].get()); forcedVariablesContainer = projectScopedContainers.GetObjectsContainersList() .GetObjectOrGroupVariablesContainer(objectName); forcedObjectName = objectName; } node.parameters[parameterIndex]->Visit(*this); forcedVariablesContainer = oldForcedVariablesContainer; forcedObjectName = oldForcedObjectName; } else { // For any other parameter, there is no special treatment being needed. node.parameters[parameterIndex]->Visit(*this); } } } void OnVisitEmptyNode(EmptyNode& node) override {} private: bool hasDoneRenaming; bool removedVariableUsed; bool IsTargetingObjectGroup(const gd::String &objectGroupName) { return !targetGroupName.empty() && objectGroupName == targetGroupName; } bool RenameOrRemoveVariableOfTargetVariableContainer( gd::String& variableName) { const auto *currentVariablesRenamingChangesetNode = GetCurrentVariablesRenamingChangesetNode(); if (!currentVariablesRenamingChangesetNode) { return false; } const auto &oldToNewVariableNames = currentVariablesRenamingChangesetNode->oldToNewVariableNames; if (oldToNewVariableNames.count(variableName) >= 1) { variableName = oldToNewVariableNames.find(variableName)->second; hasDoneRenaming = true; return true; } else if ( // Only the root variable is checked for removing. currentVariablesRenamingChangesetNode == &variablesRenamingChangesetRoot && removedVariableNames.count(variableName) >= 1) { removedVariableUsed = true; return true; } return false; // Nothing was changed or done. } void RenameVariableAndVisitChild(gd::String &variableName, ExpressionNode *childNode) { // `variableName` is modified by // `RenameOrRemoveVariableOfTargetVariableContainer`. const gd::String oldVariableName = variableName; RenameOrRemoveVariableOfTargetVariableContainer(variableName); if (childNode) { bool hasBeenPushed = PushVariablesRenamingChangesetNodeForVariable(oldVariableName); childNode->Visit(*this); PopVariablesRenamingChangesetNode(hasBeenPushed); } } void PushVariablesRenamingChangesetRoot() { variablesRenamingChangesetNodeStack.push_back(&variablesRenamingChangesetRoot); } void PushVariablesRenamingChangesetNodeForIgnoredVariables() { variablesRenamingChangesetNodeStack.push_back(nullptr); } const gd::VariablesRenamingChangesetNode *GetCurrentVariablesRenamingChangesetNode() { return variablesRenamingChangesetNodeStack.size() == 0 ? &variablesRenamingChangesetRoot : variablesRenamingChangesetNodeStack [variablesRenamingChangesetNodeStack.size() - 1]; } bool PushVariablesRenamingChangesetNodeForVariable(const gd::String& variableName) { const auto *currentVariablesRenamingChangesetNode = GetCurrentVariablesRenamingChangesetNode(); if (!currentVariablesRenamingChangesetNode) { // There were already no more change on a parent. return false; } const auto &childVariablesRenamingChangesetNodeItr = currentVariablesRenamingChangesetNode->modifiedVariables.find( variableName); if (childVariablesRenamingChangesetNodeItr == currentVariablesRenamingChangesetNode->modifiedVariables.end()) { // There is no more change on the current variable child. variablesRenamingChangesetNodeStack.push_back(nullptr); } else { variablesRenamingChangesetNodeStack.push_back( childVariablesRenamingChangesetNodeItr->second.get()); } return true; } void PopVariablesRenamingChangesetNode(bool hasBeenPushed) { if (hasBeenPushed) { variablesRenamingChangesetNodeStack.pop_back(); } } // Scope: const gd::Platform& platform; const gd::ProjectScopedContainers& projectScopedContainers; const gd::VariablesContainer* forcedVariablesContainer; gd::String forcedObjectName; // Renaming or removing to do: const gd::VariablesContainer& targetVariablesContainer; /** * Groups don't have VariablesContainer, so `targetVariablesContainer` will be * pointing to `nullVariablesContainer` and the group name is use instead to * check which variable accesses to modify in expressions. */ const gd::String& targetGroupName; const VariablesRenamingChangesetNode &variablesRenamingChangesetRoot; const std::unordered_set& removedVariableNames; gd::String objectNameToUseForVariableAccessor; std::vector variablesRenamingChangesetNodeStack; }; const gd::VariablesContainer* EventsVariableReplacer::FindForcedVariablesContainerIfAny( const gd::String& type, const gd::String& lastObjectName) { // Handle legacy pre-scoped variable parameters: in this case, we // force the "scope" at which starts the evaluation of variables. if (type == "objectvar") { return GetProjectScopedContainers() .GetObjectsContainersList() .GetObjectOrGroupVariablesContainer(lastObjectName); } else if (type == "globalvar") { return GetProjectScopedContainers() .GetVariablesContainersList() .GetTopMostVariablesContainer(); } else if (type == "scenevar") { return GetProjectScopedContainers() .GetVariablesContainersList() .GetBottomMostVariablesContainer(); } return nullptr; } bool EventsVariableReplacer::DoVisitInstruction(gd::Instruction& instruction, bool isCondition) { const auto& metadata = isCondition ? gd::MetadataProvider::GetConditionMetadata( platform, instruction.GetType()) : gd::MetadataProvider::GetActionMetadata( platform, instruction.GetType()); bool shouldDeleteInstruction = false; gd::ParameterMetadataTools::IterateOverParametersWithIndex( instruction.GetParameters(), metadata.GetParameters(), [&](const gd::ParameterMetadata ¶meterMetadata, const gd::Expression ¶meterValue, size_t parameterIndex, const gd::String &lastObjectName, size_t lastObjectIndex) { const gd::String& type = parameterMetadata.GetType(); if (!gd::ParameterMetadata::IsExpression("variable", type) && !gd::ParameterMetadata::IsExpression("number", type) && !gd::ParameterMetadata::IsExpression("string", type)) return; // Not an expression that can contain variables. auto node = parameterValue.GetRootNode(); if (node) { ExpressionVariableReplacer renamer(platform, GetProjectScopedContainers(), variablesRenamingChangesetRoot, removedVariableNames, targetVariablesContainer, targetGroupName, type == "objectvar" ? lastObjectName : ""); renamer.SetForcedInitialVariablesContainer( FindForcedVariablesContainerIfAny(type, lastObjectName)); node->Visit(renamer); if (renamer.IsRemovedVariableUsed()) { shouldDeleteInstruction = true; } else if (renamer.HasDoneRenaming()) { instruction.SetParameter( parameterIndex, ExpressionParser2NodePrinter::PrintNode(*node)); } } }); return shouldDeleteInstruction; } bool EventsVariableReplacer::DoVisitEventExpression( gd::Expression& expression, const gd::ParameterMetadata& metadata) { const gd::String& type = metadata.GetType(); if (!gd::ParameterMetadata::IsExpression("variable", type) && !gd::ParameterMetadata::IsExpression("number", type) && !gd::ParameterMetadata::IsExpression("string", type)) return false; // Not an expression that can contain variables. auto node = expression.GetRootNode(); if (node) { ExpressionVariableReplacer renamer(platform, GetProjectScopedContainers(), variablesRenamingChangesetRoot, removedVariableNames, targetVariablesContainer, targetGroupName, ""); renamer.SetForcedInitialVariablesContainer( FindForcedVariablesContainerIfAny(type, "")); node->Visit(renamer); if (renamer.IsRemovedVariableUsed()) { return true; } else if (renamer.HasDoneRenaming()) { expression = ExpressionParser2NodePrinter::PrintNode(*node); } } return false; } EventsVariableReplacer::~EventsVariableReplacer() {} } // namespace gd