Backport rules from elm-review-unused

This commit is contained in:
Jeroen Engels 2021-10-24 00:38:08 +02:00
parent 076b299a82
commit b71984aebb
12 changed files with 946 additions and 722 deletions

View File

@ -516,20 +516,21 @@ collectUsedCustomTypeArgs lookupTable (Node range pattern) =
case pattern of
Pattern.NamedPattern { name } args ->
let
usedPositions : Set Int
usedPositions =
args
|> List.indexedMap Tuple.pair
|> List.filter (\( _, subPattern ) -> not <| isWildcard subPattern)
|> List.map Tuple.first
|> Set.fromList
subList : List ( ( ModuleName, String ), Set Int )
subList =
List.concatMap (collectUsedCustomTypeArgs lookupTable) args
in
case ModuleNameLookupTable.moduleNameAt lookupTable range of
Just moduleName ->
let
usedPositions : Set Int
usedPositions =
args
|> List.indexedMap Tuple.pair
|> List.filter (\( _, subPattern ) -> not <| isWildcard subPattern)
|> List.map Tuple.first
|> Set.fromList
in
( ( moduleName, name ), usedPositions ) :: subList
Nothing ->

View File

@ -2,9 +2,6 @@ module NoUnused.CustomTypeConstructors exposing (rule)
{-| Forbid having unused custom type constructors inside the project.
# Rule
@docs rule
-}
@ -22,7 +19,6 @@ import Elm.Syntax.Pattern as Pattern exposing (Pattern)
import Elm.Syntax.Range exposing (Range)
import Elm.Syntax.Type as Type
import Elm.Syntax.TypeAnnotation as TypeAnnotation exposing (TypeAnnotation)
import NoUnused.RangeDict as RangeDict exposing (RangeDict)
import Review.Fix as Fix exposing (Fix)
import Review.ModuleNameLookupTable as ModuleNameLookupTable exposing (ModuleNameLookupTable)
import Review.Rule as Rule exposing (Error, Rule)
@ -149,7 +145,8 @@ moduleVisitor schema =
|> Rule.withDeclarationListVisitor declarationListVisitor
|> Rule.withDeclarationEnterVisitor declarationVisitor
|> Rule.withExpressionEnterVisitor expressionVisitor
|> Rule.withExpressionExitVisitor expressionExitVisitor
|> Rule.withCaseBranchEnterVisitor caseBranchEnterVisitor
|> Rule.withCaseBranchExitVisitor caseBranchExitVisitor
@ -203,7 +200,6 @@ type alias ModuleContext =
, declaredTypesWithConstructors : Dict CustomTypeName (Dict ConstructorName ConstructorInformation)
, usedFunctionsOrValues : Dict ModuleNameAsString (Set ConstructorName)
, phantomVariables : Dict ModuleName (List ( CustomTypeName, Int ))
, ignoreBlocks : List (RangeDict (Set ( ModuleName, String )))
, constructorsToIgnore : List (Set ( ModuleName, String ))
, wasUsedInLocationThatNeedsItself : Set ( ModuleNameAsString, ConstructorName )
, wasUsedInComparisons : Set ( ModuleNameAsString, ConstructorName )
@ -244,7 +240,6 @@ fromProjectToModule lookupTable metadata projectContext =
, declaredTypesWithConstructors = Dict.empty
, usedFunctionsOrValues = Dict.empty
, phantomVariables = projectContext.phantomVariables
, ignoreBlocks = []
, constructorsToIgnore = []
, wasUsedInLocationThatNeedsItself = Set.empty
, wasUsedInComparisons = Set.empty
@ -366,12 +361,12 @@ foldProjectContexts newContext previousContext =
}
mergeDictsWithLists : Dict comparable appendable -> Dict comparable appendable -> Dict comparable appendable
mergeDictsWithLists : Dict comparable (List a) -> Dict comparable (List a) -> Dict comparable (List a)
mergeDictsWithLists left right =
Dict.merge
(\key a dict -> Dict.insert key a dict)
(\key a b dict -> Dict.insert key (a ++ b) dict)
(\key b dict -> Dict.insert key b dict)
Dict.insert
(\key a b dict -> Dict.insert key (List.append a b) dict)
Dict.insert
left
right
Dict.empty
@ -622,52 +617,6 @@ isNeverOrItself lookupTable typeName node =
expressionVisitor : Node Expression -> ModuleContext -> ( List nothing, ModuleContext )
expressionVisitor node moduleContext =
let
newModuleContext : ModuleContext
newModuleContext =
case List.head moduleContext.ignoreBlocks of
Just expressionsWhereToIgnoreCases ->
case RangeDict.get (Node.range node) expressionsWhereToIgnoreCases of
Just constructorsToIgnore ->
{ moduleContext | constructorsToIgnore = constructorsToIgnore :: moduleContext.constructorsToIgnore }
Nothing ->
moduleContext
Nothing ->
moduleContext
in
expressionVisitorHelp node newModuleContext
expressionExitVisitor : Node Expression -> ModuleContext -> ( List nothing, ModuleContext )
expressionExitVisitor node moduleContext =
let
newModuleContext : ModuleContext
newModuleContext =
case Node.value node of
Expression.CaseExpression _ ->
{ moduleContext | ignoreBlocks = List.drop 1 moduleContext.ignoreBlocks }
_ ->
moduleContext
in
case List.head newModuleContext.ignoreBlocks of
Just rangesWhereToIgnoreConstructors ->
if RangeDict.member (Node.range node) rangesWhereToIgnoreConstructors then
( []
, { newModuleContext | constructorsToIgnore = List.drop 1 newModuleContext.constructorsToIgnore }
)
else
( [], newModuleContext )
Nothing ->
( [], newModuleContext )
expressionVisitorHelp : Node Expression -> ModuleContext -> ( List nothing, ModuleContext )
expressionVisitorHelp node moduleContext =
case Node.value node of
Expression.FunctionOrValue _ name ->
case ModuleNameLookupTable.moduleNameFor moduleContext.lookupTable node of
@ -779,42 +728,86 @@ expressionVisitorHelp node moduleContext =
|> List.foldl markPhantomTypesFromTypeAnnotationAsUsed moduleContext
)
Expression.CaseExpression { cases } ->
let
found : List { ignoreBlock : ( Range, Set ( ModuleName, ConstructorName ) ), fixes : Dict ConstructorName (List Fix) }
found =
List.map2
(forOne moduleContext.lookupTable)
(Nothing :: List.map (Tuple.second >> Node.range >> .end >> Just) cases)
cases
ignoredBlocks : RangeDict (Set ( ModuleName, String ))
ignoredBlocks =
List.map .ignoreBlock found
|> RangeDict.fromList
wasUsedInOtherModules : Set ( ModuleNameAsString, ConstructorName )
wasUsedInOtherModules =
found
|> List.map (.ignoreBlock >> Tuple.second >> toSetOfModuleNameAsString)
|> List.foldl Set.union moduleContext.wasUsedInOtherModules
in
( []
, { moduleContext
| ignoreBlocks = ignoredBlocks :: moduleContext.ignoreBlocks
, wasUsedInOtherModules = wasUsedInOtherModules
, fixesForRemovingConstructor =
List.foldl
mergeDictsWithLists
moduleContext.fixesForRemovingConstructor
(List.map .fixes found)
}
)
_ ->
( [], moduleContext )
caseBranchEnterVisitor : Node Expression.CaseBlock -> ( Node Pattern, Node Expression ) -> ModuleContext -> ( List nothing, ModuleContext )
caseBranchEnterVisitor caseExpression ( casePattern, body ) moduleContext =
let
previousLocation : Maybe Elm.Syntax.Range.Location
previousLocation =
findEndLocationOfPreviousElement (Node.value caseExpression).cases (Node.range casePattern) Nothing
constructors : Set ( ModuleName, String )
constructors =
constructorsInPattern moduleContext.lookupTable casePattern
fixes : Dict ConstructorName (List Fix)
fixes =
List.foldl
(\( moduleName, constructorName ) acc ->
if moduleName == [] then
let
fix : Fix
fix =
Fix.removeRange
{ start = Maybe.withDefault (Node.range casePattern).start previousLocation
, end = (Node.range body).end
}
in
Dict.update
constructorName
(\existing ->
case existing of
Just list ->
Just (fix :: list)
Nothing ->
Just [ fix ]
)
acc
else
acc
)
moduleContext.fixesForRemovingConstructor
(Set.toList constructors)
wasUsedInOtherModules : Set ( ModuleNameAsString, ConstructorName )
wasUsedInOtherModules =
toSetOfModuleNameAsString constructors
in
( []
, { moduleContext
| wasUsedInOtherModules = Set.union wasUsedInOtherModules moduleContext.wasUsedInOtherModules
, constructorsToIgnore = constructors :: moduleContext.constructorsToIgnore
, fixesForRemovingConstructor = fixes
}
)
caseBranchExitVisitor : Node Expression.CaseBlock -> ( Node Pattern, Node Expression ) -> ModuleContext -> ( List nothing, ModuleContext )
caseBranchExitVisitor _ _ moduleContext =
( []
, { moduleContext | constructorsToIgnore = List.drop 1 moduleContext.constructorsToIgnore }
)
findEndLocationOfPreviousElement : List ( Node a, Node b ) -> Range -> Maybe Elm.Syntax.Range.Location -> Maybe Elm.Syntax.Range.Location
findEndLocationOfPreviousElement nodes nodeRange previousRangeEnd =
case nodes of
( Node patternRange _, Node bodyRange _ ) :: tail ->
if patternRange == nodeRange then
previousRangeEnd
else
findEndLocationOfPreviousElement tail nodeRange (Just bodyRange.end)
[] ->
Nothing
toSetOfModuleNameAsString : Set ( ModuleName, ConstructorName ) -> Set ( ModuleNameAsString, ConstructorName )
toSetOfModuleNameAsString set =
set
@ -822,38 +815,6 @@ toSetOfModuleNameAsString set =
|> Set.filter (\( moduleName, _ ) -> moduleName /= "")
forOne : ModuleNameLookupTable -> Maybe Elm.Syntax.Range.Location -> ( Node Pattern, Node a ) -> { ignoreBlock : ( Range, Set ( ModuleName, String ) ), fixes : Dict ConstructorName (List Fix) }
forOne lookupTable previousLocation ( pattern, body ) =
let
constructors : Set ( ModuleName, String )
constructors =
constructorsInPattern lookupTable pattern
fixes : Dict ConstructorName (List Fix)
fixes =
List.foldl
(\( moduleName, constructorName ) acc ->
if moduleName == [] then
Dict.insert
constructorName
[ Fix.removeRange
{ start = Maybe.withDefault (Node.range pattern).start previousLocation
, end = (Node.range body).end
}
]
acc
else
acc
)
Dict.empty
(Set.toList constructors)
in
{ ignoreBlock = ( Node.range body, constructors )
, fixes = fixes
}
staticRanges : Node Expression -> List Range
staticRanges node =
case Node.value node of

View File

@ -2,9 +2,6 @@ module NoUnused.Dependencies exposing (rule)
{-| Forbid the use of dependencies that are never used in your project.
# Rule
@docs rule
-}
@ -70,6 +67,11 @@ dependenciesVisitor dependencies projectContext =
moduleNameToDependency : Dict String String
moduleNameToDependency =
dependencies
|> Dict.filter
(\packageName _ ->
Set.member packageName projectContext.directProjectDependencies
|| Set.member packageName projectContext.directTestDependencies
)
|> Dict.toList
|> List.concatMap
(\( packageName, dependency ) ->
@ -384,56 +386,7 @@ fromProject : Dict String Dependency -> DependencyLocation -> String -> Project
fromProject dependenciesDict dependencyLocation packageNameStr project =
case project of
Elm.Project.Application application ->
let
dependencies : Elm.Project.Deps Elm.Version.Version
dependencies =
case dependencyLocation of
InProjectDeps ->
application.depsDirect
InTestDeps ->
application.testDepsDirect
dependencyVersionDict : Dict String Elm.Version.Version
dependencyVersionDict =
[ application.depsDirect
, application.depsIndirect
, application.testDepsDirect
, application.testDepsIndirect
]
|> List.concat
|> List.map (\( name, version ) -> ( Elm.Package.toString name, version ))
|> Dict.fromList
getDependenciesAndVersion : Elm.Package.Name -> Elm.Project.Deps Elm.Version.Version
getDependenciesAndVersion name =
case Dict.get (Elm.Package.toString name) dependenciesDict of
Just deps ->
deps
|> Dependency.elmJson
|> packageDependencies
|> List.filterMap
(\depName ->
Dict.get (Elm.Package.toString depName) dependencyVersionDict
|> Maybe.map (Tuple.pair depName)
)
Nothing ->
[]
in
case List.Extra.find (isPackageWithName packageNameStr) dependencies of
Just ( packageName, version ) ->
Just
(ApplicationProject
{ application = application
, name = packageName
, version = version
, getDependenciesAndVersion = getDependenciesAndVersion
}
)
Nothing ->
Nothing
fromApplication dependenciesDict dependencyLocation packageNameStr application
Elm.Project.Package packageInfo ->
let
@ -454,6 +407,60 @@ fromProject dependenciesDict dependencyLocation packageNameStr project =
Nothing
fromApplication : Dict String Dependency -> DependencyLocation -> String -> Elm.Project.ApplicationInfo -> Maybe ProjectAndDependencyIdentifier
fromApplication dependenciesDict dependencyLocation packageNameStr application =
let
dependencies : Elm.Project.Deps Elm.Version.Version
dependencies =
case dependencyLocation of
InProjectDeps ->
application.depsDirect
InTestDeps ->
application.testDepsDirect
dependencyVersionDict : Dict String Elm.Version.Version
dependencyVersionDict =
[ application.depsDirect
, application.depsIndirect
, application.testDepsDirect
, application.testDepsIndirect
]
|> List.concat
|> List.map (\( name, version ) -> ( Elm.Package.toString name, version ))
|> Dict.fromList
getDependenciesAndVersion : Elm.Package.Name -> Elm.Project.Deps Elm.Version.Version
getDependenciesAndVersion name =
case Dict.get (Elm.Package.toString name) dependenciesDict of
Just deps ->
deps
|> Dependency.elmJson
|> packageDependencies
|> List.filterMap
(\depName ->
Dict.get (Elm.Package.toString depName) dependencyVersionDict
|> Maybe.map (Tuple.pair depName)
)
Nothing ->
[]
in
case List.Extra.find (isPackageWithName packageNameStr) dependencies of
Just ( packageName, version ) ->
Just
(ApplicationProject
{ application = application
, name = packageName
, version = version
, getDependenciesAndVersion = getDependenciesAndVersion
}
)
Nothing ->
Nothing
toProject : ProjectAndDependencyIdentifier -> Elm.Project.Project
toProject projectAndDependencyIdentifier =
case projectAndDependencyIdentifier of

View File

@ -116,6 +116,32 @@ applicationElmJsonWithoutTestDeps =
}"""
applicationElmJsonWithIndirectOtherFooWithoutTestDeps : String
applicationElmJsonWithIndirectOtherFooWithoutTestDeps =
"""
{
"type": "application",
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"author/package-with-bar": "1.0.0",
"author/package-with-foo": "1.0.0",
"elm/core": "1.0.0"
},
"indirect": {
"author/package-with-other-foo": "1.0.0"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}"""
packageElmJson : String
packageElmJson =
"""
@ -196,6 +222,34 @@ packageWithFoo =
(dummyModules "Foo")
packageWithOtherFoo : Dependency
packageWithOtherFoo =
let
elmJson : { path : String, raw : String, project : Elm.Project.Project }
elmJson =
createElmJson """
{
"type": "package",
"name": "author/package-with-other-foo",
"summary": "Summary",
"license": "BSD-3-Clause",
"version": "1.0.0",
"exposed-modules": [
"Foo"
],
"elm-version": "0.19.0 <= v < 0.20.0",
"dependencies": {
"elm/core": "1.0.0 <= v < 2.0.0"
},
"test-dependencies": {}
}"""
in
Dependency.create
"author/package-with-other-foo"
elmJson.project
(dummyModules "Foo")
packageWithFooDependingOnBar : Dependency
packageWithFooDependingOnBar =
let
@ -474,6 +528,33 @@ a = 1
|> String.replace "\u{000D}" ""
|> Review.Test.runWithProjectData (createProject (Just testModule) applicationElmJson) rule
|> Review.Test.expectNoErrors
, test "should not report dependencies for an application whose modules are imported indirect name clash" <|
\() ->
let
testModule : String
testModule =
"""module TestModule exposing (suite)
import TestFoo
import TestBar
suite = 0
"""
|> String.replace "\u{000D}" ""
in
"""
module A exposing (a)
import Foo
import Bar
a = 1
"""
|> String.replace "\u{000D}" ""
|> Review.Test.runWithProjectData
(createProject (Just testModule) applicationElmJsonWithIndirectOtherFooWithoutTestDeps
|> Project.addDependency packageWithOtherFoo
)
rule
|> Review.Test.expectNoErrors
, test "should report unused dependencies for a package when none of their modules are imported" <|
\() ->
"""

View File

@ -2,9 +2,6 @@ module NoUnused.Exports exposing (rule)
{-| Forbid the use of exposed elements that are never used in your project.
# Rule
@docs rule
-}
@ -544,12 +541,6 @@ importVisitor node moduleContext =
declarationListVisitor : List (Node Declaration) -> ModuleContext -> ( List nothing, ModuleContext )
declarationListVisitor declarations moduleContext =
let
declaredNames : Set String
declaredNames =
declarations
|> List.filterMap (Node.value >> declarationName)
|> Set.fromList
typesUsedInDeclaration_ : List ( List ( ModuleName, String ), Bool )
typesUsedInDeclaration_ =
declarations
@ -577,6 +568,13 @@ declarationListVisitor declarations moduleContext =
identity
else
let
declaredNames : Set String
declaredNames =
declarations
|> List.filterMap (Node.value >> declarationName)
|> Set.fromList
in
Dict.filter (\name _ -> Set.member name declaredNames)
)
, elementsNotToReport =

View File

@ -2,9 +2,6 @@ module NoUnused.Modules exposing (rule)
{-| Forbid the use of modules that are never used in your project.
# Rule
@docs rule
-}

View File

@ -26,7 +26,7 @@ available.
# Access
@docs head, sample
@docs head
# Convert

View File

@ -2,9 +2,6 @@ module NoUnused.Parameters exposing (rule)
{-| Report parameters that are not used.
# Rule
@docs rule
-}
@ -15,7 +12,6 @@ import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node(..))
import Elm.Syntax.Pattern as Pattern exposing (Pattern)
import Elm.Syntax.Range as Range exposing (Range)
import NoUnused.RangeDict as RangeDict exposing (RangeDict)
import Review.Fix as Fix exposing (Fix)
import Review.Rule as Rule exposing (Rule)
import Set exposing (Set)
@ -76,9 +72,12 @@ elm-review --template jfmengels/elm-review-unused/example --rules NoUnused.Param
rule : Rule
rule =
Rule.newModuleRuleSchema "NoUnused.Parameters" initialContext
|> Rule.withDeclarationEnterVisitor declarationVisitor
|> Rule.withDeclarationEnterVisitor declarationEnterVisitor
|> Rule.withDeclarationExitVisitor declarationExitVisitor
|> Rule.withExpressionEnterVisitor expressionEnterVisitor
|> Rule.withExpressionExitVisitor expressionExitVisitor
|> Rule.withLetDeclarationEnterVisitor letDeclarationEnterVisitor
|> Rule.withLetDeclarationExitVisitor letDeclarationExitVisitor
|> Rule.fromModuleRuleSchema
@ -88,7 +87,6 @@ rule =
type alias Context =
{ scopes : List Scope
, scopesToCreate : RangeDict ScopeToCreate
, knownFunctions : Dict String FunctionArgs
, locationsToIgnoreForUsed : LocationsToIgnore
}
@ -102,13 +100,6 @@ type alias Scope =
}
type alias ScopeToCreate =
{ declared : List Declared
, functionName : String
, functionArgs : FunctionArgs
}
type alias Declared =
{ name : String
, range : Range
@ -141,7 +132,6 @@ type Source
initialContext : Context
initialContext =
{ scopes = []
, scopesToCreate = RangeDict.empty
, knownFunctions = Dict.empty
, locationsToIgnoreForUsed = Dict.empty
}
@ -151,8 +141,8 @@ initialContext =
-- DECLARATION VISITOR
declarationVisitor : Node Declaration -> Context -> ( List nothing, Context )
declarationVisitor node context =
declarationEnterVisitor : Node Declaration -> Context -> ( List nothing, Context )
declarationEnterVisitor node context =
case Node.value node of
Declaration.FunctionDeclaration { declaration } ->
let
@ -163,17 +153,20 @@ declarationVisitor node context =
declared : List (List Declared)
declared =
List.map (getParametersFromPatterns NamedFunction) arguments
functionName : String
functionName =
Node.value declaration |> .name |> Node.value
in
( []
, { scopes = []
, scopesToCreate =
RangeDict.singleton
(declaration |> Node.value |> .expression |> Node.range)
{ declared = List.concat declared
, functionName = Node.value declaration |> .name |> Node.value
, functionArgs = getArgNames declared
}
, knownFunctions = Dict.empty
, { scopes =
[ { functionName = functionName
, declared = List.concat declared
, used = Set.empty
, usedRecursively = Set.empty
}
]
, knownFunctions = Dict.singleton functionName (getArgNames declared)
, locationsToIgnoreForUsed = Dict.empty
}
)
@ -182,6 +175,16 @@ declarationVisitor node context =
( [], context )
declarationExitVisitor : Node Declaration -> Context -> ( List (Rule.Error {}), Context )
declarationExitVisitor node context =
case Node.value node of
Declaration.FunctionDeclaration _ ->
report context
_ ->
( [], context )
getArgNames : List (List Declared) -> FunctionArgs
getArgNames declared =
declared
@ -214,32 +217,7 @@ getParametersFromPatterns source node =
]
Pattern.AsPattern pattern asName ->
let
parametersFromPatterns : List Declared
parametersFromPatterns =
getParametersFromPatterns source pattern
asParameter : Declared
asParameter =
{ name = Node.value asName
, range = Node.range asName
, kind = Alias
, fix = [ Fix.removeRange { start = (Node.range pattern).end, end = (Node.range asName).end } ]
, source = source
}
in
if List.isEmpty parametersFromPatterns && isPatternWildCard pattern then
[ asParameter
, { name = ""
, range = Node.range pattern
, kind = AsWithoutVariables
, fix = [ Fix.removeRange { start = (Node.range pattern).start, end = (Node.range asName).start } ]
, source = source
}
]
else
asParameter :: parametersFromPatterns
getParametersFromAsPattern source pattern asName
Pattern.RecordPattern fields ->
case fields of
@ -298,6 +276,36 @@ getParametersFromPatterns source node =
[]
getParametersFromAsPattern : Source -> Node Pattern -> Node String -> List Declared
getParametersFromAsPattern source pattern asName =
let
parametersFromPatterns : List Declared
parametersFromPatterns =
getParametersFromPatterns source pattern
asParameter : Declared
asParameter =
{ name = Node.value asName
, range = Node.range asName
, kind = Alias
, fix = [ Fix.removeRange { start = (Node.range pattern).end, end = (Node.range asName).end } ]
, source = source
}
in
if List.isEmpty parametersFromPatterns && isPatternWildCard pattern then
[ asParameter
, { name = ""
, range = Node.range pattern
, kind = AsWithoutVariables
, fix = [ Fix.removeRange { start = (Node.range pattern).start, end = (Node.range asName).start } ]
, source = source
}
]
else
asParameter :: parametersFromPatterns
isPatternWildCard : Node Pattern -> Bool
isPatternWildCard node =
case Node.value node of
@ -322,107 +330,115 @@ formatRecord fields =
expressionEnterVisitor : Node Expression -> Context -> ( List nothing, Context )
expressionEnterVisitor node context =
let
newContext : Context
newContext =
case RangeDict.get (Node.range node) context.scopesToCreate of
Just { declared, functionName, functionArgs } ->
{ context
| scopes =
{ functionName = functionName
, declared = declared
, used = Set.empty
, usedRecursively = Set.singleton "unused"
}
:: context.scopes
, knownFunctions =
Dict.insert
functionName
functionArgs
context.knownFunctions
}
Nothing ->
context
in
expressionEnterVisitorHelp node newContext
( [], expressionEnterVisitorHelp node context )
expressionEnterVisitorHelp : Node Expression -> Context -> ( List nothing, Context )
expressionEnterVisitorHelp : Node Expression -> Context -> Context
expressionEnterVisitorHelp node context =
case Node.value node of
Expression.FunctionOrValue [] name ->
( [], markValueAsUsed (Node.range node) name context )
markValueAsUsed (Node.range node) name context
Expression.RecordUpdateExpression name _ ->
( [], markValueAsUsed (Node.range name) (Node.value name) context )
markValueAsUsed (Node.range name) (Node.value name) context
Expression.LetExpression letBlock ->
let
declaredWithRange : List ( Range, ScopeToCreate )
declaredWithRange =
List.filterMap
(\letDeclaration ->
case Node.value letDeclaration of
Expression.LetFunction function ->
let
declaration : Expression.FunctionImplementation
declaration =
Node.value function.declaration
declared : List (List Declared)
declared =
List.map (getParametersFromPatterns NamedFunction) declaration.arguments
in
if List.isEmpty declared then
Nothing
else
Just
( Node.range declaration.expression
, { declared = List.concat declared
, functionName = Node.value declaration.name
, functionArgs = getArgNames declared
}
)
Expression.LetDestructuring _ _ ->
Nothing
)
letBlock.declarations
scopesToCreate : RangeDict ScopeToCreate
scopesToCreate =
RangeDict.insertAll declaredWithRange context.scopesToCreate
in
( [], { context | scopesToCreate = scopesToCreate } )
Expression.LambdaExpression { args, expression } ->
let
scopesToCreate : RangeDict ScopeToCreate
scopesToCreate =
RangeDict.insert
(Node.range expression)
{ declared = List.concatMap (getParametersFromPatterns Lambda) args
, functionName = "dummy lambda"
, functionArgs = Dict.empty
}
context.scopesToCreate
in
( [], { context | scopesToCreate = scopesToCreate } )
Expression.LambdaExpression { args } ->
{ context
| scopes =
{ functionName = "dummy lambda"
, declared = List.concatMap (getParametersFromPatterns Lambda) args
, used = Set.empty
, usedRecursively = Set.empty
}
:: context.scopes
}
Expression.Application ((Node _ (Expression.FunctionOrValue [] fnName)) :: arguments) ->
( [], registerFunctionCall fnName 0 arguments context )
registerFunctionCall fnName 0 arguments context
Expression.OperatorApplication "|>" _ lastArgument (Node _ (Expression.Application ((Node _ (Expression.FunctionOrValue [] fnName)) :: arguments))) ->
-- Ignoring "arguments" because they will be visited when the Application node will be visited anyway.
( [], registerFunctionCall fnName (List.length arguments) [ lastArgument ] context )
registerFunctionCall fnName (List.length arguments) [ lastArgument ] context
Expression.OperatorApplication "<|" _ (Node _ (Expression.Application ((Node _ (Expression.FunctionOrValue [] fnName)) :: arguments))) lastArgument ->
-- Ignoring "arguments" because they will be visited when the Application node will be visited anyway.
( [], registerFunctionCall fnName (List.length arguments) [ lastArgument ] context )
registerFunctionCall fnName (List.length arguments) [ lastArgument ] context
_ ->
context
-- EXPRESSION EXIT VISITOR
expressionExitVisitor : Node Expression -> Context -> ( List (Rule.Error {}), Context )
expressionExitVisitor (Node _ node) context =
case node of
Expression.LambdaExpression _ ->
report context
_ ->
( [], context )
letDeclarationEnterVisitor : a -> Node Expression.LetDeclaration -> Context -> ( List nothing, Context )
letDeclarationEnterVisitor _ letDeclaration context =
case Node.value letDeclaration of
Expression.LetFunction function ->
let
declaration : Expression.FunctionImplementation
declaration =
Node.value function.declaration
in
if List.isEmpty declaration.arguments then
( [], context )
else
let
functionName : String
functionName =
Node.value declaration.name
declared : List (List Declared)
declared =
List.map (getParametersFromPatterns NamedFunction) declaration.arguments
newScope : Scope
newScope =
{ functionName = functionName
, declared = List.concat declared
, used = Set.empty
, usedRecursively = Set.empty
}
in
( []
, { context
| scopes = newScope :: context.scopes
, knownFunctions = Dict.insert functionName (getArgNames declared) context.knownFunctions
}
)
Expression.LetDestructuring _ _ ->
( [], context )
letDeclarationExitVisitor : a -> Node Expression.LetDeclaration -> Context -> ( List (Rule.Error {}), Context )
letDeclarationExitVisitor _ letDeclaration context =
case Node.value letDeclaration of
Expression.LetFunction function ->
let
declaration : Expression.FunctionImplementation
declaration =
Node.value function.declaration
in
if List.isEmpty declaration.arguments then
( [], context )
else
report context
Expression.LetDestructuring _ _ ->
( [], context )
@ -446,7 +462,7 @@ registerFunctionCall fnName numberOfIgnoredArguments arguments context =
| locationsToIgnoreForUsed =
Dict.merge
Dict.insert
(\key new old -> Dict.insert key (new ++ old))
(\key new old -> Dict.insert key (List.append new old))
Dict.insert
locationsToIgnore
context.locationsToIgnoreForUsed
@ -502,20 +518,6 @@ markAllAsUsed names scopes =
{ headScope | used = Set.union names headScope.used } :: restOfScopes
-- EXPRESSION EXIT VISITOR
expressionExitVisitor : Node Expression -> Context -> ( List (Rule.Error {}), Context )
expressionExitVisitor node context =
case RangeDict.get (Node.range node) context.scopesToCreate of
Just _ ->
report context
Nothing ->
( [], context )
report : Context -> ( List (Rule.Error {}), Context )
report context =
case context.scopes of
@ -523,23 +525,7 @@ report context =
let
( errors, remainingUsed ) =
List.foldl
(\declared ( errors_, remainingUsed_ ) ->
if Set.member declared.name headScope.usedRecursively then
-- If variable was used as a recursive argument
if Set.member declared.name remainingUsed_ then
-- If variable was used somewhere else as well
( errors_, Set.remove declared.name remainingUsed_ )
else
-- If variable was used ONLY as a recursive argument
( recursiveParameterError headScope.functionName declared :: errors_, Set.remove declared.name remainingUsed_ )
else if Set.member declared.name remainingUsed_ then
( errors_, Set.remove declared.name remainingUsed_ )
else
( errorsForValue declared :: errors_, remainingUsed_ )
)
(findErrorsAndVariablesNotPartOfScope headScope)
( [], headScope.used )
headScope.declared
in
@ -554,6 +540,25 @@ report context =
( [], context )
findErrorsAndVariablesNotPartOfScope : Scope -> Declared -> ( List (Rule.Error {}), Set String ) -> ( List (Rule.Error {}), Set String )
findErrorsAndVariablesNotPartOfScope scope declared ( errors_, remainingUsed_ ) =
if Set.member declared.name scope.usedRecursively then
-- If variable was used as a recursive argument
if Set.member declared.name remainingUsed_ then
-- If variable was used somewhere else as well
( errors_, Set.remove declared.name remainingUsed_ )
else
-- If variable was used ONLY as a recursive argument
( recursiveParameterError scope.functionName declared :: errors_, Set.remove declared.name remainingUsed_ )
else if Set.member declared.name remainingUsed_ then
( errors_, Set.remove declared.name remainingUsed_ )
else
( errorsForValue declared :: errors_, remainingUsed_ )
errorsForValue : Declared -> Rule.Error {}
errorsForValue { name, kind, range, source, fix } =
Rule.errorWithFix

View File

@ -2,9 +2,6 @@ module NoUnused.Patterns exposing (rule)
{-| Report useless patterns and pattern values that are not used.
# Rule
@docs rule
-}
@ -16,7 +13,6 @@ import Elm.Syntax.Pattern as Pattern exposing (Pattern)
import Elm.Syntax.Range as Range exposing (Range)
import Elm.Writer as Writer
import NoUnused.Patterns.NameVisitor as NameVisitor
import NoUnused.RangeDict as RangeDict exposing (RangeDict)
import Review.Fix as Fix exposing (Fix)
import Review.Rule as Rule exposing (Rule)
import Set exposing (Set)
@ -69,14 +65,14 @@ rule =
Rule.newModuleRuleSchema "NoUnused.Patterns" initialContext
|> Rule.withExpressionEnterVisitor expressionEnterVisitor
|> Rule.withExpressionExitVisitor expressionExitVisitor
|> Rule.withCaseBranchEnterVisitor caseBranchEnterVisitor
|> Rule.withCaseBranchExitVisitor caseBranchExitVisitor
|> NameVisitor.withValueVisitor valueVisitor
|> Rule.fromModuleRuleSchema
type alias Context =
{ scopes : List Scope
, scopesToCreate : RangeDict (List FoundPattern)
}
List Scope
type alias Scope =
@ -102,9 +98,7 @@ type FoundPattern
initialContext : Context
initialContext =
{ scopes = []
, scopesToCreate = RangeDict.empty
}
[]
@ -113,21 +107,6 @@ initialContext =
expressionEnterVisitor : Node Expression -> Context -> ( List nothing, Context )
expressionEnterVisitor node context =
let
newContext : Context
newContext =
case RangeDict.get (Node.range node) context.scopesToCreate of
Just declared ->
{ context | scopes = { declared = declared, used = Set.empty } :: context.scopes }
Nothing ->
context
in
expressionEnterVisitorHelp node newContext
expressionEnterVisitorHelp : Node Expression -> Context -> ( List nothing, Context )
expressionEnterVisitorHelp node context =
case Node.value node of
Expression.LetExpression { declarations } ->
let
@ -141,27 +120,10 @@ expressionEnterVisitorHelp node context =
findPatterns Destructuring pattern
in
( []
, { context
| scopes =
{ declared = List.concatMap findPatternsInLetDeclaration declarations
, used = Set.empty
}
:: context.scopes
-- Will only be used to remove on exit, we are already adding the declared patterns to the scope above
, scopesToCreate = RangeDict.insert (Node.range node) [] context.scopesToCreate
}
)
Expression.CaseExpression { cases } ->
( []
, { context
| scopesToCreate =
List.foldl
(\( pattern, expr ) scopesToCreate -> RangeDict.insert (Node.range expr) (findPatterns Matching pattern) scopesToCreate)
context.scopesToCreate
cases
, { declared = List.concatMap findPatternsInLetDeclaration declarations
, used = Set.empty
}
:: context
)
_ ->
@ -170,17 +132,32 @@ expressionEnterVisitorHelp node context =
expressionExitVisitor : Node Expression -> Context -> ( List (Rule.Error {}), Context )
expressionExitVisitor node context =
case RangeDict.get (Node.range node) context.scopesToCreate of
Just _ ->
case Node.value node of
Expression.LetExpression _ ->
report context
Nothing ->
_ ->
( [], context )
caseBranchEnterVisitor : a -> ( Node Pattern, Node Expression ) -> Context -> ( List nothing, Context )
caseBranchEnterVisitor _ ( pattern, _ ) context =
( []
, { declared = findPatterns Matching pattern
, used = Set.empty
}
:: context
)
caseBranchExitVisitor : a -> b -> Context -> ( List (Rule.Error {}), Context )
caseBranchExitVisitor _ _ context =
report context
report : Context -> ( List (Rule.Error {}), Context )
report context =
case context.scopes of
case context of
headScope :: restOfScopes ->
let
{ singles, records, simplifiablePatterns } =
@ -223,7 +200,7 @@ report context =
( errors
, List.foldl
useValue
{ context | scopes = restOfScopes }
restOfScopes
(Set.toList nonUsedVars)
)
@ -687,12 +664,12 @@ errorsForValue name range context =
useValue : String -> Context -> Context
useValue name context =
case context.scopes of
case context of
[] ->
context
headScope :: restOfScopes ->
{ context | scopes = { headScope | used = Set.insert name headScope.used } :: restOfScopes }
{ headScope | used = Set.insert name headScope.used } :: restOfScopes
isNodeInContext : Context -> Node String -> Bool
@ -702,7 +679,7 @@ isNodeInContext context (Node _ value) =
isUnused : String -> Context -> Bool
isUnused name context =
case context.scopes of
case context of
[] ->
False

View File

@ -1,59 +0,0 @@
module NoUnused.RangeDict exposing (RangeDict, empty, fromList, get, insert, insertAll, member, singleton)
import Dict exposing (Dict)
import Elm.Syntax.Range exposing (Range)
type alias RangeDict v =
Dict String v
empty : RangeDict v
empty =
Dict.empty
insert : Range -> v -> RangeDict v -> RangeDict v
insert range =
Dict.insert (rangeAsString range)
insertAll : List ( Range, v ) -> RangeDict v -> RangeDict v
insertAll list dict =
List.foldl
(\( range, v ) -> insert range v)
dict
list
singleton : Range -> v -> RangeDict v
singleton range value =
Dict.singleton (rangeAsString range) value
fromList : List ( Range, v ) -> RangeDict v
fromList values =
values
|> List.map (Tuple.mapFirst rangeAsString)
|> Dict.fromList
get : Range -> RangeDict v -> Maybe v
get range =
Dict.get (rangeAsString range)
member : Range -> RangeDict v -> Bool
member range =
Dict.member (rangeAsString range)
rangeAsString : Range -> String
rangeAsString range =
[ range.start.row
, range.start.column
, range.end.row
, range.end.column
]
|> List.map String.fromInt
|> String.join "_"

View File

@ -2,9 +2,6 @@ module NoUnused.Variables exposing (rule)
{-| Report variables or types that are declared or imported but never used inside of a module.
# Rule
@docs rule
-}
@ -24,7 +21,6 @@ import Elm.Syntax.Type
import Elm.Syntax.TypeAlias exposing (TypeAlias)
import Elm.Syntax.TypeAnnotation as TypeAnnotation exposing (TypeAnnotation)
import NoUnused.NonemptyList as NonemptyList exposing (Nonempty)
import NoUnused.RangeDict as RangeDict exposing (RangeDict)
import Review.Fix as Fix exposing (Fix)
import Review.ModuleNameLookupTable as ModuleNameLookupTable exposing (ModuleNameLookupTable)
import Review.Project.Dependency as Dependency exposing (Dependency)
@ -90,9 +86,13 @@ moduleVisitor schema =
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
|> Rule.withImportVisitor importVisitor
|> Rule.withDeclarationListVisitor declarationListVisitor
|> Rule.withDeclarationEnterVisitor declarationVisitor
|> Rule.withDeclarationEnterVisitor declarationEnterVisitor
|> Rule.withDeclarationExitVisitor declarationExitVisitor
|> Rule.withExpressionEnterVisitor expressionEnterVisitor
|> Rule.withExpressionExitVisitor expressionExitVisitor
|> Rule.withLetDeclarationEnterVisitor letDeclarationEnterVisitor
|> Rule.withLetDeclarationExitVisitor letDeclarationExitVisitor
|> Rule.withCaseBranchEnterVisitor caseBranchEnterVisitor
|> Rule.withCaseBranchExitVisitor caseBranchExitVisitor
|> Rule.withFinalModuleEvaluation finalEvaluation
@ -106,7 +106,6 @@ type alias ModuleContext =
{ lookupTable : ModuleNameLookupTable
, scopes : Nonempty Scope
, inTheDeclarationOf : List String
, declarations : RangeDict String
, exposesEverything : Bool
, isApplication : Bool
, constructorNameToTypeName : Dict String String
@ -115,7 +114,7 @@ type alias ModuleContext =
, usedModules : Set ( ModuleName, ModuleName )
, unusedImportedCustomTypes : Dict String ImportedCustomType
, importedCustomTypeLookup : Dict String String
, localCustomTypes : Dict String CustomTypeData
, localTypes : Dict String TypeData
, customTypes : Dict ModuleName (Dict String (List String))
}
@ -130,13 +129,19 @@ type alias DeclaredModule =
}
type alias CustomTypeData =
type alias TypeData =
{ under : Range
, kind : TypeKind
, rangeToRemove : Range
, variants : List String
}
type TypeKind
= CustomTypeKind
| TypeAliasKind
type alias ModuleThatExposesEverything =
{ name : ModuleName
, alias : Maybe String
@ -156,6 +161,7 @@ type DeclaredModuleType
type alias Scope =
{ declared : Dict String VariableInfo
, used : Dict ModuleName (Set String)
, namesToIgnore : Set String
}
@ -194,7 +200,6 @@ fromProjectToModule =
{ lookupTable = lookupTable
, scopes = NonemptyList.fromElement emptyScope
, inTheDeclarationOf = []
, declarations = Dict.empty
, exposesEverything = False
, isApplication = isApplication
, constructorNameToTypeName = Dict.empty
@ -203,7 +208,7 @@ fromProjectToModule =
, usedModules = Set.empty
, unusedImportedCustomTypes = Dict.empty
, importedCustomTypeLookup = Dict.empty
, localCustomTypes = Dict.empty
, localTypes = Dict.empty
, customTypes = customTypes
}
)
@ -215,7 +220,7 @@ fromModuleToProject =
Rule.initContextCreator
(\metadata moduleContext ->
{ customTypes =
moduleContext.localCustomTypes
moduleContext.localTypes
|> Dict.map (\_ customType -> customType.variants)
|> Dict.singleton (Rule.moduleNameFromMetadata metadata)
@ -237,6 +242,7 @@ emptyScope : Scope
emptyScope =
{ declared = Dict.empty
, used = Dict.empty
, namesToIgnore = Set.empty
}
@ -574,22 +580,7 @@ moduleAliasRange (Node _ { moduleName }) range =
expressionEnterVisitor : Node Expression -> ModuleContext -> ( List (Error {}), ModuleContext )
expressionEnterVisitor node context =
let
newContext : ModuleContext
newContext =
case RangeDict.get (Node.range node) context.declarations of
Just functionName ->
{ context | inTheDeclarationOf = functionName :: context.inTheDeclarationOf }
Nothing ->
context
in
expressionEnterVisitorHelp node newContext
expressionEnterVisitorHelp : Node Expression -> ModuleContext -> ( List (Error {}), ModuleContext )
expressionEnterVisitorHelp (Node range value) context =
expressionEnterVisitor (Node range value) context =
case value of
Expression.FunctionOrValue [] name ->
case Dict.get name context.constructorNameToTypeName of
@ -627,98 +618,8 @@ expressionEnterVisitorHelp (Node range value) context =
Expression.PrefixOperator name ->
( [], markValueAsUsed name context )
Expression.LetExpression { declarations, expression } ->
let
letBlockContext : LetBlockContext
letBlockContext =
if List.length declarations == 1 then
HasNoOtherDeclarations <| rangeUpUntil range (Node.range expression |> .start)
else
HasMultipleDeclarations
in
List.foldl
(\declaration ( errors, foldContext ) ->
case Node.value declaration of
Expression.LetFunction function ->
let
namesUsedInArgumentPatterns : { types : List String, modules : List ( ModuleName, ModuleName ) }
namesUsedInArgumentPatterns =
function.declaration
|> Node.value
|> .arguments
|> List.map (getUsedVariablesFromPattern context)
|> foldUsedTypesAndModules
markAsInTheDeclarationOf : a -> { b | declarations : RangeDict a } -> { b | declarations : RangeDict a }
markAsInTheDeclarationOf name ctx =
{ ctx
| declarations =
RangeDict.insert
(function.declaration |> Node.value |> .expression |> Node.range)
name
ctx.declarations
}
in
( errors
, List.foldl markValueAsUsed foldContext namesUsedInArgumentPatterns.types
|> markAllModulesAsUsed namesUsedInArgumentPatterns.modules
|> registerFunction letBlockContext function
|> markAsInTheDeclarationOf (function.declaration |> Node.value |> .name |> Node.value)
)
Expression.LetDestructuring pattern _ ->
case removeParens pattern of
Node wildCardRange Pattern.AllPattern ->
( Rule.errorWithFix
{ message = "Value assigned to `_` is unused"
, details =
[ "This value has been assigned to a wildcard, which makes the value unusable. You should remove it at the location I pointed at."
]
}
wildCardRange
[ Fix.removeRange (letDeclarationToRemoveRange letBlockContext (Node.range declaration)) ]
:: errors
, foldContext
)
Node unitPattern Pattern.UnitPattern ->
( Rule.errorWithFix
{ message = "Unit value is unused"
, details =
[ "This value has no data, which makes the value unusable. You should remove it at the location I pointed at."
]
}
unitPattern
[ Fix.removeRange (letDeclarationToRemoveRange letBlockContext (Node.range declaration)) ]
:: errors
, foldContext
)
_ ->
let
namesUsedInPattern : { types : List String, modules : List ( ModuleName, ModuleName ) }
namesUsedInPattern =
getUsedVariablesFromPattern context pattern
in
( if not (introducesVariable pattern) then
Rule.errorWithFix
{ message = "Pattern doesn't introduce any variables"
, details =
[ "This value has been computed but isn't assigned to any variable, which makes the value unusable. You should remove it at the location I pointed at." ]
}
(Node.range pattern)
[ Fix.removeRange (letDeclarationToRemoveRange letBlockContext (Node.range declaration)) ]
:: errors
else
errors
, List.foldl markValueAsUsed foldContext namesUsedInPattern.types
|> markAllModulesAsUsed namesUsedInPattern.modules
)
)
( [], { context | scopes = NonemptyList.cons emptyScope context.scopes } )
declarations
Expression.RecordUpdateExpression expr _ ->
( [], markValueAsUsed (Node.value expr) context )
Expression.LambdaExpression { args } ->
let
@ -731,12 +632,155 @@ expressionEnterVisitorHelp (Node range value) context =
( []
, List.foldl markValueAsUsed context namesUsedInArgumentPatterns.types
|> markAllModulesAsUsed namesUsedInArgumentPatterns.modules
|> registerParameters args
)
Expression.CaseExpression { cases } ->
let
usedVariables : { types : List String, modules : List ( ModuleName, ModuleName ) }
usedVariables =
cases
|> List.map
(\( patternNode, _ ) ->
getUsedVariablesFromPattern context patternNode
)
|> foldUsedTypesAndModules
in
( []
, List.foldl
markValueAsUsed
context
usedVariables.types
|> markAllModulesAsUsed usedVariables.modules
)
_ ->
( [], context )
letDeclarationEnterVisitor : Node Expression.LetBlock -> Node Expression.LetDeclaration -> ModuleContext -> ( List (Error {}), ModuleContext )
letDeclarationEnterVisitor (Node range { declarations, expression }) declaration context =
let
letBlockContext : LetBlockContext
letBlockContext =
if List.length declarations == 1 then
HasNoOtherDeclarations <| rangeUpUntil range (Node.range expression |> .start)
else
HasMultipleDeclarations
in
case Node.value declaration of
Expression.LetFunction function ->
let
functionDeclaration : FunctionImplementation
functionDeclaration =
Node.value function.declaration
namesUsedInArgumentPatterns : { types : List String, modules : List ( ModuleName, ModuleName ) }
namesUsedInArgumentPatterns =
functionDeclaration.arguments
|> List.map (getUsedVariablesFromPattern context)
|> foldUsedTypesAndModules
namesToIgnore : Set String
namesToIgnore =
List.concatMap getDeclaredParametersFromPattern functionDeclaration.arguments
|> Set.fromList
newContext : ModuleContext
newContext =
{ context | inTheDeclarationOf = Node.value functionDeclaration.name :: context.inTheDeclarationOf }
|> markAllAsUsed namesUsedInArgumentPatterns.types
|> markAllModulesAsUsed namesUsedInArgumentPatterns.modules
|> registerFunction letBlockContext function
in
( []
, { newContext
| scopes = NonemptyList.cons { declared = Dict.empty, used = Dict.empty, namesToIgnore = namesToIgnore } newContext.scopes
}
)
Expression.LetDestructuring pattern _ ->
case removeParens pattern of
Node wildCardRange Pattern.AllPattern ->
( [ Rule.errorWithFix
{ message = "Value assigned to `_` is unused"
, details =
[ "This value has been assigned to a wildcard, which makes the value unusable. You should remove it at the location I pointed at."
]
}
wildCardRange
[ Fix.removeRange (letDeclarationToRemoveRange letBlockContext (Node.range declaration)) ]
]
, context
)
Node unitPattern Pattern.UnitPattern ->
( [ Rule.errorWithFix
{ message = "Unit value is unused"
, details =
[ "This value has no data, which makes the value unusable. You should remove it at the location I pointed at."
]
}
unitPattern
[ Fix.removeRange (letDeclarationToRemoveRange letBlockContext (Node.range declaration)) ]
]
, context
)
_ ->
let
namesUsedInPattern : { types : List String, modules : List ( ModuleName, ModuleName ) }
namesUsedInPattern =
getUsedVariablesFromPattern context pattern
in
( if not (introducesVariable pattern) then
[ Rule.errorWithFix
{ message = "Pattern doesn't introduce any variables"
, details =
[ "This value has been computed but isn't assigned to any variable, which makes the value unusable. You should remove it at the location I pointed at." ]
}
(Node.range pattern)
[ Fix.removeRange (letDeclarationToRemoveRange letBlockContext (Node.range declaration)) ]
]
else
[]
, List.foldl markValueAsUsed context namesUsedInPattern.types
|> markAllModulesAsUsed namesUsedInPattern.modules
)
letDeclarationExitVisitor : a -> Node Expression.LetDeclaration -> ModuleContext -> ( List (Error {}), ModuleContext )
letDeclarationExitVisitor _ declaration context =
case Node.value declaration of
Expression.LetFunction _ ->
makeReport { context | inTheDeclarationOf = List.drop 1 context.inTheDeclarationOf }
Expression.LetDestructuring _ _ ->
( [], context )
caseBranchEnterVisitor : a -> ( Node Pattern, b ) -> ModuleContext -> ( List nothing, ModuleContext )
caseBranchEnterVisitor _ ( pattern, _ ) context =
( []
, { context
| scopes =
NonemptyList.cons
{ declared = Dict.empty
, used = Dict.empty
, namesToIgnore = Set.fromList (getDeclaredParametersFromPattern pattern)
}
context.scopes
}
)
caseBranchExitVisitor : a -> ( Node Pattern, b ) -> ModuleContext -> ( List (Rule.Error {}), ModuleContext )
caseBranchExitVisitor _ _ context =
makeReport context
letDeclarationToRemoveRange : LetBlockContext -> Range -> Range
letDeclarationToRemoveRange letBlockContext range =
case letBlockContext of
@ -759,59 +803,6 @@ removeParens node =
node
expressionExitVisitor : Node Expression -> ModuleContext -> ( List (Error {}), ModuleContext )
expressionExitVisitor node context =
let
newContext : ModuleContext
newContext =
if RangeDict.member (Node.range node) context.declarations then
{ context | inTheDeclarationOf = List.drop 1 context.inTheDeclarationOf }
else
context
in
expressionExitVisitorHelp node newContext
expressionExitVisitorHelp : Node Expression -> ModuleContext -> ( List (Error {}), ModuleContext )
expressionExitVisitorHelp node context =
case Node.value node of
Expression.RecordUpdateExpression expr _ ->
( [], markValueAsUsed (Node.value expr) context )
Expression.CaseExpression { cases } ->
let
usedVariables : { types : List String, modules : List ( ModuleName, ModuleName ) }
usedVariables =
cases
|> List.map
(\( patternNode, _ ) ->
getUsedVariablesFromPattern context patternNode
)
|> foldUsedTypesAndModules
in
( []
, List.foldl markValueAsUsed context usedVariables.types
|> markAllModulesAsUsed usedVariables.modules
)
Expression.LetExpression _ ->
let
( errors, remainingUsed ) =
makeReport (NonemptyList.head context.scopes)
contextWithPoppedScope : ModuleContext
contextWithPoppedScope =
{ context | scopes = NonemptyList.pop context.scopes }
in
( errors
, markAllAsUsed remainingUsed contextWithPoppedScope
)
_ ->
( [], context )
getUsedVariablesFromPattern : ModuleContext -> Node Pattern -> { types : List String, modules : List ( ModuleName, ModuleName ) }
getUsedVariablesFromPattern context patternNode =
{ types = getUsedTypesFromPattern context.constructorNameToTypeName patternNode
@ -819,6 +810,49 @@ getUsedVariablesFromPattern context patternNode =
}
getDeclaredParametersFromPattern : Node Pattern -> List String
getDeclaredParametersFromPattern node =
getDeclaredParametersFromPatternHelp [ node ] []
getDeclaredParametersFromPatternHelp : List (Node Pattern) -> List String -> List String
getDeclaredParametersFromPatternHelp nodes acc =
case nodes of
(Node _ node) :: tail ->
case node of
Pattern.ParenthesizedPattern pattern ->
getDeclaredParametersFromPatternHelp (pattern :: tail) acc
Pattern.VarPattern name ->
getDeclaredParametersFromPatternHelp tail (name :: acc)
Pattern.AsPattern pattern (Node _ asName) ->
getDeclaredParametersFromPatternHelp (pattern :: tail) (asName :: acc)
Pattern.RecordPattern fields ->
getDeclaredParametersFromPatternHelp
tail
(List.append (List.map Node.value fields) acc)
Pattern.TuplePattern patterns ->
getDeclaredParametersFromPatternHelp (patterns ++ tail) acc
Pattern.NamedPattern _ patterns ->
getDeclaredParametersFromPatternHelp (patterns ++ tail) acc
Pattern.UnConsPattern left right ->
getDeclaredParametersFromPatternHelp (left :: right :: tail) acc
Pattern.ListPattern patterns ->
getDeclaredParametersFromPatternHelp (patterns ++ tail) acc
_ ->
getDeclaredParametersFromPatternHelp tail acc
[] ->
acc
getUsedTypesFromPattern : Dict String String -> Node Pattern -> List String
getUsedTypesFromPattern constructorNameToTypeName patternNode =
case Node.value patternNode of
@ -983,51 +1017,6 @@ registerTypes node context =
context
registerTypeAlias : Range -> TypeAlias -> ModuleContext -> ModuleContext
registerTypeAlias range { name, typeAnnotation } context =
let
contextWithRemovedShadowedImports : ModuleContext
contextWithRemovedShadowedImports =
case Node.value typeAnnotation of
TypeAnnotation.Record _ ->
{ context | importedCustomTypeLookup = Dict.remove (Node.value name) context.importedCustomTypeLookup }
_ ->
context
-- TODO Rename
typeAlias : CustomTypeData
typeAlias =
{ under = Node.range name
, rangeToRemove = untilStartOfNextLine range
, variants = []
}
in
case Node.value typeAnnotation of
TypeAnnotation.Record _ ->
if context.exposesEverything then
contextWithRemovedShadowedImports
else
registerVariable
{ typeName = "Type"
, under = Node.range name
, rangeToRemove = Just (untilStartOfNextLine range)
, warning = ""
}
(Node.value name)
contextWithRemovedShadowedImports
_ ->
{ contextWithRemovedShadowedImports
| localCustomTypes =
Dict.insert
(Node.value name)
typeAlias
contextWithRemovedShadowedImports.localCustomTypes
}
registerCustomType : Range -> Elm.Syntax.Type.Type -> ModuleContext -> ModuleContext
registerCustomType range { name, constructors } context =
let
@ -1045,29 +1034,83 @@ registerCustomType range { name, constructors } context =
|> List.map (\constructorName -> ( constructorName, typeName ))
|> Dict.fromList
customType : CustomTypeData
customType : TypeData
customType =
{ under = Node.range name
{ kind = CustomTypeKind
, under = Node.range name
, rangeToRemove = untilStartOfNextLine range
, variants = constructorNames
}
in
{ context
| localCustomTypes =
| localTypes =
Dict.insert
(Node.value name)
customType
context.localCustomTypes
context.localTypes
, constructorNameToTypeName = Dict.union constructorsForType context.constructorNameToTypeName
}
registerTypeAlias : Range -> TypeAlias -> ModuleContext -> ModuleContext
registerTypeAlias range { name, typeAnnotation } context =
let
newContext : ModuleContext
newContext =
case Node.value typeAnnotation of
TypeAnnotation.Record _ ->
{ context | importedCustomTypeLookup = Dict.remove (Node.value name) context.importedCustomTypeLookup }
-- DECLARATION VISITOR
_ ->
context
in
case Node.value typeAnnotation of
TypeAnnotation.Record _ ->
if context.exposesEverything then
newContext
else
registerVariable
{ typeName = "Type"
, under = Node.range name
, rangeToRemove = Just (untilStartOfNextLine range)
, warning = ""
}
(Node.value name)
newContext
_ ->
let
typeAlias : TypeData
typeAlias =
{ kind = TypeAliasKind
, under = Node.range name
, rangeToRemove = untilStartOfNextLine range
, variants =
case Node.value typeAnnotation of
TypeAnnotation.Record _ ->
[ Node.value name ]
_ ->
[]
}
localCustomTypes : Dict String TypeData
localCustomTypes =
Dict.insert
(Node.value name)
typeAlias
newContext.localTypes
in
{ newContext | localTypes = localCustomTypes }
declarationVisitor : Node Declaration -> ModuleContext -> ( List (Error {}), ModuleContext )
declarationVisitor node context =
-- DECLARATION ENTER VISITOR
declarationEnterVisitor : Node Declaration -> ModuleContext -> ( List (Error {}), ModuleContext )
declarationEnterVisitor node context =
case Node.value node of
Declaration.FunctionDeclaration function ->
let
@ -1081,9 +1124,12 @@ declarationVisitor node context =
namesUsedInSignature : { types : List String, modules : List ( ModuleName, ModuleName ) }
namesUsedInSignature =
function.signature
|> Maybe.map (Node.value >> .typeAnnotation >> collectNamesFromTypeAnnotation context.lookupTable)
|> Maybe.withDefault { types = [], modules = [] }
case function.signature of
Just signature ->
signature |> Node.value |> .typeAnnotation |> collectNamesFromTypeAnnotation context.lookupTable
Nothing ->
{ types = [], modules = [] }
namesUsedInArgumentPatterns : { types : List String, modules : List ( ModuleName, ModuleName ) }
namesUsedInArgumentPatterns =
@ -1114,7 +1160,11 @@ declarationVisitor node context =
newContext : ModuleContext
newContext =
{ newContextWhereFunctionIsRegistered | inTheDeclarationOf = [ functionName ], declarations = Dict.empty }
{ newContextWhereFunctionIsRegistered
| inTheDeclarationOf = [ functionName ]
, scopes = NonemptyList.cons emptyScope newContextWhereFunctionIsRegistered.scopes
}
|> registerParameters functionImplementation.arguments
|> (\ctx -> List.foldl markValueAsUsed ctx namesUsedInArgumentPatterns.types)
|> markAllAsUsed namesUsedInSignature.types
|> markAllModulesAsUsed namesUsedInSignature.modules
@ -1150,7 +1200,7 @@ declarationVisitor node context =
|> markAllModulesAsUsed modules
)
Declaration.AliasDeclaration { name, typeAnnotation } ->
Declaration.AliasDeclaration { typeAnnotation } ->
let
namesUsedInTypeAnnotation : { types : List String, modules : List ( ModuleName, ModuleName ) }
namesUsedInTypeAnnotation =
@ -1209,6 +1259,24 @@ foldUsedTypesAndModules =
List.foldl (\a b -> { types = a.types ++ b.types, modules = a.modules ++ b.modules }) { types = [], modules = [] }
-- DECLARATION EXIT VISITOR
declarationExitVisitor : Node Declaration -> ModuleContext -> ( List (Error {}), ModuleContext )
declarationExitVisitor node context =
case Node.value node of
Declaration.FunctionDeclaration _ ->
makeReport context
_ ->
( [], context )
-- FINAL EVALUATION
finalEvaluation : ModuleContext -> List (Error {})
finalEvaluation context =
let
@ -1253,7 +1321,7 @@ finalEvaluation context =
|> Dict.toList
|> List.map
(\( name, { under, rangeToRemove, openRange } ) ->
if Set.member name usedLocally && not (Dict.member name context.localCustomTypes) then
if Set.member name usedLocally && not (Dict.member name context.localTypes) then
Rule.errorWithFix
{ message = "Imported constructors for `" ++ name ++ "` are not used"
, details = details
@ -1364,22 +1432,13 @@ finalEvaluation context =
[]
else
context.localCustomTypes
context.localTypes
|> Dict.toList
|> List.filter (\( name, _ ) -> not <| Set.member name usedLocally)
|> List.map
(\( name, customType ) ->
Rule.errorWithFix
{ message = "Type `" ++ name ++ "` is not used"
, details = details
}
customType.under
[ Fix.removeRange customType.rangeToRemove ]
)
|> List.map errorForLocalType
in
List.concat
[ newRootScope
|> makeReport
[ makeReportHelp newRootScope
|> Tuple.first
, importedTypeErrors
, moduleErrors
@ -1388,6 +1447,26 @@ finalEvaluation context =
]
errorForLocalType : ( String, TypeData ) -> Error {}
errorForLocalType ( name, type_ ) =
let
kind : String
kind =
case type_.kind of
CustomTypeKind ->
"Type"
TypeAliasKind ->
"Type alias"
in
Rule.errorWithFix
{ message = kind ++ " `" ++ name ++ "` is not used"
, details = details
}
type_.under
[ Fix.removeRange type_.rangeToRemove ]
registerFunction : LetBlockContext -> Function -> ModuleContext -> ModuleContext
registerFunction letBlockContext function context =
let
@ -1415,6 +1494,27 @@ registerFunction letBlockContext function context =
(Node.value declaration.name)
registerParameters : List (Node Pattern) -> ModuleContext -> ModuleContext
registerParameters patterns context =
{ context
| scopes =
NonemptyList.mapHead
(setNamesToIgnoreFromPattern patterns)
context.scopes
}
setNamesToIgnoreFromPattern : List (Node Pattern) -> { a | namesToIgnore : Set String } -> { a | namesToIgnore : Set String }
setNamesToIgnoreFromPattern patterns scope =
let
namesToIgnore : Set String
namesToIgnore =
List.concatMap getDeclaredParametersFromPattern patterns
|> Set.fromList
in
{ scope | namesToIgnore = namesToIgnore }
type ExposedElement
= CustomType String ImportedCustomType
| TypeOrValue String VariableInfo
@ -1611,8 +1711,23 @@ getModuleName name =
String.join "." name
makeReport : Scope -> ( List (Error {}), List String )
makeReport { declared, used } =
makeReport : ModuleContext -> ( List (Error {}), ModuleContext )
makeReport context =
let
( errors, remainingUsed ) =
makeReportHelp (NonemptyList.head context.scopes)
contextWithPoppedScope : ModuleContext
contextWithPoppedScope =
{ context | scopes = NonemptyList.pop context.scopes }
in
( errors
, markAllAsUsed remainingUsed contextWithPoppedScope
)
makeReportHelp : Scope -> ( List (Error {}), List String )
makeReportHelp { declared, used, namesToIgnore } =
let
usedLocally : Set String
usedLocally =
@ -1623,11 +1738,12 @@ makeReport { declared, used } =
Dict.keys declared
|> Set.fromList
|> Set.diff usedLocally
|> (\set -> Set.diff set namesToIgnore)
|> Set.toList
errors : List (Error {})
errors =
Dict.filter (\key _ -> not <| Set.member key usedLocally) declared
Dict.filter (\key _ -> not (Set.member key usedLocally)) declared
|> Dict.toList
|> List.map (\( key, variableInfo ) -> error variableInfo key)
in

View File

@ -1006,7 +1006,7 @@ type C = C
|> Review.Test.expectErrorsForModules
[ ( "A"
, [ Review.Test.error
{ message = "Type `C` is not used"
{ message = "Type alias `C` is not used"
, details = details
, under = "C"
}
@ -1042,6 +1042,26 @@ a = C""" |> String.replace "$" " ")
]
)
]
, test "should report type alias that doesn't alias a record when it has the same name as a constructor defined in the same file" <|
\() ->
"""module A exposing (a)
type A = B | C
type alias B = Int
a = B
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Type alias `B` is not used"
, details = details
, under = "B"
}
|> Review.Test.atExactly { start = { row = 3, column = 12 }, end = { row = 3, column = 13 } }
|> Review.Test.whenFixed """module A exposing (a)
type A = B | C
a = B
"""
]
, test "should not report open type import when a constructor is used but the type is locally shadowed" <|
\() ->
[ """module A exposing (a)
@ -1302,7 +1322,7 @@ c = 1"""
]
|> Review.Test.runOnModules rule
|> Review.Test.expectNoErrors
, test "should report unused import even if a let in variable is named the same way" <|
, test "should report unused import even if a used let..in variable is named the same way" <|
\() ->
"""module SomeModule exposing (a)
import Html exposing (button, div)
@ -1321,6 +1341,126 @@ import Html exposing (div)
a = let button = 1
in button + div"""
]
, test "should report unused import even if a used function param is named in the same way" <|
\() ->
[ """module A exposing (identity)
import Used exposing (shadowed)
identity shadowed = shadowed
"""
, """module Used exposing (shadowed)
shadowed = ""
"""
]
|> Review.Test.runOnModules rule
|> Review.Test.expectErrorsForModules
[ ( "A"
, [ Review.Test.error
{ message = "Imported variable `shadowed` is not used"
, details = details
, under = "shadowed"
}
|> Review.Test.atExactly { start = { row = 2, column = 23 }, end = { row = 2, column = 31 } }
|> Review.Test.whenFixed ("""module A exposing (identity)
import Used$
identity shadowed = shadowed
""" |> String.replace "$" " ")
]
)
]
, test "should report unused import even if a used let..in function param is named in the same way" <|
\() ->
[ """module A exposing (identity)
import Used exposing (shadowed)
identity x =
let
identityHelp shadowed = shadowed
in
identityHelp x
"""
, """module Used exposing (shadowed)
shadowed = ""
"""
]
|> Review.Test.runOnModules rule
|> Review.Test.expectErrorsForModules
[ ( "A"
, [ Review.Test.error
{ message = "Imported variable `shadowed` is not used"
, details = details
, under = "shadowed"
}
|> Review.Test.atExactly { start = { row = 2, column = 23 }, end = { row = 2, column = 31 } }
|> Review.Test.whenFixed ("""module A exposing (identity)
import Used$
identity x =
let
identityHelp shadowed = shadowed
in
identityHelp x
""" |> String.replace "$" " ")
]
)
]
, test "should report unused import even if a used lambda param is named in the same way" <|
\() ->
[ """module A exposing (identity)
import Used exposing (shadowed)
identity x =
(\\shadowed -> shadowed) x
"""
, """module Used exposing (shadowed)
shadowed = ""
"""
]
|> Review.Test.runOnModules rule
|> Review.Test.expectErrorsForModules
[ ( "A"
, [ Review.Test.error
{ message = "Imported variable `shadowed` is not used"
, details = details
, under = "shadowed"
}
|> Review.Test.atExactly { start = { row = 2, column = 23 }, end = { row = 2, column = 31 } }
|> Review.Test.whenFixed ("""module A exposing (identity)
import Used$
identity x =
(\\shadowed -> shadowed) x
""" |> String.replace "$" " ")
]
)
]
, test "should report unused import even if a variant arg is named in the same way" <|
\() ->
[ """module A exposing (identity)
import Used exposing (shadowed)
identity x =
case Just x of
Nothing -> x
Just shadowed -> shadowed
"""
, """module Used exposing (shadowed)
shadowed = ""
"""
]
|> Review.Test.runOnModules rule
|> Review.Test.expectErrorsForModules
[ ( "A"
, [ Review.Test.error
{ message = "Imported variable `shadowed` is not used"
, details = details
, under = "shadowed"
}
|> Review.Test.atExactly { start = { row = 2, column = 23 }, end = { row = 2, column = 31 } }
|> Review.Test.whenFixed ("""module A exposing (identity)
import Used$
identity x =
case Just x of
Nothing -> x
Just shadowed -> shadowed
""" |> String.replace "$" " ")
]
)
]
, test "should report unused import alias when two modules share the same alias" <|
\() ->
[ """module A exposing (a)
@ -2082,7 +2222,7 @@ a = 1
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Type `UnusedType` is not used"
{ message = "Type alias `UnusedType` is not used"
, details = details
, under = "UnusedType"
}