Writing an Elm linter In Elm

Jeroen Engels (Yeuroune)

ESLint

ESLint

Writing a linter teaches you about the language

 

Let's build an Elm Linter!

foobar : String
       -> List (Html Msg)
       -> Html Msg
foobar title children =
    li []
        [ pre [] [ text title ]
        , ul [] children
        ]

Code is hard to analyze

(a + b) / 2
Integer 2
BinaryExpression
   operator +
Variable a
Variable b
BinaryExpression
   operator /

AST - Abstract Syntax Tree

elm-ast

ESLint AST

{
  "type": "Program",
  "body": [
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "BinaryExpression",
        "left": {
          "type": "BinaryExpression",
          "left": {
            "type": "Identifier",
            "name": "a"
          },
          "operator": "+",
          "right": {
            "type": "Identifier",
            "name": "b"
          }
        },
        "operator": "/",
        "right": {
          "type": "Literal",
          "value": 2,
          "raw": "2"
        }
      }
    }
  ]
}

elm-ast

Visiting the tree

Visiting the tree

Integer 2
BinaryExpression
   operator +
Variable a
Variable b
BinaryExpression
   operator /
  • Collect information as we visit the tree
  • Report errors along the way

Visiting the tree

How to write a rule

expressionFn : Context
             -> Direction Expression
             -> ( List Error, Context )

expressionFn ctx node =
    case node of
        Enter (SomeNodeType) ->
            if condition then
                ( [ createError "message" ], ctx )
            else
                ( [], { ctx | data = ctx.data ++ foobar } )

        _ ->
            ( [], ctx )
module SomeRule exposing (rule)

import Lint exposing (lint, doNothing)
import Types exposing (LintRule, Error, Direction(..))


type alias Context =
    { data: List String
    }


rule : String -> List Error
rule input =
    lint input implementation


implementation : LintRule Context
implementation =
    { statementFn = doNothing
    , typeFn = doNothing
    , expressionFn = expressionFn
    , moduleEndFn = (\ctx -> ( [], ctx ))
    , initialContext = Context []
    }

Why so many functions?

-- Could not write this
type Node
    = Statement
    | Expression
    | Type

fn : Context
   -> Direction Node
   -> ( List Error, Context )
fn ctx node =
  ( [], ctx )

Example: No Debug

  • Aim: Forbid all uses of `Debug` in your code
foo data =
    case Debug.log "data" data of
        _ -> []

Example:

No unannotated functions

  • Aim: Force all top-level functions to have a type annotation

elm-lint

elm-ast shortcomings

  • No information of *where* the error is in the code,
        elm-ast does not give the information :/
  • Elm syntax is not 100% supported, there are bugs

elm-lint

  • 4 rules implemented
    • No Debug
    • No Unannotated Functions
    • No Exposing Everything `module Main exposing (..)`
    • No Unused Variables

elm-lint wannabe rules

  • Duplicated imports `import Regex ; Import Regex`
  • No importing everything `import Regex exposing (..)`: confusing!
  • `a |> List.map f |> List.map g` --> `a |> List.map (f >> g)`
  • elm-html: No putting a `textarea`tag inside a `p` tag

elm-lint wannabe rules

  • Missing port in elm-test
  • No lingering Test.filter in elm-test
main : Test.Runner.Node.TestProgram
main =
    run emit all


port emit : ( String, Value ) -> Cmd msg


all : Test
all =
    describe "String.reverse"
    [ test "has no effect on a palindrome" testGoesHere
    , test "reverses a known string" anotherTest
    , fuzz string "restores the original string if you run it again" oneMore
    ]
        |> Test.filter (String.contains "original")

elm-lint wannabe rules

  • Handling things the compiler does not
  • `x = x + 1` caused runtime errors in v0.17
  • Was not fixed for months until v0.18 came out
  • Easily detectable

Clearly less useful than ESLint

  • Style: elm-format handles all styles and is widely adopted
  • The compiler handles (almost) all errors
  • JavaScript desperately needs a linter. Elm less so