Jeroen Engels
@jfmengels
Jeroen Engels
@jfmengels everywhere
jfmengels.net
Elm Radio podcast
elm-review
Aim: provide instinct
Not a tutorial
Elm
Disclaimer
Examples are going to be in JavaScript / TypeScript / Elm
function shout(sentence) {
return sentence.toUpperCase();
}
shout(100);
// Uncaught TypeError: sentence.toUpperCase is not a function
function shout(sentence) {
return sentence.toUpperCase();
}
shout(100);
function shout(sentence: string) {
return sentence.toUpperCase();
}
shout(100);
// Argument of type 'number' is not assignable to parameter of type 'string'.
function printAverage(scores: number[]) {
console.log("Your average score:", average(scores));
}
function average(array: number[]): number {
return sum(array) / array.length;
}
function sum(array: number[]): number {
// Correct implementation of a sum
}
printAverage([]);
function printAverage(scores: number[]) {
console.log("Your average score:", average(scores));
}
function average(array: number[]): number {
return sum(array) / array.length;
}
function sum(array: number[]): number {
// Correct implementation of a sum
}
printAverage([]);
// "Your average score: NaN"
function average(array: number[]): number {
return sum(array) / array.length;
}
printAverage([]);
// "Your average score: NaN"
test('average() returns 0 when the array is empty', () => {
assert.equals(average([]), 0);
});
function average(array: number[]): number {
if (array.length === 0) {
return 0;
}
return sum(array) / array.length;
}
array | average(array) |
---|---|
[ ] | 0 |
[ 1, 2 ] | 3 |
... | ... |
import Colors from './colors';
const buttons =
[
button({ color: Colors.primary }, "First button"),
button({ color: "#FF1122" }, "Second button"),
];
const buttons =
[
button({ color: Colors.primary }, "First button"),
button({ color: "#FF1122" }, "Second button"),
];
test("The second button's color is primary", () => {
assert.equals(buttons[1].getColor(), Colors.primary);
});
<no-input> | color buttons[0] | color buttons[1] | ... |
---|---|---|---|
Colors.primary | Colors.primary | ... |
Triangle
button({ color: "#FF1122" }, "Second button"),
^^^^^^^^
using grep
grep -e 'color: "' src/**/*.ts
#!/bin/bash
MATCH=$(grep -e 'color: "' src/**/*.ts --with-filename --line-number --color=always)
if [ -n "$MATCH" ]; then
echo "Found linting problems:"
echo ""
echo $MATCH
exit 1
fi
using grep
// 2 spaces
button({ color: "#FF1122" }, "..."),
// No spaces
button({ color:"#FF1122" }, "..."),
// Space before the colon
button({ color : "#FF1122" }, "..."),
// Uses single-quotes
button({ color: '#FF1122' }, '...'),
grep -e 'color: "' src/**/*.ts
/*
Don't ever write
button({ color: "#FF1122" }, "..."),
Instead use
button({ color: Colors.primary }, "..."),
*/
(a + b) / 2
Integer 2
Binary expression
using "+"
Reference to "a"
Binary expression
using "/"
Reference to "b"
Parenthesized
a / 0
Integer 0
Reference to "a"
Binary expression
using "/"
Rule: No division by 0
If I see a binary expression using "/" where on the right side there's an integer 0 , then report a problem.
{ color: "#FF1122" }
String literal
Field "color"
Property
const buttons =
[
button({ color: Colors.primary }, "First button"),
// linter-disable
button({ color: "#FF1122" }, "Second button"),
];
Context
Problem
Suggestion
Example
TODO Example HTTP library that sets all the right headers
TODO Making sure that one module does not import another one. That one does not import internals of another module.
TODO console.log(context) example?
TODO Forbid the usage of a v1 of something and promote a v2 instead
TODO Safe unsafe operations
TODO Html decoder
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" ]
]
//nolint:rule
# pylint: disable=rule
# rubocop:disable rule
// NOLINT
// phpcs:disable rule
@SuppressWarnings("rule")
@Suppress("rule")
// eslint-disable rule
// linter-disable
// linter-disable
// linter-disable
// elm.json
{
"type": "application",
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"dependencies": {
"...": "..."
}
}
config : List Rule
config =
[ NoBadThing.rule
, NoOtherBadThing.rule { some = "options" }
-- ...and more rules
]
//nolint:rule
# pylint: disable=rule
# rubocop:disable rule
// NOLINT
// phpcs:disable rule
@SuppressWarnings("rule")
@Suppress("rule")
// eslint-disable rule
Line 10: Don't do this thing
Line 11: Don't do this thing
Line 12: Don't do this thing
Line 13: Don't do this thing
Line 14: Don't do this thing
Line 15: Don't do this thing
Line 16: Don't do this thing
Line 17: Don't do this thing
Line 18: Don't do this thing
Line 19: Don't do this thing
Line 20: Don't do this thing
Line 21: Don't do this thing
Line 22: Don't do this thing
Line 23: Don't do this thing
Line 24: Don't do this thing
Line 25: Don't do this thing
Line 26: Don't do this thing
Line 27: Don't do this thing
Line 28: Don't do this thing
Line 29: Don't do this thing
Line 30: Don't do this thing
Line 31: Don't do this thing
Line 32: Don't do this thing
Line 33: Don't do this thing
Line 34: Don't do this thing
Line 35: Don't do this thing
Line 36: Don't do this thing
Line 37: Don't do this thing
Line 38: Don't do this thing
Line 39: Don't do this thing
Line 23: Don't do this thing
Line 38: Don't do this thing
Line 16: Don't do this thing
Line 23: Don't do this thing
Line 38: Don't do this thing
// linter-disable
this.code.will.crash();
-- elm-review-disable rule
config : List Rule
config =
[ NoUnused.Variables.rule
, NoDebug.Log.rule
]
|> Rule.ignoreErrorsForDirectories [ "tests/" ]
{-| Does something.
@deprecated Use someBetterFunction which does it better.
-}
someFunction input =
-- do something with input
//nolint:rule
# pylint: disable=rule
# rubocop:disable rule
// NOLINT
// phpcs:disable rule
@SuppressWarnings("rule")
@Suppress("rule")
// eslint-disable rule
#![deny(clippy::all)]
Line 10: Don't do this thing
Line 11: Don't do this thing
Line 12: Don't do this thing
Line 13: Don't do this thing
Line 14: Don't do this thing
Line 15: Don't do this thing
Line 16: Don't do this thing
Line 17: Don't do this thing
Line 18: Don't do this thing
Line 19: Don't do this thing
Line 20: Don't do this thing
Line 21: Don't do this thing
Line 22: Don't do this thing
Line 23: Don't do this thing
Line 24: Don't do this thing
Line 25: Don't do this thing
Line 26: Don't do this thing
Line 27: Don't do this thing
Line 28: Don't do this thing
Line 29: Don't do this thing
Line 30: Don't do this thing
Line 31: Don't do this thing
Line 32: Don't do this thing
Line 33: Don't do this thing
Line 34: Don't do this thing
Line 35: Don't do this thing
Line 36: Don't do this thing
Line 37: Don't do this thing
Line 38: Don't do this thing
Line 39: Don't do this thing
{
"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
$ elm-review suppress
Jeroen Engels
@jfmengels
Talk: Static analysis tools love pure FP
https://www.youtube.com/watch?v=_rzoyBq4hJ0
Elm Radio podcast
Slides: https://slides.com/jfmengels/why-you-dont-trust-your-linter
// linter-disable
// linter-disable
// linter-disable
// linter-disable
// linter-disable
// linter-disable
config : List Rule
config =
[ NoDeprecated.rule
|> Rule.ignoreErrorsForFiles
[ "src/Api.elm"
, -- ...and other files
]
]
No dynamic function calls
No side-effects
No mutations
No macros
...
No memory management
No race conditions
...
92% (56/61) of the recommended rules
87% (228/263) of all the rules
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" ]
]
(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 )
-- 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"
view : Model -> Html Msg
view model =
Html.button
[ Attr.style "height" "34px"
, Events.onClick UserClickedOnRemoveButton
]
[ Html.text "Remove" ]
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 )
module A exposing (used, neverUsed)
used =
"some thing"
neverUsed =
"other thing"
module B exposing (thing)
import A
thing =
A.used ++ " is exposed"
view : Model -> Html Msg
view model =
Html.button
[ Attr.style "height" "34px"
, Events.onClick UserClickedOnRemoveButton
]
[ Html.text "Remove" ]
File path
Rule name
Location
Message
File path
Rule name
Location
Message