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