diff --git a/ets2panda/lsp/BUILD.gn b/ets2panda/lsp/BUILD.gn index 121f05f97dc6ddc20d250ea61f74fa2532375854..88d0d45e75b4cc5b7798aeaac10cc79dab19f397 100644 --- a/ets2panda/lsp/BUILD.gn +++ b/ets2panda/lsp/BUILD.gn @@ -88,6 +88,7 @@ ohos_source_set("libes2panda_lsp_static") { "src/refactors/convert_template.cpp", "src/refactors/refactor_types.cpp", "src/references.cpp", + "src/register_code_fix/add_local_variable.cpp", "src/register_code_fix/add_missing_declare_property.cpp", "src/register_code_fix/add_missing_new_operator.cpp", "src/register_code_fix/constructor_for_derived_need_super_call.cpp", diff --git a/ets2panda/lsp/CMakeLists.txt b/ets2panda/lsp/CMakeLists.txt index 384341c5a66e0b5db8f0f6326772083bc1488875..b1320afd8171fc1616cb1038fa71267089a07f51 100644 --- a/ets2panda/lsp/CMakeLists.txt +++ b/ets2panda/lsp/CMakeLists.txt @@ -110,6 +110,7 @@ set(ES2PANDA_LSP_SRC ./src/types.cpp ./src/navigate_to.cpp ./src/code_fix_provider.cpp + ./src/register_code_fix/add_local_variable.cpp ./src/register_code_fix/add_missing_new_operator.cpp ./src/register_code_fix/fix_class_doesnt_implement_inherited_abstract_member.cpp ./src/register_code_fix/fix_expected_comma.cpp diff --git a/ets2panda/lsp/include/register_code_fix/add_local_variable.h b/ets2panda/lsp/include/register_code_fix/add_local_variable.h new file mode 100644 index 0000000000000000000000000000000000000000..79bad1cc4c788812363275998c43fe8d407f1ae5 --- /dev/null +++ b/ets2panda/lsp/include/register_code_fix/add_local_variable.h @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ADD_LOCAL_VARIABLE_H +#define ADD_LOCAL_VARIABLE_H + +#include +#include + +#include "lsp/include/code_fixes/code_fix_types.h" +#include "lsp/include/services/text_change/change_tracker.h" +#include "lsp/include/types.h" + +namespace ark::es2panda::lsp { + +class AddLocalVariable : public CodeFixRegistration { +public: + AddLocalVariable(); + + std::vector GetCodeActions(const CodeFixContext &context) override; + CombinedCodeActions GetAllCodeActions(const CodeFixAllContext &codeFixAll) override; + +private: + void MakeChangeForAddLocalVariable(ChangeTracker &changeTracker, es2panda_Context *context, size_t pos); + std::vector GetCodeActionsToAddLocalVariable(const CodeFixContext &context); + + std::string DetermineVariableType(ir::AstNode *unresolvedNode); + std::string GetTypeFromDirectAssignment(ir::AstNode *unresolvedNode, ir::AstNode *parent); + std::string GetTypeFromMemberAssignment(ir::AstNode *unresolvedNode, ir::AstNode *parent); + std::string InferTypeFromExpression(ir::AstNode *expression); + std::string InferTypeFromLiteral(ir::AstNode *expression); + std::string InferTypeFromComplexExpression(ir::AstNode *expression); + std::string InferTypeFromBinaryExpression(ir::AstNode *expression); + std::string InferTypeFromOtherExpressions(ir::AstNode *expression); + std::string GenerateVariableDeclaration(const std::string &variableName, const std::string &variableType); + ir::AstNode *FindInsertionPoint(ir::AstNode *unresolvedNode, bool isThisProperty); + ir::AstNode *FindClassInsertionPoint(ir::AstNode *current); + ir::AstNode *FindFunctionInsertionPoint(ir::AstNode *current); + ir::AstNode *GetFunctionBody(ir::AstNode *node); + bool IsThisPropertyAccess(es2panda_Context *context, size_t pos); +}; + +} // namespace ark::es2panda::lsp + +#endif \ No newline at end of file diff --git a/ets2panda/lsp/src/register_code_fix/add_local_variable.cpp b/ets2panda/lsp/src/register_code_fix/add_local_variable.cpp new file mode 100644 index 0000000000000000000000000000000000000000..22cfec61702d450f8f3b57e7e2d924f18d7c5375 --- /dev/null +++ b/ets2panda/lsp/src/register_code_fix/add_local_variable.cpp @@ -0,0 +1,430 @@ +/** + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "lsp/include/register_code_fix/add_local_variable.h" + +#include +#include "generated/code_fix_register.h" +#include "lsp/include/code_fix_provider.h" +#include "lsp/include/internal_api.h" +#include "ir/astNode.h" +#include "ir/base/classDefinition.h" +#include "ir/expressions/identifier.h" +#include "ir/statements/functionDeclaration.h" +#include "ir/base/methodDefinition.h" + +namespace ark::es2panda::lsp { +using codefixes::ADD_LOCAL_VARIABLE; +using codefixes::ADD_LOCAL_VARIABLE_FOR_CLASS; + +namespace { +constexpr const char *OBJECT_TYPE = "Object"; +constexpr const char *DOUBLE_TYPE = "Double"; +constexpr const char *STRING_TYPE = "String"; +constexpr const char *BOOLEAN_TYPE = "Boolean"; +constexpr const char *BIGINT_TYPE = "BigInt"; +constexpr const char *CHAR_TYPE = "Char"; +constexpr const char *ARRAY_SUFFIX = "[]"; +constexpr const char *LET_KEYWORD = "let "; +constexpr const char *TYPE_SEPARATOR = ": "; +constexpr const char *STATEMENT_TERMINATOR = ";"; +constexpr const char *INDENT = " "; +constexpr const char *CLASS_FIELD_DESCRIPTION = "Add class field declaration"; +constexpr const char *CLASS_FIELD_FIX_ALL_DESCRIPTION = "Add all missing class fields"; +constexpr const char *LOCAL_VARIABLE_DESCRIPTION = "Add local variable declaration"; +constexpr const char *LOCAL_VARIABLE_FIX_ALL_DESCRIPTION = "Add all missing variable declarations"; +} // namespace + +std::string AddLocalVariable::GetTypeFromDirectAssignment(ir::AstNode *unresolvedNode, ir::AstNode *parent) +{ + if (!parent->IsAssignmentExpression()) { + return ""; + } + + auto *assignment = parent->AsAssignmentExpression(); + if (assignment->Left() != unresolvedNode) { + return ""; + } + + auto *rightSide = assignment->Right(); + if (rightSide == nullptr) { + return ""; + } + + return InferTypeFromExpression(rightSide); +} + +std::string AddLocalVariable::GetTypeFromMemberAssignment(ir::AstNode *unresolvedNode, ir::AstNode *parent) +{ + if (!parent->IsMemberExpression()) { + return ""; + } + + auto *memberExpr = parent->AsMemberExpression(); + if (memberExpr->Property() != unresolvedNode) { + return ""; + } + + auto *memberParent = memberExpr->Parent(); + if (memberParent == nullptr || !memberParent->IsAssignmentExpression()) { + return ""; + } + + auto *assignment = memberParent->AsAssignmentExpression(); + if (assignment->Left() != memberExpr) { + return ""; + } + + auto *rightSide = assignment->Right(); + if (rightSide == nullptr) { + return ""; + } + + return InferTypeFromExpression(rightSide); +} + +std::string AddLocalVariable::DetermineVariableType(ir::AstNode *unresolvedNode) +{ + if (unresolvedNode == nullptr) { + return OBJECT_TYPE; + } + + auto *parent = unresolvedNode->Parent(); + if (parent == nullptr) { + return OBJECT_TYPE; + } + + std::string directType = GetTypeFromDirectAssignment(unresolvedNode, parent); + if (!directType.empty()) { + return directType; + } + + std::string memberType = GetTypeFromMemberAssignment(unresolvedNode, parent); + if (!memberType.empty()) { + return memberType; + } + + return OBJECT_TYPE; +} + +std::string AddLocalVariable::InferTypeFromLiteral(ir::AstNode *expression) +{ + if (expression->IsNumberLiteral()) { + return DOUBLE_TYPE; + } + + if (expression->IsStringLiteral()) { + return STRING_TYPE; + } + + if (expression->IsBooleanLiteral()) { + return BOOLEAN_TYPE; + } + + if (expression->IsBigIntLiteral()) { + return BIGINT_TYPE; + } + + if (expression->IsCharLiteral()) { + return CHAR_TYPE; + } + + if (expression->IsNullLiteral()) { + return OBJECT_TYPE; + } + + if (expression->IsUndefinedLiteral()) { + return OBJECT_TYPE; + } + + return ""; +} + +std::string AddLocalVariable::InferTypeFromBinaryExpression(ir::AstNode *expression) +{ + auto *binary = expression->AsBinaryExpression(); + auto leftType = InferTypeFromExpression(binary->Left()); + auto rightType = InferTypeFromExpression(binary->Right()); + if (leftType == BIGINT_TYPE || rightType == BIGINT_TYPE) { + return BIGINT_TYPE; + } + if (leftType == DOUBLE_TYPE || rightType == DOUBLE_TYPE) { + return DOUBLE_TYPE; + } + if (leftType == STRING_TYPE || rightType == STRING_TYPE) { + return STRING_TYPE; + } + if (leftType == BOOLEAN_TYPE && rightType == BOOLEAN_TYPE) { + return BOOLEAN_TYPE; + } + if (leftType == CHAR_TYPE || rightType == CHAR_TYPE) { + return CHAR_TYPE; + } + + return OBJECT_TYPE; +} + +std::string AddLocalVariable::InferTypeFromOtherExpressions(ir::AstNode *expression) +{ + if (expression->IsArrayExpression()) { + auto *arrayExpr = expression->AsArrayExpression(); + auto elements = arrayExpr->Elements(); + if (!elements.empty()) { + auto elementType = InferTypeFromExpression(elements[0]); + return elementType + std::string(ARRAY_SUFFIX); + } + return std::string(OBJECT_TYPE) + ARRAY_SUFFIX; + } + + if (expression->IsCallExpression()) { + return OBJECT_TYPE; + } + + if (expression->IsObjectExpression()) { + return OBJECT_TYPE; + } + + if (expression->IsThisExpression()) { + return OBJECT_TYPE; + } + + if (expression->IsNewExpression()) { + return OBJECT_TYPE; + } + + return OBJECT_TYPE; +} + +std::string AddLocalVariable::InferTypeFromComplexExpression(ir::AstNode *expression) +{ + if (expression->IsBinaryExpression()) { + return InferTypeFromBinaryExpression(expression); + } + + return InferTypeFromOtherExpressions(expression); +} + +std::string AddLocalVariable::InferTypeFromExpression(ir::AstNode *expression) +{ + if (expression == nullptr) { + return OBJECT_TYPE; + } + + std::string literalType = InferTypeFromLiteral(expression); + if (!literalType.empty()) { + return literalType; + } + + return InferTypeFromComplexExpression(expression); +} + +std::string AddLocalVariable::GenerateVariableDeclaration(const std::string &variableName, + const std::string &variableType) +{ + return std::string(LET_KEYWORD) + variableName + TYPE_SEPARATOR + variableType + STATEMENT_TERMINATOR; +} + +bool AddLocalVariable::IsThisPropertyAccess(es2panda_Context *context, size_t pos) +{ + auto *token = GetTouchingToken(context, pos, false); + if (token == nullptr || !token->IsIdentifier()) { + return false; + } + + auto *parent = token->Parent(); + if (parent == nullptr) { + return false; + } + + if (parent->IsMemberExpression()) { + auto *memberExpr = parent->AsMemberExpression(); + if (memberExpr->Object() != nullptr && memberExpr->Object()->IsThisExpression()) { + return true; + } + } + + return false; +} + +ir::AstNode *AddLocalVariable::FindClassInsertionPoint(ir::AstNode *current) +{ + while (current != nullptr) { + if (current->IsClassDefinition()) { + return current; + } + current = current->Parent(); + } + return nullptr; +} + +ir::AstNode *AddLocalVariable::FindFunctionInsertionPoint(ir::AstNode *current) +{ + while (current != nullptr) { + if (current->IsBlockStatement()) { + return current; + } + + ir::AstNode *functionBody = GetFunctionBody(current); + if (functionBody != nullptr) { + return functionBody; + } + + current = current->Parent(); + } + return nullptr; +} + +ir::AstNode *AddLocalVariable::GetFunctionBody(ir::AstNode *node) +{ + if (!node->IsFunctionDeclaration() && !node->IsMethodDefinition()) { + return nullptr; + } + + auto *functionNode = node->IsFunctionDeclaration() ? node->AsFunctionDeclaration()->Function() + : node->AsMethodDefinition()->Function(); + + if (functionNode == nullptr || functionNode->Body() == nullptr) { + return nullptr; + } + + return functionNode->Body(); +} + +ir::AstNode *AddLocalVariable::FindInsertionPoint(ir::AstNode *unresolvedNode, bool isThisProperty) +{ + if (unresolvedNode == nullptr) { + return nullptr; + } + + if (isThisProperty) { + return FindClassInsertionPoint(unresolvedNode); + } + + return FindFunctionInsertionPoint(unresolvedNode); +} + +void AddLocalVariable::MakeChangeForAddLocalVariable(ChangeTracker &changeTracker, es2panda_Context *context, + size_t pos) +{ + auto *token = GetTouchingToken(context, pos, false); + if (token == nullptr || !token->IsIdentifier()) { + return; + } + + auto *identifier = token->AsIdentifier(); + std::string variableName = std::string(identifier->Name()); + + bool isThisProperty = IsThisPropertyAccess(context, pos); + + auto *insertionPoint = FindInsertionPoint(token, isThisProperty); + if (insertionPoint == nullptr) { + return; + } + + std::string variableType = DetermineVariableType(token); + std::string declaration; + size_t insertPos = insertionPoint->Start().index; + + auto *ctx = reinterpret_cast(context); + auto sourceCode = std::string(ctx->parserProgram->SourceCode()); + + size_t bracePos = sourceCode.find('{', insertPos); + if (bracePos != std::string::npos) { + insertPos = bracePos + 1; + } + + if (isThisProperty) { + declaration = std::string(INDENT) + variableName + TYPE_SEPARATOR + variableType + STATEMENT_TERMINATOR; + } else { + declaration = std::string(INDENT) + GenerateVariableDeclaration(variableName, variableType); + } + + TextRange insertRange = {insertPos, insertPos}; + auto astContext = reinterpret_cast(context); + changeTracker.ReplaceRangeWithText(astContext->sourceFile, insertRange, declaration); +} + +std::vector AddLocalVariable::GetCodeActionsToAddLocalVariable(const CodeFixContext &context) +{ + TextChangesContext textChangesContext = {context.host, context.formatContext, context.preferences}; + auto fileTextChanges = ChangeTracker::With(textChangesContext, [&](ChangeTracker &tracker) { + MakeChangeForAddLocalVariable(tracker, context.context, context.span.start); + }); + + return fileTextChanges; +} + +AddLocalVariable::AddLocalVariable() +{ + auto functionErrorCodes = ADD_LOCAL_VARIABLE.GetSupportedCodeNumbers(); + auto classErrorCodes = ADD_LOCAL_VARIABLE_FOR_CLASS.GetSupportedCodeNumbers(); + + std::vector allErrorCodes; + allErrorCodes.insert(allErrorCodes.end(), functionErrorCodes.begin(), functionErrorCodes.end()); + allErrorCodes.insert(allErrorCodes.end(), classErrorCodes.begin(), classErrorCodes.end()); + + SetErrorCodes(allErrorCodes); + SetFixIds({ADD_LOCAL_VARIABLE.GetFixId().data(), ADD_LOCAL_VARIABLE_FOR_CLASS.GetFixId().data()}); +} + +std::vector AddLocalVariable::GetCodeActions(const CodeFixContext &context) +{ + std::vector returnedActions; + auto changes = GetCodeActionsToAddLocalVariable(context); + if (!changes.empty()) { + CodeFixAction codeAction; + + bool isThisProperty = IsThisPropertyAccess(context.context, context.span.start); + if (isThisProperty) { + codeAction.fixName = ADD_LOCAL_VARIABLE_FOR_CLASS.GetFixId().data(); + codeAction.fixId = ADD_LOCAL_VARIABLE_FOR_CLASS.GetFixId().data(); + codeAction.description = CLASS_FIELD_DESCRIPTION; + codeAction.fixAllDescription = CLASS_FIELD_FIX_ALL_DESCRIPTION; + } else { + codeAction.fixName = ADD_LOCAL_VARIABLE.GetFixId().data(); + codeAction.fixId = ADD_LOCAL_VARIABLE.GetFixId().data(); + codeAction.description = LOCAL_VARIABLE_DESCRIPTION; + codeAction.fixAllDescription = LOCAL_VARIABLE_FIX_ALL_DESCRIPTION; + } + + codeAction.changes = changes; + returnedActions.push_back(codeAction); + } + + return returnedActions; +} + +CombinedCodeActions AddLocalVariable::GetAllCodeActions(const CodeFixAllContext &codeFixAllCtx) +{ + CodeFixProvider provider; + const auto changes = provider.CodeFixAll( + codeFixAllCtx, GetErrorCodes(), [&](ChangeTracker &tracker, const DiagnosticWithLocation &diag) { + MakeChangeForAddLocalVariable(tracker, codeFixAllCtx.context, diag.GetStart()); + }); + + CombinedCodeActions combinedCodeActions; + combinedCodeActions.changes = changes.changes; + combinedCodeActions.commands = changes.commands; + + return combinedCodeActions; +} + +// NOLINTNEXTLINE(fuchsia-statically-constructed-objects, cert-err58-cpp) +AutoCodeFixRegister g_addLocalVariable(ADD_LOCAL_VARIABLE.GetFixId().data()); + +// NOLINTNEXTLINE(fuchsia-statically-constructed-objects, cert-err58-cpp) +AutoCodeFixRegister g_addLocalVariableForClass(ADD_LOCAL_VARIABLE_FOR_CLASS.GetFixId().data()); + +} // namespace ark::es2panda::lsp diff --git a/ets2panda/test/unit/lsp/CMakeLists.txt b/ets2panda/test/unit/lsp/CMakeLists.txt index a7968df400353f7af1cdca03514eac298ef8dac3..428b3951716f3fe734e79aab3446533acc67e4d0 100644 --- a/ets2panda/test/unit/lsp/CMakeLists.txt +++ b/ets2panda/test/unit/lsp/CMakeLists.txt @@ -311,6 +311,10 @@ ets2panda_add_gtest(lsp_api_add_missing_new_operator CPP_SOURCES add_missing_new_operator_test.cpp ) +ets2panda_add_gtest(lsp_api_test_add_local_variable CPP_SOURCES + add_local_variable_test.cpp +) + ets2panda_add_gtest(lsp_get_import_file_path_test CPP_SOURCES get_import_file_path_test.cpp ) diff --git a/ets2panda/test/unit/lsp/add_local_variable_test.cpp b/ets2panda/test/unit/lsp/add_local_variable_test.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d0dda3ba0d83f0efbb906475b68a1b2b9754cd1d --- /dev/null +++ b/ets2panda/test/unit/lsp/add_local_variable_test.cpp @@ -0,0 +1,250 @@ +/** + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "lsp_api_test.h" + +#include + +#include "lsp/include/api.h" +#include "lsp/include/cancellation_token.h" +#include "lsp/include/register_code_fix/add_local_variable.h" + +namespace { + +using ark::es2panda::lsp::Initializer; +using ark::es2panda::lsp::codefixes::ADD_LOCAL_VARIABLE; +using ark::es2panda::lsp::codefixes::ADD_LOCAL_VARIABLE_FOR_CLASS; + +constexpr std::string_view EXPECTED_FUNCTION_FIX_NAME = ADD_LOCAL_VARIABLE.GetFixId(); +constexpr std::string_view EXPECTED_CLASS_FIX_NAME = ADD_LOCAL_VARIABLE_FOR_CLASS.GetFixId(); +constexpr auto FUNCTION_ERROR_CODES = ADD_LOCAL_VARIABLE.GetSupportedCodeNumbers(); +constexpr auto CLASS_ERROR_CODES = ADD_LOCAL_VARIABLE_FOR_CLASS.GetSupportedCodeNumbers(); +constexpr std::string_view EXPECTED_FUNCTION_FIX_DESCRIPTION = "Add local variable declaration"; +constexpr std::string_view EXPECTED_CLASS_FIX_DESCRIPTION = "Add class field declaration"; +constexpr int DEFAULT_THROTTLE = 20; + +class AddLocalVariableTests : public LSPAPITests { +public: + static ark::es2panda::lsp::CancellationToken CreateNonCancellationToken() + { + return ark::es2panda::lsp::CancellationToken(DEFAULT_THROTTLE, &GetNullHost()); + } + + static size_t LineColToPos(es2panda_Context *context, const size_t line, const size_t col) + { + auto ctx = reinterpret_cast(context); + auto index = ark::es2panda::lexer::LineIndex(ctx->parserProgram->SourceCode()); + auto pos = index.GetOffset(ark::es2panda::lexer::SourceLocation(line, col, ctx->parserProgram)); + return pos; + } + + struct ExpectedCodeFixInfo { + size_t textChangeStart; + size_t textChangeLength; + std::string fileName; + std::string newText; + std::string_view fixName; + std::string_view description; + }; + + static void ValidateCodeFixActionInfo(const CodeFixActionInfo &info, const ExpectedCodeFixInfo &expected) + { + ASSERT_EQ(info.fixName_, expected.fixName); + ASSERT_EQ(info.fixId_, expected.fixName); + ASSERT_EQ(info.description_, expected.description); + ASSERT_EQ(info.changes_[0].fileName, expected.fileName); + ASSERT_EQ(info.changes_[0].textChanges[0].span.start, expected.textChangeStart); + ASSERT_EQ(info.changes_[0].textChanges[0].span.length, expected.textChangeLength); + ASSERT_EQ(info.changes_[0].textChanges[0].newText, expected.newText); + } + +private: + class NullCancellationToken : public ark::es2panda::lsp::HostCancellationToken { + public: + bool IsCancellationRequested() override + { + return false; + } + }; + + static NullCancellationToken &GetNullHost() + { + static NullCancellationToken instance; + return instance; + } +}; + +TEST_F(AddLocalVariableTests, TestAddLocalVariableInFunction) +{ + std::vector fileNames = {"TestAddLocalVariableInFunction.ets"}; + std::vector fileContents = {R"( +function calculate() { + result = 10 + 5; +} +)"}; + auto filePaths = CreateTempFile(fileNames, fileContents); + ASSERT_EQ(fileNames.size(), filePaths.size()); + + Initializer initializer = Initializer(); + auto *context = initializer.CreateContext(filePaths[0].c_str(), ES2PANDA_STATE_CHECKED); + const size_t start = LineColToPos(context, 3, 5); + const size_t length = 6; + const size_t expectedTextChangeStart = 23; + const size_t expectedTextChangeLength = 0; + const std::string expectedNewText = " let result: Double;"; + const int expectedFixResultSize = 1; + + std::vector errorCodes(FUNCTION_ERROR_CODES.begin(), FUNCTION_ERROR_CODES.end()); + CodeFixOptions emptyOptions = {CreateNonCancellationToken(), ark::es2panda::lsp::FormatCodeSettings(), {}}; + auto fixResult = + ark::es2panda::lsp::GetCodeFixesAtPositionImpl(context, start, start + length, errorCodes, emptyOptions); + ASSERT_EQ(fixResult.size(), expectedFixResultSize); + + ExpectedCodeFixInfo expected = { + expectedTextChangeStart, expectedTextChangeLength, filePaths[0], + expectedNewText, EXPECTED_FUNCTION_FIX_NAME, EXPECTED_FUNCTION_FIX_DESCRIPTION}; + ValidateCodeFixActionInfo(fixResult[0], expected); + + initializer.DestroyContext(context); +} + +TEST_F(AddLocalVariableTests, TestAddLocalVariableInEmptyClass) +{ + std::vector fileNames = {"TestAddLocalVariableInEmptyClass.ets"}; + std::vector fileContents = {R"( +class MyComponent { + build() { + this.title = "Hello"; + } +} +)"}; + auto filePaths = CreateTempFile(fileNames, fileContents); + ASSERT_EQ(fileNames.size(), filePaths.size()); + + Initializer initializer = Initializer(); + auto *context = initializer.CreateContext(filePaths[0].c_str(), ES2PANDA_STATE_CHECKED); + const size_t start = LineColToPos(context, 4, 14); + const size_t length = 5; + const size_t expectedTextChangeStart = 20; + const size_t expectedTextChangeLength = 0; + const std::string expectedNewText = " title: String;"; + const int expectedFixResultSize = 1; + + std::vector errorCodes(CLASS_ERROR_CODES.begin(), CLASS_ERROR_CODES.end()); + CodeFixOptions emptyOptions = {CreateNonCancellationToken(), ark::es2panda::lsp::FormatCodeSettings(), {}}; + auto fixResult = + ark::es2panda::lsp::GetCodeFixesAtPositionImpl(context, start, start + length, errorCodes, emptyOptions); + ASSERT_EQ(fixResult.size(), expectedFixResultSize); + + ExpectedCodeFixInfo expected = {expectedTextChangeStart, expectedTextChangeLength, filePaths[0], + expectedNewText, EXPECTED_CLASS_FIX_NAME, EXPECTED_CLASS_FIX_DESCRIPTION}; + ValidateCodeFixActionInfo(fixResult[0], expected); + + initializer.DestroyContext(context); +} + +TEST_F(AddLocalVariableTests, TestAddLocalVariableInClassWithMembers) +{ + std::vector fileNames = {"TestAddLocalVariableInClassWithMembers.ets"}; + std::vector fileContents = {R"( +class MyComponent { + name: String = "test"; + age: Double = 25; + + constructor() { + } + + build() { + this.title = "Hello"; + } +} +)"}; + auto filePaths = CreateTempFile(fileNames, fileContents); + ASSERT_EQ(fileNames.size(), filePaths.size()); + + Initializer initializer = Initializer(); + auto *context = initializer.CreateContext(filePaths[0].c_str(), ES2PANDA_STATE_CHECKED); + const size_t start = LineColToPos(context, 10, 14); + const size_t length = 5; + const size_t expectedTextChangeStart = 20; + const size_t expectedTextChangeLength = 0; + const std::string expectedNewText = " title: String;"; + const int expectedFixResultSize = 1; + + std::vector errorCodes(CLASS_ERROR_CODES.begin(), CLASS_ERROR_CODES.end()); + CodeFixOptions emptyOptions = {CreateNonCancellationToken(), ark::es2panda::lsp::FormatCodeSettings(), {}}; + auto fixResult = + ark::es2panda::lsp::GetCodeFixesAtPositionImpl(context, start, start + length, errorCodes, emptyOptions); + ASSERT_EQ(fixResult.size(), expectedFixResultSize); + + ExpectedCodeFixInfo expected = {expectedTextChangeStart, expectedTextChangeLength, filePaths[0], + expectedNewText, EXPECTED_CLASS_FIX_NAME, EXPECTED_CLASS_FIX_DESCRIPTION}; + ValidateCodeFixActionInfo(fixResult[0], expected); + + initializer.DestroyContext(context); +} + +TEST_F(AddLocalVariableTests, TestAddMultipleLocalVariables) +{ + std::vector fileNames = {"TestAddMultipleLocalVariables.ets"}; + std::vector fileContents = {R"( +function process() { + count = 5; + message = "done"; + return count + message.length; +} +)"}; + auto filePaths = CreateTempFile(fileNames, fileContents); + ASSERT_EQ(fileNames.size(), filePaths.size()); + + Initializer initializer = Initializer(); + auto *context = initializer.CreateContext(filePaths[0].c_str(), ES2PANDA_STATE_CHECKED); + + const size_t start1 = LineColToPos(context, 3, 5); + const size_t length1 = 5; + const size_t start2 = LineColToPos(context, 4, 5); + const size_t length2 = 7; + const size_t expectedTextChangeStart1 = 21; + const size_t expectedTextChangeLength1 = 0; + const std::string expectedNewText1 = " let count: Double;"; + const size_t expectedTextChangeStart2 = 21; + const size_t expectedTextChangeLength2 = 0; + const std::string expectedNewText2 = " let message: String;"; + const int expectedFixResultSize = 1; + + std::vector errorCodes(FUNCTION_ERROR_CODES.begin(), FUNCTION_ERROR_CODES.end()); + CodeFixOptions emptyOptions = {CreateNonCancellationToken(), ark::es2panda::lsp::FormatCodeSettings(), {}}; + + auto fixResult1 = + ark::es2panda::lsp::GetCodeFixesAtPositionImpl(context, start1, start1 + length1, errorCodes, emptyOptions); + auto fixResult2 = + ark::es2panda::lsp::GetCodeFixesAtPositionImpl(context, start2, start2 + length2, errorCodes, emptyOptions); + + ASSERT_EQ(fixResult1.size(), expectedFixResultSize); + ASSERT_EQ(fixResult2.size(), expectedFixResultSize); + + ExpectedCodeFixInfo expected1 = { + expectedTextChangeStart1, expectedTextChangeLength1, filePaths[0], + expectedNewText1, EXPECTED_FUNCTION_FIX_NAME, EXPECTED_FUNCTION_FIX_DESCRIPTION}; + ExpectedCodeFixInfo expected2 = { + expectedTextChangeStart2, expectedTextChangeLength2, filePaths[0], + expectedNewText2, EXPECTED_FUNCTION_FIX_NAME, EXPECTED_FUNCTION_FIX_DESCRIPTION}; + ValidateCodeFixActionInfo(fixResult1[0], expected1); + ValidateCodeFixActionInfo(fixResult2[0], expected2); + + initializer.DestroyContext(context); +} + +} // namespace \ No newline at end of file diff --git a/ets2panda/util/diagnostic/semantic.yaml b/ets2panda/util/diagnostic/semantic.yaml index d9295dba3c87130819074312737f3d9a1d845b63..c4cd085ee57f52234a4d1d102b8668c48e1249f1 100644 --- a/ets2panda/util/diagnostic/semantic.yaml +++ b/ets2panda/util/diagnostic/semantic.yaml @@ -1090,6 +1090,7 @@ semantic: - name: PROPERTY_NONEXISTENT id: 87 message: "Property '{}' does not exist on type '{}'" + code_fix_ids: [AddLocalVariableForClass] - name: PROP_ACCESS_WITHOUT_THIS id: 145 @@ -1396,7 +1397,7 @@ semantic: - name: UNRESOLVED_REF id: 143 message: "Unresolved reference {}" - code_fix_ids: [FixSpelling] + code_fix_ids: [FixSpelling,AddLocalVariable] - name: UNSUPPORTED_CLASS_LITERAL id: 20