diff --git a/Core/GDCore/Events/Parsers/ExpressionParser2Node.h b/Core/GDCore/Events/Parsers/ExpressionParser2Node.h index 402ea59b145d..4f7f36d43c19 100644 --- a/Core/GDCore/Events/Parsers/ExpressionParser2Node.h +++ b/Core/GDCore/Events/Parsers/ExpressionParser2Node.h @@ -59,6 +59,7 @@ struct GD_CORE_API ExpressionParserError { UnknownParameterType, MissingBehavior, VariableNameCollision, + DeprecatedExpression, }; ExpressionParserError(gd::ExpressionParserError::ErrorType type_, @@ -78,7 +79,7 @@ struct GD_CORE_API ExpressionParserError { location(startPosition_, endPosition_){}; virtual ~ExpressionParserError(){}; - gd::ExpressionParserError::ErrorType GetType() { return type; } + gd::ExpressionParserError::ErrorType GetType() const { return type; } const gd::String &GetMessage() { return message; } const gd::String &GetObjectName() { return objectName; } const gd::String &GetActualValue() { return actualValue; } diff --git a/Core/GDCore/Extensions/Metadata/AbstractFunctionMetadata.h b/Core/GDCore/Extensions/Metadata/AbstractFunctionMetadata.h index 71be1bda2346..fc9b3059ac1c 100644 --- a/Core/GDCore/Extensions/Metadata/AbstractFunctionMetadata.h +++ b/Core/GDCore/Extensions/Metadata/AbstractFunctionMetadata.h @@ -76,6 +76,13 @@ class GD_CORE_API AbstractFunctionMetadata { */ virtual AbstractFunctionMetadata &SetHidden() = 0; + /** + * \brief Set the deprecation message that explains why the function + * is deprecated and what to use instead. + */ + virtual AbstractFunctionMetadata & + SetDeprecationMessage(const gd::String &message) = 0; + /** * Set that the instruction is private - it can't be used outside of the * object/ behavior that it is attached too. diff --git a/Core/GDCore/Extensions/Metadata/ExpressionMetadata.h b/Core/GDCore/Extensions/Metadata/ExpressionMetadata.h index 00bd3d4a46ad..bde656dd768f 100644 --- a/Core/GDCore/Extensions/Metadata/ExpressionMetadata.h +++ b/Core/GDCore/Extensions/Metadata/ExpressionMetadata.h @@ -72,6 +72,26 @@ class GD_CORE_API ExpressionMetadata : public gd::AbstractFunctionMetadata { */ ExpressionMetadata& SetHidden() override; + /** + * \brief Set the deprecation message that explains why the expression + * is deprecated and what to use instead. + */ + ExpressionMetadata& SetDeprecationMessage(const gd::String& message) override { + deprecationMessage = message; + return *this; + } + + /** + * \brief Get the deprecation message that explains why the expression + * is deprecated and what to use instead. + */ + const gd::String& GetDeprecationMessage() const { return deprecationMessage; } + + /** + * \brief Check if the expression is deprecated. + */ + bool IsDeprecated() const { return !deprecationMessage.empty(); } + /** * \brief Set the group of the instruction in the IDE. */ @@ -369,6 +389,7 @@ class GD_CORE_API ExpressionMetadata : public gd::AbstractFunctionMetadata { bool isPrivate; gd::String requiredBaseObjectCapability; gd::String relevantContext; + gd::String deprecationMessage; gd::ParameterMetadataContainer parameters; }; diff --git a/Core/GDCore/Extensions/Metadata/InstructionMetadata.h b/Core/GDCore/Extensions/Metadata/InstructionMetadata.h index f88af522fdc6..c52b358a7197 100644 --- a/Core/GDCore/Extensions/Metadata/InstructionMetadata.h +++ b/Core/GDCore/Extensions/Metadata/InstructionMetadata.h @@ -200,6 +200,21 @@ class GD_CORE_API InstructionMetadata : public gd::AbstractFunctionMetadata { return *this; } + /** + * \brief Set the deprecation message that explains why the instruction + * is deprecated and what to use instead. + */ + InstructionMetadata &SetDeprecationMessage(const gd::String &message) override { + deprecationMessage = message; + return *this; + } + + /** + * \brief Get the deprecation message that explains why the instruction + * is deprecated and what to use instead. + */ + const gd::String &GetDeprecationMessage() const { return deprecationMessage; } + /** * \brief Set the group of the instruction in the IDE. */ @@ -586,6 +601,7 @@ class GD_CORE_API InstructionMetadata : public gd::AbstractFunctionMetadata { bool isBehaviorInstruction; gd::String requiredBaseObjectCapability; gd::String relevantContext; + gd::String deprecationMessage; }; } // namespace gd diff --git a/Core/GDCore/Extensions/Metadata/MultipleInstructionMetadata.h b/Core/GDCore/Extensions/Metadata/MultipleInstructionMetadata.h index d1a13423eced..d96707bb0836 100644 --- a/Core/GDCore/Extensions/Metadata/MultipleInstructionMetadata.h +++ b/Core/GDCore/Extensions/Metadata/MultipleInstructionMetadata.h @@ -113,6 +113,18 @@ class GD_CORE_API MultipleInstructionMetadata : public AbstractFunctionMetadata return *this; }; + /** + * \brief Set the deprecation message that explains why the instruction + * is deprecated and what to use instead. + */ + MultipleInstructionMetadata &SetDeprecationMessage( + const gd::String &message) override { + if (expression) expression->SetDeprecationMessage(message); + if (condition) condition->SetDeprecationMessage(message); + if (action) action->SetDeprecationMessage(message); + return *this; + } + /** * \see gd::InstructionMetadata::SetRequiresBaseObjectCapability */ diff --git a/Core/GDCore/IDE/Events/ExpressionValidator.cpp b/Core/GDCore/IDE/Events/ExpressionValidator.cpp index da259aec477f..c86a3f7c2a4a 100644 --- a/Core/GDCore/IDE/Events/ExpressionValidator.cpp +++ b/Core/GDCore/IDE/Events/ExpressionValidator.cpp @@ -295,6 +295,16 @@ ExpressionValidator::Type ExpressionValidator::ValidateFunction( return returnType; } + // Check if the expression is deprecated + if (metadata.IsDeprecated()) { + gd::String deprecationMessage = metadata.GetDeprecationMessage(); + RaiseError(gd::ExpressionParserError::ErrorType::DeprecatedExpression, + _("This expression is deprecated.") + + (deprecationMessage.empty() ? "" : " " + deprecationMessage), + function.location, + /*isFatal=*/false); + } + // Validate the type of the function if (returnType == Type::Number) { if (parentType == Type::String) { diff --git a/Core/GDCore/IDE/InstructionValidator.cpp b/Core/GDCore/IDE/InstructionValidator.cpp index d32a731e2290..c54efef066c5 100644 --- a/Core/GDCore/IDE/InstructionValidator.cpp +++ b/Core/GDCore/IDE/InstructionValidator.cpp @@ -5,6 +5,7 @@ */ #include "InstructionValidator.h" +#include "GDCore/Events/Parsers/ExpressionParser2Node.h" #include "GDCore/Extensions/Metadata/AbstractFunctionMetadata.h" #include "GDCore/Extensions/Metadata/BehaviorMetadata.h" #include "GDCore/Extensions/Metadata/InstructionMetadata.h" @@ -24,25 +25,31 @@ namespace gd { -bool InstructionValidator::IsParameterValid( +ParameterValidationResult InstructionValidator::ValidateParameter( const gd::Platform &platform, const gd::ProjectScopedContainers projectScopedContainers, const gd::Instruction &instruction, const InstructionMetadata &metadata, std::size_t parameterIndex, const gd::String &value) { + ParameterValidationResult result; + if (parameterIndex >= instruction.GetParametersCount() || parameterIndex >= metadata.GetParametersCount()) { - return false; + result.isValid = false; + return result; } + const auto ¶meterMetadata = metadata.GetParameter(parameterIndex); // TODO Remove the ternary when all parameter declarations use // "number" instead of "expression". const auto ¶meterType = parameterMetadata.GetType() == "expression" ? "number" : parameterMetadata.GetType(); + bool shouldNotBeValidated = parameterType == "layer" && value.empty(); if (shouldNotBeValidated) { - return true; + return result; // Valid by default, no deprecation warning } + if (gd::ParameterMetadata::IsExpression("number", parameterType) || gd::ParameterMetadata::IsExpression("string", parameterType) || gd::ParameterMetadata::IsExpression("variable", parameterType)) { @@ -52,13 +59,26 @@ bool InstructionValidator::IsParameterValid( parameterType, parameterMetadata.GetExtraInfo()); expressionNode.Visit(expressionValidator); - if (!expressionValidator.GetAllErrors().empty()) { - return false; + + // Check for fatal errors (validation) + if (!expressionValidator.GetFatalErrors().empty()) { + result.isValid = false; + } + + // Check for deprecation warnings in the same pass + const auto &allErrors = expressionValidator.GetAllErrors(); + for (const auto *error : allErrors) { + if (error->GetType() == + gd::ExpressionParserError::ErrorType::DeprecatedExpression) { + result.hasDeprecationWarning = true; + break; + } } + // 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 (parameterType == "objectvar" && + 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 @@ -72,7 +92,7 @@ bool InstructionValidator::IsParameterValid( objectName, gd::InstructionValidator::GetRootVariableName(variableName)) == gd::ObjectsContainersList::DoesNotExist) { - return false; + result.isValid = false; } } } else if (gd::ParameterMetadata::IsObject(parameterType)) { @@ -80,19 +100,31 @@ bool InstructionValidator::IsParameterValid( instruction.GetParameter(parameterIndex).GetPlainString(); const auto &objectsContainersList = projectScopedContainers.GetObjectsContainersList(); - return objectsContainersList.HasObjectOrGroupNamed(objectOrGroupName) && - (parameterMetadata.GetExtraInfo().empty() || - objectsContainersList.GetTypeOfObject(objectOrGroupName) == - parameterMetadata.GetExtraInfo()) && - InstructionValidator::HasRequiredBehaviors( - instruction, metadata, parameterIndex, objectsContainersList); + result.isValid = + objectsContainersList.HasObjectOrGroupNamed(objectOrGroupName) && + (parameterMetadata.GetExtraInfo().empty() || + objectsContainersList.GetTypeOfObject(objectOrGroupName) == + parameterMetadata.GetExtraInfo()) && + InstructionValidator::HasRequiredBehaviors( + instruction, metadata, parameterIndex, objectsContainersList); } else if (gd::ParameterMetadata::IsExpression("resource", parameterType)) { const auto &resourceName = instruction.GetParameter(parameterIndex).GetPlainString(); - return projectScopedContainers.GetResourcesContainersList() - .HasResourceNamed(resourceName); + result.isValid = projectScopedContainers.GetResourcesContainersList() + .HasResourceNamed(resourceName); } - return true; + + return result; +} + +bool InstructionValidator::IsParameterValid( + const gd::Platform &platform, + const gd::ProjectScopedContainers projectScopedContainers, + const gd::Instruction &instruction, const InstructionMetadata &metadata, + std::size_t parameterIndex, const gd::String &value) { + return ValidateParameter(platform, projectScopedContainers, instruction, + metadata, parameterIndex, value) + .isValid; } gd::String InstructionValidator::GetRootVariableName(const gd::String &name) { @@ -107,6 +139,16 @@ gd::String InstructionValidator::GetRootVariableName(const gd::String &name) { : squareBracketPosition); }; +bool InstructionValidator::HasDeprecationWarnings( + const gd::Platform &platform, + const gd::ProjectScopedContainers projectScopedContainers, + const gd::Instruction &instruction, const InstructionMetadata &metadata, + std::size_t parameterIndex, const gd::String &value) { + return ValidateParameter(platform, projectScopedContainers, instruction, + metadata, parameterIndex, value) + .hasDeprecationWarning; +} + bool InstructionValidator::HasRequiredBehaviors( const gd::Instruction &instruction, const gd::InstructionMetadata &instructionMetadata, diff --git a/Core/GDCore/IDE/InstructionValidator.h b/Core/GDCore/IDE/InstructionValidator.h index 5978e68aefb5..29f438f53611 100644 --- a/Core/GDCore/IDE/InstructionValidator.h +++ b/Core/GDCore/IDE/InstructionValidator.h @@ -18,8 +18,42 @@ class String; namespace gd { +/** + * \brief Result of parameter validation containing both validity status + * and deprecation warning. + */ +struct GD_CORE_API ParameterValidationResult { + bool isValid = true; + bool hasDeprecationWarning = false; + + ParameterValidationResult() = default; + ParameterValidationResult(bool isValid_, bool hasDeprecationWarning_) + : isValid(isValid_), hasDeprecationWarning(hasDeprecationWarning_) {} + + bool IsValid() const { return isValid; } + bool HasDeprecationWarning() const { return hasDeprecationWarning; } +}; + class GD_CORE_API InstructionValidator { public: + /** + * \brief Validate a parameter and check for deprecation warnings in a single + * pass. + * + * This method is more efficient than calling IsParameterValid and + * HasDeprecationWarnings separately as it only parses the expression once. + */ + static ParameterValidationResult ValidateParameter( + const gd::Platform &platform, + const gd::ProjectScopedContainers projectScopedContainers, + const gd::Instruction &instruction, const InstructionMetadata &metadata, + std::size_t parameterIndex, const gd::String &value); + + /** + * \brief Check if a parameter is valid. + * \deprecated Use ValidateParameter instead for better performance when you + * also need to check for deprecation warnings. + */ static bool IsParameterValid(const gd::Platform &platform, const gd::ProjectScopedContainers projectScopedContainers, @@ -27,6 +61,18 @@ class GD_CORE_API InstructionValidator { const InstructionMetadata &metadata, std::size_t parameterIndex, const gd::String &value); + /** + * \brief Check if a parameter expression has deprecation warnings. + * \deprecated Use ValidateParameter instead for better performance when you + * also need to check for validity. + */ + static bool + HasDeprecationWarnings(const gd::Platform &platform, + const gd::ProjectScopedContainers projectScopedContainers, + const gd::Instruction &instruction, + const InstructionMetadata &metadata, + std::size_t parameterIndex, const gd::String &value); + static gd::String GetRootVariableName(const gd::String &name); private: diff --git a/Core/GDCore/Project/EventsFunction.cpp b/Core/GDCore/Project/EventsFunction.cpp index 7c941232e3b1..b4742f82aae2 100644 --- a/Core/GDCore/Project/EventsFunction.cpp +++ b/Core/GDCore/Project/EventsFunction.cpp @@ -75,6 +75,12 @@ void EventsFunction::SerializeTo(SerializerElement& element) const { if (isAsync) { element.SetBoolAttribute("async", isAsync); } + if (isDeprecated) { + element.SetBoolAttribute("deprecated", isDeprecated); + } + if (!deprecationMessage.empty()) { + element.SetAttribute("deprecationMessage", deprecationMessage); + } events.SerializeTo(element.AddChild("events")); gd::String functionTypeStr = "Action"; @@ -116,6 +122,8 @@ void EventsFunction::UnserializeFrom(gd::Project& project, getterName = element.GetStringAttribute("getterName"); isPrivate = element.GetBoolAttribute("private"); isAsync = element.GetBoolAttribute("async"); + isDeprecated = element.GetBoolAttribute("deprecated"); + deprecationMessage = element.GetStringAttribute("deprecationMessage"); events.UnserializeFrom(project, element.GetChild("events")); gd::String functionTypeStr = element.GetStringAttribute("functionType"); diff --git a/Core/GDCore/Project/EventsFunction.h b/Core/GDCore/Project/EventsFunction.h index 886b7401759c..f5f5e1fbba8b 100644 --- a/Core/GDCore/Project/EventsFunction.h +++ b/Core/GDCore/Project/EventsFunction.h @@ -223,6 +223,34 @@ class GD_CORE_API EventsFunction { return *this; } + /** + * \brief Returns true if the function is deprecated. + */ + bool IsDeprecated() const { return isDeprecated; } + + /** + * \brief Sets whether the function is deprecated. + */ + EventsFunction& SetDeprecated(bool _isDeprecated) { + isDeprecated = _isDeprecated; + return *this; + } + + /** + * \brief Get the deprecation message that explains why the function is + * deprecated and what to use instead. + */ + const gd::String& GetDeprecationMessage() const { return deprecationMessage; } + + /** + * \brief Set the deprecation message that explains why the function is + * deprecated and what to use instead. + */ + EventsFunction& SetDeprecationMessage(const gd::String& message) { + deprecationMessage = message; + return *this; + } + /** * \brief Return the events. */ @@ -304,6 +332,8 @@ class GD_CORE_API EventsFunction { gd::ObjectGroupsContainer objectGroups; bool isPrivate = false; bool isAsync = false; + bool isDeprecated = false; + gd::String deprecationMessage; }; } // namespace gd diff --git a/GDJS/GDJS/Events/CodeGeneration/MetadataDeclarationHelper.cpp b/GDJS/GDJS/Events/CodeGeneration/MetadataDeclarationHelper.cpp index 253ccb551fa1..e6e573f24179 100644 --- a/GDJS/GDJS/Events/CodeGeneration/MetadataDeclarationHelper.cpp +++ b/GDJS/GDJS/Events/CodeGeneration/MetadataDeclarationHelper.cpp @@ -486,6 +486,8 @@ gd::String MetadataDeclarationHelper::GetDefaultSentence( }; gd::String MetadataDeclarationHelper::GetFreeFunctionSentence(const gd::EventsFunction &eventsFunction) { + // Note: [DEPRECATED] prefix is now added in the UI layer (Instruction.js) + // based on user preference "showDeprecatedInstructionWarning" return GetTranslation(eventsFunction.GetSentence()).empty() ? GetDefaultSentence(eventsFunction, 0, 1) : GetTranslation(eventsFunction.GetSentence()); @@ -494,6 +496,8 @@ gd::String MetadataDeclarationHelper::GetFreeFunctionSentence(const gd::EventsFu gd::String MetadataDeclarationHelper::GetBehaviorFunctionSentence( const gd::EventsFunction &eventsFunction, const bool excludeObjectParameter) { + // Note: [DEPRECATED] prefix is now added in the UI layer (Instruction.js) + // based on user preference "showDeprecatedInstructionWarning" return GetTranslation(eventsFunction.GetSentence()).empty() ? GetDefaultSentence(eventsFunction, excludeObjectParameter ? 2 : 0, 0) @@ -503,6 +507,8 @@ gd::String MetadataDeclarationHelper::GetBehaviorFunctionSentence( gd::String MetadataDeclarationHelper::GetObjectFunctionSentence( const gd::EventsFunction &eventsFunction, const bool excludeObjectParameter) { + // Note: [DEPRECATED] prefix is now added in the UI layer (Instruction.js) + // based on user preference "showDeprecatedInstructionWarning" return GetTranslation(eventsFunction.GetSentence()).empty() ? GetDefaultSentence(eventsFunction, excludeObjectParameter ? 1 : 0, 0) @@ -542,13 +548,13 @@ MetadataDeclarationHelper::DeclareExpressionMetadata( eventsFunction.GetExpressionType().IsNumber() ? extension.AddExpression( eventsFunction.GetName(), - GetFullName(eventsFunction), + MetadataDeclarationHelper::GetFullName(eventsFunction), eventsFunction.GetDescription() || GetFullName(eventsFunction), eventsFunction.GetGroup(), GetExtensionIconUrl(extension)) : extension.AddStrExpression( eventsFunction.GetName(), - GetFullName(eventsFunction), + MetadataDeclarationHelper::GetFullName(eventsFunction), eventsFunction.GetDescription() || GetFullName(eventsFunction), eventsFunction.GetGroup(), GetExtensionIconUrl(extension)); @@ -733,7 +739,7 @@ MetadataDeclarationHelper::DeclareBehaviorExpressionMetadata( (eventsFunction.GetExpressionType().IsNumber()) ? behaviorMetadata.AddExpression( eventsFunction.GetName(), - GetFullName(eventsFunction), + MetadataDeclarationHelper::GetFullName(eventsFunction), eventsFunction.GetDescription() || GetFullName(eventsFunction), eventsFunction.GetGroup() || @@ -742,7 +748,7 @@ MetadataDeclarationHelper::DeclareBehaviorExpressionMetadata( GetExtensionIconUrl(extension)) : behaviorMetadata.AddStrExpression( eventsFunction.GetName(), - GetFullName(eventsFunction), + MetadataDeclarationHelper::GetFullName(eventsFunction), eventsFunction.GetDescription() || GetFullName(eventsFunction), eventsFunction.GetGroup() || @@ -907,7 +913,7 @@ MetadataDeclarationHelper::DeclareObjectExpressionMetadata( (eventsFunction.GetExpressionType().IsNumber()) ? objectMetadata.AddExpression( eventsFunction.GetName(), - GetFullName(eventsFunction), + MetadataDeclarationHelper::GetFullName(eventsFunction), eventsFunction.GetDescription() || GetFullName(eventsFunction), eventsFunction.GetGroup() || @@ -916,7 +922,7 @@ MetadataDeclarationHelper::DeclareObjectExpressionMetadata( GetExtensionIconUrl(extension)) : objectMetadata.AddStrExpression( eventsFunction.GetName(), - GetFullName(eventsFunction), + MetadataDeclarationHelper::GetFullName(eventsFunction), eventsFunction.GetDescription() || GetFullName(eventsFunction), eventsFunction.GetGroup() || @@ -1556,6 +1562,12 @@ MetadataDeclarationHelper::GenerateFreeFunctionMetadata( if (eventsFunction.IsPrivate()) instructionOrExpression.SetPrivate(); + if (eventsFunction.IsDeprecated()) { + instructionOrExpression.SetHidden(); + instructionOrExpression.SetDeprecationMessage( + eventsFunction.GetDeprecationMessage()); + } + return instructionOrExpression; }; @@ -1593,6 +1605,12 @@ gd::BehaviorMetadata &MetadataDeclarationHelper::GenerateBehaviorMetadata( if (eventsFunction.IsPrivate()) instructionOrExpression.SetPrivate(); + + if (eventsFunction.IsDeprecated()) { + instructionOrExpression.SetHidden(); + instructionOrExpression.SetDeprecationMessage( + eventsFunction.GetDeprecationMessage()); + } } return behaviorMetadata; @@ -1632,6 +1650,12 @@ gd::ObjectMetadata &MetadataDeclarationHelper::GenerateObjectMetadata( if (eventsFunction.IsPrivate()) instructionOrExpression.SetPrivate(); + + if (eventsFunction.IsDeprecated()) { + instructionOrExpression.SetHidden(); + instructionOrExpression.SetDeprecationMessage( + eventsFunction.GetDeprecationMessage()); + } } UpdateCustomObjectDefaultBehaviors(project, objectMetadata); diff --git a/GDevelop.js/Bindings/Bindings.idl b/GDevelop.js/Bindings/Bindings.idl index 6efbb506b654..0c4f2d788b11 100644 --- a/GDevelop.js/Bindings/Bindings.idl +++ b/GDevelop.js/Bindings/Bindings.idl @@ -1726,6 +1726,7 @@ interface InstructionMetadata { [Const, Ref] ParameterMetadataContainer GetParameters(); long GetUsageComplexity(); boolean IsHidden(); + [Const, Ref] DOMString GetDeprecationMessage(); boolean IsPrivate(); boolean IsAsync(); boolean IsOptionallyAsync(); @@ -1792,6 +1793,8 @@ interface ExpressionMetadata { [Const, Ref] DOMString GetHelpPath(); boolean IsShown(); boolean IsPrivate(); + boolean IsDeprecated(); + [Const, Ref] DOMString GetDeprecationMessage(); boolean IsRelevantForLayoutEvents(); boolean IsRelevantForFunctionEvents(); boolean IsRelevantForAsynchronousFunctionEvents(); @@ -2922,7 +2925,19 @@ interface BehaviorParameterFiller { [Ref] Instruction instruction); }; +interface ParameterValidationResult { + boolean IsValid(); + boolean HasDeprecationWarning(); +}; + interface InstructionValidator { + [Value] ParameterValidationResult STATIC_ValidateParameter( + [Const, Ref] Platform platform, + [Const, Ref] ProjectScopedContainers projectScopedContainers, + [Const, Ref] Instruction instruction, + [Const, Ref] InstructionMetadata metadata, + long parameterIndex, + [Const] DOMString value); boolean STATIC_IsParameterValid( [Const, Ref] Platform platform, [Const, Ref] ProjectScopedContainers projectScopedContainers, @@ -2930,6 +2945,13 @@ interface InstructionValidator { [Const, Ref] InstructionMetadata metadata, long parameterIndex, [Const] DOMString value); + boolean STATIC_HasDeprecationWarnings( + [Const, Ref] Platform platform, + [Const, Ref] ProjectScopedContainers projectScopedContainers, + [Const, Ref] Instruction instruction, + [Const, Ref] InstructionMetadata metadata, + long parameterIndex, + [Const] DOMString value); }; interface ObjectTools { @@ -3065,7 +3087,26 @@ interface WholeProjectDiagnosticReport { boolean HasAnyIssue(); }; +enum ExpressionParserError_ErrorType { + "ExpressionParserError::SyntaxError", + "ExpressionParserError::InvalidOperator", + "ExpressionParserError::MismatchedType", + "ExpressionParserError::UndeclaredVariable", + "ExpressionParserError::UnknownIdentifier", + "ExpressionParserError::BracketsNotAllowedForObjects", + "ExpressionParserError::TooFewParameters", + "ExpressionParserError::TooManyParameters", + "ExpressionParserError::InvalidFunctionName", + "ExpressionParserError::MalformedVariableParameter", + "ExpressionParserError::MalformedObjectParameter", + "ExpressionParserError::UnknownParameterType", + "ExpressionParserError::MissingBehavior", + "ExpressionParserError::VariableNameCollision", + "ExpressionParserError::DeprecatedExpression" +}; + interface ExpressionParserError { + ExpressionParserError_ErrorType GetType(); [Const, Ref] DOMString GetMessage(); unsigned long GetStartPosition(); unsigned long GetEndPosition(); @@ -3187,6 +3228,10 @@ interface EventsFunction { boolean IsPrivate(); [Ref] EventsFunction SetAsync(boolean isAsync); boolean IsAsync(); + [Ref] EventsFunction SetDeprecated(boolean isDeprecated); + boolean IsDeprecated(); + [Ref] EventsFunction SetDeprecationMessage([Const] DOMString message); + [Const, Ref] DOMString GetDeprecationMessage(); boolean IsAction(); boolean IsExpression(); boolean IsCondition(); diff --git a/GDevelop.js/Bindings/Wrapper.cpp b/GDevelop.js/Bindings/Wrapper.cpp index c5f29ed42c10..890b027aad22 100644 --- a/GDevelop.js/Bindings/Wrapper.cpp +++ b/GDevelop.js/Bindings/Wrapper.cpp @@ -449,6 +449,8 @@ typedef std::unique_ptr UniquePtrBehavior; typedef std::unique_ptr UniquePtrExpressionNode; typedef std::vector VectorExpressionParserError; +typedef gd::ExpressionParserError::ErrorType + ExpressionParserError_ErrorType; typedef gd::SerializableWithNameList EventsBasedBehaviorsList; typedef gd::SerializableWithNameList @@ -677,7 +679,9 @@ typedef std::vector VectorPropertyDescriptorChoice #define STATIC_GetBehaviorsWithType GetBehaviorsWithType #define STATIC_IsBehaviorCompatibleWithObject IsBehaviorCompatibleWithObject #define STATIC_FillBehaviorParameters FillBehaviorParameters +#define STATIC_ValidateParameter ValidateParameter #define STATIC_IsParameterValid IsParameterValid +#define STATIC_HasDeprecationWarnings HasDeprecationWarnings #define STATIC_FixInvalidRequiredBehaviorProperties \ FixInvalidRequiredBehaviorProperties #define STATIC_RemoveLayerInScene RemoveLayerInScene diff --git a/GDevelop.js/scripts/generate-types.js b/GDevelop.js/scripts/generate-types.js index 715b5fbb5549..292614690608 100644 --- a/GDevelop.js/scripts/generate-types.js +++ b/GDevelop.js/scripts/generate-types.js @@ -229,6 +229,35 @@ type ExpressionCompletionDescription_CompletionKind = 0 | 1 | 2 | 3 | 4 | 5 | 6` ].join('\n'), 'types/gdexpressioncompletiondescription.js' ); + fs.writeFileSync( + 'types/expressionparsererror_errortype.js', + `// Automatically generated by GDevelop.js/scripts/generate-types.js +type ExpressionParserError_ErrorType = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 +` + ); + shell.sed( + '-i', + 'declare class gdExpressionParserError {', + [ + 'declare class gdExpressionParserError {', + ' static SyntaxError: 0;', + ' static InvalidOperator: 1;', + ' static MismatchedType: 2;', + ' static UndeclaredVariable: 3;', + ' static UnknownIdentifier: 4;', + ' static BracketsNotAllowedForObjects: 5;', + ' static TooFewParameters: 6;', + ' static TooManyParameters: 7;', + ' static InvalidFunctionName: 8;', + ' static MalformedVariableParameter: 9;', + ' static MalformedObjectParameter: 10;', + ' static UnknownParameterType: 11;', + ' static MissingBehavior: 12;', + ' static VariableNameCollision: 13;', + ' static DeprecatedExpression: 14;', + ].join('\n'), + 'types/gdexpressionparsererror.js' + ); fs.writeFileSync( 'types/particleemitterobject_renderertype.js', `// Automatically generated by GDevelop.js/scripts/generate-types.js diff --git a/GDevelop.js/types.d.ts b/GDevelop.js/types.d.ts index 1760003455e9..dc3f19a308e0 100644 --- a/GDevelop.js/types.d.ts +++ b/GDevelop.js/types.d.ts @@ -84,6 +84,24 @@ export enum ProjectDiagnostic_ErrorType { MismatchedObjectType = 3, } +export enum ExpressionParserError_ErrorType { + SyntaxError = 0, + InvalidOperator = 1, + MismatchedType = 2, + UndeclaredVariable = 3, + UnknownIdentifier = 4, + BracketsNotAllowedForObjects = 5, + TooFewParameters = 6, + TooManyParameters = 7, + InvalidFunctionName = 8, + MalformedVariableParameter = 9, + MalformedObjectParameter = 10, + UnknownParameterType = 11, + MissingBehavior = 12, + VariableNameCollision = 13, + DeprecatedExpression = 14, +} + export enum ExpressionCompletionDescription_CompletionKind { Object = 0, BehaviorWithPrefix = 1, @@ -1427,6 +1445,7 @@ export class InstructionMetadata extends AbstractFunctionMetadata { getParameters(): ParameterMetadataContainer; getUsageComplexity(): number; isHidden(): boolean; + getDeprecationMessage(): string; isPrivate(): boolean; isAsync(): boolean; isOptionallyAsync(): boolean; @@ -1475,6 +1494,8 @@ export class ExpressionMetadata extends AbstractFunctionMetadata { getHelpPath(): string; isShown(): boolean; isPrivate(): boolean; + isDeprecated(): boolean; + getDeprecationMessage(): string; isRelevantForLayoutEvents(): boolean; isRelevantForFunctionEvents(): boolean; isRelevantForAsynchronousFunctionEvents(): boolean; @@ -2084,8 +2105,15 @@ export class BehaviorParameterFiller extends EmscriptenObject { static fillBehaviorParameters(platform: Platform, projectScopedContainers: ProjectScopedContainers, instructionMetadata: InstructionMetadata, instruction: Instruction): boolean; } +export class ParameterValidationResult extends EmscriptenObject { + isValid(): boolean; + hasDeprecationWarning(): boolean; +} + export class InstructionValidator extends EmscriptenObject { + static validateParameter(platform: Platform, projectScopedContainers: ProjectScopedContainers, instruction: Instruction, metadata: InstructionMetadata, parameterIndex: number, value: string): ParameterValidationResult; static isParameterValid(platform: Platform, projectScopedContainers: ProjectScopedContainers, instruction: Instruction, metadata: InstructionMetadata, parameterIndex: number, value: string): boolean; + static hasDeprecationWarnings(platform: Platform, projectScopedContainers: ProjectScopedContainers, instruction: Instruction, metadata: InstructionMetadata, parameterIndex: number, value: string): boolean; } export class ObjectTools extends EmscriptenObject { @@ -2202,6 +2230,7 @@ export class WholeProjectDiagnosticReport extends EmscriptenObject { } export class ExpressionParserError extends EmscriptenObject { + getType(): ExpressionParserError_ErrorType; getMessage(): string; getStartPosition(): number; getEndPosition(): number; @@ -2291,6 +2320,10 @@ export class EventsFunction extends EmscriptenObject { isPrivate(): boolean; setAsync(isAsync: boolean): EventsFunction; isAsync(): boolean; + setDeprecated(isDeprecated: boolean): EventsFunction; + isDeprecated(): boolean; + setDeprecationMessage(message: string): EventsFunction; + getDeprecationMessage(): string; isAction(): boolean; isExpression(): boolean; isCondition(): boolean; diff --git a/GDevelop.js/types/expressionparsererror_errortype.js b/GDevelop.js/types/expressionparsererror_errortype.js new file mode 100644 index 000000000000..e434a1530aa4 --- /dev/null +++ b/GDevelop.js/types/expressionparsererror_errortype.js @@ -0,0 +1,2 @@ +// Automatically generated by GDevelop.js/scripts/generate-types.js +type ExpressionParserError_ErrorType = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 diff --git a/GDevelop.js/types/gdeventsfunction.js b/GDevelop.js/types/gdeventsfunction.js index a6a92436c40e..b59eae40f80d 100644 --- a/GDevelop.js/types/gdeventsfunction.js +++ b/GDevelop.js/types/gdeventsfunction.js @@ -25,6 +25,10 @@ declare class gdEventsFunction { isPrivate(): boolean; setAsync(isAsync: boolean): gdEventsFunction; isAsync(): boolean; + setDeprecated(isDeprecated: boolean): gdEventsFunction; + isDeprecated(): boolean; + setDeprecationMessage(message: string): gdEventsFunction; + getDeprecationMessage(): string; isAction(): boolean; isExpression(): boolean; isCondition(): boolean; diff --git a/GDevelop.js/types/gdexpressionmetadata.js b/GDevelop.js/types/gdexpressionmetadata.js index 74bb1fa18193..6494288baa56 100644 --- a/GDevelop.js/types/gdexpressionmetadata.js +++ b/GDevelop.js/types/gdexpressionmetadata.js @@ -9,6 +9,8 @@ declare class gdExpressionMetadata extends gdAbstractFunctionMetadata { getHelpPath(): string; isShown(): boolean; isPrivate(): boolean; + isDeprecated(): boolean; + getDeprecationMessage(): string; isRelevantForLayoutEvents(): boolean; isRelevantForFunctionEvents(): boolean; isRelevantForAsynchronousFunctionEvents(): boolean; diff --git a/GDevelop.js/types/gdexpressionparsererror.js b/GDevelop.js/types/gdexpressionparsererror.js index 42887dbf83e9..d35b6acdfe97 100644 --- a/GDevelop.js/types/gdexpressionparsererror.js +++ b/GDevelop.js/types/gdexpressionparsererror.js @@ -1,5 +1,21 @@ // Automatically generated by GDevelop.js/scripts/generate-types.js declare class gdExpressionParserError { + static SyntaxError: 0; + static InvalidOperator: 1; + static MismatchedType: 2; + static UndeclaredVariable: 3; + static UnknownIdentifier: 4; + static BracketsNotAllowedForObjects: 5; + static TooFewParameters: 6; + static TooManyParameters: 7; + static InvalidFunctionName: 8; + static MalformedVariableParameter: 9; + static MalformedObjectParameter: 10; + static UnknownParameterType: 11; + static MissingBehavior: 12; + static VariableNameCollision: 13; + static DeprecatedExpression: 14; + getType(): ExpressionParserError_ErrorType; getMessage(): string; getStartPosition(): number; getEndPosition(): number; diff --git a/GDevelop.js/types/gdinstructionmetadata.js b/GDevelop.js/types/gdinstructionmetadata.js index c02e43bbe49f..037022c9583e 100644 --- a/GDevelop.js/types/gdinstructionmetadata.js +++ b/GDevelop.js/types/gdinstructionmetadata.js @@ -14,6 +14,7 @@ declare class gdInstructionMetadata extends gdAbstractFunctionMetadata { getParameters(): gdParameterMetadataContainer; getUsageComplexity(): number; isHidden(): boolean; + getDeprecationMessage(): string; isPrivate(): boolean; isAsync(): boolean; isOptionallyAsync(): boolean; diff --git a/GDevelop.js/types/gdinstructionvalidator.js b/GDevelop.js/types/gdinstructionvalidator.js index e7042f447527..d6b5535886ce 100644 --- a/GDevelop.js/types/gdinstructionvalidator.js +++ b/GDevelop.js/types/gdinstructionvalidator.js @@ -1,6 +1,8 @@ // Automatically generated by GDevelop.js/scripts/generate-types.js declare class gdInstructionValidator { + static validateParameter(platform: gdPlatform, projectScopedContainers: gdProjectScopedContainers, instruction: gdInstruction, metadata: gdInstructionMetadata, parameterIndex: number, value: string): gdParameterValidationResult; static isParameterValid(platform: gdPlatform, projectScopedContainers: gdProjectScopedContainers, instruction: gdInstruction, metadata: gdInstructionMetadata, parameterIndex: number, value: string): boolean; + static hasDeprecationWarnings(platform: gdPlatform, projectScopedContainers: gdProjectScopedContainers, instruction: gdInstruction, metadata: gdInstructionMetadata, parameterIndex: number, value: string): boolean; delete(): void; ptr: number; }; \ No newline at end of file diff --git a/GDevelop.js/types/gdparametervalidationresult.js b/GDevelop.js/types/gdparametervalidationresult.js new file mode 100644 index 000000000000..c0c7f060e397 --- /dev/null +++ b/GDevelop.js/types/gdparametervalidationresult.js @@ -0,0 +1,7 @@ +// Automatically generated by GDevelop.js/scripts/generate-types.js +declare class gdParameterValidationResult { + isValid(): boolean; + hasDeprecationWarning(): boolean; + delete(): void; + ptr: number; +}; \ No newline at end of file diff --git a/GDevelop.js/types/libgdevelop.js b/GDevelop.js/types/libgdevelop.js index f21211acb534..b86f9034fcba 100644 --- a/GDevelop.js/types/libgdevelop.js +++ b/GDevelop.js/types/libgdevelop.js @@ -199,6 +199,7 @@ declare class libGDevelop { VariablesChangeset: Class; WholeProjectRefactorer: Class; BehaviorParameterFiller: Class; + ParameterValidationResult: Class; InstructionValidator: Class; ObjectTools: Class; EventsBasedObjectDependencyFinder: Class; @@ -218,6 +219,7 @@ declare class libGDevelop { ProjectDiagnostic: Class; DiagnosticReport: Class; WholeProjectDiagnosticReport: Class; + ExpressionParserError_ErrorType: Class; ExpressionParserError: Class; VectorExpressionParserError: Class; ExpressionParser2NodeWorker: Class; diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/EventsFunctionPropertiesEditor.js b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/EventsFunctionPropertiesEditor.js index 3ebc98b31ba8..17869211f9c5 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/EventsFunctionPropertiesEditor.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/EventsFunctionPropertiesEditor.js @@ -552,6 +552,46 @@ export const EventsFunctionPropertiesEditor = ({ forceUpdate(); }} /> + Deprecated} + checked={eventsFunction.isDeprecated()} + onCheck={(e, checked) => { + eventsFunction.setDeprecated(checked); + if (onConfigurationUpdated) + onConfigurationUpdated('isDeprecated'); + forceUpdate(); + }} + tooltipOrHelperText={ + eventsFunction.isDeprecated() ? ( + + This function is marked as deprecated. It will be + displayed with a warning in the events editor. + + ) : ( + + Mark this function as deprecated to discourage its + use. + + ) + } + /> + {eventsFunction.isDeprecated() && ( + Deprecation message + } + translatableHintText={t`Example: Use "New Action Name" instead.`} + fullWidth + multiline + value={eventsFunction.getDeprecationMessage()} + onChange={text => { + eventsFunction.setDeprecationMessage(text); + if (onConfigurationUpdated) onConfigurationUpdated(); + forceUpdate(); + }} + /> + )} {eventsFunction.isAsync() && ( ( + {children} +); + +export default DeprecatedParameterValue; diff --git a/newIDE/app/src/EventsSheet/EventsTree/Instruction.js b/newIDE/app/src/EventsSheet/EventsTree/Instruction.js index 3710631647a1..011175d1e9a9 100644 --- a/newIDE/app/src/EventsSheet/EventsTree/Instruction.js +++ b/newIDE/app/src/EventsSheet/EventsTree/Instruction.js @@ -22,6 +22,7 @@ import InstructionsList from './InstructionsList'; import DropIndicator from './DropIndicator'; import ParameterRenderingService from '../ParameterRenderingService'; import InvalidParameterValue from './InvalidParameterValue'; +import DeprecatedParameterValue from './DeprecatedParameterValue'; import MissingParameterValue from './MissingParameterValue'; import { makeDragSourceAndDropTarget } from '../../UI/DragAndDrop/DragSourceAndDropTarget'; import { @@ -279,7 +280,13 @@ const Instruction = (props: Props) => { /> ); } - return {value}; + // Add [DEPRECATED] prefix for the first text segment if the preference is enabled + // and the instruction is deprecated (hidden) + const deprecatedPrefix = + i === 0 && showDeprecatedInstructionWarning && metadata.isHidden() + ? '[DEPRECATED] ' + : ''; + return {deprecatedPrefix + value}; } const parameterMetadata = metadata.getParameter(parameterIndex); @@ -290,8 +297,10 @@ const Instruction = (props: Props) => { ? 'number' : parameterMetadata.getType(); let expressionIsValid = true; + let hasDeprecationWarning = false; if (!shouldNotBeValidated({ value, parameterType })) { - expressionIsValid = gd.InstructionValidator.isParameterValid( + // Use validateParameter for combined validation (single pass) + const validationResult = gd.InstructionValidator.validateParameter( platform, projectScopedContainers, instruction, @@ -299,6 +308,12 @@ const Instruction = (props: Props) => { parameterIndex, value ); + expressionIsValid = validationResult.isValid(); + // Check for deprecation warnings (only if the preference is enabled) + if (showDeprecatedInstructionWarning) { + hasDeprecationWarning = validationResult.hasDeprecationWarning(); + } + validationResult.delete(); // TODO Move this code inside `InstructionValidator.isParameterValid` if ( expressionIsValid && @@ -379,9 +394,11 @@ const Instruction = (props: Props) => { scope, value: formattedValue, expressionIsValid, + hasDeprecationWarning, parameterMetadata, renderObjectThumbnail, InvalidParameterValue, + DeprecatedParameterValue, MissingParameterValue, useAssignmentOperators, projectScopedContainersAccessor: @@ -463,7 +480,8 @@ const Instruction = (props: Props) => { [selectedArea]: props.selected, [warningInstruction]: showDeprecatedInstructionWarning && - !isInstructionVisible(scope, metadata), + (!isInstructionVisible(scope, metadata) || + metadata.isHidden()), })} onClick={e => { e.stopPropagation(); @@ -501,7 +519,17 @@ const Instruction = (props: Props) => { {showDeprecatedInstructionWarning && metadata.isHidden() ? ( + {props.isCondition ? ( + Deprecated condition + ) : ( + Deprecated action + )} + {': '} + {metadata.getDeprecationMessage()} + + ) : props.isCondition ? ( Deprecated condition ) : ( Deprecated action diff --git a/newIDE/app/src/EventsSheet/InstructionEditor/InstructionParametersEditor.js b/newIDE/app/src/EventsSheet/InstructionEditor/InstructionParametersEditor.js index 1ab2d124b092..b392f29500e3 100644 --- a/newIDE/app/src/EventsSheet/InstructionEditor/InstructionParametersEditor.js +++ b/newIDE/app/src/EventsSheet/InstructionEditor/InstructionParametersEditor.js @@ -28,6 +28,7 @@ import ScrollView from '../../UI/ScrollView'; import { getInstructionTutorialIds } from '../../Utils/GDevelopServices/Tutorial'; import useForceUpdate from '../../Utils/UseForceUpdate'; import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext'; +import PreferencesContext from '../../MainFrame/Preferences/PreferencesContext'; import FlatButton from '../../UI/FlatButton'; import { type ParameterFieldInterface, @@ -139,6 +140,9 @@ const InstructionParametersEditor = React.forwardRef< const { palette: { type: paletteType }, } = React.useContext(GDevelopThemeContext); + const preferences = React.useContext(PreferencesContext); + const showDeprecatedInstructionWarning = + preferences.values.showDeprecatedInstructionWarning; const forceUpdate = useForceUpdate(); @@ -311,6 +315,20 @@ const InstructionParametersEditor = React.forwardRef< + {showDeprecatedInstructionWarning && instructionMetadata.isHidden() && ( + + + {instructionMetadata.getDeprecationMessage() ? ( + <> + Deprecated:{' '} + {instructionMetadata.getDeprecationMessage()} + + ) : ( + Deprecated + )} + + + )} {instructionExtraInformation && ( {instructionExtraInformation.identifier === undefined ? ( diff --git a/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js b/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js index 598aff891eed..e554e65d6874 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js @@ -45,8 +45,10 @@ export default React.forwardRef( export const renderInlineDefaultField = ({ value, expressionIsValid, + hasDeprecationWarning, parameterMetadata, InvalidParameterValue, + DeprecatedParameterValue, MissingParameterValue, }: ParameterInlineRendererProps) => { if (!value && !parameterMetadata.isOptional()) { @@ -55,5 +57,8 @@ export const renderInlineDefaultField = ({ if (!expressionIsValid) { return {value}; } + if (hasDeprecationWarning) { + return {value}; + } return value; }; diff --git a/newIDE/app/src/EventsSheet/ParameterFields/GenericExpressionField/BackgroundHighlighting.js b/newIDE/app/src/EventsSheet/ParameterFields/GenericExpressionField/BackgroundHighlighting.js index 778b9854164c..b9e7677b34db 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/GenericExpressionField/BackgroundHighlighting.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/GenericExpressionField/BackgroundHighlighting.js @@ -4,7 +4,7 @@ import * as React from 'react'; export type Highlight = {| begin: number, end: number, - type?: 'error', + type?: 'error' | 'deprecated', message: string, |}; @@ -22,12 +22,18 @@ const defaultStyle = { color: 'transparent', }; -const highlightedText = { +const highlightedErrorText = { backgroundColor: 'rgba(244, 67, 54, 0.15)', borderSizing: 'border-box', borderBottom: '3px solid rgba(244, 67, 54, 0.7)', }; +const highlightedDeprecatedText = { + backgroundColor: 'rgba(255, 152, 0, 0.15)', + borderSizing: 'border-box', + borderBottom: '3px solid rgba(255, 152, 0, 0.7)', +}; + const BackgroundHighlighting = ({ value, style, highlights }: Props) => { const sortedHighlights = highlights .slice() @@ -47,8 +53,12 @@ const BackgroundHighlighting = ({ value, style, highlights }: Props) => { } if (lastPos < highlight.end) { + const highlightStyle = + highlight.type === 'deprecated' + ? highlightedDeprecatedText + : highlightedErrorText; elements.push( - + {value.substring(startPos, highlight.end)} ); diff --git a/newIDE/app/src/EventsSheet/ParameterFields/GenericExpressionField/index.js b/newIDE/app/src/EventsSheet/ParameterFields/GenericExpressionField/index.js index 73a8d482715f..c659de6d7385 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/GenericExpressionField/index.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/GenericExpressionField/index.js @@ -49,6 +49,7 @@ import { } from '../../../UI/KeyboardShortcuts/InteractionKeys'; import Paper from '../../../UI/Paper'; import { ProjectScopedContainersAccessor } from '../../../InstructionOrExpression/EventsScope'; +import PreferencesContext from '../../../MainFrame/Preferences/PreferencesContext'; const gd: libGDevelop = global.gd; @@ -92,6 +93,9 @@ const styles = { paddingLeft: 0, paddingRight: 0, }, + warningText: { + color: '#ff9800', + }, }; type State = {| @@ -101,6 +105,7 @@ type State = {| validatedValue: string, errorText: ?string, errorHighlights: Array, + isOnlyWarning: boolean, autocompletions: AutocompletionsState, |}; @@ -130,10 +135,12 @@ const extractErrors = ( projectScopedContainersAccessor: ProjectScopedContainersAccessor, expressionType: string, parameterMetadata: ?gdParameterMetadata, - expressionNode: gdExpressionNode + expressionNode: gdExpressionNode, + showDeprecatedInstructionWarning: boolean ): {| errorText: ?string, errorHighlights: Array, + isOnlyWarning: boolean, |} => { const expressionValidator = new gd.ExpressionValidator( gd.JsPlatform.get(), @@ -143,13 +150,24 @@ const extractErrors = ( ); expressionNode.visit(expressionValidator); const errors = expressionValidator.getAllErrors(); - - const errorHighlights: Array = mapVector(errors, error => ({ - begin: error.getStartPosition(), - end: error.getEndPosition() + 1, - message: error.getMessage(), - type: 'error', - })); + const fatalErrors = expressionValidator.getFatalErrors(); + const hasFatalErrors = fatalErrors.size() > 0; + + const errorHighlights: Array = mapVector(errors, error => { + const errorType = error.getType(); + const isDeprecated = + errorType === gd.ExpressionParserError.DeprecatedExpression; + // Skip deprecation warnings if the preference is disabled + if (isDeprecated && !showDeprecatedInstructionWarning) { + return null; + } + return { + begin: error.getStartPosition(), + end: error.getEndPosition() + 1, + message: error.getMessage(), + type: isDeprecated ? 'deprecated' : 'error', + }; + }).filter(Boolean); const otherErrorsCount = Math.max( 0, errorHighlights.length - MAX_ERRORS_COUNT @@ -168,12 +186,17 @@ const extractErrors = ( ) .join(' '); + // If there are warnings but no fatal errors, it's only a warning + const isOnlyWarning = errors.size() > 0 && !hasFatalErrors; + expressionValidator.delete(); - return { errorText, errorHighlights }; + return { errorText, errorHighlights, isOnlyWarning }; }; export default class ExpressionField extends React.Component { + static contextType = PreferencesContext; + _field: ?SemiControlledTextFieldInterface = null; _fieldElementWidth: ?number = null; _inputElement: ?HTMLInputElement = null; @@ -186,6 +209,7 @@ export default class ExpressionField extends React.Component { validatedValue: this.props.value, errorText: null, errorHighlights: [], + isOnlyWarning: false, autocompletions: getAutocompletionsInitialState(), }; @@ -443,13 +467,18 @@ export default class ExpressionField extends React.Component { const parser = new gd.ExpressionParser2(); const expressionNode = parser.parseExpression(expression).get(); - const { errorText, errorHighlights } = extractErrors( + const showDeprecatedInstructionWarning = this.context + ? this.context.values.showDeprecatedInstructionWarning + : true; + + const { errorText, errorHighlights, isOnlyWarning } = extractErrors( gd.JsPlatform.get(), project, projectScopedContainersAccessor, expressionType, parameterMetadata, - expressionNode + expressionNode, + showDeprecatedInstructionWarning ); const extraErrorText = onExtractAdditionalErrors ? onExtractAdditionalErrors(expression, expressionNode) @@ -467,6 +496,7 @@ export default class ExpressionField extends React.Component { this.setState(state => ({ errorText: formattedErrorText, errorHighlights, + isOnlyWarning, autocompletions: getAutocompletionsInitialState(), })); return; @@ -504,6 +534,7 @@ export default class ExpressionField extends React.Component { this.setState(state => ({ errorText: formattedErrorText, errorHighlights, + isOnlyWarning, autocompletions: setNewAutocompletions( state.autocompletions, allNewAutocompletions @@ -583,7 +614,17 @@ export default class ExpressionField extends React.Component { onBlur={this._handleBlurEvent} ref={field => (this._field = field)} onFocus={this._handleFocus} - errorText={this.state.errorText} + errorText={ + this.state.errorText ? ( + this.state.isOnlyWarning ? ( + + {this.state.errorText} + + ) : ( + this.state.errorText + ) + ) : null + } onClick={() => this._enqueueValidation()} onKeyDown={event => { const autocompletions = handleAutocompletionsKeyDown( diff --git a/newIDE/app/src/EventsSheet/ParameterFields/ParameterInlineRenderer.flow.js b/newIDE/app/src/EventsSheet/ParameterFields/ParameterInlineRenderer.flow.js index f8fed0474328..8ac45bcfb217 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/ParameterInlineRenderer.flow.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/ParameterInlineRenderer.flow.js @@ -8,6 +8,10 @@ export type InvalidParameterValueProps = {| isEmpty?: boolean, |}; +export type DeprecatedParameterValueProps = {| + children: React.Node, +|}; + /** * The props expected by a function that renders a parameter in the events sheet */ @@ -17,8 +21,10 @@ export type ParameterInlineRendererProps = {| parameterMetadata: gdParameterMetadata, value: string, expressionIsValid: boolean, + hasDeprecationWarning: boolean, renderObjectThumbnail: string => React.Node, InvalidParameterValue: InvalidParameterValueProps => React.Node, + DeprecatedParameterValue: DeprecatedParameterValueProps => React.Node, MissingParameterValue: () => React.Node, useAssignmentOperators: boolean, |}; diff --git a/newIDE/app/src/EventsSheet/ParameterFields/VariableField.js b/newIDE/app/src/EventsSheet/ParameterFields/VariableField.js index fa583e443bd8..4c4ce80bd1a9 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/VariableField.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/VariableField.js @@ -509,7 +509,9 @@ export const renderVariableWithIcon = ( value, parameterMetadata, expressionIsValid, + hasDeprecationWarning, InvalidParameterValue, + DeprecatedParameterValue, MissingParameterValue, projectScopedContainersAccessor, }: ParameterInlineRendererProps, @@ -529,9 +531,14 @@ export const renderVariableWithIcon = ( ) ); - const IconAndNameContainer = expressionIsValid - ? React.Fragment - : InvalidParameterValue; + let IconAndNameContainer; + if (!expressionIsValid) { + IconAndNameContainer = InvalidParameterValue; + } else if (hasDeprecationWarning) { + IconAndNameContainer = DeprecatedParameterValue; + } else { + IconAndNameContainer = React.Fragment; + } return (