Repository: elm-tooling/elm-language-server
Pull request #322 (2020-09-27 13:03:40)

Implement elm-analyse like diagnostics natively using tree-sitter
Had some fun using tree-sitter queries to start implementing some elm-analyse functionality. Currently, there is diagnostics for unsused import value and type, and unused import alias. Still working on more, just wanted to show the progress and how powerful using queries are. 

### Elm Analyse checks implemented:
- [x] Boolean Case Expression
- [x] Drop Concat Of Lists
- [x] Drop Cons Of Item And List
- [x] Fully Applied Operator as Prefix
- [x] Map Nothing To Nothing
- [x] No Top Level Signature
- [x] Single Field Record
- [x] Unnecessary List Concat
- [x] Unnecessary Port Module
- [x] Unused Import
- [x] Unused Import Alias
- [x] Unused Imported Variable
- [x] Unused Pattern Variable
- [x] Unused Top Level
- [x] Unused Type Alias
- [x] Unused Value Constructor
- [x] Unused Variable
- [x] Use Cons Over Concat

jmbockhorst: @Razzeee I added the list of checks that elm-analyse does and which ones I have implemented so far. Let me know or edit the list if there are some that you don't want to bring over. We can discuss, but I feel like there are some that we won't want to implement.
Razzeee: Agreed, I would however not try to do all in one PR, so that list might be best moved to an issue.

Patch:
diff --git a/src/providers/codeActionProvider.ts b/src/providers/codeActionProvider.ts
index 602de0b..88a71bc 100644
--- a/src/providers/codeActionProvider.ts
+++ b/src/providers/codeActionProvider.ts
@@ -18,6 +18,7 @@ import { RefactorEditUtils } from "../util/refactorEditUtils";
 import { IClientSettings, Settings } from "../util/settings";
 import { TreeUtils } from "../util/treeUtils";
 import { ElmAnalyseDiagnostics } from "./diagnostics/elmAnalyseDiagnostics";
+import { ElmDiagnostics } from "./diagnostics/elmDiagnostics";
 import { ElmMakeDiagnostics } from "./diagnostics/elmMakeDiagnostics";
 import { TypeInferenceDiagnostics } from "./diagnostics/typeInferenceDiagnostics";
 import { ExposeUnexposeHandler } from "./handlers/exposeUnexposeHandler";
@@ -29,6 +30,7 @@ export class CodeActionProvider {
   private elmAnalyse: ElmAnalyseDiagnostics | null = null;
   private elmMake: ElmMakeDiagnostics;
   private functionTypeAnnotationDiagnostics: TypeInferenceDiagnostics;
+  private elmDiagnostics: ElmDiagnostics;
   private clientSettings: IClientSettings;
 
   constructor() {
@@ -43,6 +45,7 @@ export class CodeActionProvider {
     this.functionTypeAnnotationDiagnostics = container.resolve<
       TypeInferenceDiagnostics
     >(TypeInferenceDiagnostics);
+    this.elmDiagnostics = container.resolve<ElmDiagnostics>(ElmDiagnostics);
     this.connection = container.resolve<IConnection>("Connection");
 
     this.onCodeAction = this.onCodeAction.bind(this);
@@ -72,12 +75,14 @@ export class CodeActionProvider {
     const typeAnnotation = this.functionTypeAnnotationDiagnostics.onCodeAction(
       params,
     );
+    const elmDiagnostics = this.elmDiagnostics.onCodeAction(params);
     return [
       ...this.getRefactorCodeActions(params, elmWorkspace),
       ...this.getTypeAnnotationCodeActions(params, elmWorkspace),
       ...analyse,
       ...make,
       ...typeAnnotation,
+      ...elmDiagnostics,
     ];
   }
 
diff --git a/src/providers/diagnostics/diagnosticsProvider.ts b/src/providers/diagnostics/diagnosticsProvider.ts
index 2ece945..b78ac70 100644
--- a/src/providers/diagnostics/diagnosticsProvider.ts
+++ b/src/providers/diagnostics/diagnosticsProvider.ts
@@ -14,6 +14,7 @@ import { ASTProvider } from "../astProvider";
 import { IElmWorkspace } from "../../elmWorkspace";
 import { debounce } from "ts-debounce";
 import { DiagnosticKind, FileDiagnostics } from "./fileDiagnostics";
+import { ElmDiagnostics } from "./elmDiagnostics";
 
 export interface IElmIssueRegion {
   start: { line: number; column: number };
@@ -35,6 +36,7 @@ export class DiagnosticsProvider {
   private elmMakeDiagnostics: ElmMakeDiagnostics;
   private elmAnalyseDiagnostics: ElmAnalyseDiagnostics | null = null;
   private typeInferenceDiagnostics: TypeInferenceDiagnostics;
+  private elmDiagnostics: ElmDiagnostics;
   private elmWorkspaceMatcher: ElmWorkspaceMatcher<{ uri: string }>;
   private currentDiagnostics: Map<string, FileDiagnostics>;
   private events: TextDocumentEvents;
@@ -57,6 +59,7 @@ export class DiagnosticsProvider {
     this.typeInferenceDiagnostics = container.resolve<TypeInferenceDiagnostics>(
       TypeInferenceDiagnostics,
     );
+    this.elmDiagnostics = container.resolve<ElmDiagnostics>(ElmDiagnostics);
     this.connection = container.resolve<IConnection>("Connection");
     this.events = container.resolve<TextDocumentEvents>(TextDocumentEvents);
     this.elmWorkspaceMatcher = new ElmWorkspaceMatcher((doc) =>
@@ -68,49 +71,90 @@ export class DiagnosticsProvider {
 
     this.currentDiagnostics = new Map<string, FileDiagnostics>();
     // register onChange listener if settings are not on-save only
-    void this.settings.getClientSettings().then(({ elmAnalyseTrigger }) => {
-      this.events.on("open", (d) =>
-        this.getDiagnostics(d, true, elmAnalyseTrigger),
+
+    const elmAnalyseTrigger = this.clientSettings.elmAnalyseTrigger;
+    this.events.on(
+      "open",
+      (d) => void this.getDiagnostics(d, true, elmAnalyseTrigger),
+    );
+    this.events.on(
+      "save",
+      (d) => void this.getDiagnostics(d, true, elmAnalyseTrigger),
+    );
+    this.connection.onDidChangeWatchedFiles((event) => {
+      const newDeleteEvents = event.changes
+        .filter((a) => a.type === FileChangeType.Deleted)
+        .map((a) => a.uri);
+      newDeleteEvents.forEach((uri) => {
+        this.deleteDiagnostics(uri);
+      });
+    });
+    if (this.elmAnalyseDiagnostics) {
+      this.elmAnalyseDiagnostics.on(
+        "new-diagnostics",
+        this.newElmAnalyseDiagnostics.bind(this),
       );
-      this.events.on("save", (d) =>
-        this.getDiagnostics(d, true, elmAnalyseTrigger),
+    }
+    if (elmAnalyseTrigger === "change") {
+      this.events.on(
+        "change",
+        (d) => void this.getDiagnostics(d, false, elmAnalyseTrigger),
       );
-      this.connection.onDidChangeWatchedFiles((event) => {
-        const newDeleteEvents = event.changes
-          .filter((a) => a.type === FileChangeType.Deleted)
-          .map((a) => a.uri);
-        newDeleteEvents.forEach((uri) => {
-          this.deleteDiagnostics(uri);
-        });
-      });
-      if (this.elmAnalyseDiagnostics) {
-        this.elmAnalyseDiagnostics.on(
-          "new-diagnostics",
-          this.newElmAnalyseDiagnostics.bind(this),
-        );
-      }
-      if (elmAnalyseTrigger === "change") {
-        this.events.on("change", (d) =>
-          this.getDiagnostics(d, false, elmAnalyseTrigger),
+    }
+
+    this.connection.onDidChangeConfiguration((params) => {
+      this.clientSettings = <IClientSettings>params.settings;
+
+      if (this.clientSettings.disableElmLSDiagnostics) {
+        this.currentDiagnostics.forEach((_, uri) =>
+          this.updateDiagnostics(uri, DiagnosticKind.ElmLS, []),
         );
+      } else {
+        this.workspaces.forEach((workspace) => {
+          workspace.getForest().treeIndex.forEach((treeContainer) => {
+            if (treeContainer.writeable) {
+              this.updateDiagnostics(
+                treeContainer.uri,
+                DiagnosticKind.ElmLS,
+                this.elmDiagnostics.createDiagnostics(
+                  treeContainer.tree,
+                  treeContainer.uri,
+                  workspace,
+                ),
+              );
+            }
+          });
+        });
       }
+    });
 
-      this.workspaces.forEach((workspace) => {
-        workspace.getForest().treeIndex.forEach((treeContainer) => {
-          if (treeContainer.writeable) {
-            const treeDiagnostics = this.typeInferenceDiagnostics.createDiagnostics(
-              treeContainer.tree,
-              treeContainer.uri,
-              workspace,
-            );
+    this.workspaces.forEach((workspace) => {
+      workspace.getForest().treeIndex.forEach((treeContainer) => {
+        if (treeContainer.writeable) {
+          const treeDiagnostics = this.typeInferenceDiagnostics.createDiagnostics(
+            treeContainer.tree,
+            treeContainer.uri,
+            workspace,
+          );
 
+          this.updateDiagnostics(
+            treeContainer.uri,
+            DiagnosticKind.TypeInference,
+            treeDiagnostics,
+          );
+
+          if (!this.clientSettings.disableElmLSDiagnostics) {
             this.updateDiagnostics(
               treeContainer.uri,
-              DiagnosticKind.TypeInference,
-              treeDiagnostics,
+              DiagnosticKind.ElmLS,
+              this.elmDiagnostics.createDiagnostics(
+                treeContainer.tree,
+                treeContainer.uri,
+                workspace,
+              ),
             );
           }
-        });
+        }
       });
 
       astProvider.onTreeChange(({ uri, tree }) => {
@@ -131,6 +175,14 @@ export class DiagnosticsProvider {
           DiagnosticKind.TypeInference,
           this.typeInferenceDiagnostics.createDiagnostics(tree, uri, workspace),
         );
+
+        if (!this.clientSettings.disableElmLSDiagnostics) {
+          this.updateDiagnostics(
+            uri,
+            DiagnosticKind.ElmLS,
+            this.elmDiagnostics.createDiagnostics(tree, uri, workspace),
+          );
+        }
       });
     });
   }
diff --git a/src/providers/diagnostics/elmDiagnostics.ts b/src/providers/diagnostics/elmDiagnostics.ts
new file mode 100644
index 0000000..970eb72
--- /dev/null
+++ b/src/providers/diagnostics/elmDiagnostics.ts
@@ -0,0 +1,960 @@
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+import {
+  CodeAction,
+  CodeActionKind,
+  CodeActionParams,
+  Diagnostic,
+  DiagnosticSeverity,
+  DiagnosticTag,
+  Range,
+  TextEdit,
+} from "vscode-languageserver";
+import { Language, Parser, SyntaxNode, Tree } from "web-tree-sitter";
+import { PositionUtil } from "../../positionUtil";
+import { container } from "tsyringe";
+import { Utils } from "../../util/utils";
+import { RefactorEditUtils } from "../../util/refactorEditUtils";
+import { ElmWorkspaceMatcher } from "../../util/elmWorkspaceMatcher";
+import { URI } from "vscode-uri";
+import { TreeUtils } from "../../util/treeUtils";
+import { IElmWorkspace } from "../../elmWorkspace";
+import { findType } from "../../util/types/typeInference";
+
+export class ElmDiagnostics {
+  ELMLS = "ElmLS";
+
+  private language: Language;
+  private elmWorkspaceMatcher: ElmWorkspaceMatcher<URI>;
+
+  constructor() {
+    this.language = container.resolve<Parser>("Parser").getLanguage();
+    this.elmWorkspaceMatcher = new ElmWorkspaceMatcher((uri) => uri);
+  }
+
+  public createDiagnostics = (
+    tree: Tree,
+    uri: string,
+    elmWorkspace: IElmWorkspace,
+  ): Diagnostic[] => {
+    try {
+      return [
+        ...this.getUnusedImportDiagnostics(tree),
+        ...this.getUnusedImportValueAndTypeDiagnostics(tree),
+        ...this.getUnusedImportAliasDiagnostics(tree),
+        ...this.getUnusedPatternVariableDiagnostics(tree),
+        ...this.getCaseBranchMapNothingToNothingDiagnostics(tree),
+        ...this.getBooleanCaseExpressionDiagnostics(tree),
+        ...this.getDropConcatOfListsDiagnostics(tree),
+        ...this.getDropConsOfItemAndListDiagnostics(tree),
+        ...this.getUseConsOverConcatDiagnostics(tree),
+        ...this.getSingleFieldRecordDiagnostics(tree, uri, elmWorkspace),
+        ...this.getUnnecessaryListConcatDiagnostics(tree),
+        ...this.getUnnecessaryPortModuleDiagnostics(tree),
+        ...this.getFullyAppliedOperatorAsPrefixDiagnostics(tree),
+        ...this.getUnusedTypeAliasDiagnostics(tree),
+        ...this.getUnusedValueConstructorDiagnostics(tree),
+      ];
+    } catch (e) {
+      console.log(e);
+    }
+    return [];
+  };
+
+  public onCodeAction(params: CodeActionParams): CodeAction[] {
+    const { uri } = params.textDocument;
+    const elmDiagnostics: Diagnostic[] = params.context.diagnostics.filter(
+      (diagnostic) => diagnostic.source === this.ELMLS,
+    );
+
+    return this.convertDiagnosticsToCodeActions(elmDiagnostics, uri);
+  }
+
+  private convertDiagnosticsToCodeActions(
+    diagnostics: Diagnostic[],
+    uri: string,
+  ): CodeAction[] {
+    const result: CodeAction[] = [];
+
+    const elmWorkspace = this.elmWorkspaceMatcher.getElmWorkspaceFor(
+      URI.parse(uri),
+    );
+
+    const forest = elmWorkspace.getForest();
+
+    const treeContainer = forest.getByUri(uri);
+
+    if (treeContainer) {
+      diagnostics.forEach((diagnostic) => {
+        if (diagnostic.code === "unused_imported_value") {
+          const node = TreeUtils.getNamedDescendantForPosition(
+            treeContainer.tree.rootNode,
+            diagnostic.range.start,
+          );
+
+          const importClause = TreeUtils.findParentOfType(
+            "import_clause",
+            node,
+          );
+
+          if (!importClause) {
+            return;
+          }
+
+          const moduleName = TreeUtils.findFirstNamedChildOfType(
+            "upper_case_qid",
+            importClause,
+          );
+
+          if (!moduleName) {
+            return;
+          }
+
+          const removeValueEdit = RefactorEditUtils.removeValueFromImport(
+            treeContainer.tree,
+            moduleName.text,
+            node.text,
+          );
+
+          if (removeValueEdit) {
+            result.push({
+              diagnostics: [diagnostic],
+              edit: {
+                changes: {
+                  [uri]: [removeValueEdit],
+                },
+              },
+              kind: CodeActionKind.QuickFix,
+              title: `Remove unused ${
+                node.type === "exposed_type" ? "type" : "value"
+              } \`${node.text}\``,
+            });
+          }
+        }
+
+        if (diagnostic.code === "unused_alias") {
+          const node = TreeUtils.getNamedDescendantForPosition(
+            treeContainer.tree.rootNode,
+            diagnostic.range.end,
+          );
+
+          result.push({
+            diagnostics: [diagnostic],
+            edit: {
+              changes: { [uri]: [TextEdit.del(diagnostic.range)] },
+            },
+            kind: CodeActionKind.QuickFix,
+            title: `Remove unused alias \`${node.text}\``,
+          });
+        }
+
+        if (diagnostic.code === "unused_pattern") {
+          const node = TreeUtils.getNamedDescendantForPosition(
+            treeContainer.tree.rootNode,
+            diagnostic.range.start,
+          );
+
+          const edit =
+            node.parent?.parent?.type === "record_pattern"
+              ? RefactorEditUtils.removeRecordPatternValue(node.parent)
+              : TextEdit.replace(diagnostic.range, "_");
+
+          result.push({
+            diagnostics: [diagnostic],
+            edit: {
+              changes: { [uri]: [edit] },
+            },
+            kind: CodeActionKind.QuickFix,
+            title: `Fix unused pattern \`${node.text}\``,
+          });
+        }
+      });
+    }
+    return result;
+  }
+
+  private getUnusedImportDiagnostics(tree: Tree): Diagnostic[] {
+    const diagnostics: Diagnostic[] = [];
+
+    const moduleImports = this.language
+      .query(
+        `
+        (import_clause 
+          (upper_case_qid) @moduleName
+        )
+        `,
+      )
+      .matches(tree.rootNode)
+      .map((match) => match.captures[0].node)
+      .filter(
+        (node) =>
+          node.nextNamedSibling?.type !== "exposing_list" &&
+          node.nextNamedSibling?.type !== "as_clause",
+      );
+
+    // Would need to adjust tree-sitter (use fields) to get a better query
+    moduleImports.forEach((moduleImport) => {
+      const references = this.language
+        .query(
+          `
+          (value_qid
+            (
+              (upper_case_identifier)
+              (dot)
+            )* @module.reference
+          )
+          (upper_case_qid
+            (
+              (upper_case_identifier)
+              (dot)
+            )* @module.reference
+          )
+          `,
+        )
+        .matches(tree.rootNode)
+        .filter(Utils.notUndefined.bind(this))
+        .filter(
+          (match) =>
+            match.captures.length > 0 &&
+            match.captures[0].node.parent?.type !== "import_clause",
+        )
+        .map((match) => match.captures.map((n) => n.node.text).join("."))
+        .filter((moduleReference) => moduleReference === moduleImport.text);
+
+      if (references.length === 0 && moduleImport.parent) {
+        diagnostics.push({
+          code: "unused_import",
+          range: this.getNodeRange(moduleImport.parent),
+          message: `Unused import \`${moduleImport.text}\``,
+          severity: DiagnosticSeverity.Warning,
+          source: this.ELMLS,
+          tags: [DiagnosticTag.Unnecessary],
+        });
+      }
+    });
+
+    return diagnostics;
+  }
+
+  private getUnusedImportValueAndTypeDiagnostics(tree: Tree): Diagnostic[] {
+    const diagnostics: Diagnostic[] = [];
+
+    const exposedValuesAndTypes = this.language
+      .query(
+        `
+        (import_clause 
+          (exposing_list
+            (exposed_value) @exposedValue
+          )
+        )
+        (import_clause 
+          (exposing_list
+            (exposed_type) @exposedType
+          )
+        )
+        `,
+      )
+      .matches(tree.rootNode)
+      .map((match) => match.captures[0].node);
+
+    exposedValuesAndTypes.forEach((exposedValueOrType) => {
+      if (exposedValueOrType.text.endsWith("(..)")) {
+        return;
+      }
+
+      const references = this.language
+        .query(
+          `
+          ((value_expr) @value.reference
+            (#eq? @value.reference "${exposedValueOrType.text}")
+          )
+          ((type_ref 
+            (upper_case_qid) @type.reference)
+            (#eq? @type.reference "${exposedValueOrType.text}")
+          )
+          `,
+        )
+        .matches(tree.rootNode)
+        .filter(Utils.notUndefined.bind(this));
+
+      if (references.length === 0) {
+        diagnostics.push({
+          code: "unused_imported_value",
+          range: this.getNodeRange(exposedValueOrType),
+          message: `Unused imported ${
+            exposedValueOrType.type === "exposed_type" ? "type" : "value"
+          } \`${exposedValueOrType.text}\``,
+          severity: DiagnosticSeverity.Warning,
+          source: this.ELMLS,
+          tags: [DiagnosticTag.Unnecessary],
+        });
+      }
+    });
+
+    return diagnostics;
+  }
+
+  private getUnusedImportAliasDiagnostics(tree: Tree): Diagnostic[] {
+    const diagnostics: Diagnostic[] = [];
+
+    const moduleAliases = this.language
+      .query(
+        `
+        (import_clause 
+          (as_clause
+            (upper_case_identifier) @moduleAlias
+          )
+        )
+        `,
+      )
+      .matches(tree.rootNode)
+      .map((match) => match.captures[0].node);
+
+    moduleAliases.forEach((moduleAlias) => {
+      const references = this.language
+        .query(
+          `
+          (value_qid
+            (
+              (upper_case_identifier)
+              (dot)
+            )* @module.reference
+          )
+          (upper_case_qid
+            (
+              (upper_case_identifier)
+              (dot)
+            )* @module.reference
+          )
+          `,
+        )
+        .matches(tree.rootNode)
+        .filter(Utils.notUndefined.bind(this))
+        .filter((match) => match.captures.length > 0)
+        .map((match) => match.captures[0].node.text)
+        .filter((moduleReference) => moduleReference === moduleAlias.text);
+
+      if (references.length === 0 && moduleAlias.parent) {
+        diagnostics.push({
+          code: "unused_alias",
+          range: this.getNodeRange(moduleAlias.parent),
+          message: `Unused import alias \`${moduleAlias.text}\``,
+          severity: DiagnosticSeverity.Warning,
+          source: this.ELMLS,
+          tags: [DiagnosticTag.Unnecessary],
+        });
+      }
+    });
+
+    return diagnostics;
+  }
+
+  private getUnusedPatternVariableDiagnostics(tree: Tree): Diagnostic[] {
+    const diagnostics: Diagnostic[] = [];
+
+    const patternMatches = this.language
+      .query(
+        `
+        (value_declaration
+          (function_declaration_left
+            [
+              (pattern)
+              (record_pattern)
+              (lower_pattern)
+            ] @pattern
+          )
+        ) @patternScope
+
+        ; For some reason, we can match on the let_in_expr
+        (value_declaration
+          [
+            (pattern)
+            (record_pattern)
+          ] @pattern
+        ) @patternScope
+
+        ; For let expr variables
+        (value_declaration
+          (function_declaration_left
+            (lower_case_identifier) @pattern
+          )
+        ) @patternScope
+
+        (case_of_branch
+          (pattern) @pattern
+        ) @patternScope
+
+        (anonymous_function_expr
+          (pattern) @pattern
+        ) @patternScope
+        `,
+      )
+      .matches(tree.rootNode);
+
+    patternMatches
+      .filter(Utils.notUndefined.bind(this))
+      .map((match) => {
+        let scope = match.captures[0].node;
+        const patternMatch = match.captures[1].node;
+
+        // Adjust the scope of let_in_expr due to the query bug above
+        if (
+          scope.type === "value_declaration" &&
+          scope.parent?.type === "let_in_expr" &&
+          (patternMatch.type === "lower_case_identifier" ||
+            patternMatch.parent?.type === "value_declaration")
+        ) {
+          scope = scope.parent;
+        }
+
+        if (
+          patternMatch.type === "lower_case_identifier" &&
+          scope.parent?.type === "file"
+        ) {
+          scope = scope.parent;
+        }
+
+        return patternMatch.type === "lower_pattern" ||
+          patternMatch.type === "lower_case_identifier"
+          ? [{ scope, pattern: patternMatch }]
+          : patternMatch.descendantsOfType("lower_pattern").map((pattern) => {
+              return { scope, pattern };
+            });
+      })
+      .reduce((a, b) => a.concat(b), [])
+      .forEach(({ scope, pattern }) => {
+        const references = this.language
+          .query(
+            `
+            (
+              [
+                (value_expr)
+                (record_base_identifier)
+                (exposed_value)
+              ] @patternVariable.reference
+              (#eq? @patternVariable.reference "${pattern.text}")
+            )
+            (
+              (module_declaration
+                (exposing_list
+                  (double_dot)
+                ) @exposingAll 
+              )
+            )
+            `,
+          )
+          .matches(scope)
+          .filter(Utils.notUndefined.bind(this));
+
+        if (scope.type === "file") {
+          let outsideRef = false;
+          const topLevelDeclaration = TreeUtils.findParentOfType(
+            "value_declaration",
+            pattern,
+          );
+
+          for (const ref of references) {
+            const valueDeclaration = TreeUtils.findParentOfType(
+              "value_declaration",
+              ref.captures[0].node,
+              true,
+            );
+
+            if (valueDeclaration?.id !== topLevelDeclaration?.id) {
+              outsideRef = true;
+              break;
+            }
+          }
+
+          if (!outsideRef) {
+            diagnostics.push({
+              code: "unused_top_level",
+              range: this.getNodeRange(pattern),
+              message: `Unused top level definition \`${pattern.text}\``,
+              severity: DiagnosticSeverity.Warning,
+              source: this.ELMLS,
+              tags: [DiagnosticTag.Unnecessary],
+            });
+          }
+        } else if (references.length === 0) {
+          {
+            diagnostics.push({
+              code: "unused_pattern",
+              range: this.getNodeRange(pattern),
+              message: `Unused pattern variable \`${pattern.text}\``,
+              severity: DiagnosticSeverity.Warning,
+              source: this.ELMLS,
+              tags: [DiagnosticTag.Unnecessary],
+            });
+          }
+        }
+      });
+
+    return diagnostics;
+  }
+
+  private getCaseBranchMapNothingToNothingDiagnostics(
+    tree: Tree,
+  ): Diagnostic[] {
+    const diagnostics: Diagnostic[] = [];
+
+    const caseBranches = this.language
+      .query(
+        `
+        (
+          (case_of_branch 
+            (pattern) @casePattern
+            (value_expr) @caseValue
+          ) @caseBranch
+          (#eq? @casePattern "Nothing")
+          (#eq? @caseValue "Nothing")
+        )
+      `,
+      )
+      .matches(tree.rootNode)
+      .map((match) => match.captures[0].node);
+
+    caseBranches.forEach((caseBranch) => {
+      diagnostics.push({
+        code: "map_nothing_to_nothing",
+        range: this.getNodeRange(caseBranch),
+        message: `\`Nothing\` mapped to \`Nothing\` in case expression. Use Maybe.map or Maybe.andThen instead.`,
+        severity: DiagnosticSeverity.Warning,
+        source: this.ELMLS,
+      });
+    });
+
+    return diagnostics;
+  }
+
+  private getBooleanCaseExpressionDiagnostics(tree: Tree): Diagnostic[] {
+    const diagnostics: Diagnostic[] = [];
+
+    // For some reason, we can't match on case_expr, tree-sitter throws a memory access error
+    const caseExpressions = this.language
+      .query(
+        `
+        (
+          (case_of_branch
+            pattern: (pattern) @casePattern1
+            (#match? @casePattern1 "^(True|False)$")
+          ) @caseBranch
+          (case_of_branch
+            pattern: (pattern) @casePattern2
+            (#match? @casePattern2 "^(True|False|_)$")
+          )
+        )
+        `,
+      )
+      .matches(tree.rootNode)
+      .map((match) => match.captures[0].node.parent)
+      .filter(Utils.notUndefinedOrNull.bind(this));
+
+    caseExpressions.forEach((caseExpr) => {
+      diagnostics.push({
+        code: "boolean_case_expr",
+        range: this.getNodeRange(caseExpr),
+        message: `Use an if expression instead of a case expression.`,
+        severity: DiagnosticSeverity.Warning,
+        source: this.ELMLS,
+      });
+    });
+
+    return diagnostics;
+  }
+
+  private getDropConcatOfListsDiagnostics(tree: Tree): Diagnostic[] {
+    const diagnostics: Diagnostic[] = [];
+
+    const listExpressions = this.language
+      .query(
+        `
+        (
+          (list_expr) @startList
+          .
+          (operator
+            (operator_identifier
+              "++"
+            )
+          )
+          .
+          (list_expr) @endList
+        )
+        `,
+      )
+      .matches(tree.rootNode)
+      .map((match) => [match.captures[0].node, match.captures[1].node])
+      .filter(Utils.notUndefinedOrNull.bind(this));
+
+    listExpressions.forEach(([startList, endList]) => {
+      diagnostics.push({
+        code: "drop_concat_of_lists",
+        range: {
+          start: this.getNodeRange(startList).start,
+          end: this.getNodeRange(endList).end,
+        },
+        message: `If you concatenate two lists, then you can merge them into one list.`,
+        severity: DiagnosticSeverity.Warning,
+        source: this.ELMLS,
+      });
+    });
+
+    return diagnostics;
+  }
+
+  private getDropConsOfItemAndListDiagnostics(tree: Tree): Diagnostic[] {
+    const diagnostics: Diagnostic[] = [];
+
+    const consExpressions = this.language
+      .query(
+        `
+        (bin_op_expr
+          (_) @itemExpr
+          .
+          (operator
+            (operator_identifier
+              "::"
+            )
+          )
+          .
+          (list_expr) @listExpr
+        )
+      `,
+      )
+      .matches(tree.rootNode)
+      .map((match) => [match.captures[0].node, match.captures[1].node])
+      .filter(Utils.notUndefinedOrNull.bind(this));
+
+    consExpressions.forEach(([itemExpr, listExpr]) => {
+      diagnostics.push({
+        code: "drop_cons_of_item_and_list",
+        range: {
+          start: this.getNodeRange(itemExpr).start,
+          end: this.getNodeRange(listExpr).end,
+        },
+        message: `If you cons an item to a literal list, then you can just put the item into the list.`,
+        severity: DiagnosticSeverity.Warning,
+        source: this.ELMLS,
+      });
+    });
+
+    return diagnostics;
+  }
+
+  private getUseConsOverConcatDiagnostics(tree: Tree): Diagnostic[] {
+    const diagnostics: Diagnostic[] = [];
+
+    const concatExpressions = this.language
+      .query(
+        `
+        (bin_op_expr
+          (list_expr
+            (left_square_bracket)
+            .
+            (_)
+            .
+            (right_square_bracket)
+          ) @firstPart
+          .
+          (operator
+            (operator_identifier
+              "++"
+            )
+          )
+          .
+          (_) @lastPart
+        )
+      `,
+      )
+      .matches(tree.rootNode)
+      .map((match) => [match.captures[0].node, match.captures[1].node])
+      .filter(Utils.notUndefinedOrNull.bind(this));
+
+    concatExpressions.forEach(([firstPart, lastPart]) => {
+      diagnostics.push({
+        code: "use_cons_over_concat",
+        range: {
+          start: this.getNodeRange(firstPart).start,
+          end: this.getNodeRange(lastPart).end,
+        },
+        message: `If you concatenate two lists, but the first item is a single element list, then you should use the cons operator.`,
+        severity: DiagnosticSeverity.Warning,
+        source: this.ELMLS,
+      });
+    });
+
+    return diagnostics;
+  }
+
+  getSingleFieldRecordDiagnostics(
+    tree: Tree,
+    uri: string,
+    elmWorkspace: IElmWorkspace,
+  ): Diagnostic[] {
+    const diagnostics: Diagnostic[] = [];
+
+    const recordTypes = this.language
+      .query(
+        `
+        (record_type
+          (left_brace)
+          .
+          (_)
+          .
+          (right_brace)
+        ) @recordType
+      `,
+      )
+      .matches(tree.rootNode)
+      .map((match) => match.captures[0].node)
+      .filter(Utils.notUndefinedOrNull.bind(this));
+
+    recordTypes.forEach((recordType) => {
+      let isSingleField = true;
+      if (recordType.parent?.type === "type_ref" && recordType.parent.parent) {
+        const type = findType(recordType.parent, uri, elmWorkspace);
+
+        const singleField = recordType.descendantsOfType(
+          "lower_case_identifier",
+        )[0];
+
+        if (
+          type.nodeType === "Record" &&
+          Object.keys(type.fields).length > 1 &&
+          type.fields[singleField.text]
+        ) {
+          isSingleField = false;
+        }
+      }
+
+      if (isSingleField) {
+        diagnostics.push({
+          code: "single_field_record",
+          range: this.getNodeRange(recordType),
+          message: `Using a record is obsolete if you only plan to store a single field in it.`,
+          severity: DiagnosticSeverity.Warning,
+          source: this.ELMLS,
+        });
+      }
+    });
+
+    return diagnostics;
+  }
+
+  private getUnnecessaryListConcatDiagnostics(tree: Tree): Diagnostic[] {
+    const diagnostics: Diagnostic[] = [];
+
+    const listConcats = this.language
+      .query(
+        `
+        (
+          (function_call_expr
+            target: (_) @target
+            arg: (list_expr
+              (left_square_bracket)
+              .
+              (list_expr)
+              .
+              ((comma) . (list_expr))*
+              .
+              (right_square_bracket)
+            )
+          ) @functionCall
+          (#eq? @target "List.concat")
+        )
+        `,
+      )
+      .matches(tree.rootNode)
+      .map((match) => match.captures[0].node)
+      .filter(Utils.notUndefinedOrNull.bind(this));
+
+    listConcats.forEach((listConcat) => {
+      diagnostics.push({
+        code: "unnecessary_list_concat",
+        range: this.getNodeRange(listConcat),
+        message: `You should just merge the arguments of \`List.concat\` to a single list.`,
+        severity: DiagnosticSeverity.Warning,
+        source: this.ELMLS,
+      });
+    });
+
+    return diagnostics;
+  }
+
+  private getUnnecessaryPortModuleDiagnostics(tree: Tree): Diagnostic[] {
+    const diagnostics: Diagnostic[] = [];
+
+    const unusedPortMatches = this.language
+      .query(
+        `
+        (module_declaration
+          (port)
+        ) @portModule
+        
+        (port_annotation) @portAnnotation
+        `,
+      )
+      .matches(tree.rootNode);
+
+    if (
+      unusedPortMatches[0]?.captures[0].name === "portModule" &&
+      !unusedPortMatches[1]
+    ) {
+      diagnostics.push({
+        code: "unnecessary_port_module",
+        range: this.getNodeRange(unusedPortMatches[0].captures[0].node),
+        message: `Module is definined as a \`port\` module, but does not define any ports.`,
+        severity: DiagnosticSeverity.Warning,
+        source: this.ELMLS,
+      });
+    }
+
+    return diagnostics;
+  }
+
+  private getFullyAppliedOperatorAsPrefixDiagnostics(tree: Tree): Diagnostic[] {
+    const diagnostics: Diagnostic[] = [];
+
+    const operatorFunctions = this.language
+      .query(
+        `
+        (function_call_expr
+          target: (operator_as_function_expr)
+          .
+          (_) @arg1
+          .
+          (_) @arg2
+        ) @functionCall
+        `,
+      )
+      .matches(tree.rootNode)
+      .map((match) => match.captures[0].node)
+      .filter(Utils.notUndefinedOrNull.bind(this));
+
+    operatorFunctions.forEach((operatorFunction) => {
+      diagnostics.push({
+        code: "no_uncurried_prefix",
+        range: this.getNodeRange(operatorFunction),
+        message: `Don't use fully applied prefix notation for operators.`,
+        severity: DiagnosticSeverity.Warning,
+        source: this.ELMLS,
+      });
+    });
+
+    return diagnostics;
+  }
+
+  private getUnusedTypeAliasDiagnostics(tree: Tree): Diagnostic[] {
+    const diagnostics: Diagnostic[] = [];
+
+    const typeAliases = this.language
+      .query(
+        `
+        (type_alias_declaration
+          (upper_case_identifier) @typeAlias  
+        )
+        `,
+      )
+      .matches(tree.rootNode)
+      .map((match) => match.captures[0].node)
+      .filter(Utils.notUndefinedOrNull.bind(this));
+
+    typeAliases.forEach((typeAlias) => {
+      const references = this.language
+        .query(
+          `
+          (
+            [
+              (value_expr)
+              (exposed_type)
+            ] @value.reference
+            (#eq? @value.reference "${typeAlias.text}")
+          )
+          ((type_ref 
+            (upper_case_qid) @type.reference)
+            (#eq? @type.reference "${typeAlias.text}")
+          )
+          `,
+        )
+        .matches(tree.rootNode)
+        .filter(Utils.notUndefined.bind(this));
+
+      if (references.length === 0 && typeAlias.parent) {
+        diagnostics.push({
+          code: "unused_type_alias",
+          range: this.getNodeRange(typeAlias.parent),
+          message: `Type alias \`${typeAlias.text}\` is not used.`,
+          severity: DiagnosticSeverity.Warning,
+          source: this.ELMLS,
+          tags: [DiagnosticTag.Unnecessary],
+        });
+      }
+    });
+
+    return diagnostics;
+  }
+
+  private getUnusedValueConstructorDiagnostics(tree: Tree): Diagnostic[] {
+    const diagnostics: Diagnostic[] = [];
+
+    const unionVariants = this.language
+      .query(
+        `
+        (type_declaration
+          (upper_case_identifier) @typeName
+          (union_variant
+            (upper_case_identifier) @unionVariant
+          ) 
+        )
+        `,
+      )
+      .matches(tree.rootNode)
+      .map((match) => [match.captures[1].node, match.captures[0].node])
+      .filter(Utils.notUndefinedOrNull.bind(this));
+
+    unionVariants.forEach(([unionVariant, typeName]) => {
+      const references = this.language
+        .query(
+          `
+          (
+            (exposed_type) @exposed.reference
+            (#eq? @exposed.reference "${typeName.text}(..)")
+          )
+          (
+            (value_expr) @value.reference
+            (#eq? @value.reference "${unionVariant.text}")
+          )
+          ((type_ref 
+            (upper_case_qid) @type.reference)
+            (#eq? @type.reference "${unionVariant.text}")
+          )
+          `,
+        )
+        .matches(tree.rootNode)
+        .filter(Utils.notUndefined.bind(this));
+
+      if (references.length === 0 && unionVariant.parent) {
+        diagnostics.push({
+          code: "unused_value_constructor",
+          range: this.getNodeRange(unionVariant.parent),
+          message: `Value constructor \`${unionVariant.text}\` is not used.`,
+          severity: DiagnosticSeverity.Warning,
+          source: this.ELMLS,
+          tags: [DiagnosticTag.Unnecessary],
+        });
+      }
+    });
+
+    return diagnostics;
+  }
+
+  private getNodeRange(node: SyntaxNode): Range {
+    const end = PositionUtil.FROM_TS_POSITION(node.endPosition).toVSPosition();
+    return {
+      start: PositionUtil.FROM_TS_POSITION(node.startPosition).toVSPosition(),
+      end: {
+        ...end,
+        character: end.character,
+      },
+    };
+  }
+}
diff --git a/src/providers/diagnostics/fileDiagnostics.ts b/src/providers/diagnostics/fileDiagnostics.ts
index 1a27cb1..bbebc78 100644
--- a/src/providers/diagnostics/fileDiagnostics.ts
+++ b/src/providers/diagnostics/fileDiagnostics.ts
@@ -6,6 +6,7 @@ export const enum DiagnosticKind {
   ElmAnalyse,
   ElmTest,
   TypeInference,
+  ElmLS,
 }
 
 export function diagnosticsEquals(a: Diagnostic, b: Diagnostic): boolean {
@@ -48,6 +49,7 @@ export class FileDiagnostics {
       ...this.getForKind(DiagnosticKind.ElmAnalyse),
       ...this.getForKind(DiagnosticKind.ElmTest),
       ...this.getForKind(DiagnosticKind.TypeInference),
+      ...this.getForKind(DiagnosticKind.ElmLS),
     ];
   }
 
diff --git a/src/server.ts b/src/server.ts
index b5c557a..c612b3c 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -29,6 +29,7 @@ import {
   SelectionRangeProvider,
   WorkspaceSymbolProvider,
 } from "./providers";
+import { ElmDiagnostics } from "./providers/diagnostics/elmDiagnostics";
 import { Settings } from "./util/settings";
 
 export interface ILanguageServer {
@@ -135,6 +136,10 @@ export class Server implements ILanguageServer {
       useValue: new TypeInferenceDiagnostics(),
     });
 
+    container.register(ElmDiagnostics, {
+      useValue: new ElmDiagnostics(),
+    });
+
     const clientSettings = await settings.getClientSettings();
 
     container.register("ClientSettings", {
diff --git a/src/typings/tree-sitter-web.d.ts b/src/typings/tree-sitter-web.d.ts
index 77210e3..e7742c5 100644
--- a/src/typings/tree-sitter-web.d.ts
+++ b/src/typings/tree-sitter-web.d.ts
@@ -37,4 +37,14 @@ declare module "web-tree-sitter" {
   interface Language {
     query(source: string): Query;
   }
+
+  class Parser {
+    static init(): Promise<void>;
+    delete(): void;
+    parse(input: string | Input, previousTree?: Tree, options?: Options): Tree;
+    getLanguage(): Language;
+    setLanguage(language: Language): void;
+    getLogger(): Logger;
+    setLogger(logFunc: Logger): void;
+  }
 }
diff --git a/src/util/refactorEditUtils.ts b/src/util/refactorEditUtils.ts
index 8f93ed5..9d154cd 100644
--- a/src/util/refactorEditUtils.ts
+++ b/src/util/refactorEditUtils.ts
@@ -90,27 +90,40 @@ export class RefactorEditUtils {
     const importClause = TreeUtils.findImportClauseByName(tree, moduleName);
 
     if (importClause) {
-      const exposedValues = TreeUtils.descendantsOfType(
-        importClause,
-        "exposed_value",
-      );
+      const exposedValuesAndTypes = [
+        ...TreeUtils.descendantsOfType(importClause, "exposed_value"),
+        ...TreeUtils.descendantsOfType(importClause, "exposed_type"),
+      ];
 
-      if (exposedValues.length === 1 && exposedValues[0].text === valueName) {
-        // Remove the entire import if it was the only one
-        return TextEdit.del(
-          Range.create(
-            Position.create(
-              importClause.startPosition.row,
-              importClause.startPosition.column,
-            ),
-            Position.create(
-              importClause.endPosition.row,
-              importClause.endPosition.column,
-            ),
-          ),
+      if (
+        exposedValuesAndTypes.length === 1 &&
+        exposedValuesAndTypes[0].text === valueName
+      ) {
+        // Remove the entire exposing list if it was the only one
+        const exposingList = TreeUtils.findFirstNamedChildOfType(
+          "exposing_list",
+          importClause,
         );
+
+        if (exposingList) {
+          return TextEdit.del(
+            Range.create(
+              Position.create(
+                exposingList.startPosition.row,
+                exposingList.startPosition.column,
+              ),
+              Position.create(
+                exposingList.endPosition.row,
+                exposingList.endPosition.column,
+              ),
+            ),
+          );
+        }
       } else {
-        return this.removeValueFromExposingList(exposedValues, valueName);
+        return this.removeValueFromExposingList(
+          exposedValuesAndTypes,
+          valueName,
+        );
       }
     }
   }
@@ -259,6 +272,29 @@ export class RefactorEditUtils {
     }
   }
 
+  public static removeRecordPatternValue(pattern: SyntaxNode): TextEdit {
+    let startPosition = pattern.startPosition;
+    let endPosition = pattern.endPosition;
+
+    if (pattern.previousNamedSibling?.text === ",") {
+      startPosition = pattern.previousNamedSibling.startPosition;
+    }
+
+    if (
+      pattern.previousNamedSibling?.text !== "," &&
+      pattern.nextNamedSibling?.text === ","
+    ) {
+      endPosition = pattern.nextNamedSibling.endPosition;
+    }
+
+    return TextEdit.del(
+      Range.create(
+        Position.create(startPosition.row, startPosition.column),
+        Position.create(endPosition.row, endPosition.column),
+      ),
+    );
+  }
+
   private static removeValueFromExposingList(
     exposedNodes: SyntaxNode[],
     valueName: string,
diff --git a/src/util/settings.ts b/src/util/settings.ts
index e145b66..b188cbc 100644
--- a/src/util/settings.ts
+++ b/src/util/settings.ts
@@ -8,6 +8,7 @@ export interface IClientSettings {
   elmAnalyseTrigger: ElmAnalyseTrigger;
   trace: { server: string };
   extendedCapabilities?: IExtendedCapabilites;
+  disableElmLSDiagnostics: boolean;
 }
 
 export interface IExtendedCapabilites {
@@ -25,6 +26,7 @@ export class Settings {
     elmPath: "",
     elmTestPath: "",
     trace: { server: "off" },
+    disableElmLSDiagnostics: false,
   };
   private connection: IConnection;
 
diff --git a/src/util/types/typeExpression.ts b/src/util/types/typeExpression.ts
index 16aee35..2d8cad1 100644
--- a/src/util/types/typeExpression.ts
+++ b/src/util/types/typeExpression.ts
@@ -272,16 +272,19 @@ export class TypeExpression {
     const segmentTypes = typeExpr.segments.map((s) =>
       this.typeSignatureSegmentType(s),
     );
-    if (segmentTypes.length === 1) {
-      return segmentTypes[0];
-    } else {
-      return uncurryFunction(
-        TFunction(
-          segmentTypes.slice(0, segmentTypes.length - 1),
-          segmentTypes[segmentTypes.length - 1],
-        ),
-      );
-    }
+    const type =
+      segmentTypes.length === 1
+        ? segmentTypes[0]
+        : uncurryFunction(
+            TFunction(
+              segmentTypes.slice(0, segmentTypes.length - 1),
+              segmentTypes[segmentTypes.length - 1],
+            ),
+          );
+
+    this.expressionTypes.set(typeExpr, type);
+
+    return type;
   }
 
   private toResult(type: Type): InferenceResult {
diff --git a/src/util/types/typeInference.ts b/src/util/types/typeInference.ts
index fd8ceef..fc877d3 100644
--- a/src/util/types/typeInference.ts
+++ b/src/util/types/typeInference.ts
@@ -2482,31 +2482,36 @@ export function findType(
   workspace: IElmWorkspace,
 ): Type {
   try {
-    let declaration: SyntaxNode | null = node;
-    while (
-      declaration &&
-      (declaration.type !== "value_declaration" ||
-        declaration.parent?.type !== "file")
-    ) {
-      declaration = declaration.parent;
-    }
+    const declaration = mapSyntaxNodeToExpression(
+      TreeUtils.findParentOfType("value_declaration", node, true),
+    );
 
-    // We can't find the top level declaration
-    if (
-      declaration?.type !== "value_declaration" ||
-      declaration.parent?.type !== "file"
-    ) {
-      return TUnknown;
-    }
+    const findTypeOrParentType = (
+      expr: SyntaxNode | undefined,
+      inferenceResult: InferenceResult,
+    ): Type | undefined => {
+      const found = expr
+        ? inferenceResult.expressionTypes.get(expr as Expression)
+        : undefined;
 
-    const mappedDeclaration = mapSyntaxNodeToExpression(declaration);
+      if (found) {
+        return found;
+      }
 
-    if (
-      mappedDeclaration &&
-      mappedDeclaration.nodeType === "ValueDeclaration"
-    ) {
+      // Check if the parent is the same text and position
+      if (
+        expr &&
+        expr.text === expr.parent?.text &&
+        expr.startIndex === expr.parent?.startIndex &&
+        expr.endIndex === expr.parent?.endIndex
+      ) {
+        return findTypeOrParentType(expr.parent, inferenceResult);
+      }
+    };
+
+    if (declaration && declaration.nodeType === "ValueDeclaration") {
       const inferenceResult = InferenceScope.valueDeclarationInference(
-        mappedDeclaration,
+        declaration,
         uri,
         workspace,
         new Set<EValueDeclaration>(),
@@ -2533,32 +2538,45 @@ export function findType(
         );
       }
 
-      const findTypeOrParentType = (
-        expr: SyntaxNode | undefined,
-      ): Type | undefined => {
-        const found = expr
-          ? inferenceResult.expressionTypes.get(expr as Expression)
-          : undefined;
+      return findTypeOrParentType(node, inferenceResult) ?? TUnknown;
+    }
 
-        if (found) {
-          return found;
-        }
+    const typeAliasDeclaration = mapSyntaxNodeToExpression(
+      TreeUtils.findParentOfType("type_alias_declaration", node),
+    );
 
-        // Check if the parent is the same text and position
-        if (
-          expr &&
-          expr.text === expr.parent?.text &&
-          expr.startIndex === expr.parent?.startIndex &&
-          expr.endIndex === expr.parent?.endIndex
-        ) {
-          return findTypeOrParentType(expr.parent);
-        }
-      };
+    if (
+      typeAliasDeclaration &&
+      typeAliasDeclaration.nodeType === "TypeAliasDeclaration"
+    ) {
+      const inferenceResult = TypeExpression.typeAliasDeclarationInference(
+        typeAliasDeclaration,
+        uri,
+        workspace,
+      );
 
-      return findTypeOrParentType(node) ?? TUnknown;
-    } else {
-      return TUnknown;
+      if (node.type === "type_alias_declaration") {
+        return inferenceResult.type;
+      }
+
+      return findTypeOrParentType(node, inferenceResult) ?? TUnknown;
     }
+
+    const typeDeclaration = mapSyntaxNodeToExpression(
+      TreeUtils.findParentOfType("type_alias_declaration", node),
+    );
+
+    if (typeDeclaration && typeDeclaration.nodeType === "TypeDeclaration") {
+      const inferenceResult = TypeExpression.typeDeclarationInference(
+        typeDeclaration,
+        uri,
+        workspace,
+      );
+
+      return findTypeOrParentType(node, inferenceResult) ?? TUnknown;
+    }
+
+    return TUnknown;
   } catch (error) {
     const connection = container.resolve<IConnection>("Connection");
     connection.console.warn(`Error while trying to infer a type. ${error}`);


Test patch:
diff --git a/test/diagnosticTests/elmDiagnostics.test.ts b/test/diagnosticTests/elmDiagnostics.test.ts
new file mode 100644
index 0000000..18cae63
--- /dev/null
+++ b/test/diagnosticTests/elmDiagnostics.test.ts
@@ -0,0 +1,1532 @@
+import {
+  Diagnostic,
+  DiagnosticSeverity,
+  DiagnosticTag,
+  Range,
+} from "vscode-languageserver";
+import { URI } from "vscode-uri";
+import { ElmDiagnostics } from "../../src/providers/diagnostics/elmDiagnostics";
+import { diagnosticsEquals } from "../../src/providers/diagnostics/fileDiagnostics";
+import { Utils } from "../../src/util/utils";
+import { baseUri } from "../utils/mockElmWorkspace";
+import { getSourceFiles } from "../utils/sourceParser";
+import { SourceTreeParser } from "../utils/sourceTreeParser";
+
+describe("ElmLSDiagnostics", () => {
+  let elmDiagnostics: ElmDiagnostics;
+  const treeParser = new SourceTreeParser();
+
+  const debug = process.argv.find((arg) => arg === "--debug");
+
+  async function testDiagnostics(
+    source: string,
+    code: string,
+    expectedDiagnostics: Diagnostic[],
+  ) {
+    await treeParser.init();
+    elmDiagnostics = new ElmDiagnostics();
+
+    const workspace = treeParser.getWorkspace(getSourceFiles(source));
+    const uri = URI.file(baseUri + "Main.elm").toString();
+    const treeContainer = workspace.getForest().getByUri(uri);
+
+    if (!treeContainer) {
+      fail();
+    }
+
+    const diagnostics = elmDiagnostics
+      .createDiagnostics(treeContainer.tree, uri, workspace)
+      .filter((diagnostic) => diagnostic.code === code);
+
+    const diagnosticsEqual = Utils.arrayEquals(
+      diagnostics,
+      expectedDiagnostics,
+      diagnosticsEquals,
+    );
+
+    if (debug && !diagnosticsEqual) {
+      console.log(
+        `Expecting ${JSON.stringify(expectedDiagnostics)}, got ${JSON.stringify(
+          diagnostics,
+        )}`,
+      );
+    }
+
+    expect(diagnosticsEqual).toBeTruthy();
+  }
+
+  describe("boolean case expressions", () => {
+    const diagnosticWithRange = (range: Range): Diagnostic => {
+      return {
+        code: "boolean_case_expr",
+        message: "Use an if expression instead of a case expression.",
+        source: "ElmLS",
+        severity: DiagnosticSeverity.Warning,
+        range,
+      };
+    };
+
+    it("boolean case true", async () => {
+      const source = `
+module Bar exposing (..)
+
+foo x =
+    case x of
+        True ->
+            x
+        _ -> not x
+			`;
+
+      await testDiagnostics(source, "boolean_case_expr", [
+        diagnosticWithRange({
+          start: { line: 4, character: 4 },
+          end: { line: 7, character: 18 },
+        }),
+      ]);
+    });
+
+    it("boolean case false", async () => {
+      const source = `
+module Bar exposing (..)
+
+foo x =
+    case x of
+        False ->
+            x
+        _ -> not x
+			`;
+
+      await testDiagnostics(source, "boolean_case_expr", [
+        diagnosticWithRange({
+          start: { line: 4, character: 4 },
+          end: { line: 7, character: 18 },
+        }),
+      ]);
+    });
+
+    it("no boolean case", async () => {
+      const source = `
+module Bar exposing (..)
+
+foo x =
+    case x of
+        Some ->
+            x
+        Other -> not x
+			`;
+
+      await testDiagnostics(source, "boolean_case_expr", []);
+    });
+  });
+
+  describe("unused top level", () => {
+    const diagnosticWithRangeAndName = (
+      range: Range,
+      name: string,
+    ): Diagnostic => {
+      return {
+        code: "unused_top_level",
+        message: `Unused top level definition \`${name}\``,
+        source: "ElmLS",
+        severity: DiagnosticSeverity.Warning,
+        range,
+        tags: [DiagnosticTag.Unnecessary],
+      };
+    };
+
+    it("unused function", async () => {
+      const source = `
+module Bar exposing (foo)
+
+foo = some
+
+baz = 2
+
+some = 1
+			`;
+
+      await testDiagnostics(source, "unused_top_level", [
+        diagnosticWithRangeAndName(
+          {
+            start: { line: 5, character: 0 },
+            end: { line: 5, character: 3 },
+          },
+          "baz",
+        ),
+      ]);
+    });
+
+    it("used variable as record update", async () => {
+      const source = `
+module Bar exposing (..)
+
+addUsedVariable x =
+    { x | name = "John" }
+			`;
+
+      await testDiagnostics(source, "unused_top_level", []);
+    });
+
+    it("used variable in case expression", async () => {
+      const source = `
+module Bar exposing (..)
+
+foo x =
+    case x of
+      Bar -> 1
+			`;
+
+      await testDiagnostics(source, "unused_top_level", []);
+    });
+
+    it("used variable in all declaration", async () => {
+      const source = `
+module Bar exposing (..)
+
+x y =
+  case y of
+   ( b, _ ) ->
+    let
+        _ =
+            Debug.log "Unknown" b
+    in
+        model ! []
+			`;
+
+      await testDiagnostics(source, "unused_top_level", []);
+    });
+
+    it("only used in self", async () => {
+      const source = `
+module Bar exposing (foo, Some(..)))
+
+type Some = Thing
+
+foo = 1
+
+bar = bar + foo
+			`;
+
+      await testDiagnostics(source, "unused_top_level", [
+        diagnosticWithRangeAndName(
+          {
+            start: { line: 7, character: 0 },
+            end: { line: 7, character: 3 },
+          },
+          "bar",
+        ),
+      ]);
+    });
+
+    it("destructuring same name", async () => {
+      const source = `
+module Foo exposing (..)
+
+error : Model -> Maybe Error
+error { error } =
+    error
+			`;
+
+      await testDiagnostics(source, "unused_top_level", []);
+    });
+
+    it("used imported variable in pattern match", async () => {
+      const source = `
+module Foo exposing (foo)
+
+import Color exposing (Color(..))
+
+foo c =
+  case c of
+    Blue -> 1
+			`;
+
+      await testDiagnostics(source, "unused_top_level", []);
+    });
+
+    it("used imported variable as opaque", async () => {
+      const source = `
+module Foo exposing (foo)
+
+import Color exposing (Color(..))
+
+foo (Blue c) =
+  c
+			`;
+
+      await testDiagnostics(source, "unused_top_level", []);
+    });
+
+    it("used in destructuring let", async () => {
+      const source = `
+module Foo exposing (..)
+
+import Some exposing (Bar(..))
+
+x =
+  let
+    (Bar 1) = some
+  in
+    1
+			`;
+
+      await testDiagnostics(source, "unused_top_level", []);
+    });
+
+    it("used binary imported function as prefix", async () => {
+      const source = `
+module Foo exposing (..)
+
+import List.Extra exposing ((!!))
+
+getItemAtIndex : Int -> Maybe String
+getItemAtIndex index =
+    let
+        someList =
+            [ "a", "b", "c" ]
+    in
+    (!!) someList index
+			`;
+
+      await testDiagnostics(source, "unused_top_level", []);
+    });
+  });
+
+  describe("unused import", () => {
+    const diagnosticWithRangeAndName = (
+      range: Range,
+      name: string,
+    ): Diagnostic => {
+      return {
+        code: "unused_import",
+        message: `Unused import \`${name}\``,
+        source: "ElmLS",
+        severity: DiagnosticSeverity.Warning,
+        range,
+        tags: [DiagnosticTag.Unnecessary],
+      };
+    };
+
+    it("used as qualified", async () => {
+      const source = `
+module Foo exposing (..)
+
+import Bar
+
+foo = Bar.add 1
+			`;
+
+      await testDiagnostics(source, "unused_import", []);
+    });
+
+    it("used as qualified in pattern", async () => {
+      const source = `
+module Main exposing (..)
+
+import Bar
+
+z a =
+    case a of
+        Bar.Z ->
+            1
+			`;
+
+      await testDiagnostics(source, "unused_import", []);
+    });
+
+    it("used in type reference", async () => {
+      const source = `
+module Foo exposing (..)
+
+import Bar
+
+foo : Bar.Thing
+foo = bar
+			`;
+
+      await testDiagnostics(source, "unused_import", []);
+    });
+
+    it("used in type alias", async () => {
+      const source = `
+module Foo exposing (..)
+
+import Bar
+
+type alias Thing = { name : Bar.Name }
+			`;
+
+      await testDiagnostics(source, "unused_import", []);
+    });
+
+    it("unused but has alias", async () => {
+      const source = `
+module Foo exposing (..)
+
+import Bar as B
+
+foo = 1
+			`;
+
+      await testDiagnostics(source, "unused_import", []);
+    });
+
+    it("unused but has exposing", async () => {
+      const source = `
+module Foo exposing (..)
+
+import Bar exposing (baz)
+
+foo = 1
+			`;
+
+      await testDiagnostics(source, "unused_import", []);
+    });
+
+    it("unused import", async () => {
+      const source = `
+module Foo exposing (..)
+
+import Bar
+
+foo = 1
+			`;
+
+      await testDiagnostics(source, "unused_import", [
+        diagnosticWithRangeAndName(
+          {
+            start: { line: 3, character: 0 },
+            end: { line: 3, character: 10 },
+          },
+          "Bar",
+        ),
+      ]);
+    });
+  });
+
+  describe("unused import alias", () => {
+    const diagnosticWithRangeAndName = (
+      range: Range,
+      name: string,
+    ): Diagnostic => {
+      return {
+        code: "unused_alias",
+        message: `Unused import alias \`${name}\``,
+        source: "ElmLS",
+        severity: DiagnosticSeverity.Warning,
+        range,
+        tags: [DiagnosticTag.Unnecessary],
+      };
+    };
+
+    it("no usage for alias", async () => {
+      const source = `
+module Foo exposing (..)
+
+import Bar as B
+
+foo = (+) 1 2
+			`;
+
+      await testDiagnostics(source, "unused_alias", [
+        diagnosticWithRangeAndName(
+          {
+            start: { line: 3, character: 11 },
+            end: { line: 3, character: 15 },
+          },
+          "B",
+        ),
+      ]);
+    });
+
+    it("used as qualified", async () => {
+      const source = `
+module Foo exposing (..)
+
+import Bar as B
+
+foo = B.add 1
+			`;
+
+      await testDiagnostics(source, "unused_alias", []);
+    });
+
+    it("used as qualified in pattern", async () => {
+      const source = `
+module Main exposing (..)
+
+import X as Y
+
+z a =
+    case a of
+        Y.Z ->
+            1
+			`;
+
+      await testDiagnostics(source, "unused_alias", []);
+    });
+
+    it("used in type reference", async () => {
+      const source = `
+module Foo exposing (..)
+
+import Bar as B
+
+foo : B.Thing
+foo = bar
+			`;
+
+      await testDiagnostics(source, "unused_alias", []);
+    });
+
+    it("used in type alias", async () => {
+      const source = `
+module Foo exposing (..)
+
+import Bar as B
+
+foo = B.math.add 1
+			`;
+
+      await testDiagnostics(source, "unused_alias", []);
+    });
+
+    it("used as qualified with nested record access", async () => {
+      const source = `
+module Foo exposing (..)
+
+
+import Bar as B
+
+type alias Thing = { name : B.Name }
+			`;
+
+      await testDiagnostics(source, "unused_alias", []);
+    });
+  });
+
+  describe("unused imported value or type", () => {
+    const diagnosticWithRangeAndName = (
+      range: Range,
+      name: string,
+      type: string,
+    ): Diagnostic => {
+      return {
+        code: "unused_imported_value",
+        message: `Unused imported ${type} \`${name}\``,
+        source: "ElmLS",
+        severity: DiagnosticSeverity.Warning,
+        range,
+        tags: [DiagnosticTag.Unnecessary],
+      };
+    };
+
+    it("used variable as record update", async () => {
+      const source = `
+module Bar exposing (..)
+
+addUsedVariable x =
+    { x | name = "John" }
+			`;
+
+      await testDiagnostics(source, "unused_imported_value", []);
+    });
+
+    it("used variable in case expression", async () => {
+      const source = `
+module Bar exposing (..)
+
+foo x =
+    case x of
+      Bar -> 1
+		`;
+
+      await testDiagnostics(source, "unused_imported_value", []);
+    });
+
+    it("used variable in all declaration", async () => {
+      const source = `
+module Bar exposing (..)
+
+x y =
+  case y of
+   ( b, _ ) ->
+    let
+        _ =
+            Debug.log "Unknown" b
+    in
+        model ! []
+		`;
+
+      await testDiagnostics(source, "unused_imported_value", []);
+    });
+
+    it("used value constructor", async () => {
+      const source = `
+module Bar exposing (foo)
+
+type Some = Thing
+
+foo = Thing
+		`;
+
+      await testDiagnostics(source, "unused_imported_value", []);
+    });
+
+    it("exposed value constructor", async () => {
+      const source = `
+module Bar exposing (foo, Some(..))
+
+type Some = Thing
+
+foo = 1
+		`;
+
+      await testDiagnostics(source, "unused_imported_value", []);
+    });
+
+    it("destructuring same name", async () => {
+      const source = `
+module Foo exposing (..)
+
+error : Model -> Maybe Error
+error { error } =
+    error
+		`;
+
+      await testDiagnostics(source, "unused_imported_value", []);
+    });
+
+    it("unused imported variable", async () => {
+      const source = `
+module Foo exposing (foo)
+
+import Html exposing (div)
+
+foo = 1
+		`;
+
+      await testDiagnostics(source, "unused_imported_value", [
+        diagnosticWithRangeAndName(
+          {
+            start: { line: 3, character: 22 },
+            end: { line: 3, character: 25 },
+          },
+          "div",
+          "value",
+        ),
+      ]);
+    });
+
+    it("used imported variable in pattern match", async () => {
+      const source = `
+module Foo exposing (foo)
+
+import Color exposing (Color(..))
+
+foo c =
+  case c of
+    Blue -> 1
+		`;
+
+      await testDiagnostics(source, "unused_imported_value", []);
+    });
+
+    it("used imported variable as opaque", async () => {
+      const source = `
+module Foo exposing (foo)
+
+import Color exposing (Color(..))
+
+foo (Blue c) =
+  c
+		`;
+
+      await testDiagnostics(source, "unused_imported_value", []);
+    });
+
+    it("used in destructuring let", async () => {
+      const source = `
+module Foo exposing (..)
+
+import Some exposing (Bar(..))
+
+x =
+  let
+    (Bar 1) = some
+  in
+    1
+		`;
+
+      await testDiagnostics(source, "unused_imported_value", []);
+    });
+
+    it("unused imported type", async () => {
+      const source = `
+module Foo exposing (..)
+
+import Some exposing (Thing, Other)
+
+x : Int -> Other
+x y =
+  Some.other y
+		`;
+
+      await testDiagnostics(source, "unused_imported_value", [
+        diagnosticWithRangeAndName(
+          {
+            start: { line: 3, character: 22 },
+            end: { line: 3, character: 27 },
+          },
+          "Thing",
+          "type",
+        ),
+      ]);
+    });
+  });
+
+  describe("unused pattern variable", () => {
+    const diagnosticWithRangeAndName = (
+      range: Range,
+      name: string,
+    ): Diagnostic => {
+      return {
+        code: "unused_pattern",
+        message: `Unused pattern variable \`${name}\``,
+        source: "ElmLS",
+        severity: DiagnosticSeverity.Warning,
+        range,
+        tags: [DiagnosticTag.Unnecessary],
+      };
+    };
+
+    it("used variable as record update", async () => {
+      const source = `
+module Bar exposing (..)
+
+addUsedVariable x =
+    { x | name = "John" }
+			`;
+
+      await testDiagnostics(source, "unused_pattern", []);
+    });
+
+    it("used variable in case expression", async () => {
+      const source = `
+module Bar exposing (..)
+
+foo x =
+    case x of
+      Bar -> 1
+			`;
+
+      await testDiagnostics(source, "unused_pattern", []);
+    });
+
+    it("used variable in all declaration", async () => {
+      const source = `
+module Bar exposing (..)
+
+x y =
+  case y of
+   ( b, _ ) ->
+    let
+        _ =
+            Debug.log "Unknown" b
+    in
+        model ! []
+			`;
+
+      await testDiagnostics(source, "unused_pattern", []);
+    });
+
+    it("used value constructor", async () => {
+      const source = `
+module Bar exposing (foo)
+
+type Some = Thing
+
+foo = Thing
+			`;
+
+      await testDiagnostics(source, "unused_pattern", []);
+    });
+
+    it("exposed value constructor", async () => {
+      const source = `
+module Bar exposing (foo, Some(..))
+
+type Some = Thing
+
+foo = 1
+			`;
+
+      await testDiagnostics(source, "unused_pattern", []);
+    });
+
+    it("destructuring same name", async () => {
+      const source = `
+module Foo exposing (..)
+
+error : Model -> Maybe Error
+error { error } =
+    error
+			`;
+
+      await testDiagnostics(source, "unused_pattern", []);
+    });
+
+    it("used imported variable in pattern match", async () => {
+      const source = `
+module Foo exposing (foo)
+
+import Color exposing (Color(..))
+
+foo c =
+  case c of
+    Blue -> 1
+			`;
+
+      await testDiagnostics(source, "unused_pattern", []);
+    });
+
+    it("used imported variable as opaque", async () => {
+      const source = `
+module Foo exposing (foo)
+
+import Color exposing (Color(..))
+
+foo (Blue c) =
+  c
+			`;
+
+      await testDiagnostics(source, "unused_pattern", []);
+    });
+
+    it("unused in case pattern", async () => {
+      const source = `
+module Foo exposing (foo)
+
+foo x =
+  case x of
+    Just y ->
+      1
+			`;
+
+      await testDiagnostics(source, "unused_pattern", [
+        diagnosticWithRangeAndName(
+          {
+            start: { line: 5, character: 9 },
+            end: { line: 5, character: 10 },
+          },
+          "y",
+        ),
+      ]);
+    });
+
+    it("unused in case pattern as single", async () => {
+      const source = `
+module Foo exposing (foo)
+
+foo x =
+  case x of
+    y ->
+      1
+			`;
+
+      await testDiagnostics(source, "unused_pattern", [
+        diagnosticWithRangeAndName(
+          {
+            start: { line: 5, character: 4 },
+            end: { line: 5, character: 5 },
+          },
+          "y",
+        ),
+      ]);
+    });
+
+    it("used in destructuring let", async () => {
+      const source = `
+module Foo exposing (foo)
+
+import Some exposing (Bar(..))
+
+x =
+  let
+    (Bar 1) = some
+  in
+    1
+			`;
+
+      await testDiagnostics(source, "unused_pattern", []);
+    });
+  });
+
+  describe("drop cons of item and list", () => {
+    const diagnosticWithRange = (range: Range): Diagnostic => {
+      return {
+        code: "drop_cons_of_item_and_list",
+        message: `If you cons an item to a literal list, then you can just put the item into the list.`,
+        source: "ElmLS",
+        severity: DiagnosticSeverity.Warning,
+        range,
+      };
+    };
+
+    it("no optimization", async () => {
+      const source = `
+module Bar exposing (foo)
+
+foo : Int
+foo =
+    [1, 2] ++ var
+			`;
+
+      await testDiagnostics(source, "drop_cons_of_item_and_list", []);
+    });
+
+    it("cons with literal list", async () => {
+      const source = `
+module Bar exposing (foo)
+
+foo : Int
+foo =
+    1 :: [2 , 3]
+			`;
+
+      await testDiagnostics(source, "drop_cons_of_item_and_list", [
+        diagnosticWithRange({
+          start: { line: 5, character: 4 },
+          end: { line: 5, character: 16 },
+        }),
+      ]);
+    });
+  });
+
+  describe("map nothing to nothing", () => {
+    const diagnosticWithRange = (range: Range): Diagnostic => {
+      return {
+        code: "map_nothing_to_nothing",
+        message: `\`Nothing\` mapped to \`Nothing\` in case expression. Use Maybe.map or Maybe.andThen instead.`,
+        source: "ElmLS",
+        severity: DiagnosticSeverity.Warning,
+        range,
+      };
+    };
+
+    it("map nothing to nothing", async () => {
+      const source = `
+module Foo exposing (..)
+
+y = case x of
+    Just a -> Just (a + 1)
+    Nothing -> Nothing
+			`;
+
+      await testDiagnostics(source, "map_nothing_to_nothing", [
+        diagnosticWithRange({
+          start: { line: 5, character: 4 },
+          end: { line: 5, character: 22 },
+        }),
+      ]);
+    });
+
+    it("map nothing to something", async () => {
+      const source = `
+module Foo exposing (..)
+
+y = case x of
+    Just a -> Just (a + 1)
+    Nothing -> 0
+			`;
+
+      await testDiagnostics(source, "map_nothing_to_nothing", []);
+    });
+
+    it("map something to nothing", async () => {
+      const source = `
+module Foo exposing (..)
+
+y = case x of
+    Just a -> Nothing
+    Nothing -> 0
+			`;
+
+      await testDiagnostics(source, "map_nothing_to_nothing", []);
+    });
+  });
+
+  describe("drop concat of lists", () => {
+    const diagnosticWithRange = (range: Range): Diagnostic => {
+      return {
+        code: "drop_concat_of_lists",
+        message: `If you concatenate two lists, then you can merge them into one list.`,
+        source: "ElmLS",
+        severity: DiagnosticSeverity.Warning,
+        range,
+      };
+    };
+
+    it("could use cons", async () => {
+      const source = `
+module Bar exposing (foo)
+
+foo : Int
+foo =
+    [1] ++ [3, 4]
+			`;
+
+      await testDiagnostics(source, "drop_concat_of_lists", [
+        diagnosticWithRange({
+          start: { line: 5, character: 4 },
+          end: { line: 5, character: 17 },
+        }),
+      ]);
+    });
+
+    it("no optimization", async () => {
+      const source = `
+module Bar exposing (foo)
+
+foo : Int
+foo =
+    [1, 2] ++ var
+			`;
+
+      await testDiagnostics(source, "drop_concat_of_lists", []);
+    });
+
+    it("concat multi element list", async () => {
+      const source = `
+module Bar exposing (foo)
+
+foo : Int
+foo =
+    [1, 2] ++ [3, 4]
+			`;
+
+      await testDiagnostics(source, "drop_concat_of_lists", [
+        diagnosticWithRange({
+          start: { line: 5, character: 4 },
+          end: { line: 5, character: 20 },
+        }),
+      ]);
+    });
+  });
+
+  describe("use cons over concat", () => {
+    const diagnosticWithRange = (range: Range): Diagnostic => {
+      return {
+        code: "use_cons_over_concat",
+        message: `If you concatenate two lists, but the first item is a single element list, then you should use the cons operator.`,
+        source: "ElmLS",
+        severity: DiagnosticSeverity.Warning,
+        range,
+      };
+    };
+
+    it("no optimization", async () => {
+      const source = `
+module Bar exposing (foo)
+
+foo : Int
+foo =
+    [1, 2] ++ bar
+			`;
+
+      await testDiagnostics(source, "use_cons_over_concat", []);
+    });
+
+    it("concat single item to var", async () => {
+      const source = `
+module Bar exposing (foo)
+
+foo : Int
+foo =
+    [1] ++ bar
+			`;
+
+      await testDiagnostics(source, "use_cons_over_concat", [
+        diagnosticWithRange({
+          start: { line: 5, character: 4 },
+          end: { line: 5, character: 14 },
+        }),
+      ]);
+    });
+  });
+
+  describe("single field record", () => {
+    const diagnosticWithRange = (range: Range): Diagnostic => {
+      return {
+        code: "single_field_record",
+        message: `Using a record is obsolete if you only plan to store a single field in it.`,
+        source: "ElmLS",
+        severity: DiagnosticSeverity.Warning,
+        range,
+      };
+    };
+
+    it("single field", async () => {
+      const source = `
+module Bar exposing (foo)
+
+type alias Foo =
+  { x : Int }
+			`;
+
+      await testDiagnostics(source, "single_field_record", [
+        diagnosticWithRange({
+          start: { line: 4, character: 2 },
+          end: { line: 4, character: 13 },
+        }),
+      ]);
+    });
+
+    it("single field generic", async () => {
+      const source = `
+module Bar exposing (foo)
+
+type alias Foo =
+  { a | x : Int }
+			`;
+
+      await testDiagnostics(source, "single_field_record", []);
+    });
+
+    it("multi field", async () => {
+      const source = `
+module Bar exposing (foo)
+
+type alias Foo =
+  { x : Int
+  , y : String
+  }
+			`;
+
+      await testDiagnostics(source, "single_field_record", []);
+    });
+
+    it("single field nested", async () => {
+      const source = `
+module Bar exposing (foo)
+
+type alias Foo =
+  { x : Int
+  , y : { z : String }
+  }
+			`;
+
+      await testDiagnostics(source, "single_field_record", [
+        diagnosticWithRange({
+          start: { line: 5, character: 8 },
+          end: { line: 5, character: 22 },
+        }),
+      ]);
+    });
+
+    it("single field in type", async () => {
+      const source = `
+module Bar exposing (foo)
+
+type Foo =
+  Bar { x : Int }
+			`;
+
+      await testDiagnostics(source, "single_field_record", [
+        diagnosticWithRange({
+          start: { line: 4, character: 6 },
+          end: { line: 4, character: 17 },
+        }),
+      ]);
+    });
+
+    it("single field as generic arg", async () => {
+      const source = `
+module Bar exposing (foo)
+
+type alias Params x a =
+    { x
+        | id : String
+        , label : String
+        , action : a
+    }
+-- more type aliasses that extends from Params
+type alias CheckboxParams a =
+    Params { checked : Bool } (Bool -> a)
+			`;
+
+      await testDiagnostics(source, "single_field_record", []);
+    });
+  });
+
+  describe("unnecessary list concat", () => {
+    const diagnosticWithRange = (range: Range): Diagnostic => {
+      return {
+        code: "unnecessary_list_concat",
+        message: `You should just merge the arguments of \`List.concat\` to a single list.`,
+        source: "ElmLS",
+        severity: DiagnosticSeverity.Warning,
+        range,
+      };
+    };
+
+    it("could merge", async () => {
+      const source = `
+module Bar exposing (foo)
+
+foo =
+    List.concat [ [1], [2] ]
+			`;
+
+      await testDiagnostics(source, "unnecessary_list_concat", [
+        diagnosticWithRange({
+          start: { line: 4, character: 4 },
+          end: { line: 4, character: 28 },
+        }),
+      ]);
+    });
+
+    it("could merge 2", async () => {
+      const source = `
+module Bar exposing (foo)
+
+foo x =
+  case x of
+    DropConsOfItemAndList fileName range ->
+        ( String.concat
+            [ "Adding an item to the front of a literal list, but instead you can just put it in the list. "
+            , fileName
+            , " at "
+            , rangeToString range
+            ]
+        , always (List.concat [ [ fileName ], [] ])
+        , [ range ]
+        , True
+        )
+			`;
+
+      await testDiagnostics(source, "unnecessary_list_concat", [
+        diagnosticWithRange({
+          start: { line: 12, character: 18 },
+          end: { line: 12, character: 50 },
+        }),
+      ]);
+    });
+
+    it("could not merge", async () => {
+      const source = `
+module Bar exposing (foo)
+
+foo =
+    List.concat [ bar, [2] ]
+			`;
+
+      await testDiagnostics(source, "unnecessary_list_concat", []);
+    });
+  });
+
+  describe("unnecessary port module", () => {
+    const diagnosticWithRange = (range: Range): Diagnostic => {
+      return {
+        code: "unnecessary_port_module",
+        message: `Module is definined as a \`port\` module, but does not define any ports.`,
+        source: "ElmLS",
+        severity: DiagnosticSeverity.Warning,
+        range,
+      };
+    };
+
+    it("no ports", async () => {
+      const source = `
+port module Bar exposing (foo)
+
+foo = 1
+
+bar = 2
+
+type alias Foo = {}
+
+type Bar = Other
+			`;
+
+      await testDiagnostics(source, "unnecessary_port_module", [
+        diagnosticWithRange({
+          start: { line: 1, character: 0 },
+          end: { line: 1, character: 30 },
+        }),
+      ]);
+    });
+
+    it("some ports", async () => {
+      const source = `
+port module Bar exposing (foo)
+
+bar = 2
+
+type alias Foo = {}
+
+type Bar = Other
+
+port foo : String -> Cmd msg
+			`;
+
+      await testDiagnostics(source, "unnecessary_port_module", []);
+    });
+  });
+
+  describe("fully applied operator as prefix", () => {
+    const diagnosticWithRange = (range: Range): Diagnostic => {
+      return {
+        code: "no_uncurried_prefix",
+        message: `Don't use fully applied prefix notation for operators.`,
+        source: "ElmLS",
+        severity: DiagnosticSeverity.Warning,
+        range,
+      };
+    };
+
+    it("prefix as application with two args", async () => {
+      const source = `
+module Foo exposing (..)
+
+foo = (+) 1 2
+			`;
+
+      await testDiagnostics(source, "no_uncurried_prefix", [
+        diagnosticWithRange({
+          start: { line: 3, character: 6 },
+          end: { line: 3, character: 13 },
+        }),
+      ]);
+    });
+
+    it("prefix as application with one arg", async () => {
+      const source = `
+module Foo exposing (..)
+
+foo = (+) 1
+			`;
+
+      await testDiagnostics(source, "no_uncurried_prefix", []);
+    });
+  });
+
+  describe("unused type alias", () => {
+    const diagnosticWithRangeAndName = (
+      range: Range,
+      name: string,
+    ): Diagnostic => {
+      return {
+        code: "unused_type_alias",
+        message: `Type alias \`${name}\` is not used.`,
+        source: "ElmLS",
+        severity: DiagnosticSeverity.Warning,
+        range,
+        tags: [DiagnosticTag.Unnecessary],
+      };
+    };
+
+    it("unused but exposed", async () => {
+      const source = `
+module Foo exposing (Bar)
+
+type alias Bar = Int
+			`;
+
+      await testDiagnostics(source, "unused_type_alias", []);
+    });
+
+    it("used in signature", async () => {
+      const source = `
+module Foo exposing (foo)
+
+type alias Bar = Int
+
+foo : Bar
+foo = 1
+			`;
+
+      await testDiagnostics(source, "unused_type_alias", []);
+    });
+
+    it("used as function", async () => {
+      const source = `
+module Foo exposing (foo)
+
+type alias Person = { name : String, age : Int}
+
+foo =
+    Person "John" 12
+			`;
+
+      await testDiagnostics(source, "unused_type_alias", []);
+    });
+
+    it("used in port", async () => {
+      const source = `
+module Foo exposing (foo)
+
+type alias Person = { name : String, age : Int}
+
+port foo : Person -> Cmd msg
+			`;
+
+      await testDiagnostics(source, "unused_type_alias", []);
+    });
+
+    it("used alias in record", async () => {
+      const source = `
+module Foo exposing (InputInterfaces)
+
+type alias InputFiles =
+    List String
+
+type alias InputInterfaces =
+    List ( String, InputFiles )
+			`;
+
+      await testDiagnostics(source, "unused_type_alias", []);
+    });
+
+    it("used alias in type", async () => {
+      const source = `
+module Foo exposing (Patch(..))
+
+type alias InputFiles =
+    List String
+
+type Patch
+    = OnFiles InputFiles
+			`;
+
+      await testDiagnostics(source, "unused_type_alias", []);
+    });
+
+    it("unused type alias", async () => {
+      const source = `
+module Foo exposing (foo)
+
+type alias Person = { name : String, age : Int}
+
+foo = 1
+			`;
+
+      await testDiagnostics(source, "unused_type_alias", [
+        diagnosticWithRangeAndName(
+          {
+            start: { line: 3, character: 0 },
+            end: { line: 3, character: 47 },
+          },
+          "Person",
+        ),
+      ]);
+    });
+  });
+
+  describe("unused value constructor", () => {
+    const diagnosticWithRangeAndName = (
+      range: Range,
+      name: string,
+    ): Diagnostic => {
+      return {
+        code: "unused_value_constructor",
+        message: `Value constructor \`${name}\` is not used.`,
+        source: "ElmLS",
+        severity: DiagnosticSeverity.Warning,
+        range,
+        tags: [DiagnosticTag.Unnecessary],
+      };
+    };
+
+    it("unused but exposed", async () => {
+      const source = `
+module Foo exposing (Foo(..))
+
+type Foo = Bar
+			`;
+
+      await testDiagnostics(source, "unused_value_constructor", []);
+    });
+
+    it("used and not exposed", async () => {
+      const source = `
+module Foo exposing (foo)
+
+type Foo = Bar Int
+
+foo = Bar 1
+			`;
+
+      await testDiagnostics(source, "unused_value_constructor", []);
+    });
+
+    it("unused and not exposed", async () => {
+      const source = `
+module Foo exposing (foo)
+
+type Foo = Bar Int
+
+foo (Bar i) = i
+			`;
+
+      await testDiagnostics(source, "unused_value_constructor", [
+        diagnosticWithRangeAndName(
+          {
+            start: { line: 3, character: 11 },
+            end: { line: 3, character: 18 },
+          },
+          "Bar",
+        ),
+      ]);
+    });
+
+    it("used value constructor", async () => {
+      const source = `
+module Bar exposing (foo)
+
+type Some = Thing
+
+foo = Thing
+			`;
+
+      await testDiagnostics(source, "unused_value_constructor", []);
+    });
+
+    it("unused value constructor not exposed", async () => {
+      const source = `
+module Bar exposing (foo, Some))
+
+type Some = Thing | Other
+			`;
+
+      await testDiagnostics(source, "unused_value_constructor", [
+        diagnosticWithRangeAndName(
+          {
+            start: { line: 3, character: 12 },
+            end: { line: 3, character: 17 },
+          },
+          "Thing",
+        ),
+        diagnosticWithRangeAndName(
+          {
+            start: { line: 3, character: 20 },
+            end: { line: 3, character: 25 },
+          },
+          "Other",
+        ),
+      ]);
+    });
+
+    it("exposed value constructor", async () => {
+      const source = `
+module Bar exposing (foo, Some(..)))
+
+type Some = Thing
+
+foo = 1
+			`;
+
+      await testDiagnostics(source, "unused_value_constructor", []);
+    });
+  });
+});
diff --git a/test/utils/sourceParser.ts b/test/utils/sourceParser.ts
index e799ee4..7e01e93 100644
--- a/test/utils/sourceParser.ts
+++ b/test/utils/sourceParser.ts
@@ -175,7 +175,7 @@ export function getTargetPositionFromSource(
   }
 }
 
-function getSourceFiles(source: string): { [K: string]: string } {
+export function getSourceFiles(source: string): { [K: string]: string } {
   const sources: { [K: string]: string } = {};
   let currentFile = "";
   const regex = /--@ ([a-zA-Z/]+.elm)/;