Repository: elm-tooling/elm-language-server
Pull request #587 (2021-05-04 12:46:35)

Improve unused_pattern for empty constructs

jmbockhorst: Any test case or example of this?

Patch:
diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts
index eec8f78..271ba12 100644
--- a/src/compiler/binder.ts
+++ b/src/compiler/binder.ts
@@ -179,7 +179,7 @@ export function bindTreeContainer(sourceFile: ISourceFile): void {
             };
           }
         })
-        .filter(Utils.notUndefined) ?? [];
+        .filter(Utils.notUndefined.bind(bindExposing)) ?? [];
 
     // Union variants get bound to the parent container
     unionVariants.forEach((variant) => {
diff --git a/src/compiler/patternMatches.ts b/src/compiler/patternMatches.ts
index 2afa54c..010ac5f 100644
--- a/src/compiler/patternMatches.ts
+++ b/src/compiler/patternMatches.ts
@@ -150,7 +150,7 @@ export class PatternMatches {
           return this.isExhaustive(
             matrix
               .map(this.specializeRowByAnything.bind(this))
-              .filter(Utils.notUndefined),
+              .filter(Utils.notUndefined.bind(this)),
             n - 1,
           ).map((result) => [Anything, ...result]);
         } else {
@@ -159,11 +159,11 @@ export class PatternMatches {
           if (numSeen < alts.numAlts) {
             const missing: Pattern[] = alts.alts
               .map((alt) => isMissing(alts, ctors, alt))
-              .filter(Utils.notUndefined);
+              .filter(Utils.notUndefined.bind(this));
             return this.isExhaustive(
               matrix
                 .map(this.specializeRowByAnything.bind(this))
-                .filter(Utils.notUndefined),
+                .filter(Utils.notUndefined.bind(this)),
               n - 1,
             ).flatMap((ex) => missing.map((m) => [m, ...ex]));
           } else {
@@ -173,7 +173,7 @@ export class PatternMatches {
                   .map((row) =>
                     this.specializeRowByCtor(ctor.name, ctor.arity, row),
                   )
-                  .filter(Utils.notUndefined),
+                  .filter(Utils.notUndefined.bind(this)),
                 ctor.arity + n - 1,
               ).map((patterns) =>
                 recoverCtor(alts, ctor.name, ctor.arity, patterns),
@@ -234,7 +234,7 @@ export class PatternMatches {
             return this.isUseful(
               matrix
                 .map((row) => this.specializeRowByCtor(name, args.length, row))
-                .filter(Utils.notUndefined),
+                .filter(Utils.notUndefined.bind(this)),
               [...args, ...patterns],
             );
           }
@@ -246,7 +246,7 @@ export class PatternMatches {
               return this.isUseful(
                 matrix
                   .map(this.specializeRowByAnything.bind(this))
-                  .filter(Utils.notUndefined),
+                  .filter(Utils.notUndefined.bind(this)),
                 patterns,
               );
             } else {
@@ -256,7 +256,7 @@ export class PatternMatches {
                     .map((row) =>
                       this.specializeRowByCtor(alt.name, alt.arity, row),
                     )
-                    .filter(Utils.notUndefined),
+                    .filter(Utils.notUndefined.bind(this)),
                   [...replicate(Anything, alt.arity), ...patterns],
                 );
               });
@@ -268,7 +268,7 @@ export class PatternMatches {
             return this.isUseful(
               matrix
                 .map((row) => this.specializeRowByLiteral(literal, row))
-                .filter(Utils.notUndefined),
+                .filter(Utils.notUndefined.bind(this)),
               patterns,
             );
           }
diff --git a/src/compiler/references.ts b/src/compiler/references.ts
index 271ed7a..49f91da 100644
--- a/src/compiler/references.ts
+++ b/src/compiler/references.ts
@@ -453,10 +453,10 @@ export class References {
                   );
 
                 if (imported) {
-                  const importNameNode = checker.findImportModuleNameNodes(
+                  const importNameNode = checker.findImportModuleNameNode(
                     moduleNameToLookFor,
                     sourceFileToCheck,
-                  )[0];
+                  );
 
                   if (importNameNode) {
                     references.push({ node: importNameNode, uri });
@@ -799,7 +799,7 @@ export class References {
       .map((field) =>
         TreeUtils.findFirstNamedChildOfType("lower_case_identifier", field),
       )
-      .filter(Utils.notUndefinedOrNull)
+      .filter(Utils.notUndefinedOrNull.bind(this))
       .filter((field) => field.text === fieldName);
   }
 
diff --git a/src/compiler/typeChecker.ts b/src/compiler/typeChecker.ts
index c48effc..9254d7c 100644
--- a/src/compiler/typeChecker.ts
+++ b/src/compiler/typeChecker.ts
@@ -91,10 +91,10 @@ export interface TypeChecker {
     sourceFile: ISourceFile,
     cancellationToken?: ICancellationToken,
   ) => Diagnostic[];
-  findImportModuleNameNodes: (
+  findImportModuleNameNode: (
     moduleNameOrAlias: string,
     sourceFile: ISourceFile,
-  ) => SyntaxNode[];
+  ) => SyntaxNode | undefined;
 }
 
 export function createTypeChecker(program: IProgram): TypeChecker {
@@ -123,7 +123,7 @@ export function createTypeChecker(program: IProgram): TypeChecker {
     getDiagnostics,
     getDiagnosticsAsync,
     getSuggestionDiagnostics,
-    findImportModuleNameNodes,
+    findImportModuleNameNode,
   };
 
   return typeChecker;
@@ -549,7 +549,7 @@ export function createTypeChecker(program: IProgram): TypeChecker {
 
         const moduleNameOrAlias = nodeParent.text.substring(0, endPos);
         const moduleName =
-          findImportModuleNameNodes(moduleNameOrAlias, sourceFile)[0]?.text ??
+          findImportModuleNameNode(moduleNameOrAlias, sourceFile)?.text ??
           moduleNameOrAlias;
 
         const moduleImport = findImport(sourceFile, moduleName, "Module")[0];
@@ -564,7 +564,7 @@ export function createTypeChecker(program: IProgram): TypeChecker {
 
       const moduleImport = findImport(
         sourceFile,
-        findImportModuleNameNodes(upperCaseQidText, sourceFile)[0]?.text ??
+        findImportModuleNameNode(upperCaseQidText, sourceFile)?.text ??
           upperCaseQidText,
         "Module",
       )[0];
@@ -622,7 +622,7 @@ export function createTypeChecker(program: IProgram): TypeChecker {
               (s) => s.node.type !== "infix_declaration",
             ),
         )
-        .find(Utils.notUndefined);
+        .find(Utils.notUndefined.bind(findDefinition));
 
       if (localBinding) {
         return {
@@ -642,7 +642,7 @@ export function createTypeChecker(program: IProgram): TypeChecker {
               ?.map((node) => node.text)
               .join(".") ?? "";
           const moduleName =
-            findImportModuleNameNodes(moduleNameOrAlias, sourceFile)[0]?.text ??
+            findImportModuleNameNode(moduleNameOrAlias, sourceFile)?.text ??
             moduleNameOrAlias;
 
           const moduleImport = findImport(sourceFile, moduleName, "Module")[0];
@@ -762,7 +762,7 @@ export function createTypeChecker(program: IProgram): TypeChecker {
 
       const moduleImport = findImport(
         sourceFile,
-        findImportModuleNameNodes(fullModuleName, sourceFile)[0]?.text ??
+        findImportModuleNameNode(fullModuleName, sourceFile)?.text ??
           fullModuleName,
         "Module",
       )[0];
@@ -850,8 +850,7 @@ export function createTypeChecker(program: IProgram): TypeChecker {
       return "";
     }
 
-    const moduleImport = findImportModuleNameNodes(module, sourceFile)[0]
-      ?.parent;
+    const moduleImport = findImportModuleNameNode(module, sourceFile)?.parent;
 
     if (!moduleImport) {
       return;
@@ -876,17 +875,15 @@ export function createTypeChecker(program: IProgram): TypeChecker {
   /**
    * Get the module name node if the name is an alias
    */
-  function findImportModuleNameNodes(
+  function findImportModuleNameNode(
     moduleNameOrAlias: string,
     sourceFile: ISourceFile,
-  ): SyntaxNode[] {
+  ): SyntaxNode | undefined {
     return (
       sourceFile.symbolLinks
         ?.get(sourceFile.tree.rootNode)
-        ?.getAll(moduleNameOrAlias)
-        ?.filter((s) => s.type === "Import")
-        .map((s) => s.node.childForFieldName("moduleName"))
-        .filter(Utils.notUndefinedOrNull) ?? []
+        ?.get(moduleNameOrAlias, (s) => s.type === "Import")
+        ?.node.childForFieldName("moduleName") ?? undefined
     );
   }
 
diff --git a/src/compiler/typeExpression.ts b/src/compiler/typeExpression.ts
index 247223e..7fc4eca 100644
--- a/src/compiler/typeExpression.ts
+++ b/src/compiler/typeExpression.ts
@@ -404,7 +404,7 @@ export class TypeExpression {
         typeRef,
       )
         ?.map(mapSyntaxNodeToExpression)
-        .filter(Utils.notUndefined)
+        .filter(Utils.notUndefined.bind(this))
         .map((arg) => this.typeSignatureSegmentType(arg)) ?? [];
 
     const definition = findDefinition(
diff --git a/src/compiler/typeInference.ts b/src/compiler/typeInference.ts
index 171d21a..d218875 100644
--- a/src/compiler/typeInference.ts
+++ b/src/compiler/typeInference.ts
@@ -539,7 +539,7 @@ export class InferenceScope {
   private getBinding(e: Expression): Type | undefined {
     return this.ancestors
       .map((a) => a.bindings.get(e))
-      .find(Utils.notUndefined);
+      .find(Utils.notUndefined.bind(this));
   }
 
   public static valueDeclarationInference(
@@ -910,7 +910,7 @@ export class InferenceScope {
         const patterns = pattern
           .descendantsOfType("lower_pattern")
           ?.map(mapSyntaxNodeToExpression)
-          .filter(Utils.notUndefined);
+          .filter(Utils.notUndefined.bind(this));
 
         patterns?.forEach((pat) => {
           const patType = result.expressionTypes.get(pat);
@@ -2242,7 +2242,7 @@ export class InferenceScope {
 
             return undefined;
           })
-          .filter(Utils.notUndefined),
+          .filter(Utils.notUndefined.bind(this)),
       ),
     );
   }
diff --git a/src/compiler/utils/expressionTree.ts b/src/compiler/utils/expressionTree.ts
index 4c3a23b..458b2b8 100644
--- a/src/compiler/utils/expressionTree.ts
+++ b/src/compiler/utils/expressionTree.ts
@@ -129,7 +129,7 @@ export interface ENumberConstant extends SyntaxNode {
   nodeType: "NumberConstant";
   isFloat: boolean;
 }
-export interface EStringConstant extends SyntaxNode {
+interface EStringConstant extends SyntaxNode {
   nodeType: "StringConstant";
 }
 export interface ETypeExpression extends SyntaxNode {
@@ -299,7 +299,7 @@ export function mapSyntaxNodeToExpression(
           binOpExpr.nodeType = "BinOpExpr";
           binOpExpr.parts = node.children
             .map(mapSyntaxNodeToExpression)
-            .filter(Utils.notUndefined);
+            .filter(Utils.notUndefined.bind(mapSyntaxNodeToExpression));
           return binOpExpr;
         }
       }
@@ -357,7 +357,7 @@ export function mapSyntaxNodeToExpression(
         typeExpression.nodeType = "TypeExpression";
         typeExpression.segments = node.children
           .map(mapSyntaxNodeToExpression)
-          .filter(Utils.notUndefined);
+          .filter(Utils.notUndefined.bind(mapSyntaxNodeToExpression));
         return typeExpression;
       }
 
@@ -398,7 +398,9 @@ export function mapSyntaxNodeToExpression(
         functionDeclarationLeft.params = node.namedChildren
           .filter((n) => n.type.includes("pattern") || n.type === "unit_expr")
           ?.map(mapSyntaxNodeToExpression)
-          .filter(Utils.notUndefined) as Pattern[];
+          .filter(
+            Utils.notUndefined.bind(mapSyntaxNodeToExpression),
+          ) as Pattern[];
         return functionDeclarationLeft;
       }
 
@@ -434,7 +436,7 @@ export function mapSyntaxNodeToExpression(
         unionVariant.params = node.children
           .slice(1)
           .map(mapSyntaxNodeToExpression)
-          .filter(Utils.notUndefined);
+          .filter(Utils.notUndefined.bind(mapSyntaxNodeToExpression));
         return unionVariant;
       }
 
@@ -443,7 +445,7 @@ export function mapSyntaxNodeToExpression(
         ifElseExpr.nodeType = "IfElseExpr";
         ifElseExpr.exprList = node.namedChildren
           .map((n) => mapSyntaxNodeToExpression(n))
-          .filter(Utils.notUndefined);
+          .filter(Utils.notUndefined.bind(mapSyntaxNodeToExpression));
         return ifElseExpr;
       }
 
@@ -469,7 +471,9 @@ export function mapSyntaxNodeToExpression(
         caseOfExpr.branches = node.namedChildren
           .slice(3)
           .map(mapSyntaxNodeToExpression)
-          .filter(Utils.notUndefined) as ECaseOfBranch[];
+          .filter(
+            Utils.notUndefined.bind(mapSyntaxNodeToExpression),
+          ) as ECaseOfBranch[];
         return caseOfExpr;
       }
 
@@ -511,7 +515,7 @@ export function mapSyntaxNodeToExpression(
         tupleExpr.nodeType = "TupleExpr";
         tupleExpr.exprList = node.namedChildren
           .map(mapSyntaxNodeToExpression)
-          .filter(Utils.notUndefined);
+          .filter(Utils.notUndefined.bind(mapSyntaxNodeToExpression));
         return tupleExpr;
       }
 
@@ -529,7 +533,9 @@ export function mapSyntaxNodeToExpression(
           node,
         )
           ?.map(mapSyntaxNodeToExpression)
-          .filter(Utils.notUndefined) as EPattern[];
+          .filter(
+            Utils.notUndefined.bind(mapSyntaxNodeToExpression),
+          ) as EPattern[];
         return tuplePattern;
       }
 
@@ -541,7 +547,9 @@ export function mapSyntaxNodeToExpression(
           node,
         )
           ?.map(mapSyntaxNodeToExpression)
-          .filter(Utils.notUndefined) as ETypeExpression[];
+          .filter(
+            Utils.notUndefined.bind(mapSyntaxNodeToExpression),
+          ) as ETypeExpression[];
         tupleType.unitExpr = mapSyntaxNodeToExpression(
           node.childForFieldName("unitExpr"),
         ) as EUnitExpr;
@@ -563,7 +571,7 @@ export function mapSyntaxNodeToExpression(
         listPattern.parts = node.namedChildren
           .filter((n) => n.type.includes("pattern"))
           .map(mapSyntaxNodeToExpression)
-          .filter(Utils.notUndefined);
+          .filter(Utils.notUndefined.bind(mapSyntaxNodeToExpression));
         return listPattern;
       }
 
@@ -574,7 +582,7 @@ export function mapSyntaxNodeToExpression(
         unionPattern.namedParams = node
           .descendantsOfType("lower_pattern")
           .map(mapSyntaxNodeToExpression)
-          .filter(Utils.notUndefined);
+          .filter(Utils.notUndefined.bind(mapSyntaxNodeToExpression));
         unionPattern.argPatterns = node.namedChildren
           .slice(1)
           .filter(
@@ -595,7 +603,7 @@ export function mapSyntaxNodeToExpression(
           parts: node.namedChildren
             .filter((n) => n.type.includes("pattern"))
             .map(mapSyntaxNodeToExpression)
-            .filter(Utils.notUndefined),
+            .filter(Utils.notUndefined.bind(mapSyntaxNodeToExpression)),
         } as EConsPattern);
 
       case "record_type": {
@@ -609,7 +617,9 @@ export function mapSyntaxNodeToExpression(
           node,
         )
           ?.map(mapSyntaxNodeToExpression)
-          .filter(Utils.notUndefined) as EFieldType[];
+          .filter(
+            Utils.notUndefined.bind(mapSyntaxNodeToExpression),
+          ) as EFieldType[];
         return recordType;
       }
 
@@ -664,7 +674,9 @@ export function mapSyntaxNodeToExpression(
           node,
         )
           ?.map(mapSyntaxNodeToExpression)
-          .filter(Utils.notUndefined) as ELowerPattern[];
+          .filter(
+            Utils.notUndefined.bind(mapSyntaxNodeToExpression),
+          ) as ELowerPattern[];
         return recordPattern;
       }
 
@@ -677,7 +689,9 @@ export function mapSyntaxNodeToExpression(
         recordExpr.fields =
           (TreeUtils.findAllNamedChildrenOfType("field", node)
             ?.map(mapSyntaxNodeToExpression)
-            .filter(Utils.notUndefined) as EField[]) ?? [];
+            .filter(
+              Utils.notUndefined.bind(mapSyntaxNodeToExpression),
+            ) as EField[]) ?? [];
         return recordExpr;
       }
 
@@ -744,7 +758,7 @@ export function mapTypeAliasDeclaration(
       typeAliasDeclaration,
     )
       ?.map(mapSyntaxNodeToExpression)
-      .filter(Utils.notUndefined) ?? [];
+      .filter(Utils.notUndefined.bind(mapSyntaxNodeToExpression)) ?? [];
   typeAliasDeclaration.typeExpression = mapSyntaxNodeToExpression(
     typeAliasDeclaration.childForFieldName("typeExpression"),
   ) as ETypeExpression;
diff --git a/src/protocol.ts b/src/protocol.ts
index afdc5a3..7506fb1 100644
--- a/src/protocol.ts
+++ b/src/protocol.ts
@@ -1,5 +1,4 @@
 import { CodeActionParams, RequestType } from "vscode-languageserver";
-import { URI } from "vscode-uri";
 import { IParams } from "./util/elmWorkspaceMatcher";
 
 // eslint-disable-next-line @typescript-eslint/no-namespace
@@ -52,24 +51,3 @@ export const GetDiagnosticsRequest = new RequestType<
   void,
   void
 >("elm/getDiagnostics");
-
-export const FindTestsRequest = new RequestType<
-  IFindTestsParams,
-  IFindTestsResponse,
-  void
->("elm/findTests");
-
-export interface IFindTestsParams {
-  projectFolder: URI;
-}
-
-export interface IFindTestsResponse {
-  suites?: TestSuite[];
-}
-
-export type TestSuite = {
-  label: string;
-  tests?: TestSuite[];
-  file: string;
-  position: { line: number; character: number };
-};
diff --git a/src/providers/codeAction/addMissingCaseBranchesCodeAction.ts b/src/providers/codeAction/addMissingCaseBranchesCodeAction.ts
index 8e9e7e1..790dc4b 100644
--- a/src/providers/codeAction/addMissingCaseBranchesCodeAction.ts
+++ b/src/providers/codeAction/addMissingCaseBranchesCodeAction.ts
@@ -50,7 +50,7 @@ function getEdits(params: ICodeActionParams, range: Range): TextEdit[] {
     );
     const patterns = branches
       .map((branch) => branch.childForFieldName("pattern"))
-      .filter(Utils.notUndefinedOrNull);
+      .filter(Utils.notUndefinedOrNull.bind(getEdits));
 
     const branchIndent = getSpaces(branches[0].startPosition.column);
     const branchExprIndent = getSpaces(
diff --git a/src/providers/codeAction/addMissingRecordFieldCodeAction.ts b/src/providers/codeAction/addMissingRecordFieldCodeAction.ts
index 560dc86..c73375d 100644
--- a/src/providers/codeAction/addMissingRecordFieldCodeAction.ts
+++ b/src/providers/codeAction/addMissingRecordFieldCodeAction.ts
@@ -8,7 +8,6 @@ import { Diagnostics } from "../../compiler/diagnostics";
 import { TypeChecker } from "../../compiler/typeChecker";
 import { Type } from "../../compiler/typeInference";
 import { ICodeActionParams } from "../paramsExtensions";
-import { Utils } from "../../util/utils";
 
 const errorCodes = [Diagnostics.RecordField.code];
 const fixId = "add_missing_record_field";
@@ -43,7 +42,7 @@ CodeActionProvider.registerCodeAction({
         // Not used
       },
       (changes, diagnostic) => {
-        Utils.mergeChanges(changes, getEdits(params, diagnostic.range));
+        mergeChanges(changes, getEdits(params, diagnostic.range));
       },
     );
   },
@@ -136,3 +135,16 @@ function createFields(
 
   return {};
 }
+
+function mergeChanges(
+  a: { [uri: string]: TextEdit[] },
+  b: { [uri: string]: TextEdit[] },
+): void {
+  Object.entries(b).forEach(([uri, edits]) => {
+    if (a[uri]) {
+      a[uri].push(...edits);
+    } else {
+      a[uri] = edits;
+    }
+  });
+}
diff --git a/src/providers/codeAction/index.ts b/src/providers/codeAction/index.ts
index b839e45..1a06af5 100644
--- a/src/providers/codeAction/index.ts
+++ b/src/providers/codeAction/index.ts
@@ -1,6 +1,5 @@
 import "./importCodeAction";
 import "./makeDeclarationFromUsageCodeAction";
-import "./makeExternalDeclarationFromUsageCodeAction";
 import "./addTypeAnnotationCodeAction";
 import "./extractFunctionCodeAction";
 import "./exposeUnexposeCodeAction";
diff --git a/src/providers/codeAction/makeExternalDeclarationFromUsageCodeAction.ts b/src/providers/codeAction/makeExternalDeclarationFromUsageCodeAction.ts
deleted file mode 100644
index 5eabd52..0000000
--- a/src/providers/codeAction/makeExternalDeclarationFromUsageCodeAction.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-import { Range } from "vscode-languageserver";
-import { TextEdit } from "vscode-languageserver-textdocument";
-import { RefactorEditUtils } from "../../util/refactorEditUtils";
-import { TreeUtils } from "../../util/treeUtils";
-import { Diagnostics } from "../../compiler/diagnostics";
-import { CodeActionProvider, ICodeAction } from "../codeActionProvider";
-import { ICodeActionParams } from "../paramsExtensions";
-import { Utils } from "../../util/utils";
-
-const errorCodes = [Diagnostics.MissingValue.code];
-const fixId = "make_external_declaration_from_usage";
-
-CodeActionProvider.registerCodeAction({
-  errorCodes,
-  fixId,
-  getCodeActions: (params: ICodeActionParams) => {
-    const allEdits = getEdits(params, params.range);
-
-    return allEdits
-      .map(([edits, moduleName]) => {
-        if (Object.keys(edits).length > 0) {
-          return CodeActionProvider.getCodeAction(
-            params,
-            `Create function in module '${moduleName}'`,
-            edits,
-          );
-        }
-      })
-      .filter(Utils.notUndefinedOrNull);
-  },
-  getFixAllCodeAction: (params: ICodeActionParams): ICodeAction | undefined => {
-    return CodeActionProvider.getFixAllCodeAction(
-      "Create all missing external functions",
-      params,
-      errorCodes,
-      fixId,
-      () => {
-        // Not used
-      },
-      (edits, diagnostic) => {
-        const firstEdits = getEdits(params, diagnostic.range)[0];
-        if (firstEdits) {
-          Utils.mergeChanges(edits, firstEdits[0]);
-        }
-      },
-    );
-  },
-});
-
-function getEdits(
-  params: ICodeActionParams,
-  range: Range,
-): [{ [uri: string]: TextEdit[] }, string][] {
-  const nodeAtPosition = TreeUtils.getNamedDescendantForRange(
-    params.sourceFile,
-    range,
-  );
-
-  if (
-    nodeAtPosition.type === "value_qid" &&
-    nodeAtPosition.parent?.type === "value_expr"
-  ) {
-    const funcName = nodeAtPosition.lastNamedChild?.text ?? nodeAtPosition.text;
-    const modulePrefix = nodeAtPosition.namedChildren
-      .slice(0, -2)
-      .map((n) => n.text)
-      .join("");
-
-    const checker = params.program.getTypeChecker();
-
-    const moduleNames = checker
-      .findImportModuleNameNodes(modulePrefix, params.sourceFile)
-      .map((n) => n.text);
-
-    return moduleNames
-      .map((moduleName) => {
-        const destinationSource = params.program.getSourceFileOfImportableModule(
-          params.sourceFile,
-          moduleName,
-        );
-
-        if (destinationSource && destinationSource.writeable) {
-          const type = checker.findType(nodeAtPosition);
-          const typeString: string = checker.typeToString(
-            type,
-            params.sourceFile,
-          );
-
-          const edit = RefactorEditUtils.createTopLevelFunction(
-            destinationSource.tree.rootNode.endPosition.row,
-            funcName,
-            typeString,
-            type.nodeType === "Function" ? type.params.length : 0,
-            `Debug.todo "TODO"`,
-          );
-
-          const exposeEdit = RefactorEditUtils.exposeValueInModule(
-            destinationSource.tree,
-            funcName,
-          );
-
-          if (edit && exposeEdit) {
-            return [
-              { [destinationSource.uri]: [edit, exposeEdit] },
-              moduleName,
-            ] as [{ [uri: string]: TextEdit[] }, string];
-          }
-        }
-      })
-      .filter(Utils.notUndefinedOrNull);
-  }
-
-  return [];
-}
diff --git a/src/providers/codeActionLs/removeUnusedCodeAction.ts b/src/providers/codeActionLs/removeUnusedCodeAction.ts
index 55349ba..e4f27e6 100644
--- a/src/providers/codeActionLs/removeUnusedCodeAction.ts
+++ b/src/providers/codeActionLs/removeUnusedCodeAction.ts
@@ -34,7 +34,7 @@ CodeActionProvider.registerCodeAction({
           return CodeActionProvider.getCodeAction(params, title, edits);
         }
       })
-      .filter(Utils.notUndefined);
+      .filter(Utils.notUndefined.bind(this));
   },
   getFixAllCodeAction: (params) => {
     const importsMap = new Map<string, Set<string>>();
diff --git a/src/providers/completionProvider.ts b/src/providers/completionProvider.ts
index 0235317..71c49e3 100644
--- a/src/providers/completionProvider.ts
+++ b/src/providers/completionProvider.ts
@@ -558,10 +558,10 @@ export class CompletionProvider {
       const dotIndex = label.lastIndexOf(".");
       const valuePart = label.slice(dotIndex + 1);
 
-      const importNode = checker.findImportModuleNameNodes(
+      const importNode = checker.findImportModuleNameNode(
         element.fromModule.name,
         sourceFile,
-      )[0]?.parent;
+      )?.parent;
 
       // Check if a value is already imported for this module using the exposing list
       // In this case, we want to prefex the unqualified value since they are using the import exposing list
@@ -1156,14 +1156,14 @@ export class CompletionProvider {
             } else if (!aMatches && bMatches) {
               return 1;
             } else {
-              const aModuleImported = !!checker.findImportModuleNameNodes(
+              const aModuleImported = !!checker.findImportModuleNameNode(
                 a.module,
                 sourceFile,
-              )[0];
-              const bModuleImported = !!checker.findImportModuleNameNodes(
+              );
+              const bModuleImported = !!checker.findImportModuleNameNode(
                 b.module,
                 sourceFile,
-              )[0];
+              );
 
               if (aModuleImported && !bModuleImported) {
                 return -1;
@@ -1281,7 +1281,7 @@ export class CompletionProvider {
               moduleName,
             );
           })
-          .filter(Utils.notUndefined),
+          .filter(Utils.notUndefined.bind(this)),
       );
 
       alreadyImported = true;
diff --git a/src/providers/diagnostics/elmLsDiagnostics.ts b/src/providers/diagnostics/elmLsDiagnostics.ts
index 534dfb5..087a7c9 100644
--- a/src/providers/diagnostics/elmLsDiagnostics.ts
+++ b/src/providers/diagnostics/elmLsDiagnostics.ts
@@ -486,7 +486,7 @@ export class ElmLsDiagnostics {
 
     const moduleReferences = this.moduleReferencesQuery
       .matches(tree.rootNode)
-      .filter(Utils.notUndefined)
+      .filter(Utils.notUndefined.bind(this))
       .filter(
         (match) =>
           match.captures.length > 0 &&
@@ -529,7 +529,7 @@ export class ElmLsDiagnostics {
 
     const allUsages = this.exposedValueAndTypeUsagesQuery
       .matches(tree.rootNode)
-      .filter(Utils.notUndefined);
+      .filter(Utils.notUndefined.bind(this));
     exposedValuesAndTypes.forEach((exposedValueOrType) => {
       if (exposedValueOrType.text.endsWith("(..)")) {
         return;
@@ -565,7 +565,7 @@ export class ElmLsDiagnostics {
 
     const allAliasReferences = this.moduleAliasReferencesQuery
       .matches(tree.rootNode)
-      .filter(Utils.notUndefined)
+      .filter(Utils.notUndefined.bind(this))
       .filter((match) => match.captures.length > 0)
       .map((match) => match.captures[0].node.text);
 
@@ -602,7 +602,7 @@ export class ElmLsDiagnostics {
     const scopeCache = new SyntaxNodeMap<SyntaxNode, QueryResult[]>();
 
     patternMatches
-      .filter(Utils.notUndefined)
+      .filter(Utils.notUndefined.bind(this))
       .flatMap((match) => {
         let scope = match.captures[0].node;
         const patternMatch = match.captures[1].node;
@@ -636,7 +636,7 @@ export class ElmLsDiagnostics {
           .getOrSet(scope, () =>
             this.patternReferencesQuery
               .matches(scope)
-              .filter(Utils.notUndefined),
+              .filter(Utils.notUndefined.bind(this)),
           )
           .filter(
             (result) =>
@@ -675,16 +675,21 @@ export class ElmLsDiagnostics {
             });
           }
         } else if (references.length === 0) {
-          {
-            diagnostics.push({
-              range: this.getNodeRange(pattern),
-              message: `Unused pattern variable \`${pattern.text}\``,
-              severity: DiagnosticSeverity.Warning,
-              source: "ElmLS",
-              tags: [DiagnosticTag.Unnecessary],
-              data: { uri: tree.uri, code: "unused_pattern" },
-            });
-          }
+          diagnostics.push({
+            // Extend the range for cases of {}
+            range: this.getNodeRange(
+              pattern.type === "lower_pattern" &&
+                pattern.text === "" &&
+                pattern.parent?.parent
+                ? pattern.parent.parent
+                : pattern,
+            ),
+            message: `Unused pattern variable \`${pattern.text}\``,
+            severity: DiagnosticSeverity.Warning,
+            source: "ElmLS",
+            tags: [DiagnosticTag.Unnecessary],
+            data: { uri: tree.uri, code: "unused_pattern" },
+          });
         }
       });
 
@@ -720,7 +725,7 @@ export class ElmLsDiagnostics {
     const caseExpressions = this.booleanCaseExpressionsQuery
       .matches(tree.rootNode)
       .map((match) => match.captures[0].node.parent)
-      .filter(Utils.notUndefinedOrNull);
+      .filter(Utils.notUndefinedOrNull.bind(this));
 
     caseExpressions.forEach((caseExpr) => {
       diagnostics.push({
@@ -741,7 +746,7 @@ export class ElmLsDiagnostics {
     const listExpressions = this.concatOfListsQuery
       .matches(tree.rootNode)
       .map((match) => [match.captures[0].node, match.captures[1].node])
-      .filter(Utils.notUndefinedOrNull);
+      .filter(Utils.notUndefinedOrNull.bind(this));
 
     listExpressions.forEach(([startList, endList]) => {
       diagnostics.push({
@@ -765,7 +770,7 @@ export class ElmLsDiagnostics {
     const consExpressions = this.consOfItemAndListQuery
       .matches(tree.rootNode)
       .map((match) => [match.captures[0].node, match.captures[1].node])
-      .filter(Utils.notUndefinedOrNull);
+      .filter(Utils.notUndefinedOrNull.bind(this));
 
     consExpressions.forEach(([itemExpr, listExpr]) => {
       diagnostics.push({
@@ -789,7 +794,7 @@ export class ElmLsDiagnostics {
     const concatExpressions = this.useConsOverConcatQuery
       .matches(tree.rootNode)
       .map((match) => [match.captures[0].node, match.captures[1].node])
-      .filter(Utils.notUndefinedOrNull);
+      .filter(Utils.notUndefinedOrNull.bind(this));
 
     concatExpressions.forEach(([firstPart, lastPart]) => {
       diagnostics.push({
@@ -817,7 +822,7 @@ export class ElmLsDiagnostics {
     const recordTypes = this.singleFieldRecordTypesQuery
       .matches(tree.rootNode)
       .map((match) => match.captures[0].node)
-      .filter(Utils.notUndefinedOrNull);
+      .filter(Utils.notUndefinedOrNull.bind(this));
 
     recordTypes.forEach((recordType) => {
       let isSingleField = true;
@@ -857,7 +862,7 @@ export class ElmLsDiagnostics {
     const listConcats = this.unnecessaryListConcatQuery
       .matches(tree.rootNode)
       .map((match) => match.captures[0].node)
-      .filter(Utils.notUndefinedOrNull);
+      .filter(Utils.notUndefinedOrNull.bind(this));
 
     listConcats.forEach((listConcat) => {
       diagnostics.push({
@@ -901,7 +906,7 @@ export class ElmLsDiagnostics {
     const operatorFunctions = this.operatorFunctionsQuery
       .matches(tree.rootNode)
       .map((match) => match.captures[0].node)
-      .filter(Utils.notUndefinedOrNull);
+      .filter(Utils.notUndefinedOrNull.bind(this));
 
     operatorFunctions.forEach((operatorFunction) => {
       diagnostics.push({
@@ -935,11 +940,11 @@ export class ElmLsDiagnostics {
     const typeAliases = this.typeAliasesQuery
       .matches(tree.rootNode)
       .map((match) => match.captures[0].node)
-      .filter(Utils.notUndefinedOrNull);
+      .filter(Utils.notUndefinedOrNull.bind(this));
 
     const typeAliasUsages = this.typeAliasUsagesQuery
       .matches(tree.rootNode)
-      .filter(Utils.notUndefined);
+      .filter(Utils.notUndefined.bind(this));
 
     typeAliases.forEach((typeAlias) => {
       const references = typeAliasUsages.filter(
@@ -979,11 +984,11 @@ export class ElmLsDiagnostics {
     const unionVariants = this.unionVariantsQuery
       .matches(tree.rootNode)
       .map((match) => [match.captures[1].node, match.captures[0].node])
-      .filter(Utils.notUndefinedOrNull);
+      .filter(Utils.notUndefinedOrNull.bind(this));
 
     const unionVariantUsages = this.unionVariantUsagesQuery
       .matches(tree.rootNode)
-      .filter(Utils.notUndefined);
+      .filter(Utils.notUndefined.bind(this));
 
     unionVariants.forEach(([unionVariant, typeName]) => {
       const references = unionVariantUsages.filter(
diff --git a/src/server.ts b/src/server.ts
index e185a35..55a8d3e 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -37,7 +37,6 @@ import { ElmLsDiagnostics } from "./providers/diagnostics/elmLsDiagnostics";
 import { FileEventsHandler } from "./providers/handlers/fileEventsHandler";
 import { Settings } from "./util/settings";
 import { TextDocumentEvents } from "./util/textDocumentEvents";
-import { FindTestsProvider } from "./providers/findTestsProvider";
 
 export interface ILanguageServer {
   readonly capabilities: InitializeResult;
@@ -191,8 +190,6 @@ export class Server implements ILanguageServer {
     new RenameProvider();
     new FileEventsHandler();
     new LinkedEditingRangesProvider();
-
-    new FindTestsProvider();
   }
 
   private getElmJsonFolder(uri: string): URI {
diff --git a/src/util/utils.ts b/src/util/utils.ts
index eb4f86a..68bb2d5 100644
--- a/src/util/utils.ts
+++ b/src/util/utils.ts
@@ -1,16 +1,13 @@
-import { Range, TextEdit } from "vscode-languageserver";
+import { Range } from "vscode-languageserver";
 
 export type NonEmptyArray<T> = [T, ...T[]];
 
 export class Utils {
-  public static notUndefined<T>(this: void, x: T | undefined): x is T {
+  public static notUndefined<T>(x: T | undefined): x is T {
     return x !== undefined;
   }
 
-  public static notUndefinedOrNull<T>(
-    this: void,
-    x: T | undefined | null,
-  ): x is T {
+  public static notUndefinedOrNull<T>(x: T | undefined | null): x is T {
     return x !== undefined && x !== null;
   }
 
@@ -87,17 +84,4 @@ export class Utils {
     }
     return newArray;
   }
-
-  public static mergeChanges(
-    a: { [uri: string]: TextEdit[] },
-    b: { [uri: string]: TextEdit[] },
-  ): void {
-    Object.entries(b).forEach(([uri, edits]) => {
-      if (a[uri]) {
-        a[uri].push(...edits);
-      } else {
-        a[uri] = edits;
-      }
-    });
-  }
 }


Test patch:
diff --git a/src/providers/findTestsProvider.ts b/src/providers/findTestsProvider.ts
deleted file mode 100644
index b67a67c..0000000
--- a/src/providers/findTestsProvider.ts
+++ /dev/null
@@ -1,274 +0,0 @@
-import { container } from "tsyringe";
-import { Connection, ResponseError } from "vscode-languageserver";
-import { SyntaxNode } from "web-tree-sitter";
-import { ISourceFile } from "../compiler/forest";
-import { IProgram, Program } from "../compiler/program";
-import { TypeChecker } from "../compiler/typeChecker";
-import { Type } from "../compiler/typeInference";
-import {
-  EFunctionCallExpr,
-  EValueExpr,
-  EStringConstant,
-  EListExpr,
-  mapSyntaxNodeToExpression,
-  Expression,
-  ELetInExpr,
-} from "../compiler/utils/expressionTree";
-import {
-  FindTestsRequest,
-  IFindTestsParams,
-  IFindTestsResponse,
-  TestSuite,
-} from "../protocol";
-import { NoWorkspaceContainsError } from "../util/noWorkspaceContainsError";
-import { TreeUtils } from "../util/treeUtils";
-import { Utils } from "../util/utils";
-
-export class FindTestsProvider {
-  constructor() {
-    const connection = container.resolve<Connection>("Connection");
-    connection.onRequest(FindTestsRequest, (params: IFindTestsParams) => {
-      connection.console.info(
-        `Finding tests is requested ${params.projectFolder.toString()}`,
-      );
-      try {
-        const elmWorkspaces: Program[] = container.resolve("ElmWorkspaces");
-        const program = elmWorkspaces.find(
-          (program) =>
-            program.getRootPath().toString() == params.projectFolder.toString(),
-        );
-        if (!program) {
-          throw new NoWorkspaceContainsError(params.projectFolder);
-        }
-        const suites = findAllTestSuites(program);
-        connection.console.info(
-          `Found ${
-            suites.length
-          } top test suites in ${params.projectFolder.toString()}`,
-        );
-        return <IFindTestsResponse>{ suites };
-      } catch (error) {
-        connection.console.error(`Error finding tests ${error}`);
-        return new ResponseError(1, `Error finding tests ${error}`);
-      }
-    });
-  }
-}
-
-// export for testing
-export function findAllTestSuites(program: IProgram): TestSuite[] {
-  const typeChecker = program.getTypeChecker();
-  return Array.from(program.getForest(true).treeMap.values())
-    .filter((sourceFile) => sourceFile.isTestFile)
-    .map((sourceFile) => {
-      const topSuites = TreeUtils.findAllTopLevelFunctionDeclarations(
-        sourceFile.tree,
-      )
-        ?.map((top) => {
-          return (
-            top &&
-            findTestSuite(
-              findTestFunctionCall(top, typeChecker),
-              sourceFile,
-              typeChecker,
-            )
-          );
-        })
-        .filter(Utils.notUndefined);
-      return topSuites ? rootSuite(sourceFile, topSuites) : undefined;
-    })
-    .filter(Utils.notUndefined);
-}
-
-function rootSuite(
-  sourceFile: ISourceFile,
-  topSuites: TestSuite[],
-): TestSuite | undefined {
-  const file = sourceFile.uri.toString();
-  const label = sourceFile.moduleName;
-  return label && topSuites.length > 0
-    ? <TestSuite>{
-        label,
-        tests: topSuites,
-        file,
-        position: { line: 0, character: 0 },
-      }
-    : undefined;
-}
-
-// export for testing
-export function findTestFunctionCall(
-  node: SyntaxNode,
-  typeChecker: TypeChecker,
-): EFunctionCallExpr | undefined {
-  const letIn = findChildExpr("LetInExpr", node);
-  if (letIn) {
-    return findTestFunctionCall(letIn.body, typeChecker);
-  }
-  const call = findExpr("FunctionCallExpr", node);
-  if (!call) {
-    return undefined;
-  }
-  const t: Type = typeChecker.findType(call);
-
-  const isTest = (t: Type): boolean =>
-    t.nodeType === "Union" && t.module === "Test.Internal" && t.name === "Test";
-
-  if (isTest(t)) {
-    return call;
-  }
-  if (t.nodeType === "Function" && isTest(t.return)) {
-    // TODO do we need this case?
-    return call;
-  }
-}
-
-function isTestSuite(
-  call: EFunctionCallExpr,
-  sourceFile: ISourceFile,
-  typeChecker: TypeChecker,
-): boolean {
-  const funName = findExpr("ValueExpr", call.target)?.name;
-  const dot = funName?.lastIndexOf(".") ?? -1;
-  const prefix = dot > -1 ? funName?.substring(0, dot) : undefined;
-  const qualifier: string =
-    prefix !== undefined
-      ? typeChecker.getQualifierForName(sourceFile, prefix, "describe") ?? ""
-      : "";
-  const moduleName =
-    prefix !== undefined
-      ? typeChecker.findImportModuleNameNodes(prefix, sourceFile)[0]?.text
-      : "Test";
-  return (
-    qualifier !== undefined &&
-    funName === `${qualifier}describe` &&
-    moduleName === "Test"
-  );
-}
-
-function findFirstStringArg(
-  call: EFunctionCallExpr,
-  typeChecker: TypeChecker,
-): Expression | undefined {
-  const stringArg = call.args.find((arg) => {
-    const t: Type = typeChecker.findType(arg);
-    return typeChecker.typeToString(t) === "String";
-  });
-  return stringArg;
-}
-
-// export for testing
-export function findTestSuite(
-  call: EFunctionCallExpr | undefined,
-  sourceFile: ISourceFile,
-  typeChecker: TypeChecker,
-): TestSuite | undefined {
-  if (!call) {
-    return undefined;
-  }
-
-  const stringArg = findFirstStringArg(call, typeChecker);
-  const labelParts = findAllExprs("StringConstant", stringArg)
-    ?.map((e) => e.text)
-    .map((l) => stringLiteralToLabel(l));
-  const position: TestSuite["position"] = {
-    line: call.startPosition.row,
-    character: call.startPosition.column,
-  };
-  const file = sourceFile.uri.toString();
-  const label = labelParts?.length === 1 ? labelParts[0] : undefined;
-  if (label && isTestSuite(call, sourceFile, typeChecker)) {
-    const testExprs = findExpr("ListExpr", call.args[1])?.exprList;
-    const tests: TestSuite[] | undefined = testExprs
-      ?.map((e) => findTestFunctionCall(e, typeChecker))
-      .map((call) => findTestSuite(call, sourceFile, typeChecker))
-      .filter(Utils.notUndefined);
-    return tests && tests.length > 0
-      ? <TestSuite>{ label, tests, file, position }
-      : undefined;
-  }
-  return label ? <TestSuite>{ label, file, position } : undefined;
-}
-
-type ExpressionNodeTypes = {
-  ValueExpr: EValueExpr;
-  StringConstant: EStringConstant;
-  ListExpr: EListExpr;
-  FunctionCallExpr: EFunctionCallExpr;
-  LetInExpr: ELetInExpr;
-};
-
-const typeByNodeType: Map<keyof ExpressionNodeTypes, string> = new Map([
-  ["ValueExpr", "value_expr"],
-  ["StringConstant", "string_constant_expr"],
-  ["ListExpr", "list_expr"],
-  ["FunctionCallExpr", "function_call_expr"],
-  ["LetInExpr", "let_in_expr"],
-]);
-
-function findExpr<K extends keyof ExpressionNodeTypes>(
-  key: K,
-  node: SyntaxNode | undefined,
-): ExpressionNodeTypes[K] | undefined {
-  if (!node) {
-    return undefined;
-  }
-  const type = typeByNodeType.get(key);
-  if (!type) {
-    return undefined;
-  }
-  const n =
-    node.type === type ? node : TreeUtils.descendantsOfType(node, type)[0];
-  const e = mapSyntaxNodeToExpression(n);
-  return e && mapExpr(key, e);
-}
-
-function findChildExpr<K extends keyof ExpressionNodeTypes>(
-  key: K,
-  node: SyntaxNode | undefined,
-): ExpressionNodeTypes[K] | undefined {
-  if (!node) {
-    return undefined;
-  }
-  const type = typeByNodeType.get(key);
-  if (!type) {
-    return undefined;
-  }
-  const n =
-    node.type === type ? node : TreeUtils.findFirstNamedChildOfType(type, node);
-  const e = mapSyntaxNodeToExpression(n);
-  return e && mapExpr(key, e);
-}
-
-function findAllExprs<K extends keyof ExpressionNodeTypes>(
-  key: K,
-  node: SyntaxNode | undefined,
-): ExpressionNodeTypes[K][] | undefined {
-  if (!node) {
-    return undefined;
-  }
-  const type = typeByNodeType.get(key);
-  if (!type) {
-    return undefined;
-  }
-  const children = TreeUtils.descendantsOfType(node, type);
-  const es = children?.map((n) => mapSyntaxNodeToExpression(n));
-  return es?.map((e) => e && mapExpr(key, e)).flatMap((v) => (v ? [v] : []));
-}
-
-function mapExpr<K extends keyof ExpressionNodeTypes>(
-  k: K,
-  e: Expression,
-): ExpressionNodeTypes[K] | undefined {
-  return e?.nodeType === k ? (e as ExpressionNodeTypes[K]) : undefined;
-}
-
-// export for testing
-export function stringLiteralToLabel(literal: string): string {
-  if (literal.startsWith('"""')) {
-    // without unescaping
-    return literal.substring(3, literal.length - 3);
-  }
-  // with unescaping
-  return String(JSON.parse(literal));
-}
diff --git a/test/codeActionTests/codeActionTestBase.ts b/test/codeActionTests/codeActionTestBase.ts
index 784d638..d18cd4a 100644
--- a/test/codeActionTests/codeActionTestBase.ts
+++ b/test/codeActionTests/codeActionTestBase.ts
@@ -141,16 +141,15 @@ export async function testCodeAction(
       trimTrailingWhitespace(expectedResultAfterEdits),
     );
 
-    const changesToApply = codeActions[testFixAll ? codeActions.length - 1 : 0]
-      .edit!.changes!;
-
     Object.entries(expectedSources).forEach(([uri, source]) => {
-      const edits = changesToApply[URI.file(baseUri + uri).toString()];
-      if (edits) {
-        expect(
-          applyEditsToSource(stripCommentLines(result.sources[uri]), edits),
-        ).toEqual(source);
-      }
+      expect(
+        applyEditsToSource(
+          stripCommentLines(result.sources[uri]),
+          codeActions[testFixAll ? codeActions.length - 1 : 0].edit!.changes![
+            URI.file(baseUri + uri).toString()
+          ],
+        ),
+      ).toEqual(source);
     });
   }
 }
diff --git a/test/codeActionTests/makeExternalDeclarationFromUsageCodeActions.test.ts b/test/codeActionTests/makeExternalDeclarationFromUsageCodeActions.test.ts
deleted file mode 100644
index 14eeb9e..0000000
--- a/test/codeActionTests/makeExternalDeclarationFromUsageCodeActions.test.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-import { testCodeAction } from "./codeActionTestBase";
-
-describe("make external declaration from usage", () => {
-  it("make external function", async () => {
-    const source = `
---@ Test.elm
-module Test exposing (..)
-
-import App
-
-func = App.bar
-          --^
-
---@ App.elm
-module App exposing (foo)
-
-foo = ""
-`;
-
-    const expectedSource = `
---@ Test.elm
-module Test exposing (..)
-
-import App
-
-func = App.bar
-          --^
-
---@ App.elm
-module App exposing (foo, bar)
-
-foo = ""
-
-
-
-bar : a
-bar =
-    Debug.todo "TODO"`;
-
-    await testCodeAction(
-      source,
-      [{ title: `Create function in module 'App'` }],
-      expectedSource,
-    );
-  });
-
-  it("make external function in nested module", async () => {
-    const source = `
---@ Test.elm
-module Test exposing (..)
-
-import Modules.App
-
-func = Modules.App.bar
-                  --^
-
---@ Modules/App.elm
-module Modules.App exposing (foo)
-
-foo = ""
-`;
-
-    const expectedSource = `
---@ Test.elm
-module Test exposing (..)
-
-import Modules.App
-
-func = Modules.App.bar
-
---@ Modules/App.elm
-module Modules.App exposing (foo, bar)
-
-foo = ""
-
-
-
-bar : a
-bar =
-    Debug.todo "TODO"`;
-
-    await testCodeAction(
-      source,
-      [{ title: `Create function in module 'Modules.App'` }],
-      expectedSource,
-    );
-  });
-
-  it("make external function in aliased module", async () => {
-    const source = `
---@ Test.elm
-module Test exposing (..)
-
-import Modules.App as App
-
-func = App.bar
-          --^
-
---@ Modules/App.elm
-module Modules.App exposing (foo)
-
-foo = ""
-`;
-
-    const expectedSource = `
---@ Test.elm
-module Test exposing (..)
-
-import Modules.App as App
-
-func = App.bar
-
---@ Modules/App.elm
-module Modules.App exposing (foo, bar)
-
-foo = ""
-
-
-
-bar : a
-bar =
-    Debug.todo "TODO"`;
-
-    await testCodeAction(
-      source,
-      [{ title: `Create function in module 'Modules.App'` }],
-      expectedSource,
-    );
-  });
-
-  it("make external function in multiple modules", async () => {
-    const source = `
---@ Test.elm
-module Test exposing (..)
-
-import Modules.App as App
-import App
-
-func = App.bar
-          --^
-
---@ Modules/App.elm
-module Modules.App exposing (foo)
-
-foo = ""
-
---@ App.elm
-module Modules.App exposing (foo)
-
-foo = ""
-`;
-
-    await testCodeAction(source, [
-      { title: `Create function in module 'Modules.App'` },
-      { title: `Create function in module 'App'` },
-    ]);
-  });
-
-  it("make external function in non existent module", async () => {
-    const source = `
---@ Test.elm
-module Test exposing (..)
-
-import Modules.App as App
-
-func = App.bar
-          --^
-`;
-
-    await testCodeAction(source, []);
-  });
-});
diff --git a/test/diagnosticTests/elmLsDiagnostics.test.ts b/test/diagnosticTests/elmLsDiagnostics.test.ts
index efece76..07961e4 100644
--- a/test/diagnosticTests/elmLsDiagnostics.test.ts
+++ b/test/diagnosticTests/elmLsDiagnostics.test.ts
@@ -942,6 +942,27 @@ x =
 
       await testDiagnostics(source, "unused_pattern", []);
     });
+
+    it("unused {}", async () => {
+      const source = `
+module Foo exposing (foo)
+
+foo value =
+    case value of
+        {} ->
+            "test"
+			`;
+
+      await testDiagnostics(source, "unused_pattern", [
+        diagnosticWithRangeAndName(
+          {
+            start: { line: 5, character: 8 },
+            end: { line: 5, character: 10 },
+          },
+          "",
+        ),
+      ]);
+    });
   });
 
   describe("drop cons of item and list", () => {
diff --git a/test/findTestsProvider.test.ts b/test/findTestsProvider.test.ts
deleted file mode 100644
index 8cacc0d..0000000
--- a/test/findTestsProvider.test.ts
+++ /dev/null
@@ -1,422 +0,0 @@
-import { getSourceFiles } from "./utils/sourceParser";
-import { SourceTreeParser, testUri } from "./utils/sourceTreeParser";
-import { URI } from "vscode-uri";
-import {
-  findAllTestSuites,
-  stringLiteralToLabel,
-} from "../src/providers/findTestsProvider";
-import { TestSuite } from "../src/protocol";
-
-const basicsSources = `
---@ Basics.elm
-module Basics exposing ((<|),(++), Int, Float, Bool(..), Order(..), negate)
-
-infix left  0 (<|) = apL
-infix right 5 (++) = append
-
-type Int = Int
-
-type Float = Float
-
-type Bool = True | False
-
-add : number -> number -> number
-add =
-  Elm.Kernel.Basics.add
-
-append : appendable -> appendable -> appendable
-append =
-  Elm.Kernel.Utils.append
-
-apL : (a -> b) -> a -> b
-apL f x =
-  f x
-
---@ String.elm
-module String exposing (String,append)
-
-type String = String
-
-append : String -> String -> String
-append =
-  Elm.Kernel.String.append
-`;
-
-const sourceTestModule = `
---@ TestInternal.elm
-module Test.Internal exposing (Test(..))
-
-type Test = T
-
---@ Test.elm
-module Test exposing (Test(..), describe, test, fuzz)
-
-import Expect exposing (..)
-import Test.Internal as Internal
-
-type alias Test = Internal.Test
-
-describe : String -> List Test -> Test
-describe untrimmedDesc tests = T
-
-test : String -> (() -> Expectation) -> Test
-test untrimmedDesc thunk = T
-
-fuzz : Fuzzer a -> String -> (a -> Expectation) -> Test
-fuzz fuzzer desc thunk = T
-
---@ Expect.elm
-module Expect exposing (Expectation(..), equal)
-
-type Expectation = E
-
-equal : a -> a -> Expectation
-equal aa bb = E
-
---@ Fuzz.elm
-module Fuzz exposing (int)
-
-type Fuzzer a = F a
-
-int Fuzzer Int
-int = F 13
-`;
-
-describe("FindTestsProvider", () => {
-  const treeParser = new SourceTreeParser();
-
-  const testModuleUri = URI.file(testUri + "/MyModule.elm").toString();
-
-  async function testFindTests(source: string, expected: TestSuite[]) {
-    await treeParser.init();
-
-    const sources = getSourceFiles(basicsSources + sourceTestModule + source);
-    const program = await treeParser.getProgram(sources);
-
-    const suites = findAllTestSuites(program);
-    expect(suites).toEqual(expected);
-  }
-
-  const moduleSuite = (tests: TestSuite[]): TestSuite => {
-    return {
-      label: "MyModule",
-      file: testModuleUri,
-      position: { line: 0, character: 0 },
-      tests,
-    };
-  };
-
-  const aTest = (line: number, character: number): TestSuite => ({
-    label: "a test",
-    file: testModuleUri,
-    position: { line, character },
-  });
-
-  test("simple", async () => {
-    const source = `
---@ tests/MyModule.elm
-module MyModule exposing (..)
-
-import Test exposing (..)
-
-topSuite : Test
-topSuite =
-    describe "top suite" [ test "a test" <| \_ -> Expect.equal False False ]
-`;
-
-    await testFindTests(source, [
-      moduleSuite([
-        {
-          label: "top suite",
-          file: testModuleUri,
-          position: { line: 6, character: 4 },
-          tests: [aTest(6, 27)],
-        },
-      ]),
-    ]);
-  });
-
-  test("some", async () => {
-    const source = `
---@ tests/MyModule.elm
-module MyModule exposing (..)
-
-import Expect
-import Test exposing (..)
-
-topSuite : Test
-topSuite =
-    describe "top suite"
-    [ test "first" <| \_ -> Expect.equal True True
-    , describe "nested"
-        [ test "second" <| \_ -> Expect.equal False False
-        ]
-    ]
-`;
-
-    await testFindTests(source, [
-      moduleSuite([
-        {
-          label: "top suite",
-          file: testModuleUri,
-          position: { line: 7, character: 4 },
-          tests: [
-            {
-              label: "first",
-              file: testModuleUri,
-              position: { line: 8, character: 6 },
-            },
-            {
-              label: "nested",
-              file: testModuleUri,
-              position: { line: 9, character: 6 },
-              tests: [
-                {
-                  label: "second",
-                  file: testModuleUri,
-                  position: { line: 10, character: 10 },
-                },
-              ],
-            },
-          ],
-        },
-      ]),
-    ]);
-  });
-
-  test("ignore non Test top levels", async () => {
-    const source = `
---@ tests/MyModule.elm
-module MyModule exposing (..)
-
-import Test exposing (..)
-
-someThingElse = True
-
-topSuite = describe "top suite" [ test "a test" <| \_ -> Expect.equal False False ]
-`;
-
-    await testFindTests(source, [
-      moduleSuite([
-        {
-          label: "top suite",
-          file: testModuleUri,
-          position: { line: 6, character: 11 },
-          tests: [aTest(6, 34)],
-        },
-      ]),
-    ]);
-  });
-
-  test("with let/in", async () => {
-    const source = `
---@ tests/MyModule.elm
-module MyModule exposing (..)
-
-import Test exposing (..)
-
-topSuite : Test
-topSuite =
-    let
-        foo =
-            doit bar
-    in
-    let
-        foo =
-            doit bar
-    in
-    describe "top suite" [ test "a test" <| \_ -> Expect.equal False False ]
-`;
-
-    await testFindTests(source, [
-      moduleSuite([
-        {
-          label: "top suite",
-          file: testModuleUri,
-          position: { line: 14, character: 4 },
-          tests: [aTest(14, 27)],
-        },
-      ]),
-    ]);
-  });
-
-  test("with deep let/in", async () => {
-    const source = `
---@ tests/MyModule.elm
-module MyModule exposing (..)
-
-import Test exposing (..)
-
-topSuite : Test
-topSuite =
-    describe "top suite"
-        [ let
-            a =
-                doit 13
-          in
-          describe "deeper suite" [ test "a test" <| \_ -> Expect.equal False False ]
-        ]
-`;
-
-    await testFindTests(source, [
-      moduleSuite([
-        {
-          label: "top suite",
-          file: testModuleUri,
-          position: { line: 6, character: 4 },
-          tests: [
-            {
-              label: "deeper suite",
-              file: testModuleUri,
-              position: { line: 11, character: 10 },
-              tests: [aTest(11, 36)],
-            },
-          ],
-        },
-      ]),
-    ]);
-  });
-
-  test("import without expose", async () => {
-    const source = `
---@ tests/MyModule.elm
-module MyModule exposing (..)
-
-import Test
-
-topSuite = Test.describe "top suite" [ Test.test "a test" <| \_ -> Expect.equal False False ]
-`;
-
-    await testFindTests(source, [
-      moduleSuite([
-        {
-          label: "top suite",
-          file: testModuleUri,
-          position: { line: 4, character: 11 },
-          tests: [aTest(4, 39)],
-        },
-      ]),
-    ]);
-  });
-
-  test("import with alias", async () => {
-    const source = `
---@ tests/MyModule.elm
-module MyModule exposing (..)
-
-import Test as T
-
-topSuite = T.describe "top suite" [ T.test "a test" <| \_ -> Expect.equal False False ]
-`;
-
-    await testFindTests(source, [
-      moduleSuite([
-        {
-          label: "top suite",
-          file: testModuleUri,
-          position: { line: 4, character: 11 },
-          tests: [aTest(4, 36)],
-        },
-      ]),
-    ]);
-  });
-
-  test("dynamic label is ignored", async () => {
-    const source = `
---@ tests/MyModule.elm
-module MyModule exposing (..)
-
-import Test exposing (..)
-
-topSuite = describe ("top suite" ++ "13") [ test "a test" <| \_ -> Expect.equal False False ]
-`;
-
-    await testFindTests(source, []);
-  });
-
-  test("fuzz", async () => {
-    const source = `
---@ tests/MyModule.elm
-module MyModule exposing (..)
-
-import Expect
-import Test exposing (..)
-import Fuzz exposing (..)
-
-top = fuzz int "top fuzz" <| \_ -> Expect.equal True True
-`;
-
-    await testFindTests(source, [
-      moduleSuite([
-        {
-          label: "top fuzz",
-          file: testModuleUri,
-          position: { line: 6, character: 6 },
-        },
-      ]),
-    ]);
-  });
-
-  test("multiline label", async () => {
-    const source = `
---@ tests/MyModule.elm
-module MyModule exposing (..)
-
-import Test exposing (..)
-
-topSuite = describe """top suite
-over
-"multiple"
-lines
-""" [ test "a test" <| \_ -> Expect.equal False False ]
-`;
-
-    await testFindTests(source, [
-      moduleSuite([
-        {
-          label: 'top suite\nover\n"multiple"\nlines\n',
-          file: testModuleUri,
-          position: { line: 4, character: 11 },
-          tests: [aTest(8, 6)],
-        },
-      ]),
-    ]);
-  });
-
-  test("nested empty suites", async () => {
-    const source = `
---@ tests/MyModule.elm
-module MyModule exposing (..)
-
-import Expect
-import Test exposing (..)
-
-topSuite : Test
-topSuite =
-    describe "top suite"
-    [ describe "nested" []
-    ]
-`;
-
-    await testFindTests(source, []);
-  });
-});
-
-describe("string literal to label", () => {
-  test("simple", () => {
-    expect(stringLiteralToLabel('"a simple string"')).toEqual(
-      "a simple string",
-    );
-  });
-  test("escaped", () => {
-    expect(stringLiteralToLabel('"a simple \\\\ \\n string"')).toEqual(
-      "a simple \\ \n string",
-    );
-  });
-
-  test("multiline", () => {
-    expect(stringLiteralToLabel('"""a multi\nline\nstring"""')).toEqual(
-      "a multi\nline\nstring",
-    );
-  });
-});
diff --git a/test/utils/sourceTreeParser.ts b/test/utils/sourceTreeParser.ts
index 26cf250..8f3a043 100644
--- a/test/utils/sourceTreeParser.ts
+++ b/test/utils/sourceTreeParser.ts
@@ -10,7 +10,7 @@ import * as path from "../../src/util/path";
 import { Utils } from "../../src/util/utils";
 
 export const baseUri = path.join(__dirname, "../sources/src/");
-export const testUri = path.join(baseUri, "tests");
+const testUri = path.join(baseUri, "tests");
 
 export class SourceTreeParser {
   private parser?: Parser;