From f5526cfdf14bbbd7b19c25a9358c976170a775ec Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 13:45:52 +0000 Subject: [PATCH] =?UTF-8?q?fix(definition):=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D1=85=D0=BE=D0=B4=20=D0=BA=20=D0=BE=D0=BF=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B5=D0=BD=D0=B8=D1=8E=20=D1=87=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20source-defined=20?= =?UTF-8?q?=D1=81=D0=B8=D0=BC=D0=B2=D0=BE=D0=BB=20=D0=B4=D0=B5=D1=81=D0=BA?= =?UTF-8?q?=D1=80=D0=B8=D0=BF=D1=82=D0=BE=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F12 на члене платформенного/конфигурационного типа, разрешённом через TypeService.memberAt в синтетический PlatformMemberSymbol (например, метод OneScript-класса в fluent-цепочке asserts: Ожидаем.Что(X).ИмеетТип(...)), не срабатывал: DefinitionProvider отсекал такие ссылки фильтром isSourceDefinedSymbolReference, хотя дескриптор уже несёт source-defined символ-источник (OScriptModuleMembersProvider.withSourceSymbol). Добавлен маленький Reference->Reference map (unwrapPlatformMemberSource), разворачивающий ссылку на источник перед фильтром; основной пайплайн перехода не меняется. Платформенные члены без источника остаются hover-only. Обсуждение и дизайн: #4197 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01KZQafLUdXJF7ZDqphzs6tX --- .../providers/DefinitionProvider.java | 36 +++++ .../providers/DefinitionProviderUnitTest.java | 146 ++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProviderUnitTest.java diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProvider.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProvider.java index c7bf5d8afd5..c3fccdb33e5 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProvider.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProvider.java @@ -27,6 +27,7 @@ import com.github._1c_syntax.bsl.languageserver.events.LanguageServerInitializeRequestReceivedEvent; import com.github._1c_syntax.bsl.languageserver.references.ReferenceResolver; import com.github._1c_syntax.bsl.languageserver.references.model.Reference; +import com.github._1c_syntax.bsl.languageserver.types.symbol.PlatformMemberSymbol; import lombok.RequiredArgsConstructor; import org.eclipse.lsp4j.ClientCapabilities; import org.eclipse.lsp4j.DefinitionCapabilities; @@ -110,12 +111,47 @@ private List findLocationLinks(DocumentContext documentContext, De Position position = params.getPosition(); return referenceResolver.findReference(documentContext.getUri(), position) + .map(DefinitionProvider::unwrapPlatformMemberSource) .filter(Reference::isSourceDefinedSymbolReference) .map(DefinitionProvider::toLocationLink) .map(Collections::singletonList) .orElse(Collections.emptyList()); } + /** + * Если ссылка указывает на член платформенного/конфигурационного типа + * (синтетический {@link PlatformMemberSymbol}, разрешённый через + * {@code TypeService.memberAt}) и у его дескриптора есть source-defined + * символ-источник (метод OneScript-класса, экспортная переменная-свойство + * и т.п. — см. {@code OScriptModuleMembersProvider}), возвращает ссылку, + * перенаправленную на этот источник; иначе возвращает ссылку без изменений. + *

+ * Так основной пайплайн перехода не меняется: платформенные члены без + * источника остаются {@link PlatformMemberSymbol} и отсекаются фильтром + * {@link Reference#isSourceDefinedSymbolReference()} (остаётся только hover). + *

+ * Обсуждение и дизайн: https://github.com/1c-syntax/bsl-language-server/pull/4197 + * + * @param reference исходная ссылка под курсором. + * @return ссылка на source-defined источник либо исходная ссылка. + */ + private static Reference unwrapPlatformMemberSource(Reference reference) { + if (!(reference.symbol() instanceof PlatformMemberSymbol platformMember)) { + return reference; + } + return platformMember.getDescriptor().getSourceSymbol() + .filter(SourceDefinedSymbol.class::isInstance) + .map(SourceDefinedSymbol.class::cast) + .map(sourceSymbol -> new Reference( + reference.from(), + sourceSymbol, + reference.uri(), + reference.selectionRange(), + reference.occurrenceType() + )) + .orElse(reference); + } + private static LocationLink toLocationLink(Reference reference) { SourceDefinedSymbol symbol = (SourceDefinedSymbol) reference.symbol(); diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProviderUnitTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProviderUnitTest.java new file mode 100644 index 00000000000..008bfd8c182 --- /dev/null +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProviderUnitTest.java @@ -0,0 +1,146 @@ +/* + * This file is a part of BSL Language Server. + * + * Copyright (c) 2018-2026 + * Alexey Sosnoviy , Nikita Fedkin and contributors + * + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * BSL Language Server is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * BSL Language Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with BSL Language Server. + */ +package com.github._1c_syntax.bsl.languageserver.providers; + +import com.github._1c_syntax.bsl.languageserver.ClientCapabilitiesHolder; +import com.github._1c_syntax.bsl.languageserver.context.DocumentContext; +import com.github._1c_syntax.bsl.languageserver.context.symbol.MethodSymbol; +import com.github._1c_syntax.bsl.languageserver.context.symbol.Symbol; +import com.github._1c_syntax.bsl.languageserver.references.ReferenceResolver; +import com.github._1c_syntax.bsl.languageserver.references.model.OccurrenceType; +import com.github._1c_syntax.bsl.languageserver.references.model.Reference; +import com.github._1c_syntax.bsl.languageserver.types.model.MemberDescriptor; +import com.github._1c_syntax.bsl.languageserver.types.symbol.PlatformMemberSymbol; +import com.github._1c_syntax.bsl.languageserver.utils.Ranges; +import org.eclipse.lsp4j.DefinitionParams; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Mockito-юнит для {@link DefinitionProvider}: переход к определению члена + * платформенного/конфигурационного типа, разрешённого через + * {@code TypeService.memberAt} в синтетический {@link PlatformMemberSymbol}. + *

+ * Покрывает разворот ссылки на source-defined символ-источник дескриптора + * без поднятия полного {@code ServerContext}. + * Обсуждение: https://github.com/1c-syntax/bsl-language-server/pull/4197 + */ +class DefinitionProviderUnitTest { + + private static final URI SOURCE_URI = URI.create("file:///lib/asserts/assertion.os"); + + @Test + void definitionFollowsSourceSymbolOfPlatformMember() { + // given — член (метод OS-класса) с прикреплённым source-defined символом-источником + var sourceRange = Ranges.create(10, 0, 40); + var sourceSelectionRange = Ranges.create(10, 9, 17); + var sourceSymbol = sourceDefinedMethod(sourceRange, sourceSelectionRange); + + var memberRange = Ranges.create(3, 24, 32); + var reference = platformMemberReference(sourceSymbol, memberRange); + + var provider = providerReturning(reference, true); + + var params = new DefinitionParams(); + params.setPosition(new Position(3, 26)); + + // when + var definitions = provider.getDefinition(mock(DocumentContext.class), params); + + // then — переход ведёт к источнику, origin = диапазон обращения к члену + assertThat(definitions.isRight()).isTrue(); + assertThat(definitions.getRight()).hasSize(1); + + var link = definitions.getRight().get(0); + assertThat(link.getTargetUri()).isEqualTo(SOURCE_URI.toString()); + assertThat(link.getTargetRange()).isEqualTo(sourceRange); + assertThat(link.getTargetSelectionRange()).isEqualTo(sourceSelectionRange); + assertThat(link.getOriginSelectionRange()).isEqualTo(memberRange); + } + + @Test + void definitionEmptyForPlatformMemberWithoutSource() { + // given — платформенный член без символа-источника (нет объявления в исходниках) + var memberRange = Ranges.create(3, 24, 32); + var member = new PlatformMemberSymbol("Добавить", null, + MemberDescriptor.method("Добавить"), -1, List.of()); + var reference = new Reference( + mock(MethodSymbol.class), member, SOURCE_URI, memberRange, OccurrenceType.REFERENCE); + + var provider = providerReturning(reference, true); + + var params = new DefinitionParams(); + params.setPosition(new Position(3, 26)); + + // when + var definitions = provider.getDefinition(mock(DocumentContext.class), params); + + // then — перехода нет (остаётся только hover) + assertThat(definitions.isRight()).isTrue(); + assertThat(definitions.getRight()).isEmpty(); + } + + private static MethodSymbol sourceDefinedMethod(Range range, Range selectionRange) { + var owner = mock(DocumentContext.class); + when(owner.getUri()).thenReturn(SOURCE_URI); + + var sourceSymbol = mock(MethodSymbol.class); + when(sourceSymbol.getOwner()).thenReturn(owner); + when(sourceSymbol.getRange()).thenReturn(range); + when(sourceSymbol.getSelectionRange()).thenReturn(selectionRange); + return sourceSymbol; + } + + private static Reference platformMemberReference(Symbol sourceSymbol, Range memberRange) { + var descriptor = MemberDescriptor.method("ИмеетТип").withSourceSymbol(sourceSymbol); + var member = new PlatformMemberSymbol("ИмеетТип", null, descriptor, 1, List.of()); + return new Reference( + mock(MethodSymbol.class), member, SOURCE_URI, memberRange, OccurrenceType.REFERENCE); + } + + private static DefinitionProvider providerReturning(Reference reference, boolean linkSupport) { + var referenceResolver = mock(ReferenceResolver.class); + when(referenceResolver.findReference(any(), any())).thenReturn(Optional.of(reference)); + + var capabilitiesHolder = mock(ClientCapabilitiesHolder.class); + var provider = new DefinitionProvider(referenceResolver, capabilitiesHolder); + if (linkSupport) { + var capabilities = new org.eclipse.lsp4j.ClientCapabilities(); + var textDocument = new org.eclipse.lsp4j.TextDocumentClientCapabilities(); + textDocument.setDefinition(new org.eclipse.lsp4j.DefinitionCapabilities(false, true)); + capabilities.setTextDocument(textDocument); + when(capabilitiesHolder.getCapabilities()).thenReturn(Optional.of(capabilities)); + provider.handleInitializeEvent(); + } + return provider; + } +}