/**
 * @name Incompatible dependency injection
 * @description Dependency-injecting a service of the wrong kind causes an error at runtime.
 * @kind problem
 * @problem.severity error
 * @precision high
 * @id js/angular/incompatible-service
 * @tags correctness
 *       frameworks/angularjs
 */

import javascript
import AngularJS

/**
 * Holds if `request` originates from a "service", "directive" or "filter" method call.
 */
predicate isServiceDirectiveOrFilterFunction(InjectableFunctionServiceRequest request) {
  exists(InjectableFunction f | f = request.getAnInjectedFunction() |
    exists(ServiceRecipeDefinition def | def.getAFactoryFunction() = f) or
    exists(FactoryRecipeDefinition def | def.getAFactoryFunction() = f) or
    exists(DirectiveDefinition def | def.getAFactoryFunction() = f) or
    exists(FilterDefinition def | def.getAFactoryFunction() = f)
  )
}

/**
 * Holds if `request` originates from a "controller" method call.
 */
predicate isControllerFunction(InjectableFunctionServiceRequest request) {
  exists(InjectableFunction f | f = request.getAnInjectedFunction() |
    exists(ControllerDefinition def | def.getAFactoryFunction() = f)
  )
}

/**
 * Holds if `request` originates from a "run" method call.
 */
predicate isRunMethod(InjectableFunctionServiceRequest request) {
  exists(InjectableFunction f | f = request.getAnInjectedFunction() |
    exists(RunMethodDefinition def | def.getRunMethod() = f)
  )
}

/**
 * Holds if `request` originates from a "config" method call.
 */
predicate isConfigMethod(InjectableFunctionServiceRequest request) {
  exists(InjectableFunction f | f = request.getAnInjectedFunction() |
    exists(ConfigMethodDefinition def | def.getConfigMethod() = f)
  )
}

/**
 * Holds if `kind` is a service kind that is compatible with all requests.
 */
predicate isWildcardKind(string kind) {
  kind = "type" or // builtins of kind "type" are usable everywhere
  kind = "decorator" // a decorator is always allowed, its decoratee might not be
}

/**
 * Holds if `request` is compatible with a service of kind `kind`.
 * (see https://docs.angularjs.org/guide/di)
 */
predicate isCompatibleRequestedService(InjectableFunctionServiceRequest request, string kind) {
  isWildcardKind(kind)
  or
  (
    isServiceDirectiveOrFilterFunction(request) or
    isRunMethod(request) or
    isControllerFunction(request)
  ) and
  (
    kind = "value" or
    kind = "service" or
    kind = "factory" or
    kind = "constant" or
    kind = "provider-value"
  )
  or
  isControllerFunction(request) and
  kind = "controller-only"
  or
  isConfigMethod(request) and
  (
    kind = "constant" or
    kind = "provider"
  )
}

/**
 * Gets the kind of a service named `serviceName`.
 */
string getServiceKind(InjectableFunctionServiceRequest request, string serviceName) {
  exists(ServiceReference id | id = request.getAServiceDefinition(serviceName) |
    id = getBuiltinServiceOfKind(result)
    or
    exists(CustomServiceDefinition custom |
      id = custom.getServiceReference() and
      (
        custom instanceof ValueRecipeDefinition and result = "value"
        or
        custom instanceof ServiceRecipeDefinition and result = "service"
        or
        custom instanceof FactoryRecipeDefinition and result = "factory"
        or
        custom instanceof DecoratorRecipeDefinition and result = "decorator"
        or
        custom instanceof ConstantRecipeDefinition and result = "constant"
        or
        (
          custom instanceof ProviderRecipeDefinition and
          if serviceName.matches("%Provider")
          then result = "provider"
          else result = "provider-value"
        )
      )
    )
  )
}

from
  InjectableFunctionServiceRequest request, string name, string componentDescriptionString,
  string compatibleWithString, string kind
where
  name = request.getAServiceName() and
  name != "$provide" and
  name != "$injector" and // special case: these services are always allowed
  kind = getServiceKind(request, name) and
  exists(request.getAServiceDefinition(name)) and // ignore unknown/undefined services
  not isCompatibleRequestedService(request, kind) and
  compatibleWithString = concat(string compatibleKind |
      isCompatibleRequestedService(request, compatibleKind) and
      not isWildcardKind(compatibleKind)
    |
      "'" + compatibleKind + "'", ", " order by compatibleKind
    ).regexpReplaceAll(",(?=[^,]+$)", " or") and
  (
    isServiceDirectiveOrFilterFunction(request) and
    componentDescriptionString = "Components such as services, directives, filters, and animations"
    or
    isControllerFunction(request) and
    componentDescriptionString = "Controllers"
    or
    isRunMethod(request) and
    componentDescriptionString = "Run methods"
    or
    isConfigMethod(request) and
    componentDescriptionString = "Config methods"
  )
select request,
  "'" + name + "' is a dependency of kind '" + kind + "', and cannot be injected here. " +
    componentDescriptionString + " can only be injected with dependencies of kind " +
    compatibleWithString + "."
