diff --git a/ets2panda/lsp/BUILD.gn b/ets2panda/lsp/BUILD.gn index b95bffb17d38e483246cd469a9871079bf10434b..1c7c69ea080c82850413cc0fe19e08216212d244 100644 --- a/ets2panda/lsp/BUILD.gn +++ b/ets2panda/lsp/BUILD.gn @@ -89,8 +89,8 @@ ohos_source_set("libes2panda_lsp_static") { "src/refactors/refactor_types.cpp", "src/references.cpp", "src/register_code_fix/add_missing_declare_property.cpp", - "src/register_code_fix/constructor_for_derived_need_super_call.cpp", "src/register_code_fix/add_missing_new_operator.cpp", + "src/register_code_fix/constructor_for_derived_need_super_call.cpp", "src/register_code_fix/convert_const_to_let.cpp", "src/register_code_fix/fix_add_function_return_statement.cpp", "src/register_code_fix/fix_class_doesnt_implement_inherited_abstract_member.cpp", @@ -98,10 +98,11 @@ ohos_source_set("libes2panda_lsp_static") { "src/register_code_fix/fix_class_super_must_precede_this_access.cpp", "src/register_code_fix/fix_expected_comma.cpp", "src/register_code_fix/fix_extends_interface_becomes_implements.cpp", + "src/register_code_fix/fix_import_non_exported_member.cpp", "src/register_code_fix/fix_missing_call_parantheses.cpp", "src/register_code_fix/fix_nan_equality.cpp", - "src/register_code_fix/fix_return_type_in_async_function.cpp", "src/register_code_fix/fix_remove_override_modifier.cpp", + "src/register_code_fix/fix_return_type_in_async_function.cpp", "src/register_code_fix/forgotten_this_property_access.cpp", "src/register_code_fix/import_fixes.cpp", "src/register_code_fix/remove_accidental_call_parentheses.cpp", diff --git a/ets2panda/lsp/CMakeLists.txt b/ets2panda/lsp/CMakeLists.txt index e64e2249cc4174d14a04d2abedcec2684ce36b22..bba265017776442c00a386f58651176280bcca8b 100644 --- a/ets2panda/lsp/CMakeLists.txt +++ b/ets2panda/lsp/CMakeLists.txt @@ -120,6 +120,7 @@ set(ES2PANDA_LSP_SRC ./src/register_code_fix/convert_const_to_let.cpp ./src/register_code_fix/constructor_for_derived_need_super_call ./src/register_code_fix/fix_missing_call_parantheses.cpp + ./src/register_code_fix/fix_import_non_exported_member.cpp ./src/register_code_fix/fix_nan_equality.cpp ./src/register_code_fix/forgotten_this_property_access.cpp ./src/register_code_fix/import_fixes.cpp diff --git a/ets2panda/lsp/include/register_code_fix/fix_import_non_exported_member.h b/ets2panda/lsp/include/register_code_fix/fix_import_non_exported_member.h new file mode 100644 index 0000000000000000000000000000000000000000..10ef49e69a348c44ded688e5d064f7f7d8449db8 --- /dev/null +++ b/ets2panda/lsp/include/register_code_fix/fix_import_non_exported_member.h @@ -0,0 +1,43 @@ +/** + * 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 FIX_IMPORT_NON_EXPORTED_MEMBER_H +#define FIX_IMPORT_NON_EXPORTED_MEMBER_H + +#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 FixImportNonExportedMember : public CodeFixRegistration { +public: + FixImportNonExportedMember(); + + std::vector GetCodeActions(const CodeFixContext &context) override; + CombinedCodeActions GetAllCodeActions(const CodeFixAllContext &codeFixAll) override; + +private: + void MakeChangeForImportNonExportedMember(ChangeTracker &changeTracker, es2panda_Context *context, size_t pos); + bool FindImportDeclaration(ir::AstNode *&importDeclNode); + bool FindFunctionName(ir::AstNode *importDeclNode, util::StringView &functionName); + void ProcessExportPosition(ir::AstNode *funcDecl, const util::StringView &functionName, size_t &exportPosition, + ChangeTracker &changeTracker); + std::vector GetCodeActionsToImportNonExportedMember(const CodeFixContext &context); +}; + +} // namespace ark::es2panda::lsp +#endif \ No newline at end of file diff --git a/ets2panda/lsp/src/register_code_fix/fix_import_non_exported_member.cpp b/ets2panda/lsp/src/register_code_fix/fix_import_non_exported_member.cpp new file mode 100644 index 0000000000000000000000000000000000000000..52691ff6d2bd8015c23a18d7403731471754e8bb --- /dev/null +++ b/ets2panda/lsp/src/register_code_fix/fix_import_non_exported_member.cpp @@ -0,0 +1,151 @@ +/** + * 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/fix_import_non_exported_member.h" +#include + +#include "compiler/lowering/util.h" +#include "generated/code_fix_register.h" +#include "lsp/include/code_fix_provider.h" +#include "lsp/include/internal_api.h" + +namespace ark::es2panda::lsp { +using codefixes::FIX_IMPORT_NON_EXPORTED_MEMBER; + +void FixImportNonExportedMember::MakeChangeForImportNonExportedMember(ChangeTracker &changeTracker, + es2panda_Context *context, size_t pos) +{ + auto *token = GetTouchingToken(context, pos, false); + if (token == nullptr) { + return; + } + + auto *importDeclNode = token; + util::StringView functionName; + + if (!FindImportDeclaration(importDeclNode)) { + return; + } + + if (!FindFunctionName(importDeclNode, functionName)) { + return; + } + + size_t exportPosition = 0; + + if (importDeclNode->IsETSImportDeclaration()) { + auto funcDecl = importDeclNode->AsETSImportDeclaration(); + if (!funcDecl->Specifiers().empty()) { + ProcessExportPosition(funcDecl, functionName, exportPosition, changeTracker); + } + } +} + +bool FixImportNonExportedMember::FindImportDeclaration(ir::AstNode *&importDeclNode) +{ + while (importDeclNode != nullptr && !importDeclNode->IsETSImportDeclaration()) { + importDeclNode = importDeclNode->Parent(); + } + return (importDeclNode != nullptr); +} + +bool FixImportNonExportedMember::FindFunctionName(ir::AstNode *importDeclNode, util::StringView &functionName) +{ + importDeclNode->FindChild([&](ir::AstNode *n) { + if (n->IsIdentifier() && n->Parent()->IsImportSpecifier()) { + functionName = n->AsIdentifier()->Name(); + return true; + } + return false; + }); + return !functionName.Empty(); +} + +void FixImportNonExportedMember::ProcessExportPosition(ir::AstNode *funcDecl, const util::StringView &functionName, + size_t &exportPosition, ChangeTracker &changeTracker) +{ + Initializer initializer = Initializer(); + const auto path = funcDecl->AsETSImportDeclaration()->ResolvedSource(); + auto targetContext = initializer.CreateContext(std::string(path).c_str(), ES2PANDA_STATE_CHECKED); + auto cctx = reinterpret_cast(targetContext); + auto nodeOfAllContext = cctx->parserProgram->Ast(); + if (targetContext == nullptr || nodeOfAllContext == nullptr) { + return; + } + + nodeOfAllContext->FindChild([&](ir::AstNode *n) { + if (n->IsIdentifier() && n->AsIdentifier()->Name() == functionName) { + exportPosition = n->Parent()->Start().index; + return true; + } + return false; + }); + + const std::string replaceText("export"); + changeTracker.ReplaceRangeWithText(cctx->sourceFile, {exportPosition, exportPosition}, replaceText); +} + +std::vector FixImportNonExportedMember::GetCodeActionsToImportNonExportedMember( + const CodeFixContext &context) +{ + TextChangesContext textChangesContext = {context.host, context.formatContext, context.preferences}; + auto fileTextChanges = ChangeTracker::With(textChangesContext, [&](ChangeTracker &tracker) { + MakeChangeForImportNonExportedMember(tracker, context.context, context.span.start); + }); + + return fileTextChanges; +} + +FixImportNonExportedMember::FixImportNonExportedMember() +{ + auto errorCodes = FIX_IMPORT_NON_EXPORTED_MEMBER.GetSupportedCodeNumbers(); + SetErrorCodes({errorCodes.begin(), errorCodes.end()}); + SetFixIds({FIX_IMPORT_NON_EXPORTED_MEMBER.GetFixId().data()}); +} + +std::vector FixImportNonExportedMember::GetCodeActions(const CodeFixContext &context) +{ + std::vector returnedActions; + auto changes = GetCodeActionsToImportNonExportedMember(context); + if (!changes.empty()) { + CodeFixAction codeAction; + codeAction.fixName = FIX_IMPORT_NON_EXPORTED_MEMBER.GetFixId().data(); + codeAction.description = "Fix Import Non Exported Member"; + codeAction.changes = changes; + codeAction.fixId = FIX_IMPORT_NON_EXPORTED_MEMBER.GetFixId().data(); + returnedActions.push_back(codeAction); + } + + return returnedActions; +} + +CombinedCodeActions FixImportNonExportedMember::GetAllCodeActions(const CodeFixAllContext &codeFixAllCtx) +{ + CodeFixProvider provider; + const auto changes = provider.CodeFixAll( + codeFixAllCtx, GetErrorCodes(), [&](ChangeTracker &tracker, const DiagnosticWithLocation &diag) { + MakeChangeForImportNonExportedMember(tracker, codeFixAllCtx.context, diag.GetStart()); + }); + + CombinedCodeActions combinedCodeActions; + combinedCodeActions.changes = changes.changes; + combinedCodeActions.commands = changes.commands; + + return combinedCodeActions; +} + +// NOLINTNEXTLINE +AutoCodeFixRegister g_fixImportNonExportedMember( + FIX_IMPORT_NON_EXPORTED_MEMBER.GetFixId().data()); +} // namespace ark::es2panda::lsp diff --git a/ets2panda/test/unit/lsp/CMakeLists.txt b/ets2panda/test/unit/lsp/CMakeLists.txt index f17ee968970e9ee38826324c0dfc360e35552603..ef85e049edf3480d378da8c7bc83d6d7304b3048 100644 --- a/ets2panda/test/unit/lsp/CMakeLists.txt +++ b/ets2panda/test/unit/lsp/CMakeLists.txt @@ -315,6 +315,9 @@ ets2panda_add_gtest(lsp_get_signature_test CPP_SOURCES get_signature_test.cpp ) +ets2panda_add_gtest(lsp_api_test_fix_import_non_exported_member CPP_SOURCES + fix_import_non_exported_member_test.cpp +) ets2panda_add_gtest(lsp_api_get_node_test CPP_SOURCES get_node_test.cpp ) diff --git a/ets2panda/test/unit/lsp/fix_import_non_exported_member_test.cpp b/ets2panda/test/unit/lsp/fix_import_non_exported_member_test.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9fbb7fa2b410ec10021a43d5cf6d257ab4a6ac3e --- /dev/null +++ b/ets2panda/test/unit/lsp/fix_import_non_exported_member_test.cpp @@ -0,0 +1,108 @@ +/** + * 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 "gtest/gtest.h" +#include "lsp_api_test.h" +#include "lsp/include/cancellation_token.h" +#include "lsp/include/register_code_fix/fix_import_non_exported_member.h" +#include "generated/code_fix_register.h" + +namespace { +using ark::es2panda::lsp::codefixes::FIX_IMPORT_NON_EXPORTED_MEMBER; +constexpr auto ERROR_CODES = FIX_IMPORT_NON_EXPORTED_MEMBER.GetSupportedCodeNumbers(); +constexpr int DEFAULT_THROTTLE = 20; +constexpr int EXPECTED_START = 39; + +class FixImportNonExportedMemberTest : public LSPAPITests { +protected: + class NullCancellationToken : public ark::es2panda::lsp::HostCancellationToken { + public: + bool IsCancellationRequested() override + { + return false; + } + }; + + // Returns a static instance of NullCancellationToken for testing + static NullCancellationToken &GetNullHost() + { + static NullCancellationToken instance; + return instance; + } + + // Converts line and column positions into an offset position in the source code + 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; + } + + // Validates the code fix action info by comparing it with expected values + static void ValidateCodeFixActionInfo(const CodeFixActionInfo &info, const size_t expectedTextChangeStart, + const std::string &expectedFileName) + { + ASSERT_EQ(info.fixName_, "FixImportNonExportedMember"); + ASSERT_EQ(info.fixId_, "FixImportNonExportedMember"); + ASSERT_EQ(info.description_, "Fix Import Non Exported Member"); + ASSERT_EQ(info.changes_[0].fileName, expectedFileName); + ASSERT_EQ(info.changes_[0].textChanges[0].span.start, expectedTextChangeStart); + ASSERT_EQ(info.changes_[0].textChanges[0].newText, "export"); + } +}; + +TEST_F(FixImportNonExportedMemberTest, TestImportNonExportedMemberFix) +{ + // Create test files and their contents + std::vector fileNames = {"MainModuleDone.ets", "TestImportNonExportedMember.ets"}; + std::vector fileContents = { + R"( + // MainModuleDone.ets + function myFunction() { + console.log("Hello World!"); + } + )", + R"( + // TestImportNonExportedMember.ets + import { myFunction } from './MainModuleDone'; // Error: `myFunction` is not exported + + myFunction(); + )"}; + auto filePaths = CreateTempFile(fileNames, fileContents); + + ASSERT_EQ(fileNames.size(), filePaths.size()); + ark::es2panda::lsp::Initializer initializer = ark::es2panda::lsp::Initializer(); + auto *context = initializer.CreateContext(filePaths[1].c_str(), ES2PANDA_STATE_CHECKED); + + const size_t start = LineColToPos(context, 3, 36); + const size_t length = 6; + + const int expectedFixResultSize = 1; + + std::vector errorCodes(ERROR_CODES.begin(), ERROR_CODES.end()); + ark::es2panda::lsp::CancellationToken cancelationToken(DEFAULT_THROTTLE, &GetNullHost()); + CodeFixOptions emptyOptions = {cancelationToken, ark::es2panda::lsp::FormatCodeSettings(), {}}; + auto fixResult = + ark::es2panda::lsp::GetCodeFixesAtPositionImpl(context, start, start + length, errorCodes, emptyOptions); + + ASSERT_EQ(fixResult.size(), expectedFixResultSize); + + ValidateCodeFixActionInfo(fixResult[0], EXPECTED_START, filePaths[0]); + + // Clean up the context after the test + initializer.DestroyContext(context); +} +} // namespace diff --git a/ets2panda/util/diagnostic/semantic.yaml b/ets2panda/util/diagnostic/semantic.yaml index 13edf6570b41e54f1882e4f25e150c852b2a2ca4..0f66970d877dfb00846d13eee7b7b22028ea0f3e 100644 --- a/ets2panda/util/diagnostic/semantic.yaml +++ b/ets2panda/util/diagnostic/semantic.yaml @@ -554,6 +554,7 @@ semantic: - name: IMPORTED_NOT_EXPORTED id: 355 message: "Imported element not exported '{}'" + code_fix_ids: [FixImportNonExportedMember] - name: IMPORT_NOT_FOUND id: 356