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 4ee7805a8c8..8a75e0345d1 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 @@ -198,6 +198,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); + } + /** * Добавить обращение к переменной в индекс. * @@ -269,6 +298,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 afb3de3d663..974b3069eb6 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 @@ -25,6 +25,7 @@ import com.github._1c_syntax.bsl.languageserver.context.events.DocumentContextContentChangedEvent; import com.github._1c_syntax.bsl.languageserver.context.symbol.SourceDefinedSymbol; import com.github._1c_syntax.bsl.languageserver.context.symbol.VariableSymbol; +import com.github._1c_syntax.bsl.languageserver.utils.CommonModuleReference; 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.Modules; @@ -32,6 +33,7 @@ import com.github._1c_syntax.bsl.languageserver.utils.Ranges; import com.github._1c_syntax.bsl.languageserver.utils.Strings; import com.github._1c_syntax.bsl.languageserver.utils.Trees; +import com.github._1c_syntax.bsl.mdclasses.CF; import com.github._1c_syntax.bsl.parser.BSLParser; import com.github._1c_syntax.bsl.parser.BSLParserBaseVisitor; import com.github._1c_syntax.bsl.parser.BSLParserRuleContext; @@ -47,7 +49,10 @@ 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; @@ -263,6 +268,7 @@ private class VariableSymbolReferenceIndexFinder extends BSLParserBaseVisitor variableToCommonModuleMap = new HashMap<>(); @Override public BSLParserRuleContext visitModuleVarDeclaration(BSLParser.ModuleVarDeclarationContext ctx) { @@ -283,6 +289,10 @@ public BSLParserRuleContext visitModuleVarDeclaration(BSLParser.ModuleVarDeclara @Override public BSLParserRuleContext visitSub(BSLParser.SubContext ctx) { currentScope = documentContext.getSymbolTree().getModule(); + + // При входе в новый метод очищаем mappings только для локальных переменных. + // Модульные переменные должны сохраняться между методами. + clearLocalVariableMappings(); if (!Trees.nodeContainsErrors(ctx)) { documentContext @@ -296,6 +306,58 @@ public BSLParserRuleContext visitSub(BSLParser.SubContext ctx) { return result; } + /** + * Очищает mappings для локальных переменных, сохраняя модульные. + */ + private void clearLocalVariableMappings() { + var moduleSymbolTree = documentContext.getSymbolTree(); + var module = moduleSymbolTree.getModule(); + + // Оставляем только те mappings, которые соответствуют модульным переменным + variableToCommonModuleMap.keySet().removeIf(variableKey -> { + // Ищем переменную на уровне модуля + var moduleVariable = moduleSymbolTree.getVariableSymbol(variableKey, module); + // Если переменной нет на уровне модуля - это локальная переменная, удаляем mapping + return moduleVariable.isEmpty(); + }); + } + + @Override + public BSLParserRuleContext 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 (CommonModuleReference.isCommonModuleExpression(expression)) { + var commonModuleOpt = CommonModuleReference.extractCommonModuleName(expression) + .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 BSLParserRuleContext visitLValue(BSLParser.LValueContext ctx) { if (ctx.IDENTIFIER() == null) { @@ -323,6 +385,21 @@ public BSLParserRuleContext visitCallStatement(BSLParser.CallStatementContext ct } 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 @@ -338,6 +415,27 @@ public BSLParserRuleContext visitComplexIdentifier(BSLParser.ComplexIdentifierCo } 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().equals(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); + } + + // Check if this is a manager module reference (e.g., Справочники.ИмяСправочника.Метод()) + if (!isInsideCallStatement) { + processManagerModuleReference(ctx); + } + findVariableSymbol(variableName) .ifPresent(s -> addVariableUsage( s.getRootParent(SymbolKind.Method), variableName, Ranges.create(ctx.IDENTIFIER()), true @@ -435,5 +533,137 @@ private void addVariableUsage(Optional methodSymbol, !usage ); } + + private void processCommonModuleMethodCalls(List 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) + ); + } + } + } + + /** + * Обрабатывает обращение к модулю менеджера (например, Справочники.ИмяСправочника.Метод()). + * Находит и индексирует вызовы методов менеджера объектов метаданных. + */ + private void processManagerModuleReference(BSLParser.ComplexIdentifierContext ctx) { + // Проверяем, является ли это обращением к модулю менеджера + var parentCtx = Trees.getRootParent(ctx, BSLParser.RULE_expression); + if (!(parentCtx instanceof BSLParser.ExpressionContext expressionCtx)) { + return; + } + + var managerInfo = CommonModuleReference.extractManagerModuleInfo(expressionCtx); + if (managerInfo.isEmpty()) { + return; + } + + var info = managerInfo.get(); + var configuration = documentContext.getServerContext().getConfiguration(); + + // Ищем объект метаданных по типу менеджера и имени + var mdoRef = findManagerModuleMdoRef(configuration, info.managerType(), info.objectName()); + if (mdoRef.isEmpty()) { + return; + } + + // Обрабатываем вызовы методов на модуле менеджера + // Модификаторы после первого (имени объекта) могут содержать вызовы методов + var modifiers = ctx.modifier(); + if (modifiers.size() > 1) { + // Пропускаем первый модификатор (имя объекта), обрабатываем остальные + for (int i = 1; i < modifiers.size(); i++) { + var modifier = modifiers.get(i); + var accessCall = modifier.accessCall(); + if (accessCall != null) { + processManagerModuleAccessCall(accessCall, mdoRef.get()); + } + } + } + } + + /** + * Находит mdoRef для модуля менеджера по типу и имени объекта. + */ + private Optional findManagerModuleMdoRef( + CF configuration, + String managerType, + String objectName + ) { + // Преобразуем тип менеджера в тип объекта метаданных + var mdoTypeName = mapManagerTypeToMdoType(managerType); + if (mdoTypeName.isEmpty()) { + return Optional.empty(); + } + + // Формируем mdoRef в формате "ТипОбъекта.ИмяОбъекта" + var mdoRef = mdoTypeName.get() + "." + objectName; + + // Проверяем, что такой объект существует в конфигурации + var child = configuration.findChild(mdoRef); + if (child.isPresent()) { + return Optional.of(mdoRef); + } + + return Optional.empty(); + } + + /** + * Преобразует тип менеджера в тип объекта метаданных. + */ + private Optional mapManagerTypeToMdoType(String managerType) { + var lowerType = managerType.toLowerCase(Locale.ENGLISH); + return switch (lowerType) { + case "справочники", "catalogs" -> Optional.of("Catalog"); + case "документы", "documents" -> Optional.of("Document"); + case "регистрысведений", "informationregisters" -> Optional.of("InformationRegister"); + case "регистрынакопления", "accumulationregisters" -> Optional.of("AccumulationRegister"); + case "регистрыбухгалтерии", "accountingregisters" -> Optional.of("AccountingRegister"); + case "регистрырасчета", "calculationregisters" -> Optional.of("CalculationRegister"); + case "планывидовхарактеристик", "chartsofcharacteristictypes" -> Optional.of("ChartOfCharacteristicTypes"); + case "планысчетов", "chartsofaccounts" -> Optional.of("ChartOfAccounts"); + case "планывидоврасчета", "chartsofcalculationtypes" -> Optional.of("ChartOfCalculationTypes"); + case "планыобмена", "exchangeplans" -> Optional.of("ExchangePlan"); + case "бизнеспроцессы", "businessprocesses" -> Optional.of("BusinessProcess"); + case "задачи", "tasks" -> Optional.of("Task"); + default -> Optional.empty(); + }; + } + + /** + * Обрабатывает вызов метода на модуле менеджера. + */ + private void processManagerModuleAccessCall(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.ManagerModule, + methodNameToken.getText(), + Ranges.create(methodNameToken) + ); + } + } + } } } diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/utils/CommonModuleReference.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/utils/CommonModuleReference.java new file mode 100644 index 00000000000..82e8b36ff96 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/utils/CommonModuleReference.java @@ -0,0 +1,344 @@ +/* + * 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 com.github._1c_syntax.utils.CaseInsensitivePattern; +import lombok.experimental.UtilityClass; + +import java.util.Optional; +import java.util.regex.Pattern; + +/** + * Утилитный класс для работы с вызовами ОбщийМодуль и ссылками на модули менеджеров. + *

+ * Предоставляет методы для анализа конструкций получения ссылки на общий модуль + * через ОбщегоНазначения.ОбщийМодуль("ИмяМодуля"), ОбщегоНазначенияКлиент.ОбщийМодуль("ИмяМодуля") + * и других вариантов, а также для работы с модулями менеджеров (Справочники.Имя, Документы.Имя и т.д.) + */ +@UtilityClass +public class CommonModuleReference { + + private static final Pattern COMMON_MODULE_METHOD = CaseInsensitivePattern.compile( + "^(ОбщийМодуль|CommonModule)$"); + + // Поддержка различных вариантов общих модулей для получения ссылок + private static final Pattern COMMON_USE_MODULE = CaseInsensitivePattern.compile( + "^(ОбщегоНазначения|ОбщегоНазначенияКлиент|ОбщегоНазначенияСервер|" + + "ОбщегоНазначенияКлиентСервер|ОбщегоНазначенияПовтИсп|" + + "CommonUse|CommonUseClient|CommonUseServer|CommonUseClientServer)$"); + + // Паттерн для модулей менеджеров (Справочники, Документы, РегистрыСведений и т.д.) + private static final Pattern MANAGER_MODULE_TYPES = CaseInsensitivePattern.compile( + "^(Справочники|Документы|РегистрыСведений|РегистрыНакопления|РегистрыБухгалтерии|" + + "РегистрыРасчета|ПланыВидовХарактеристик|ПланыСчетов|ПланыВидовРасчета|" + + "ПланыОбмена|БизнесПроцессы|Задачи|ПланыОбмена|" + + "Catalogs|Documents|InformationRegisters|AccumulationRegisters|AccountingRegisters|" + + "CalculationRegisters|ChartsOfCharacteristicTypes|ChartsOfAccounts|ChartsOfCalculationTypes|" + + "ExchangePlans|BusinessProcesses|Tasks)$"); + + /** + * Проверить, является ли expression вызовом получения ссылки на общий модуль. + * Распознает паттерны: + * - ОбщегоНазначения.ОбщийМодуль("ИмяМодуля") + * - ОбщегоНазначенияКлиент.ОбщийМодуль("ИмяМодуля") + * - ОбщийМодуль("ИмяМодуля") + * И другие варианты общих модулей + * + * @param expression Контекст выражения + * @return true, если это вызов ОбщийМодуль + */ + public static boolean isCommonModuleExpression(BSLParser.ExpressionContext expression) { + if (expression == null) { + return false; + } + + var members = expression.member(); + if (members.isEmpty()) { + return false; + } + + // В выражении могут быть один или несколько members + // Для простого вызова ОбщийМодуль("Name") - один member с globalMethodCall + // Для ОбщегоНазначения.ОбщийМодуль("Name") - один member с complexIdentifier и modifier + + for (var member : members) { + // Случай 1: globalMethodCall - ОбщийМодуль("Name") + if (member.IDENTIFIER() != null) { + var identifier = member.IDENTIFIER().getText(); + if (COMMON_MODULE_METHOD.matcher(identifier).matches()) { + return true; + } + } + + // Случай 2: complexIdentifier с модификаторами + var complexId = member.complexIdentifier(); + if (complexId != null) { + // Проверяем базовый идентификатор + var identifier = complexId.IDENTIFIER(); + if (identifier != null) { + var idText = identifier.getText(); + + // Случай 2a: ОбщийМодуль с доп. модификаторами + if (COMMON_MODULE_METHOD.matcher(idText).matches()) { + return true; + } + + // Случай 2b: ОбщегоНазначения.ОбщийМодуль("Name") + if (COMMON_USE_MODULE.matcher(idText).matches()) { + // Проверяем, есть ли вызов ОбщийМодуль в модификаторах + for (var modifier : complexId.modifier()) { + if (isCommonModuleCallInModifier(modifier)) { + return true; + } + } + } + } + } + } + + return false; + } + + /** + * Извлечь имя общего модуля из expression. + * + * @param expression Контекст выражения + * @return Имя модуля, если удалось извлечь + */ + public static Optional extractCommonModuleName(BSLParser.ExpressionContext expression) { + if (expression == null) { + return Optional.empty(); + } + + var members = expression.member(); + if (members.isEmpty()) { + return Optional.empty(); + } + + for (var member : members) { + // Случай 1: complexIdentifier + var complexId = member.complexIdentifier(); + if (complexId != null) { + var identifier = complexId.IDENTIFIER(); + if (identifier != null) { + var idText = identifier.getText(); + + // Случай 2a: ОбщийМодуль("Name") - параметры в модификаторах + if (COMMON_MODULE_METHOD.matcher(idText).matches()) { + return extractModuleNameFromModifiers(complexId.modifier()); + } + + // Случай 2b: ОбщегоНазначения.ОбщийМодуль("Name") + if (COMMON_USE_MODULE.matcher(idText).matches()) { + for (var modifier : complexId.modifier()) { + var moduleName = extractModuleNameFromModifier(modifier); + if (moduleName.isPresent()) { + return moduleName; + } + } + } + } + + // Случай 2c: globalMethodCall внутри complexIdentifier + var globalMethodCall = complexId.globalMethodCall(); + if (globalMethodCall != null && globalMethodCall.methodName() != null) { + var methodName = globalMethodCall.methodName().IDENTIFIER(); + if (methodName != null && COMMON_MODULE_METHOD.matcher(methodName.getText()).matches()) { + return extractParameterFromDoCall(globalMethodCall.doCall()); + } + } + } + } + + return Optional.empty(); + } + + private static boolean isCommonModuleCallInModifier(BSLParser.ModifierContext modifier) { + var accessCall = modifier.accessCall(); + if (accessCall == null) { + return false; + } + + var methodCall = accessCall.methodCall(); + if (methodCall == null) { + return false; + } + + var methodName = methodCall.methodName(); + if (methodName == null || methodName.IDENTIFIER() == null) { + return false; + } + + return COMMON_MODULE_METHOD.matcher(methodName.IDENTIFIER().getText()).matches(); + } + + private static Optional extractModuleNameFromModifier(BSLParser.ModifierContext modifier) { + var accessCall = modifier.accessCall(); + if (accessCall == null) { + return Optional.empty(); + } + + var methodCall = accessCall.methodCall(); + if (methodCall == null || methodCall.methodName() == null) { + return Optional.empty(); + } + + var methodName = methodCall.methodName().IDENTIFIER(); + if (methodName == null || !COMMON_MODULE_METHOD.matcher(methodName.getText()).matches()) { + return Optional.empty(); + } + + return extractParameterFromDoCall(methodCall.doCall()); + } + + private static Optional extractModuleNameFromModifiers( + java.util.List modifiers) { + + for (var modifier : modifiers) { + var moduleName = extractParameterFromDoCall(modifier.accessCall()); + if (moduleName.isPresent()) { + return moduleName; + } + } + return Optional.empty(); + } + + private static Optional extractParameterFromDoCall(BSLParser.AccessCallContext accessCall) { + if (accessCall == null) { + return Optional.empty(); + } + + var methodCall = accessCall.methodCall(); + if (methodCall == null) { + return Optional.empty(); + } + + return extractParameterFromDoCall(methodCall.doCall()); + } + + private static Optional extractParameterFromDoCall(BSLParser.DoCallContext doCall) { + if (doCall == null) { + return Optional.empty(); + } + + var callParamList = doCall.callParamList(); + if (callParamList == null) { + return Optional.empty(); + } + + var params = callParamList.callParam(); + if (params.isEmpty()) { + return Optional.empty(); + } + + // Берем первый параметр - имя модуля + var firstParam = params.get(0); + return Optional.ofNullable(firstParam.getText()) + .map(Strings::trimQuotes); + } + + /** + * Проверить, является ли expression обращением к модулю менеджера. + * Распознает паттерны типа: Справочники.ИмяСправочника, Документы.ИмяДокумента и т.д. + * + * @param expression Контекст выражения + * @return true, если это обращение к модулю менеджера + */ + public static boolean isManagerModuleExpression(BSLParser.ExpressionContext expression) { + if (expression == null) { + return false; + } + + var members = expression.member(); + if (members.isEmpty()) { + return false; + } + + for (var member : members) { + var complexId = member.complexIdentifier(); + if (complexId != null) { + var identifier = complexId.IDENTIFIER(); + if (identifier != null) { + var idText = identifier.getText(); + // Проверяем, является ли это типом модуля менеджера (Справочники, Документы и т.д.) + if (MANAGER_MODULE_TYPES.matcher(idText).matches()) { + // Должен быть хотя бы один модификатор (имя объекта) + return !complexId.modifier().isEmpty(); + } + } + } + } + + return false; + } + + /** + * Извлечь информацию о модуле менеджера из expression. + * + * @param expression Контекст выражения + * @return Пара (тип менеджера, имя объекта), например ("Справочники", "Номенклатура") + */ + public static Optional extractManagerModuleInfo(BSLParser.ExpressionContext expression) { + if (expression == null) { + return Optional.empty(); + } + + var members = expression.member(); + if (members.isEmpty()) { + return Optional.empty(); + } + + for (var member : members) { + var complexId = member.complexIdentifier(); + if (complexId != null) { + var identifier = complexId.IDENTIFIER(); + if (identifier != null) { + var managerType = identifier.getText(); + if (MANAGER_MODULE_TYPES.matcher(managerType).matches()) { + // Ищем имя объекта в первом модификаторе + var modifiers = complexId.modifier(); + if (!modifiers.isEmpty()) { + var firstModifier = modifiers.get(0); + if (firstModifier.accessProperty() != null && + firstModifier.accessProperty().IDENTIFIER() != null) { + var objectName = firstModifier.accessProperty().IDENTIFIER().getText(); + return Optional.of(new ManagerModuleInfo(managerType, objectName)); + } + } + } + } + } + } + + return Optional.empty(); + } + + /** + * Информация о модуле менеджера. + * + * @param managerType Тип менеджера (Справочники, Документы и т.д.) + * @param objectName Имя объекта метаданных + */ + public record ManagerModuleInfo(String managerType, String objectName) { + } +} 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 cda194d139b..a9ff17e2875 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 @@ -286,4 +286,171 @@ void testRebuildClearReferences() { // then 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); + } } \ No newline at end of file diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/utils/CommonModuleReferenceTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/utils/CommonModuleReferenceTest.java new file mode 100644 index 00000000000..210ebc68c0e --- /dev/null +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/utils/CommonModuleReferenceTest.java @@ -0,0 +1,62 @@ +/* + * 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.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 static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@CleanupContextBeforeClassAndAfterEachTestMethod +class CommonModuleReferenceTest { + + @Test + void testDetectCommonModuleExpression() { + var code = "Процедура Тест()\n" + + " Модуль = ОбщегоНазначения.ОбщийМодуль(\"ПервыйОбщийМодуль\");\n" + + "КонецПроцедуры"; + + 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(CommonModuleReference.isCommonModuleExpression(expression)).isTrue(); + + var moduleName = CommonModuleReference.extractCommonModuleName(expression); + assertThat(moduleName).isPresent(); + assertThat(moduleName.get()).isEqualTo("ПервыйОбщийМодуль"); + } +} 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: Вызов функции в условии + Если МодульУправлениеДоступом.НеУстаревшаяФункция() Тогда + // что-то делаем + КонецЕсли; + +КонецПроцедуры