Add SimplifyPiping rule

This commit is contained in:
Jeroen Engels 2017-01-21 00:39:12 +01:00
parent 6ef2ee0d94
commit 65ed24f942
5 changed files with 249 additions and 1 deletions

View File

@ -22,7 +22,8 @@ You can read the slides for my [presentation](http://slides.com/jeroenengels/elm
- [NoDebug](rules/NoDebug.md) - Forbid the use of `Debug` before it goes into production.
- [NoDuplicateImports](rules/NoDuplicateImports.md) - Forbid importing the same module several times in a file.
- [NoUnannotatedFunction](rules/NoUnannotatedFunction.md) - Ensure every top-level function declaration has a type annotation.
- [NoExposingEverything](rules/no-class.md) - Forbid exporting everything in your modules `module Main exposing (..)`, to make your module explicit in what it exposes.
- [NoExposingEverything](rules/NoExposingEverything.md) - Forbid exporting everything in your modules `module Main exposing (..)`, to make your module explicit in what it exposes.
- [SimplifyPiping](rules/SimplifyPiping.md) - Simplify piped functions like `List.map f >> List.map g` to `List.map (f >> g)`
More rule ideas in this [slide](http://slides.com/jeroenengels/elm-lint#/5/3) and the ones below it.
Note that some rules were implemented but may not be good ideas. Think for yourself and ask the community whether you should enable them.

View File

@ -21,6 +21,7 @@ import NoExposingEverything
import NoImportingEverything
import NoUnannotatedFunction
import NoUnusedVariables
import SimplifyPiping
type Msg
@ -35,6 +36,7 @@ rules =
, NoImportingEverything.rule
, NoUnannotatedFunction.rule
, NoUnusedVariables.rule
, SimplifyPiping.rule
]

117
rules/SimplifyPiping.elm Normal file
View File

@ -0,0 +1,117 @@
module SimplifyPiping exposing (rule)
import Lint exposing (lint, doNothing)
import Types exposing (LintRule, Error, Direction(..))
import Ast.Expression exposing (..)
import Set exposing (Set)
type alias Context =
{}
rule : String -> List Error
rule input =
lint input implementation
implementation : LintRule Context
implementation =
{ statementFn = doNothing
, typeFn = doNothing
, expressionFn = expressionFn
, moduleEndFn = (\ctx -> ( [], ctx ))
, initialContext = Context
}
error : String -> String -> Error
error op fn =
Error
"SimplifyPiping"
("Instead of `" ++ fn ++ " f " ++ op ++ " List.map g`, try " ++ fn ++ " (f " ++ op ++ " g)")
simplifiableFns : Set String
simplifiableFns =
Set.fromList
[ "List.map"
, "Set.map"
, "Array.map"
, "Array.indexedMap"
]
nameOfMethod : List (List String) -> String
nameOfMethod members =
members
|> List.concatMap (\a -> a)
|> String.join "."
reportIfSimplifiableMethod : String -> Expression -> Expression -> List Error
reportIfSimplifiableMethod op left right =
case [ left, right ] of
[ Application (Access (Variable names1) fns1) _, Application (Access (Variable names2) fns2) _ ] ->
if [ names1, fns1 ] == [ names2, fns2 ] && Set.member (nameOfMethod [ names1, fns1 ]) simplifiableFns then
[ error op <| nameOfMethod [ names1, fns1 ] ]
else
[]
_ ->
[]
expressionFn : Context -> Direction Expression -> ( List Error, Context )
expressionFn ctx node =
case node of
Enter (BinOp (Variable [ "|>" ]) (Application left _) right) ->
case right of
-- X.y f data |> X.y g |> foo
BinOp (Variable [ "|>" ]) subRight _ ->
( reportIfSimplifiableMethod ">>" left subRight, ctx )
-- X.y f data |> X.y g
_ ->
( reportIfSimplifiableMethod ">>" left right, ctx )
-- X.y f <| X.y g data
Enter (BinOp (Variable [ "<|" ]) left (Application right _)) ->
case left of
-- foo <| X.y f <| X.y g data
BinOp (Variable [ "<|" ]) subLeft _ ->
( reportIfSimplifiableMethod "<<" left subLeft, ctx )
_ ->
( reportIfSimplifiableMethod "<<" left right, ctx )
-- a |> X.y f |> X.y g
Enter (BinOp (Variable [ "|>" ]) _ (BinOp (Variable [ "|>" ]) left right)) ->
( reportIfSimplifiableMethod ">>" left right, ctx )
-- X.y f <| X.y g <| a
Enter (BinOp (Variable [ "<|" ]) left (BinOp (Variable [ "<|" ]) right _)) ->
( reportIfSimplifiableMethod "<<" left right, ctx )
Enter (BinOp (Variable [ ">>" ]) left right) ->
case left of
-- foo >> X.y f >> X.y g
BinOp (Variable [ ">>" ]) _ subLeft ->
( reportIfSimplifiableMethod ">>" subLeft right, ctx )
-- X.y f >> X.y g
_ ->
( reportIfSimplifiableMethod ">>" left right, ctx )
Enter (BinOp (Variable [ "<<" ]) left right) ->
case left of
-- foo << X.y f << X.y g
BinOp (Variable [ "<<" ]) _ subLeft ->
( reportIfSimplifiableMethod "<<" subLeft right, ctx )
-- X.y f << X.y g
_ ->
( reportIfSimplifiableMethod "<<" left right, ctx )
_ ->
( [], ctx )

View File

@ -8,6 +8,7 @@ import NoDuplicateImportsTest
import NoImportingEverythingTest
import NoUnannotatedFunctionTest
import NoUnusedVariablesTest
import SimplifyPipingTest
main : Test.Runner.Node.TestProgram
@ -26,4 +27,5 @@ all =
, NoImportingEverythingTest.all
, NoUnannotatedFunctionTest.all
, NoUnusedVariablesTest.all
, SimplifyPipingTest.all
]

View File

@ -0,0 +1,126 @@
port module SimplifyPipingTest exposing (all)
import Expect
import Test exposing (describe, test, Test)
import SimplifyPiping exposing (rule)
import Types exposing (Error)
error : String -> String -> Error
error op fn =
Error "SimplifyPiping" ("Instead of `" ++ fn ++ " f " ++ op ++ " List.map g`, try " ++ fn ++ " (f " ++ op ++ " g)")
tests : List Test
tests =
[ test "should not report piping of the result of different functions of the same module" <|
\() ->
rule "a = b |> List.map f |> List.filter g"
|> Expect.equal []
, test "should not report piping of the result of similarly named functions of different modules" <|
\() ->
rule "a = b |> List.map f |> Set.map g"
|> Expect.equal []
, test "should report piping of List.map" <|
\() ->
rule "a = b |> List.map f |> List.map g"
|> Expect.equal [ error ">>" "List.map" ]
, test "should report piping of Set.map" <|
\() ->
rule "a = b |> Set.map f |> Set.map g"
|> Expect.equal [ error ">>" "Set.map" ]
, test "should report piping of Array.map" <|
\() ->
rule "a = b |> Array.map f |> Array.map g"
|> Expect.equal [ error ">>" "Array.map" ]
, test "should report piping of the result of Array.indexedMap" <|
\() ->
rule "a = b |> Array.indexedMap f |> Array.indexedMap g"
|> Expect.equal [ error ">>" "Array.indexedMap" ]
, test "should not report piping of the incomplete simplifiable function call" <|
\() ->
rule "a = List.map f |> List.map g"
|> Expect.equal []
, test "should not report piping the function in itself" <|
\() ->
rule "a = List.map f |> List.map g"
|> Expect.equal []
, test "should report piping the complete call into the same function" <|
\() ->
rule "a = List.map f data |> List.map g"
|> Expect.equal [ error ">>" "List.map" ]
, test "should report piping the complete call into the same function with an additional pipe at the end" <|
\() ->
rule "a = List.map f data |> List.map g |> foo"
|> Expect.equal [ error ">>" "List.map" ]
, test "should not report the use of a single simplifiable function" <|
\() ->
rule "a = List.map f"
|> Expect.equal []
, test "should not report the use of a single simplifiable function with piped functions as the argument" <|
\() ->
rule "a = List.map (f >> g)"
|> Expect.equal []
, test "should not report any piping of similar methods" <|
\() ->
rule "a = List.foo fn |> List.foo fn2"
|> Expect.equal []
, test "should not report direct piping of List.map and friends" <|
\() ->
rule """
a = List.map |> List.map
b = List.map >> List.map
c = Set.map |> Set.map
d = Set.map >> Set.map
"""
|> Expect.equal []
, test "should report piping right to left" <|
\() ->
rule "a = List.map f <| List.map g <| b"
|> Expect.equal [ error "<<" "List.map" ]
, test "should report piping right to left with complete call at the right side" <|
\() ->
rule "a = List.map f <| List.map g data"
|> Expect.equal [ error "<<" "List.map" ]
, test "should report piping right to left with complete call at the right side and additional pipe on the left" <|
\() ->
rule "a = a <| List.map f <| List.map g data"
|> Expect.equal [ error "<<" "List.map" ]
, test "should report piping the functions directly" <|
\() ->
rule "a = List.map f >> List.map g"
|> Expect.equal [ error ">>" "List.map" ]
, test "should report piping the functions directly with additional pipe on the left" <|
\() ->
rule "a = foo >> List.map f >> List.map g"
|> Expect.equal [ error ">>" "List.map" ]
, test "should report piping the functions directly with additional pipe on the right" <|
\() ->
rule "a = List.map f >> List.map g >> bar"
|> Expect.equal [ error ">>" "List.map" ]
, test "should report piping the functions directly with additional pipe on both sides" <|
\() ->
rule "a = foo >> List.map f >> List.map g >> bar"
|> Expect.equal [ error ">>" "List.map" ]
, test "should report piping the functions directly, right to left" <|
\() ->
rule "a = List.map f << List.map g"
|> Expect.equal [ error "<<" "List.map" ]
, test "should report piping the functions directly with additional pipe on the left, right to left" <|
\() ->
rule "a = foo << List.map f << List.map g"
|> Expect.equal [ error "<<" "List.map" ]
, test "should report piping the functions directly with additional pipe on the right, right to left" <|
\() ->
rule "a = List.map f << List.map g << bar"
|> Expect.equal [ error "<<" "List.map" ]
, test "should report piping the functions directly with additional pipe on both sides, right to left" <|
\() ->
rule "a = foo << List.map f << List.map g << bar"
|> Expect.equal [ error "<<" "List.map" ]
]
all : Test
all =
describe "SimplifyPiping" tests