<NME> ElmProject.kt
<BEF> package org.elm.workspace

import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.util.indexing.LightDirectoryIndex
import java.nio.file.Path
import java.nio.file.Paths


/**
 * The logical representation of an Elm project. An Elm project can be an application
 * or a package, and it specifies its dependencies.
 *
 * @param manifestPath The location of the manifest file (e.g. `elm.json`). Uniquely identifies a project.
 * @param dependencies Additional Elm packages that this project depends on directly
 * @param testDependencies Additional Elm packages that this project's **tests** depends on directly
 * @param sourceDirectories The relative paths to one-or-more directories containing Elm source files belonging to this project.
 * @param testsRelativeDirPath The path to the directory containing unit tests, relative to the [projectDirPath].
 * Typically this will be "tests": see [testsDirPath] for more info.
 */
sealed class ElmProject(
        val manifestPath: Path,
        val dependencies: List<ElmPackageProject>,
        val testDependencies: List<ElmPackageProject>,
        val sourceDirectories: List<Path>,
        testsRelativeDirPath: String
) : UserDataHolderBase() {

    /**
     * The path to the directory containing the Elm project JSON file.
     */
    val projectDirPath: Path = manifestPath.parent

    /**
     * The path to the directory containing unit tests, relative to the [projectDirPath]. Typically this will be "tests":
     * see [testsDirPath] for more info.
     *
     * Note that this path is normalized (see [Path.normalize]) so can safely be compared to [DEFAULT_TESTS_DIR_NAME].
     * For example, if the user specifies a value such as `"./foo/"` in some config file, this property will return `"foo"`.
     */
    val testsRelativeDirPath = Paths.get(testsRelativeDirPath).normalize().toString()

    /**
     * The path to the directory containing unit tests.
     *
     * For packages this will be a directory called "tests", as elm-test requires packages to have tests in a top-level
     * "tests" directory. For applications the default behaviour is the same as for packages, but optionally tests can
     * be put in some other directory, as long as when elm-test is called, the path to those tests is specified as a
     * cmd-line argument.
     */
    val testsDirPath: Path = projectDirPath.resolve(this.testsRelativeDirPath)

    /**
     * A flag indicating whether this project use a custom folder for unit tests (i.e. where the tests are any folder other
     * than the default `"tests"`).
     */
    val isCustomTestsDir = this.testsRelativeDirPath != DEFAULT_TESTS_DIR_NAME

    /**
     * A name which can be shown in the UI. Note that while Elm packages have user-assigned
     * names, applications do not. Thus, in order to cover both cases, we use the name
     * of the parent directory.
     */
    val presentableName: String =
            projectDirPath.fileName?.toString() ?: "UNKNOWN"

    /**
     * Returns the absolute paths for each source directory.
     *
     * @see sourceDirectories
     */
    val absoluteSourceDirectories: List<Path> =
            sourceDirectories.map { projectDirPath.resolve(it).normalize() }

    /**
     * Returns all the source directories, i.e. the [absoluteSourceDirectories] and the [testsDirPath].
     */
    val allSourceDirs: Sequence<Path> =
            absoluteSourceDirectories.asSequence() + sequenceOf(testsDirPath)

    /**
     * Returns all packages which this project depends on directly, whether it be for normal,
     * production code or for tests.
     */
    val allDirectDeps: Sequence<ElmPackageProject> =
            sequenceOf(dependencies, testDependencies).flatten()

    /**
     * Returns the direct and indirect dependencies of the receiver, recursively.
                is ElmPackageProject -> elmVersion.contains(version.xyz)
            }

    companion object {

        fun parse(manifestPath: Path, repo: ElmPackageRepository, ignoreTestDeps: Boolean = false): ElmProject {

    /**
     * Returns true if this project is compatible with Elm compiler [version].
     *
     * This is a looser form of a version check that allows for Elm compiler versions that include
     * alpha/beta/rc suffixes. e.g. "0.19.1-alpha-4"
     */
    fun isCompatibleWith(version: Version) =
            when (this) {
                is ElmApplicationProject -> elmVersion.xyz == version.xyz
                is LamderaApplicationProject -> elmVersion.xyz == version.xyz
                is ElmPackageProject -> elmVersion.contains(version.xyz)
                is ElmReviewProject -> elmVersion.xyz == version.xyz
            }

    /**
     * Return `true` iff this package is the core package for the current version of Elm.
     */
    open fun isCore(): Boolean = false
}


class ElmReviewProject(
    manifestPath: Path,
    val elmVersion: Version,
    dependencies: List<ElmPackageProject>,
    testDependencies: List<ElmPackageProject>,
    sourceDirectories: List<Path>,
    testsRelativeDirPath: String = DEFAULT_TESTS_DIR_NAME
) : ElmProject(manifestPath, dependencies, testDependencies, sourceDirectories, testsRelativeDirPath)

/**
 * Represents an Elm application
 */
class ElmApplicationProject(
        manifestPath: Path,
        val elmVersion: Version,
        dependencies: List<ElmPackageProject>,
        testDependencies: List<ElmPackageProject>,
        sourceDirectories: List<Path>,
        testsRelativeDirPath: String = DEFAULT_TESTS_DIR_NAME
) : ElmProject(manifestPath, dependencies, testDependencies, sourceDirectories, testsRelativeDirPath)


/**
 * Represents a Lamdera application
 */
class LamderaApplicationProject(
        manifestPath: Path,
        val elmVersion: Version,
        dependencies: List<ElmPackageProject>,
        testDependencies: List<ElmPackageProject>,
        sourceDirectories: List<Path>,
        testsRelativeDirPath: String = DEFAULT_TESTS_DIR_NAME
) : ElmProject(manifestPath, dependencies, testDependencies, sourceDirectories, testsRelativeDirPath)


// TODO ?
// class ElmLamderaProgramTest: ElmProject()


/**
 * Represents an Elm package/library
 */
class ElmPackageProject(
        manifestPath: Path,
        val elmVersion: Constraint,
        dependencies: List<ElmPackageProject>,
        testDependencies: List<ElmPackageProject>,
        sourceDirectories: List<Path>,
        val name: String,
        val version: Version,
        val exposedModules: List<String>
) : ElmProject(
        manifestPath = manifestPath,
        dependencies = dependencies,
        testDependencies = testDependencies,
        sourceDirectories = sourceDirectories,
        testsRelativeDirPath = DEFAULT_TESTS_DIR_NAME
) {
    override fun isCore(): Boolean =
            name == "elm/core"
}

/**
 * A dummy sentinel value because [LightDirectoryIndex] needs it.
 */
val noProjectSentinel = ElmApplicationProject(
        manifestPath = Paths.get("/elm.json"),
        elmVersion = Version(0, 0, 0),
        dependencies = emptyList(),
        testDependencies = emptyList(),
        sourceDirectories = emptyList()
)


/**
 * The default name of the directory which contains unit tests.
 */
const val DEFAULT_TESTS_DIR_NAME = "tests"
        val name: String,
        val version: Version,
        val exposedModules: List<String>
) : ElmProject(manifestPath, dependencies, testDependencies, sourceDirectories)


private fun ExactDependenciesDTO.depsToPackages(repo: ElmPackageRepository) =

<MSG> Optimize reference resolve (#585)

## ModuleScope

Although these loops were already cached, they were cached as a list of elements. Most reference implementations that used this scope would copy the returned list to an array (in `getVariants`), then filter that array down the the element that matches their name (in `resolveInner`).

The elements are now cached as both an array and a map of name to element, so now we don't make any copies, and `resolveInner` can do an O(1) map lookup. As an additional benefit, we only look up the element's `referenceName` once, where we were previously looking it up for each element in the array being searched.

## ImportScope

These results are now cached in a form similar to the form used in `ModuleScope`.

More importantly, calculating the values has been improved. Previously, calculating exposed names involved resolving the reference of each name in the exposing list. To do so, we would walk up from the reference to the file, then call a `ModuleScope.getDeclared*` function and filter the list for a matching name. But since all the exposed elements are defined in the same file, we now only call `ModuleScope` once and filter all the declarations in one pass. 

## ExpressionScope

This scope is very hot, but isn't amenable to caching due to the fact that the tree walk is unique for each reference, and retrieving a cached value is slow enough that caching intermediate results (e.g. `ElmValueDeclaration.declaredNames`) is a pessimization.

The main improvement to this scope is making the tree walk lazy so that e.g. resolving a reference to a function parameter doesn't need to call `ModuleScope.getVisibleValues`.

## GlobalScope

Since these results are the same for all files in a project, the results are now cached on the `ElmProject`. Additionally, the loops benefit from the improvements to `ModuleScope`

## Microoptimizations

I'm not going to list every change here, but there are a number of places that would walk over the same part of the Psi tree repeatedly. For example, `ElmUpperCaseQID.fillName`, which is a hot function, would gather all it's children into a list three times (once each for `isQualified`, `qualifierPrefix`, and `refName`). Now that function just calls `text` if it isn't a stub.
<DFF> @@ -89,6 +89,11 @@ sealed class ElmProject(
                 is ElmPackageProject -> elmVersion.contains(version.xyz)
             }
 
+    /**
+     * Return `true` iff this package is the core package for the current version of Elm.
+     */
+    open fun isCore(): Boolean = false
+
     companion object {
 
         fun parse(manifestPath: Path, repo: ElmPackageRepository, ignoreTestDeps: Boolean = false): ElmProject {
@@ -219,7 +224,11 @@ class ElmPackageProject(
         val name: String,
         val version: Version,
         val exposedModules: List<String>
-) : ElmProject(manifestPath, dependencies, testDependencies, sourceDirectories)
+) : ElmProject(manifestPath, dependencies, testDependencies, sourceDirectories) {
+    override fun isCore(): Boolean {
+        return (name == "elm/core" || name == "elm-lang/core") // TODO [drop 0.18] remove "elm-core/lang" clause
+    }
+}
 
 
 private fun ExactDependenciesDTO.depsToPackages(repo: ElmPackageRepository) =

Optimize reference resolve (#585)