Skip to content

Commit 940e22c

Browse files
committed
feat(types): владелец подчинённого справочника типизируется по метаданным
В синтакс-помощнике стандартный реквизит Владелец объявлен типом Неопределено — и для неподчинённого справочника это правда. У подчинённого владельцы известны из Catalog.getOwners(), оттуда и берётся тип (несколько владельцев дают объединение). Заодно бестиповый стандартный реквизит из mdclasses больше не регистрируется: он перекрывал объявление платформы, у которой есть и тип, и описание.
1 parent 6fb9c39 commit 940e22c

9 files changed

Lines changed: 677 additions & 3 deletions

File tree

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* This file is a part of BSL Language Server.
3+
*
4+
* Copyright (c) 2018-2026
5+
* Alexey Sosnoviy <labotamy@gmail.com>, Nikita Fedkin <nixel2007@gmail.com> and contributors
6+
*
7+
* SPDX-License-Identifier: LGPL-3.0-or-later
8+
*
9+
* BSL Language Server is free software; you can redistribute it and/or
10+
* modify it under the terms of the GNU Lesser General Public
11+
* License as published by the Free Software Foundation; either
12+
* version 3.0 of the License, or (at your option) any later version.
13+
*
14+
* BSL Language Server is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17+
* Lesser General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU Lesser General Public
20+
* License along with BSL Language Server.
21+
*/
22+
package com.github._1c_syntax.bsl.languageserver.types.registry;
23+
24+
import com.github._1c_syntax.bsl.languageserver.context.FileType;
25+
import com.github._1c_syntax.bsl.languageserver.infrastructure.WorkspaceScope;
26+
import com.github._1c_syntax.bsl.languageserver.types.model.MemberDescriptor;
27+
import com.github._1c_syntax.bsl.languageserver.types.model.MemberKind;
28+
import com.github._1c_syntax.bsl.languageserver.types.model.TypeRef;
29+
import com.github._1c_syntax.bsl.languageserver.types.model.TypeSet;
30+
import com.github._1c_syntax.bsl.mdo.Catalog;
31+
import com.github._1c_syntax.bsl.mdo.MD;
32+
import lombok.RequiredArgsConstructor;
33+
import org.jspecify.annotations.Nullable;
34+
import org.springframework.stereotype.Component;
35+
36+
import java.util.List;
37+
import java.util.Map;
38+
import java.util.Optional;
39+
40+
/**
41+
* Типизирует стандартный реквизит {@code Владелец} подчинённого справочника.
42+
* <p>
43+
* В синтакс-помощнике он объявлен типом {@code Неопределено} — и для неподчинённого
44+
* справочника это правда. У подчинённого владельцы известны из метаданных, и тип
45+
* берётся оттуда.
46+
*/
47+
@Component
48+
@WorkspaceScope
49+
@RequiredArgsConstructor
50+
class CatalogOwnerTypesRegistrar {
51+
52+
/** Стандартный реквизит подчинённого объекта, ведущий на владельца. */
53+
private static final String OWNER_PROPERTY = "Владелец";
54+
55+
private final TypeRegistry typeRegistry;
56+
57+
/**
58+
* Типизирует стандартный реквизит {@code Владелец} у подчинённого справочника.
59+
* <p>
60+
* В синтакс-помощнике он объявлен типом {@code Неопределено} — и для неподчинённого
61+
* справочника это правда («Неопределено — для неподчиненного справочника»), поэтому
62+
* там ничего не меняется. У подчинённого владельцы известны из метаданных
63+
* ({@code Catalog.getOwners()}), и тип берётся оттуда: владельцев может быть несколько,
64+
* тогда получается объединение.
65+
* <p>
66+
* Здесь не подстановка в плейсхолдер, а прямая замена типа члена: подставлять некуда.
67+
* Члены перебираются у дженерика, поэтому реквизит уточняется сразу во всех типах
68+
* семейства, где он объявлен ({@code СправочникСсылка}, {@code СправочникОбъект} …),
69+
* без перечисления их в коде.
70+
* <p>
71+
* Типы владельцев резолвятся <b>внутри источника членов</b>, а не здесь: справочник
72+
* может обрабатываться раньше своего владельца, и тогда типа владельца ещё нет.
73+
* Порядок обхода конфигурации на это не намекает и полагаться на него нельзя:
74+
* {@code getChildrenByMdoRef()} отдаёт неизменяемую хеш-карту, а её обход JDK
75+
* рандомизирует от запуска к запуску. К моменту, когда членов спросят,
76+
* зарегистрированы все типы.
77+
*/
78+
void registerOwnerMembers(MD md, String familyCore, String mdName) {
79+
if (!(md instanceof Catalog catalog)) {
80+
return;
81+
}
82+
var ownerNames = catalog.getOwners().stream()
83+
.map(owner -> refTypeName(owner.getMdoRefRu()))
84+
.toList();
85+
if (ownerNames.isEmpty()) {
86+
return;
87+
}
88+
for (var generic : typeRegistry.findAllGenericsByFamilyCore(familyCore)) {
89+
registerOwnerMembersOn(generic, mdName, ownerNames);
90+
}
91+
}
92+
93+
/** Регистрирует уточнённый {@code Владелец} на специализации одного дженерика семейства. */
94+
private void registerOwnerMembersOn(TypeRef generic, String mdName, List<String> ownerNames) {
95+
var parameters = typeRegistry.getTypeParameters(generic);
96+
if (parameters.size() != 1) {
97+
return;
98+
}
99+
var bindings = Map.of(parameters.get(0), mdName);
100+
var specialized = typeRegistry.resolve(TypeRef.specialize(generic, bindings).qualifiedName())
101+
.orElse(null);
102+
if (specialized == null || specialized.equals(generic)) {
103+
return;
104+
}
105+
typeRegistry.registerMemberOverride(specialized, () -> ownerMembers(generic, ownerNames), FileType.BSL);
106+
}
107+
108+
/**
109+
* Реквизит {@code Владелец} дженерика с типом владельцев; пусто — у типа его нет
110+
* либо ни один владелец не зарегистрирован (тогда обобщённое объявление платформы
111+
* остаётся как есть — врать про тип хуже, чем не уточнить).
112+
*/
113+
private List<MemberDescriptor> ownerMembers(TypeRef generic, List<String> ownerNames) {
114+
var ownerRefs = ownerNames.stream()
115+
.map(typeRegistry::resolve)
116+
.flatMap(Optional::stream)
117+
.toList();
118+
if (ownerRefs.isEmpty()) {
119+
return List.of();
120+
}
121+
var ownerTypes = TypeSet.of(ownerRefs);
122+
for (var member : typeRegistry.getMembers(generic, FileType.BSL)) {
123+
if (member.kind() == MemberKind.PROPERTY && member.matches(OWNER_PROPERTY)) {
124+
return List.of(member.withReturnTypes(ownerTypes));
125+
}
126+
}
127+
return List.of();
128+
}
129+
130+
/** {@code Справочник.Владелец1} → {@code СправочникСсылка.Владелец1}. */
131+
private static String refTypeName(String mdoRef) {
132+
var dot = mdoRef.indexOf('.');
133+
return dot < 0 ? mdoRef : mdoRef.substring(0, dot) + "Ссылка" + mdoRef.substring(dot);
134+
}
135+
136+
}

src/main/java/com/github/_1c_syntax/bsl/languageserver/types/registry/ConfigurationTypesProvider.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@
3232
import com.github._1c_syntax.bsl.languageserver.infrastructure.WorkspaceScope;
3333
import com.github._1c_syntax.bsl.languageserver.types.model.BilingualString;
3434
import com.github._1c_syntax.bsl.languageserver.types.model.MemberDescriptor;
35+
import com.github._1c_syntax.bsl.languageserver.types.model.MemberKind;
3536
import com.github._1c_syntax.bsl.languageserver.types.model.MemberSource;
3637
import com.github._1c_syntax.bsl.languageserver.types.model.PlatformMetadata;
3738
import com.github._1c_syntax.bsl.languageserver.types.model.TypeRef;
3839
import com.github._1c_syntax.bsl.languageserver.types.model.TypeSet;
3940
import com.github._1c_syntax.bsl.mdo.Attribute;
4041
import com.github._1c_syntax.bsl.mdo.AttributeOwner;
4142
import com.github._1c_syntax.bsl.mdo.CalculationRegister;
43+
import com.github._1c_syntax.bsl.mdo.Catalog;
4244
import com.github._1c_syntax.bsl.mdo.ChartOfAccounts;
4345
import com.github._1c_syntax.bsl.mdo.ChartOfCalculationTypes;
4446
import com.github._1c_syntax.bsl.mdo.CommonAttribute;
@@ -70,6 +72,7 @@
7072
import java.util.List;
7173
import java.util.Locale;
7274
import java.util.Map;
75+
import java.util.Optional;
7376
import java.util.Set;
7477
import java.util.concurrent.CompletableFuture;
7578
import java.util.concurrent.atomic.AtomicBoolean;
@@ -178,6 +181,7 @@ public class ConfigurationTypesProvider {
178181
private final LanguageServerConfiguration configuration;
179182
private final MetadataCollectionSpecializer metadataCollectionSpecializer;
180183
private final ConfigurationGenericExpander genericExpander;
184+
private final CatalogOwnerTypesRegistrar catalogOwnerTypes;
181185
private final ServiceModuleEventRegistrar serviceModuleEventRegistrar;
182186
private final RegisterTypesRegistrar registerTypesRegistrar;
183187
private final RecorderIndex recorderIndex;
@@ -367,6 +371,7 @@ private void registerSpecializationsAndExpansions(MD md, TypeRef ref, String nam
367371
registerFamilySpecializations(familyCore, name);
368372
registerTypesRegistrar.registerFamilyFixups(md, familyCore, name);
369373
registerHierarchySuppressions(md, familyCore, name);
374+
catalogOwnerTypes.registerOwnerMembers(md, familyCore, name);
370375
registerDerivedSpecializations(md, name);
371376
if (md instanceof DocumentJournal journal) {
372377
registerDocumentJournalColumnMembers(journal, familyCore, name);
@@ -683,7 +688,6 @@ private void registerInheritedMembers(TypeRef target, String exactName) {
683688
return typeRegistry.getMembers(parent, FileType.BSL);
684689
}, FileType.BSL);
685690
}
686-
687691
/**
688692
* Регистрирует специализации ВСЕХ зарегистрированных дженериков семейства
689693
* (с qualifiedName, начинающимся с {@code familyCore}) для конкретного
@@ -908,6 +912,12 @@ private List<MemberDescriptor> buildAttributeMembers(List<? extends Attribute> a
908912
var description = platformDescriptions.getOrDefault(lc, BilingualString.EMPTY);
909913
var meta = platformMetadata.getOrDefault(lc, PlatformMetadata.EMPTY);
910914
var returnTypes = resolveAttributeReturnTypes(attribute);
915+
if (returnTypes.isEmpty() && attribute instanceof StandardAttribute) {
916+
// Стандартный реквизит платформа объявляет сама — с типом, описанием и мета.
917+
// mdclasses про его тип знает не всегда (у `Владелец` типа нет вовсе), и
918+
// бестиповый дубль отсюда только перекрыл бы объявление платформы.
919+
continue;
920+
}
911921
var primaryName = bilingualName.primary();
912922
MemberDescriptor descriptor;
913923
if (returnTypes.isEmpty()) {

src/main/resources/com/github/_1c_syntax/bsl/languageserver/types/registry/builtin-platform-types.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2820,7 +2820,11 @@
28202820
},
28212821
{
28222822
"name": "Владелец",
2823-
"kind": "PROPERTY"
2823+
"nameRu": "Владелец",
2824+
"nameEn": "Owner",
2825+
"kind": "PROPERTY",
2826+
"returnType": "Неопределено",
2827+
"description": "Содержит ссылку на владельца элемента справочника. Имеет смысл только для подчиненных справочников."
28242828
},
28252829
{
28262830
"name": "ЭтоГруппа",
@@ -3474,6 +3478,15 @@
34743478
"kind": "TYPE",
34753479
"name": "СправочникСсылка.<Имя справочника>",
34763480
"members": [
3481+
{
3482+
"name": "Владелец",
3483+
"nameRu": "Владелец",
3484+
"nameEn": "Owner",
3485+
"kind": "PROPERTY",
3486+
"returnType": "Неопределено",
3487+
"accessMode": "READ",
3488+
"description": "Содержит ссылку на владельца элемента справочника. Имеет смысл только для подчиненных справочников."
3489+
},
34773490
{
34783491
"name": "<Имя реквизита>",
34793492
"kind": "PROPERTY",
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* This file is a part of BSL Language Server.
3+
*
4+
* Copyright (c) 2018-2026
5+
* Alexey Sosnoviy <labotamy@gmail.com>, Nikita Fedkin <nixel2007@gmail.com> and contributors
6+
*
7+
* SPDX-License-Identifier: LGPL-3.0-or-later
8+
*
9+
* BSL Language Server is free software; you can redistribute it and/or
10+
* modify it under the terms of the GNU Lesser General Public
11+
* License as published by the Free Software Foundation; either
12+
* version 3.0 of the License, or (at your option) any later version.
13+
*
14+
* BSL Language Server is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17+
* Lesser General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU Lesser General Public
20+
* License along with BSL Language Server.
21+
*/
22+
package com.github._1c_syntax.bsl.languageserver.types.registry;
23+
24+
import com.github._1c_syntax.bsl.languageserver.context.AbstractServerContextAwareTest;
25+
import com.github._1c_syntax.bsl.languageserver.context.FileType;
26+
import com.github._1c_syntax.bsl.languageserver.types.model.MemberKind;
27+
import com.github._1c_syntax.bsl.languageserver.types.model.TypeRef;
28+
import com.github._1c_syntax.bsl.languageserver.util.CleanupContextBeforeClassAndAfterClass;
29+
import com.github._1c_syntax.utils.Absolute;
30+
import org.junit.jupiter.api.BeforeEach;
31+
import org.junit.jupiter.api.Test;
32+
import org.springframework.beans.factory.annotation.Autowired;
33+
34+
import java.util.List;
35+
36+
import static org.assertj.core.api.Assertions.assertThat;
37+
38+
/**
39+
* Стандартный реквизит {@code Владелец} подчинённого справочника. В синтакс-помощнике он
40+
* объявлен типом {@code Неопределено}; конкретный тип известен только из метаданных —
41+
* из списка владельцев.
42+
*/
43+
@CleanupContextBeforeClassAndAfterClass
44+
class CatalogOwnerTypeTest extends AbstractServerContextAwareTest {
45+
46+
/**
47+
* В общей фикстуре справочники никому не подчинены, поэтому нужна эта: справочник,
48+
* подчинённый другому справочнику той же конфигурации.
49+
* <p>
50+
* Порядок обработки этих двух справочников зафиксировать нельзя — конфигурация
51+
* отдаёт детей неизменяемой хеш-картой, обход которой JDK рандомизирует от запуска
52+
* к запуску. Поэтому тест проходит независимо от порядка только потому, что типы
53+
* владельцев резолвятся лениво (см. {@code registerOwnerMembers}); при резолве на
54+
* регистрации он был бы плавающим.
55+
*/
56+
private static final String PATH_TO_METADATA = "src/test/resources/metadata/catalogOwners";
57+
58+
private static final String OWNER = "Владелец";
59+
60+
@Autowired
61+
private ConfigurationTypesProvider provider;
62+
63+
@Autowired
64+
private TypeRegistry typeRegistry;
65+
66+
@BeforeEach
67+
void setUp() {
68+
initServerContextOnce(Absolute.path(PATH_TO_METADATA));
69+
context.getConfiguration();
70+
provider.tryRegister();
71+
}
72+
73+
@Test
74+
void ownerOfSubordinateCatalogIsTypedByItsOwners() {
75+
assertThat(memberTypes("СправочникСсылка.Справочник1", OWNER))
76+
.containsExactly("СправочникСсылка.Справочник2");
77+
}
78+
79+
@Test
80+
void ownerIsRefinedInEveryTypeOfTheFamilyThatDeclaresIt() {
81+
// Члены перебираются у дженерика, поэтому реквизит уточняется всюду, где объявлен, —
82+
// без перечисления типов семейства в коде.
83+
assertThat(memberTypes("СправочникОбъект.Справочник1", OWNER))
84+
.containsExactly("СправочникСсылка.Справочник2");
85+
}
86+
87+
@Test
88+
void ownerOfIndependentCatalogStaysUndefined() {
89+
// Для неподчинённого справочника в синтакс-помощнике так и написано:
90+
// «Неопределено — для неподчиненного справочника». Тип не теряется — его и нет.
91+
assertThat(memberTypes("СправочникСсылка.Справочник2", OWNER))
92+
.containsExactly("Неопределено");
93+
assertThat(memberTypes("СправочникОбъект.Справочник2", OWNER))
94+
.containsExactly("Неопределено");
95+
}
96+
97+
@Test
98+
void standardAttributeWithoutTypeInMetadataKeepsThePlatformDeclaration() {
99+
// Реквизиты из mdclasses регистрируются поверх платформенных, поэтому бестиповый
100+
// стандартный реквизит перекрывал бы объявление платформы вместе с его типом.
101+
// Проверяется на дженерике: у него источник из конфигурации заведомо один — платформа.
102+
assertThat(memberTypes("СправочникСсылка.<Имя справочника>", OWNER))
103+
.containsExactly("Неопределено");
104+
}
105+
106+
private List<String> memberTypes(String typeName, String memberName) {
107+
var ref = typeRegistry.resolve(typeName).orElseThrow();
108+
var member = typeRegistry.getMembers(ref, FileType.BSL).stream()
109+
.filter(m -> m.kind() == MemberKind.PROPERTY && m.matches(memberName))
110+
.findFirst();
111+
assertThat(member).as("реквизит %s у %s", memberName, typeName).isPresent();
112+
return member.orElseThrow().returnTypes().refs().stream()
113+
.map(TypeRef::qualifiedName)
114+
.toList();
115+
}
116+
}

src/test/java/com/github/_1c_syntax/bsl/languageserver/types/registry/ConfigurationTypesProviderHelpersTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,8 @@ private static ConfigurationTypesProvider newProvider(TypeRegistry registry,
323323
MetadataCollectionSpecializer mcs) {
324324
var recorderIndex = new RecorderIndex();
325325
return new ConfigurationTypesProvider(registry, serverProvider, globalScope, lsConfig, mcs,
326-
new ConfigurationGenericExpander(registry, serverProvider), new ServiceModuleEventRegistrar(registry),
326+
new ConfigurationGenericExpander(registry, serverProvider), new CatalogOwnerTypesRegistrar(registry),
327+
new ServiceModuleEventRegistrar(registry),
327328
new RegisterTypesRegistrar(registry, recorderIndex), recorderIndex, new SimpleAsyncTaskExecutor());
328329
}
329330

0 commit comments

Comments
 (0)