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
- 313