elm-review/tests/NoUnused/Variables.elm
2021-08-19 20:57:33 +02:00

1683 lines
61 KiB
Elm

module NoUnused.Variables exposing (rule)
{-| Report variables or types that are declared or imported but never used inside of a module.
# Rule
@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 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)
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)
a n =
n + 1
b =
a 2
## Success
module A exposing (a)
a n =
n + 1
## 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 elmJsonVisitor
|> Rule.withDependenciesProjectVisitor 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 moduleDefinitionVisitor
|> Rule.withImportVisitor importVisitor
|> Rule.withDeclarationListVisitor declarationListVisitor
|> Rule.withDeclarationEnterVisitor declarationVisitor
|> Rule.withExpressionEnterVisitor expressionEnterVisitor
|> Rule.withExpressionExitVisitor expressionExitVisitor
|> 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
, declarations : RangeDict 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
, localCustomTypes : Dict String CustomTypeData
, 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 CustomTypeData =
{ under : Range
, rangeToRemove : Range
, variants : List String
}
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)
}
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 = []
, declarations = Dict.empty
, exposesEverything = False
, isApplication = isApplication
, constructorNameToTypeName = Dict.empty
, declaredModules = []
, exposingAllModules = []
, usedModules = Set.empty
, unusedImportedCustomTypes = Dict.empty
, importedCustomTypeLookup = Dict.empty
, localCustomTypes = Dict.empty
, customTypes = customTypes
}
)
|> Rule.withModuleNameLookupTable
fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext
fromModuleToProject =
Rule.initContextCreator
(\metadata moduleContext ->
{ customTypes =
moduleContext.localCustomTypes
|> Dict.map (\_ customType -> customType.variants)
|> Dict.singleton (Rule.moduleNameFromMetadata metadata)
-- Will be ignored in foldProjectContexts
, isApplication = True
}
)
|> Rule.withMetadata
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
}
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 -> ( List nothing, 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 -> ( List nothing, 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 -> ( 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 =
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.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.LambdaExpression { args } ->
let
namesUsedInArgumentPatterns : { types : List String, modules : List ( ModuleName, ModuleName ) }
namesUsedInArgumentPatterns =
args
|> List.map (getUsedVariablesFromPattern context)
|> foldUsedTypesAndModules
in
( []
, List.foldl markValueAsUsed context namesUsedInArgumentPatterns.types
|> markAllModulesAsUsed namesUsedInArgumentPatterns.modules
)
_ ->
( [], 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
removeParens : Node Pattern -> Node Pattern
removeParens node =
case Node.value node of
Pattern.ParenthesizedPattern pattern ->
removeParens pattern
_ ->
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
, modules = getUsedModulesFromPattern context.lookupTable patternNode
}
getUsedTypesFromPattern : Dict String String -> Node Pattern -> List String
getUsedTypesFromPattern constructorNameToTypeName patternNode =
case Node.value patternNode of
Pattern.AllPattern ->
[]
Pattern.UnitPattern ->
[]
Pattern.CharPattern _ ->
[]
Pattern.StringPattern _ ->
[]
Pattern.IntPattern _ ->
[]
Pattern.HexPattern _ ->
[]
Pattern.FloatPattern _ ->
[]
Pattern.TuplePattern patterns ->
List.concatMap (getUsedTypesFromPattern constructorNameToTypeName) patterns
Pattern.RecordPattern _ ->
[]
Pattern.UnConsPattern pattern1 pattern2 ->
List.concatMap (getUsedTypesFromPattern constructorNameToTypeName) [ pattern1, pattern2 ]
Pattern.ListPattern patterns ->
List.concatMap (getUsedTypesFromPattern constructorNameToTypeName) patterns
Pattern.VarPattern _ ->
[]
Pattern.NamedPattern qualifiedNameRef patterns ->
case qualifiedNameRef.moduleName of
[] ->
(Dict.get qualifiedNameRef.name constructorNameToTypeName |> Maybe.withDefault qualifiedNameRef.name)
:: List.concatMap (getUsedTypesFromPattern constructorNameToTypeName) patterns
_ ->
List.concatMap (getUsedTypesFromPattern constructorNameToTypeName) patterns
Pattern.AsPattern pattern _ ->
getUsedTypesFromPattern constructorNameToTypeName pattern
Pattern.ParenthesizedPattern pattern ->
getUsedTypesFromPattern constructorNameToTypeName pattern
getUsedModulesFromPattern : ModuleNameLookupTable -> Node Pattern -> List ( ModuleName, ModuleName )
getUsedModulesFromPattern lookupTable patternNode =
case Node.value patternNode of
Pattern.AllPattern ->
[]
Pattern.UnitPattern ->
[]
Pattern.CharPattern _ ->
[]
Pattern.StringPattern _ ->
[]
Pattern.IntPattern _ ->
[]
Pattern.HexPattern _ ->
[]
Pattern.FloatPattern _ ->
[]
Pattern.TuplePattern patterns ->
List.concatMap (getUsedModulesFromPattern lookupTable) patterns
Pattern.RecordPattern _ ->
[]
Pattern.UnConsPattern pattern1 pattern2 ->
List.concatMap (getUsedModulesFromPattern lookupTable) [ pattern1, pattern2 ]
Pattern.ListPattern patterns ->
List.concatMap (getUsedModulesFromPattern lookupTable) patterns
Pattern.VarPattern _ ->
[]
Pattern.NamedPattern qualifiedNameRef patterns ->
case ModuleNameLookupTable.moduleNameFor lookupTable patternNode of
Just realModuleName ->
( realModuleName, qualifiedNameRef.moduleName ) :: List.concatMap (getUsedModulesFromPattern lookupTable) patterns
Nothing ->
List.concatMap (getUsedModulesFromPattern lookupTable) patterns
Pattern.AsPattern pattern _ ->
getUsedModulesFromPattern lookupTable pattern
Pattern.ParenthesizedPattern pattern ->
getUsedModulesFromPattern lookupTable pattern
introducesVariable : Node Pattern -> Bool
introducesVariable patternNode =
case Node.value patternNode of
Pattern.VarPattern _ ->
True
Pattern.AsPattern _ _ ->
True
Pattern.RecordPattern fields ->
not (List.isEmpty fields)
Pattern.TuplePattern patterns ->
List.any introducesVariable patterns
Pattern.UnConsPattern pattern1 pattern2 ->
List.any introducesVariable [ pattern1, pattern2 ]
Pattern.ListPattern patterns ->
List.any introducesVariable patterns
Pattern.NamedPattern _ patterns ->
List.any introducesVariable patterns
Pattern.ParenthesizedPattern pattern ->
introducesVariable pattern
_ ->
False
-- DECLARATION LIST VISITOR
declarationListVisitor : List (Node Declaration) -> ModuleContext -> ( List (Error {}), 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
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
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 : CustomTypeData
customType =
{ under = Node.range name
, rangeToRemove = untilStartOfNextLine range
, variants = constructorNames
}
in
{ context
| localCustomTypes =
Dict.insert
(Node.value name)
customType
context.localCustomTypes
, constructorNameToTypeName = Dict.union constructorsForType context.constructorNameToTypeName
}
-- DECLARATION VISITOR
declarationVisitor : Node Declaration -> ModuleContext -> ( List (Error {}), ModuleContext )
declarationVisitor 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
namesUsedInSignature : { types : List String, modules : List ( ModuleName, ModuleName ) }
namesUsedInSignature =
function.signature
|> Maybe.map (Node.value >> .typeAnnotation >> collectNamesFromTypeAnnotation context.lookupTable)
|> Maybe.withDefault { types = [], modules = [] }
namesUsedInArgumentPatterns : { types : List String, modules : List ( ModuleName, ModuleName ) }
namesUsedInArgumentPatterns =
function.declaration
|> Node.value
|> .arguments
|> List.map (getUsedVariablesFromPattern context)
|> foldUsedTypesAndModules
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 ], declarations = Dict.empty }
|> (\ctx -> List.foldl markValueAsUsed ctx namesUsedInArgumentPatterns.types)
|> markAllAsUsed namesUsedInSignature.types
|> markAllModulesAsUsed namesUsedInSignature.modules
|> markAllModulesAsUsed namesUsedInArgumentPatterns.modules
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
{ types, modules } =
constructors
|> List.concatMap (Node.value >> .arguments)
|> List.map (collectNamesFromTypeAnnotation context.lookupTable)
|> foldUsedTypesAndModules
in
( []
, types
|> List.filter ((/=) (Node.value name))
|> List.foldl markAsUsed context
|> markAllModulesAsUsed modules
)
Declaration.AliasDeclaration { name, typeAnnotation } ->
let
namesUsedInTypeAnnotation : { types : List String, modules : List ( ModuleName, ModuleName ) }
namesUsedInTypeAnnotation =
collectNamesFromTypeAnnotation context.lookupTable typeAnnotation
in
( []
, List.foldl markAsUsed context namesUsedInTypeAnnotation.types
|> markAllModulesAsUsed namesUsedInTypeAnnotation.modules
)
Declaration.PortDeclaration { name, typeAnnotation } ->
let
namesUsedInTypeAnnotation : { types : List String, modules : List ( ModuleName, ModuleName ) }
namesUsedInTypeAnnotation =
collectNamesFromTypeAnnotation context.lookupTable typeAnnotation
contextWithUsedElements : ModuleContext
contextWithUsedElements =
List.foldl markAsUsed context namesUsedInTypeAnnotation.types
|> markAllModulesAsUsed namesUsedInTypeAnnotation.modules
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 )
foldUsedTypesAndModules : List { types : List String, modules : List ( ModuleName, ModuleName ) } -> { types : List String, modules : List ( ModuleName, ModuleName ) }
foldUsedTypesAndModules =
List.foldl (\a b -> { types = a.types ++ b.types, modules = a.modules ++ b.modules }) { types = [], modules = [] }
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.localCustomTypes) 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.localCustomTypes
|> 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 ]
)
in
List.concat
[ newRootScope
|> makeReport
|> Tuple.first
, importedTypeErrors
, moduleErrors
, List.filterMap Tuple.first moduleThatExposeEverythingErrors
, customTypeErrors
]
registerFunction : LetBlockContext -> Function -> ModuleContext -> ModuleContext
registerFunction letBlockContext function context =
let
declaration : FunctionImplementation
declaration =
Node.value function.declaration
namesUsedInSignature : { types : List String, modules : List ( ModuleName, ModuleName ) }
namesUsedInSignature =
case Maybe.map Node.value function.signature of
Just signature ->
collectNamesFromTypeAnnotation context.lookupTable signature.typeAnnotation
Nothing ->
{ types = [], modules = [] }
in
List.foldl markAsUsed context namesUsedInSignature.types
|> markAllModulesAsUsed namesUsedInSignature.modules
|> registerVariable
{ typeName = "`let in` variable"
, under = Node.range declaration.name
, rangeToRemove = Just (letDeclarationToRemoveRange letBlockContext (Node.range function.declaration))
, warning = ""
}
(Node.value declaration.name)
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 : ModuleNameLookupTable -> Node TypeAnnotation -> { types : List String, modules : List ( ModuleName, ModuleName ) }
collectNamesFromTypeAnnotation lookupTable node =
{ types = collectTypesFromTypeAnnotation node
, modules = collectModuleNamesFromTypeAnnotation lookupTable node
}
collectTypesFromTypeAnnotation : Node TypeAnnotation -> List String
collectTypesFromTypeAnnotation node =
case Node.value node of
TypeAnnotation.FunctionTypeAnnotation a b ->
collectTypesFromTypeAnnotation a ++ collectTypesFromTypeAnnotation b
TypeAnnotation.Typed nameNode params ->
let
name : List String
name =
case Node.value nameNode of
( [], str ) ->
[ str ]
_ ->
[]
in
name ++ List.concatMap collectTypesFromTypeAnnotation params
TypeAnnotation.Record list ->
list
|> List.map (Node.value >> Tuple.second)
|> List.concatMap collectTypesFromTypeAnnotation
TypeAnnotation.GenericRecord _ list ->
list
|> Node.value
|> List.map (Node.value >> Tuple.second)
|> List.concatMap collectTypesFromTypeAnnotation
TypeAnnotation.Tupled list ->
List.concatMap collectTypesFromTypeAnnotation list
TypeAnnotation.GenericType _ ->
[]
TypeAnnotation.Unit ->
[]
collectModuleNamesFromTypeAnnotation : ModuleNameLookupTable -> Node TypeAnnotation -> List ( ModuleName, ModuleName )
collectModuleNamesFromTypeAnnotation lookupTable node =
case Node.value node of
TypeAnnotation.FunctionTypeAnnotation a b ->
collectModuleNamesFromTypeAnnotation lookupTable a ++ collectModuleNamesFromTypeAnnotation lookupTable b
TypeAnnotation.Typed nameNode params ->
case ModuleNameLookupTable.moduleNameFor lookupTable nameNode of
Just realModuleName ->
( realModuleName, Tuple.first (Node.value nameNode) ) :: List.concatMap (collectModuleNamesFromTypeAnnotation lookupTable) params
Nothing ->
List.concatMap (collectModuleNamesFromTypeAnnotation lookupTable) params
TypeAnnotation.Record list ->
list
|> List.map (Node.value >> Tuple.second)
|> List.concatMap (collectModuleNamesFromTypeAnnotation lookupTable)
TypeAnnotation.GenericRecord _ list ->
list
|> Node.value
|> List.map (Node.value >> Tuple.second)
|> List.concatMap (collectModuleNamesFromTypeAnnotation lookupTable)
TypeAnnotation.Tupled list ->
List.concatMap (collectModuleNamesFromTypeAnnotation lookupTable) list
TypeAnnotation.GenericType _ ->
[]
TypeAnnotation.Unit ->
[]
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 }
markAllModulesAsUsed : List ( ModuleName, ModuleName ) -> ModuleContext -> ModuleContext
markAllModulesAsUsed names context =
List.foldl markModuleAsUsed context names
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 : Scope -> ( List (Error {}), List String )
makeReport { declared, used } =
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.toList
errors : List (Error {})
errors =
Dict.filter (\key _ -> not <| Set.member key usedLocally) declared
|> Dict.toList
|> List.map (\( key, variableInfo ) -> error variableInfo key)
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