Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,10 @@
var prev = registeredByUri.put(documentContext.getUri(), ref);
globalScopeProvider.indexModuleType(documentContext.getUri(), ref);
if (prev != null && prev.equals(ref)) {
// тот же URI/тип — источник уже зарегистрирован, AST подхватится автоматически
// источник уже зарегистрирован, но содержимое изменилось (rebuild) — его member-source
// лениво читает символьное дерево, поэтому точечно сбрасываем memo членов только этого
// типа; кэши прочих типов остаются валидными (без сдвига глобальной эпохи).
typeRegistry.invalidateMembers(ref);
return;
}

Expand Down Expand Up @@ -160,6 +163,12 @@
documentContext.getSymbolTree().getModule());

if (prev != null && prev.equals(ref)) {
// содержимое изменилось (rebuild): точечно пересобрать memo членов самого модуля
// и члена GLOBAL_CONTEXT (в него вошёл обновлённый symbol-источник модуля), а также
// name-индекс глобальной области — без сдвига глобальной эпохи членов.
typeRegistry.invalidateMembers(ref);
typeRegistry.invalidateMembers(TypeRegistry.GLOBAL_CONTEXT);
globalScopeProvider.invalidateNameIndex();
return;
}

Expand All @@ -179,11 +188,11 @@
documentContext.getSymbolTree().getMethods().stream()
.filter(MethodSymbol::isExport)
.map(this::toMethodMember)
.forEach(members::add);

Check warning on line 191 in src/main/java/com/github/_1c_syntax/bsl/languageserver/types/registry/ConfigurationModuleMembersProvider.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "collect(Collectors.toList())" instead of "forEach(members::add)".

See more on https://sonarcloud.io/project/issues?id=1c-syntax_bsl-language-server&issues=AZ9sCAlmTAot3AzgAs5K&open=AZ9sCAlmTAot3AzgAs5K&pullRequest=4280
documentContext.getSymbolTree().getVariables().stream()
.filter(VariableSymbol::isExport)
.map(this::toVariableMember)
.forEach(members::add);

Check warning on line 195 in src/main/java/com/github/_1c_syntax/bsl/languageserver/types/registry/ConfigurationModuleMembersProvider.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "collect(Collectors.toList())" instead of "forEach(members::add)".

See more on https://sonarcloud.io/project/issues?id=1c-syntax_bsl-language-server&issues=AZ9sCAlmTAot3AzgAs5L&open=AZ9sCAlmTAot3AzgAs5L&pullRequest=4280
return members;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,19 @@ public GlobalScopeProvider(BslContextHolder bslContextHolder, TypeRegistry typeR
this.byFileType = Map.of(FileType.BSL, bsl, FileType.OS, os);
}

/**
* Точечно сбросить эпоха-кэшированный name-индекс глобальной области: следующий
* {@link #globalMember} пересоберёт его из актуальных членов {@code GLOBAL_CONTEXT}.
* <p>
* Нужен, когда содержимое глобального члена изменилось без сдвига эпохи членов
* реестра — например, при правке общего модуля (BSL): его member-source в
* {@code GLOBAL_CONTEXT} обновляют точечно ({@link TypeRegistry#invalidateMembers}),
* а не через эпоху, поэтому индекс надо освежить отдельно.
*/
public void invalidateNameIndex() {
globalIndexRef.set(null);
}

/**
* Резолв безпрефиксного имени в член глобальной области — синтетического типа
* {@link TypeRegistry#GLOBAL_CONTEXT} (глобальная функция-метод либо глобальное
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -443,8 +443,9 @@ public boolean isEnumType(@Nullable TypeRef ref, FileType fileType) {
* @param fileType язык, в котором он виден без префикса.
*/
public void registerGlobalPropertyType(TypeRef ref, FileType fileType) {
globalPropertyTypes.get(fileType).add(ref);
membersEpoch.incrementAndGet();
if (globalPropertyTypes.get(fileType).add(ref)) {
membersEpoch.incrementAndGet();
}
}

/**
Expand All @@ -456,9 +457,13 @@ public void registerGlobalPropertyType(TypeRef ref, FileType fileType) {
* @param declaration символ-источник, объявивший тип.
*/
public void registerGlobalPropertyType(TypeRef ref, FileType fileType, SourceDefinedSymbol declaration) {
globalPropertyTypes.get(fileType).add(ref);
globalPropertySymbols.put(ref, new WeakReference<>(declaration));
membersEpoch.incrementAndGet();
if (globalPropertyTypes.get(fileType).add(ref)) {
membersEpoch.incrementAndGet();
}
// Повторная пометка (правка уже зарегистрированного модуля) обновляет только
// symbol-источник; инвалидацию memo GLOBAL_CONTEXT-члена и name-индекса, куда
// символ уже вошёл, выполняет вызывающий провайдер точечно.
}

/**
Expand Down Expand Up @@ -571,14 +576,39 @@ private List<MemberSource> resolveMemberSources(TypeRef ref, FileType fileType)
}

/**
* Сбросить memo {@link #getMembers}. Member-source'ы конфигурационных модулей и
* OScript-библиотек лениво читают символьное дерево документа и меняют вывод при
* правке без ре-регистрации источника — поэтому при любом изменении содержимого
* документа memo надо инвалидировать.
* Сбросить memo {@link #getMembers} на изменение содержимого OScript-документа.
* <p>
* Member-source'ы лениво читают символьное дерево документа и меняют вывод при
* правке без ре-регистрации источника, поэтому memo надо инвалидировать. Для
* OScript инвалидация широкая (эпоха), потому что типы связаны межфайловым
* наследованием ({@code &Расширяет}): правка родителя меняет унаследованные
* члены наследника в другом документе. Для BSL member-source конфигурационного
* модуля самодостаточен в пределах документа — там инвалидация точечная, по
* затронутому типу (см. {@code ConfigurationModuleMembersProvider}), поэтому
* глобальную эпоху на BSL-правках не дёргаем (иначе на пакетном анализе каждый
* {@code rebuildDocument} сносил бы memo всех типов).
*
* @param event событие изменения содержимого документа.
*/
@EventListener
public void invalidateMembersCache(DocumentContextContentChangedEvent event) {
membersEpoch.incrementAndGet();
if (event.getSource().getFileType() == FileType.OS) {
membersEpoch.incrementAndGet();
}
}

/**
* Точечно сбросить memo {@link #getMembers} для одного типа во всех языках —
* без сдвига глобальной эпохи (кэши прочих типов остаются валидными).
* Применяется при правке содержимого документа, чьи member-source'ы читают
* только этот тип (BSL-модуль как источник членов своего типа-обёртки).
*
* @param ref тип, memo членов которого нужно пересобрать.
*/
public void invalidateMembers(TypeRef ref) {
for (var fileType : FileType.values()) {
membersCache.remove(new MembersKey(ref, fileType));
}
Comment on lines +588 to +591

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Make targeted invalidation generation-based.

Both APIs clear cached values without changing a version, allowing an in-flight stale computation to republish data after invalidation.

  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/registry/TypeRegistry.java#L608-L611: increment a canonical per-type generation and validate it in getMembers; this should also cover alternate TypeRefs resolving to the same source.
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/registry/GlobalScopeProvider.java#L175-L176: include the GLOBAL_CONTEXT generation in GlobalIndex and reject publication from an older generation.
📍 Affects 2 files
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/registry/TypeRegistry.java#L608-L611 (this comment)
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/registry/GlobalScopeProvider.java#L175-L176
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/types/registry/TypeRegistry.java`
around lines 608 - 611, Make targeted invalidation generation-based: in
TypeRegistry.invalidateMembers, increment a canonical per-type generation keyed
by the resolved source so alternate TypeRefs share it, and have getMembers
capture and validate that generation before publishing cached results. In
GlobalScopeProvider at the specified site, include the GLOBAL_CONTEXT generation
in GlobalIndex and reject publication from computations created under an older
generation. Apply the TypeRegistry change in TypeRegistry.java:608-611 and the
GlobalIndex change in GlobalScopeProvider.java:175-176.

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@

import com.github._1c_syntax.bsl.languageserver.context.FileType;
import com.github._1c_syntax.bsl.languageserver.context.AbstractServerContextAwareTest;
import com.github._1c_syntax.bsl.languageserver.context.events.DocumentContextContentChangedEvent;
import com.github._1c_syntax.bsl.languageserver.types.model.MemberKind;
import com.github._1c_syntax.bsl.languageserver.util.CleanupContextBeforeClassAndAfterClass;
import com.github._1c_syntax.bsl.languageserver.util.TestUtils;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;

import static com.github._1c_syntax.bsl.languageserver.util.TestUtils.PATH_TO_METADATA;
import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -41,6 +43,39 @@
@Autowired
private GlobalScopeProvider globalScopeProvider;

@Autowired
private ApplicationEventPublisher eventPublisher;

@Test
void bslContentChangeInvalidatesOnlyEditedModuleType() {
// given — общий модуль прогрет; мемоизированы и его члены, и члены несвязанного
// платформенного типа (Массив)
initServerContext(PATH_TO_METADATA);
context.getConfiguration();
var moduleDoc = TestUtils.getDocumentContextFromFile(
"src/test/resources/metadata/designer/CommonModules/ПервыйОбщийМодуль/Ext/Module.bsl");

var moduleType = globalScopeProvider.globalMember("ПервыйОбщийМодуль", FileType.BSL)
.flatMap(member -> member.returnTypes().refs().stream().findFirst())
.orElseThrow();
var arrayType = typeRegistry.resolve("Массив").orElseThrow();

assertThat(typeRegistry.getMembers(moduleType, FileType.BSL))
.extracting(member -> member.name()).contains("НеУстаревшаяПроцедура");

Check warning on line 64 in src/test/java/com/github/_1c_syntax/bsl/languageserver/types/registry/ConfigurationModuleMembersProviderTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this lambda with method reference 'MemberDescriptor::name'.

See more on https://sonarcloud.io/project/issues?id=1c-syntax_bsl-language-server&issues=AZ9sCAr6TAot3AzgAs5M&open=AZ9sCAr6TAot3AzgAs5M&pullRequest=4280
var arrayMembersBefore = typeRegistry.getMembers(arrayType, FileType.BSL);
assertThat(arrayMembersBefore).isNotEmpty();

// when — изменение содержимого модуля (как при rebuildDocument на анализе/правке)
eventPublisher.publishEvent(new DocumentContextContentChangedEvent(moduleDoc));

// then — memo несвязанного типа не тронуто (тот же экземпляр списка): инвалидация
// точечная, глобальная эпоха на BSL-правке не двигается (иначе Массив пересобрался бы)
assertThat(typeRegistry.getMembers(arrayType, FileType.BSL)).isSameAs(arrayMembersBefore);
// а члены самого отредактированного модуля пересобираются и остаются корректными
assertThat(typeRegistry.getMembers(moduleType, FileType.BSL))
.extracting(member -> member.name()).contains("НеУстаревшаяПроцедура");

Check warning on line 76 in src/test/java/com/github/_1c_syntax/bsl/languageserver/types/registry/ConfigurationModuleMembersProviderTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this lambda with method reference 'MemberDescriptor::name'.

See more on https://sonarcloud.io/project/issues?id=1c-syntax_bsl-language-server&issues=AZ9sCAr6TAot3AzgAs5N&open=AZ9sCAr6TAot3AzgAs5N&pullRequest=4280
}

@Test
void registersManagerModuleMembers() {
initServerContext(PATH_TO_METADATA);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import static org.assertj.core.api.Assertions.assertThat;

Expand Down Expand Up @@ -115,6 +116,35 @@ void sourceSharedByBothLanguagesIsRegisteredPerFileType() {
assertThat(typeRegistry.getConstructors(ref, FileType.OS)).hasSize(1);
}

@Test
void invalidateMembersEvictsOnlyGivenType() {
// given — два типа с изменяемыми источниками членов, оба прочитаны и мемоизированы
var refA = typeRegistry.intern(TypeKind.PLATFORM, "ТестовыйТочечныйA");
var refB = typeRegistry.intern(TypeKind.PLATFORM, "ТестовыйТочечныйB");
var nameA = new AtomicReference<>("A1");
var nameB = new AtomicReference<>("B1");
typeRegistry.registerMemberSource(refA,
() -> List.of(MemberDescriptor.property(nameA.get(), TypeRef.UNKNOWN, "")), FileType.BSL);
typeRegistry.registerMemberSource(refB,
() -> List.of(MemberDescriptor.property(nameB.get(), TypeRef.UNKNOWN, "")), FileType.BSL);
assertThat(typeRegistry.getMembers(refA, FileType.BSL))
.extracting(MemberDescriptor::name).containsExactly("A1");
assertThat(typeRegistry.getMembers(refB, FileType.BSL))
.extracting(MemberDescriptor::name).containsExactly("B1");

// when — оба источника поменяли вывод, но инвалидируем ТОЛЬКО тип A
nameA.set("A2");
nameB.set("B2");
typeRegistry.invalidateMembers(refA);

// then — A пересобран (виден новый член), B остался из кэша: инвалидация точечная,
// без сдвига глобальной эпохи (иначе B тоже пересобрался бы в "B2")
assertThat(typeRegistry.getMembers(refA, FileType.BSL))
.extracting(MemberDescriptor::name).containsExactly("A2");
assertThat(typeRegistry.getMembers(refB, FileType.BSL))
.extracting(MemberDescriptor::name).containsExactly("B1");
}

@Test
void registerMemberOverrideInvalidatesMembersCache() {
// given — члены типа уже прочитаны и мемоизированы
Expand Down
Loading