@@ -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 ;
0 commit comments