and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * BSL Language Server is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * BSL Language Server is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with BSL Language Server.
+ */
+package com.github._1c_syntax.bsl.languageserver.configuration.references;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Настройки для построения индекса ссылок.
+ *
+ * Позволяет указать список модулей и методов, возвращающих ссылку на общий модуль
+ * (например, ОбщегоНазначения.ОбщийМодуль("ИмяМодуля")).
+ */
+@Data
+@AllArgsConstructor(onConstructor = @__({@JsonCreator(mode = JsonCreator.Mode.DISABLED)}))
+@NoArgsConstructor
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ReferencesOptions {
+
+ /**
+ * Список паттернов "Модуль.Метод" для методов, возвращающих ссылку на общий модуль.
+ *
+ * Формат: "ИмяМодуля.ИмяМетода", например:
+ *
+ * - "ОбщегоНазначения.ОбщийМодуль"
+ * - "ОбщегоНазначенияКлиент.ОбщийМодуль"
+ * - "CommonUse.CommonModule"
+ * - "ОбщийМодуль" - для локального вызова без указания модуля
+ *
+ *
+ * По умолчанию включает стандартные варианты из БСП.
+ */
+ private List commonModuleAccessors = new ArrayList<>(List.of(
+ // Локальный вызов
+ "ОбщийМодуль",
+ "CommonModule",
+ // Стандартные модули БСП
+ "ОбщегоНазначения.ОбщийМодуль",
+ "ОбщегоНазначенияКлиент.ОбщийМодуль",
+ "ОбщегоНазначенияСервер.ОбщийМодуль",
+ "ОбщегоНазначенияКлиентСервер.ОбщийМодуль",
+ "ОбщегоНазначенияПовтИсп.ОбщийМодуль",
+ // Английские варианты
+ "CommonUse.CommonModule",
+ "CommonUseClient.CommonModule",
+ "CommonUseServer.CommonModule",
+ "CommonUseClientServer.CommonModule"
+ ));
+}
diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/configuration/references/package-info.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/configuration/references/package-info.java
new file mode 100644
index 00000000000..676c5f5dba7
--- /dev/null
+++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/configuration/references/package-info.java
@@ -0,0 +1,28 @@
+/*
+ * This file is a part of BSL Language Server.
+ *
+ * Copyright (c) 2018-2025
+ * Alexey Sosnoviy , Nikita Fedkin and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * BSL Language Server is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * BSL Language Server is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with BSL Language Server.
+ */
+/**
+ * Пакет содержит настройки для построения индекса ссылок.
+ */
+@NullMarked
+package com.github._1c_syntax.bsl.languageserver.configuration.references;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/hover/DescriptionFormatter.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/hover/DescriptionFormatter.java
index 69bc8ef339d..a0ca7ca79c2 100644
--- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/hover/DescriptionFormatter.java
+++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/hover/DescriptionFormatter.java
@@ -23,6 +23,7 @@
import com.github._1c_syntax.bsl.languageserver.context.symbol.AnnotationSymbol;
import com.github._1c_syntax.bsl.languageserver.context.symbol.MethodSymbol;
+import com.github._1c_syntax.bsl.languageserver.context.symbol.ModuleSymbol;
import com.github._1c_syntax.bsl.languageserver.context.symbol.ParameterDefinition;
import com.github._1c_syntax.bsl.languageserver.context.symbol.VariableSymbol;
import com.github._1c_syntax.bsl.languageserver.context.symbol.description.MethodDescription;
@@ -137,6 +138,23 @@ public String getSectionWithCodeFences(Collection codeBlocks, String res
return codeFences;
}
+ public String getLocation(ModuleSymbol symbol) {
+ var documentContext = symbol.getOwner();
+ var uri = documentContext.getUri();
+
+ var mdObject = documentContext.getMdObject();
+ String mdoRefLocal = mdObject.map(md -> documentContext.getServerContext()
+ .getConfiguration()
+ .getMdoRefLocal(md)
+ ).orElseGet(documentContext::getMdoRef);
+
+ return String.format(
+ "[%s](%s)",
+ mdoRefLocal,
+ uri
+ );
+ }
+
public String getLocation(MethodSymbol symbol) {
var documentContext = symbol.getOwner();
var startPosition = symbol.getSelectionRange().getStart();
diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/hover/ModuleSymbolMarkupContentBuilder.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/hover/ModuleSymbolMarkupContentBuilder.java
new file mode 100644
index 00000000000..d1463a028ce
--- /dev/null
+++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/hover/ModuleSymbolMarkupContentBuilder.java
@@ -0,0 +1,139 @@
+/*
+ * This file is a part of BSL Language Server.
+ *
+ * Copyright (c) 2018-2025
+ * Alexey Sosnoviy , Nikita Fedkin and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * BSL Language Server is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * BSL Language Server is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with BSL Language Server.
+ */
+package com.github._1c_syntax.bsl.languageserver.hover;
+
+import com.github._1c_syntax.bsl.languageserver.context.symbol.ModuleSymbol;
+import com.github._1c_syntax.bsl.languageserver.utils.Resources;
+import com.github._1c_syntax.bsl.mdo.CommonModule;
+import lombok.RequiredArgsConstructor;
+import org.eclipse.lsp4j.MarkupContent;
+import org.eclipse.lsp4j.MarkupKind;
+import org.eclipse.lsp4j.SymbolKind;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.StringJoiner;
+
+/**
+ * Построитель контента для всплывающего окна для {@link ModuleSymbol}.
+ */
+@Component
+@RequiredArgsConstructor
+public class ModuleSymbolMarkupContentBuilder implements MarkupContentBuilder {
+
+ private final Resources resources;
+ private final DescriptionFormatter descriptionFormatter;
+
+ @Override
+ public MarkupContent getContent(ModuleSymbol symbol) {
+ var markupBuilder = new StringJoiner("\n");
+
+ // Местоположение модуля
+ String moduleLocation = descriptionFormatter.getLocation(symbol);
+ descriptionFormatter.addSectionIfNotEmpty(markupBuilder, moduleLocation);
+
+ // Информация о модуле из метаданных
+ String moduleInfo = getModuleInfo(symbol);
+ descriptionFormatter.addSectionIfNotEmpty(markupBuilder, moduleInfo);
+
+ String content = markupBuilder.toString();
+ return new MarkupContent(MarkupKind.MARKDOWN, content);
+ }
+
+ @Override
+ public SymbolKind getSymbolKind() {
+ return SymbolKind.Module;
+ }
+
+ private String getModuleInfo(ModuleSymbol symbol) {
+ var documentContext = symbol.getOwner();
+ var mdObject = documentContext.getMdObject();
+
+ if (mdObject.isEmpty()) {
+ return "";
+ }
+
+ var mdo = mdObject.get();
+ if (!(mdo instanceof CommonModule commonModule)) {
+ return "";
+ }
+
+ var moduleInfoBuilder = new StringJoiner("\n");
+
+ // Комментарий
+ var comment = commonModule.getComment();
+ if (!comment.isBlank()) {
+ moduleInfoBuilder.add(comment);
+ moduleInfoBuilder.add("");
+ }
+
+ // Флаги доступности
+ var flags = new ArrayList();
+
+ if (commonModule.isServer()) {
+ flags.add(getResourceString("server"));
+ }
+ if (commonModule.isClientManagedApplication()) {
+ flags.add(getResourceString("clientManagedApplication"));
+ }
+ if (commonModule.isClientOrdinaryApplication()) {
+ flags.add(getResourceString("clientOrdinaryApplication"));
+ }
+ if (commonModule.isExternalConnection()) {
+ flags.add(getResourceString("externalConnection"));
+ }
+ if (commonModule.isServerCall()) {
+ flags.add(getResourceString("serverCall"));
+ }
+ if (commonModule.isPrivileged()) {
+ flags.add(getResourceString("privilegedMode"));
+ }
+ if (commonModule.isGlobal()) {
+ flags.add(getResourceString("global"));
+ }
+
+ if (!flags.isEmpty()) {
+ var flagsHeader = "**" + getResourceString("availability") + ":** ";
+ moduleInfoBuilder.add(flagsHeader + String.join(", ", flags));
+ moduleInfoBuilder.add("");
+ }
+
+ // Режим повторного использования
+ var returnValueReuse = commonModule.getReturnValuesReuse();
+ var reuseKey = switch (returnValueReuse) {
+ case DURING_REQUEST -> "duringRequest";
+ case DURING_SESSION -> "duringSession";
+ case DONT_USE, UNKNOWN -> "";
+ };
+
+ if (!reuseKey.isEmpty()) {
+ var reuseHeader = "**" + getResourceString("returnValuesReuse") + ":** ";
+ moduleInfoBuilder.add(reuseHeader + getResourceString(reuseKey));
+ }
+
+ return moduleInfoBuilder.toString();
+ }
+
+ private String getResourceString(String key) {
+ return resources.getResourceString(getClass(), key);
+ }
+}
diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndex.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndex.java
index 3987fd57513..521c5707b76 100644
--- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndex.java
+++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndex.java
@@ -195,6 +195,35 @@ public void addMethodCall(URI uri, String mdoRef, ModuleType moduleType, String
locationRepository.updateLocation(symbolOccurrence);
}
+ /**
+ * Добавить ссылку на модуль в индекс.
+ *
+ * @param uri URI документа, откуда произошло обращение к модулю.
+ * @param mdoRef Ссылка на объект-метаданных модуля (например, CommonModule.ОбщийМодуль1).
+ * @param moduleType Тип модуля (например, {@link ModuleType#CommonModule}).
+ * @param range Диапазон, в котором происходит обращение к модулю.
+ */
+ public void addModuleReference(URI uri, String mdoRef, ModuleType moduleType, Range range) {
+ var symbol = Symbol.builder()
+ .mdoRef(mdoRef)
+ .moduleType(moduleType)
+ .scopeName("")
+ .symbolKind(SymbolKind.Module)
+ .symbolName("")
+ .build()
+ .intern();
+
+ var location = new Location(uri, range);
+ var symbolOccurrence = SymbolOccurrence.builder()
+ .occurrenceType(OccurrenceType.REFERENCE)
+ .symbol(symbol)
+ .location(location)
+ .build();
+
+ symbolOccurrenceRepository.save(symbolOccurrence);
+ locationRepository.updateLocation(symbolOccurrence);
+ }
+
/**
* Добавить обращение к переменной в индекс.
*
@@ -266,6 +295,12 @@ private Optional getSourceDefinedSymbol(Symbol symbolEntity
.or(() -> symbolTree.getVariableSymbol(symbolName, symbolTree.getModule())));
}
+ if (symbolEntity.symbolKind() == SymbolKind.Module) {
+ return serverContext.getDocument(mdoRef, moduleType)
+ .map(DocumentContext::getSymbolTree)
+ .map(SymbolTree::getModule);
+ }
+
return serverContext.getDocument(mdoRef, moduleType)
.map(DocumentContext::getSymbolTree)
.flatMap(symbolTree -> symbolTree.getMethodSymbol(symbolName));
diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndexFiller.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndexFiller.java
index b9fea5f235f..0dbcd7e6e22 100644
--- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndexFiller.java
+++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndexFiller.java
@@ -21,6 +21,7 @@
*/
package com.github._1c_syntax.bsl.languageserver.references;
+import com.github._1c_syntax.bsl.languageserver.configuration.LanguageServerConfiguration;
import com.github._1c_syntax.bsl.languageserver.context.DocumentContext;
import com.github._1c_syntax.bsl.languageserver.context.events.DocumentContextContentChangedEvent;
import com.github._1c_syntax.bsl.languageserver.context.events.ServerContextDocumentRemovedEvent;
@@ -28,6 +29,7 @@
import com.github._1c_syntax.bsl.languageserver.context.symbol.VariableSymbol;
import com.github._1c_syntax.bsl.languageserver.utils.MdoRefBuilder;
import com.github._1c_syntax.bsl.languageserver.utils.Methods;
+import com.github._1c_syntax.bsl.languageserver.utils.ModuleReference;
import com.github._1c_syntax.bsl.languageserver.utils.Modules;
import com.github._1c_syntax.bsl.languageserver.utils.NotifyDescription;
import com.github._1c_syntax.bsl.languageserver.utils.Ranges;
@@ -37,19 +39,23 @@
import com.github._1c_syntax.bsl.parser.BSLParser;
import com.github._1c_syntax.bsl.parser.BSLParserBaseVisitor;
import com.github._1c_syntax.bsl.types.ModuleType;
-import org.jspecify.annotations.Nullable;
import lombok.RequiredArgsConstructor;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.ParseTree;
+import org.antlr.v4.runtime.tree.TerminalNode;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.SymbolKind;
+import org.jspecify.annotations.Nullable;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.EnumSet;
+import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
+import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -73,6 +79,7 @@ public class ReferenceIndexFiller {
);
private final ReferenceIndex index;
+ private final LanguageServerConfiguration languageServerConfiguration;
@EventListener
public void handleEvent(DocumentContextContentChangedEvent event) {
@@ -133,6 +140,9 @@ public ParserRuleContext visitCallStatement(BSLParser.CallStatementContext ctx)
return super.visitCallStatement(ctx);
}
+ // Добавляем ссылку на модуль по позиции идентификатора (только для общих модулей)
+ addModuleReferenceForCommonModuleIdentifier(ctx.IDENTIFIER());
+
Methods.getMethodName(ctx).ifPresent(methodName -> checkCall(mdoRef, methodName));
return super.visitCallStatement(ctx);
@@ -145,6 +155,9 @@ public ParserRuleContext visitComplexIdentifier(BSLParser.ComplexIdentifierConte
return super.visitComplexIdentifier(ctx);
}
+ // Добавляем ссылку на модуль по позиции идентификатора (только для общих модулей)
+ addModuleReferenceForCommonModuleIdentifier(ctx.IDENTIFIER());
+
Methods.getMethodName(ctx).ifPresent(methodName -> checkCall(mdoRef, methodName));
return super.visitComplexIdentifier(ctx);
}
@@ -219,6 +232,31 @@ private void checkCall(String mdoRef, Token methodName) {
}
}
+ /**
+ * Добавляет ссылку на модуль по позиции идентификатора, только если идентификатор является
+ * именем общего модуля. Для вызовов вида Справочники.Имя.Метод() ссылка не добавляется,
+ * так как "Справочники" - это тип MDO, а не имя модуля.
+ */
+ private void addModuleReferenceForCommonModuleIdentifier(@Nullable TerminalNode identifier) {
+ if (identifier == null) {
+ return;
+ }
+
+ var identifierText = identifier.getText();
+
+ documentContext.getServerContext()
+ .getConfiguration()
+ .findCommonModule(identifierText)
+ .ifPresent(commonModule -> {
+ index.addModuleReference(
+ documentContext.getUri(),
+ commonModule.getMdoReference().getMdoRef(),
+ ModuleType.CommonModule,
+ Ranges.create(identifier)
+ );
+ });
+ }
+
private void addMethodCall(String mdoRef, ModuleType moduleType, String methodName, Range range) {
index.addMethodCall(documentContext.getUri(), mdoRef, moduleType, methodName, range);
}
@@ -273,11 +311,20 @@ private Set calcParams(BSLParser.@Nullable ParamListContext paramList) {
}
}
- @RequiredArgsConstructor
private class VariableSymbolReferenceIndexFinder extends BSLParserBaseVisitor {
private final DocumentContext documentContext;
+ private final ModuleReference.ParsedAccessors parsedAccessors;
+ @SuppressWarnings("NullAway.Init")
private SourceDefinedSymbol currentScope;
+ private final Map variableToCommonModuleMap = new HashMap<>();
+
+ private VariableSymbolReferenceIndexFinder(DocumentContext documentContext) {
+ this.documentContext = documentContext;
+ this.parsedAccessors = ModuleReference.parseAccessors(
+ languageServerConfiguration.getReferencesOptions().getCommonModuleAccessors()
+ );
+ }
@Override
public ParserRuleContext visitModuleVarDeclaration(BSLParser.ModuleVarDeclarationContext ctx) {
@@ -299,6 +346,10 @@ public ParserRuleContext visitModuleVarDeclaration(BSLParser.ModuleVarDeclaratio
public ParserRuleContext visitSub(BSLParser.SubContext ctx) {
currentScope = documentContext.getSymbolTree().getModule();
+ // При входе в новый метод очищаем mappings только для локальных переменных.
+ // Модульные переменные должны сохраняться между методами.
+ clearLocalVariableMappings();
+
if (!Trees.nodeContainsErrors(ctx)) {
documentContext
.getSymbolTree()
@@ -311,6 +362,58 @@ public ParserRuleContext visitSub(BSLParser.SubContext ctx) {
return result;
}
+ /**
+ * Очищает mappings для локальных переменных, сохраняя модульные.
+ */
+ private void clearLocalVariableMappings() {
+ var moduleSymbolTree = documentContext.getSymbolTree();
+ var module = moduleSymbolTree.getModule();
+
+ // Оставляем только те mappings, которые соответствуют модульным переменным
+ variableToCommonModuleMap.keySet().removeIf((String variableKey) -> {
+ // Ищем переменную на уровне модуля
+ var moduleVariable = moduleSymbolTree.getVariableSymbol(variableKey, module);
+ // Если переменной нет на уровне модуля - это локальная переменная, удаляем mapping
+ return moduleVariable.isEmpty();
+ });
+ }
+
+ @Override
+ public ParserRuleContext visitAssignment(BSLParser.AssignmentContext ctx) {
+ // Detect pattern: Variable = ОбщегоНазначения.ОбщийМодуль("ModuleName") or Variable = ОбщийМодуль("ModuleName")
+ var lValue = ctx.lValue();
+ var expression = ctx.expression();
+
+ if (lValue != null && lValue.IDENTIFIER() != null && expression != null) {
+ var variableKey = lValue.IDENTIFIER().getText().toLowerCase(Locale.ENGLISH);
+ if (ModuleReference.isCommonModuleExpression(expression, parsedAccessors)) {
+ var commonModuleOpt = ModuleReference.extractCommonModuleName(expression, parsedAccessors)
+ .flatMap(moduleName -> documentContext.getServerContext()
+ .getConfiguration()
+ .findCommonModule(moduleName));
+ if (commonModuleOpt.isPresent()) {
+ var mdoRef = commonModuleOpt.get().getMdoReference().getMdoRef();
+ variableToCommonModuleMap.put(variableKey, mdoRef);
+
+ index.addModuleReference(
+ documentContext.getUri(),
+ mdoRef,
+ ModuleType.CommonModule,
+ Ranges.create(expression)
+ );
+ } else {
+ // Модуль не найден - удаляем старый mapping если был
+ variableToCommonModuleMap.remove(variableKey);
+ }
+ } else {
+ // Переменная переназначена на что-то другое - очищаем mapping
+ variableToCommonModuleMap.remove(variableKey);
+ }
+ }
+
+ return super.visitAssignment(ctx);
+ }
+
@Override
public ParserRuleContext visitLValue(BSLParser.LValueContext ctx) {
if (ctx.IDENTIFIER() == null) {
@@ -338,6 +441,21 @@ public ParserRuleContext visitCallStatement(BSLParser.CallStatementContext ctx)
}
var variableName = ctx.IDENTIFIER().getText();
+
+ // Check if variable references a common module
+ var commonModuleMdoRef = variableToCommonModuleMap.get(variableName.toLowerCase(Locale.ENGLISH));
+
+ if (commonModuleMdoRef != null) {
+ // Process method calls on the common module variable
+ // Check both modifiers and accessCall
+ if (!ctx.modifier().isEmpty()) {
+ processCommonModuleMethodCalls(ctx.modifier(), commonModuleMdoRef);
+ }
+ if (ctx.accessCall() != null) {
+ processCommonModuleAccessCall(ctx.accessCall(), commonModuleMdoRef);
+ }
+ }
+
findVariableSymbol(variableName)
.ifPresent(s -> addVariableUsage(
s.getRootParent(SymbolKind.Method), variableName, Ranges.create(ctx.IDENTIFIER()), true
@@ -353,6 +471,22 @@ public ParserRuleContext visitComplexIdentifier(BSLParser.ComplexIdentifierConte
}
var variableName = ctx.IDENTIFIER().getText();
+
+ // Check if we are inside a callStatement - if so, skip processing here to avoid duplication
+ var parentCallStatement = Trees.getRootParent(ctx, BSLParser.RULE_callStatement);
+ var isInsideCallStatement = false;
+ if (parentCallStatement instanceof BSLParser.CallStatementContext callStmt) {
+ isInsideCallStatement = callStmt.IDENTIFIER() != null
+ && callStmt.IDENTIFIER().getText().equalsIgnoreCase(variableName);
+ }
+
+ // Check if variable references a common module
+ var commonModuleMdoRef = variableToCommonModuleMap.get(variableName.toLowerCase(Locale.ENGLISH));
+ if (commonModuleMdoRef != null && !ctx.modifier().isEmpty() && !isInsideCallStatement) {
+ // Process method calls on the common module variable
+ processCommonModuleMethodCalls(ctx.modifier(), commonModuleMdoRef);
+ }
+
findVariableSymbol(variableName)
.ifPresent(s -> addVariableUsage(
s.getRootParent(SymbolKind.Method), variableName, Ranges.create(ctx.IDENTIFIER()), true
@@ -450,5 +584,30 @@ private void addVariableUsage(Optional methodSymbol,
!usage
);
}
+
+ private void processCommonModuleMethodCalls(List extends BSLParser.ModifierContext> modifiers, String mdoRef) {
+ for (var modifier : modifiers) {
+ var accessCall = modifier.accessCall();
+ if (accessCall != null) {
+ processCommonModuleAccessCall(accessCall, mdoRef);
+ }
+ }
+ }
+
+ private void processCommonModuleAccessCall(BSLParser.AccessCallContext accessCall, String mdoRef) {
+ var methodCall = accessCall.methodCall();
+ if (methodCall != null && methodCall.methodName() != null) {
+ var methodNameToken = methodCall.methodName().IDENTIFIER();
+ if (methodNameToken != null) {
+ index.addMethodCall(
+ documentContext.getUri(),
+ mdoRef,
+ ModuleType.CommonModule,
+ methodNameToken.getText(),
+ Ranges.create(methodNameToken)
+ );
+ }
+ }
+ }
}
}
diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/utils/ModuleReference.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/utils/ModuleReference.java
new file mode 100644
index 00000000000..16ba543efd0
--- /dev/null
+++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/utils/ModuleReference.java
@@ -0,0 +1,295 @@
+/*
+ * This file is a part of BSL Language Server.
+ *
+ * Copyright (c) 2018-2025
+ * Alexey Sosnoviy , Nikita Fedkin and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * BSL Language Server is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * BSL Language Server is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with BSL Language Server.
+ */
+package com.github._1c_syntax.bsl.languageserver.utils;
+
+import com.github._1c_syntax.bsl.parser.BSLParser;
+import lombok.experimental.UtilityClass;
+import org.antlr.v4.runtime.tree.TerminalNode;
+import org.jspecify.annotations.Nullable;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Утилитный класс для работы со ссылками на общие модули.
+ *
+ * Предоставляет методы для анализа конструкций получения ссылки на общий модуль
+ * через ОбщегоНазначения.ОбщийМодуль("ИмяМодуля"), ОбщегоНазначенияКлиент.ОбщийМодуль("ИмяМодуля")
+ * и других вариантов.
+ */
+@UtilityClass
+public class ModuleReference {
+
+ /**
+ * Предварительно разобранные паттерны доступа к общим модулям.
+ *
+ * Структура:
+ * - localMethods: Set методов для локального вызова (без модуля)
+ * - moduleMethodPairs: Map из имени модуля -> Set методов этого модуля
+ */
+ public record ParsedAccessors(
+ Set localMethods,
+ Map> moduleMethodPairs
+ ) {}
+
+ /**
+ * Разбирает список паттернов доступа к общим модулям один раз.
+ *
+ * Вызывается один раз при инициализации и результат кэшируется.
+ *
+ * @param commonModuleAccessors Список паттернов "Модуль.Метод" или "Метод" для локального вызова
+ * @return Предварительно разобранные паттерны
+ */
+ public static ParsedAccessors parseAccessors(List commonModuleAccessors) {
+ var localMethods = new HashSet();
+ var moduleMethodPairs = new HashMap>();
+
+ for (var pattern : commonModuleAccessors) {
+ var patternLower = pattern.toLowerCase(Locale.ENGLISH);
+
+ if (patternLower.contains(".")) {
+ var parts = patternLower.split("\\.", 2);
+ if (parts.length == 2) {
+ moduleMethodPairs
+ .computeIfAbsent(parts[0], k -> new HashSet<>())
+ .add(parts[1]);
+ }
+ } else {
+ localMethods.add(patternLower);
+ }
+ }
+
+ return new ParsedAccessors(localMethods, moduleMethodPairs);
+ }
+
+ /**
+ * Проверить, является ли expression вызовом получения ссылки на общий модуль.
+ * Использует предварительно разобранные паттерны.
+ *
+ * @param expression Контекст выражения
+ * @param parsedAccessors Предварительно разобранные паттерны
+ * @return true, если это вызов метода получения общего модуля
+ */
+ public static boolean isCommonModuleExpression(
+ BSLParser.ExpressionContext expression,
+ ParsedAccessors parsedAccessors
+ ) {
+
+ var members = expression.member();
+ if (members.isEmpty()) {
+ return false;
+ }
+
+ for (var member : members) {
+ if (isCommonModuleExpressionMember(member, parsedAccessors)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Извлечь имя общего модуля из expression.
+ *
+ * @param expression Контекст выражения
+ * @param parsedAccessors Предварительно разобранные паттерны
+ * @return Имя модуля, если удалось извлечь
+ */
+ public static Optional extractCommonModuleName(
+ BSLParser.ExpressionContext expression,
+ ParsedAccessors parsedAccessors
+ ) {
+
+ var members = expression.member();
+ if (members.isEmpty()) {
+ return Optional.empty();
+ }
+
+ for (var member : members) {
+ var result = extractCommonModuleNameFromMember(member, parsedAccessors);
+ if (result.isPresent()) {
+ return result;
+ }
+ }
+
+ return Optional.empty();
+ }
+
+ // ===== Private helper methods =====
+
+ private static boolean isCommonModuleExpressionMember(
+ BSLParser.MemberContext member,
+ ParsedAccessors parsedAccessors
+ ) {
+ // Случай 1: IDENTIFIER - ОбщийМодуль("Name")
+ if (member.IDENTIFIER() != null) {
+ var identifier = member.IDENTIFIER().getText();
+ if (isLocalMethodMatch(identifier, parsedAccessors)) {
+ return true;
+ }
+ }
+
+ // Случай 2: complexIdentifier с модификаторами
+ var complexId = member.complexIdentifier();
+ if (complexId == null) {
+ return false;
+ }
+
+ var identifier = complexId.IDENTIFIER();
+ if (identifier == null) {
+ return false;
+ }
+
+ var idText = identifier.getText();
+
+ // Случай 2a: Локальный вызов - ОбщийМодуль("Name")
+ if (isLocalMethodMatch(idText, parsedAccessors)) {
+ return true;
+ }
+
+ // Случай 2b: Модуль.Метод - ОбщегоНазначения.ОбщийМодуль("Name")
+ return hasMatchingModifierMethodCall(complexId, idText, parsedAccessors);
+ }
+
+ private static boolean hasMatchingModifierMethodCall(
+ BSLParser.ComplexIdentifierContext complexId,
+ String moduleName,
+ ParsedAccessors parsedAccessors
+ ) {
+ return complexId.modifier().stream()
+ .flatMap(modifier -> extractMethodNameFromModifier(modifier).stream())
+ .anyMatch(methodName -> isModuleMethodMatch(methodName, moduleName, parsedAccessors));
+ }
+
+ private static Optional extractMethodNameFromModifier(BSLParser.ModifierContext modifier) {
+ return Optional.ofNullable(modifier.accessCall())
+ .map(BSLParser.AccessCallContext::methodCall)
+ .map(BSLParser.MethodCallContext::methodName)
+ .map(BSLParser.MethodNameContext::IDENTIFIER)
+ .map(TerminalNode::getText);
+ }
+
+ private static Optional extractCommonModuleNameFromMember(
+ BSLParser.MemberContext member,
+ ParsedAccessors parsedAccessors
+ ) {
+ var complexId = member.complexIdentifier();
+ if (complexId == null) {
+ return Optional.empty();
+ }
+
+ var identifier = complexId.IDENTIFIER();
+ if (identifier != null) {
+ var idText = identifier.getText();
+
+ // Случай 1: Локальный вызов - ОбщийМодуль("Name")
+ if (isLocalMethodMatch(idText, parsedAccessors)) {
+ return extractModuleNameFromModifiers(complexId.modifier());
+ }
+
+ // Случай 2: Модуль.Метод - ОбщегоНазначения.ОбщийМодуль("Name")
+ var result = extractModuleNameFromMatchingModifier(complexId, idText, parsedAccessors);
+ if (result.isPresent()) {
+ return result;
+ }
+ }
+
+ // Случай 3: globalMethodCall внутри complexIdentifier
+ return extractModuleNameFromGlobalMethodCall(complexId, parsedAccessors);
+ }
+
+ private static Optional extractModuleNameFromMatchingModifier(
+ BSLParser.ComplexIdentifierContext complexId,
+ String moduleName,
+ ParsedAccessors parsedAccessors
+ ) {
+ return complexId.modifier().stream()
+ .filter(modifier -> extractMethodNameFromModifier(modifier)
+ .filter(methodName -> isModuleMethodMatch(methodName, moduleName, parsedAccessors))
+ .isPresent())
+ .findFirst()
+ .flatMap(modifier -> Optional.ofNullable(modifier.accessCall())
+ .map(BSLParser.AccessCallContext::methodCall)
+ .map(BSLParser.MethodCallContext::doCall)
+ .flatMap(ModuleReference::extractParameterFromDoCall));
+ }
+
+ private static Optional extractModuleNameFromGlobalMethodCall(
+ BSLParser.ComplexIdentifierContext complexId,
+ ParsedAccessors parsedAccessors
+ ) {
+ var globalMethodCall = complexId.globalMethodCall();
+ if (globalMethodCall == null || globalMethodCall.methodName() == null) {
+ return Optional.empty();
+ }
+
+ var methodName = globalMethodCall.methodName().IDENTIFIER();
+ if (methodName != null && isLocalMethodMatch(methodName.getText(), parsedAccessors)) {
+ return extractParameterFromDoCall(globalMethodCall.doCall());
+ }
+ return Optional.empty();
+ }
+
+ private static boolean isLocalMethodMatch(String methodName, ParsedAccessors parsedAccessors) {
+ return parsedAccessors.localMethods().contains(methodName.toLowerCase(Locale.ENGLISH));
+ }
+
+ private static boolean isModuleMethodMatch(String methodName, String moduleName, ParsedAccessors parsedAccessors) {
+ var moduleMethods = parsedAccessors.moduleMethodPairs().get(moduleName.toLowerCase(Locale.ENGLISH));
+ return moduleMethods != null && moduleMethods.contains(methodName.toLowerCase(Locale.ENGLISH));
+ }
+
+ private static Optional extractModuleNameFromModifiers(
+ List extends BSLParser.ModifierContext> modifiers
+ ) {
+ for (var modifier : modifiers) {
+ var moduleName = extractParameterFromDoCall(modifier.accessCall());
+ if (moduleName.isPresent()) {
+ return moduleName;
+ }
+ }
+ return Optional.empty();
+ }
+
+ private static Optional extractParameterFromDoCall(BSLParser.@Nullable AccessCallContext accessCall) {
+ return Optional.ofNullable(accessCall)
+ .map(BSLParser.AccessCallContext::methodCall)
+ .map(BSLParser.MethodCallContext::doCall)
+ .flatMap(ModuleReference::extractParameterFromDoCall);
+ }
+
+ private static Optional extractParameterFromDoCall(BSLParser.@Nullable DoCallContext doCall) {
+ return Optional.ofNullable(doCall)
+ .map(BSLParser.DoCallContext::callParamList)
+ .map(BSLParser.CallParamListContext::callParam)
+ .filter(params -> !params.isEmpty())
+ .map(params -> params.get(0))
+ .map(BSLParser.CallParamContext::getText)
+ .map(Strings::trimQuotes);
+ }
+}
diff --git a/src/main/resources/com/github/_1c_syntax/bsl/languageserver/configuration/schema.json b/src/main/resources/com/github/_1c_syntax/bsl/languageserver/configuration/schema.json
index a120d11dc7c..10f52879f8d 100644
--- a/src/main/resources/com/github/_1c_syntax/bsl/languageserver/configuration/schema.json
+++ b/src/main/resources/com/github/_1c_syntax/bsl/languageserver/configuration/schema.json
@@ -1121,6 +1121,34 @@
],
"title": "Send errors and exceptions to developers of BSL Language Server.",
"default": "ask"
+ },
+ "references": {
+ "$id": "#/properties/references",
+ "type": "object",
+ "title": "Reference index configuration.",
+ "properties": {
+ "commonModuleAccessors": {
+ "$id": "#/properties/references/commonModuleAccessors",
+ "type": "array",
+ "title": "List of 'Module.Method' patterns for methods returning common module references (e.g. CommonUse.CommonModule(\"ModuleName\")).",
+ "items": {
+ "type": "string"
+ },
+ "default": [
+ "ОбщийМодуль",
+ "CommonModule",
+ "ОбщегоНазначения.ОбщийМодуль",
+ "ОбщегоНазначенияКлиент.ОбщийМодуль",
+ "ОбщегоНазначенияСервер.ОбщийМодуль",
+ "ОбщегоНазначенияКлиентСервер.ОбщийМодуль",
+ "ОбщегоНазначенияПовтИсп.ОбщийМодуль",
+ "CommonUse.CommonModule",
+ "CommonUseClient.CommonModule",
+ "CommonUseServer.CommonModule",
+ "CommonUseClientServer.CommonModule"
+ ]
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/src/main/resources/com/github/_1c_syntax/bsl/languageserver/hover/ModuleSymbolMarkupContentBuilder_en.properties b/src/main/resources/com/github/_1c_syntax/bsl/languageserver/hover/ModuleSymbolMarkupContentBuilder_en.properties
new file mode 100644
index 00000000000..46224d27e3b
--- /dev/null
+++ b/src/main/resources/com/github/_1c_syntax/bsl/languageserver/hover/ModuleSymbolMarkupContentBuilder_en.properties
@@ -0,0 +1,14 @@
+# Availability flags
+availability=Availability
+server=Server
+clientManagedApplication=Client (Managed Application)
+clientOrdinaryApplication=Client (Ordinary Application)
+externalConnection=External Connection
+serverCall=Server Call
+privilegedMode=Privileged Mode
+global=Global
+
+# Return values reuse
+returnValuesReuse=Return Values Reuse
+duringRequest=During Request
+duringSession=During Session
diff --git a/src/main/resources/com/github/_1c_syntax/bsl/languageserver/hover/ModuleSymbolMarkupContentBuilder_ru.properties b/src/main/resources/com/github/_1c_syntax/bsl/languageserver/hover/ModuleSymbolMarkupContentBuilder_ru.properties
new file mode 100644
index 00000000000..97d53be47f4
--- /dev/null
+++ b/src/main/resources/com/github/_1c_syntax/bsl/languageserver/hover/ModuleSymbolMarkupContentBuilder_ru.properties
@@ -0,0 +1,14 @@
+# Availability flags
+availability=Доступность
+server=Сервер
+clientManagedApplication=Клиент (Управляемое приложение)
+clientOrdinaryApplication=Клиент (Обычное приложение)
+externalConnection=Внешнее соединение
+serverCall=Вызов сервера
+privilegedMode=Привилегированный режим
+global=Глобальный
+
+# Return values reuse
+returnValuesReuse=Повторное использование возвращаемых значений
+duringRequest=На время вызова
+duringSession=На время сеанса
diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/hover/ModuleSymbolMarkupContentBuilderTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/hover/ModuleSymbolMarkupContentBuilderTest.java
new file mode 100644
index 00000000000..4a88d20ec63
--- /dev/null
+++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/hover/ModuleSymbolMarkupContentBuilderTest.java
@@ -0,0 +1,135 @@
+/*
+ * This file is a part of BSL Language Server.
+ *
+ * Copyright (c) 2018-2025
+ * Alexey Sosnoviy , Nikita Fedkin and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * BSL Language Server is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * BSL Language Server is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with BSL Language Server.
+ */
+package com.github._1c_syntax.bsl.languageserver.hover;
+
+import com.github._1c_syntax.bsl.languageserver.context.ServerContext;
+import com.github._1c_syntax.bsl.languageserver.util.CleanupContextBeforeClassAndAfterClass;
+import com.github._1c_syntax.bsl.types.ModuleType;
+import jakarta.annotation.PostConstruct;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.nio.file.Paths;
+import java.util.Arrays;
+
+import static com.github._1c_syntax.bsl.languageserver.util.TestUtils.PATH_TO_METADATA;
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest
+@CleanupContextBeforeClassAndAfterClass
+class ModuleSymbolMarkupContentBuilderTest {
+
+ @Autowired
+ private ModuleSymbolMarkupContentBuilder markupContentBuilder;
+
+ @Autowired
+ private ServerContext serverContext;
+
+ @PostConstruct
+ void prepareServerContext() {
+ serverContext.setConfigurationRoot(Paths.get(PATH_TO_METADATA));
+ serverContext.populateContext();
+ }
+
+ @Test
+ void testContentFromCommonModule() {
+ // given
+ var documentContext = serverContext.getDocument("CommonModule.ПервыйОбщийМодуль", ModuleType.CommonModule).orElseThrow();
+ var moduleSymbol = documentContext.getSymbolTree().getModule();
+
+ // when
+ var content = markupContentBuilder.getContent(moduleSymbol).getValue();
+
+ // then
+ assertThat(content).isNotEmpty();
+
+ var blocks = Arrays.asList(content.split("---\n?"));
+
+ // Должны быть: местоположение, информация о модуле
+ assertThat(blocks).hasSizeGreaterThanOrEqualTo(2);
+
+ // Сигнатура - для CommonModule показывается только имя модуля
+ assertThat(blocks.get(0)).contains("ОбщийМодуль.ПервыйОбщийМодуль");
+
+ // Местоположение - используется локализованный mdoRef
+ assertThat(blocks.get(1)).contains("Доступность:");
+ }
+
+ @Test
+ void testContentFromManagerModule() {
+ // given
+ var documentContext = serverContext.getDocument("Catalog.Справочник1", ModuleType.ManagerModule).orElseThrow();
+ var moduleSymbol = documentContext.getSymbolTree().getModule();
+
+ // when
+ var content = markupContentBuilder.getContent(moduleSymbol).getValue();
+
+ // then
+ assertThat(content).isNotEmpty();
+
+ var blocks = Arrays.asList(content.split("---\n?"));
+
+ assertThat(blocks).hasSizeGreaterThanOrEqualTo(1);
+
+ // Для ManagerModule используется локализованный mdoRef
+ assertThat(blocks.get(0)).contains("Справочник.Справочник1");
+ }
+
+ @Test
+ void testContentFromObjectModule() {
+ // given
+ var documentContext = serverContext.getDocument("Catalog.Справочник1", ModuleType.ObjectModule).orElseThrow();
+ var moduleSymbol = documentContext.getSymbolTree().getModule();
+
+ // when
+ var content = markupContentBuilder.getContent(moduleSymbol).getValue();
+
+ // then
+ assertThat(content).isNotEmpty();
+
+ var blocks = Arrays.asList(content.split("---\n?"));
+
+ assertThat(blocks).hasSizeGreaterThanOrEqualTo(1);
+
+ // Для ObjectModule используется локализованный mdoRef
+ assertThat(blocks.get(0)).contains("Справочник.Справочник1");
+ }
+
+ @Test
+ void testCommonModuleWithMetadataInfo() {
+ // given
+ var documentContext = serverContext.getDocument("CommonModule.ПервыйОбщийМодуль", ModuleType.CommonModule).orElseThrow();
+ var moduleSymbol = documentContext.getSymbolTree().getModule();
+
+ // when
+ var content = markupContentBuilder.getContent(moduleSymbol).getValue();
+
+ // then
+ assertThat(content).isNotEmpty();
+
+ // Проверяем, что контент содержит секции с информацией о модуле
+ // (флаги доступности, режим повторного использования)
+ // Конкретные значения зависят от тестовых метаданных
+ assertThat(content).contains("---");
+ }
+}
diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProviderTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProviderTest.java
index d7c3dd7da16..a93b5936117 100644
--- a/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProviderTest.java
+++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProviderTest.java
@@ -49,6 +49,7 @@ class DefinitionProviderTest {
private ServerContext serverContext;
private static final String PATH_TO_FILE = "./src/test/resources/providers/definition.bsl";
+ private static final String PATH_TO_COMMON_MODULE_FILE = "./src/test/resources/providers/definitionCommonModule.bsl";
@PostConstruct
void prepareServerContext() {
@@ -93,7 +94,7 @@ void testDefinitionOfLocalMethod() {
}
@Test
- void testDefinitionOfCommonModule() {
+ void testDefinitionOfManagerModuleMethod() {
var documentContext = TestUtils.getDocumentContextFromFile(PATH_TO_FILE);
var managerModule = serverContext.getDocument("Catalog.Справочник1", ModuleType.ManagerModule).orElseThrow();
var methodSymbol = managerModule.getSymbolTree().getMethodSymbol("ТестЭкспортная").orElseThrow();
@@ -114,4 +115,31 @@ void testDefinitionOfCommonModule() {
assertThat(definition.getTargetRange()).isEqualTo(methodSymbol.getRange());
assertThat(definition.getOriginSelectionRange()).isEqualTo(Ranges.create(6, 24, 38));
}
+
+ @Test
+ void testDefinitionOfCommonModuleName() {
+ // Тест: клик на "ПервыйОбщийМодуль" в "ПервыйОбщийМодуль.НеУстаревшаяПроцедура()"
+ // должен вести к модулю ПервыйОбщийМодуль
+ var documentContext = TestUtils.getDocumentContextFromFile(PATH_TO_COMMON_MODULE_FILE);
+ var commonModule = serverContext.getDocument("CommonModule.ПервыйОбщийМодуль", ModuleType.CommonModule).orElseThrow();
+ var moduleSymbol = commonModule.getSymbolTree().getModule();
+
+ var params = new DefinitionParams();
+ // Position on "ПервыйОбщийМодуль" (line 2, columns 0-17)
+ params.setPosition(new Position(1, 5));
+
+ // when
+ var definitions = definitionProvider.getDefinition(documentContext, params);
+
+ // then
+ assertThat(definitions).hasSize(1);
+
+ var definition = definitions.get(0);
+
+ assertThat(definition.getTargetUri()).isEqualTo(commonModule.getUri().toString());
+ assertThat(definition.getTargetSelectionRange()).isEqualTo(moduleSymbol.getSelectionRange());
+ assertThat(definition.getTargetRange()).isEqualTo(moduleSymbol.getRange());
+ // "ПервыйОбщийМодуль" spans 17 characters
+ assertThat(definition.getOriginSelectionRange()).isEqualTo(Ranges.create(1, 0, 17));
+ }
}
diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndexFillerTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndexFillerTest.java
index 6edac53faa0..7d9c3dbda48 100644
--- a/src/test/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndexFillerTest.java
+++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndexFillerTest.java
@@ -288,6 +288,173 @@ void testRebuildClearReferences() {
assertThat(referencesTo).hasSize(1);
}
+ @Test
+ void testFindCommonModuleVariableReferences() throws IOException {
+ var path = Absolute.path("src/test/resources/metadata/designer");
+ serverContext.setConfigurationRoot(path);
+
+ var documentContext = TestUtils.getDocumentContextFromFile(
+ "./src/test/resources/references/ReferenceIndexCommonModuleVariable.bsl"
+ );
+
+ // Load the common module that will be referenced
+ var file = new File("src/test/resources/metadata/designer",
+ "CommonModules/ПервыйОбщийМодуль/Ext/Module.bsl");
+ var uri = Absolute.uri(file);
+ var commonModuleContext = TestUtils.getDocumentContext(
+ uri,
+ FileUtils.readFileToString(file, StandardCharsets.UTF_8),
+ serverContext
+ );
+
+ referenceIndexFiller.fill(documentContext);
+
+ // Check that exported methods from common module are referenced
+ var procMethod = commonModuleContext.getSymbolTree().getMethodSymbol("НеУстаревшаяПроцедура");
+ assertThat(procMethod).isPresent();
+ var referencesToProc = referenceIndex.getReferencesTo(procMethod.get());
+ // Filter to only references from our test document
+ var referencesToProcFromTest = referencesToProc.stream()
+ .filter(ref -> ref.getUri().equals(documentContext.getUri()))
+ .toList();
+ assertThat(referencesToProcFromTest).hasSize(1);
+
+ var funcMethod = commonModuleContext.getSymbolTree().getMethodSymbol("НеУстаревшаяФункция");
+ assertThat(funcMethod).isPresent();
+ var referencesToFunc = referenceIndex.getReferencesTo(funcMethod.get());
+ // Filter to only references from our test document
+ var referencesToFuncFromTest = referencesToFunc.stream()
+ .filter(ref -> ref.getUri().equals(documentContext.getUri()))
+ .toList();
+ // Должно быть 2 вызова: в assignment и в условии
+ assertThat(referencesToFuncFromTest).hasSize(2);
+ }
+
+ @Test
+ void testCommonModuleVariableReassignment() throws IOException {
+ var path = Absolute.path("src/test/resources/metadata/designer");
+ serverContext.setConfigurationRoot(path);
+
+ var documentContext = TestUtils.getDocumentContextFromFile(
+ "./src/test/resources/references/ReferenceIndexCommonModuleReassignment.bsl"
+ );
+
+ // Load the common module that will be referenced
+ var file = new File("src/test/resources/metadata/designer",
+ "CommonModules/ПервыйОбщийМодуль/Ext/Module.bsl");
+ var uri = Absolute.uri(file);
+ var commonModuleContext = TestUtils.getDocumentContext(
+ uri,
+ FileUtils.readFileToString(file, StandardCharsets.UTF_8),
+ serverContext
+ );
+
+ referenceIndexFiller.fill(documentContext);
+
+ // В первой процедуре должна быть только одна ссылка на НеУстаревшаяПроцедура
+ // (до переназначения переменной на Неопределено)
+ var procMethod = commonModuleContext.getSymbolTree().getMethodSymbol("НеУстаревшаяПроцедура");
+ assertThat(procMethod).isPresent();
+ var referencesToProc = referenceIndex.getReferencesTo(procMethod.get());
+ var referencesToProcFromTest = referencesToProc.stream()
+ .filter(ref -> ref.getUri().equals(documentContext.getUri()))
+ .toList();
+ // Должно быть 2 вызова: по одному из каждой процедуры (до переназначения)
+ assertThat(referencesToProcFromTest).hasSize(2);
+
+ // НеУстаревшаяФункция не должна индексироваться после переназначения на Неопределено
+ var funcMethod = commonModuleContext.getSymbolTree().getMethodSymbol("НеУстаревшаяФункция");
+ assertThat(funcMethod).isPresent();
+ var referencesToFunc = referenceIndex.getReferencesTo(funcMethod.get());
+ var referencesToFuncFromTest = referencesToFunc.stream()
+ .filter(ref -> ref.getUri().equals(documentContext.getUri()))
+ .toList();
+ // Не должно быть ссылок, так как вызов после переназначения на Неопределено
+ assertThat(referencesToFuncFromTest).hasSize(0);
+ }
+
+ @Test
+ void testCommonModuleModuleLevelVariable() throws IOException {
+ var path = Absolute.path("src/test/resources/metadata/designer");
+ serverContext.setConfigurationRoot(path);
+
+ var documentContext = TestUtils.getDocumentContextFromFile(
+ "./src/test/resources/references/ReferenceIndexCommonModuleLevel.bsl"
+ );
+
+ // Load the common module that will be referenced
+ var file = new File("src/test/resources/metadata/designer",
+ "CommonModules/ПервыйОбщийМодуль/Ext/Module.bsl");
+ var uri = Absolute.uri(file);
+ var commonModuleContext = TestUtils.getDocumentContext(
+ uri,
+ FileUtils.readFileToString(file, StandardCharsets.UTF_8),
+ serverContext
+ );
+
+ referenceIndexFiller.fill(documentContext);
+
+ // Модульная переменная МодульУровняМодуля используется в двух процедурах
+ var procMethod = commonModuleContext.getSymbolTree().getMethodSymbol("НеУстаревшаяПроцедура");
+ assertThat(procMethod).isPresent();
+ var referencesToProc = referenceIndex.getReferencesTo(procMethod.get());
+ var referencesToProcFromTest = referencesToProc.stream()
+ .filter(ref -> ref.getUri().equals(documentContext.getUri()))
+ .toList();
+ // Должно быть 2 вызова: из ПерваяПроцедура и из ТретьяПроцедура
+ assertThat(referencesToProcFromTest).hasSize(2);
+
+ var funcMethod = commonModuleContext.getSymbolTree().getMethodSymbol("НеУстаревшаяФункция");
+ assertThat(funcMethod).isPresent();
+ var referencesToFunc = referenceIndex.getReferencesTo(funcMethod.get());
+ var referencesToFuncFromTest = referencesToFunc.stream()
+ .filter(ref -> ref.getUri().equals(documentContext.getUri()))
+ .toList();
+ // Должна быть 1 ссылка из ВтораяПроцедура (модульная переменная)
+ assertThat(referencesToFuncFromTest).hasSize(1);
+ }
+
+ @Test
+ void testCommonModuleVariableIsolationBetweenMethods() throws IOException {
+ var path = Absolute.path("src/test/resources/metadata/designer");
+ serverContext.setConfigurationRoot(path);
+
+ var documentContext = TestUtils.getDocumentContextFromFile(
+ "./src/test/resources/references/ReferenceIndexCommonModuleIsolation.bsl"
+ );
+
+ // Load the common module that will be referenced
+ var file = new File("src/test/resources/metadata/designer",
+ "CommonModules/ПервыйОбщийМодуль/Ext/Module.bsl");
+ var uri = Absolute.uri(file);
+ var commonModuleContext = TestUtils.getDocumentContext(
+ uri,
+ FileUtils.readFileToString(file, StandardCharsets.UTF_8),
+ serverContext
+ );
+
+ referenceIndexFiller.fill(documentContext);
+
+ // В первой процедуре должна быть ссылка на НеУстаревшаяПроцедура
+ var procMethod = commonModuleContext.getSymbolTree().getMethodSymbol("НеУстаревшаяПроцедура");
+ assertThat(procMethod).isPresent();
+ var referencesToProc = referenceIndex.getReferencesTo(procMethod.get());
+ var referencesToProcFromTest = referencesToProc.stream()
+ .filter(ref -> ref.getUri().equals(documentContext.getUri()))
+ .toList();
+ assertThat(referencesToProcFromTest).hasSize(1);
+
+ // Во второй процедуре НЕ должно быть ссылки на НеУстаревшаяФункция
+ // так как переменная Модуль там имеет другое значение (Неопределено)
+ var funcMethod = commonModuleContext.getSymbolTree().getMethodSymbol("НеУстаревшаяФункция");
+ assertThat(funcMethod).isPresent();
+ var referencesToFunc = referenceIndex.getReferencesTo(funcMethod.get());
+ var referencesToFuncFromTest = referencesToFunc.stream()
+ .filter(ref -> ref.getUri().equals(documentContext.getUri()))
+ .toList();
+ assertThat(referencesToFuncFromTest).hasSize(0);
+ }
+
@Test
void testHandleServerContextDocumentRemovedEvent() {
// given
diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndexTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndexTest.java
index 217cec0fe37..2ce77da2692 100644
--- a/src/test/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndexTest.java
+++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndexTest.java
@@ -257,7 +257,8 @@ void testCantGetReferenceToNonExportCommonModuleMethod() {
var documentContext = TestUtils.getDocumentContextFromFile(PATH_TO_FILE);
var uri = documentContext.getUri();
- var position = new Position(4, 10);
+ // Position on "Тест" method name (non-export method)
+ var position = new Position(4, 24);
// when
var reference = referenceIndex.getReference(uri, position);
@@ -274,24 +275,35 @@ void testGetReferencesFromLocalMethodSymbol() {
var commonModuleContext = serverContext.getDocument("CommonModule.ПервыйОбщийМодуль", ModuleType.CommonModule).orElseThrow();
var commonModuleMethodSymbol = commonModuleContext.getSymbolTree().getMethodSymbol("УстаревшаяПроцедура").orElseThrow();
+ var commonModuleSymbol = commonModuleContext.getSymbolTree().getModule();
var managerModuleContext = serverContext.getDocument("InformationRegister.РегистрСведений1", ModuleType.ManagerModule).orElseThrow();
var managerModuleMethodSymbol = managerModuleContext.getSymbolTree().getMethodSymbol("УстаревшаяПроцедура").orElseThrow();
var uri = documentContext.getUri().toString();
var locationLocal = new Location(uri, Ranges.create(1, 4, 16));
+ var locationCommonModuleName1 = new Location(uri, Ranges.create(2, 4, 21)); // ПервыйОбщийМодуль on line 3
var locationCommonModule = new Location(uri, Ranges.create(2, 22, 41));
var locationManagerModule = new Location(uri, Ranges.create(3, 38, 57));
+ var locationCommonModuleName2 = new Location(uri, Ranges.create(4, 4, 21)); // ПервыйОбщийМодуль on line 5
// when
var references = referenceIndex.getReferencesFrom(localMethodSymbol);
// then
+ // 5 references from ReferenceIndex.bsl:
+ // - line 2: local method call ИмяПроцедуры()
+ // - line 3: module name ПервыйОбщийМодуль
+ // - line 3: method УстаревшаяПроцедура in common module
+ // - line 4: method УстаревшаяПроцедура in manager module
+ // - line 5: module name ПервыйОбщийМодуль
assertThat(references)
- .hasSize(3)
+ .hasSize(5)
.contains(Reference.of(localMethodSymbol, localMethodSymbol, locationLocal))
+ .contains(Reference.of(localMethodSymbol, commonModuleSymbol, locationCommonModuleName1))
.contains(Reference.of(localMethodSymbol, commonModuleMethodSymbol, locationCommonModule))
.contains(Reference.of(localMethodSymbol, managerModuleMethodSymbol, locationManagerModule))
+ .contains(Reference.of(localMethodSymbol, commonModuleSymbol, locationCommonModuleName2))
;
}
diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/utils/ModuleReferenceTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/utils/ModuleReferenceTest.java
new file mode 100644
index 00000000000..de2c57550fa
--- /dev/null
+++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/utils/ModuleReferenceTest.java
@@ -0,0 +1,120 @@
+/*
+ * This file is a part of BSL Language Server.
+ *
+ * Copyright (c) 2018-2025
+ * Alexey Sosnoviy , Nikita Fedkin and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * BSL Language Server is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * BSL Language Server is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with BSL Language Server.
+ */
+package com.github._1c_syntax.bsl.languageserver.utils;
+
+import com.github._1c_syntax.bsl.languageserver.configuration.references.ReferencesOptions;
+import com.github._1c_syntax.bsl.languageserver.util.CleanupContextBeforeClassAndAfterEachTestMethod;
+import com.github._1c_syntax.bsl.languageserver.util.TestUtils;
+import com.github._1c_syntax.bsl.parser.BSLParser;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest
+@CleanupContextBeforeClassAndAfterEachTestMethod
+class ModuleReferenceTest {
+
+ private static final ModuleReference.ParsedAccessors DEFAULT_ACCESSORS =
+ ModuleReference.parseAccessors(new ReferencesOptions().getCommonModuleAccessors());
+
+ @Test
+ void testDetectCommonModuleExpression() {
+ var code = """
+ Процедура Тест()
+ Модуль = ОбщегоНазначения.ОбщийМодуль("ПервыйОбщийМодуль");
+ КонецПроцедуры""";
+
+ var documentContext = TestUtils.getDocumentContext(code);
+ var ast = documentContext.getAst();
+
+ // Find assignment
+ var assignments = new ArrayList();
+ Trees.findAllRuleNodes(ast, BSLParser.RULE_assignment).forEach(node ->
+ assignments.add((BSLParser.AssignmentContext) node)
+ );
+
+ assertThat(assignments).hasSize(1);
+
+ var expression = assignments.get(0).expression();
+ assertThat(ModuleReference.isCommonModuleExpression(expression, DEFAULT_ACCESSORS)).isTrue();
+
+ var moduleName = ModuleReference.extractCommonModuleName(expression, DEFAULT_ACCESSORS);
+ assertThat(moduleName).isPresent();
+ assertThat(moduleName.get()).isEqualTo("ПервыйОбщийМодуль");
+ }
+
+ @Test
+ void testCustomAccessor() {
+ var code = """
+ Процедура Тест()
+ Модуль = МойМодуль.ПолучитьОбщийМодуль("ТестовыйМодуль");
+ КонецПроцедуры""";
+
+ var documentContext = TestUtils.getDocumentContext(code);
+ var ast = documentContext.getAst();
+
+ var assignments = new ArrayList();
+ Trees.findAllRuleNodes(ast, BSLParser.RULE_assignment).forEach(node ->
+ assignments.add((BSLParser.AssignmentContext) node)
+ );
+
+ assertThat(assignments).hasSize(1);
+
+ var expression = assignments.get(0).expression();
+
+ // With default accessors - should not match
+ assertThat(ModuleReference.isCommonModuleExpression(expression, DEFAULT_ACCESSORS)).isFalse();
+
+ // With custom accessor - should match
+ var customAccessors = ModuleReference.parseAccessors(List.of("МойМодуль.ПолучитьОбщийМодуль"));
+ assertThat(ModuleReference.isCommonModuleExpression(expression, customAccessors)).isTrue();
+
+ var moduleName = ModuleReference.extractCommonModuleName(expression, customAccessors);
+ assertThat(moduleName).isPresent();
+ assertThat(moduleName.get()).isEqualTo("ТестовыйМодуль");
+ }
+
+ @Test
+ void testParseAccessors() {
+ var accessors = List.of(
+ "ОбщийМодуль",
+ "CommonModule",
+ "ОбщегоНазначения.ОбщийМодуль",
+ "Common.CommonModule"
+ );
+
+ var parsed = ModuleReference.parseAccessors(accessors);
+
+ // Проверяем локальные методы
+ assertThat(parsed.localMethods()).containsExactlyInAnyOrder("общиймодуль", "commonmodule");
+
+ // Проверяем пары модуль.метод
+ assertThat(parsed.moduleMethodPairs()).containsKey("общегоназначения");
+ assertThat(parsed.moduleMethodPairs().get("общегоназначения")).contains("общиймодуль");
+ assertThat(parsed.moduleMethodPairs()).containsKey("common");
+ assertThat(parsed.moduleMethodPairs().get("common")).contains("commonmodule");
+ }
+}
diff --git a/src/test/resources/providers/definitionCommonModule.bsl b/src/test/resources/providers/definitionCommonModule.bsl
new file mode 100644
index 00000000000..c6d722801b5
--- /dev/null
+++ b/src/test/resources/providers/definitionCommonModule.bsl
@@ -0,0 +1,2 @@
+// Тест для definition общего модуля
+ПервыйОбщийМодуль.НеУстаревшаяПроцедура();
diff --git a/src/test/resources/references/ReferenceIndexCommonModuleIsolation.bsl b/src/test/resources/references/ReferenceIndexCommonModuleIsolation.bsl
new file mode 100644
index 00000000000..87e3b40262d
--- /dev/null
+++ b/src/test/resources/references/ReferenceIndexCommonModuleIsolation.bsl
@@ -0,0 +1,22 @@
+// Тест для проверки изоляции mappings между методами
+
+Процедура ПерваяПроцедура()
+
+ // В первой процедуре переменная ссылается на общий модуль
+ Модуль = ОбщегоНазначения.ОбщийМодуль("ПервыйОбщийМодуль");
+ Модуль.НеУстаревшаяПроцедура(); // Должно индексироваться
+
+КонецПроцедуры
+
+Процедура ВтораяПроцедура()
+
+ // Во второй процедуре переменная с тем же именем используется для чего-то другого
+ Модуль = Неопределено;
+
+ // Этот вызов НЕ должен индексироваться как вызов общего модуля
+ Попытка
+ Модуль.НеУстаревшаяФункция();
+ Исключение
+ КонецПопытки;
+
+КонецПроцедуры
diff --git a/src/test/resources/references/ReferenceIndexCommonModuleLevel.bsl b/src/test/resources/references/ReferenceIndexCommonModuleLevel.bsl
new file mode 100644
index 00000000000..a0c34391a44
--- /dev/null
+++ b/src/test/resources/references/ReferenceIndexCommonModuleLevel.bsl
@@ -0,0 +1,40 @@
+// Тест для проверки работы с модульными переменными
+
+Перем МодульУровняМодуля; // Модульная переменная
+
+Процедура ПерваяПроцедура()
+
+ // Устанавливаем модульную переменную в первой процедуре
+ МодульУровняМодуля = ОбщегоНазначения.ОбщийМодуль("ПервыйОбщийМодуль");
+ МодульУровняМодуля.НеУстаревшаяПроцедура(); // Должно индексироваться
+
+КонецПроцедуры
+
+Процедура ВтораяПроцедура()
+
+ // Используем ту же модульную переменную во второй процедуре
+ // Mapping должен сохраниться из первой процедуры
+ МодульУровняМодуля.НеУстаревшаяФункция(); // Должно индексироваться
+
+КонецПроцедуры
+
+Процедура ТретьяПроцедура()
+
+ // Локальная переменная с тем же именем что и в другой процедуре
+ Локальная = ОбщегоНазначения.ОбщийМодуль("ПервыйОбщийМодуль");
+ Локальная.НеУстаревшаяПроцедура(); // Должно индексироваться
+
+КонецПроцедуры
+
+Процедура ЧетвертаяПроцедура()
+
+ // Локальная переменная с тем же именем - не должна иметь mapping из третьей процедуры
+ Локальная = Неопределено;
+
+ // Этот вызов НЕ должен индексироваться
+ Попытка
+ Локальная.НеУстаревшаяПроцедура();
+ Исключение
+ КонецПопытки;
+
+КонецПроцедуры
diff --git a/src/test/resources/references/ReferenceIndexCommonModuleReassignment.bsl b/src/test/resources/references/ReferenceIndexCommonModuleReassignment.bsl
new file mode 100644
index 00000000000..5fe20160950
--- /dev/null
+++ b/src/test/resources/references/ReferenceIndexCommonModuleReassignment.bsl
@@ -0,0 +1,31 @@
+// Тест для проверки очистки mapping при переназначении переменной
+
+Процедура ТестПереназначения()
+
+ // Сначала присваиваем ссылку на общий модуль
+ МодульДоступа = ОбщегоНазначения.ОбщийМодуль("ПервыйОбщийМодуль");
+ МодульДоступа.НеУстаревшаяПроцедура(); // Должно быть найдено
+
+ // Теперь переназначаем переменную на обычное значение
+ МодульДоступа = Неопределено;
+
+ // После переназначения эти вызовы НЕ должны индексироваться как вызовы общего модуля
+ // (это будут вызовы на неопределённом значении, что является ошибкой, но это не наша проблема)
+ Попытка
+ МодульДоступа.НеУстаревшаяФункция(); // НЕ должно индексироваться как вызов общего модуля
+ Исключение
+ КонецПопытки;
+
+КонецПроцедуры
+
+Процедура ТестПереназначенияНаДругойМодуль()
+
+ // Присваиваем один модуль
+ Модуль = ОбщегоНазначения.ОбщийМодуль("ПервыйОбщийМодуль");
+ Модуль.НеУстаревшаяПроцедура(); // Должно индексироваться
+
+ // Переназначаем на другой модуль
+ Модуль = ОбщегоНазначения.ОбщийМодуль("ГлобальныйСерверныйМодуль");
+ // После переназначения старый mapping должен быть очищен
+
+КонецПроцедуры
diff --git a/src/test/resources/references/ReferenceIndexCommonModuleVariable.bsl b/src/test/resources/references/ReferenceIndexCommonModuleVariable.bsl
new file mode 100644
index 00000000000..81f2c546fd5
--- /dev/null
+++ b/src/test/resources/references/ReferenceIndexCommonModuleVariable.bsl
@@ -0,0 +1,18 @@
+// Тест для поддержки поиска ссылок на метод общего модуля,
+// полученный через ОбщегоНазначения.ОбщийМодуль
+
+Процедура Тест()
+
+ // Паттерн 1: Прямое обращение через ОбщегоНазначения.ОбщийМодуль
+ МодульУправлениеДоступом = ОбщегоНазначения.ОбщийМодуль("ПервыйОбщийМодуль");
+ МодульУправлениеДоступом.НеУстаревшаяПроцедура();
+
+ // Паттерн 2: Вызов функции общего модуля через переменную
+ Результат = МодульУправлениеДоступом.НеУстаревшаяФункция();
+
+ // Паттерн 3: Вызов функции в условии
+ Если МодульУправлениеДоступом.НеУстаревшаяФункция() Тогда
+ // что-то делаем
+ КонецЕсли;
+
+КонецПроцедуры