Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
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);
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 @@ -65,7 +65,6 @@
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Supplier;
Expand Down Expand Up @@ -109,7 +108,7 @@
* {@code GlobalScopeProvider → TypeRegistry}, без цикла.
*/
private final TypeRegistry typeRegistry;
/** Эпоха-кэшированный name-индекс членов GLOBAL_CONTEXT (см. {@link #globalMember}). */
/** Name-индекс членов GLOBAL_CONTEXT, кэшированный по наборам-источникам (см. {@link #globalMember}). */
private final AtomicReference<GlobalIndex> globalIndexRef = new AtomicReference<>();
/**
* URI документа-модуля → его тип-значение (обратный индекс к name-keyed записям).
Expand Down Expand Up @@ -179,12 +178,18 @@
if (name == null || name.isBlank()) {
return Optional.empty();
}
var epoch = typeRegistry.membersEpoch();
// Индекс — производная от членов GLOBAL_CONTEXT, поэтому его актуальность определяется
// самими наборами-источниками: getMembers отдаёт тот же экземпляр списка, пока memo живо,
// и новый — после любой инвалидации (эпоха или пер-типовое поколение). Отдельный счётчик
// поколения индекса не нужен: сверки идентичности источников достаточно, и она же
// отбрасывает индекс, собранный параллельно из устаревших членов.
var bslSource = typeRegistry.getMembers(TypeRegistry.GLOBAL_CONTEXT, FileType.BSL);
var osSource = typeRegistry.getMembers(TypeRegistry.GLOBAL_CONTEXT, FileType.OS);
var index = globalIndexRef.get();
if (index == null || index.epoch() != epoch) {
index = new GlobalIndex(epoch, Map.of(
FileType.BSL, globalNameIndex(FileType.BSL),
FileType.OS, globalNameIndex(FileType.OS)));
if (index == null || index.bslSource() != bslSource || index.osSource() != osSource) {

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the "equals" method if value comparison was intended.

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

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the "equals" method if value comparison was intended.

See more on https://sonarcloud.io/project/issues?id=1c-syntax_bsl-language-server&issues=AZ-Fk_ebqf24P60kk4Rf&open=AZ-Fk_ebqf24P60kk4Rf&pullRequest=4280
index = new GlobalIndex(bslSource, osSource, Map.of(
FileType.BSL, globalNameIndex(bslSource),
FileType.OS, globalNameIndex(osSource)));
globalIndexRef.set(index);
}
return Optional.ofNullable(index.byName().get(fileType).get(name.toLowerCase(Locale.ROOT)));
Expand Down Expand Up @@ -250,9 +255,9 @@
return result;
}

private Map<String, MemberDescriptor> globalNameIndex(FileType fileType) {
private Map<String, MemberDescriptor> globalNameIndex(Collection<MemberDescriptor> members) {

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make "globalNameIndex" a "static" method.

See more on https://sonarcloud.io/project/issues?id=1c-syntax_bsl-language-server&issues=AZ-Fk_ebqf24P60kk4Rg&open=AZ-Fk_ebqf24P60kk4Rg&pullRequest=4280
var map = new HashMap<String, MemberDescriptor>();
for (var member : typeRegistry.getMembers(TypeRegistry.GLOBAL_CONTEXT, fileType)) {
for (var member : members) {
var ru = member.bilingualName().ru();
var en = member.bilingualName().en();
if (!ru.isBlank()) {
Expand All @@ -265,8 +270,13 @@
return map;
}

/** Эпоха-кэшированный индекс имён членов GLOBAL_CONTEXT в разрезе языка. */
private record GlobalIndex(long epoch, Map<FileType, Map<String, MemberDescriptor>> byName) {
/**
* Индекс имён членов GLOBAL_CONTEXT в разрезе языка вместе с наборами-источниками,
* из которых он собран: сверка их идентичности и есть критерий актуальности индекса.
*/
private record GlobalIndex(Collection<MemberDescriptor> bslSource,
Collection<MemberDescriptor> osSource,
Map<FileType, Map<String, MemberDescriptor>> byName) {
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,15 @@ public class TypeRegistry {
*/
private final AtomicLong membersEpoch = new AtomicLong();
private final Map<MembersKey, CachedMembers> membersCache = new ConcurrentHashMap<>();
/**
* Пер-типовые поколения memo членов: точечная инвалидация ({@link #invalidateMembers})
* инкрементирует поколение ключа, а {@link #getMembers} штампует им запись кэша и
* отвергает публикацию из устаревшего поколения. Защищает от гонки, когда параллельный
* незавершённый {@code computeMembers} публикует устаревший результат уже ПОСЛЕ
* инвалидации (эпоха при точечной инвалидации не двигается). Ключи заводятся только для
* реально инвалидированных типов — для нетронутых поколение по умолчанию {@code 0}.
*/
private final Map<MembersKey, Long> membersGeneration = new ConcurrentHashMap<>();

private record MembersKey(TypeRef ref, FileType fileType) implements Comparable<MembersKey> {

Expand All @@ -141,7 +150,7 @@ public int compareTo(MembersKey other) {
}
}

private record CachedMembers(long epoch, List<MemberDescriptor> members) {
private record CachedMembers(long epoch, long generation, List<MemberDescriptor> members) {
}

/** Пустой контейнер с разрезами по всем языкам. */
Expand Down Expand Up @@ -389,12 +398,16 @@ public Type get(TypeRef ref) {
public Collection<MemberDescriptor> getMembers(TypeRef ref, FileType fileType) {
var epoch = membersEpoch.get();
var key = new MembersKey(ref, fileType);
var generation = membersGeneration.getOrDefault(key, 0L);
var cached = membersCache.get(key);
if (cached != null && cached.epoch() == epoch) {
if (cached != null && cached.epoch() == epoch && cached.generation() == generation) {
return cached.members();
}
var members = computeMembers(ref, fileType);
membersCache.put(key, new CachedMembers(epoch, members));
// Штампуем поколением, снятым ДО вычисления: если во время computeMembers прошла
// точечная инвалидация (bump поколения), запись окажется устаревшей и будет
// отвергнута следующим чтением — гонка «публикация устаревшего результата» закрыта.
membersCache.put(key, new CachedMembers(epoch, generation, members));
return members;
}

Expand Down Expand Up @@ -443,8 +456,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 +470,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 +589,44 @@ 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-модуль как источник членов своего типа-обёртки).
* <p>
* Инвалидация — через инкремент пер-типового поколения ({@link #membersGeneration}),
* а не удаление записи: это отвергает и уже закэшированный результат, и устаревший
* результат параллельного незавершённого {@code computeMembers}, который допишется
* в кэш уже после инвалидации. Запись выселяется перезаписью при следующем чтении.
*
* @param ref тип, memo членов которого нужно пересобрать.
*/
public void invalidateMembers(TypeRef ref) {
for (var fileType : FileType.values()) {
membersGeneration.merge(new MembersKey(ref, fileType), 1L, Long::sum);
}
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,14 @@

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.MemberDescriptor;
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 +44,39 @@ class ConfigurationModuleMembersProviderTest extends AbstractServerContextAwareT
@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(MemberDescriptor::name).contains("НеУстаревшаяПроцедура");
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(MemberDescriptor::name).contains("НеУстаревшаяПроцедура");
}

@Test
void registersManagerModuleMembers() {
initServerContext(PATH_TO_METADATA);
Expand Down
Loading
Loading