lue-bird / elm-review-upgrade

tell me how to get rid of outdated stuff
https://dark.elm.dmy.fr/packages/lue-bird/elm-review-upgrade/latest/
MIT License
0 stars 1 forks source link

Some patterns we'd like to use this for #2

Open jakub-nlx opened 1 month ago

jakub-nlx commented 1 month ago

We're trying to update some legacy APIs to our new fancy APIs.

For instance the following legacy API:

module Ui.Interaction exposing (Interaction(..))

type Interaction msg
    = Disabled
    | -- Deprecated, pending state is always visually equivalent to disabled state
      Pending
    | Href String
    | OnClick msg

---

module Ui.Input exposing (..)

import DesignSystem.MainButton as MainButton
import Ui.Interaction exposing (Interaction(..))

{-| Deprecated and implemented using `MainButton`

@deprecated use `MainButton` instead

-}
button :
    { label : String
    , interaction : Interaction msg
    , danger : Bool
    }
    -> H.Html msg
button config =
    let
        tag =
            if config.danger then
                MainButton.danger

            else
                MainButton.regular
    in
    case config.interaction of
        Disabled ->
            tag [] config.label

        Pending ->
            tag [] config.label

        OnClick msg ->
            tag [ MainButton.onClick msg ] config.label

        Href href ->
            tag [ MainButton.linkTo href ] config.label

To the new MainButton. One thing where this package could assist is in helping to deconstruct record arguments, as that can be quite tricky. At the moment I'm doing something like:

    Upgrade.application
            { oldName = ( "Ui.Input", "button" )
            , oldArgumentNames = [ "config" ]
            , oldArgumentsToNew =
                \arguments ->
                    case arguments of
                        [ RecordExpr rec ] ->
                            let
                                get name =
                                    List.Extra.findMap
                                        (\(Node _ ( Node _ key, Node _ value )) ->
                                            if key == name then
                                                Just value

                                            else
                                                Nothing
                                        )
                                        rec

                            in
                             Maybe.map3
                                (\label interaction isDanger ->
                                    -- some more stuff
                                )
                                (get "label")
                                (get "interaction" |> Maybe.andThen convertInteraction)
                                (get "danger" |> Maybe.andThen toBool)
           }

which is kind of OK, but probably could be made nicer.

The other issue which is tricky in updating this automatically is handling conditionals.

We have a lot of code that looks like:

Ui.Input.button
    { label = "Save"
    , interaction =
        if model.isSaving then
            Pending

        else if slotDraft == lastSavedSlot then
            Disabled

        else
            OnClick (SaveSlot slotDraft)
    , danger = False
    }

which we'd like to upgrade to:

import Attr

DesignSystem.MainButton.regular
    [ Attr.if_ (not (model.isSaving || slotDraft == lastSavedSlot)) 
            (DesignSystem.MainButton.onClick  (SaveSlot slotDraft))
   ]
   "Save"

This gets pretty tricky pretty quickly. I wonder if what this needs is a more powerful pattern matching library for elm-syntax?

gampleman commented 1 month ago

Actually I prototyped the following library for elm-syntax:

Syntax.Match ~~~elm module Syntax.Match exposing (..) import Elm.Syntax.Expression exposing (Expression(..)) import Elm.Syntax.Infix exposing (InfixDirection(..)) import Elm.Syntax.Node exposing (Node(..)) import List.Extra import Maybe.Extra import Set exposing (Set) type alias Context = {} type Matcher a = Matcher (Context -> Expression -> Maybe a) match : Expression -> Matcher a -> Maybe a match expr (Matcher f) = case expr of ParenthesizedExpression (Node _ ex) -> match ex (Matcher f) _ -> f {} expr any : Matcher Expression any = Matcher (\_ expr -> Just expr) booleanLiteral : Matcher Bool booleanLiteral = Matcher (\_ expr -> case expr of FunctionOrValue [] "True" -> Just True FunctionOrValue [] "False" -> Just False _ -> Nothing ) functionOrValue : List String -> String -> Matcher Expression functionOrValue path name = Matcher (\{} expr -> case expr of FunctionOrValue path_ name_ -> if name == name_ then Just expr -- else if name_ == name && List.Extra.find (\( p, n, bare ) -> Set.member name bare || (path == p && List.head path_ == Just n)) imports /= Nothing then -- Just expr else Nothing _ -> Nothing ) call1 : Matcher a -> Matcher b -> Matcher ( a, b ) call1 (Matcher f) (Matcher g) = Matcher (\context expr -> case expr of Application [ Node _ a, Node _ b ] -> case ( f context a, g context b ) of ( Just a_, Just b_ ) -> Just ( a_, b_ ) _ -> Nothing OperatorApplication "|>" Left (Node _ b) (Node _ a) -> case ( f context a, g context b ) of ( Just a_, Just b_ ) -> Just ( a_, b_ ) _ -> Nothing _ -> Nothing ) call2 : Matcher a -> Matcher b -> Matcher c -> Matcher ( a, b, c ) call2 (Matcher f) (Matcher g) (Matcher h) = Matcher (\context expr -> case expr of Application [ Node _ a, Node _ b, Node _ c ] -> case ( f context a, g context b, h context c ) of ( Just a_, Just b_, Just c_ ) -> Just ( a_, b_, c_ ) _ -> Nothing _ -> case match expr <| call1 (Matcher f) (call1 (Matcher g) (Matcher h)) of Just ( a, ( b, c ) ) -> Just ( a, b, c ) _ -> Nothing ) ifBlock : Matcher condition -> Matcher true -> Matcher false -> Matcher ( condition, true, false ) ifBlock (Matcher f) (Matcher g) (Matcher h) = Matcher (\context expr -> case expr of IfBlock (Node _ condition) (Node _ true) (Node _ false) -> case ( f context condition, g context true, h context false ) of ( Just condition_, Just true_, Just false_ ) -> Just ( condition_, true_, false_ ) _ -> Nothing _ -> Nothing ) map : (a -> b) -> Matcher a -> Matcher b map f (Matcher g) = Matcher (\context expr -> g context expr |> Maybe.map f ) map3 : (a -> b -> c -> d) -> Matcher a -> Matcher b -> Matcher c -> Matcher d map3 f g h i = Matcher (\context expr -> Maybe.map3 f (match expr g) (match expr h) (match expr i) ) oneOf : List (Matcher a) -> Matcher a oneOf matchers = Matcher (\context expr -> List.map (\(Matcher f) -> f context expr) matchers |> Maybe.Extra.orList ) andThen : (a -> Matcher b) -> Matcher a -> Matcher b andThen f (Matcher g) = Matcher (\context expr -> case g context expr of Just a -> f a |> (\(Matcher h) -> h context expr) Nothing -> Nothing ) recordField : String -> Matcher a -> Matcher a recordField name (Matcher f) = Matcher (\context expr -> case expr of RecordExpr fields -> List.Extra.findMap (\(Node _ ( Node _ key, Node _ value )) -> if key == name then f context value else Nothing ) fields _ -> Nothing ) ~~~

with this library, the problem turned quite nicely into a decoder/parser sort of scenario which was quite nice and flexible:

module DesignSystemUpgrade exposing (rule)

import Elm.CodeGen as CG
import Elm.Syntax.Expression exposing (Expression(..))
import Elm.Syntax.Node exposing (Node(..))
import List.Extra
import Syntax.Match as Match
import Upgrade

disabledInteraction =
    Match.functionOrValue [] "Disabled"
        |> Match.map (\_ -> Nothing)

pendingInteraction =
    Match.functionOrValue [] "Pending"
        |> Match.map (\_ -> Nothing)

ignoredInteractions =
    Match.oneOf [ disabledInteraction, pendingInteraction ]

clickInteraction =
    Match.call1 (Match.functionOrValue [] "OnClick") Match.any
        |> Match.map (\( _, tag ) -> Just (CG.apply [ CG.fqVal [ "DesignSystem", "MainButton" ] "onClick", tag ]))

hrefInteraction =
    Match.call1 (Match.functionOrValue [] "Href") Match.any
        |> Match.map (\( _, tag ) -> Just (CG.apply [ CG.fqVal [ "DesignSystem", "MainButton" ] "linkTo", tag ]))

meaningfulInteraction =
    Match.oneOf [ clickInteraction, hrefInteraction ]

baseIf =
    Match.ifBlock Match.any meaningfulInteraction ignoredInteractions
        |> Match.map
            (\( condition, interaction_, _ ) ->
                Maybe.map (\interaction -> CG.apply [ CG.fqVal [ "Attr" ] "if_", condition, interaction ]) interaction_
            )

invertIf =
    Match.ifBlock Match.any ignoredInteractions meaningfulInteraction
        |> Match.map
            (\( condition, _, interaction ) ->
                Maybe.map (\int -> CG.apply [ CG.fqVal [ "Attr" ] "if_", CG.apply [ CG.fqVal [] "not", condition ], int ]) interaction
            )

ifThenElse1 =
    Match.ifBlock Match.any meaningfulInteraction (Match.ifBlock Match.any ignoredInteractions ignoredInteractions)
        |> Match.map
            (\( firstCond, a_, ( secondCond, _, _ ) ) ->
                Maybe.map
                    (\a ->
                        CG.apply [ CG.fqVal [ "Attr" ] "if_", firstCond, a ]
                    )
                    a_
            )

ifThenElse2 =
    Match.ifBlock Match.any ignoredInteractions (Match.ifBlock Match.any meaningfulInteraction meaningfulInteraction)
        |> Match.map
            (\( firstCond, _, ( secondCond, b_, _ ) ) ->
                Maybe.map (\b -> CG.apply [ CG.fqVal [ "Attr" ] "if_", CG.applyBinOp (CG.apply [ CG.fqVal [] "not", firstCond ]) CG.and secondCond, b ]) b_
            )

ifThenElse3 =
    Match.ifBlock Match.any ignoredInteractions (Match.ifBlock Match.any ignoredInteractions meaningfulInteraction)
        |> Match.map
            (\( firstCond, _, ( secondCond, _, c_ ) ) ->
                Maybe.map (\c -> CG.apply [ CG.fqVal [ "Attr" ] "if_", CG.apply [ CG.fqVal [] "not", CG.applyBinOp firstCond CG.or secondCond ], c ]) c_
            )

compositeMatch =
    Match.oneOf
        [ ifThenElse3
        , ifThenElse2
        , ifThenElse1
        , invertIf
        , baseIf
        , meaningfulInteraction
        , ignoredInteractions
        ]
        |> Match.map
            (\v ->
                case v of
                    Just int ->
                        CG.list [ int ]

                    Nothing ->
                        CG.list []
            )

parser =
    Match.map3
        (\label interaction isDanger ->
            Upgrade.call
                ( "DesignSystem.MainButton"
                , if isDanger then
                    "danger"

                  else
                    "primary"
                )
                [ interaction, CG.parens label ]
        )
        (Match.recordField "label" Match.any)
        (Match.recordField "interaction" compositeMatch)
        (Match.recordField "danger" Match.booleanLiteral)

rule =
    Upgrade.rule
        [ Upgrade.application
            { oldName = ( "Ui.Input", "button" )
            , oldArgumentNames = [ "config" ]
            , oldArgumentsToNew =
                \arguments ->
                    case arguments of
                        [ rec ] ->
                            Match.match rec parser

                        _ ->
                            Nothing
            }
        ]

So I wonder if we could provide a library like that?