<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.
     */

    companion object {

        fun parse(manifestPath: Path, toolchain: ElmToolchain): ElmProject {
            val inputStream = LocalFileSystem.getInstance().refreshAndFindFileByPath(manifestPath.toString())?.inputStream
                    ?: throw ProjectLoadException("Could not find file $manifestPath")
            return parse(inputStream, manifestPath, toolchain)
        }

        /**
            val p = stack.removeAt(0)
         *
         * @throws ProjectLoadException if the JSON cannot be parsed
         */
        fun parse(inputStream: InputStream, manifestPath: Path, toolchain: ElmToolchain): ElmProject {

            if (manifestPath.endsWith(ELM_LEGACY_JSON)) {
                val elmStuffPath = manifestPath.resolveSibling("elm-stuff")
     * 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
}
                            manifestPath = manifestPath,
                            elmVersion = dto.elmVersion,
                            dependencies = dto.dependencies.depsToPackages(toolchain),
                            testDependencies = dto.testDependencies.depsToPackages(toolchain),
                            sourceDirectories = dto.sourceDirectories
                    )
                }
    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>,
                            manifestPath = manifestPath,
                            elmVersion = dto.elmVersion,
                            dependencies = dto.dependencies.constraintDepsToPackages(toolchain),
                            testDependencies = dto.testDependencies.constraintDepsToPackages(toolchain),
                            sourceDirectories = listOf(Paths.get("src")),
                            name = dto.name,
                            version = dto.version,
 */
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"

private fun Map<String, Version>.depsToPackages(toolchain: ElmToolchain) =
        map { (name, version) ->
            loadPackage(toolchain, name, version)
        }

private fun Map<String, Constraint>.constraintDepsToPackages(toolchain: ElmToolchain) =
                    .firstOrNull()
                    ?: throw ProjectLoadException("Could not load $name ($constraint)")

            loadPackage(toolchain, name, version)
        }

fun loadPackage(toolchain: ElmToolchain, name: String, version: Version): ElmPackageProject {
    val manifestPath = toolchain.findPackageManifest(name, version)
            ?: throw ProjectLoadException("Could not load $name ($version): manifest not found")
    // TODO [kl] guard against circular dependencies
    val elmProject = ElmProject.parse(manifestPath, toolchain) as? ElmPackageProject
            ?: throw ProjectLoadException("Could not load $name ($version): expected a package!")

    return elmProject

<MSG> Do not attempt to load a dependency's test-dependencies (#115)


<DFF> @@ -84,10 +84,10 @@ sealed class ElmProject(
 
     companion object {
 
-        fun parse(manifestPath: Path, toolchain: ElmToolchain): ElmProject {
+        fun parse(manifestPath: Path, toolchain: ElmToolchain, ignoreTestDeps: Boolean = false): ElmProject {
             val inputStream = LocalFileSystem.getInstance().refreshAndFindFileByPath(manifestPath.toString())?.inputStream
                     ?: throw ProjectLoadException("Could not find file $manifestPath")
-            return parse(inputStream, manifestPath, toolchain)
+            return parse(inputStream, manifestPath, toolchain, ignoreTestDeps)
         }
 
         /**
@@ -95,7 +95,7 @@ sealed class ElmProject(
          *
          * @throws ProjectLoadException if the JSON cannot be parsed
          */
-        fun parse(inputStream: InputStream, manifestPath: Path, toolchain: ElmToolchain): ElmProject {
+        fun parse(inputStream: InputStream, manifestPath: Path, toolchain: ElmToolchain, ignoreTestDeps: Boolean = false): ElmProject {
 
             if (manifestPath.endsWith(ELM_LEGACY_JSON)) {
                 val elmStuffPath = manifestPath.resolveSibling("elm-stuff")
@@ -120,7 +120,7 @@ sealed class ElmProject(
                             manifestPath = manifestPath,
                             elmVersion = dto.elmVersion,
                             dependencies = dto.dependencies.depsToPackages(toolchain),
-                            testDependencies = dto.testDependencies.depsToPackages(toolchain),
+                            testDependencies = if (ignoreTestDeps) emptyList() else dto.testDependencies.depsToPackages(toolchain),
                             sourceDirectories = dto.sourceDirectories
                     )
                 }
@@ -139,7 +139,7 @@ sealed class ElmProject(
                             manifestPath = manifestPath,
                             elmVersion = dto.elmVersion,
                             dependencies = dto.dependencies.constraintDepsToPackages(toolchain),
-                            testDependencies = dto.testDependencies.constraintDepsToPackages(toolchain),
+                            testDependencies = if (ignoreTestDeps) emptyList() else dto.testDependencies.constraintDepsToPackages(toolchain),
                             sourceDirectories = listOf(Paths.get("src")),
                             name = dto.name,
                             version = dto.version,
@@ -222,7 +222,7 @@ private fun ExactDependenciesDTO.depsToPackages(toolchain: ElmToolchain) =
 
 private fun Map<String, Version>.depsToPackages(toolchain: ElmToolchain) =
         map { (name, version) ->
-            loadPackage(toolchain, name, version)
+            loadDependency(toolchain, name, version)
         }
 
 private fun Map<String, Constraint>.constraintDepsToPackages(toolchain: ElmToolchain) =
@@ -233,14 +233,17 @@ private fun Map<String, Constraint>.constraintDepsToPackages(toolchain: ElmToolc
                     .firstOrNull()
                     ?: throw ProjectLoadException("Could not load $name ($constraint)")
 
-            loadPackage(toolchain, name, version)
+            loadDependency(toolchain, name, version)
         }
 
-fun loadPackage(toolchain: ElmToolchain, name: String, version: Version): ElmPackageProject {
+fun loadDependency(toolchain: ElmToolchain, name: String, version: Version): ElmPackageProject {
     val manifestPath = toolchain.findPackageManifest(name, version)
             ?: throw ProjectLoadException("Could not load $name ($version): manifest not found")
     // TODO [kl] guard against circular dependencies
-    val elmProject = ElmProject.parse(manifestPath, toolchain) as? ElmPackageProject
+    // NOTE: we ignore the test dependencies of our dependencies because it is highly unlikely
+    // that they have been installed by Elm in the local package cache (the user would have
+    // to actually run the package's tests from within the package cache, which no one is going to do).
+    val elmProject = ElmProject.parse(manifestPath, toolchain, ignoreTestDeps = true) as? ElmPackageProject
             ?: throw ProjectLoadException("Could not load $name ($version): expected a package!")
 
     return elmProject

Do not attempt to load a dependency's test-dependencies (#115)