content: /*******************************************************************************
   * Copyright (C) 2021, 1C-Soft LLC and others.
   *
   * This program and the accompanying materials are made
   * available under the terms of the Eclipse Public License 2.0
   * which is available at https://www.eclipse.org/legal/epl-2.0/
   *
   * SPDX-License-Identifier: EPL-2.0
   *
   * Contributors:
   *     1C-Soft LLC - initial API and implementation
   *******************************************************************************/
  package com.e1c.v8codestyle.bsl.strict.check;
  
  import static com.e1c.v8codestyle.bsl.strict.check.StrictTypeAnnotationCheckExtension.PARAM_CHECK_ANNOTATION_IN_MODULE_DESCRIPTION;
  
  import java.text.MessageFormat;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.List;
  import java.util.Objects;
  import java.util.Optional;
  import java.util.Set;
  import java.util.Spliterator;
  import java.util.Spliterators;
  import java.util.TreeSet;
  import java.util.stream.Collectors;
  import java.util.stream.StreamSupport;
  
  import org.eclipse.core.resources.IProject;
  import org.eclipse.core.runtime.IProgressMonitor;
  import org.eclipse.emf.ecore.EObject;
  import org.eclipse.emf.ecore.resource.Resource;
  import org.eclipse.xtext.EcoreUtil2;
  import org.eclipse.xtext.naming.IQualifiedNameConverter;
  import org.eclipse.xtext.scoping.IScope;
  import org.eclipse.xtext.scoping.IScopeProvider;
  import org.eclipse.xtext.util.Pair;
  
  import com._1c.g5.v8.dt.bsl.comment.DocumentationCommentProperties;
  import com._1c.g5.v8.dt.bsl.common.IBslPreferences;
  import com._1c.g5.v8.dt.bsl.documentation.comment.BslDocumentationComment;
  import com._1c.g5.v8.dt.bsl.documentation.comment.BslDocumentationComment.ReturnSection;
  import com._1c.g5.v8.dt.bsl.documentation.comment.BslMultiLineCommentDocumentationProvider;
  import com._1c.g5.v8.dt.bsl.documentation.comment.IDescriptionPart;
  import com._1c.g5.v8.dt.bsl.model.BslPackage.Literals;
  import com._1c.g5.v8.dt.bsl.model.Function;
  import com._1c.g5.v8.dt.bsl.model.ReturnStatement;
  import com._1c.g5.v8.dt.bsl.resource.DynamicFeatureAccessComputer;
  import com._1c.g5.v8.dt.bsl.resource.TypesComputer;
  import com._1c.g5.v8.dt.bsl.util.BslUtil;
  import com._1c.g5.v8.dt.core.platform.IBmModelManager;
  import com._1c.g5.v8.dt.core.platform.IResourceLookup;
  import com._1c.g5.v8.dt.core.platform.IV8Project;
  import com._1c.g5.v8.dt.core.platform.IV8ProjectManager;
  import com._1c.g5.v8.dt.mcore.McorePackage;
  import com._1c.g5.v8.dt.mcore.Property;
  import com._1c.g5.v8.dt.mcore.TypeItem;
  import com._1c.g5.v8.dt.mcore.util.McoreUtil;
  import com._1c.g5.v8.dt.metadata.mdclass.ScriptVariant;
  import com._1c.g5.v8.dt.platform.IEObjectTypeNames;
  import com.e1c.g5.dt.core.api.naming.INamingService;
  import com.e1c.g5.dt.core.api.platform.BmOperationContext;
  import com.e1c.g5.v8.dt.bsl.check.DocumentationCommentBasicDelegateCheck;
  import com.e1c.g5.v8.dt.check.CheckComplexity;
  import com.e1c.g5.v8.dt.check.ICheckParameters;
  import com.e1c.g5.v8.dt.check.settings.IssueSeverity;
  import com.e1c.g5.v8.dt.check.settings.IssueType;
  import com.e1c.v8codestyle.bsl.strict.StrictTypeUtil;
  import com.google.common.base.Predicates;
  import com.google.inject.Inject;
  
  /**
   *  Documenting comment validator.
   *  Validate user's data types of function constructor in return types specified in documenting comments.
   *
   * @author Dmitriy Marmyshev
   *
   */
  public class FunctionCtorReturnSectionCheck
      extends DocumentationCommentBasicDelegateCheck
  {
  
      private static final String CHECK_ID = "constructor-function-return-section"; //$NON-NLS-1$
  
      private static final String PARAM_CHECK_TYPES = "checkTypes"; //$NON-NLS-1$
  
      private static final Set<String> DEFAULT_CHECK_TYPES = Set.of(IEObjectTypeNames.STRUCTURE,
          IEObjectTypeNames.FIXED_STRUCTURE, IEObjectTypeNames.VALUE_TABLE, IEObjectTypeNames.VALUE_TREE);
  
      private final TypesComputer typesComputer;
  
      private final DynamicFeatureAccessComputer dynamicComputer;
  
      private final IScopeProvider scopeProvider;
  
      private final IQualifiedNameConverter qualifiedNameConverter;
  
      private final BslMultiLineCommentDocumentationProvider commentProvider;
  
      private final IBslPreferences bslPreferences;
  
      /**
       * Instantiates a new function constructor documentation comment return section check.
       *
       * @param resourceLookup the resource lookup service, cannot be {@code null}.
       * @param v8ProjectManager the v8 project manager service, cannot be {@code null}.
       * @param qualifiedNameConverter the qualified name converter service, cannot be {@code null}.
       * @param bslPreferences the BSL preferences service, cannot be {@code null}.
       * @param typesComputer the types computer service, cannot be {@code null}.
       * @param dynamicComputer the dynamic computer service, cannot be {@code null}.
       * @param scopeProvider the scope provider service, cannot be {@code null}.
       * @param commentProvider the comment provider service, cannot be {@code null}.
       * @param namingService service for getting names of EDT object and resources, cannot be <code>null</code>
       * @param bmModelManager service for getting instance of Bm Model by {@link EObject}, cannot be <code>null</code>
       * @param v8ProjectManager {@link IV8ProjectManager} for getting {@link IV8Project} by {@link EObject}, cannot be <code>null</code>
       */
      @Inject
      public FunctionCtorReturnSectionCheck(IResourceLookup resourceLookup,
          IQualifiedNameConverter qualifiedNameConverter, IBslPreferences bslPreferences, TypesComputer typesComputer,
          DynamicFeatureAccessComputer dynamicComputer, IScopeProvider scopeProvider,
          BslMultiLineCommentDocumentationProvider commentProvider, INamingService namingService,
          IBmModelManager bmModelManager, IV8ProjectManager v8ProjectManager)
      {
          super(resourceLookup, namingService, bmModelManager, v8ProjectManager);
          this.typesComputer = typesComputer;
          this.dynamicComputer = dynamicComputer;
          this.scopeProvider = scopeProvider;
          this.commentProvider = commentProvider;
          this.qualifiedNameConverter = qualifiedNameConverter;
          this.bslPreferences = bslPreferences;
      }
  
      @Override
      public String getCheckId()
      {
          return CHECK_ID;
      }
  
      @Override
      protected void configureCheck(CheckConfigurer builder)
      {
          builder.title(Messages.FunctionCtorReturnSectionCheck_title)
              .description(Messages.FunctionCtorReturnSectionCheck_description)
              .complexity(CheckComplexity.NORMAL)
              .severity(IssueSeverity.MAJOR)
              .issueType(IssueType.CODE_STYLE)
              .delegate(ReturnSection.class);
          builder.parameter(PARAM_CHECK_TYPES, String.class, String.join(",", DEFAULT_CHECK_TYPES), //$NON-NLS-1$
              Messages.FunctionCtorReturnSectionCheck_User_extandable_Data_type_list_comma_separated);
          builder.parameter(PARAM_CHECK_ANNOTATION_IN_MODULE_DESCRIPTION, Boolean.class, Boolean.FALSE.toString(),
              Messages.StrictTypeAnnotationCheckExtension_Check__strict_types_annotation_in_module_desctioption);
  
      }
  
      @Override
      protected void checkDocumentationCommentObject(IDescriptionPart object, BslDocumentationComment root,
          DocumentationCommentResultAcceptor resultAceptor, ICheckParameters parameters, BmOperationContext context,
          IProgressMonitor monitor)
      {
          if (monitor.isCanceled() || !(root.getMethod() instanceof Function)
              || parameters.getBoolean(PARAM_CHECK_ANNOTATION_IN_MODULE_DESCRIPTION)
                  && !StrictTypeUtil.hasStrictTypeAnnotation(root.getModule()))
          {
              return;
          }
  
          ReturnSection returnSection = (ReturnSection)object;
  
          Function method = (Function)root.getMethod();
  
          IScope typeScope = scopeProvider.getScope(method, McorePackage.Literals.TYPE_DESCRIPTION__TYPES);
  
          IProject project = resourceLookup.getProject(root.getModule());
  
          boolean isRussianScript = isRussianScriptVariantProject(project);
  
          DocumentationCommentProperties props = bslPreferences.getDocumentCommentProperties(project);
  
          boolean oldFormat = props.oldCommentFormat();
  
          Collection<TypeItem> computedReturnTypes = root.computeReturnTypes(typeScope, scopeProvider,
              qualifiedNameConverter, commentProvider, v8ProjectManager, oldFormat, method, context);
  
          Set<String> checkTypes = getCheckTypes(parameters);
  
          Collection<String> computedReturnTypeNames = BslUtil.computeTypeNames(computedReturnTypes, method);
  
          if (isUserDataTypes(computedReturnTypeNames, checkTypes))
          {
              List<ReturnStatement> returns =
                  StreamSupport
                      .stream(Spliterators.spliteratorUnknownSize(EcoreUtil2.getAllContents(method, false),
                          Spliterator.ORDERED), false)
                      .filter(ReturnStatement.class::isInstance)
                      .map(ReturnStatement.class::cast)
                      .collect(Collectors.toList());
  
              Resource res = method.eResource();
  
              Collection<Pair<Collection<Property>, TypeItem>> coputedProperties =
                  dynamicComputer.getAllProperties(computedReturnTypes, res);
  
              for (ReturnStatement statment : returns)
              {
                  List<TypeItem> returnTypes =
                      typesComputer.computeTypes(statment.getExpression(), method.environments());
  
                  Collection<Pair<Collection<Property>, TypeItem>> properties =
                      dynamicComputer.getAllProperties(returnTypes, res);
  
                  for (TypeItem returnType : returnTypes)
                  {
                      if (returnType.eIsProxy())
                      {
                          continue;
                      }
                      String returnTypeName = McoreUtil.getTypeName(returnType);
                      if (returnTypeName != null && computedReturnTypeNames.contains(returnTypeName))
                      {
                          if (isUserDataTypes(List.of(returnTypeName), checkTypes))
                          {
                              Optional<Pair<Collection<Property>, TypeItem>> declaredProperties =
                                  coputedProperties.stream()
                                      .filter(t -> returnTypeName.equals(McoreUtil.getTypeName(t.getSecond())))
                                      .findAny();
                              Optional<Pair<Collection<Property>, TypeItem>> typeProperties = properties.stream()
                                  .filter(t -> returnTypeName.equals(McoreUtil.getTypeName(t.getSecond())))
                                  .findAny();
  
                              checkTypeProperties(method, statment, isRussianScript, returnType,
                                  declaredProperties.orElse(null), typeProperties.orElse(null), resultAceptor);
                          }
                      }
                      else if (isWarningReturnNonDeclaredType(method, returnSection, returnType, computedReturnTypes))
                      {
                          addWarningReturnNonDeclaredType(statment, isRussianScript, returnType, returnTypeName,
                              resultAceptor);
                      }
                  }
              }
          }
      }
  
      private Set<String> getCheckTypes(ICheckParameters parameters)
      {
          String[] types = parameters.getString(PARAM_CHECK_TYPES).replace(" ", " ").split(","); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
  
          Set<String> checkTypes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
          checkTypes.addAll(Set.of(types));
          return checkTypes;
      }
  
      private void checkTypeProperties(Function function, ReturnStatement statment, boolean useRussianScript,
          TypeItem returnType, Pair<Collection<Property>, TypeItem> declaredProperties,
          Pair<Collection<Property>, TypeItem> typeProperties, DocumentationCommentResultAcceptor resultAceptor)
      {
          if (declaredProperties == null || typeProperties == null)
          {
              return;
          }
  
          List<String> declaredProertyNames = new ArrayList<>();
  
          // check declared properties and types
          for (Property declaredProperty : declaredProperties.getFirst())
          {
              String propertyName = useRussianScript ? declaredProperty.getNameRu() : declaredProperty.getName();
              declaredProertyNames.add(propertyName);
              Collection<String> declaredType =
                  com._1c.g5.v8.dt.bsl.util.BslUtil.computeTypeNames(declaredProperty.getTypes(), function);
              if (declaredType.isEmpty())
              {
                  continue;
              }
  
              List<TypeItem> types = typeProperties.getFirst()
                  .stream()
                  .filter(p -> useRussianScript ? p.getNameRu().equals(declaredProperty.getNameRu())
                      : p.getName().equals(declaredProperty.getName()))
                  .flatMap(p -> p.getTypes().stream())
                  .collect(Collectors.toList());
  
              List<TypeItem> missingTypes = types.stream().filter(t -> {
                  String typeName = McoreUtil.getTypeCategory(t);
                  if (typeName != null)
                  {
                      if (!declaredType.contains(typeName))
                      {
                          return !"CommonModule".equals(McoreUtil.getTypeCategory(t)) //$NON-NLS-1$
                              || !declaredType.contains("CommonModule"); //$NON-NLS-1$
                      }
                  }
                  return false;
              }).collect(Collectors.toList());
              if (types.isEmpty())
              {
                  addWarningDeclaredNonReturningProperty(statment, useRussianScript, declaredProperty, resultAceptor);
              }
              else if (!missingTypes.isEmpty())
              {
                  addWarningDeclaredNonReturningPropertyType(statment, useRussianScript, declaredProperty, missingTypes,
                      resultAceptor);
              }
          }
  
          // check for non declared properties
          if (isWarningReturnNonDeclaredProperty(function, statment, declaredProperties.getFirst()))
          {
              List<String> nonDeclaredProperties = typeProperties.getFirst()
                  .stream()
                  .map(useRussianScript ? Property::getNameRu : Property::getName)
                  .filter(Predicates.not(declaredProertyNames::contains))
                  .collect(Collectors.toList());
              if (!nonDeclaredProperties.isEmpty())
              {
                  addWarningNonDeclaredReturningProperty(statment, useRussianScript, nonDeclaredProperties,
                      resultAceptor);
              }
          }
      }
  
      private void addWarningReturnNonDeclaredType(ReturnStatement statment, boolean isRussianScript, TypeItem returnType,
          String returnTypeName, DocumentationCommentResultAcceptor resultAceptor)
      {
          final String message = MessageFormat.format(Messages.FunctionCtorReturnSectionCheck_Return_non_declared_type__T,
              isRussianScript ? McoreUtil.getTypeNameRu(returnType) : returnTypeName);
  
          resultAceptor.addIssue(message, statment, Literals.RETURN_STATEMENT__EXPRESSION);
      }
  
      private boolean isWarningReturnNonDeclaredType(Function function, ReturnSection returnSection, TypeItem returnType,
          Collection<TypeItem> computedReturnTypes)
      {
          return function.isExport();
      }
  
      private boolean isUserDataTypes(Collection<String> computedReturnTypeNames, Set<String> checkTypes)
      {
          if (computedReturnTypeNames.isEmpty())
          {
              return false;
          }
  
          for (String typeName : computedReturnTypeNames)
          {
              if (checkTypes.contains(typeName))
              {
                  return true;
              }
          }
          return false;
      }
  
      private void addWarningDeclaredNonReturningProperty(ReturnStatement statment, boolean useRussianScript,
          Property property, DocumentationCommentResultAcceptor resultAceptor)
      {
          final String message = MessageFormat.format(
              Messages.FunctionCtorReturnSectionCheck_Declared_property__N__with_type__T__not_returning,
              useRussianScript ? property.getNameRu() : property.getName(), String.join(", ", //$NON-NLS-1$
                  property.getTypes()
                      .stream()
                      .map(useRussianScript ? McoreUtil::getTypeNameRu : McoreUtil::getTypeName)
                      .filter(Objects::nonNull)
                      .collect(Collectors.toList())));
  
          resultAceptor.addIssue(message, statment, Literals.RETURN_STATEMENT__EXPRESSION);
      }
  
      private void addWarningDeclaredNonReturningPropertyType(ReturnStatement statment, boolean useRussianScript,
          Property property, List<TypeItem> missingTypes, DocumentationCommentResultAcceptor resultAceptor)
      {
          final String message = MessageFormat.format(
              Messages.FunctionCtorReturnSectionCheck_Declared_property__N__with_type__T__missing_returning_types__M,
              useRussianScript ? property.getNameRu() : property.getName(), String.join(", ", //$NON-NLS-1$
                  property.getTypes()
                      .stream()
                      .map(useRussianScript ? McoreUtil::getTypeNameRu : McoreUtil::getTypeName)
                      .filter(Objects::nonNull)
                      .collect(Collectors.toList())),
              String.join(", ", //$NON-NLS-1$
                  missingTypes.stream()
                      .map(useRussianScript ? McoreUtil::getTypeNameRu : McoreUtil::getTypeName)
                      .filter(Objects::nonNull)
                      .collect(Collectors.toList())));
  
          resultAceptor.addIssue(message, statment, Literals.RETURN_STATEMENT__EXPRESSION);
  
      }
  
      private boolean isWarningReturnNonDeclaredProperty(Function function, ReturnStatement statment,
          Collection<Property> computedReturnProperties)
      {
          return function.isExport();
      }
  
      private void addWarningNonDeclaredReturningProperty(ReturnStatement statment, boolean useRussianScript,
          Collection<String> properties, DocumentationCommentResultAcceptor resultAceptor)
      {
  
          final String message = MessageFormat.format(
              Messages.FunctionCtorReturnSectionCheck_Return_non_declared_propertes__N, String.join(", ", properties)); //$NON-NLS-1$
  
          resultAceptor.addIssue(message, statment, Literals.RETURN_STATEMENT__EXPRESSION);
  
      }
  
      private boolean isRussianScriptVariantProject(IProject project)
      {
          IV8Project v8Project = v8ProjectManager.getProject(project);
  
          return (v8Project != null) && (v8Project.getScriptVariant() == ScriptVariant.RUSSIAN);
      }
  
  }
  