Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,13 @@ hover, completion, signature help и ряд диагностик. См. корн
`registry/FormAttributeTypeIndex`, наполняемый при регистрации формы: свойством формы такой
тип не выставишь, а обратному преобразованию нужен именно он. Имя реквизита и имя типа
берутся только из строковых литералов — собранное в переменной статически неизвестно.
**Именем типа состав колонок не выражен**, поэтому у табличных коллекций его приходится
приписывать отдельно, иначе на сервер приезжает `ТаблицаЗначений` без единой колонки:
у реквизита колонки берутся из его объявления в форме (`FormDataTypesRegistrar`
кладёт их в объявленный тип полями строки — тем же представлением, в котором их видит
таблица, собранная в коде), у `ДанныеФормыВЗначение` — у самих преобразуемых данных
(`TableCollectionInference.columnsOf` читает и накопленные поля, и члены типа строки).
Названный вторым параметром тип уточнения не отменяет: набор сужается до него.

Уточнение по виду — **изменение типа на месте**, наравне с
операторами-мутаторами: у созданного в коде элемента вид задаётся отдельной строкой
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ private TypeSet refinedCallTypes(TypeSet leftTypes, String memberName,
return adjusted;
}
var formTypes = formExpressionInference.refinedCallTypes(
ctx.documentContext, leftTypes, memberName, call);
ctx.documentContext, leftTypes, memberName, call, node -> inferInternal(node, ctx));
if (formTypes != null) {
return formTypes;
}
Expand Down Expand Up @@ -481,7 +481,7 @@ private TypeSet inferMethodCall(MethodCallNode call, InferenceContext ctx) {
// объявленный возврат обобщённый (`Произвольный`), а прикладной тип известен
// из аргументов вызова либо из объявления реквизита.
var convertedValue = formExpressionInference.convertedValueType(
ctx.documentContext, name.getText(), call, null);
ctx.documentContext, name.getText(), call, null, node -> inferInternal(node, ctx));
if (convertedValue != null) {
return convertedValue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import com.github._1c_syntax.bsl.languageserver.types.registry.TypeRegistry;
import com.github._1c_syntax.bsl.languageserver.utils.Ranges;
import com.github._1c_syntax.bsl.parser.BSLParser;
import com.github._1c_syntax.bsl.languageserver.utils.expressiontree.BslExpression;
import com.github._1c_syntax.bsl.languageserver.utils.expressiontree.MethodCallNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -46,6 +47,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

/**
* Знания о формах, нужные при выводе типа выражения.
Expand Down Expand Up @@ -105,6 +107,7 @@
private final FormAttributeTypeIndex formAttributeTypeIndex;
private final GlobalScopeProvider globalScopeProvider;
private final AssignmentByReceiverIndex assignmentByReceiverIndex;
private final TableCollectionInference tableCollectionInference;

/**
* Тип формы, открываемой по имени: {@code ОткрытьФорму("Справочник.Контрагенты.ФормаОбъекта")}
Expand Down Expand Up @@ -150,10 +153,12 @@
* @param receiver типы получателя.
* @param memberName имя вызываемого метода.
* @param call узел вызова.
* @param inferrer вывод типов выражения — им берутся типы преобразуемых данных.
* @return уточнённый тип; {@code null}, если правило неприменимо.
*/
public @Nullable TypeSet refinedCallTypes(DocumentContext documentContext, TypeSet receiver,
String memberName, MethodCallNode call) {
String memberName, MethodCallNode call,
Function<BslExpression, TypeSet> inferrer) {
var managerForm = formByNameResolver.resolveManagerForm(documentContext, receiver, memberName,
firstStringLiteral(call), documentContext.getFileType());
if (managerForm.isPresent()) {
Expand All @@ -162,7 +167,7 @@
if (isFindByName(memberName)) {
return memberByLiteralName(documentContext, receiver, firstStringLiteral(call));
}
return convertedValueType(documentContext, memberName, call, receiver);
return convertedValueType(documentContext, memberName, call, receiver, inferrer);
}

/**
Expand All @@ -175,31 +180,91 @@
* задан вторым параметром, он обязателен;</li>
* <li>{@code РеквизитФормыВЗначение("Объект")} — тип берётся из объявления реквизита
* в {@code Form.xml}: это {@code ДокументОбъект.Заказ}, а не
* {@code ДанныеФормыСтруктура}, которой реквизит выглядит на форме. Второй
* параметр, если он есть, тип задаёт и здесь.</li>
* {@code ДанныеФормыСтруктура}, которой реквизит выглядит на форме. У
* реквизита-таблицы (дерева) значений оттуда же приходят колонки. Второй
* параметр, если он есть, называет тип и здесь.</li>
* </ul>
* Названный тип берётся голым, поэтому колонки к нему приходится приписывать
* отдельно: у {@code ТаблицаЗначений} состав колонок в имени типа не выражен, а
* потерять его — значит вернуть на сервер таблицу, к которой не обратиться.
* <p>
* Тип, собранный в переменной ({@code РеквизитФормыВЗначение(ИмяРеквизита)}), статически
* неизвестен — тогда работает объявленный возврат платформенной функции.
*
* @param documentContext документ с вызовом.
* @param methodName имя вызываемой функции.
* @param call узел вызова.
* @param receiver типы получателя; {@code null} — вызов без квалификации.
* @param inferrer вывод типов выражения — им берутся типы преобразуемых данных.
* @return тип результата; {@code null}, если это не обратное преобразование либо
* статически определить тип нечем.
*/
public @Nullable TypeSet convertedValueType(DocumentContext documentContext, String methodName,
MethodCallNode call, @Nullable TypeSet receiver) {
MethodCallNode call, @Nullable TypeSet receiver,
Function<BslExpression, TypeSet> inferrer) {
var name = methodName.toLowerCase(Locale.ROOT);
var attributeToValue = ATTRIBUTE_TO_VALUE_FUNCTIONS.contains(name);
if (!attributeToValue && !DATA_TO_VALUE_FUNCTIONS.contains(name)) {
return null;
}
var explicitType = typeArgument(call, documentContext);
if (explicitType != null) {
TypeSet refined = null;
if (attributeToValue) {
refined = declaredAttributeType(documentContext, call, receiver);
} else if (explicitType != null && TableCollectionInference.carriesColumns(explicitType)) {

Check failure on line 214 in src/main/java/com/github/_1c_syntax/bsl/languageserver/types/inferencer/FormExpressionInference.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

"if ... else if" constructs should end with "else" clauses.

See more on https://sonarcloud.io/project/issues?id=1c-syntax_bsl-language-server&issues=AZ_NdRHlFKD1CCdtwass&open=AZ_NdRHlFKD1CCdtwass&pullRequest=4405
// Колонки преобразуемых данных только дополняют названный тип: без второго
// параметра преобразование неполно, и выдумывать коллекцию не из чего.
refined = columnsOfConvertedData(documentContext, call, explicitType, inferrer);
}
if (refined == null) {
return explicitType;
}
return attributeToValue ? declaredAttributeType(documentContext, call, receiver) : null;
return explicitType == null ? refined : narrowedToExplicit(refined, explicitType);
}

/**
* Названный вторым параметром тип — с уточнениями из объявления реквизита либо из самих
* преобразуемых данных.
* <p>
* У {@code РеквизитФормыВЗначение("Таблица", Тип("ТаблицаЗначений"))} второй параметр
* называет тот же тип, что объявлен в форме, но колонки известны только из объявления:
* взять голый {@code ТаблицаЗначений} значило бы их потерять.
*
* @param declared набор с уточнениями.
* @param explicitType тип, названный вторым параметром.
* @return уточнённый набор, суженный до названного типа; сам названный тип, если
* уточнений про него нет — тогда это преобразование к другому типу.
*/
private static TypeSet narrowedToExplicit(TypeSet declared, TypeSet explicitType) {
var result = TypeSet.EMPTY;
for (var ref : explicitType.refs()) {
result = result.union(declared.retaining(ref));
}
return result.isEmpty() ? explicitType : result;
}

/**
* Названный тип с колонками преобразуемых данных формы:
* {@code ДанныеФормыВЗначение(Объект.Товары, Тип("ТаблицаЗначений"))} даёт ту же
* таблицу, что лежала на форме, а колонки объявлены только там. Дерево значений —
* тем же путём: колонки у него лежат так же, полями строки.
* <p>
* Имя типа во втором параметре про состав колонок не говорит ничего, поэтому берутся
* они у первого — у самих данных.
*
* @return коллекция с колонками; {@code null}, если первый аргумент — не табличная
* коллекция либо колонок у него не выведено.
*/
private @Nullable TypeSet columnsOfConvertedData(DocumentContext documentContext, MethodCallNode call,
TypeSet explicitType,
Function<BslExpression, TypeSet> inferrer) {
var arguments = call.arguments();
if (arguments.isEmpty()) {
return null;
}
var data = inferrer.apply(arguments.get(0));
return tableCollectionInference.withColumns(
explicitType, tableCollectionInference.columnsOf(data, documentContext.getFileType()));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ class TableCollectionInference {
private static final String VALUE_TABLE = "ТаблицаЗначений";
/** Строка таблицы значений — общая для всего пакета: её же резолвит {@link ExpressionTypeInferencer}. */
static final String VALUE_TABLE_ROW = "СтрокаТаблицыЗначений";

/**
* Коллекции, у которых колонки лежат полями строки: {@code коллекция → её строка}.
* Только у них есть состав колонок, который можно перенести на другое значение.
*/
private static final Map<String, String> COLLECTION_ROWS = Map.of(
VALUE_TABLE, VALUE_TABLE_ROW,
"ДеревоЗначений", "СтрокаДереваЗначений");

private static final String COLUMNS_COLLECTION = "КоллекцияКолонокТаблицыЗначений";
private static final String COLUMN = "КолонкаТаблицыЗначений";
private static final String ARRAY = "Массив";
Expand Down Expand Up @@ -119,10 +128,10 @@ private TypeSet inferMethod(TypeSet receiver, MemberDescriptor member, MethodCal
return unloadColumn(columnsOf(receiver, fileType), call);
}
if (member.matches(UNLOAD)) {
return unloaded(selected(columnsOf(receiver, fileType), call, UNLOAD_COLUMNS_ARG));
return valueTableWith(selected(columnsOf(receiver, fileType), call, UNLOAD_COLUMNS_ARG));
}
if (member.matches(UNLOAD_COLUMNS)) {
return unloaded(selected(columnsOf(receiver, fileType), call, FIRST_ARG));
return valueTableWith(selected(columnsOf(receiver, fileType), call, FIRST_ARG));
}
return null;
}
Expand Down Expand Up @@ -151,8 +160,12 @@ private MemberDescriptor resolveMember(TypeSet receiver, String memberName, Memb
* Колонки табличной коллекции: {@code имя → типы значения}. Собираются и из уточнений,
* накопленных на месте, и из свойств зарегистрированного типа строки. Generic-слот
* ({@code <Имя колонки>}) колонкой не считается — это заготовка, а не колонка.
*
* @param receiver типы табличной коллекции.
* @param fileType язык файла-потребителя.
* @return колонки по именам; пусто, если получатель их не несёт.
*/
private Map<String, TypeSet> columnsOf(TypeSet receiver, FileType fileType) {
Map<String, TypeSet> columnsOf(TypeSet receiver, FileType fileType) {
var columns = new LinkedHashMap<String, TypeSet>();
for (var ref : receiver.refs()) {
var elements = receiver.getElementTypes(ref);
Expand Down Expand Up @@ -212,18 +225,61 @@ private static List<String> literalNames(MethodCallNode call, int argIndex) {

/** {@code ТаблицаЗначений}, строка которой несёт указанные колонки. */
@Nullable
private TypeSet unloaded(Map<String, TypeSet> columns) {
var tableRef = resolve(VALUE_TABLE);
var rowRef = resolve(VALUE_TABLE_ROW);
if (columns.isEmpty() || tableRef == null || rowRef == null) {
private TypeSet valueTableWith(Map<String, TypeSet> columns) {
return withColumns(resolve(VALUE_TABLE), VALUE_TABLE_ROW, columns);
}

/**
* Лежат ли у этого типа колонки полями строки — то есть можно ли перенести на него
* состав колонок другого значения. Так устроены таблица и дерево значений.
*
* @param types типы значения.
* @return {@code true}, если это таблица либо дерево значений.
*/
static boolean carriesColumns(TypeSet types) {
return types.refs().stream().anyMatch(ref -> COLLECTION_ROWS.containsKey(ref.qualifiedName()));
}

/**
* Табличная коллекция названного типа, строка которой несёт указанные колонки.
* <p>
* Нужно обратному преобразованию данных формы: {@code ДанныеФормыВЗначение} называет
* тип вторым параметром, а состав колонок в имени типа не выражен.
*
* @param target тип, в который преобразуют.
* @param columns колонки по именам.
* @return коллекция с колонками; {@code null}, если это не таблица и не дерево
* значений, колонок нет либо самих типов в реестре не нашлось.
*/
@Nullable
TypeSet withColumns(TypeSet target, Map<String, TypeSet> columns) {
for (var ref : target.refs()) {
var rowName = COLLECTION_ROWS.get(ref.qualifiedName());
if (rowName != null) {
return withColumns(ref, rowName, columns);
}
}
return null;
}

/**
* Коллекция с колонками, разложенными полями её строки.
*
* @return коллекция с колонками; {@code null}, если колонок нет либо самой коллекции
* или её строки в реестре не нашлось.
*/
@Nullable
private TypeSet withColumns(@Nullable TypeRef collectionRef, String rowName, Map<String, TypeSet> columns) {
var rowRef = resolve(rowName);
if (columns.isEmpty() || collectionRef == null || rowRef == null) {
return null;
}
var fields = new LinkedHashMap<String, LocalField>();
var fields = LinkedHashMap.<String, LocalField>newLinkedHashMap(columns.size());
for (var column : columns.entrySet()) {
fields.put(column.getKey(), new LocalField(column.getValue(), ""));
}
var row = TypeSet.of(rowRef).withFields(rowRef, fields);
return TypeSet.of(tableRef).withElement(tableRef, row);
return TypeSet.of(collectionRef).withElement(collectionRef, row);
}

/** {@code Массив} значений одной колонки — с типом элемента этой колонки. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ void register(TypeRef formRef, Map<String, TypeSet> declaredTypes) {
*
* @param formRef тип формы.
* @param attributeName имя реквизита.
* @return объявленный тип; пустой набор, если это не форма либо реквизита с таким
* @return объявленный тип — у реквизита-таблицы (дерева) значений вместе с колонками,
* объявленными в форме; пустой набор, если это не форма либо реквизита с таким
* именем у неё нет.
*/
public TypeSet declaredType(TypeRef formRef, String attributeName) {
Expand Down
Loading
Loading