/**
 * Provides classes for working with name resolution of namespaces and types.
 */

import javascript

/**
 * A fully qualified name relative to a specific root,
 * usually referring to a TypeScript namespace or type.
 *
 * Canonical names are organized in a prefix tree, that is, the "parent"
 * of a canonical name is the name corresponding to its prefix.
 *
 * It is possible for two different canonical names to have the same
 * qualified name, namely if they are rooted in different scopes. The `hasQualifiedName`
 * predicates deal specifically with canonical names that are rooted in
 * the global scope or in the scope of a named module.
 *
 * This class is only populated when full TypeScript extraction is enabled.
 */
class CanonicalName extends @symbol {
  /**
   * Gets the parent of this canonical name, that is, the prefix of its qualified name.
   */
  CanonicalName getParent() { symbol_parent(this, result) }

  /**
   * Gets a child of this canonical name, i.e. an extension of its qualified name.
   */
  CanonicalName getAChild() { result.getParent() = this }

  /**
   * Gets the name without prefix.
   */
  string getName() { symbols(this, _, result) }

  /**
   * Gets the name of the external module represented by this canonical name, if any.
   */
  string getExternalModuleName() {
    symbol_module(this, result)
    or
    exists(PackageJSON pkg |
      getModule() = pkg.getMainModule() and
      result = pkg.getPackageName()
    )
  }

  /**
   * Gets the name of the global variable represented by this canonical name, if any.
   */
  string getGlobalName() { symbol_global(this, result) }

  /**
   * Gets the module represented by this canonical name, if such a module exists
   * and was extracted.
   */
  Module getModule() { ast_node_symbol(result, this) }

  /**
   * Holds if this canonical name has a child, i.e. an extension of its qualified name.
   */
  predicate hasChild() { exists(getAChild()) }

  /**
   * True if this has no parent.
   */
  predicate isRoot() { not exists(getParent()) }

  /**
   * Holds if this is the export namespace of a module.
   */
  predicate isModuleRoot() {
    exists(getModule()) or
    exists(getExternalModuleName())
  }

  /**
   * Holds if this is the export namespace of the given module.
   */
  predicate isModuleRoot(StmtContainer mod) { ast_node_symbol(mod, this) }

  /**
   * Holds if this canonical name is exported by its parent.
   *
   * Some entities, such as type parameters, have canonical names that are not
   * available through qualified name access.
   */
  predicate isExportedMember() { this instanceof @member_symbol }

  /** Holds if this has the given qualified name, rooted in the global scope. */
  predicate hasQualifiedName(string globalName) {
    globalName = getGlobalName()
    or
    exists(string prefix |
      getParent().hasQualifiedName(prefix) and
      globalName = prefix + "." + getName()
    )
  }

  /** Holds if this has the given qualified name, rooted in the given external module. */
  predicate hasQualifiedName(string moduleName, string exportedName) {
    moduleName = getParent().getExternalModuleName() and
    exportedName = getName()
    or
    exists(string prefix |
      getParent().hasQualifiedName(moduleName, prefix) and
      exportedName = prefix + "." + getName()
    )
  }

  /**
   * Gets the qualified name without the root.
   */
  string getRelativeName() {
    if getParent().isModuleRoot()
    then result = getName()
    else
      if exists(getGlobalName())
      then result = min(getGlobalName())
      else
        if exists(getParent())
        then result = getParent().getRelativeName() + "." + getName()
        else (
          not isModuleRoot() and result = getName()
        )
  }

  /**
   * Gets the outermost scope from which this type can be accessed by a qualified name (without using an `import`).
   *
   * This is typically the top-level of a module, but for locally declared types (e.g. types declared inside a function),
   * this is the container where the type is declared.
   */
  Scope getRootScope() {
    exists(CanonicalName root | root = getRootName() |
      if exists(root.getModule())
      then result = root.getModule().getScope()
      else
        if exists(root.getGlobalName())
        then result instanceof GlobalScope
        else result = getADefinitionNode().getContainer().getScope()
    )
  }

  private CanonicalName getRootName() {
    if exists(getGlobalName()) or isModuleRoot() or not exists(getParent())
    then result = this
    else result = getParent().getRootName()
  }

  private ExprOrStmt getADefinitionNode() {
    result = this.(TypeName).getADefinition() or
    result = this.(Namespace).getADefinition()
  }

  /**
   * Gets a string describing the root scope of this canonical name.
   */
  string describeRoot() {
    exists(CanonicalName root | root = getRootName() |
      if exists(root.getExternalModuleName())
      then result = "module '" + min(root.getExternalModuleName()) + "'"
      else
        if exists(root.getModule().getFile().getRelativePath())
        then result = root.getModule().getFile().getRelativePath()
        else
          if exists(root.getGlobalName())
          then result = "global scope"
          else
            if exists(root.getADefinitionNode())
            then
              exists(StmtContainer container |
                container = root.getADefinitionNode().getContainer()
              |
                result = container.(TopLevel).getFile().getRelativePath()
                or
                not container instanceof TopLevel and
                result = container.getLocation().toString()
              )
            else result = "unknown scope"
    )
  }

  /**
   * Gets the fully qualified name, followed by the name of its enclosing module or file.
   *
   * For example, the class `Server` from the `http` module has `toString` value: `Server in module 'http'`.
   *
   * In the file `foo.ts` below, the class `C` has `toString` value `C in foo.ts:2` because it is
   * defined in a lexical scope starting on line 2:
   * ```
   * namespace X {
   *   function doWork() {
   *     class C {}
   *   }
   * }
   * ```
   */
  string toString() {
    if isModuleRoot()
    then result = describeRoot()
    else result = getRelativeName() + " in " + describeRoot()
  }
}

/**
 * The canonical name for a type.
 */
class TypeName extends CanonicalName {
  TypeName() {
    exists(TypeReference ref | type_symbol(ref, this)) or
    exists(TypeDefinition def | ast_node_symbol(def, this)) or
    base_type_names(_, this) or
    base_type_names(this, _)
  }

  /**
   * Gets a definition of the type with this canonical name, if any.
   */
  TypeDefinition getADefinition() { ast_node_symbol(result, this) }

  /**
   * Gets a type annotation that refers to this type name.
   */
  TypeAccess getAnAccess() { result.getTypeName() = this }

  /**
   * Gets a type that refers to this canonical name.
   */
  TypeReference getATypeReference() { result.getTypeName() = this }

  /**
   * Gets a type named in the `extends` or `implements` clause of this type.
   */
  TypeName getABaseTypeName() { base_type_names(this, result) }

  /**
   * Gets the "self type" that refers to this canonical name.
   *
   * For a generic class or interface, the type arguments on the self type
   * all refer to the corresponding type parameters declared on that class or interface.
   *
   * For example, the "self type" of `Array` is `Array<T>`, where `T` refers to the
   * type parameter declared on the `Array` type.
   */
  TypeReference getType() { self_types(this, result) }
}

/**
 * The canonical name for a namespace.
 */
class Namespace extends CanonicalName {
  Namespace() {
    getAChild().isExportedMember() or
    exists(NamespaceDefinition def | ast_node_symbol(def, this)) or
    exists(NamespaceAccess ref | ast_node_symbol(ref, this))
  }

  /**
   * Gets a definition of the type with this canonical name, if any.
   */
  NamespaceDefinition getADefinition() { ast_node_symbol(result, this) }

  /**
   * Gets a part of a type annotation that refers to this namespace.
   */
  NamespaceAccess getAnAccess() { result.getNamespace() = this }

  /** Gets a namespace nested in this one. */
  Namespace getNamespaceMember(string name) {
    result.getParent() = this and
    result.getName() = name and
    result.isExportedMember()
  }

  /** Gets the type of the given name in this namespace, if any. */
  TypeName getTypeMember(string name) {
    result.getParent() = this and
    result.getName() = name and
    result.isExportedMember()
  }

  /**
   * Gets a namespace declaration or top-level whose exports contribute directly to this namespace.
   *
   * This includes containers that don't actually contain any `export` statements, but whose
   * exports would contribute to this namespace, if there were any.
   *
   * Does not include containers whose exports only contribute indirectly, through re-exports.
   * That is, there is at most one namespace associated with a given container.
   *
   * Namespaces defined by enum declarations have no exporting containers.
   */
  StmtContainer getAnExportingContainer() { ast_node_symbol(result, this) }
}

/**
 * The canonical name for a function.
 */
class CanonicalFunctionName extends CanonicalName {
  CanonicalFunctionName() {
    exists(Function fun | ast_node_symbol(fun, this)) or
    exists(InvokeExpr invoke | ast_node_symbol(invoke, this))
  }

  /**
   * Gets a function with this canonical name.
   */
  Function getADefinition() { ast_node_symbol(result, this) }

  /**
   * Gets the implementation of this function, if it exists.
   */
  Function getImplementation() { result = getADefinition() and result.hasBody() }
}
