Skip to content

Commit 6d4caf6

Browse files
authored
Merge pull request #4376 from 1c-syntax/feat/types-tabular-row-see-ref
feat(types): тип уточняется ссылкой «См.»
2 parents eba4f03 + b60112f commit 6d4caf6

9 files changed

Lines changed: 451 additions & 7 deletions

File tree

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ dependencies {
9696
api("org.springframework.ai:spring-ai-starter-mcp-server-webmvc")
9797

9898
// 1c-syntax
99-
api("io.github.1c-syntax:bsl-parser:0.38.0")
99+
api("io.github.1c-syntax:bsl-parser:0.39.0")
100100
api("io.github.1c-syntax:utils:0.10.1")
101101
api("io.github.1c-syntax:mdclasses:0.19.0.82-SNAPSHOT")
102102
api("io.github.1c-syntax:bsl-common-library:0.12.4")

src/main/java/com/github/_1c_syntax/bsl/languageserver/hover/DescriptionFormatter.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,11 @@ private Map<String, String> typesToMap(List<TypeDescription> parameterTypes, int
358358
} else {
359359
typeName = "`%s`".formatted(type.name());
360360
}
361+
var hyperlink = type.hyperlink();
362+
if (hyperlink != null && !(type instanceof HyperlinkTypeDescription)) {
363+
// Тип, уточнённый ссылкой: автор написал обе половины — показываем обе.
364+
typeName = "%s: [%s](%s)".formatted(typeName, hyperlink.link(), hyperlink.link());
365+
}
361366

362367
types.merge(typeDescription, typeName, "%s | %s"::formatted);
363368
});

src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/BslDocSemanticTokensSupplier.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.github._1c_syntax.bsl.languageserver.utils.DescriptionTypes;
2929
import com.github._1c_syntax.bsl.parser.description.SourceDefinedSymbolDescription;
3030
import com.github._1c_syntax.bsl.parser.description.TypeDescription;
31+
import com.github._1c_syntax.bsl.parser.description.support.SimpleRange;
3132
import lombok.RequiredArgsConstructor;
3233
import lombok.Setter;
3334
import org.eclipse.lsp4j.ClientCapabilities;
@@ -45,6 +46,7 @@
4546
import java.util.List;
4647
import java.util.Map;
4748
import java.util.Optional;
49+
import java.util.stream.Stream;
4850

4951
/**
5052
* Сапплаер семантических токенов для BSL документации (описаний методов и переменных).
@@ -305,13 +307,29 @@ private void addTypeTokens(
305307
var fileType = documentContext.getFileType();
306308
DescriptionTypes.typesOf(description)
307309
.filter(type -> !validateTypeResolution || isResolvable(type, fileType))
308-
.map(type -> type.element().range())
310+
.flatMap(BslDocSemanticTokensSupplier::typeRanges)
309311
.distinct()
310312
.forEach(range -> helper.addEntry(semanticElements,
311313
range.startLine(), range.startCharacter(), range.length(),
312314
SemanticTokenTypes.Type, SemanticTokenModifiers.Documentation));
313315
}
314316

317+
/**
318+
* Области типа: его имя и — у типа, уточнённого ссылкой, — сама ссылка. Отдельно
319+
* стоящая ссылка подсвечивается как тип, поэтому и уточняющая должна.
320+
*
321+
* @param type описание типа.
322+
* @return области для подсветки.
323+
*/
324+
private static Stream<SimpleRange> typeRanges(TypeDescription type) {
325+
var element = Stream.of(type.element().range());
326+
var hyperlink = type.hyperlink();
327+
if (hyperlink == null || type.variant() != TypeDescription.Variant.SIMPLE) {
328+
return element;
329+
}
330+
return Stream.concat(element, Stream.of(hyperlink.range()));
331+
}
332+
315333
/**
316334
* Резолвится ли тип описания в реальный тип через {@link TypeService}. Имя типа для резолва берётся
317335
* из семантических аксессоров парсера ({@link DescriptionTypes#resolveName}); гиперссылки {@code См.}

src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/SymbolTypeIndex.java

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,25 @@ public class SymbolTypeIndex {
7676
/** Коллекция строк — у дерева значений строки лежат в ней, а не в самом дереве. */
7777
private static final String ROWS = "Строки";
7878

79+
/** Наименьшая ссылка на метаданные: вид объекта, его имя и имя подчинённого. */
80+
private static final int MIN_METADATA_SEGMENTS = 3;
81+
82+
/** Номер части ссылки на метаданные: вид объекта. */
83+
private static final int KIND_PART = 0;
84+
85+
/** Номер части ссылки на метаданные: имя объекта. */
86+
private static final int NAME_PART = 1;
87+
88+
/** Номер части ссылки на метаданные: имя подчинённого — табличной части либо реквизита. */
89+
private static final int CHILD_PART = 2;
90+
91+
/** Номер части ссылки на метаданные: имя реквизита табличной части. */
92+
private static final int ATTRIBUTE_PART = 3;
93+
94+
private static final String OBJECT = "Объект.";
95+
private static final String TABULAR_SECTION = "ТабличнаяЧасть.";
96+
private static final String TABULAR_SECTION_ROW = "ТабличнаяЧастьСтрока.";
97+
7998
private final TypeRegistry typeRegistry;
8099

81100
private final Map<MethodSymbol, TypeSet> declaredReturnTypes = new ConcurrentHashMap<>();
@@ -358,6 +377,10 @@ private TypeSet resolveSeeReference(
358377
if (!hyperlinkTypes.isEmpty()) {
359378
return hyperlinkTypes;
360379
}
380+
var metadataTypes = resolveMetadataPath(link, fileType);
381+
if (!metadataTypes.isEmpty()) {
382+
return metadataTypes;
383+
}
361384
// Не разрешилось как ссылка на член (Модуль.Метод / Тип.Член) — пробуем
362385
// трактовать как полное имя типа (например, квалифицированный платформенный
363386
// тип) через TypeRegistry ниже.
@@ -369,6 +392,84 @@ private TypeSet resolveSeeReference(
369392
return typeRegistry.resolve(link, fileType).map(TypeSet::of).orElse(TypeSet.EMPTY);
370393
}
371394

395+
/**
396+
* Тип по ссылке в нотации конфигуратора: {@code Справочник.Товары.ЕдиницыИзмерения} —
397+
* табличная часть, {@code Справочник.Товары.ЕдиницыИзмерения.Единица} — её реквизит,
398+
* {@code Справочник.Товары.Артикул} — реквизит самого объекта.
399+
* <p>
400+
* Имена таких типов реестр складывает из того же вида объекта метаданных, что стоит
401+
* в начале ссылки, поэтому путь собирается прямо из её частей. Части, оставшиеся
402+
* после объекта метаданных, читаются как цепочка членов ({@link #walkMembers}).
403+
*
404+
* @param link ссылка целиком.
405+
* @param fileType язык, на котором резолвятся имена.
406+
* @return тип по ссылке; {@link TypeSet#EMPTY}, если такого пути в метаданных нет.
407+
*/
408+
private TypeSet resolveMetadataPath(String link, FileType fileType) {
409+
// Пустые части сохраняются (-1): «Справочник.Товары.ЕдиницыИзмерения.» — не ссылка
410+
// на саму табличную часть, а ссылка на её реквизит с пустым именем.
411+
var parts = link.split("\\.", -1);
412+
if (parts.length < MIN_METADATA_SEGMENTS) {
413+
return TypeSet.EMPTY;
414+
}
415+
var kind = parts[KIND_PART];
416+
var mdName = parts[NAME_PART];
417+
var childName = parts[CHILD_PART];
418+
419+
var section = typeRegistry.resolve(kind + TABULAR_SECTION + mdName + "." + childName, fileType);
420+
if (section.isPresent()) {
421+
if (parts.length == MIN_METADATA_SEGMENTS) {
422+
return TypeSet.of(section.get());
423+
}
424+
// Реквизиты есть у строки табличной части, а не у неё самой.
425+
return typeRegistry.resolve(kind + TABULAR_SECTION_ROW + mdName + "." + childName, fileType)
426+
.map(rowRef -> walkMembers(rowRef, parts, ATTRIBUTE_PART, fileType))
427+
.orElse(TypeSet.EMPTY);
428+
}
429+
return typeRegistry.resolve(kind + OBJECT + mdName, fileType)
430+
.map(objectRef -> walkMembers(objectRef, parts, CHILD_PART, fileType))
431+
.orElse(TypeSet.EMPTY);
432+
}
433+
434+
/**
435+
* Проход по цепочке членов: каждая следующая часть ссылки берётся как член типа,
436+
* полученного на предыдущей.
437+
* <p>
438+
* Цепочка обрывается, если члена с таким именем нет либо предыдущая часть дала
439+
* больше одного типа — продолжать неоднозначный путь не от чего.
440+
*
441+
* @param ref тип, от которого идёт проход.
442+
* @param parts части ссылки.
443+
* @param from номер части, с которой начинается проход.
444+
* @param fileType язык, на котором ищутся члены.
445+
* @return типы последней части; {@link TypeSet#EMPTY}, если цепочка оборвалась.
446+
*/
447+
private TypeSet walkMembers(TypeRef ref, String[] parts, int from, FileType fileType) {
448+
var current = memberTypes(ref, parts[from], fileType);
449+
for (var i = from + 1; i < parts.length && !current.isEmpty(); i++) {
450+
var refs = current.refs();
451+
if (refs.size() != 1) {
452+
return TypeSet.EMPTY;
453+
}
454+
current = memberTypes(refs.iterator().next(), parts[i], fileType);
455+
}
456+
return current;
457+
}
458+
459+
/**
460+
* Типы члена по имени.
461+
*
462+
* @param ref тип-владелец.
463+
* @param name имя члена.
464+
* @param fileType язык, на котором ищется член.
465+
* @return типы члена; {@link TypeSet#EMPTY}, если члена с таким именем нет.
466+
*/
467+
private TypeSet memberTypes(TypeRef ref, String name, FileType fileType) {
468+
return typeRegistry.findMember(ref, MemberKind.PROPERTY, name, fileType)
469+
.map(MemberDescriptor::returnTypes)
470+
.orElse(TypeSet.EMPTY);
471+
}
472+
372473
/**
373474
* Возвращаемый тип локальной функции, на которую указывает {@code см.}-ссылка.
374475
* <p>
@@ -452,7 +553,7 @@ private TypeSet resolveTypeDescription(TypeDescription td, ResolutionContext con
452553
return switch (td.variant()) {
453554
case HYPERLINK ->
454555
resolveSeeReference(td.name(), context.owner(), context.fileType(), context.visited());
455-
case SIMPLE -> resolveSimple(td.name());
556+
case SIMPLE -> resolveSimple(td, context);
456557
case COLLECTION -> resolveCollection((CollectionTypeDescription) td, context);
457558
};
458559
}
@@ -608,6 +709,46 @@ private Optional<TypeRef> resolveOne(String name) {
608709
* имя сперва спрашивается у реестра как набор; всё прочее — один тип, а незнакомое
609710
* имя остаётся пользовательским типом, как и было.
610711
*/
712+
/**
713+
* Простой тип, возможно уточнённый ссылкой: {@code СтрокаТабличнойЧасти: См. Справочник.Товары.ЕдиницыИзмерения}.
714+
* <p>
715+
* Голова такой записи говорит, чем значение является, а ссылка указывает на коллекцию,
716+
* элементом которой оно служит: строку табличной части, элемент коллекции формы. Поэтому
717+
* у коллекции берётся её элемент — вместе с колонками, которые у него уже есть. Ссылка на
718+
* тип, коллекцией не являющийся, отдаётся как есть.
719+
*
720+
* @param td описание типа.
721+
* @param context контекст разрешения.
722+
* @return тип; {@link TypeSet#EMPTY}, если не разрешился ни ссылкой, ни именем.
723+
*/
724+
private TypeSet resolveSimple(TypeDescription td, ResolutionContext context) {
725+
var hyperlink = td.hyperlink();
726+
if (hyperlink == null) {
727+
return resolveSimple(td.name());
728+
}
729+
var linked = resolveSeeReference(hyperlink.link(), context.owner(), context.fileType(), context.visited());
730+
if (linked.isEmpty()) {
731+
return resolveSimple(td.name());
732+
}
733+
var element = elementOf(linked);
734+
return element.isEmpty() ? linked : element;
735+
}
736+
737+
/**
738+
* Элемент коллекции: уточнённый по месту, а если его нет — тип элемента из реестра.
739+
*
740+
* @param types типы коллекции.
741+
* @return типы элемента; {@link TypeSet#EMPTY}, если коллекции среди них нет.
742+
*/
743+
private TypeSet elementOf(TypeSet types) {
744+
var result = TypeSet.EMPTY;
745+
for (var ref : types.refs()) {
746+
var attached = types.getElementTypes(ref);
747+
result = result.union(attached.isEmpty() ? typeRegistry.getDefaultElementTypes(ref) : attached);
748+
}
749+
return result;
750+
}
751+
611752
private TypeSet resolveSimple(String name) {
612753
if (name.isBlank()) {
613754
return TypeSet.EMPTY;

src/main/java/com/github/_1c_syntax/bsl/languageserver/types/inferencer/ExpressionTypeInferencer.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,7 +1063,9 @@ private TypeSet declaredTypes(VariableSymbol variable) {
10631063
// один путь до присваивания не дошёл.
10641064
return TypeSet.of(UNDEFINED);
10651065
}
1066-
return entry;
1066+
// Объявленному типу-коллекции нужен тип её элемента: «Для Каждого» по параметру,
1067+
// чей тип объявлен комментарием, иначе не знает, что за строку он перебирает.
1068+
return attachDefaultElementTypes(entry);
10671069
}
10681070

10691071
/**
@@ -1147,9 +1149,6 @@ private boolean assignedOnEveryPath(
11471149
) {
11481150
var graph = controlFlowGraphIndex.graphOf(owner, body, CfgBuildOptions.defaults());
11491151
var entry = graph.getEntryPoint();
1150-
if (entry == null) {
1151-
return false;
1152-
}
11531152
if (assigns(entry, positions)) {
11541153
return true;
11551154
}

src/test/java/com/github/_1c_syntax/bsl/languageserver/hover/MethodSymbolMarkupContentBuilderTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,31 @@ void testDeprecatedMethodWithoutInfo() {
180180
assertThat(content).doesNotContain("**Устарела.** ");
181181
}
182182

183+
@Test
184+
void testTypeRefinedByHyperlinkShowsBothParts() {
185+
// given: тип параметра уточнён ссылкой — автор написал обе половины записи.
186+
var documentContext = TestUtils.getDocumentContext("""
187+
// Возвращаемое значение:
188+
// Структура:
189+
// * Ссылка - Строка
190+
Функция НовыйОбъектДанных() Экспорт
191+
Возврат Новый Структура;
192+
КонецФункции
193+
194+
// Параметры:
195+
// Данные - Структура: См. НовыйОбъектДанных
196+
Процедура СоСсылкойНаМетод(Данные) Экспорт
197+
КонецПроцедуры
198+
""");
199+
var methodSymbol = documentContext.getSymbolTree().getMethodSymbol("СоСсылкойНаМетод").orElseThrow();
200+
201+
// when
202+
var content = markupContentBuilder.getContent(referenceTo(documentContext, methodSymbol)).getValue();
203+
204+
// then: в подсказке видны и голова описания, и ссылка — как в исходной записи.
205+
assertThat(content).contains("`Структура`: [НовыйОбъектДанных](НовыйОбъектДанных)");
206+
}
207+
183208
@Test
184209
void testNonDeprecatedMethodHasNoDeprecationBlock() {
185210
// given

src/test/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/BslDocSemanticTokensSupplierTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,29 @@ void testElementsInDescriptionStartingAtNonZeroColumn() {
248248
));
249249
}
250250

251+
@Test
252+
void testTypeRefinedByHyperlinkHighlightsBothParts() {
253+
// given - тип, уточнённый ссылкой: подсвечиваются и голова описания, и сама ссылка,
254+
// ровно как подсвечивается отдельно стоящая ссылка.
255+
String bsl = """
256+
// Параметры:
257+
// Объект - СтрокаТабличнойЧасти: См. Справочник.Справочник1.ТабличнаяЧасть1
258+
Процедура Тест(Объект)
259+
КонецПроцедуры
260+
""";
261+
262+
// when
263+
var decoded = helper.getDecodedTokens(bsl, supplier);
264+
265+
// then
266+
helper.assertContainsTokens(decoded, List.of(
267+
new ExpectedToken(1, 13, 20, SemanticTokenTypes.Type,
268+
Set.of(SemanticTokenModifiers.Documentation), "СтрокаТабличнойЧасти"),
269+
new ExpectedToken(1, 39, 38, SemanticTokenTypes.Type,
270+
Set.of(SemanticTokenModifiers.Documentation), "Справочник.Справочник1.ТабличнаяЧасть1")
271+
));
272+
}
273+
251274
@Test
252275
void testTrailingVariableDescriptionTypeHighlighting() {
253276
// given - висячий (trailing) комментарий переменной с типом в начале (нотация «тип в начале»).

0 commit comments

Comments
 (0)