// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

/// @docImport 'package:analyzer/src/error/best_practices_verifier.dart';
library;

import 'package:_fe_analyzer_shared/src/flow_analysis/flow_analysis.dart';
import 'package:_fe_analyzer_shared/src/types/shared_type.dart';
import 'package:analyzer/dart/ast/syntactic_entity.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/ast/extensions.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/inheritance_manager3.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag;
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/error/listener.dart';
import 'package:analyzer/src/utilities/extensions/object.dart';

/// Methods useful in detecting errors.  This mixin exists to allow code to be
/// more easily shared between the two visitors that do the majority of error
/// reporting (ResolverVisitor and ErrorVerifier).
mixin ErrorDetectionHelpers {
  DiagnosticReporter get diagnosticReporter;

  InheritanceManager3 get inheritance;

  bool get strictCasts;

  TypeSystemImpl get typeSystem;

  /// Verify that the given [expression] can be assigned to its corresponding
  /// parameters. The [expectedStaticType] is the expected static type of the
  /// parameter. The [actualStaticType] is the actual static type of the
  /// argument.
  void checkForArgumentTypeNotAssignable(
    Expression expression,
    TypeImpl expectedStaticType,
    TypeImpl actualStaticType,
    NonAssignabilityReporter nonAssignabilityReporter, {
    Map<SharedTypeView, NonPromotionReason> Function()? whyNotPromoted,
  }) {
    if (expectedStaticType is! VoidType &&
        checkForUseOfVoidResult(expression)) {
      return;
    }

    checkForAssignableExpressionAtType(
      expression,
      actualStaticType,
      expectedStaticType,
      nonAssignabilityReporter,
      whyNotPromoted: whyNotPromoted,
    );
  }

  /// Verify that the given [argument] can be assigned to its corresponding
  /// parameter.
  ///
  /// See [diag.argumentTypeNotAssignable].
  void checkForArgumentTypeNotAssignableForArgument(
    ExpressionImpl argument, {
    bool promoteParameterToNullable = false,
    Map<SharedTypeView, NonPromotionReason> Function()? whyNotPromoted,
  }) {
    var parameter = argument.correspondingParameter;
    TypeImpl? correspondingParameterType;
    if (parameter != null) {
      correspondingParameterType = parameter.type;
    } else if (argument.parent
        case ArgumentListImpl(
          parent: FunctionExpressionInvocationImpl(
            function: Expression(:var staticType),
          ),
        )
        when identical(staticType, DynamicTypeImpl.instance) ||
            identical(staticType, NeverTypeImpl.instance) ||
            staticType is InterfaceTypeImpl && staticType.isDartCoreFunction) {
      // Treat dynamic invocations as having function types where all formal
      // parameter types are 'dynamic'.
      correspondingParameterType = DynamicTypeImpl.instance;
    }

    if (correspondingParameterType != null) {
      _checkForArgumentTypeNotAssignableForArgument(
        argument: argument is NamedExpressionImpl
            ? argument.expression
            : argument,
        staticParameterType: correspondingParameterType,
        promoteParameterToNullable: promoteParameterToNullable,
        whyNotPromoted: whyNotPromoted,
      );
    }
  }

  void checkForAssignableExpressionAtType(
    Expression expression,
    TypeImpl actualStaticType,
    TypeImpl expectedStaticType,
    NonAssignabilityReporter nonAssignabilityReporter, {
    Map<SharedTypeView, NonPromotionReason> Function()? whyNotPromoted,
  }) {
    if (expectedStaticType is! VoidType &&
        checkForUseOfVoidResult(expression)) {
      return;
    }

    if (!typeSystem.isAssignableTo(
      actualStaticType,
      expectedStaticType,
      strictCasts: strictCasts,
    )) {
      AstNode getErrorNode(AstNode node) {
        if (node is CascadeExpression) {
          return getErrorNode(node.target);
        }
        if (node is ParenthesizedExpression) {
          return getErrorNode(node.expression);
        }
        return node;
      }

      if (expectedStaticType is RecordTypeImpl &&
          expectedStaticType.positionalFields.length == 1 &&
          actualStaticType is! RecordType &&
          expression is ParenthesizedExpression) {
        var field = expectedStaticType.positionalFields.first;
        if (typeSystem.isAssignableTo(
          field.type,
          actualStaticType,
          strictCasts: strictCasts,
        )) {
          diagnosticReporter.report(
            diag.recordLiteralOnePositionalNoTrailingCommaByType.at(expression),
          );
          return;
        }
      }
      var whyNotPromotedMessages = computeWhyNotPromotedMessages(
        expression,
        whyNotPromoted?.call(),
      );
      diagnosticReporter.report(
        nonAssignabilityReporter
            .createDiagnostic(
              expectedStaticType: expectedStaticType,
              actualStaticType: actualStaticType,
            )
            .withContextMessages(whyNotPromotedMessages)
            .at(getErrorNode(expression)),
      );
    }
  }

  /// Verify that the given constructor field [initializer] has compatible field
  /// and initializer expression types. The [fieldElement] is the static element
  /// from the name in the [ConstructorFieldInitializer].
  ///
  /// See [diag.constFieldInitializerNotAssignable], and
  /// [diag.fieldInitializerNotAssignable].
  void checkForFieldInitializerNotAssignable(
    ConstructorFieldInitializerImpl initializer,
    InternalFieldElement fieldElement, {
    required bool isConstConstructor,
    required Map<SharedTypeView, NonPromotionReason> Function()? whyNotPromoted,
  }) {
    // prepare field type
    var fieldType = fieldElement.type;
    // prepare expression type
    Expression expression = initializer.expression;
    // test the static type of the expression
    var staticType = expression.typeOrThrow;
    if (typeSystem.isAssignableTo(
      staticType,
      fieldType,
      strictCasts: strictCasts,
    )) {
      if (fieldType is! VoidType) {
        checkForUseOfVoidResult(expression);
      }
      return;
    }
    var messages = computeWhyNotPromotedMessages(
      expression,
      whyNotPromoted?.call(),
    );
    // report problem
    if (isConstConstructor) {
      // TODO(paulberry): this error should be based on the actual type of the
      // constant, not the static type.  See dartbug.com/21119.
      diagnosticReporter.atNode(
        expression,
        diag.constFieldInitializerNotAssignable,
        arguments: [staticType, fieldType],
        contextMessages: messages,
      );
    } else {
      diagnosticReporter.atNode(
        expression,
        diag.fieldInitializerNotAssignable,
        arguments: [staticType, fieldType],
        contextMessages: messages,
      );
    }

    // TODO(brianwilkerson): Define a hint corresponding to these errors and
    // report it if appropriate.
    //        // test the propagated type of the expression
    //        Type propagatedType = expression.getPropagatedType();
    //        if (propagatedType != null && propagatedType.isAssignableTo(fieldType)) {
    //          return false;
    //        }
    //        // report problem
    //        if (isEnclosingConstructorConst) {
    //          errorReporter.reportTypeErrorForNode(
    //              CompileTimeErrorCode.CONST_FIELD_INITIALIZER_NOT_ASSIGNABLE,
    //              expression,
    //              propagatedType == null ? staticType : propagatedType,
    //              fieldType);
    //        } else {
    //          errorReporter.reportTypeErrorForNode(
    //              StaticWarningCode.FIELD_INITIALIZER_NOT_ASSIGNABLE,
    //              expression,
    //              propagatedType == null ? staticType : propagatedType,
    //              fieldType);
    //        }
    //        return true;
  }

  /// Check for situations where the result of a method or function is used,
  /// when it returns 'void'. Or, in rare cases, when other types of expressions
  /// are void, such as identifiers.
  ///
  /// See [diag.useOfVoidResult].
  bool checkForUseOfVoidResult(Expression expression) {
    if (!identical(expression.staticType, VoidTypeImpl.instance)) {
      return false;
    }

    if (expression is MethodInvocation) {
      SimpleIdentifier methodName = expression.methodName;
      diagnosticReporter.report(diag.useOfVoidResult.at(methodName));
    } else {
      diagnosticReporter.report(diag.useOfVoidResult.at(expression));
    }

    return true;
  }

  void checkIndexExpressionIndex(
    Expression index, {
    required InternalExecutableElement? readElement,
    required InternalExecutableElement? writeElement,
    required Map<SharedTypeView, NonPromotionReason> Function()? whyNotPromoted,
  }) {
    if (readElement is InternalMethodElement) {
      var parameters = readElement.formalParameters;
      if (parameters.isNotEmpty) {
        _checkForArgumentTypeNotAssignableForArgument(
          argument: index,
          staticParameterType: parameters[0].type,
          promoteParameterToNullable: false,
          whyNotPromoted: whyNotPromoted,
        );
      }
    }

    if (writeElement is InternalMethodElement) {
      var parameters = writeElement.formalParameters;
      if (parameters.isNotEmpty) {
        _checkForArgumentTypeNotAssignableForArgument(
          argument: index,
          staticParameterType: parameters[0].type,
          promoteParameterToNullable: false,
          whyNotPromoted: whyNotPromoted,
        );
      }
    }
  }

  /// Computes the appropriate set of context messages to report along with an
  /// error that may have occurred because an expression was not type promoted.
  ///
  /// If the expression is `null`, it means the expression that was not type
  /// promoted was an implicit `this`.
  ///
  /// [errorEntity] is the entity whose location will be associated with the
  /// error.  This is needed for test instrumentation.
  ///
  /// [whyNotPromoted] should be the non-promotion details returned by the flow
  /// analysis engine.
  List<DiagnosticMessage> computeWhyNotPromotedMessages(
    SyntacticEntity errorEntity,
    Map<SharedTypeView, NonPromotionReason>? whyNotPromoted,
  );

  /// If an assignment from [type] to [context] is a case of an implicit 'call'
  /// method, returns the element of the 'call' method.
  ///
  /// From the spec:
  ///
  /// > Let `e` be an expression whose static type is an interface type that has
  /// > a method named `call`. In the case where the context type for `e`
  /// > is a function type or the type `Function`, `e` is treated as `e.call`.
  InternalMethodElement? getImplicitCallMethod(
    DartType type,
    DartType context,
    SyntacticEntity errorNode,
  ) {
    var visitedTypes = {type};
    while (type is TypeParameterType) {
      if (type.nullabilitySuffix != NullabilitySuffix.none) {
        // The value might be `null`, so implicit `.call` tearoff is invalid.
        return null;
      }
      type = type.bound;
      if (!visitedTypes.add(type)) {
        // A cycle!
        return null;
      }
    }
    if (typeSystem.acceptsFunctionType(context) &&
        type is InterfaceType &&
        type.nullabilitySuffix != NullabilitySuffix.question) {
      return inheritance
          .getMember3(
            type,
            Name.forLibrary(
              type.element.library,
              MethodElement.CALL_METHOD_NAME,
            ),
          )
          .ifTypeOrNull();
    } else {
      return null;
    }
  }

  /// Return the variable element represented by the given [expression], or
  /// `null` if there is no such element.
  VariableElement? getVariableElement(Expression? expression) {
    if (expression is Identifier) {
      var element = expression.element;
      if (element is VariableElement) {
        return element;
      }
    }
    return null;
  }

  void _checkForArgumentTypeNotAssignableForArgument({
    required Expression argument,
    required TypeImpl staticParameterType,
    required bool promoteParameterToNullable,
    Map<SharedTypeView, NonPromotionReason> Function()? whyNotPromoted,
  }) {
    if (promoteParameterToNullable) {
      staticParameterType = typeSystem.makeNullable(staticParameterType);
    }
    checkForArgumentTypeNotAssignable(
      argument,
      staticParameterType,
      argument.typeOrThrow,
      const NonAssignabilityReporterForArgument(),
      whyNotPromoted: whyNotPromoted,
    );
  }
}

/// Abstract helper class for reporting an error related to non-assignability.
///
/// Concrete derived classes exist for handling the different varieties of
/// non-assignability errors.
abstract class NonAssignabilityReporter {
  const NonAssignabilityReporter();

  /// Creates the appropriate [LocatableDiagnostic] to report that the user has
  /// tried to assign [actualStaticType] to [expectedStaticType].
  LocatableDiagnostic createDiagnostic({
    required TypeImpl expectedStaticType,
    required TypeImpl actualStaticType,
  });
}

/// Helper class for reporting the error [diag.argumentTypeNotAssignable].
class NonAssignabilityReporterForArgument extends NonAssignabilityReporter {
  const NonAssignabilityReporterForArgument();

  @override
  LocatableDiagnostic createDiagnostic({
    required TypeImpl expectedStaticType,
    required TypeImpl actualStaticType,
  }) {
    var additionalInfo = <String>[];
    if (expectedStaticType is RecordTypeImpl &&
        actualStaticType is RecordTypeImpl) {
      var actualPositionalFields = actualStaticType.positionalFields.length;
      var expectedPositionalFields = expectedStaticType.positionalFields.length;
      if (expectedPositionalFields != 0 &&
          actualPositionalFields != expectedPositionalFields) {
        additionalInfo.add(
          'Expected $expectedPositionalFields positional arguments, but got $actualPositionalFields instead.',
        );
      }
      var actualNamedFieldsLength = actualStaticType.namedFields.length;
      var expectedNamedFieldsLength = expectedStaticType.namedFields.length;
      if (expectedNamedFieldsLength != 0 &&
          actualNamedFieldsLength != expectedNamedFieldsLength) {
        additionalInfo.add(
          'Expected $expectedNamedFieldsLength named arguments, but got $actualNamedFieldsLength instead.',
        );
      }
      var namedFields = expectedStaticType.namedFields;
      if (namedFields.isNotEmpty) {
        for (var field in actualStaticType.namedFields) {
          if (!namedFields.any(
            (element) =>
                element.name == field.name && field.type == element.type,
          )) {
            additionalInfo.add(
              'Unexpected named argument `${field.name}` with type `${field.type.getDisplayString()}`.',
            );
          }
        }
      }
    }
    return diag.argumentTypeNotAssignable.withArguments(
      actualStaticType: actualStaticType,
      expectedStaticType: expectedStaticType,
      additionalInfo: additionalInfo.join(' '),
    );
  }
}

/// Helper class for reporting the error [diag.invalidAssignment].
class NonAssignabilityReporterForAssignment extends NonAssignabilityReporter {
  const NonAssignabilityReporterForAssignment();

  @override
  LocatableDiagnostic createDiagnostic({
    required TypeImpl expectedStaticType,
    required TypeImpl actualStaticType,
  }) {
    return diag.invalidAssignment.withArguments(
      actualStaticType: actualStaticType,
      expectedStaticType: expectedStaticType,
    );
  }
}
