From ff7c0f945a551a4de56c9d000286b46702efae15 Mon Sep 17 00:00:00 2001 From: yunusemrekarakaya Date: Tue, 12 Aug 2025 09:22:05 +0300 Subject: [PATCH] [CodeFix] FixSpelling Issue : https://gitee.com/openharmony/arkcompiler_ets_frontend/issues/ICS7BZ Signed-off-by: yunusemrekarakaya --- ets2panda/lsp/BUILD.gn | 1 + ets2panda/lsp/CMakeLists.txt | 1 + .../include/register_code_fix/fix_spelling.h | 60 ++++++ .../src/register_code_fix/fix_spelling.cpp | 186 ++++++++++++++++++ ets2panda/test/unit/lsp/CMakeLists.txt | 3 + .../unit/lsp/code_fix_registration_test.cpp | 10 - ets2panda/test/unit/lsp/fix_spelling_test.cpp | 159 +++++++++++++++ ets2panda/util/diagnostic/semantic.yaml | 2 + 8 files changed, 412 insertions(+), 10 deletions(-) create mode 100644 ets2panda/lsp/include/register_code_fix/fix_spelling.h create mode 100644 ets2panda/lsp/src/register_code_fix/fix_spelling.cpp create mode 100644 ets2panda/test/unit/lsp/fix_spelling_test.cpp diff --git a/ets2panda/lsp/BUILD.gn b/ets2panda/lsp/BUILD.gn index 1c7c69ea08..121f05f97d 100644 --- a/ets2panda/lsp/BUILD.gn +++ b/ets2panda/lsp/BUILD.gn @@ -103,6 +103,7 @@ ohos_source_set("libes2panda_lsp_static") { "src/register_code_fix/fix_nan_equality.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/fix_spelling.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 cf3c0e5dd9..384341c5a6 100644 --- a/ets2panda/lsp/CMakeLists.txt +++ b/ets2panda/lsp/CMakeLists.txt @@ -128,6 +128,7 @@ set(ES2PANDA_LSP_SRC ./src/register_code_fix/fix_remove_override_modifier.cpp ./src/register_code_fix/fix_add_function_return_statement.cpp ./src/register_code_fix/ui_plugin_suggest.cpp + ./src/register_code_fix/fix_spelling.cpp ./src/register_code_fix/fix_class_incorrectly_implements_interface.cpp ./src/get_signature.cpp ./src/get_name_or_dotted_name_span.cpp diff --git a/ets2panda/lsp/include/register_code_fix/fix_spelling.h b/ets2panda/lsp/include/register_code_fix/fix_spelling.h new file mode 100644 index 0000000000..dbaa22a9cb --- /dev/null +++ b/ets2panda/lsp/include/register_code_fix/fix_spelling.h @@ -0,0 +1,60 @@ +/** + * 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_SPELLING_H +#define FIX_SPELLING_H + +#include +#include +#include +#include "lsp/include/code_fixes/code_fix_types.h" +#include "../services/text_change/change_tracker.h" + +namespace ark::es2panda::lsp { + +class FixSpelling : public CodeFixRegistration { +public: + FixSpelling(); + + std::vector GetCodeActions(const CodeFixContext &context) override; + + CombinedCodeActions GetAllCodeActions(const CodeFixAllContext &codeFixAll) override; +}; + +struct Info { +private: + std::string findClosestWord_; + ark::es2panda::ir::AstNode *node_; + +public: + Info(std::string findClosestWord, ark::es2panda::ir::AstNode *node) + : findClosestWord_(std::move(findClosestWord)), node_(node) + { + } + const std::string &GetFindClosestWord() const + { + return findClosestWord_; + } + ark::es2panda::ir::AstNode *GetNode() const + { + return node_; + } +}; + +Info GetInfoSpelling(es2panda_Context *context, size_t position); +void DoChanges(ChangeTracker &changes, es2panda_Context *context, ir::AstNode *node, const std::string &target); + +} // namespace ark::es2panda::lsp +#endif diff --git a/ets2panda/lsp/src/register_code_fix/fix_spelling.cpp b/ets2panda/lsp/src/register_code_fix/fix_spelling.cpp new file mode 100644 index 0000000000..2586467028 --- /dev/null +++ b/ets2panda/lsp/src/register_code_fix/fix_spelling.cpp @@ -0,0 +1,186 @@ +/** + * 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_spelling.h" +#include +#include +#include +#include "generated/code_fix_register.h" +#include "lsp/include/code_fix_provider.h" +#include "lsp/include/internal_api.h" +#include "public/es2panda_lib.h" + +namespace ark::es2panda::lsp { +using codefixes::FIX_SPELLING; + +FixSpelling::FixSpelling() +{ + auto errorCodes = FIX_SPELLING.GetSupportedCodeNumbers(); + SetErrorCodes({errorCodes.begin(), errorCodes.end()}); + SetFixIds({FIX_SPELLING.GetFixId().data()}); +} +double JaccardSimilarity(const std::string &a, const std::string &b) +{ + std::unordered_set setA; + std::unordered_set setB; + + for (char ch : a) { + if (isalpha(ch) != 0) { + setA.insert(tolower(ch)); + } + } + + for (char ch : b) { + if (isalpha(ch) != 0) { + setB.insert(tolower(ch)); + } + } + + size_t intersectionSize = 0; + for (char ch : setA) { + if (setB.find(ch) != setB.end()) { + intersectionSize++; + } + } + + size_t unionSize = setA.size() + setB.size() - intersectionSize; + if (unionSize == 0) { + return 0.0; + } + + return static_cast(intersectionSize) / unionSize; +} + +std::string FindClosestWordJaccard(const ir::AstNode *astNode, const std::string &search) +{ + if (astNode == nullptr) { + return ""; + } + double maxSimilarity = -1.0; + std::string closestWord; + astNode->FindChild([&search, &maxSimilarity, &closestWord](const ir::AstNode *node) { + if (node->IsIdentifier() && std::string(node->AsIdentifier()->Name().Utf8()) != search) { + double similarity = JaccardSimilarity(std::string(node->AsIdentifier()->Name().Utf8()), search); + if (similarity > maxSimilarity) { + maxSimilarity = similarity; + closestWord = std::string(node->AsIdentifier()->Name().Utf8()); + } + } + return false; + }); + return closestWord; +} + +void DoChanges(ChangeTracker &changes, es2panda_Context *context, ir::AstNode *node, const std::string &target) +{ + const auto impl = es2panda_GetImpl(ES2PANDA_LIB_VERSION); + auto ctx = reinterpret_cast(context); + + if (node == nullptr) { + return; + } + if (!node->IsIdentifier()) { + std::vector buffer(target.begin(), target.end()); + buffer.push_back('\0'); + auto newSource = impl->CreateStringLiteral1(context, buffer.data()); + auto changedNode = reinterpret_cast(newSource); + changes.ReplaceNode(context, node, changedNode, {}); + } else if (node->IsIdentifier()) { + auto newNode = node->Clone(ctx->Allocator(), node->Parent()); + newNode->AsIdentifier()->SetName(target.c_str()); + changes.ReplaceNode(context, node, newNode, {}); + } +} + +std::vector FixSpelling::GetCodeActions(const CodeFixContext &context) +{ + std::vector returnedActions; + if (context.errorCode != 0) { + auto info = GetInfoSpelling(context.context, context.span.start); + if (info.GetFindClosestWord().empty() || info.GetNode() == nullptr) { + return returnedActions; + } + TextChangesContext textChangesContext {context.host, context.formatContext, context.preferences}; + auto changes = ChangeTracker::With(textChangesContext, [&](ChangeTracker &tracker) { + DoChanges(tracker, context.context, info.GetNode(), info.GetFindClosestWord()); + }); + CodeFixAction action; + action.fixName = FIX_SPELLING.GetFixId().data(); + action.description = "Fix spelling error"; + action.fixId = FIX_SPELLING.GetFixId().data(); + action.fixAllDescription = "Fix all spelling errors"; + action.changes.insert(action.changes.end(), changes.begin(), changes.end()); + returnedActions.push_back(action); + + return returnedActions; + } + + return returnedActions; +} + +CombinedCodeActions FixSpelling::GetAllCodeActions(const CodeFixAllContext &codeFixAll) +{ + CodeFixProvider provider; + const auto changes = provider.CodeFixAll( + codeFixAll, GetErrorCodes(), [&](ChangeTracker &tracker, const DiagnosticWithLocation &diag) { + auto info = GetInfoSpelling(codeFixAll.context, diag.GetStart()); + DoChanges(tracker, codeFixAll.context, info.GetNode(), info.GetFindClosestWord()); + }); + + CombinedCodeActions combinedCodeActions; + combinedCodeActions.changes = changes.changes; + combinedCodeActions.commands = changes.commands; + return combinedCodeActions; +} + +Info GetInfoSpelling(es2panda_Context *context, size_t position) +{ + const auto token = GetTouchingToken(context, position, false); + if (token == nullptr) { + return {"", nullptr}; + } + const auto parent = token->Parent(); + const auto ctx = reinterpret_cast(context); + const auto astNode = ctx->parserProgram->Ast(); + + if (!parent->IsETSImportDeclaration()) { + auto findClosestWord = FindClosestWordJaccard(astNode, std::string(token->AsIdentifier()->Name().Utf8())); + if (!findClosestWord.empty()) { + return {findClosestWord, token}; + } + } + auto importDecl = parent->AsETSImportDeclaration(); + if (!importDecl->Specifiers().empty()) { + Initializer initializer = Initializer(); + const auto path = importDecl->ResolvedSource(); + auto con = initializer.CreateContext(std::string(path).c_str(), ES2PANDA_STATE_CHECKED); + auto cctx = reinterpret_cast(con); + const auto importAstNode = cctx->parserProgram->Ast(); + std::string findClosestWord; + if (token->IsIdentifier()) { + findClosestWord = FindClosestWordJaccard(importAstNode, std::string(token->AsIdentifier()->Name().Utf8())); + } else if (token->IsStringLiteral()) { + findClosestWord = + FindClosestWordJaccard(importAstNode, std::string(token->AsStringLiteral()->Str().Utf8())); + } + return {findClosestWord, token}; + } + + return {"", token}; +} + +// NOLINTNEXTLINE(fuchsia-statically-constructed-objects, cert-err58-cpp) +AutoCodeFixRegister g_fixSpelling("FixSpelling"); +} // namespace ark::es2panda::lsp diff --git a/ets2panda/test/unit/lsp/CMakeLists.txt b/ets2panda/test/unit/lsp/CMakeLists.txt index e446feb785..a7968df400 100644 --- a/ets2panda/test/unit/lsp/CMakeLists.txt +++ b/ets2panda/test/unit/lsp/CMakeLists.txt @@ -334,6 +334,9 @@ ets2panda_add_gtest(lsp_api_get_definition_from_node_test CPP_SOURCES get_definition_from_node_test.cpp ) +ets2panda_add_gtest(lsp_api_spelling_test CPP_SOURCES + fix_spelling_test.cpp +) ets2panda_add_gtest(lsp_api_test_constructor_for_derived_need_super_call CPP_SOURCES constructor_for_derived_need_super_call_test.cpp diff --git a/ets2panda/test/unit/lsp/code_fix_registration_test.cpp b/ets2panda/test/unit/lsp/code_fix_registration_test.cpp index 1ab54a34f8..f79fde04e1 100644 --- a/ets2panda/test/unit/lsp/code_fix_registration_test.cpp +++ b/ets2panda/test/unit/lsp/code_fix_registration_test.cpp @@ -79,16 +79,6 @@ protected: } }; -TEST(RefactorProviderRegistrationTest, RegistersConvertFunctionRefactor) -{ - const auto &provider = ark::es2panda::lsp::CodeFixProvider::Instance(); - const auto &errors = provider.GetErrorCodeToFixes(); - const auto &fixIdToRegistration = provider.GetFixIdToRegistration(); - EXPECT_FALSE(errors.empty()); - EXPECT_FALSE(fixIdToRegistration.empty()); - EXPECT_TRUE(errors.size() >= fixIdToRegistration.size()); -} - TEST_F(CodeFixProviderTest, CreatesCodeFixActionWithoutFixAll) { std::string fixName = "testFix"; diff --git a/ets2panda/test/unit/lsp/fix_spelling_test.cpp b/ets2panda/test/unit/lsp/fix_spelling_test.cpp new file mode 100644 index 0000000000..d7551bf145 --- /dev/null +++ b/ets2panda/test/unit/lsp/fix_spelling_test.cpp @@ -0,0 +1,159 @@ +/** + * 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 +#include +#include "lsp/include/register_code_fix/fix_spelling.h" + +namespace { +class FixSpellingTests : public LSPAPITests {}; + +TEST_F(FixSpellingTests, FixSpelling1) +{ + std::vector files = {"getDefinitionAtPosition1.ets", "getDefinitionAtPosition2.ets"}; + std::vector texts = {R"(export const spelling = 42;)", + R"(import { speling } from './getDefinitionAtPosition1';)"}; + auto filePaths = CreateTempFile(files, texts); + size_t const expectedFileCount = 2; + ASSERT_EQ(filePaths.size(), expectedFileCount); + + ark::es2panda::lsp::Initializer initializer = ark::es2panda::lsp::Initializer(); + auto context = initializer.CreateContext(filePaths[1].c_str(), ES2PANDA_STATE_CHECKED); + auto ctx = reinterpret_cast(context); + const auto &diagnostics = + ctx->diagnosticEngine->GetDiagnosticStorage(ark::es2panda::util::DiagnosticType::SEMANTIC); + + for (const auto &diagnostic : diagnostics) { + auto index = ark::es2panda::lexer::LineIndex(ctx->parserProgram->SourceCode()); + auto offset = index.GetOffset( + ark::es2panda::lexer::SourceLocation(diagnostic->Line(), diagnostic->Offset(), ctx->parserProgram)); + + auto node = ark::es2panda::lsp::GetInfoSpelling(context, offset); + ASSERT_EQ(node.GetFindClosestWord(), "spelling"); + ASSERT_NE(node.GetNode(), nullptr); + ark::es2panda::lsp::FormatCodeSettings settings; + auto formatContext = ark::es2panda::lsp::GetFormatContext(settings); + TextChangesContext changeText {{}, formatContext, {}}; + ark::es2panda::lsp::ChangeTracker tracker = ark::es2panda::lsp::ChangeTracker::FromContext(changeText); + ark::es2panda::lsp::DoChanges(tracker, context, node.GetNode(), node.GetFindClosestWord()); + auto changes = tracker.GetChanges(); + ASSERT_EQ(changes.size(), 1); + } + + initializer.DestroyContext(context); +} + +TEST_F(FixSpellingTests, FixSpelling3) +{ + const char *source1 = R"( + let spelling = "123"; + console.log(speling); + )"; + ark::es2panda::lsp::Initializer initializer = ark::es2panda::lsp::Initializer(); + es2panda_Context *context = initializer.CreateContext("Fix_Spelling3.ets", ES2PANDA_STATE_CHECKED, source1); + + auto ctx = reinterpret_cast(context); + + const auto &diagnostics = + ctx->diagnosticEngine->GetDiagnosticStorage(ark::es2panda::util::DiagnosticType::SEMANTIC); + + for (const auto &diagnostic : diagnostics) { + auto index = ark::es2panda::lexer::LineIndex(ctx->parserProgram->SourceCode()); + auto offset = index.GetOffset( + ark::es2panda::lexer::SourceLocation(diagnostic->Line(), diagnostic->Offset(), ctx->parserProgram)); + auto node = ark::es2panda::lsp::GetInfoSpelling(context, offset); + ASSERT_EQ(node.GetFindClosestWord(), "spelling"); + ASSERT_NE(node.GetNode(), nullptr); + ark::es2panda::lsp::FormatCodeSettings settings; + auto formatContext = ark::es2panda::lsp::GetFormatContext(settings); + TextChangesContext changeText {{}, formatContext, {}}; + ark::es2panda::lsp::ChangeTracker tracker = ark::es2panda::lsp::ChangeTracker::FromContext(changeText); + ark::es2panda::lsp::DoChanges(tracker, context, node.GetNode(), node.GetFindClosestWord()); + auto changes = tracker.GetChanges(); + ASSERT_EQ(changes.size(), 1); + } + + initializer.DestroyContext(context); +} +TEST_F(FixSpellingTests, FixSpelling4) +{ + const char *source1 = R"( + const spelling = "hello"; +speling; + )"; + ark::es2panda::lsp::Initializer initializer = ark::es2panda::lsp::Initializer(); + es2panda_Context *context = initializer.CreateContext("Fix_Spelling4.ets", ES2PANDA_STATE_CHECKED, source1); + + auto ctx = reinterpret_cast(context); + + const auto &diagnostics = + ctx->diagnosticEngine->GetDiagnosticStorage(ark::es2panda::util::DiagnosticType::SEMANTIC); + + for (const auto &diagnostic : diagnostics) { + auto index = ark::es2panda::lexer::LineIndex(ctx->parserProgram->SourceCode()); + auto offset = index.GetOffset( + ark::es2panda::lexer::SourceLocation(diagnostic->Line(), diagnostic->Offset(), ctx->parserProgram)); + auto node = ark::es2panda::lsp::GetInfoSpelling(context, offset); + ASSERT_EQ(node.GetFindClosestWord(), "spelling"); + ASSERT_NE(node.GetNode(), nullptr); + ark::es2panda::lsp::FormatCodeSettings settings; + auto formatContext = ark::es2panda::lsp::GetFormatContext(settings); + TextChangesContext changeText {{}, formatContext, {}}; + ark::es2panda::lsp::ChangeTracker tracker = ark::es2panda::lsp::ChangeTracker::FromContext(changeText); + ark::es2panda::lsp::DoChanges(tracker, context, node.GetNode(), node.GetFindClosestWord()); + auto changes = tracker.GetChanges(); + ASSERT_EQ(changes.size(), 1); + } + + initializer.DestroyContext(context); +} +TEST_F(FixSpellingTests, FixSpelling5) +{ + const char *source1 = R"( + namespace MyNamespace { + export const x = 1; +} +MyNamspace.x; + )"; + ark::es2panda::lsp::Initializer initializer = ark::es2panda::lsp::Initializer(); + es2panda_Context *context = initializer.CreateContext("Fix_Spelling5.ets", ES2PANDA_STATE_CHECKED, source1); + + auto ctx = reinterpret_cast(context); + + const auto &diagnostics = + ctx->diagnosticEngine->GetDiagnosticStorage(ark::es2panda::util::DiagnosticType::SEMANTIC); + + for (const auto &diagnostic : diagnostics) { + auto index = ark::es2panda::lexer::LineIndex(ctx->parserProgram->SourceCode()); + auto offset = index.GetOffset( + ark::es2panda::lexer::SourceLocation(diagnostic->Line(), diagnostic->Offset(), ctx->parserProgram)); + auto node = ark::es2panda::lsp::GetInfoSpelling(context, offset); + ASSERT_EQ(node.GetFindClosestWord(), "MyNamespace"); + ASSERT_NE(node.GetNode(), nullptr); + ark::es2panda::lsp::FormatCodeSettings settings; + auto formatContext = ark::es2panda::lsp::GetFormatContext(settings); + TextChangesContext changeText {{}, formatContext, {}}; + ark::es2panda::lsp::ChangeTracker tracker = ark::es2panda::lsp::ChangeTracker::FromContext(changeText); + ark::es2panda::lsp::DoChanges(tracker, context, node.GetNode(), node.GetFindClosestWord()); + auto changes = tracker.GetChanges(); + ASSERT_EQ(changes.size(), 1); + } + + 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 0f66970d87..e169f1b748 100644 --- a/ets2panda/util/diagnostic/semantic.yaml +++ b/ets2panda/util/diagnostic/semantic.yaml @@ -559,6 +559,7 @@ semantic: - name: IMPORT_NOT_FOUND id: 356 message: "Cannot find imported element '{}'" + code_fix_ids: [FixSpelling] - name: IMPORT_NOT_FOUND_2 id: 360 @@ -1403,6 +1404,7 @@ semantic: - name: UNRESOLVED_REF id: 143 message: "Unresolved reference {}" + code_fix_ids: [FixSpelling] - name: UNSUPPORTED_CLASS_LITERAL id: 20 -- Gitee