elm-review/tests/NoUnused/Variables.elm
2022-08-22 21:49:08 +02:00

1768 lines
62 KiB
Elm

module NoUnused.Variables exposing (rule)
{-| Report variables or types that are declared or imported but never used inside of a module.
@docs rule
-}
import Dict exposing (Dict)
import Elm.Project
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Exposing as Exposing
import Elm.Syntax.Expression as Expression exposing (Expression, Function, FunctionImplementation)
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.ModuleName exposing (ModuleName)
import Elm.Syntax.Node as Node exposing (Node(..))
import Elm.Syntax.Pattern as Pattern exposing (Pattern)
import Elm.Syntax.Range exposing (Range)
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 Review.Fix as Fix exposing (Fix)
import Review.ModuleNameLookupTable as ModuleNameLookupTable exposing (ModuleNameLookupTable)
import Review.Project.Dependency as Dependency exposing (Dependency)
import Review.Rule as Rule exposing (Error, Rule)
import Set exposing (Set)
{-| Report variables or types that are declared or imported but never used.
🔧 Running with `--fix` will automatically remove all the reported errors.
config =
[ NoUnused.Variables.rule
]
## Fail
module A exposing (a, b)
import UnusedImport
a n =
n + 1
b =
let
unused =
some thing
_ =
someOther thing
in
2
c =
a 2
## Success
module A exposing (a, b)
a n =
n + 1
b =
2
## Exception
To avoid resorting to weird workarounds that are sometimes used in internal interactive examples, the rule won't report
values assigned to `_` if a direct call to `Debug.log` is assigned to it.
a value =
let
_ =
Debug.log "value" value
in
value + 1
If you enable the [`NoDebug.Log`](https://package.elm-lang.org/packages/jfmengels/elm-review-debug/latest/NoDebug-Log) rule
from the [`jfmengels/elm-review-debug`](https://package.elm-lang.org/packages/jfmengels/elm-review-debug/latest/) package,
and configure it to ignore the locations where it's acceptable, then the combination of both rules will make sure to
clean up code like the above in all the other locations.
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/elm-review-unused/example --rules NoUnused.Variables
```
-}
rule : Rule
rule =
Rule.newProjectRuleSchema "NoUnused.Variables" initialContext
|> Rule.withElmJsonProjectVisitor (\project context -> ( [], elmJsonVisitor project context ))
|> Rule.withDirectDependenciesProjectVisitor dependenciesVisitor
|> Rule.withModuleVisitor moduleVisitor
|> Rule.withModuleContextUsingContextCreator
{ fromProjectToModule = fromProjectToModule
, fromModuleToProject = fromModuleToProject
, foldProjectContexts = foldProjectContexts
}
|> Rule.withContextFromImportedModules
|> Rule.fromProjectRuleSchema
moduleVisitor : Rule.ModuleRuleSchema schemaState ModuleContext -> Rule.ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } ModuleContext
moduleVisitor schema =
schema
|> Rule.withModuleDefinitionVisitor (\module_ context -> ( [], moduleDefinitionVisitor module_ context ))
|> Rule.withImportVisitor importVisitor
|> Rule.withDeclarationListVisitor (\nodes context -> ( [], declarationListVisitor nodes context ))
|> Rule.withDeclarationEnterVisitor declarationEnterVisitor
|> Rule.withDeclarationExitVisitor declarationExitVisitor
|> Rule.withExpressionEnterVisitor (\node context -> ( [], expressionEnterVisitor node context ))
|> Rule.withExpressionExitVisitor expressionExitVisitor
|> Rule.withLetDeclarationEnterVisitor letDeclarationEnterVisitor
|> Rule.withLetDeclarationExitVisitor letDeclarationExitVisitor
|> Rule.withCaseBranchEnterVisitor (\_ casePattern context -> ( [], caseBranchEnterVisitor casePattern context ))
|> Rule.withCaseBranchExitVisitor caseBranchExitVisitor
|> Rule.withFinalModuleEvaluation finalEvaluation
type alias ProjectContext =
{ isApplication : Bool
, customTypes : Dict ModuleName (Dict String (List String))
}
type alias ModuleContext =
{ lookupTable : ModuleNameLookupTable
, scopes : Nonempty Scope
, inTheDeclarationOf : List String
, exposesEverything : Bool
, isApplication : Bool
, constructorNameToTypeName : Dict String String
, declaredModules : List DeclaredModule
, exposingAllModules : List ModuleThatExposesEverything
, usedModules : Set ( ModuleName, ModuleName )
, unusedImportedCustomTypes : Dict String ImportedCustomType
, importedCustomTypeLookup : Dict String String
, localTypes : Dict String TypeData
, customTypes : Dict ModuleName (Dict String (List String))
}
type alias DeclaredModule =
{ moduleName : ModuleName
, alias : Maybe String
, typeName : String
, variableType : DeclaredModuleType
, under : Range
, rangeToRemove : Range
}
type alias TypeData =
{ under : Range
, kind : TypeKind
, rangeToRemove : Range
, variants : List String
}
type TypeKind
= CustomTypeKind
| TypeAliasKind
type alias ModuleThatExposesEverything =
{ name : ModuleName
, alias : Maybe String
, moduleNameRange : Range
, exposingRange : Range
, importRange : Range
, wasUsedImplicitly : Bool
, wasUsedWithModuleName : Bool
}
type DeclaredModuleType
= ImportedModule
| ModuleAlias { originalNameOfTheImport : String, exposesSomething : Bool }
type alias Scope =
{ declared : Dict String VariableInfo
, used : Dict ModuleName (Set String)
, namesToIgnore : Set String
}
type alias VariableInfo =
{ typeName : String
, under : Range
, rangeToRemove : Maybe Range
, warning : String
}
type alias ImportedCustomType =
{ typeName : String
, under : Range
, rangeToRemove : Range
, openRange : Range
}
type LetBlockContext
= HasMultipleDeclarations
| HasNoOtherDeclarations Range
initialContext : ProjectContext
initialContext =
{ isApplication = True
, customTypes = Dict.empty
}
fromProjectToModule : Rule.ContextCreator ProjectContext ModuleContext
fromProjectToModule =
Rule.initContextCreator
(\lookupTable { isApplication, customTypes } ->
{ lookupTable = lookupTable
, scopes = NonemptyList.fromElement emptyScope
, inTheDeclarationOf = []
, exposesEverything = False
, isApplication = isApplication
, constructorNameToTypeName = Dict.empty
, declaredModules = []
, exposingAllModules = []
, usedModules = Set.empty
, unusedImportedCustomTypes = Dict.empty
, importedCustomTypeLookup = Dict.empty
, localTypes = Dict.empty
, customTypes = customTypes
}
)
|> Rule.withModuleNameLookupTable
fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext
fromModuleToProject =
Rule.initContextCreator
(\moduleName moduleContext ->
{ customTypes =
moduleContext.localTypes
|> Dict.map (\_ customType -> customType.variants)
|> Dict.singleton moduleName
-- Will be ignored in foldProjectContexts
, isApplication = True
}
)
|> Rule.withModuleName
foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext
foldProjectContexts newProjectContext previousProjectContext =
{ isApplication = previousProjectContext.isApplication
, customTypes = Dict.union newProjectContext.customTypes previousProjectContext.customTypes
}
emptyScope : Scope
emptyScope =
{ declared = Dict.empty
, used = Dict.empty
, namesToIgnore = Set.empty
}
error : { typeName : String, under : Range, rangeToRemove : Maybe Range, warning : String } -> String -> Error {}
error variableInfo name =
Rule.errorWithFix
{ message = variableInfo.typeName ++ " `" ++ name ++ "` is not used" ++ variableInfo.warning
, details = details
}
variableInfo.under
(case variableInfo.rangeToRemove of
Just rangeToRemove ->
[ Fix.removeRange rangeToRemove ]
Nothing ->
[]
)
details : List String
details =
[ "You should either use this value somewhere, or remove it at the location I pointed at."
]
-- ELM.JSON VISITOR
elmJsonVisitor : Maybe { a | project : Elm.Project.Project } -> ProjectContext -> ProjectContext
elmJsonVisitor maybeElmJson projectContext =
case Maybe.map .project maybeElmJson of
Just (Elm.Project.Application _) ->
{ projectContext | isApplication = True }
Just (Elm.Project.Package _) ->
{ projectContext | isApplication = False }
Nothing ->
-- Sensible default, because now `main` won't be reported.
{ projectContext | isApplication = True }
-- DEPENDENCIES VISITOR
dependenciesVisitor : Dict String Dependency -> ProjectContext -> ( List (Error nothing), ProjectContext )
dependenciesVisitor dependencies projectContext =
let
customTypes : Dict ModuleName (Dict String (List String))
customTypes =
dependencies
|> Dict.values
|> List.concatMap Dependency.modules
|> List.map
(\module_ ->
( String.split "." module_.name
, module_.unions
|> List.map (\{ name, tags } -> ( name, List.map Tuple.first tags ))
|> Dict.fromList
)
)
|> Dict.fromList
in
( [], { projectContext | customTypes = customTypes } )
-- MODULE DEFINITION VISITOR
moduleDefinitionVisitor : Node Module -> ModuleContext -> ModuleContext
moduleDefinitionVisitor (Node _ moduleNode) context =
case Module.exposingList moduleNode of
Exposing.All _ ->
{ context | exposesEverything = True }
Exposing.Explicit list ->
let
names : List String
names =
List.map getExposingName list
in
markAllAsUsed names context
getExposingName : Node Exposing.TopLevelExpose -> String
getExposingName node =
case Node.value node of
Exposing.FunctionExpose name ->
name
Exposing.TypeOrAliasExpose name ->
name
Exposing.TypeExpose { name } ->
name
Exposing.InfixExpose name ->
name
importVisitor : Node Import -> ModuleContext -> ( List (Error {}), ModuleContext )
importVisitor ((Node importRange import_) as node) context =
let
errors : List (Error {})
errors =
case import_.moduleAlias of
Just moduleAlias ->
if Node.value moduleAlias == Node.value import_.moduleName then
[ Rule.errorWithFix
{ message = "Module `" ++ String.join "." (Node.value moduleAlias) ++ "` is aliased as itself"
, details = [ "The alias is the same as the module name, and brings no useful value" ]
}
(Node.range moduleAlias)
[ Fix.removeRange <| moduleAliasRange node (Node.range moduleAlias) ]
]
else
[]
Nothing ->
[]
in
case import_.exposingList of
Nothing ->
( errors, registerModuleNameOrAlias node context )
Just declaredImports ->
let
contextWithAlias : ModuleContext
contextWithAlias =
case import_.moduleAlias of
Just moduleAlias ->
registerModuleAlias node moduleAlias context
Nothing ->
context
in
( errors
, case Node.value declaredImports of
Exposing.All _ ->
if Dict.member (Node.value import_.moduleName) context.customTypes then
{ contextWithAlias
| exposingAllModules =
{ name = Node.value import_.moduleName
, alias = Maybe.map (Node.value >> String.join ".") import_.moduleAlias
, moduleNameRange = Node.range import_.moduleName
, exposingRange = Node.range declaredImports
, importRange = importRange
, wasUsedImplicitly = False
, wasUsedWithModuleName = False
}
:: context.exposingAllModules
}
else
contextWithAlias
Exposing.Explicit list ->
let
customTypesFromModule : Dict String (List String)
customTypesFromModule =
context.customTypes
|> Dict.get (Node.value import_.moduleName)
|> Maybe.withDefault Dict.empty
in
List.foldl
(registerExposedElements customTypesFromModule)
contextWithAlias
(collectExplicitlyExposedElements (Node.range declaredImports) list)
)
registerExposedElements : Dict String (List String) -> ExposedElement -> ModuleContext -> ModuleContext
registerExposedElements customTypesFromModule importedElement context =
case importedElement of
CustomType name variableInfo ->
case Dict.get name customTypesFromModule of
Just constructorNames ->
{ context
| unusedImportedCustomTypes = Dict.insert name variableInfo context.unusedImportedCustomTypes
, importedCustomTypeLookup =
Dict.union
(constructorNames
|> List.map (\constructorName -> ( constructorName, name ))
|> Dict.fromList
)
context.importedCustomTypeLookup
}
Nothing ->
context
TypeOrValue name variableInfo ->
registerVariable variableInfo name context
collectExplicitlyExposedElements : Range -> List (Node Exposing.TopLevelExpose) -> List ExposedElement
collectExplicitlyExposedElements exposingNodeRange list =
let
listWithPreviousRange : List (Maybe Range)
listWithPreviousRange =
Nothing
:: (list
|> List.map (Node.range >> Just)
|> List.take (List.length list - 1)
)
listWithNextRange : List Range
listWithNextRange =
(list
|> List.map Node.range
|> List.drop 1
)
++ [ { start = { row = 0, column = 0 }, end = { row = 0, column = 0 } } ]
in
list
|> List.map3 (\prev next current -> ( prev, current, next )) listWithPreviousRange listWithNextRange
|> List.indexedMap
(\index ( maybePreviousRange, Node range value, nextRange ) ->
let
rangeToRemove : Range
rangeToRemove =
if List.length list == 1 then
exposingNodeRange
else if index == 0 then
{ range | end = nextRange.start }
else
case maybePreviousRange of
Nothing ->
range
Just previousRange ->
{ range | start = previousRange.end }
in
case value of
Exposing.FunctionExpose name ->
TypeOrValue
name
{ typeName = "Imported variable"
, under = untilEndOfVariable name range
, rangeToRemove = Just rangeToRemove
, warning = ""
}
|> Just
Exposing.InfixExpose name ->
TypeOrValue
name
{ typeName = "Imported operator"
, under = untilEndOfVariable name range
, rangeToRemove = Just rangeToRemove
, warning = ""
}
|> Just
Exposing.TypeOrAliasExpose name ->
TypeOrValue
name
{ typeName = "Imported type"
, under = untilEndOfVariable name range
, rangeToRemove = Just rangeToRemove
, warning = ""
}
|> Just
Exposing.TypeExpose { name, open } ->
case open of
Just openRange ->
CustomType
name
{ typeName = "Imported type"
, under = range
, rangeToRemove = rangeToRemove
, openRange = openRange
}
|> Just
Nothing ->
-- Can't happen with `elm-syntax`. If open is Nothing, then this we'll have a
-- `Exposing.TypeOrAliasExpose`, not a `Exposing.TypeExpose`.
Nothing
)
|> List.filterMap identity
registerModuleNameOrAlias : Node Import -> ModuleContext -> ModuleContext
registerModuleNameOrAlias ((Node range { moduleAlias, moduleName }) as node) context =
case moduleAlias of
Just moduleAlias_ ->
registerModuleAlias node moduleAlias_ context
Nothing ->
registerModule
{ moduleName = Node.value moduleName
, alias = Nothing
, typeName = "Imported module"
, variableType = ImportedModule
, under = Node.range moduleName
, rangeToRemove = untilStartOfNextLine range
}
context
registerModuleAlias : Node Import -> Node ModuleName -> ModuleContext -> ModuleContext
registerModuleAlias ((Node range { exposingList, moduleName }) as node) moduleAlias context =
registerModule
{ moduleName = Node.value moduleName
, alias = Just (getModuleName (Node.value moduleAlias))
, variableType =
ModuleAlias
{ originalNameOfTheImport = getModuleName <| Node.value moduleName
, exposesSomething = exposingList /= Nothing
}
, typeName = "Module alias"
, under = Node.range moduleAlias
, rangeToRemove =
case exposingList of
Nothing ->
untilStartOfNextLine range
Just _ ->
moduleAliasRange node (Node.range moduleAlias)
}
context
moduleAliasRange : Node Import -> Range -> Range
moduleAliasRange (Node _ { moduleName }) range =
{ range | start = (Node.range moduleName).end }
expressionEnterVisitor : Node Expression -> ModuleContext -> ModuleContext
expressionEnterVisitor (Node range value) context =
case value of
Expression.FunctionOrValue [] name ->
case Dict.get name context.constructorNameToTypeName of
Just typeName ->
markValueAsUsed typeName context
Nothing ->
case Dict.get name context.importedCustomTypeLookup of
Just customTypeName ->
{ context | unusedImportedCustomTypes = Dict.remove customTypeName context.unusedImportedCustomTypes }
Nothing ->
case ModuleNameLookupTable.moduleNameAt context.lookupTable range of
Just realModuleName ->
context
|> markValueAsUsed name
|> markModuleAsUsed ( realModuleName, [] )
Nothing ->
markValueAsUsed name context
Expression.FunctionOrValue moduleName _ ->
case ModuleNameLookupTable.moduleNameAt context.lookupTable range of
Just realModuleName ->
markModuleAsUsed ( realModuleName, moduleName ) context
Nothing ->
context
Expression.OperatorApplication name _ _ _ ->
markValueAsUsed name context
Expression.PrefixOperator name ->
markValueAsUsed name context
Expression.RecordUpdateExpression expr _ ->
markValueAsUsed (Node.value expr) context
Expression.LambdaExpression { args } ->
markValuesFromPatternsAsUsed args
{ context | scopes = NonemptyList.cons (scopeWithPatternsToIgnore args) context.scopes }
Expression.CaseExpression { cases } ->
markValuesFromPatternsAsUsed
(List.map (\( patternNode, _ ) -> patternNode) cases)
context
Expression.LetExpression _ ->
{ context
| scopes = NonemptyList.cons { declared = Dict.empty, used = Dict.empty, namesToIgnore = Set.empty } context.scopes
}
_ ->
context
expressionExitVisitor : Node Expression -> ModuleContext -> ( List (Error {}), ModuleContext )
expressionExitVisitor node context =
case Node.value node of
Expression.LetExpression _ ->
makeReport context
Expression.LambdaExpression _ ->
makeReport context
_ ->
( [], 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
namesToIgnore : Set String
namesToIgnore =
List.concatMap getDeclaredParametersFromPattern functionDeclaration.arguments
|> Set.fromList
newContext : ModuleContext
newContext =
{ context | inTheDeclarationOf = Node.value functionDeclaration.name :: context.inTheDeclarationOf }
|> markValuesFromPatternsAsUsed functionDeclaration.arguments
|> registerFunction letBlockContext function (Node.range declaration)
in
( []
, { newContext
| scopes = NonemptyList.cons { declared = Dict.empty, used = Dict.empty, namesToIgnore = namesToIgnore } newContext.scopes
}
)
Expression.LetDestructuring pattern value ->
case removeParensFromPattern pattern of
Node wildCardRange Pattern.AllPattern ->
if isDebugLog context.lookupTable value then
( [], context )
else
( [ 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
)
_ ->
( if introducesVariable [ pattern ] then
[]
else
[ 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)) ]
]
, markValuesFromPatternsAsUsed [ pattern ] context
)
isDebugLog : ModuleNameLookupTable -> Node Expression -> Bool
isDebugLog lookupTable node =
case Node.value node of
Expression.Application [ functionWithParens, _, _ ] ->
let
function : Node Expression
function =
removeParensFromExpression functionWithParens
in
case Node.value function of
Expression.FunctionOrValue _ "log" ->
ModuleNameLookupTable.moduleNameFor lookupTable function == Just [ "Debug" ]
_ ->
False
Expression.OperatorApplication "|>" _ _ pipeFunction ->
case Node.value (removeParensFromExpression pipeFunction) of
Expression.Application [ functionWithParens, _ ] ->
let
function : Node Expression
function =
removeParensFromExpression functionWithParens
in
case Node.value function of
Expression.FunctionOrValue _ "log" ->
ModuleNameLookupTable.moduleNameFor lookupTable function == Just [ "Debug" ]
_ ->
False
_ ->
False
Expression.OperatorApplication "<|" _ pipeFunction _ ->
case Node.value (removeParensFromExpression pipeFunction) of
Expression.Application [ functionWithParens, _ ] ->
let
function : Node Expression
function =
removeParensFromExpression functionWithParens
in
case Node.value function of
Expression.FunctionOrValue _ "log" ->
ModuleNameLookupTable.moduleNameFor lookupTable function == Just [ "Debug" ]
_ ->
False
_ ->
False
_ ->
False
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 : ( Node Pattern, b ) -> ModuleContext -> 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
HasMultipleDeclarations ->
range
HasNoOtherDeclarations letDeclarationsRange ->
-- If there are no other declarations in the let in block,
-- we also need to remove the `let in` keywords.
letDeclarationsRange
removeParensFromPattern : Node Pattern -> Node Pattern
removeParensFromPattern node =
case Node.value node of
Pattern.ParenthesizedPattern pattern ->
removeParensFromPattern pattern
_ ->
node
removeParensFromExpression : Node Expression -> Node Expression
removeParensFromExpression node =
case Node.value node of
Expression.ParenthesizedExpression expr ->
removeParensFromExpression expr
_ ->
node
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) :: restOfNodes ->
case node of
Pattern.ParenthesizedPattern pattern ->
getDeclaredParametersFromPatternHelp (pattern :: restOfNodes) acc
Pattern.VarPattern name ->
getDeclaredParametersFromPatternHelp restOfNodes (name :: acc)
Pattern.AsPattern pattern (Node _ asName) ->
getDeclaredParametersFromPatternHelp (pattern :: restOfNodes) (asName :: acc)
Pattern.RecordPattern fields ->
getDeclaredParametersFromPatternHelp
restOfNodes
(List.map Node.value fields ++ acc)
Pattern.TuplePattern patterns ->
getDeclaredParametersFromPatternHelp (patterns ++ restOfNodes) acc
Pattern.NamedPattern _ patterns ->
getDeclaredParametersFromPatternHelp (patterns ++ restOfNodes) acc
Pattern.UnConsPattern left right ->
getDeclaredParametersFromPatternHelp (left :: right :: restOfNodes) acc
Pattern.ListPattern patterns ->
getDeclaredParametersFromPatternHelp (patterns ++ restOfNodes) acc
_ ->
getDeclaredParametersFromPatternHelp restOfNodes acc
[] ->
acc
markValuesFromPatternsAsUsed : List (Node Pattern) -> ModuleContext -> ModuleContext
markValuesFromPatternsAsUsed nodes context =
case nodes of
[] ->
context
node :: restOfNodes ->
case Node.value node of
Pattern.TuplePattern patterns ->
markValuesFromPatternsAsUsed (patterns ++ restOfNodes) context
Pattern.UnConsPattern left right ->
markValuesFromPatternsAsUsed (left :: right :: restOfNodes) context
Pattern.ListPattern patterns ->
markValuesFromPatternsAsUsed (patterns ++ restOfNodes) context
Pattern.NamedPattern qualifiedNameRef patterns ->
let
contextAfterTypeUsage : ModuleContext
contextAfterTypeUsage =
case qualifiedNameRef.moduleName of
[] ->
let
name : String
name =
Dict.get qualifiedNameRef.name context.constructorNameToTypeName
|> Maybe.withDefault qualifiedNameRef.name
in
markValueAsUsed name context
_ ->
context
contextAfterModuleUsage : ModuleContext
contextAfterModuleUsage =
case ModuleNameLookupTable.moduleNameFor context.lookupTable node of
Just realModuleName ->
markModuleAsUsed ( realModuleName, qualifiedNameRef.moduleName ) contextAfterTypeUsage
Nothing ->
contextAfterTypeUsage
in
markValuesFromPatternsAsUsed
(patterns ++ restOfNodes)
contextAfterModuleUsage
Pattern.AsPattern pattern _ ->
markValuesFromPatternsAsUsed (pattern :: restOfNodes) context
Pattern.ParenthesizedPattern pattern ->
markValuesFromPatternsAsUsed (pattern :: restOfNodes) context
_ ->
markValuesFromPatternsAsUsed restOfNodes context
introducesVariable : List (Node Pattern) -> Bool
introducesVariable nodes =
case nodes of
[] ->
False
patternNode :: restOfNodes ->
case Node.value patternNode of
Pattern.VarPattern _ ->
True
Pattern.AsPattern _ _ ->
True
Pattern.RecordPattern fields ->
if List.isEmpty fields then
introducesVariable restOfNodes
else
True
Pattern.TuplePattern patterns ->
introducesVariable (patterns ++ restOfNodes)
Pattern.UnConsPattern left right ->
introducesVariable (left :: right :: restOfNodes)
Pattern.ListPattern patterns ->
introducesVariable (patterns ++ restOfNodes)
Pattern.NamedPattern _ patterns ->
introducesVariable (patterns ++ restOfNodes)
Pattern.ParenthesizedPattern pattern ->
introducesVariable (pattern :: restOfNodes)
_ ->
introducesVariable restOfNodes
-- DECLARATION LIST VISITOR
declarationListVisitor : List (Node Declaration) -> ModuleContext -> ModuleContext
declarationListVisitor nodes context =
List.foldl registerTypes context nodes
registerTypes : Node Declaration -> ModuleContext -> ModuleContext
registerTypes node context =
case Node.value node of
Declaration.CustomTypeDeclaration customType ->
registerCustomType (Node.range node) customType context
Declaration.AliasDeclaration typeAliasDeclaration ->
registerTypeAlias (Node.range node) typeAliasDeclaration context
_ ->
context
registerCustomType : Range -> Elm.Syntax.Type.Type -> ModuleContext -> ModuleContext
registerCustomType range { name, constructors } context =
let
typeName : String
typeName =
Node.value name
constructorNames : List String
constructorNames =
List.map (Node.value >> .name >> Node.value) constructors
constructorsForType : Dict String String
constructorsForType =
constructorNames
|> List.map (\constructorName -> ( constructorName, typeName ))
|> Dict.fromList
customType : TypeData
customType =
{ kind = CustomTypeKind
, under = Node.range name
, rangeToRemove = untilStartOfNextLine range
, variants = constructorNames
}
in
{ context
| localTypes =
Dict.insert
(Node.value name)
customType
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 }
_ ->
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 }
-- DECLARATION ENTER VISITOR
declarationEnterVisitor : Node Declaration -> ModuleContext -> ( List (Error {}), ModuleContext )
declarationEnterVisitor node context =
case Node.value node of
Declaration.FunctionDeclaration function ->
let
functionImplementation : FunctionImplementation
functionImplementation =
Node.value function.declaration
functionName : String
functionName =
Node.value functionImplementation.name
typeAnnotation : List (Node TypeAnnotation)
typeAnnotation =
case function.signature of
Just signature ->
[ (Node.value signature).typeAnnotation ]
Nothing ->
[]
newContextWhereFunctionIsRegistered : ModuleContext
newContextWhereFunctionIsRegistered =
if
context.exposesEverything
-- The main function is "exposed" by default for applications
|| (context.isApplication && functionName == "main")
then
context
else
registerVariable
{ typeName = "Top-level variable"
, under = Node.range functionImplementation.name
, rangeToRemove = Just (untilStartOfNextLine (Node.range node))
, warning = ""
}
functionName
context
newContext : ModuleContext
newContext =
{ newContextWhereFunctionIsRegistered
| inTheDeclarationOf = [ functionName ]
, scopes = NonemptyList.cons (scopeWithPatternsToIgnore functionImplementation.arguments) newContextWhereFunctionIsRegistered.scopes
}
|> markValuesFromPatternsAsUsed (Node.value function.declaration).arguments
|> collectNamesFromTypeAnnotation Nothing typeAnnotation
shadowingImportError : List (Error {})
shadowingImportError =
case Dict.get functionName (NonemptyList.head context.scopes).declared of
Just existingVariable ->
if existingVariable.typeName == "Imported variable" then
[ error existingVariable functionName ]
else
[]
_ ->
[]
in
( shadowingImportError, newContext )
Declaration.CustomTypeDeclaration { name, constructors } ->
let
arguments : List (Node TypeAnnotation)
arguments =
List.concatMap (Node.value >> .arguments) constructors
in
( []
, collectNamesFromTypeAnnotation (Just (Node.value name)) arguments context
)
Declaration.AliasDeclaration { typeAnnotation } ->
( []
, collectNamesFromTypeAnnotation Nothing [ typeAnnotation ] context
)
Declaration.PortDeclaration { name, typeAnnotation } ->
let
contextWithUsedElements : ModuleContext
contextWithUsedElements =
collectNamesFromTypeAnnotation Nothing [ typeAnnotation ] context
in
( []
, if context.exposesEverything then
contextWithUsedElements
else
registerVariable
{ typeName = "Port"
, under = Node.range name
, rangeToRemove = Nothing
, warning = " (Warning: Removing this port may break your application if it is used in the JS code)"
}
(Node.value name)
contextWithUsedElements
)
Declaration.InfixDeclaration { operator, function } ->
( []
, context
|> markValueAsUsed (Node.value function)
|> registerVariable
{ typeName = "Declared operator"
, under = Node.range operator
, rangeToRemove = Just (Node.range node)
, warning = ""
}
(Node.value operator)
)
Declaration.Destructuring _ _ ->
( [], context )
-- 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
rootScope : Scope
rootScope =
NonemptyList.head context.scopes
namesOfCustomTypesUsedByCallingAConstructor : Set String
namesOfCustomTypesUsedByCallingAConstructor =
context.constructorNameToTypeName
|> Dict.filter (\usedName _ -> Set.member usedName (Dict.get [] rootScope.used |> Maybe.withDefault Set.empty))
|> Dict.values
|> Set.fromList
newRootScope : Scope
newRootScope =
{ rootScope
| used =
Dict.update []
(\set ->
set
|> Maybe.withDefault Set.empty
|> Set.union namesOfCustomTypesUsedByCallingAConstructor
|> Just
)
rootScope.used
}
moduleNamesInUse : Set String
moduleNamesInUse =
context.declaredModules
|> List.map (\{ alias, moduleName } -> Maybe.withDefault (getModuleName moduleName) alias)
|> Set.fromList
usedLocally : Set String
usedLocally =
Dict.get [] rootScope.used |> Maybe.withDefault Set.empty
importedTypeErrors : List (Error {})
importedTypeErrors =
context.unusedImportedCustomTypes
|> Dict.toList
|> List.map
(\( name, { under, rangeToRemove, openRange } ) ->
if Set.member name usedLocally && not (Dict.member name context.localTypes) then
Rule.errorWithFix
{ message = "Imported constructors for `" ++ name ++ "` are not used"
, details = details
}
under
-- If the constructors are not used but the type itself is, then only remove the `(..)`
[ Fix.removeRange openRange ]
else
Rule.errorWithFix
{ message = "Imported type `" ++ name ++ "` is not used"
, details = details
}
under
[ Fix.removeRange rangeToRemove ]
)
moduleThatExposeEverythingErrors : List ( Maybe (Error {}), Maybe ( ModuleName, ModuleName ) )
moduleThatExposeEverythingErrors =
List.map
(\({ importRange, exposingRange } as module_) ->
if not module_.wasUsedImplicitly then
if module_.wasUsedWithModuleName then
( Just
(Rule.errorWithFix
{ message = "No imported elements from `" ++ String.join "." module_.name ++ "` are used"
, details = details
}
exposingRange
[ Fix.removeRange exposingRange ]
)
, Nothing
)
else
( Just
(Rule.errorWithFix
{ message = "Imported module `" ++ String.join "." module_.name ++ "` is not used"
, details = details
}
module_.moduleNameRange
[ Fix.removeRange { importRange | end = { row = importRange.end.row + 1, column = 1 } } ]
)
, Maybe.map (\alias -> ( module_.name, [ alias ] )) module_.alias
)
else
( Nothing, Nothing )
)
context.exposingAllModules
usedModules : Set ( ModuleName, ModuleName )
usedModules =
Set.union
(Set.fromList (List.filterMap Tuple.second moduleThatExposeEverythingErrors))
context.usedModules
moduleErrors : List (Error {})
moduleErrors =
context.declaredModules
|> List.filter
(\variableInfo ->
not
(case variableInfo.alias of
Just alias ->
Set.member ( variableInfo.moduleName, [ alias ] ) usedModules
Nothing ->
Set.member ( variableInfo.moduleName, variableInfo.moduleName ) usedModules
)
)
|> List.map
(\variableInfo ->
let
name : String
name =
case variableInfo.alias of
Just alias ->
alias
Nothing ->
getModuleName variableInfo.moduleName
fix : List Fix
fix =
case variableInfo.variableType of
ImportedModule ->
[ Fix.removeRange variableInfo.rangeToRemove ]
ModuleAlias { originalNameOfTheImport, exposesSomething } ->
if not exposesSomething || not (Set.member originalNameOfTheImport moduleNamesInUse) then
[ Fix.removeRange variableInfo.rangeToRemove ]
else
[]
in
Rule.errorWithFix
{ message = variableInfo.typeName ++ " `" ++ name ++ "` is not used"
, details = details
}
variableInfo.under
fix
)
customTypeErrors : List (Error {})
customTypeErrors =
if context.exposesEverything then
[]
else
context.localTypes
|> Dict.toList
|> List.filter (\( name, _ ) -> not <| Set.member name usedLocally)
|> List.map errorForLocalType
in
List.concat
[ makeReportHelp newRootScope
|> Tuple.first
, importedTypeErrors
, moduleErrors
, List.filterMap Tuple.first moduleThatExposeEverythingErrors
, customTypeErrors
]
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 -> Range -> ModuleContext -> ModuleContext
registerFunction letBlockContext function functionDeclarationRange context =
let
declaration : FunctionImplementation
declaration =
Node.value function.declaration
typeAnnotations : List (Node TypeAnnotation)
typeAnnotations =
case Maybe.map Node.value function.signature of
Just signature ->
[ signature.typeAnnotation ]
Nothing ->
[]
in
context
|> collectNamesFromTypeAnnotation Nothing typeAnnotations
|> registerVariable
{ typeName = "`let in` variable"
, under = Node.range declaration.name
, rangeToRemove = Just (letDeclarationToRemoveRange letBlockContext functionDeclarationRange)
, warning = ""
}
(Node.value declaration.name)
scopeWithPatternsToIgnore : List (Node Pattern) -> Scope
scopeWithPatternsToIgnore patterns =
{ declared = Dict.empty
, used = Dict.empty
, namesToIgnore =
List.concatMap getDeclaredParametersFromPattern patterns
|> Set.fromList
}
type ExposedElement
= CustomType String ImportedCustomType
| TypeOrValue String VariableInfo
untilEndOfVariable : String -> Range -> Range
untilEndOfVariable name range =
if range.start.row == range.end.row then
range
else
{ range | end = { row = range.start.row, column = range.start.column + String.length name } }
collectNamesFromTypeAnnotation : Maybe String -> List (Node TypeAnnotation) -> ModuleContext -> ModuleContext
collectNamesFromTypeAnnotation exception nodes context =
case nodes of
[] ->
context
node :: restOfNodes ->
case Node.value node of
TypeAnnotation.FunctionTypeAnnotation left right ->
collectNamesFromTypeAnnotation exception (left :: right :: restOfNodes) context
TypeAnnotation.Typed (Node typeRange ( rawModuleName, typeName )) params ->
let
contextAfterTypeUsage : ModuleContext
contextAfterTypeUsage =
if Just typeName /= exception then
case rawModuleName of
[] ->
markAsUsed typeName context
_ ->
context
else
context
contextAfterModuleUsage : ModuleContext
contextAfterModuleUsage =
case ModuleNameLookupTable.moduleNameAt context.lookupTable typeRange of
Just realModuleName ->
markModuleAsUsed ( realModuleName, rawModuleName ) contextAfterTypeUsage
Nothing ->
contextAfterTypeUsage
in
collectNamesFromTypeAnnotation
exception
(params ++ restOfNodes)
contextAfterModuleUsage
TypeAnnotation.Record fields ->
let
subNodes : List (Node TypeAnnotation)
subNodes =
List.map (\(Node _ ( _, value )) -> value) fields
in
collectNamesFromTypeAnnotation exception (subNodes ++ restOfNodes) context
TypeAnnotation.GenericRecord _ (Node _ fields) ->
let
subNodes : List (Node TypeAnnotation)
subNodes =
List.map (\(Node _ ( _, value )) -> value) fields
in
collectNamesFromTypeAnnotation exception (subNodes ++ restOfNodes) context
TypeAnnotation.Tupled list ->
collectNamesFromTypeAnnotation exception (list ++ restOfNodes) context
_ ->
collectNamesFromTypeAnnotation exception restOfNodes context
registerModule : DeclaredModule -> ModuleContext -> ModuleContext
registerModule declaredModule context =
{ context | declaredModules = declaredModule :: context.declaredModules }
registerVariable : VariableInfo -> String -> ModuleContext -> ModuleContext
registerVariable variableInfo name context =
let
scopes : Nonempty Scope
scopes =
NonemptyList.mapHead
(\scope ->
{ scope | declared = Dict.insert name variableInfo scope.declared }
)
context.scopes
in
{ context | scopes = scopes }
markAllAsUsed : List String -> ModuleContext -> ModuleContext
markAllAsUsed names context =
List.foldl markAsUsed context names
markValueAsUsed : String -> ModuleContext -> ModuleContext
markValueAsUsed name context =
if Dict.member name context.constructorNameToTypeName then
markAsUsed name context
else
case Dict.get name context.importedCustomTypeLookup of
Just customTypeName ->
{ context | unusedImportedCustomTypes = Dict.remove customTypeName context.unusedImportedCustomTypes }
_ ->
markAsUsed name context
markAsUsed : String -> ModuleContext -> ModuleContext
markAsUsed name context =
if List.member name context.inTheDeclarationOf then
context
else
let
scopes : Nonempty Scope
scopes =
NonemptyList.mapHead
(\scope ->
{ scope
| used =
Dict.update []
(\set ->
set
|> Maybe.withDefault Set.empty
|> Set.insert name
|> Just
)
scope.used
}
)
context.scopes
in
{ context | scopes = scopes }
markModuleAsUsed : ( ModuleName, ModuleName ) -> ModuleContext -> ModuleContext
markModuleAsUsed (( realModuleName, aliasName ) as realAndAliasModuleNames) context =
{ context
| usedModules = Set.insert realAndAliasModuleNames context.usedModules
, exposingAllModules =
List.map
(\module_ ->
if module_.name == realModuleName then
if module_.name == aliasName || Just (String.join "." aliasName) == module_.alias then
{ module_ | wasUsedWithModuleName = True }
else if aliasName == [] then
{ module_ | wasUsedImplicitly = True }
else
module_
else
module_
)
context.exposingAllModules
}
getModuleName : List String -> String
getModuleName name =
String.join "." name
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 =
Dict.get [] used |> Maybe.withDefault Set.empty
nonUsedVars : List String
nonUsedVars =
Dict.keys declared
|> Set.fromList
|> Set.diff usedLocally
|> (\set -> Set.diff set namesToIgnore)
|> Set.toList
errors : List (Error {})
errors =
Dict.foldl
(\key variableInfo acc ->
if not (Set.member key usedLocally) then
error variableInfo key :: acc
else
acc
)
[]
declared
in
( errors, nonUsedVars )
-- RANGE MANIPULATION
{-| Include everything until the line after the end.
-}
untilStartOfNextLine : Range -> Range
untilStartOfNextLine range =
if range.end.column == 1 then
range
else
{ range | end = { row = range.end.row + 1, column = 1 } }
{-| Make a range stop at a position. If the position is not inside the range,
then the range won't change.
range : Range
range =
rangeUpUntil
(Node.range node)
(node |> Node.value |> .typeAnnotation |> Node.range |> .start)
-}
rangeUpUntil : Range -> { row : Int, column : Int } -> Range
rangeUpUntil range position =
let
positionAsInt_ : Int
positionAsInt_ =
positionAsInt position
in
if positionAsInt range.start <= positionAsInt_ && positionAsInt range.end >= positionAsInt_ then
{ range | end = position }
else
range
positionAsInt : { row : Int, column : Int } -> Int
positionAsInt { row, column } =
-- This is a quick and simple heuristic to be able to sort ranges.
-- It is entirely based on the assumption that no line is longer than
-- 1.000.000 characters long. Then, as long as ranges don't overlap,
-- this should work fine.
row * 1000000 + column