diff --git a/ets2panda/lsp/BUILD.gn b/ets2panda/lsp/BUILD.gn index 121f05f97dc6ddc20d250ea61f74fa2532375854..0ebb1f82385f9decb4d1b0ae3effdf67e23a33b9 100644 --- a/ets2panda/lsp/BUILD.gn +++ b/ets2panda/lsp/BUILD.gn @@ -90,6 +90,7 @@ ohos_source_set("libes2panda_lsp_static") { "src/references.cpp", "src/register_code_fix/add_missing_declare_property.cpp", "src/register_code_fix/add_missing_new_operator.cpp", + "src/register_code_fix/add_name_to_nameless_parameter.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", @@ -103,7 +104,6 @@ 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 384341c5a66e0b5db8f0f6326772083bc1488875..189cebcb13f2d0abf8bf578b337d91fc323b4150 100644 --- a/ets2panda/lsp/CMakeLists.txt +++ b/ets2panda/lsp/CMakeLists.txt @@ -116,6 +116,7 @@ set(ES2PANDA_LSP_SRC ./src/register_code_fix/fix_extends_interface_becomes_implements.cpp ./src/register_code_fix/fix_class_super_must_precede_this_access.cpp ./src/register_code_fix/fix_return_type_in_async_function.cpp + ./src/register_code_fix/add_name_to_nameless_parameter.cpp ./src/register_code_fix/add_missing_declare_property.cpp ./src/register_code_fix/convert_const_to_let.cpp ./src/register_code_fix/constructor_for_derived_need_super_call diff --git a/ets2panda/lsp/include/register_code_fix/add_name_to_nameless_parameter.h b/ets2panda/lsp/include/register_code_fix/add_name_to_nameless_parameter.h new file mode 100644 index 0000000000000000000000000000000000000000..f761a26d056eaa002866a4db93c79f5f6e6359c6 --- /dev/null +++ b/ets2panda/lsp/include/register_code_fix/add_name_to_nameless_parameter.h @@ -0,0 +1,42 @@ +/** + 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_NAME_TO_NAMELESS_PARAMETER_H +#define ADD_NAME_TO_NAMELESS_PARAMETER_H + +#include +#include +#include +#include "lsp/include/types.h" +#include "lsp/include/code_fixes/code_fix_types.h" +#include "lsp/include/services/text_change/change_tracker.h" + +namespace ark::es2panda::lsp { +class AddNameToNamelessParameter : public CodeFixRegistration { +public: + AddNameToNamelessParameter(); + std::vector GetCodeActions(const CodeFixContext &context) override; + CombinedCodeActions GetAllCodeActions(const CodeFixAllContext &codeFixAll) override; + static std::vector GetCodeActionsToFix(const CodeFixContext &context); + +private: + static void MakeChange(ChangeTracker &changeTracker, es2panda_Context *context, size_t pos, + std::vector &fixedNodes); + static bool IsIdentifier(const ir::AstNode *node); + static const ir::AstNode *FindParameterNode(const ir::AstNode *start); + static std::string GetNodeText(std::string_view fullSource, const ir::AstNode *node); +}; +} // namespace ark::es2panda::lsp +#endif // ADD_NAME_TO_NAMELESS_PARAMETER_H diff --git a/ets2panda/lsp/src/register_code_fix/add_name_to_nameless_parameter.cpp b/ets2panda/lsp/src/register_code_fix/add_name_to_nameless_parameter.cpp new file mode 100644 index 0000000000000000000000000000000000000000..284a1b881639486001e0506f9c95e40f04017fd7 --- /dev/null +++ b/ets2panda/lsp/src/register_code_fix/add_name_to_nameless_parameter.cpp @@ -0,0 +1,214 @@ +/** + 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 +#include +#include +#include "public/es2panda_lib.h" +#include "lsp/include/internal_api.h" +#include "generated/code_fix_register.h" +#include "lsp/include/code_fix_provider.h" +#include "lsp/include/register_code_fix/add_name_to_nameless_parameter.h" + +namespace ark::es2panda::lsp { +using codefixes::ADD_NAME_TO_NAMELESS_PARAMETER; +namespace { +inline std::string ToString(const util::StringView &sv) +{ + auto u = sv.Utf8(); + return std::string(u.data(), u.size()); +} + +inline std::string ToString(std::string_view sv) +{ + return std::string(sv.data(), sv.size()); +} + +inline std::string_view Slice(std::string_view sv, size_t start, size_t length) +{ + return std::string_view(sv.data() + start, length); +} + +// Count commas between the opening '(' and the parameter start (0-based index). +int ComputeParamIndexByText(const std::string &fullSource, size_t paramStart) +{ + if (fullSource.empty() || paramStart == 0 || paramStart > fullSource.size()) { + return 0; + } + const size_t lparen = fullSource.rfind('(', paramStart); + if (lparen == std::string::npos) { + return 0; + } + int idx = 0; + for (size_t i = lparen + 1; i < paramStart; ++i) { + if (fullSource[i] == ',') { + ++idx; + } + } + return idx; +} + +// Return the start index of the current parameter (right after the nearest '(' or ',' before tokenStart) +size_t ParamSliceStart(const std::string &src, size_t tokenStart) +{ + const size_t lp = src.rfind('(', tokenStart); + const size_t cm = src.rfind(',', tokenStart); + size_t start = std::max(lp == std::string::npos ? 0 : lp, cm == std::string::npos ? 0 : cm); + if (start < src.size()) { + ++start; + } + return start; +} + +// true if there's a ':' between the start of *this* parameter and the token start +bool HasColonBeforeTokenInSameParam(const std::string &src, size_t tokenStart) +{ + if (tokenStart == 0 || tokenStart > src.size()) { + return false; + } + const size_t start = ParamSliceStart(src, tokenStart); + const size_t colon = src.find(':', start); + return colon != std::string::npos && colon < tokenStart; +} + +// true if there's a '=' between the start of *this* parameter and the token start +bool HasEqualsBeforeTokenInSameParam(const std::string &src, size_t tokenStart) +{ + if (tokenStart == 0 || tokenStart > src.size()) { + return false; + } + const size_t start = ParamSliceStart(src, tokenStart); + const size_t eq = src.find('=', start); + return eq != std::string::npos && eq < tokenStart; +} + +} // namespace + +AddNameToNamelessParameter::AddNameToNamelessParameter() +{ + auto errorCodes = ADD_NAME_TO_NAMELESS_PARAMETER.GetSupportedCodeNumbers(); + SetErrorCodes({errorCodes.begin(), errorCodes.end()}); + SetFixIds({ADD_NAME_TO_NAMELESS_PARAMETER.GetFixId().data()}); +} + +bool AddNameToNamelessParameter::IsIdentifier(const ir::AstNode *node) +{ + return node != nullptr && node->IsIdentifier(); +} + +const ir::AstNode *AddNameToNamelessParameter::FindParameterNode(const ir::AstNode *start) +{ + const ir::AstNode *n = start; + while (n != nullptr) { + if (n->Type() == ir::AstNodeType::ETS_PARAMETER_EXPRESSION) { + return n; + } + n = n->Parent(); + } + return (start != nullptr) ? start->Parent() : nullptr; +} + +std::string AddNameToNamelessParameter::GetNodeText(std::string_view fullSource, const ir::AstNode *node) +{ + if (node == nullptr) { + return {}; + } + const size_t start = node->Range().start.index; + const size_t end = node->Range().end.index; + if (end < start) { + return {}; + } + return ToString(Slice(fullSource, start, end - start)); +} + +void AddNameToNamelessParameter::MakeChange(ChangeTracker &changeTracker, es2panda_Context *context, size_t pos, + std::vector &fixedNodes) +{ + auto *token = GetTouchingToken(context, pos, false); + if (token == nullptr || !IsIdentifier(token)) { + return; + } + + const ir::AstNode *paramNode = FindParameterNode(token); + if (paramNode == nullptr) { + return; + } + + using ark::es2panda::public_lib::Context; + auto *pub = reinterpret_cast(context); + if (pub == nullptr || pub->parserProgram == nullptr) { + return; + } + + const std::string fullSource = ToString(pub->parserProgram->SourceCode()); + const size_t tokenStart = token->Range().start.index; + if (HasColonBeforeTokenInSameParam(fullSource, tokenStart) || + HasEqualsBeforeTokenInSameParam(fullSource, tokenStart)) { + return; + } + + const std::string typeText = GetNodeText(fullSource, token); + const int idx = ComputeParamIndexByText(fullSource, tokenStart); + const std::string nameText = "arg" + std::to_string(idx); + const size_t tokenEnd = token->Range().end.index; + if (tokenEnd < tokenStart) { + return; + } + + const TextSpan replaceSpan(tokenStart, tokenEnd - tokenStart); + const std::string replacement = nameText + ": " + typeText; + const std::string filePath = (pub->sourceFile != nullptr) ? ToString(pub->sourceFile->filePath) : std::string(); + TextChange tc(replaceSpan, replacement); + FileTextChanges fc(filePath, {tc}); + const SourceFile *owner = pub->sourceFile; + changeTracker.PushRaw(owner, fc); + fixedNodes.push_back(const_cast(paramNode)); +} + +std::vector AddNameToNamelessParameter::GetCodeActionsToFix(const CodeFixContext &context) +{ + TextChangesContext textChangesContext = {context.host, context.formatContext, context.preferences}; + std::vector fixedNodes; + auto fileTextChanges = ChangeTracker::With(textChangesContext, [&](ChangeTracker &tracker) { + MakeChange(tracker, context.context, context.span.start, fixedNodes); + }); + + return fileTextChanges; +} + +std::vector AddNameToNamelessParameter::GetCodeActions(const CodeFixContext &context) +{ + std::vector actions; + auto changes = GetCodeActionsToFix(context); + if (!changes.empty()) { + CodeFixAction action; + action.fixName = ADD_NAME_TO_NAMELESS_PARAMETER.GetFixId().data(); + action.description = "Add parameter name"; + action.changes = changes; + action.fixId = ADD_NAME_TO_NAMELESS_PARAMETER.GetFixId().data(); + actions.push_back(action); + } + return actions; +} + +CombinedCodeActions AddNameToNamelessParameter::GetAllCodeActions([[maybe_unused]] const CodeFixAllContext & /*unused*/) +{ + return {}; +} + +// NOLINTNEXTLINE(fuchsia-statically-constructed-objects, cert-err58-cpp) +AutoCodeFixRegister g_addNameToNamelessParameter( + ADD_NAME_TO_NAMELESS_PARAMETER.GetFixId().data()); +} // namespace ark::es2panda::lsp \ No newline at end of file diff --git a/ets2panda/test/unit/lsp/CMakeLists.txt b/ets2panda/test/unit/lsp/CMakeLists.txt index a7968df400353f7af1cdca03514eac298ef8dac3..299bec24bd4228098bad0191813a8a9c8c7b1441 100644 --- a/ets2panda/test/unit/lsp/CMakeLists.txt +++ b/ets2panda/test/unit/lsp/CMakeLists.txt @@ -340,4 +340,8 @@ ets2panda_add_gtest(lsp_api_spelling_test CPP_SOURCES ets2panda_add_gtest(lsp_api_test_constructor_for_derived_need_super_call CPP_SOURCES constructor_for_derived_need_super_call_test.cpp +) + +ets2panda_add_gtest(lsp_api_test_add_name_to_nameless_parameter CPP_SOURCES + add_name_to_nameless_parameter_test.cpp ) \ No newline at end of file diff --git a/ets2panda/test/unit/lsp/add_name_to_nameless_parameter_test.cpp b/ets2panda/test/unit/lsp/add_name_to_nameless_parameter_test.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a2746679a74d45547824c9f439e300a354e1cd9e --- /dev/null +++ b/ets2panda/test/unit/lsp/add_name_to_nameless_parameter_test.cpp @@ -0,0 +1,133 @@ +/** + 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 "public/es2panda_lib.h" +#include "lsp/include/internal_api.h" +#include "lsp/include/register_code_fix/add_name_to_nameless_parameter.h" + +namespace { +using ark::es2panda::lsp::codefixes::ADD_NAME_TO_NAMELESS_PARAMETER; +const size_t NO_FIX_WHEN_ALREADY_HAVE_NAME_IDX = 15; +constexpr auto ERROR_CODES = ADD_NAME_TO_NAMELESS_PARAMETER.GetSupportedCodeNumbers(); +constexpr int DEFAULT_THROTTLE = 20; +const size_t ADD_NAME_KEEP_TYPE_IDX = 12; +const size_t NO_FIX_OUTSIDE_PARAM_CTX_IDX = 22; +const size_t ADD_NAME_FOR_SECOND_PARAM_IDX = 15; +class FixAddNameToNamelessParameterTests : public LSPAPITests { +public: + class NullCancellationToken : public ark::es2panda::lsp::HostCancellationToken { + public: + bool IsCancellationRequested() override + { + return false; + } + }; + static ark::es2panda::lsp::CancellationToken CreateToken() + { + static NullCancellationToken nullToken; + return ark::es2panda::lsp::CancellationToken(DEFAULT_THROTTLE, &nullToken); + } +}; + +TEST_F(FixAddNameToNamelessParameterTests, AddsNameAndKeepsType) +{ + ark::es2panda::lsp::Initializer initializer; + const std::string sourceCode = R"( +function f(number) { + return 1; +} +)"; + + es2panda_Context *ctx = initializer.CreateContext("add_name_to_nameless_parameter_add_name.ets", + ES2PANDA_STATE_CHECKED, sourceCode.c_str()); + std::vector errorCodes(ERROR_CODES.begin(), ERROR_CODES.end()); + CodeFixOptions emptyOptions = {CreateToken(), ark::es2panda::lsp::FormatCodeSettings(), {}}; + const size_t length = 1; + auto fixResult = ark::es2panda::lsp::GetCodeFixesAtPositionImpl( + ctx, ADD_NAME_KEEP_TYPE_IDX, ADD_NAME_KEEP_TYPE_IDX + length, errorCodes, emptyOptions); + ASSERT_EQ(fixResult.size(), 1); + ASSERT_FALSE(fixResult[0].changes_.empty()); + ASSERT_FALSE(fixResult[0].changes_[0].textChanges.empty()); + const auto &change = fixResult[0].changes_[0].textChanges[0]; + EXPECT_EQ(change.newText, "arg0: number"); + initializer.DestroyContext(ctx); +} + +TEST_F(FixAddNameToNamelessParameterTests, NoFixWhenParameterAlreadyHasNameAndType) +{ + ark::es2panda::lsp::Initializer initializer; + const std::string sourceCode = R"( +function f(x: number) { +return x; +} +)"; + + es2panda_Context *ctx = initializer.CreateContext("add_name_to_nameless_parameter_no_fix.ets", + ES2PANDA_STATE_CHECKED, sourceCode.c_str()); + std::vector errorCodes(ERROR_CODES.begin(), ERROR_CODES.end()); + const size_t length = 1; + CodeFixOptions opts = {CreateToken(), ark::es2panda::lsp::FormatCodeSettings(), {}}; + auto fixes = ark::es2panda::lsp::GetCodeFixesAtPositionImpl( + ctx, NO_FIX_WHEN_ALREADY_HAVE_NAME_IDX, NO_FIX_WHEN_ALREADY_HAVE_NAME_IDX + length, errorCodes, opts); + EXPECT_TRUE(fixes.empty()); + initializer.DestroyContext(ctx); +} + +TEST_F(FixAddNameToNamelessParameterTests, NoFixOutsideParameterContext) +{ + ark::es2panda::lsp::Initializer initializer; + const std::string sourceCode = R"( +function f(number) { +return number; +} +)"; + + es2panda_Context *ctx = initializer.CreateContext("add_name_to_nameless_parameter_outside.ets", + ES2PANDA_STATE_CHECKED, sourceCode.c_str()); + std::vector errorCodes(ERROR_CODES.begin(), ERROR_CODES.end()); + const size_t length = 1; + CodeFixOptions opts = {CreateToken(), ark::es2panda::lsp::FormatCodeSettings(), {}}; + auto fixes = ark::es2panda::lsp::GetCodeFixesAtPositionImpl( + ctx, NO_FIX_OUTSIDE_PARAM_CTX_IDX, NO_FIX_OUTSIDE_PARAM_CTX_IDX + length, errorCodes, opts); + EXPECT_TRUE(fixes.empty()); + initializer.DestroyContext(ctx); +} + +TEST_F(FixAddNameToNamelessParameterTests, AddsNameForSecondParameter) +{ + ark::es2panda::lsp::Initializer initializer; + const std::string sourceCode = R"( +function g(x, string) { +return x; +} +)"; + + es2panda_Context *ctx = + initializer.CreateContext("add_name_to_nameless_parameter_2.ets", ES2PANDA_STATE_CHECKED, sourceCode.c_str()); + std::vector errorCodes(ERROR_CODES.begin(), ERROR_CODES.end()); + const size_t length = 1; + CodeFixOptions opts = {CreateToken(), ark::es2panda::lsp::FormatCodeSettings(), {}}; + auto fixes = ark::es2panda::lsp::GetCodeFixesAtPositionImpl( + ctx, ADD_NAME_FOR_SECOND_PARAM_IDX, ADD_NAME_FOR_SECOND_PARAM_IDX + length, errorCodes, opts); + ASSERT_EQ(fixes.size(), 1); + ASSERT_FALSE(fixes[0].changes_.empty()); + ASSERT_FALSE(fixes[0].changes_[0].textChanges.empty()); + const auto &change = fixes[0].changes_[0].textChanges[0]; + EXPECT_EQ(change.newText, "arg1: string"); + initializer.DestroyContext(ctx); +} +} // namespace diff --git a/ets2panda/util/diagnostic/syntax.yaml b/ets2panda/util/diagnostic/syntax.yaml index 2401da7d0e2450558426cb7d33c1c12f4a8a8e92..7027b66d7a7e0afd6c1e407d6fcc1f515f5d410f 100644 --- a/ets2panda/util/diagnostic/syntax.yaml +++ b/ets2panda/util/diagnostic/syntax.yaml @@ -315,6 +315,7 @@ syntax: - name: EXPLICIT_PARAM_TYPE id: 194 message: "Parameter declaration should have an explicit type annotation." + code_fix_ids: [AddNameToNamelessParameter] - name: EXPORT_DEFAULT_NO_REEXPORT id: 244