Repository: elm-tooling/elm-language-server
Pull request #330 (2020-10-03 19:00:41)

Add more inference tests and fix bug with operator functions
Added some good inference tests and found a bug in the process with operator as function expressions. Also, the make declaration code action should now have the inferred type for a function with no params.


Patch:
diff --git a/src/util/types/expressionTree.ts b/src/util/types/expressionTree.ts
index d105fd9..1d3a71c 100644
--- a/src/util/types/expressionTree.ts
+++ b/src/util/types/expressionTree.ts
@@ -111,6 +111,7 @@ export interface EOperator extends SyntaxNode {
 }
 export interface EOperatorAsFunctionExpr extends SyntaxNode {
   nodeType: "OperatorAsFunctionExpr";
+  operator: EOperator;
 }
 export interface ENumberConstant extends SyntaxNode {
   nodeType: "NumberConstant";
@@ -639,6 +640,13 @@ export function mapSyntaxNodeToExpression(
         nodeType: "GlslCodeExpr",
         content: TreeUtils.findFirstNamedChildOfType("glsl_content", node),
       } as EGlslCodeExpr);
+    case "operator_as_function_expr":
+      return Object.assign(node, {
+        nodeType: "OperatorAsFunctionExpr",
+        operator: mapSyntaxNodeToExpression(
+          TreeUtils.findFirstNamedChildOfType("operator_identifier", node),
+        ),
+      } as EOperatorAsFunctionExpr);
     default:
       return mapSyntaxNodeToExpression(node.firstNamedChild);
   }
diff --git a/src/util/types/typeInference.ts b/src/util/types/typeInference.ts
index 1934097..5b0c77d 100644
--- a/src/util/types/typeInference.ts
+++ b/src/util/types/typeInference.ts
@@ -514,6 +514,14 @@ function missingFunctionError(node: SyntaxNode): Diagnostic {
   };
 }
 
+function missingValueError(node: SyntaxNode): Diagnostic {
+  return {
+    node,
+    endNode: node,
+    message: `No definition found for \`${node.text}\``,
+  };
+}
+
 interface RecordDiff {
   extra: Map<string, Type>;
   missing: Map<string, Type>;
@@ -974,7 +982,8 @@ export class InferenceScope {
       );
 
     if (!definition) {
-      return TUnknown;
+      this.diagnostics.push(missingValueError(e));
+      return TVar("a");
     }
 
     const binding = this.getBinding(definition.expr);
@@ -1369,10 +1378,12 @@ export class InferenceScope {
     );
   }
 
-  private inferOperatorAsFunctionExpr(operator: EOperatorAsFunctionExpr): Type {
+  private inferOperatorAsFunctionExpr(
+    operatorFunction: EOperatorAsFunctionExpr,
+  ): Type {
     // Find operator reference
     const definition = findDefinition(
-      operator,
+      operatorFunction.operator,
       this.uri,
       this.elmWorkspace.getImports(),
     );


Test patch:
diff --git a/test/typeInference.test.ts b/test/typeInference.test.ts
index 1e606dc..a751b23 100644
--- a/test/typeInference.test.ts
+++ b/test/typeInference.test.ts
@@ -8,8 +8,10 @@ import { SourceTreeParser } from "./utils/sourceTreeParser";
 
 const basicsSources = `
 --@ Basics.elm
-module Basics exposing (add, (+), Int, Float, Bool(..))
+module Basics exposing ((+), (|>), (==), Int, Float, Bool(..))
 
+infix left  0 (|>) = apR
+infix non   4 (==) = eq
 infix left  6 (+)  = add
 
 type Int = Int
@@ -22,6 +24,13 @@ add : number -> number -> number
 add =
   Elm.Kernel.Basics.add
 
+apR : a -> (a -> b) -> b
+apR x f =
+  f x
+
+eq : a -> a -> Bool
+eq =
+  Elm.Kernel.Utils.equal
 `;
 describe("test type inference", () => {
   const treeParser = new SourceTreeParser();
@@ -49,7 +58,14 @@ describe("test type inference", () => {
 
     const nodeType = findType(nodeAtPosition, testUri, workspace);
 
-    expect(TypeRenderer.typeToString(nodeType, tree)).toEqual(expectedType);
+    expect(
+      TypeRenderer.typeToString(
+        nodeType,
+        tree,
+        testUri,
+        workspace.getImports(),
+      ),
+    ).toEqual(expectedType);
   }
 
   test("simple function", async () => {
@@ -146,4 +162,196 @@ func a b c =
       "number -> { a | first : { b | second : number } } -> Maybe number -> number",
     );
   });
+
+  test("function with imported types", async () => {
+    const source = `
+--@ App.elm
+module App exposing (..)
+
+type Maybe a = Just a | Nothing 
+
+plus : number -> number -> number
+plus a b = a + b
+
+--@ Test.elm
+module Test exposing (..)
+
+import App exposing (..)
+
+func a b c =
+--^
+  let
+    result = a + 1
+
+  in
+    (plus result b.first.second) +
+      (if a == 1 then
+        0
+      else
+        1) +
+      (case c of
+        Just value ->
+          value
+
+        Nothing ->
+          0)
+
+`;
+    await testTypeInference(
+      basicsSources + source,
+      "number -> { a | first : { b | second : number } } -> Maybe number -> number",
+    );
+  });
+
+  test("simple function with imported types", async () => {
+    const source = `
+--@ App.elm
+module App exposing (..)
+
+plus a b = a + b
+
+--@ Test.elm
+module Test exposing (..)
+
+import App exposing (..)
+
+func a b =
+--^
+  plus a b
+`;
+    await testTypeInference(
+      basicsSources + source,
+      "number -> number -> number",
+    );
+  });
+
+  test("functions with records", async () => {
+    const source = `
+--@ App.elm
+module App exposing (..)
+
+type alias Model = {
+  field : Int
+}
+
+access : Model -> Int
+access model =
+  model.field
+
+
+--@ Test.elm
+module Test exposing (..)
+
+import App
+
+func model =
+--^
+  App.access model
+`;
+    await testTypeInference(basicsSources + source, "App.Model -> Int");
+
+    const source2 = `
+--@ App.elm
+module App exposing (..)
+
+type alias Model = {
+  field : Int
+}
+
+access : Model -> Int
+access model =
+  model.field
+
+--@ Test.elm
+module Test exposing (..)
+
+import App exposing (..)
+
+func model =
+--^
+  access model
+`;
+    await testTypeInference(basicsSources + source2, "Model -> Int");
+  });
+
+  test("field accessor function", async () => {
+    const source = `
+--@ Test.elm
+module Test exposing (..)
+
+func model =
+--^
+  model |> .field |> (+) 1
+`;
+    await testTypeInference(
+      basicsSources + source,
+      "{ a | field : number } -> number",
+    );
+  });
+
+  test("union constructor inference", async () => {
+    const source = `
+    --@ App.elm
+module App exposing (Result(..))
+
+type Result = Result { field : Int }
+
+--@ Test.elm
+module Test exposing (..)
+
+import App exposing (Result(..))
+
+func model =
+--^
+  model |> .field |> Result
+`;
+    await testTypeInference(
+      basicsSources + source,
+      "{ a | field : { field : Int } } -> Result",
+    );
+
+    const source2 = `
+    --@ App.elm
+module App exposing (..)
+
+type Result = Result { field : Int }
+
+--@ Test.elm
+module Test exposing (..)
+
+import App
+
+func model =
+--^
+  model |> .field |> App.Result
+`;
+    await testTypeInference(
+      basicsSources + source2,
+      "{ a | field : { field : Int } } -> App.Result",
+    );
+
+    const source3 = `
+    --@ App.elm
+module App exposing (Result(..), toResult)
+
+type Result = Result { field : Int }
+
+toResult : Int -> Result
+toResult a =
+  Result { field = a }
+
+--@ Test.elm
+module Test exposing (..)
+
+import App
+
+func model =
+--^
+  model |> .field |> App.toResult
+`;
+    await testTypeInference(
+      basicsSources + source3,
+      "{ a | field : Int } -> App.Result",
+    );
+  });
 });