Designing a Static Analysis Tool for Elm

Jeroen Engels

@jfmengels

Elm Radio

elm-review

  • Creating guarantees for your projects
  • Communication through helpful error messages
  • Quality reports without false positives
  • Enforcing rules for real
  • Building trust around automatic fixes
  • Supporting a community of rule authors

Static Code Analysis Tools

Linting

Linting?

Linters are about

enforcing code style

Code formatters

module MyModule exposing (Model, Msg, update, view)

import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)


type alias Model =
    { count : Int }


type Msg
    = Increment
    | Decrement


update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { model | count = model.count + 1 }

        Decrement ->
            { model | count = model.count - 1 }


view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Increment ] [ text "+1" ]
        , div [] [ text <| String.fromInt model.count ]
        , button [ onClick Decrement ] [ text "-1" ]
        ]

Creating guarantees

in a project

We have a great product,

it's working well and it never crashes.

But we have an inconsistent UI.
Let's improve that.

-- Page/Checkout.elm
viewConfirmButton : Html msg
viewConfirmButton =
    Html.button
        [ Attr.style "height" "35px"
        , Events.onClick UserClickedOnConfirmButton
        ]
        [ Html.text "Confirm" ]
-- Page/Payment.elm
viewPayButton : Html msg
viewPayButton =
    Html.button
        [ Attr.style "height" "33px"
        , Events.onClick UserClickedOnPayButton
        ]
        [ Html.text "Pay" ]
module Ui.Button exposing (confirm, cancel, andSomeOtherButtons)
  
confirm : msg -> String -> Html msg

cancel : msg -> String -> Html msg
-- Page/Checkout.elm
import Ui.Button as Button
viewConfirmButton : Html msg
viewConfirmButton =
    Button.confirm UserClickedOnConfirmButton "Confirm"
-- Page/Payment.elm
import Ui.Button as Button
viewPayButton : Html msg
viewPayButton =
    Button.confirm UserClickedOnPayButton "Pay"

"We now have great modules for every UI element.

Now we are sure to have a consistent UI across the application."

New pull request comes in...

view : Model -> Html Msg
view model =
    Html.button
        [ Attr.style "height" "34px"
        , Events.onClick UserClickedOnRemoveButton
        ]
        [ Html.text "Remove" ]

How do we prevent this from happening?

Types...? Tests...?

view : Model -> Html Msg
view model =
    Html.button
        [ Attr.style "height" "34px"
        , Events.onClick UserClickedOnRemoveButton
        ]
        [ Html.text "Remove" ]

If you can detect the problem just by reading the code, static code analysis can help.

Linter with custom rules

Writing a

Static Code Analysis Rule 

Written in Elm, for Elm

  • Every Elm developer knows Elm

  • No runtime errors

Configuration

import NoUnused.Variables
import NoUsingHtmlButton

config : List Rule
config =
    [ NoUnused.Variables.rule
    , NoUsingHtmlButton.rule
    ]

Abstract Syntax Trees

(a + b) / 2
Integer 2
OperatorApplication
   operator +
FunctionOrValue a
OperatorApplication
   operator /
FunctionOrValue b
ParenthesizedExpression
rule : Rule
rule =
    Rule.newSchema "NoUsingHtmlButton" ()
        |> Rule.withExpressionVisitor expressionVisitor
        |> Rule.fromSchema


expressionVisitor : Node Expression -> Context -> ( List Error, Context )
expressionVisitor node context =
    case Node.value node of
        Expression.FunctionOrValue [ "Html" ] "button" ->
            ( [ -- Report an error
              ]
            , context
            )

        _ ->
            ( [], context )
expressionVisitor : Node Expression -> Context -> ( List Error, Context )
expressionVisitor node context =
    case Node.value node of
        Expression.FunctionOrValue [ "Html" ] "button" ->
            ( [ Rule.error
                    "Do not use Html.button"
                    (Node.range node)
              ]
            , context
            )

        _ ->
            ( [], context )

Rule name

Source

code extract

Message

File path

Are we getting the

message across?

Developers need to understand

  • What they did wrong

  • Why it is a problem

  • How to move forward

expressionVisitor : Node Expression -> Context -> ( List Error, Context )
expressionVisitor node context =
    case Node.value node of
        Expression.FunctionOrValue [ "Html" ] "button" ->
            ( [ Rule.error
                    "Use Ui.Button instead of the native Html.button"
                    (Node.range node)
              ]
            , context
            )

        _ ->
            ( [], context )
expressionVisitor : Node Expression -> Context -> ( List Error, Context )
expressionVisitor node context =
    case Node.value node of
        Expression.FunctionOrValue [ "Html" ] "button" ->
            ( [ Rule.error
            	  { message = "Use Ui.Button instead of the native Html.button"
                  , details =
                    [ "At fruits.com, we try to have a consistent UI across the application, and one of the ways we do that is by having a single nice module to create buttons, named Ui.Button."
                    , "Here, you defined a button using `Html.button` or `Html.Styled.Button`, which is likely not to have the consistent UI we aim for or some of the guarantees we created around our buttons."
                    , "Instead, you should use the Ui.Button module. I suggest reading the documentation in that module, but here is what it would kind of look like: ..."
                    ]
                  }
                  (Node.range node)
              ]
            , context
            )

        _ ->
            ( [], context )

Context

Problem

Suggestion

Example

Rule name

Source

code extract

Message

File path

Not understanding reports leads to workarounds

view : Model -> Html Msg
view model =
    -- elm-review-disable-next-line
    Html.button
        [ Attr.css
            [ Css.height (Css.px 34)
            , Css.fontSize (Css.px 16)
            ]
        , Events.onClick UserClickedOnRemoveButton
        ]
        [ Html.text "Remove" ]

Trust

No false positives

No disable comments

Developers need to trust their tools

False positives

False negatives

Linters report lots of

false positives

Missing information

Assumptions

False positives/negatives

Providing information

  • Type information

  • Dependencies

  • Project manifest, README

  • Collecting data while traversing

type alias Context =
  { isUiButtonModule : Bool }

rule : Rule
rule =
    Rule.newSchema "NoUnusedDeclarations" { isUiButtonModule = False }
    	-- visitors...
        |> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
        |> Rule.fromSchema

moduleDefinitionVisitor : Node Module -> Context -> ( List Error, Context )
moduleDefinitionVisitor (Node _ { moduleName }) context =
    ( [], { isUiButtonModule = (moduleName == "Ui.Button") } )
module Ui.Button exposing (confirm, cancel, andSomeOtherButtons)
expressionVisitor : Node Expression -> Context -> ( List Error, Context )
expressionVisitor node context =
    case Node.value node of
        Expression.FunctionOrValue [ "Html" ] "button" ->
            if not context.isUiButtonModule then
                ( [ Rule.error {- ... -} ]
                , context
                )

            else
                ( [], context )

        _ ->
            ( [], context )

Gathering context from multiple files

module A exposing (used, neverUsed)

used =
    "some thing"

neverUsed =
    "other thing"
module B exposing (thing)

import A

thing =
    A.used ++ " is exposed"

Report unused exports

What can you deduce from the target language?

Elm is knowable

Elm is knowable

Elm is knowable

No dynamic property access

const data = {
    fn1: (a) => { /* do something */ },
    fn2: (a) => { /* do something else */ }
};

function format(kind, value) {
    return data['fn' + kind];
}

Elm is knowable

No dynamic property access

No variable return types

No side-effects

No mutations

No macros

...

No memory management

No race conditions

...

No disable comments

Elm is knowable

Better understanding of the code

Almost no false positives

Ignoring reports

Indeterminate rules

Generated/vendored code

Ignoring rules

config : List Rule
config =
    [ NoUnused.Variables.rule
    , NoDebug.Log.rule
    ]
        |> Rule.ignoreErrorsForDirectories [ "src-generated/", "src-vendor/" ]

Allow existing errors

Deprecating a function

{-| Does something.

@deprecated Use someBetterFunction which does it better.

-}
someFunction input =
    -- do something with input

Deprecating a function

Ignoring per file

config : List Rule
config =
    [ NoDeprecated.rule
        |> Rule.ignoreErrorsForFiles
            [ "src/Api.elm"
            , -- ...and other files
            ]
    ]

Suppressing errors

{
  "version": 1,
  "automatically created by": "elm-review suppress",
  "learn more": "elm-review suppress --help",
  "suppressions": [
    { "count": 7, "filePath": "src/Api.elm" }
  ]
}

review/suppressed/NoDeprecated.json

Deprecating code

Deprecating code

Deprecating code

Automatic fixes

Automatic fixes

🤬

code style

Automatic fixes

❤️

code formatters

Fixes should be trustworthy

Prompt for fixes

No built-in rules:

Packages as first-class citizens

Breaking change in a rule

Breaking change in the tool

=

Levelling the field for

third-party rules

  • You can use static analysis tools to add new guarantees to your project if they allow custom rules
  • Communication is key, and we should explain the problem, give context and suggestions to move forward. Otherwise people will do ugly workarounds.
  • Tools can do better than just allowing to disable errors: Suppressing errors, better rules
  • Language authors can make tooling report less false positives by not including features and keeping the language small
  • Communities can have better tooling if they adopt code formatters
  • Users need to trust their static analysis tool, which we can do by
    • Providing rule authors with all the valuable information available
    • Raising the quality of the rules to report less false positives
    • ???
  • (no built-in rules) We can even out the playing field by separating the tool from the rules.

Conclusion

Use static analysis tools to create guarantees

Communication

  • What is the problem?

  • Why is it a problem?

  • How to solve the problem?

Remove false positives

by providing information

Simpler and knowable

languages allow better analysis

Tools can create trust

through ...?

Built-in rules...?

Thank you!

Jeroen Engels

@jfmengels

Elm Radio

Gathering context

module Main exposing (main)
import Html

main =
    Html.text used

used =
    "used"

unused =
    "unused"
type alias Context =
    { declared : Set String
    , used : Set String
    }

initialContext : Context
initialContext =
    { declared = Set.empty
    , used = Set.empty
    }

rule : Rule
rule =
    Rule.newSchema "NoUnusedDeclarations" initialContext
    	-- visitors...
        |> Rule.fromSchema
rule : Rule
rule =
    Rule.newSchema "NoUnusedDeclarations" initialContext
    	-- visitors...
        |> Rule.withDeclarationVisitor declarationVisitor
        |> Rule.fromSchema

declarationVisitor : Node Declaration -> Context -> ( List Error, Context )
declarationVisitor node context =
    case Node.value node of
        Declaration.FunctionDeclaration { name } ->
            ( []
            , { context | declared = Set.insert name context.declared }
            )

        _ ->
            ( [], context )
rule : Rule
rule =
    Rule.newSchema "NoUnusedDeclarations" initialContext
    	-- visitors...
        |> Rule.withExpressionVisitor expressionVisitor
        |> Rule.fromSchema

expressionVisitor : Node Expression -> Context -> ( List Error, Context )
expressionVisitor node context =
    case Node.value node of
        Expression.FunctionOrValue [] name ->
            ( []
            , { context | used = Set.insert name context.used }
            )

        _ ->
            ( [], context )
rule : Rule
rule =
    Rule.newSchema "NoUnusedDeclarations" initialContext
    	-- visitors...
        |> Rule.withFinalEvaluation finalEvaluation
        |> Rule.fromSchema

finalEvaluation : Context -> List Error
finalEvaluation context =
    Set.diff context.declared context.used
    	|> Set.toList
        |> List.map createError

Elm

Language for making web apps

 

Pure functional language

 

Easy to learn

 

Statically typed

 

​No runtime errors

Copy of Designing a Static Analysis Tool for Elm

By Jeroen Engels

Copy of Designing a Static Analysis Tool for Elm

Talk for GOTO Aarhus 2022

  • 236