Repository: elm-tooling/elm-language-server
Pull request #324 (2020-09-30 05:59:12)

Fix anonymous function param definitions and add code actions for let expr functions
Fixed anonymous function param definitions and was able to enable some existing tests. This fixed some inference bugs. 
Also added a code action to add the type annotation to functions in let expr. 

Inference bugs fixed:
- WebGl type renders as `shader` (like intellij)
- Use module alias if there is one
- Fix functions that used anonymous functions with record or tuple param deconstructors (like `tuple` in `elm-codec`)

All other reported bugs behave the same as intellij-elm, so I don't think we should worry about those edge cases for now.


Patch:
diff --git a/package-lock.json b/package-lock.json
index eb3861a..93504bc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1204,13 +1204,13 @@
       "dev": true
     },
     "@typescript-eslint/eslint-plugin": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.3.0.tgz",
-      "integrity": "sha512-RqEcaHuEKnn3oPFislZ6TNzsBLqpZjN93G69SS+laav/I8w/iGMuMq97P0D2/2/kW4SCebHggqhbcCfbDaaX+g==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.2.0.tgz",
+      "integrity": "sha512-zBNRkzvLSwo6y5TG0DVcmshZIYBHKtmzD4N+LYnfTFpzc4bc79o8jNRSb728WV7A4Cegbs+MV5IRAj8BKBgOVQ==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/experimental-utils": "4.3.0",
-        "@typescript-eslint/scope-manager": "4.3.0",
+        "@typescript-eslint/experimental-utils": "4.2.0",
+        "@typescript-eslint/scope-manager": "4.2.0",
         "debug": "^4.1.1",
         "functional-red-black-tree": "^1.0.1",
         "regexpp": "^3.0.0",
@@ -1236,28 +1236,28 @@
       }
     },
     "@typescript-eslint/experimental-utils": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.3.0.tgz",
-      "integrity": "sha512-cmmIK8shn3mxmhpKfzMMywqiEheyfXLV/+yPDnOTvQX/ztngx7Lg/OD26J8gTZfkLKUmaEBxO2jYP3keV7h2OQ==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.2.0.tgz",
+      "integrity": "sha512-5BBj6BjgHEndBaQQpUVzRIPERz03LBc0MCQkHwUaH044FJFL08SwWv/sQftk7gf0ShZ2xZysz0LTwCwNt4Xu3w==",
       "dev": true,
       "requires": {
         "@types/json-schema": "^7.0.3",
-        "@typescript-eslint/scope-manager": "4.3.0",
-        "@typescript-eslint/types": "4.3.0",
-        "@typescript-eslint/typescript-estree": "4.3.0",
+        "@typescript-eslint/scope-manager": "4.2.0",
+        "@typescript-eslint/types": "4.2.0",
+        "@typescript-eslint/typescript-estree": "4.2.0",
         "eslint-scope": "^5.0.0",
         "eslint-utils": "^2.0.0"
       }
     },
     "@typescript-eslint/parser": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.3.0.tgz",
-      "integrity": "sha512-JyfRnd72qRuUwItDZ00JNowsSlpQGeKfl9jxwO0FHK1qQ7FbYdoy5S7P+5wh1ISkT2QyAvr2pc9dAemDxzt75g==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.2.0.tgz",
+      "integrity": "sha512-54jJ6MwkOtowpE48C0QJF9iTz2/NZxfKVJzv1ha5imigzHbNSLN9yvbxFFH1KdlRPQrlR8qxqyOvLHHxd397VA==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/scope-manager": "4.3.0",
-        "@typescript-eslint/types": "4.3.0",
-        "@typescript-eslint/typescript-estree": "4.3.0",
+        "@typescript-eslint/scope-manager": "4.2.0",
+        "@typescript-eslint/types": "4.2.0",
+        "@typescript-eslint/typescript-estree": "4.2.0",
         "debug": "^4.1.1"
       },
       "dependencies": {
@@ -1279,29 +1279,29 @@
       }
     },
     "@typescript-eslint/scope-manager": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.3.0.tgz",
-      "integrity": "sha512-cTeyP5SCNE8QBRfc+Lgh4Xpzje46kNUhXYfc3pQWmJif92sjrFuHT9hH4rtOkDTo/si9Klw53yIr+djqGZS1ig==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.2.0.tgz",
+      "integrity": "sha512-Tb402cxxObSxWIVT+PnBp5ruT2V/36yj6gG4C9AjkgRlZpxrLAzWDk3neen6ToMBGeGdxtnfFLoJRUecGz9mYQ==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "4.3.0",
-        "@typescript-eslint/visitor-keys": "4.3.0"
+        "@typescript-eslint/types": "4.2.0",
+        "@typescript-eslint/visitor-keys": "4.2.0"
       }
     },
     "@typescript-eslint/types": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.3.0.tgz",
-      "integrity": "sha512-Cx9TpRvlRjOppGsU6Y6KcJnUDOelja2NNCX6AZwtVHRzaJkdytJWMuYiqi8mS35MRNA3cJSwDzXePfmhU6TANw==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.2.0.tgz",
+      "integrity": "sha512-xkv5nIsxfI/Di9eVwN+G9reWl7Me9R5jpzmZUch58uQ7g0/hHVuGUbbn4NcxcM5y/R4wuJIIEPKPDb5l4Fdmwg==",
       "dev": true
     },
     "@typescript-eslint/typescript-estree": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.3.0.tgz",
-      "integrity": "sha512-ZAI7xjkl+oFdLV/COEz2tAbQbR3XfgqHEGy0rlUXzfGQic6EBCR4s2+WS3cmTPG69aaZckEucBoTxW9PhzHxxw==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.2.0.tgz",
+      "integrity": "sha512-iWDLCB7z4MGkLipduF6EOotdHNtgxuNKnYD54nMS/oitFnsk4S3S/TE/UYXQTra550lHtlv9eGmp+dvN9pUDtA==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "4.3.0",
-        "@typescript-eslint/visitor-keys": "4.3.0",
+        "@typescript-eslint/types": "4.2.0",
+        "@typescript-eslint/visitor-keys": "4.2.0",
         "debug": "^4.1.1",
         "globby": "^11.0.1",
         "is-glob": "^4.0.1",
@@ -1328,12 +1328,12 @@
       }
     },
     "@typescript-eslint/visitor-keys": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.3.0.tgz",
-      "integrity": "sha512-xZxkuR7XLM6RhvLkgv9yYlTcBHnTULzfnw4i6+z2TGBLy9yljAypQaZl9c3zFvy7PNI7fYWyvKYtohyF8au3cw==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-WIf4BNOlFOH2W+YqGWa6YKLcK/EB3gEj2apCrqLw6mme1RzBy0jtJ9ewJgnrZDB640zfnv8L+/gwGH5sYp/rGw==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "4.3.0",
+        "@typescript-eslint/types": "4.2.0",
         "eslint-visitor-keys": "^2.0.0"
       }
     },
diff --git a/package.json b/package.json
index eca6ef4..3a68ed3 100644
--- a/package.json
+++ b/package.json
@@ -31,8 +31,8 @@
   "devDependencies": {
     "@types/jest": "^26.0.14",
     "@types/node": "14.11.2",
-    "@typescript-eslint/eslint-plugin": "^4.3.0",
-    "@typescript-eslint/parser": "^4.3.0",
+    "@typescript-eslint/eslint-plugin": "^4.2.0",
+    "@typescript-eslint/parser": "^4.2.0",
     "copyfiles": "^2.3.0",
     "doctoc": "^1.4.0",
     "eslint": "^7.10.0",
diff --git a/src/providers/codeActionProvider.ts b/src/providers/codeActionProvider.ts
index 1fe7600..d355cc4 100644
--- a/src/providers/codeActionProvider.ts
+++ b/src/providers/codeActionProvider.ts
@@ -1,3 +1,5 @@
+import { findType } from "../util/types/typeInference";
+import { TypeRenderer } from "../util/types/typeRenderer";
 import { container } from "tsyringe";
 import {
   ApplyWorkspaceEditResponse,
@@ -6,6 +8,7 @@ import {
   CodeActionParams,
   ExecuteCommandParams,
   IConnection,
+  TextEdit,
 } from "vscode-languageserver";
 import { URI } from "vscode-uri";
 import { SyntaxNode, Tree } from "web-tree-sitter";
@@ -74,6 +77,7 @@ export class CodeActionProvider {
       ...make,
       ...typeAnnotation,
       ...this.getRefactorCodeActions(params, elmWorkspace),
+      ...this.getTypeAnnotationCodeActions(params, elmWorkspace),
     ];
   }
 
@@ -84,6 +88,61 @@ export class CodeActionProvider {
     return this.elmAnalyse && this.elmAnalyse.onExecuteCommand(params);
   }
 
+  private getTypeAnnotationCodeActions(
+    params: CodeActionParams,
+    elmWorkspace: IElmWorkspace,
+  ): CodeAction[] {
+    // Top level annotation are handled by diagnostics
+
+    const codeActions: CodeAction[] = [];
+
+    const forest = elmWorkspace.getForest();
+    const tree = forest.getTree(params.textDocument.uri);
+
+    if (tree) {
+      const nodeAtPosition = TreeUtils.getNamedDescendantForPosition(
+        tree.rootNode,
+        params.range.start,
+      );
+
+      if (
+        nodeAtPosition.parent?.type === "function_declaration_left" &&
+        TreeUtils.findParentOfType("let_in_expr", nodeAtPosition) &&
+        nodeAtPosition.parent.parent &&
+        !TreeUtils.getTypeAnnotation(nodeAtPosition.parent.parent)
+      ) {
+        const typeString: string = TypeRenderer.typeToString(
+          findType(nodeAtPosition, params.textDocument.uri, elmWorkspace),
+          nodeAtPosition.tree,
+          params.textDocument.uri,
+          elmWorkspace.getImports(),
+        );
+
+        codeActions.push({
+          edit: {
+            changes: {
+              [params.textDocument.uri]: [
+                TextEdit.insert(
+                  {
+                    line: nodeAtPosition.startPosition.row,
+                    character: nodeAtPosition.startPosition.column,
+                  },
+                  `${nodeAtPosition.text} : ${typeString}\n${Array(
+                    nodeAtPosition.startPosition.column + 1,
+                  ).join(" ")}`,
+                ),
+              ],
+            },
+          },
+          kind: CodeActionKind.QuickFix,
+          title: "Add inferred annotation",
+        });
+      }
+    }
+
+    return codeActions;
+  }
+
   private getRefactorCodeActions(
     params: CodeActionParams,
     elmWorkspace: IElmWorkspace,
@@ -116,8 +175,9 @@ export class CodeActionProvider {
     const codeActions: CodeAction[] = [];
 
     if (
-      nodeAtPosition.parent?.type === "type_annotation" ||
-      nodeAtPosition.parent?.type === "function_declaration_left"
+      (nodeAtPosition.parent?.type === "type_annotation" ||
+        nodeAtPosition.parent?.type === "function_declaration_left") &&
+      !TreeUtils.findParentOfType("let_in_expr", nodeAtPosition)
     ) {
       const functionName = nodeAtPosition.text;
 
diff --git a/src/util/treeUtils.ts b/src/util/treeUtils.ts
index c6b7828..3c889d2 100644
--- a/src/util/treeUtils.ts
+++ b/src/util/treeUtils.ts
@@ -1421,31 +1421,20 @@ export class TreeUtils {
     node: SyntaxNode,
     functionParameterName: string,
   ): SyntaxNode | undefined {
-    if (
-      node.type === "parenthesized_expr" ||
-      node.type === "anonymous_function_expr"
-    ) {
-      const anonymousFunctionExprNodes =
-        node.type === "anonymous_function_expr"
-          ? [node]
-          : this.descendantsOfType(node, "anonymous_function_expr");
-      const match = anonymousFunctionExprNodes
-        .map((a) => a.children)
-        .reduce((a, b) => a.concat(b), [])
-        .find(
-          (child) =>
-            child.type === "pattern" && child.text === functionParameterName,
-        )?.firstNamedChild;
+    const anonymousFunctionExprNodes = TreeUtils.getAllAncestorsOfType(
+      "anonymous_function_expr",
+      node,
+    );
 
-      if (match) {
-        return match;
-      }
-    }
-    if (node.parent) {
-      return this.findAnonymousFunctionParameterDefinition(
-        node.parent,
-        functionParameterName,
-      );
+    const match = anonymousFunctionExprNodes
+      .map((a) => TreeUtils.findAllNamedChildrenOfType("pattern", a) ?? [])
+      .reduce((a, b) => a.concat(b), [])
+      .map((pattern) => pattern.descendantsOfType("lower_pattern"))
+      .reduce((a, b) => a.concat(b), [])
+      .find((child) => child.text === functionParameterName);
+
+    if (match) {
+      return match;
     }
   }
 
@@ -2168,9 +2157,18 @@ export class TreeUtils {
           .some((imp) => imp.alias === name)
       ) {
         return "";
-      } else {
-        return `${module}.`;
       }
+
+      const asClause = TreeUtils.findFirstNamedChildOfType(
+        "as_clause",
+        moduleImport,
+      );
+
+      if (asClause) {
+        return `${asClause?.lastNamedChild?.text ?? module}.`;
+      }
+
+      return `${module}.`;
     }
   }
 
diff --git a/src/util/types/typeExpression.ts b/src/util/types/typeExpression.ts
index bdbd0f0..5863082 100644
--- a/src/util/types/typeExpression.ts
+++ b/src/util/types/typeExpression.ts
@@ -289,6 +289,7 @@ export class TypeExpression {
       type,
       expressionTypes: this.expressionTypes,
       diagnostics: this.diagnostics,
+      resolvedDeclarations: new SyntaxNodeMap(),
     };
   }
 
diff --git a/src/util/types/typeInference.ts b/src/util/types/typeInference.ts
index a3412b7..6363bbb 100644
--- a/src/util/types/typeInference.ts
+++ b/src/util/types/typeInference.ts
@@ -536,6 +536,7 @@ export interface InferenceResult {
   expressionTypes: SyntaxNodeMap<Expression, Type>;
   diagnostics: Diagnostic[];
   type: Type;
+  resolvedDeclarations: SyntaxNodeMap<EValueDeclaration, Type>;
 }
 
 export class InferenceScope {
@@ -720,6 +721,7 @@ export class InferenceScope {
       expressionTypes: this.expressionTypes,
       diagnostics: this.diagnostics,
       type: ret,
+      resolvedDeclarations: this.resolvedDeclarations,
     };
   }
 
@@ -800,6 +802,7 @@ export class InferenceScope {
       expressionTypes: this.expressionTypes,
       diagnostics: this.diagnostics,
       type: uncurryFunction(TFunction(paramVars, bodyType)),
+      resolvedDeclarations: this.resolvedDeclarations,
     };
   }
 
@@ -820,6 +823,7 @@ export class InferenceScope {
       expressionTypes: this.expressionTypes,
       diagnostics: this.diagnostics,
       type: bodyType,
+      resolvedDeclarations: this.resolvedDeclarations,
     };
   }
 
@@ -834,6 +838,7 @@ export class InferenceScope {
       expressionTypes: this.expressionTypes,
       diagnostics: this.diagnostics,
       type,
+      resolvedDeclarations: this.resolvedDeclarations,
     };
   }
 
@@ -2411,8 +2416,8 @@ export function findType(
   let declaration: SyntaxNode | null = node;
   while (
     declaration &&
-    declaration.type !== "value_declaration" &&
-    declaration.parent?.type !== "file"
+    (declaration.type !== "value_declaration" ||
+      declaration.parent?.type !== "file")
   ) {
     declaration = declaration.parent;
   }
@@ -2436,7 +2441,17 @@ export function findType(
     );
 
     if (node.parent?.type === "function_declaration_left") {
-      return inferenceResult.type;
+      if (node.parent.parent?.parent?.type === "file") {
+        // Top level function
+        return inferenceResult.type;
+      } else {
+        // Let expr function
+        return (
+          inferenceResult.resolvedDeclarations.get(
+            node.parent.parent as EValueDeclaration,
+          ) ?? TUnknown
+        );
+      }
     }
 
     const findTypeOrParentType = (
diff --git a/src/util/types/typeRenderer.ts b/src/util/types/typeRenderer.ts
index 853f32d..d0c6acc 100644
--- a/src/util/types/typeRenderer.ts
+++ b/src/util/types/typeRenderer.ts
@@ -36,11 +36,8 @@ export class TypeRenderer {
 
     switch (t.nodeType) {
       case "Unknown":
-        return "Unknown";
       case "InProgressBinding":
-        throw new Error(
-          "Should never try to convert an in progress binding type to a string",
-        );
+        return "unknown";
       case "Unit":
         return "()";
       case "Var":
@@ -66,6 +63,10 @@ export class TypeRenderer {
   }
 
   private renderUnion(t: TUnion): string {
+    if (t.module === "WebGL" && t.name === "Shader") {
+      return "shader";
+    }
+
     let type;
     if (t.params.length === 0) {
       type = t.name;


Test patch:
diff --git a/test/definitionProviderTests/miscDefinition.test.ts b/test/definitionProviderTests/miscDefinition.test.ts
index dbd74e4..30d4315 100644
--- a/test/definitionProviderTests/miscDefinition.test.ts
+++ b/test/definitionProviderTests/miscDefinition.test.ts
@@ -52,7 +52,7 @@ foo =
 
   // LAMBDAS (ANONYMOUS FUNCTIONS)
 
-  xit(`test lambda parameter ref`, async () => {
+  it(`test lambda parameter ref`, async () => {
     const source = `
 f = \\x -> x
    --X  --^
@@ -60,9 +60,9 @@ f = \\x -> x
     await testBase.testDefinition(source);
   });
 
-  xit(`test lambda parameter nested`, async () => {
+  it(`test lambda parameter nested`, async () => {
     const source = `
-f = \\x -> (\() -> x)
+f = \\x -> (\\() -> x)
    --X          --^
 `;
     await testBase.testDefinition(source);
@@ -70,31 +70,31 @@ f = \\x -> (\() -> x)
 
   it(`test lambda parameter nested and should not resolve`, async () => {
     const source = `
-f = \() -> x (\\x -> ())
+f = \\() -> x (\\x -> ())
          --^unresolved
 `;
     await testBase.testDefinition(source);
   });
 
-  xit(`test lambda parameter destructured record field ref`, async () => {
+  it(`test lambda parameter destructured record field ref`, async () => {
     const source = `
-f = \{x} -> x
+f = \\{x} -> x
     --X   --^
 `;
     await testBase.testDefinition(source);
   });
 
-  xit(`test lambda parameter destructured tuple ref`, async () => {
+  it(`test lambda parameter destructured tuple ref`, async () => {
     const source = `
-f = \(x,y) -> x
+f = \\(x,y) -> x
     --X     --^
 `;
     await testBase.testDefinition(source);
   });
 
-  xit(`test lambda parameter destructured with alias`, async () => {
+  it(`test lambda parameter destructured with alias`, async () => {
     const source = `
-f = \((x,y) as point) -> point
+f = \\((x,y) as point) -> point
                --X       --^
 `;
     await testBase.testDefinition(source);