2021-02-10 19:12:15 +03:00
module NoUnnecessaryTrailingUnderscore exposing ( rule )
{- |
@ docs rule
- }
import Dict exposing ( Dict )
import Elm.Syntax.Declaration as Declaration exposing ( Declaration )
import Elm.Syntax.Expression as Expression exposing ( Expression )
import Elm.Syntax.Node as Node exposing ( Node )
import Elm.Syntax.Pattern as Pattern
import Elm.Syntax.Range exposing ( Range )
import Review.Rule as Rule exposing ( Rule )
import Set exposing ( Set )
{- | R e p o r t s u n n e c e s s a r y o r s u b o p t i m a l t r a i l i n g u n d e r s c o r e s i n v a r i a b l e n a m e s .
config =
[ NoUnnecessaryTrailingUnderscore . rule
]
I don't know how widespread this usage is , but I tend to append variable names with ` _ ` to avoid [ shadowing conflicts ] ( https : // github . com / elm / compiler / blob / 9 d 97114702 bf 6846 cab 622 a 2203 f 60 c 2 d 4 ebedf 2 / hints / shadowing . md ) , for instance like this :
viewName name =
case name of
Just name_ -> .. .
Nothing -> .. .
Obviously when I am able to figure out a better name for one of the two variables , I go for that . In this case , I may rename the argument to ` viewName ` to ` maybeName ` for instance .
But I notice that relatively often , the need for having the trailing underscore disappears , because I changed some code and the variable name that required me to add a trailing underscore has also disappeared . When that happens , the trailing underscore becomes a distraction and I find it nicer to rename the variable to not have that underscore .
This rule does not propose a fix for the issues ( at least for now ) . I recommend renaming the variables through your IDE , as that will be a simpler and safer process .
That said , as we'll see in the following sections and examples , these renames may end up in shadowing issues , so please do these renames with a compiler running next to ( or ` elm - test --watch` for the changes happening in test files) to notice problems early on.
# # Fail
# # # 1 . Unneeded trailing underscore
When a variable has a trailing underscore that could be removed without an issue .
viewName maybeName =
case maybeName of
Just name_ ->
name_
# # # 2 . Top - level declarations containing an underscore
This rule reports top - level declarations containing an underscore , even when removing the underscore would cause a shadowing conflict .
The idea here is that if you have both ` viewName ` and ` viewName_ ` , someone who looks at the code may have a hard time figuring which function they should use and what the differences are between the two .
viewName_ name = .. .
When ` viewName_ ` is only used inside ` viewName ` , an alternative name that you could go for is appending ` Help ` , such as ` viewNameHelp ` , which I ' ve seen regularly often in several packages and codebases .
# # # 3 . Let declarations on the same level where one has an underscore and one doesn't
a =
let
name =
" J e r o e n "
name_ =
" E n g e l s "
in
name ++ " " ++ name_
Very similar to the previous point , we report ` name_ ` because this name is too confusing in the sense that readers won't know which one to use when .
In such instances , I believe there are clear benefits from spending a little bit of time figuring out a better name .
Here is another instance where a better name could be given .
a =
let
model =
{ a = 1 , b = 2 }
model_ =
{ model | b = model . b + 1 }
in
doSomething model_
In this example , even simple naming schemes like ` initialModel ` and ` modelWithUpdatedB ` , or ` modelStep 1 ` and ` modelStep 2 ` would be an improvement over a trailing underscore . I ' m sure you can find even better names for your specific use - case !
# # # 4 . When an underscore is used to avoid a shadowing conflict with a more deeply nested variable
view model_ =
case model_ of
Loaded model ->
text model . name
In this case , ` model_ ` has a trailing underscore to avoid a conflict with ` model ` declared in a deeper scope .
I tend to find constructs like these to be an indication that either
- one or both of those variables have a bad name for what they're represent ( ` model ` could be ` loadedModel ` ? )
- if they represent the same type , again we have an issue where it's not clear when one should be used over the other
# # Success
We don't report errors when there is a reasonable use - case for adding a trailing underscore , or when a variable does not have a trailing underscore .
viewName name =
case name of
Just name_ ->
name_
# # When ( not ) to enable this rule
This is a pretty personal rule that I ' d like to enforce on my own projects .
I have not yet tested it extensively , but I feel like it could bring some value and bring me some sense of satisfaction when I know that the project adheres to this rule .
I feel comfortable enough with asking people making pull requests to the projects I maintain to make changes in order to follow this rule .
But I will probably not enforce this rule in a project where my team is bigger , because this may be too big of a source of frustration for my colleagues , especially if they tend to notice problems in the CI and no way to autofix the issues .
I recommend AGAINST enforcing this rule if you do not agree with the choices I have made , or if you do not have that habit of adding trailing underscores .
If you see some value in it , you may still want to use this rule to detect places where naming could be improved and make improvements to these places , but not end up enforcing it .
# # Try it out
You can try this rule out by running the following command :
` ` ` bash
elm - review --template jfmengels/elm-review-code-style/example --rules NoUnnecessaryTrailingUnderscore
` ` `
- }
rule : Rule
rule =
Rule . newModuleRuleSchema " N o U n n e c e s s a r y T r a i l i n g U n d e r s c o r e " initialContext
|> Rule . withDeclarationListVisitor declarationListVisitor
|> Rule . withDeclarationEnterVisitor declarationVisitor
|> Rule . withExpressionEnterVisitor expressionVisitor
|> Rule . withExpressionExitVisitor expressionExitVisitor
|> Rule . fromModuleRuleSchema
type alias Context =
{ scopes : Scopes
, scopesToAdd : Dict RangeLike ( Set String )
}
type alias Scopes =
( Set String , List ( Set String ) )
type alias RangeLike =
List Int
initialContext : Context
initialContext =
{ scopes = ( Set . empty , [ ] )
, scopesToAdd = Dict . empty
}
declarationListVisitor : List ( Node Declaration ) -> Context -> ( List ( Rule . Error { } ) , Context )
declarationListVisitor declarations context =
let
namesToAdd : Set String
namesToAdd =
List . filterMap
( \ node ->
case Node . value node of
Declaration . FunctionDeclaration function ->
function . declaration
|> Node . value
|> . name
|> Node . value
|> Just
_ ->
Nothing
)
declarations
|> Set . fromList
errors : List ( Rule . Error { } )
errors =
List . filterMap
( \ node ->
case Node . value node of
Declaration . FunctionDeclaration function ->
reportFunction
Set . empty
context . scopes
function
{ message = " T o p - l e v e l d e c l a r a t i o n n a m e s s h o u l d n o t e n d w i t h a n u n d e r s c o r e "
, details =
[ " A t r a i l i n g u n d e r s c o r e \" _ \" i s o f t e n u s e d t o p r e v e n t s h a d o w i n g i s s u e s , b u t t o p - l e v e l d e c l a r a t i o n s s h o u l d n o t r e s o l v e t h e s e i s s u e s i n t h a t m a n n e r . "
]
}
_ ->
Nothing
)
declarations
in
( errors
, { context | scopes = Tuple . mapFirst ( Set . union namesToAdd ) context . scopes }
)
declarationVisitor : Node Declaration -> Context -> ( List ( Rule . Error { } ) , Context )
declarationVisitor node context =
case Node . value node of
Declaration . FunctionDeclaration function ->
let
arguments : List ( Node Pattern . Pattern )
arguments =
function . declaration
|> Node . value
|> . arguments
body : Node Expression
body =
function . declaration
|> Node . value
|> . expression
in
report
[ ( arguments , body ) ]
context
_ ->
( [ ] , context )
type alias ScopeNames =
{ name : String
, range : Range
, origin : NameOrigin
}
type NameOrigin
= FromRecord
| NotFromRecord
getDeclaredVariableNames : Node Pattern . Pattern -> List ScopeNames
getDeclaredVariableNames pattern =
case Node . value pattern of
Pattern . VarPattern name ->
[ { name = name , range = Node . range pattern , origin = NotFromRecord } ]
Pattern . ParenthesizedPattern subPattern ->
getDeclaredVariableNames subPattern
Pattern . AsPattern subPattern name ->
{ name = Node . value name , range = Node . range name , origin = NotFromRecord } :: getDeclaredVariableNames subPattern
Pattern . TuplePattern patterns ->
List . concatMap getDeclaredVariableNames patterns
Pattern . UnConsPattern left right ->
List . concatMap getDeclaredVariableNames [ left , right ]
Pattern . ListPattern patterns ->
List . concatMap getDeclaredVariableNames patterns
Pattern . NamedPattern _ patterns ->
List . concatMap getDeclaredVariableNames patterns
Pattern . RecordPattern fields ->
List . map ( \ field -> { name = Node . value field , range = Node . range field , origin = FromRecord } ) fields
_ ->
[ ]
reservedElmKeywords : Set String
reservedElmKeywords =
Set . fromList
[ " i f _ "
, " t h e n _ "
, " e l s e _ "
, " c a s e _ "
, " o f _ "
, " l e t _ "
, " i n _ "
, " t y p e _ "
, " m o d u l e _ "
, " w h e r e _ "
, " i m p o r t _ "
, " e x p o s i n g _ "
, " a s _ "
, " p o r t _ "
2021-05-26 23:29:13 +03:00
-- Until `elm-format` and `elm-syntax` allow `infix` as an identifier
, " i n f i x _ "
2021-02-10 19:12:15 +03:00
]
expressionVisitor : Node Expression -> Context -> ( List ( Rule . Error { } ) , Context )
expressionVisitor node context =
let
newContext : Context
newContext =
case Dict . get ( Node . range node |> rangeToRangeLike ) context . scopesToAdd of
Just scopeToAdd ->
{ context | scopes = addNewScope scopeToAdd context . scopes }
Nothing ->
context
in
expressionVisitorHelp node newContext
expressionExitVisitor : Node Expression -> Context -> ( List nothing , Context )
expressionExitVisitor node context =
let
newContext : Context
newContext =
if Dict . member ( Node . range node |> rangeToRangeLike ) context . scopesToAdd then
{ context | scopes = popScope context . scopes }
else
context
in
( [ ] , newContext )
expressionVisitorHelp : Node Expression -> Context -> ( List ( Rule . Error { } ) , Context )
expressionVisitorHelp node context =
case Node . value node of
Expression . CaseExpression { cases } ->
report
( List . map ( Tuple . mapFirst List . singleton ) cases )
context
Expression . LambdaExpression { args , expression } ->
report
[ ( args , expression ) ]
context
Expression . LetExpression { declarations , expression } ->
let
namesFromLetDeclarations : Set String
namesFromLetDeclarations =
Set . fromList ( getNamesFromLetDeclarations declarations )
( errors , newContext ) =
report
( List . concatMap
( \ declaration ->
case Node . value declaration of
Expression . LetFunction function ->
let
functionImplementation : Expression . FunctionImplementation
functionImplementation =
Node . value function . declaration
in
[ ( functionImplementation . arguments
, functionImplementation . expression
)
]
Expression . LetDestructuring _ _ ->
[ ]
)
declarations
)
{ context | scopes = addNewScope namesFromLetDeclarations context . scopes }
in
( reportErrorsForLet namesFromLetDeclarations context . scopes declarations ++ errors
, { newContext
| scopesToAdd =
Dict . insert
( rangeToRangeLike ( Node . range expression ) )
namesFromLetDeclarations
newContext . scopesToAdd
}
)
_ ->
( [ ] , context )
getNamesFromLetDeclarations : List ( Node Expression . LetDeclaration ) -> List String
getNamesFromLetDeclarations declarations =
List . concatMap
( \ declaration ->
case Node . value declaration of
Expression . LetFunction function ->
[ function . declaration
|> Node . value
|> . name
|> Node . value
]
Expression . LetDestructuring pattern _ ->
List . map . name ( getDeclaredVariableNames pattern )
)
declarations
reportErrorsForLet : Set String -> Scopes -> List ( Node Expression . LetDeclaration ) -> List ( Rule . Error { } )
reportErrorsForLet namesFromLetDeclarations scopes declarations =
List . concatMap
( \ node ->
case Node . value node of
Expression . LetFunction function ->
let
functionName : String
functionName =
function . declaration
|> Node . value
|> . name
|> Node . value
in
case
reportFunction
namesFromLetDeclarations
scopes
function
( defaultErrorAndMessage functionName )
of
Just newError ->
[ newError ]
Nothing ->
[ ]
Expression . LetDestructuring pattern _ ->
let
declaredVariables : List ScopeNames
declaredVariables =
getDeclaredVariableNames pattern
names : Set String
names =
declaredVariables
|> List . map . name
|> Set . fromList
in
List . filterMap ( error ( addNewScope names scopes ) ) declaredVariables
)
declarations
reportFunction : Set String -> Scopes -> Expression . Function -> { message : String , details : List String } -> Maybe ( Rule . Error { } )
reportFunction namesOnTheSameLevel scopes function messageAndDetails =
let
functionNameNode : Node String
functionNameNode =
function . declaration
|> Node . value
|> . name
functionName : String
functionName =
Node . value functionNameNode
functionNameWithoutUnderscore : String
functionNameWithoutUnderscore =
String . dropRight 1 functionName
in
if
String . endsWith " _ " functionName
&& not ( isDefinedInScope scopes functionNameWithoutUnderscore )
&& not ( Set . member functionName reservedElmKeywords )
then
if Set . member functionNameWithoutUnderscore namesOnTheSameLevel then
Just
( Rule . error
{ message = functionName ++ " s h o u l d n o t e n d w i t h a n u n d e r s c o r e "
, details =
[ " I t s e e m s t h a t i t h a s b e e n u s e d t o p r e v e n t s h a d o w i n g i s s u e s w i t h a v a r i a b l e o n t h e s a m e l e v e l , b u t t h i s i s c o n f u s i n g . W h e n s h o u l d \" " ++ functionName ++ " \" b e u s e d a n d w h e n s h o u l d \" " ++ functionNameWithoutUnderscore ++ " \" b e u s e d ? "
, " P l e a s e r e n a m e t h i s v a r i a b l e i n a w a y t h a t m a k e s i t m o r e e x p l i c i t w h e n o r h o w e a c h s h o u l d b e u s e d . "
]
}
( Node . range functionNameNode )
)
else
Just
( Rule . error
messageAndDetails
( Node . range functionNameNode )
)
else
Nothing
report : List ( List ( Node Pattern . Pattern ) , Node a ) -> Context -> ( List ( Rule . Error { } ) , Context )
report patternsAndBody context =
let
scopesToAdd : List { errors : List ( Rule . Error { } ) , scopesToAdd : ( RangeLike , Set String ) }
scopesToAdd =
List . map
( \ ( patterns , expression ) ->
let
declaredVariables : List ScopeNames
declaredVariables =
List . concatMap getDeclaredVariableNames patterns
names : Set String
names =
declaredVariables
|> List . map . name
|> Set . fromList
in
{ errors = List . filterMap ( error ( addNewScope names context . scopes ) ) declaredVariables
, scopesToAdd =
( rangeToRangeLike ( Node . range expression )
, names
)
}
)
patternsAndBody
in
( List . concatMap . errors scopesToAdd
, { context
| scopesToAdd =
Dict . union
( scopesToAdd |> List . map . scopesToAdd |> Dict . fromList )
context . scopesToAdd
}
)
addNewScope : Set String -> Scopes -> Scopes
addNewScope set ( head , tail ) =
( set , head :: tail )
popScope : Scopes -> Scopes
popScope ( head , tail ) =
case tail of
[ ] ->
( head , tail )
newHead :: newTail ->
( newHead , newTail )
error : Scopes -> ScopeNames -> Maybe ( Rule . Error { } )
error scopes { range , name , origin } =
if
String . endsWith " _ " name
&& not ( isDefinedInScope scopes ( String . dropRight 1 name ) )
&& not ( Set . member name reservedElmKeywords )
&& shouldNameBeReported origin
then
Just
( Rule . error
( defaultErrorAndMessage name )
range
)
else
Nothing
defaultErrorAndMessage : String -> { message : String , details : List String }
defaultErrorAndMessage name =
{ message = name ++ " s h o u l d n o t e n d w i t h a n u n d e r s c o r e "
, details =
[ " I t l o o k s l i k e t h i s w a s u s e d t o a v o i d a s h a d o w i n g i s s u e , b u t t h e v a r i a b l e i t w o u l d h a v e c l a s h e d w i t h i s n o t p r e s e n t i n t h e s c o p e o f w h e r e t h i s v a r i a b l e w a s d e c l a r e d a n y m o r e . Y o u s h o u l d r e n a m e t h e v a r i a b l e a n d r e m o v e t h e u n d e r s c o r e . "
, " N o t e t h a t t h i s m a y n o t b e a s a f e c h a n g e , i n t h a t r e n a m i n g m a y c l a s h w i t h a v a l u e d e c l a r e d d e e p e r i n t h e e x p r e s s i o n , b u t I t h i n k i t ' s l e s s c o n f u s i n g t o h a v e t h e n e s t e d v a r i a b l e h a v e a t r a i l i n g u n d e r s c o r e r a t h e r t h a n t h e v a r i a b l e d e c l a r e d h i g h e r - u p . "
]
}
shouldNameBeReported : NameOrigin -> Bool
shouldNameBeReported origin =
case origin of
FromRecord ->
False
NotFromRecord ->
True
isDefinedInScope : Scopes -> String -> Bool
isDefinedInScope ( top , rest ) name =
List . any ( Set . member name ) ( top :: rest )
rangeToRangeLike : Range -> RangeLike
rangeToRangeLike range =
[ range . start . row
, range . start . column
, range . end . row
, range . end . column
]