Repository: elm-tooling/elm-language-server
Pull request #372 (2020-11-23 05:15:48)

Type inference and diagnostic fixes
Some minor inference fixes and some diagnostics cleanup. 

Fixes inference bugs with the Task module (due to it being an effect module).

https://github.com/Razzeee/tree-sitter-elm/pull/66


Patch:
diff --git a/src/cancellation.ts b/src/cancellation.ts
index 8ff0e8a..7c05a28 100644
--- a/src/cancellation.ts
+++ b/src/cancellation.ts
@@ -207,7 +207,12 @@ export class ThrottledCancellationToken implements ICancellationToken {
     if (duration >= this.throttleWaitMilliseconds) {
       // Check no more than once every throttle wait milliseconds
       this.lastCancellationCheckTime = time;
-      return this.cancellationToken.isCancellationRequested;
+
+      try {
+        return this.cancellationToken.isCancellationRequested;
+      } catch {
+        //
+      }
     }
 
     return false;
diff --git a/src/elmWorkspace.ts b/src/elmWorkspace.ts
index 2a64de0..ea8631b 100644
--- a/src/elmWorkspace.ts
+++ b/src/elmWorkspace.ts
@@ -31,6 +31,46 @@ interface IFolder {
   isExposed: boolean;
 }
 
+type ElmJson = IElmApplicationJson | IElmPackageJson;
+
+interface IElmApplicationJson {
+  type: "application";
+  "source-directories": string[];
+  "elm-version": string;
+  dependencies: {
+    direct: {
+      [module: string]: string;
+    };
+    indirect: {
+      [module: string]: string;
+    };
+  };
+  "test-dependencies": {
+    direct: {
+      [module: string]: string;
+    };
+    indirect: {
+      [module: string]: string;
+    };
+  };
+}
+
+interface IElmPackageJson {
+  type: "package";
+  name: string;
+  summary: string;
+  license: string;
+  version: string;
+  "exposed-modules": string[];
+  "elm-version": string;
+  dependencies: {
+    [module: string]: string;
+  };
+  "test-dependencies": {
+    [module: string]: string;
+  };
+}
+
 export interface IElmWorkspace {
   init(progressCallback: (percent: number) => void): void;
   hasDocument(uri: URI): boolean;
@@ -46,14 +86,13 @@ export interface IElmWorkspace {
 }
 
 export interface IRootFolder {
-  uri: string;
   writeable: boolean;
   maintainerAndPackageName?: string;
 }
 
 export class ElmWorkspace implements IElmWorkspace {
-  private elmFolders: IRootFolder[] = [];
-  private forest: IForest = new Forest([]);
+  private elmFolders = new Map<string, IRootFolder>();
+  private forest: IForest = new Forest(new Map());
   private parser: Parser;
   private connection: Connection;
   private settings: Settings;
@@ -91,9 +130,9 @@ export class ElmWorkspace implements IElmWorkspace {
   }
 
   public getPath(uri: URI): string | undefined {
-    return this.elmFolders
-      .map((f) => f.uri)
-      .find((elmFolder) => uri.fsPath.startsWith(elmFolder));
+    return Array.from(this.elmFolders.keys()).find((elmFolder) =>
+      uri.fsPath.startsWith(elmFolder),
+    );
   }
 
   public getForest(synchronize = true): IForest {
@@ -159,30 +198,26 @@ export class ElmWorkspace implements IElmWorkspace {
     this.connection.console.info(`Reading elm.json from ${pathToElmJson}`);
     try {
       // Find elm files and feed them to tree sitter
-      const elmJson = require(pathToElmJson);
-      const type = elmJson.type;
-      if (type === "application") {
+      const elmJson = require(pathToElmJson) as ElmJson;
+      if (elmJson.type === "application") {
         elmJson["source-directories"].forEach((folder: string) => {
-          this.elmFolders.push({
+          this.elmFolders.set(path.resolve(this.rootPath.fsPath, folder), {
             maintainerAndPackageName: undefined,
-            uri: path.resolve(this.rootPath.fsPath, folder),
             writeable: true,
           });
         });
       } else {
-        this.elmFolders.push({
+        this.elmFolders.set(path.join(this.rootPath.fsPath, "src"), {
           maintainerAndPackageName: undefined,
-          uri: path.join(this.rootPath.fsPath, "src"),
           writeable: true,
         });
       }
-      this.elmFolders.push({
+      this.elmFolders.set(path.join(this.rootPath.fsPath, "tests"), {
         maintainerAndPackageName: undefined,
-        uri: path.join(this.rootPath.fsPath, "tests"),
         writeable: true,
       });
       this.connection.console.info(
-        `${this.elmFolders.length} source-dirs and test folders found`,
+        `${this.elmFolders.size} source-dirs and test folders found`,
       );
 
       const elmHome = this.findElmHome();
@@ -190,60 +225,30 @@ export class ElmWorkspace implements IElmWorkspace {
         elmVersion,
       )}/`;
       const dependencies: { [index: string]: string } =
-        type === "application"
+        elmJson.type === "application"
           ? {
               ...elmJson.dependencies.direct,
               ...elmJson.dependencies.indirect,
               ...elmJson["test-dependencies"].direct,
+              ...elmJson["test-dependencies"].indirect,
             }
           : { ...elmJson.dependencies, ...elmJson["test-dependencies"] };
-      if (type === "application") {
+      if (elmJson.type === "application") {
         for (const key in dependencies) {
           if (Object.prototype.hasOwnProperty.call(dependencies, key)) {
             const maintainer = key.substring(0, key.indexOf("/"));
             const packageName = key.substring(key.indexOf("/") + 1, key.length);
 
             const pathToPackageWithVersion = `${packagesRoot}${maintainer}/${packageName}/${dependencies[key]}`;
-            this.elmFolders.push({
+            this.elmFolders.set(pathToPackageWithVersion, {
               maintainerAndPackageName: `${maintainer}/${packageName}`,
-              uri: pathToPackageWithVersion,
               writeable: false,
             });
           }
         }
       } else {
-        for (const key in dependencies) {
-          if (Object.prototype.hasOwnProperty.call(dependencies, key)) {
-            const maintainer = key.substring(0, key.indexOf("/"));
-            const packageName = key.substring(key.indexOf("/") + 1, key.length);
-
-            const pathToPackage = `${packagesRoot}${maintainer}/${packageName}/`;
-            const readDir = await readdir(pathToPackage, "utf8");
-
-            const allVersionFolders = readDir.map((folderName) => {
-              return {
-                version: folderName,
-                versionPath: `${pathToPackage}${folderName}`,
-              };
-            });
-
-            const matchedFolder = utils.findDepVersion(
-              allVersionFolders,
-              dependencies[key],
-            );
-            const pathToPackageWithVersion = matchedFolder
-              ? `${matchedFolder.versionPath}`
-              : `${
-                  allVersionFolders[allVersionFolders.length - 1].versionPath
-                }`;
-
-            this.elmFolders.push({
-              maintainerAndPackageName: `${maintainer}/${packageName}`,
-              uri: pathToPackageWithVersion,
-              writeable: false,
-            });
-          }
-        }
+        // Resolve dependency tree recursively
+        await this.resolveDependencies(dependencies, packagesRoot);
       }
 
       const elmFilePaths = await this.findElmFilesInFolders(this.elmFolders);
@@ -282,17 +287,56 @@ export class ElmWorkspace implements IElmWorkspace {
     }
   }
 
+  private async resolveDependencies(
+    dependencies: { [index: string]: string },
+    packagesRoot: string,
+  ): Promise<void> {
+    for (const key in dependencies) {
+      const maintainer = key.substring(0, key.indexOf("/"));
+      const packageName = key.substring(key.indexOf("/") + 1, key.length);
+
+      const pathToPackage = `${packagesRoot}${maintainer}/${packageName}/`;
+      const readDir = await readdir(pathToPackage, "utf8");
+
+      const allVersionFolders = readDir.map((folderName) => {
+        return {
+          version: folderName,
+          versionPath: `${pathToPackage}${folderName}`,
+        };
+      });
+
+      const matchedFolder = utils.findDepVersion(
+        allVersionFolders,
+        dependencies[key],
+      );
+      const pathToPackageWithVersion = matchedFolder
+        ? `${matchedFolder.versionPath}`
+        : `${allVersionFolders[allVersionFolders.length - 1].versionPath}`;
+
+      if (!this.elmFolders.has(pathToPackageWithVersion)) {
+        this.elmFolders.set(pathToPackageWithVersion, {
+          maintainerAndPackageName: `${maintainer}/${packageName}`,
+          writeable: false,
+        });
+      }
+
+      // Resolve all dependencies for this dependency
+      const elmJsonPath = path.join(pathToPackageWithVersion, "elm.json");
+      const elmJson = require(elmJsonPath) as ElmJson;
+
+      if (elmJson.type === "package") {
+        await this.resolveDependencies(elmJson.dependencies, packagesRoot);
+      }
+    }
+  }
+
   private async findElmFilesInFolders(
-    elmFolders: {
-      uri: string;
-      writeable: boolean;
-      maintainerAndPackageName?: string;
-    }[],
+    elmFolders: Map<string, IRootFolder>,
   ): Promise<IFolder[]> {
     let elmFilePathPromises: Promise<IFolder[]>[] = [];
-    for (const element of elmFolders) {
+    for (const [uri, element] of elmFolders) {
       elmFilePathPromises = elmFilePathPromises.concat(
-        this.findElmFilesInFolder(element),
+        this.findElmFilesInFolder({ uri, ...element }),
       );
     }
     return (await Promise.all(elmFilePathPromises)).reduce(
diff --git a/src/forest.ts b/src/forest.ts
index 12642a6..d9a89bc 100644
--- a/src/forest.ts
+++ b/src/forest.ts
@@ -50,7 +50,13 @@ export class Forest implements IForest {
   >();
   public moduleToUrisMap: Map<string, string[]> = new Map<string, string[]>();
 
-  constructor(private elmFolders: IRootFolder[]) {}
+  private elmFolders: { uri: string; writeable: boolean }[] = [];
+
+  constructor(elmFolders: ReadonlyMap<string, IRootFolder>) {
+    elmFolders.forEach(({ writeable }, uri) =>
+      this.elmFolders.push({ writeable, uri }),
+    );
+  }
 
   public getTree(uri: string): Tree | undefined {
     return this.getByUri(uri)?.tree;
diff --git a/src/imports.ts b/src/imports.ts
index 5f48d77..b3fead6 100644
--- a/src/imports.ts
+++ b/src/imports.ts
@@ -45,11 +45,7 @@ export class Imports extends MultiMap<string, IImport> {
     ];
 
     importNodes.forEach((importNode) => {
-      const moduleName = TreeUtils.findFirstNamedChildOfType(
-        "upper_case_qid",
-        importNode,
-      )?.text;
-
+      const moduleName = importNode.childForFieldName("moduleName")?.text;
       if (moduleName) {
         const uri = treeContainer.resolvedModules?.get(moduleName);
 
@@ -83,16 +79,10 @@ export class Imports extends MultiMap<string, IImport> {
                 foundModule.maintainerAndPackageName,
               ).forEach((imp) => result.set(imp.alias, imp));
 
-              const exposingList = TreeUtils.findFirstNamedChildOfType(
-                "exposing_list",
-                importNode,
-              );
+              const exposingList = importNode.childForFieldName("exposing");
 
               if (exposingList) {
-                const doubleDot = TreeUtils.findFirstNamedChildOfType(
-                  "double_dot",
-                  exposingList,
-                );
+                const doubleDot = exposingList.childForFieldName("doubleDot");
                 if (doubleDot) {
                   this.getAllExposedCompletions(
                     exposedFromRemoteModule,
diff --git a/src/providers/diagnostics/diagnosticsProvider.ts b/src/providers/diagnostics/diagnosticsProvider.ts
index f777a2b..8a33cbe 100644
--- a/src/providers/diagnostics/diagnosticsProvider.ts
+++ b/src/providers/diagnostics/diagnosticsProvider.ts
@@ -1,7 +1,6 @@
 import { container, injectable } from "tsyringe";
 import {
   CancellationToken,
-  CancellationTokenSource,
   Connection,
   Diagnostic,
   FileChangeType,
@@ -16,6 +15,7 @@ import { MultistepOperation } from "../../util/multistepOperation";
 import { IClientSettings } from "../../util/settings";
 import { TextDocumentEvents } from "../../util/textDocumentEvents";
 import { ASTProvider } from "../astProvider";
+import { DiagnosticsRequest } from "./diagnosticsRequest";
 import { ElmLsDiagnostics } from "./elmLsDiagnostics";
 import { ElmMakeDiagnostics } from "./elmMakeDiagnostics";
 import { DiagnosticKind, FileDiagnostics } from "./fileDiagnostics";
@@ -44,11 +44,6 @@ class PendingDiagnostics extends Map<string, number> {
   }
 }
 
-interface IPendingRequest {
-  token: CancellationTokenSource;
-  files: string[];
-}
-
 @injectable()
 export class DiagnosticsProvider {
   private elmMakeDiagnostics: ElmMakeDiagnostics;
@@ -62,7 +57,7 @@ export class DiagnosticsProvider {
   private elmWorkspaceMatcher: ElmWorkspaceMatcher<URI>;
   private documentEvents: TextDocumentEvents;
 
-  private pendingRequest: IPendingRequest | undefined;
+  private pendingRequest: DiagnosticsRequest | undefined;
   private pendingDiagnostics: PendingDiagnostics;
   private diagnosticsDelayer: Delayer<any>;
   private diagnosticsOperation: MultistepOperation;
@@ -153,7 +148,8 @@ export class DiagnosticsProvider {
     astProvider.onTreeChange(({ treeContainer, declaration }) => {
       if (!clientInitiatedDiagnostics) {
         if (this.pendingRequest) {
-          this.pendingRequest.token.cancel();
+          this.pendingRequest.cancel();
+          this.pendingRequest = undefined;
         }
 
         this.requestDiagnostics(treeContainer.uri);
@@ -188,7 +184,7 @@ export class DiagnosticsProvider {
       return f();
     }
 
-    this.pendingRequest.token.cancel();
+    this.pendingRequest.cancel();
     this.pendingRequest = undefined;
     const result = f();
 
@@ -201,7 +197,7 @@ export class DiagnosticsProvider {
       const orderedFiles = this.pendingDiagnostics.getOrderedFiles();
 
       if (this.pendingRequest) {
-        this.pendingRequest.token.cancel();
+        this.pendingRequest.cancel();
 
         this.pendingRequest.files.forEach((file) => {
           if (!orderedFiles.includes(file)) {
@@ -221,10 +217,15 @@ export class DiagnosticsProvider {
       });
 
       if (orderedFiles.length) {
-        this.pendingRequest = {
-          token: this.getDiagnosticsWithCancellation(orderedFiles, 0),
-          files: orderedFiles,
-        };
+        const request = (this.pendingRequest = DiagnosticsRequest.execute(
+          this.getDiagnostics.bind(this),
+          orderedFiles,
+          () => {
+            if (request === this.pendingRequest) {
+              this.pendingRequest = undefined;
+            }
+          },
+        ));
       }
 
       this.pendingDiagnostics.clear();
@@ -268,85 +269,77 @@ export class DiagnosticsProvider {
     });
   }
 
-  private getDiagnosticsWithCancellation(
-    files: string[],
-    delay: number,
-  ): CancellationTokenSource {
-    const cancellationToken = new CancellationTokenSource();
-
-    this.getDiagnostics(files, delay, cancellationToken.token);
-
-    return cancellationToken;
-  }
-
   private getDiagnostics(
     files: string[],
     delay: number,
     cancellationToken: CancellationToken,
-  ): void {
+  ): Promise<void> {
     const followMs = Math.min(delay, 200);
 
-    this.diagnosticsOperation.startNew(
-      cancellationToken,
-      (next) => {
-        const seq = this.changeSeq;
-
-        let index = 0;
-        const goNext = (): void => {
-          index++;
-          if (files.length > index) {
-            next.delay(followMs, checkOne);
-          }
-        };
+    return new Promise((resolve) =>
+      this.diagnosticsOperation.startNew(
+        cancellationToken,
+        (next) => {
+          const seq = this.changeSeq;
+
+          let index = 0;
+          const goNext = (): void => {
+            index++;
+            if (files.length > index) {
+              next.delay(followMs, checkOne);
+            }
+          };
 
-        const checkOne = async (): Promise<void> => {
-          if (this.changeSeq !== seq) {
-            return;
-          }
+          const checkOne = async (): Promise<void> => {
+            if (this.changeSeq !== seq) {
+              return;
+            }
 
-          const uri = files[index];
-          const workspace = this.elmWorkspaceMatcher.getElmWorkspaceFor(
-            URI.parse(uri),
-          );
+            const uri = files[index];
+            const workspace = this.elmWorkspaceMatcher.getElmWorkspaceFor(
+              URI.parse(uri),
+            );
 
-          const treeContainer = workspace.getForest().getByUri(uri);
+            const treeContainer = workspace.getForest().getByUri(uri);
 
-          if (!treeContainer) {
-            goNext();
-            return;
-          }
-
-          this.updateDiagnostics(
-            uri,
-            DiagnosticKind.TypeInference,
-            await this.typeInferenceDiagnostics.getDiagnosticsForFile(
-              treeContainer,
-              workspace,
-              cancellationToken,
-            ),
-          );
-
-          if (this.changeSeq !== seq) {
-            return;
-          }
+            if (!treeContainer) {
+              goNext();
+              return;
+            }
 
-          next.immediate(() => {
             this.updateDiagnostics(
               uri,
-              DiagnosticKind.ElmLS,
-              this.elmLsDiagnostics.createDiagnostics(treeContainer, workspace),
+              DiagnosticKind.TypeInference,
+              await this.typeInferenceDiagnostics.getDiagnosticsForFile(
+                treeContainer,
+                workspace,
+                cancellationToken,
+              ),
             );
-            goNext();
-          });
-        };
 
-        if (files.length > 0 && this.changeSeq === seq) {
-          next.delay(delay, checkOne);
-        }
-      },
-      () => {
-        //
-      },
+            if (this.changeSeq !== seq) {
+              return;
+            }
+
+            next.immediate(() => {
+              this.updateDiagnostics(
+                uri,
+                DiagnosticKind.ElmLS,
+                this.elmLsDiagnostics.createDiagnostics(
+                  treeContainer,
+                  workspace,
+                ),
+              );
+              goNext();
+            });
+          };
+
+          if (files.length > 0 && this.changeSeq === seq) {
+            next.delay(delay, checkOne);
+          }
+        },
+        resolve,
+      ),
     );
   }
 
diff --git a/src/providers/diagnostics/diagnosticsRequest.ts b/src/providers/diagnostics/diagnosticsRequest.ts
new file mode 100644
index 0000000..f60416b
--- /dev/null
+++ b/src/providers/diagnostics/diagnosticsRequest.ts
@@ -0,0 +1,52 @@
+import {
+  CancellationToken,
+  CancellationTokenSource,
+} from "vscode-languageserver";
+
+export class DiagnosticsRequest {
+  public static execute(
+    requestor: (
+      files: string[],
+      delay: number,
+      cancellationToken: CancellationToken,
+    ) => Promise<void>,
+    files: string[],
+    onDone: () => void,
+  ): DiagnosticsRequest {
+    return new DiagnosticsRequest(requestor, files, onDone);
+  }
+
+  private done = false;
+  private readonly token: CancellationTokenSource = new CancellationTokenSource();
+
+  private constructor(
+    requestor: (
+      files: string[],
+      delay: number,
+      cancellationToken: CancellationToken,
+    ) => Promise<void>,
+    public readonly files: string[],
+    onDone: () => void,
+  ) {
+    if (!files.length) {
+      this.done = true;
+      onDone();
+    } else {
+      requestor(files, 0, this.token.token).finally(() => {
+        if (this.done) {
+          return;
+        }
+        this.done = true;
+        onDone();
+      });
+    }
+  }
+
+  public cancel(): void {
+    if (!this.done) {
+      this.token.cancel();
+    }
+
+    this.token.dispose();
+  }
+}
diff --git a/src/providers/diagnostics/typeInferenceDiagnostics.ts b/src/providers/diagnostics/typeInferenceDiagnostics.ts
index c302abe..3d38828 100644
--- a/src/providers/diagnostics/typeInferenceDiagnostics.ts
+++ b/src/providers/diagnostics/typeInferenceDiagnostics.ts
@@ -54,7 +54,7 @@ export class TypeInferenceDiagnostics {
     const allTopLevelFunctions =
       TreeUtils.findAllTopLevelFunctionDeclarations(treeContainer.tree) ?? [];
 
-    return new Promise((resolve) => {
+    return new Promise((resolve, reject) => {
       this.operation.startNew(
         cancellationToken,
         (next) => {
@@ -70,7 +70,7 @@ export class TypeInferenceDiagnostics {
 
           const checkOne = (): void => {
             if (this.changeSeq !== seq) {
-              return;
+              reject();
             }
 
             diagnostics.push(
@@ -89,7 +89,13 @@ export class TypeInferenceDiagnostics {
             next.immediate(checkOne);
           }
         },
-        () => resolve(diagnostics),
+        () => {
+          if (diagnostics.length === 0) {
+            console.log();
+          }
+          resolve(diagnostics);
+        },
+        reject,
       );
     });
   }
diff --git a/src/util/multistepOperation.ts b/src/util/multistepOperation.ts
index 43bd162..a5a4d25 100644
--- a/src/util/multistepOperation.ts
+++ b/src/util/multistepOperation.ts
@@ -15,6 +15,7 @@ export class MultistepOperation implements INextStep {
   private immediateId: NodeJS.Immediate | undefined;
   private cancellationToken: CancellationToken | undefined;
   private done: (() => void) | undefined;
+  private cancelled: (() => void) | undefined;
 
   constructor(private connection: Connection) {}
 
@@ -22,19 +23,22 @@ export class MultistepOperation implements INextStep {
     cancellationToken: CancellationToken,
     action: (next: INextStep) => void,
     done: () => void,
+    cancelled?: () => void,
   ): void {
     this.complete();
     this.cancellationToken = cancellationToken;
     this.done = done;
+    this.cancelled = cancelled;
     this.executeAction(action);
   }
 
   private complete(): void {
     if (this.done !== undefined) {
       this.done();
-      this.cancellationToken = undefined;
       this.done = undefined;
     }
+
+    this.cancellationToken = undefined;
     this.setTimerHandle(undefined);
     this.setImmediateId(undefined);
   }
@@ -59,19 +63,30 @@ export class MultistepOperation implements INextStep {
 
   private executeAction(action: (next: INextStep) => void): void {
     let stop = false;
+    let cancelled = false;
     try {
       if (this.cancellationToken?.isCancellationRequested) {
         stop = true;
+        cancelled = true;
       } else {
         action(this);
       }
     } catch (e) {
-      stop = true;
       // ignore cancellation request
       if (!(e instanceof OperationCanceledException)) {
         this.connection.console.error(`${e} delayed processing of request`);
       }
+
+      stop = true;
+      cancelled = true;
     }
+
+    if (cancelled && this.cancelled) {
+      this.cancelled();
+      this.cancelled = undefined;
+      return;
+    }
+
     if (stop || !this.hasPendingWork()) {
       this.complete();
     }
diff --git a/src/util/types/expressionTree.ts b/src/util/types/expressionTree.ts
index c9e4f6c..4aea5d5 100644
--- a/src/util/types/expressionTree.ts
+++ b/src/util/types/expressionTree.ts
@@ -345,6 +345,7 @@ export function mapSyntaxNodeToExpression(
             functionCallExpr.target = target;
             functionCallExpr.args = node.children
               .slice(1)
+              .filter((n) => !n.type.includes("comment"))
               .map(mapSyntaxNodeToExpression) as Expression[];
 
             addTime("functionCallExpr", performance.now() - start);
@@ -823,14 +824,13 @@ export function mapTypeAnnotation(typeAnnotation: ETypeAnnotation): void {
 
 export function findDefinition(
   e: SyntaxNode | undefined | null,
-  uri: string,
   elmWorkspace: IElmWorkspace,
 ): { expr: Expression; uri: string } | undefined {
   if (!e) {
     return;
   }
 
-  const treeContainer = elmWorkspace.getForest().getByUri(uri);
+  const treeContainer = elmWorkspace.getForest().getByUri(e.tree.uri);
 
   if (!treeContainer) {
     return;
diff --git a/src/util/types/typeChecker.ts b/src/util/types/typeChecker.ts
index 0eda53a..ba81bc0 100644
--- a/src/util/types/typeChecker.ts
+++ b/src/util/types/typeChecker.ts
@@ -231,17 +231,14 @@ export function createTypeChecker(workspace: IElmWorkspace): TypeChecker {
         "getDiagnosticsFromDeclaration must take a node of type value_declaraion",
       );
     }
-    try {
-      return InferenceScope.valueDeclarationInference(
-        mapSyntaxNodeToExpression(valueDeclaration) as EValueDeclaration,
-        valueDeclaration.tree.uri,
-        workspace,
-        new Set(),
-        cancellationToken,
-      ).diagnostics;
-    } catch {
-      return [];
-    }
+
+    return InferenceScope.valueDeclarationInference(
+      mapSyntaxNodeToExpression(valueDeclaration) as EValueDeclaration,
+      valueDeclaration.tree.uri,
+      workspace,
+      new Set(),
+      cancellationToken,
+    ).diagnostics;
   }
 
   function getAllImports(treeContainer: ITreeContainer): Imports {
diff --git a/src/util/types/typeExpression.ts b/src/util/types/typeExpression.ts
index f3e9e43..c986fbf 100644
--- a/src/util/types/typeExpression.ts
+++ b/src/util/types/typeExpression.ts
@@ -326,7 +326,6 @@ export class TypeExpression {
   private typeVariableType(typeVariable: ETypeVariable): Type {
     const definition = findDefinition(
       typeVariable.firstNamedChild,
-      this.uri,
       this.workspace,
     );
 
@@ -397,11 +396,8 @@ export class TypeExpression {
       fieldExpressions,
     );
 
-    const baseTypeDefinition = findDefinition(
-      record.baseType,
-      this.uri,
-      this.workspace,
-    )?.expr;
+    const baseTypeDefinition = findDefinition(record.baseType, this.workspace)
+      ?.expr;
 
     const baseType = baseTypeDefinition
       ? this.getTypeVar(baseTypeDefinition)
@@ -433,7 +429,6 @@ export class TypeExpression {
 
     const definition = findDefinition(
       typeRef.firstNamedChild?.lastNamedChild,
-      this.uri,
       this.workspace,
     );
 
diff --git a/src/util/types/typeInference.ts b/src/util/types/typeInference.ts
index d791779..5c5bdb2 100644
--- a/src/util/types/typeInference.ts
+++ b/src/util/types/typeInference.ts
@@ -942,11 +942,11 @@ export class InferenceScope {
 
   private inferChildDeclaration(
     declaration: EValueDeclaration,
-    activeScopes = new Set(this.activeScopes.values()),
+    activeScopes = this.activeScopes,
   ): InferenceResult {
     const result = this.inferChild(
       (inference) => inference.inferDeclaration(declaration, false),
-      activeScopes,
+      new Set(activeScopes.values()),
     );
 
     this.resolvedDeclarations.set(declaration, result.type);
@@ -1024,11 +1024,8 @@ export class InferenceScope {
 
   private inferReferenceElement(e: Expression): Type {
     const definition =
-      findDefinition(
-        e.firstNamedChild?.lastNamedChild,
-        this.uri,
-        this.elmWorkspace,
-      ) ?? findDefinition(e.firstNamedChild, this.uri, this.elmWorkspace);
+      findDefinition(e.firstNamedChild?.lastNamedChild, this.elmWorkspace) ??
+      findDefinition(e.firstNamedChild, this.elmWorkspace);
 
     if (!definition) {
       this.diagnostics.push(missingValueError(e));
@@ -1394,7 +1391,7 @@ export class InferenceScope {
   ): [Type, IOperatorPrecedence] {
     const start = performance.now();
     // Find operator reference
-    const definition = findDefinition(e, this.uri, this.elmWorkspace);
+    const definition = findDefinition(e, this.elmWorkspace);
 
     const opDeclaration = mapSyntaxNodeToExpression(definition?.expr.parent);
 
@@ -1456,7 +1453,6 @@ export class InferenceScope {
     // Find operator reference
     const definition = findDefinition(
       operatorFunction.operator,
-      this.uri,
       this.elmWorkspace,
     );
 
@@ -1580,8 +1576,8 @@ export class InferenceScope {
   private inferLambda(lambaExpr: EAnonymousFunctionExpr): Type {
     return this.inferChild(
       (inference) => inference.lambdaInference(lambaExpr),
-      undefined,
-      true,
+      /* activeScopes */ undefined,
+      /* recursionAllowed */ true,
     ).type;
   }
 
@@ -1916,12 +1912,13 @@ export class InferenceScope {
   ): void {
     const variant = findDefinition(
       unionPattern.constructor.lastNamedChild,
-      this.uri,
       this.elmWorkspace,
     );
 
     if (variant?.expr.nodeType !== "UnionVariant") {
-      throw new Error("Could not find UnionVariant for " + unionPattern.text);
+      throw new Error(
+        "Could not find UnionVariant for " + unionPattern.constructor.text,
+      );
     }
 
     const variantType = TypeExpression.unionVariantInference(
diff --git a/src/util/types/typeReplacement.ts b/src/util/types/typeReplacement.ts
index 261d1b8..178c463 100644
--- a/src/util/types/typeReplacement.ts
+++ b/src/util/types/typeReplacement.ts
@@ -18,8 +18,10 @@ export function resetReplaceTime(): void {
 }
 
 export class TypeReplacement {
+  private cachedReplacements = new Map<TVar, Type>();
+
   constructor(
-    private replacements: Map<TVar, Type | [boolean, Type]>,
+    private replacements: ReadonlyMap<TVar, Type>,
     private freshen: boolean,
     private keepRecordsMutable: boolean,
     private varsToRemainRigid?: TVar[],
@@ -219,6 +221,12 @@ export class TypeReplacement {
   }
 
   private getReplacement(key: TVar): Type | undefined {
+    const cachedReplacement = this.cachedReplacements.get(key);
+
+    if (cachedReplacement) {
+      return cachedReplacement;
+    }
+
     const replacement = this.replacements.get(key);
 
     if (!replacement) {
@@ -230,7 +238,7 @@ export class TypeReplacement {
           return undefined;
         } else {
           const newVar = TVar(key.name);
-          this.replacements.set(key, [true, newVar]);
+          this.cachedReplacements.set(key, newVar);
           return newVar;
         }
       }
@@ -238,21 +246,8 @@ export class TypeReplacement {
       return undefined;
     }
 
-    let hasBeenAccessed = false;
-    let storedType;
-    if (Array.isArray(replacement)) {
-      hasBeenAccessed = replacement[0];
-      storedType = replacement[1];
-    } else {
-      storedType = replacement;
-    }
-
-    if (hasBeenAccessed) {
-      return storedType;
-    }
-
-    const replacedType = this.replace(storedType);
-    this.replacements.set(key, [true, replacedType]);
+    const replacedType = this.replace(replacement);
+    this.cachedReplacements.set(key, replacedType);
     return replacedType;
   }
 }


Test patch:
diff --git a/test/typeInference.test.ts b/test/typeInference.test.ts
index 82a74dc..acc0505 100644
--- a/test/typeInference.test.ts
+++ b/test/typeInference.test.ts
@@ -599,4 +599,42 @@ world =
 `;
     await testTypeInference(basicsSources + source, "Html msg");
   });
+
+  test("var accessed multiple times", async () => {
+    const source = `
+--@ Test.elm
+module Test exposing (..)
+
+add a b =
+    a + b
+
+func a =
+--^
+    if a == 1 then
+        a + 1
+    else
+        add a 1
+`;
+    await testTypeInference(basicsSources + source, "number -> number");
+  });
+
+  test("effect module", async () => {
+    const source = `
+--@ Effect.elm
+effect module Effect where { command = MyCmd } exposing (func)
+
+func a b =
+    a + b
+
+--@ Test.elm
+module Test exposing (..)
+
+import Effect
+
+func a =
+--^
+    Effect.func a 1
+`;
+    await testTypeInference(basicsSources + source, "number -> number");
+  });
 });
diff --git a/test/utils/mockElmWorkspace.ts b/test/utils/mockElmWorkspace.ts
index 5eaae33..1276e28 100644
--- a/test/utils/mockElmWorkspace.ts
+++ b/test/utils/mockElmWorkspace.ts
@@ -20,7 +20,7 @@ import {
 export const baseUri = Path.join(__dirname, "../sources/src/");
 
 export class MockElmWorkspace implements IElmWorkspace {
-  private forest: IForest = new Forest([]);
+  private forest: IForest = new Forest(new Map());
   private parser: Parser;
   private typeCache = new TypeCache();
   private possibleImportsCache = new PossibleImportsCache();