Backport scope v0.2.0

This commit is contained in:
Jeroen Engels 2020-05-16 22:33:36 +02:00
parent 52fd8d968e
commit ee8f294c18
8 changed files with 532 additions and 293 deletions

View File

@ -32,7 +32,7 @@ expressionVisitor : Node Expression -> Direction -> Context -> ( List (Error {})
expressionVisitor node direction context = expressionVisitor node direction context =
case ( direction, Node.value node ) of case ( direction, Node.value node ) of
( Rule.OnEnter, FunctionOrValue moduleName "button" ) -> ( Rule.OnEnter, FunctionOrValue moduleName "button" ) ->
if Scope.realModuleName context.scope "button" moduleName == [ "Html" ] then if Scope.moduleNameForValue context.scope "button" moduleName == [ "Html" ] then
( [ Rule.error ( [ Rule.error
{ message = "Do not use `Html.button` directly" { message = "Do not use `Html.button` directly"
, details = [ "At fruits.com, we've built a nice `Button` module that suits our needs better. Using this module instead of `Html.button` ensures we have a consistent button experience across the website." ] , details = [ "At fruits.com, we've built a nice `Button` module that suits our needs better. Using this module instead of `Html.button` ensures we have a consistent button experience across the website." ]

View File

@ -0,0 +1,202 @@
module ModuleNameForTypeTest exposing (all)
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Node as Node exposing (Node(..))
import Elm.Syntax.TypeAnnotation as TypeAnnotation exposing (TypeAnnotation)
import Fixtures.Dependencies as Dependencies
import Review.Project as Project exposing (Project)
import Review.Rule as Rule exposing (Error, Rule)
import Review.Test
import Scope
import Test exposing (Test, test)
all : Test
all =
Test.describe "Scope.moduleNameForType"
[ test "should return the module that defined the type" <|
\() ->
[ """module A exposing (..)
import Bar as Baz exposing (baz)
import ExposesSomeThings exposing (..)
import ExposesEverything exposing (..)
import Foo.Bar
import Html exposing (..)
import Http exposing (get)
import Something.B as Something
type A = B | C
type Role = NormalUser Bool | Admin (Maybe A)
type alias User =
{ role : Role
, age : ( Msg, Unknown )
}
type alias GenericRecord generic = { generic | foo : A }
a : SomeCustomType -> SomeTypeAlias -> SomeOtherTypeAlias -> NonExposedCustomType
a = 1
""", """module ExposesSomeThings exposing (SomeOtherTypeAlias)
type NonExposedCustomType = Variant
type alias SomeOtherTypeAlias = {}
""", """module ExposesEverything exposing (..)
type SomeCustomType = VariantA | VariantB
type alias SomeTypeAlias = {}
type Msg = SomeMsgToBeShadowed | SomeOtherMsg
""", """module Something.B exposing (..)
b = 1
type Foo = Bar
type alias BAlias = {}
""" ]
|> Review.Test.runOnModulesWithProjectData project projectRule
|> Review.Test.expectErrorsForModules
[ ( "A"
, [ Review.Test.error
{ message = """
<nothing>.Bool -> Basics.Bool
<nothing>.Maybe -> Maybe.Maybe
<nothing>.A -> <nothing>.A
<nothing>.Role -> <nothing>.Role
<nothing>.Msg -> ExposesEverything.Msg
<nothing>.Unknown -> <nothing>.Unknown
<nothing>.A -> <nothing>.A
<nothing>.SomeCustomType -> ExposesEverything.SomeCustomType
<nothing>.SomeTypeAlias -> ExposesEverything.SomeTypeAlias
<nothing>.SomeOtherTypeAlias -> ExposesSomeThings.SomeOtherTypeAlias
<nothing>.NonExposedCustomType -> <nothing>.NonExposedCustomType
"""
, details = [ "details" ]
, under = "module"
}
]
)
]
]
type alias ModuleContext =
{ scope : Scope.ModuleContext
, texts : List String
}
project : Project
project =
Project.new
|> Project.addDependency Dependencies.elmCore
|> Project.addDependency Dependencies.elmHtml
projectRule : Rule
projectRule =
Rule.newProjectRuleSchema "TestRule" { scope = Scope.initialProjectContext }
|> Scope.addProjectVisitors
|> Rule.withModuleVisitor moduleVisitor
|> Rule.withModuleContext
{ fromProjectToModule =
\_ _ projectContext ->
{ scope = Scope.fromProjectToModule projectContext.scope
, texts = []
}
, fromModuleToProject =
\_ moduleNameNode moduleContext ->
{ scope = Scope.fromModuleToProject moduleNameNode moduleContext.scope
}
, foldProjectContexts = \a b -> { scope = Scope.foldProjectContexts a.scope b.scope }
}
|> Rule.fromProjectRuleSchema
moduleVisitor : Rule.ModuleRuleSchema schemaState ModuleContext -> Rule.ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } ModuleContext
moduleVisitor schema =
schema
|> Rule.withDeclarationVisitor declarationVisitor
|> Rule.withFinalModuleEvaluation finalEvaluation
declarationVisitor : Node Declaration -> Rule.Direction -> ModuleContext -> ( List nothing, ModuleContext )
declarationVisitor node direction context =
case ( direction, Node.value node ) of
( Rule.OnEnter, Declaration.CustomTypeDeclaration { constructors } ) ->
let
types : List String
types =
constructors
|> List.concatMap (Node.value >> .arguments)
|> List.concatMap (typeAnnotationNames context.scope)
in
( [], { context | texts = context.texts ++ types } )
( Rule.OnEnter, Declaration.AliasDeclaration { typeAnnotation } ) ->
( [], { context | texts = context.texts ++ typeAnnotationNames context.scope typeAnnotation } )
( Rule.OnEnter, Declaration.FunctionDeclaration function ) ->
case function.signature |> Maybe.map (Node.value >> .typeAnnotation) of
Nothing ->
( [], context )
Just typeAnnotation ->
( [], { context | texts = context.texts ++ typeAnnotationNames context.scope typeAnnotation } )
_ ->
( [], context )
typeAnnotationNames : Scope.ModuleContext -> Node TypeAnnotation -> List String
typeAnnotationNames scope typeAnnotation =
case Node.value typeAnnotation of
TypeAnnotation.GenericType name ->
[ "<nothing>." ++ name ++ " -> <generic>" ]
TypeAnnotation.Typed (Node _ ( moduleName, typeName )) typeParameters ->
let
nameInCode : String
nameInCode =
case moduleName of
[] ->
"<nothing>." ++ typeName
_ ->
String.join "." moduleName ++ "." ++ typeName
realName : String
realName =
case Scope.moduleNameForType scope typeName moduleName of
[] ->
"<nothing>." ++ typeName
moduleName_ ->
String.join "." moduleName_ ++ "." ++ typeName
in
(nameInCode ++ " -> " ++ realName)
:: List.concatMap (typeAnnotationNames scope) typeParameters
TypeAnnotation.Unit ->
[]
TypeAnnotation.Tupled typeAnnotations ->
List.concatMap (typeAnnotationNames scope) typeAnnotations
TypeAnnotation.Record typeAnnotations ->
List.concatMap (Node.value >> Tuple.second >> typeAnnotationNames scope) typeAnnotations
TypeAnnotation.GenericRecord _ typeAnnotations ->
List.concatMap (Node.value >> Tuple.second >> typeAnnotationNames scope) (Node.value typeAnnotations)
TypeAnnotation.FunctionTypeAnnotation arg returnType ->
typeAnnotationNames scope arg ++ typeAnnotationNames scope returnType
finalEvaluation : ModuleContext -> List (Error {})
finalEvaluation context =
if List.isEmpty context.texts then
[]
else
[ Rule.error
{ message = "\n" ++ String.join "\n" context.texts ++ "\n"
, details = [ "details" ]
}
{ start = { row = 1, column = 1 }
, end = { row = 1, column = 7 }
}
]

View File

@ -1,9 +1,7 @@
module ScopeTest exposing (all) module ModuleNameForValueTest exposing (all)
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Expression as Expression exposing (Expression) import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node(..)) import Elm.Syntax.Node as Node exposing (Node(..))
import Elm.Syntax.TypeAnnotation as TypeAnnotation exposing (TypeAnnotation)
import Fixtures.Dependencies as Dependencies import Fixtures.Dependencies as Dependencies
import Review.Project as Project exposing (Project) import Review.Project as Project exposing (Project)
import Review.Rule as Rule exposing (Error, Rule) import Review.Rule as Rule exposing (Error, Rule)
@ -14,16 +12,16 @@ import Test exposing (Test, test)
all : Test all : Test
all = all =
Test.describe "Scope" Test.describe "Scope.moduleNameForValue"
[ realModuleNameTestsForModuleRule [ forModuleRule
, realModuleNameTestsForProjectRule , forProjectRule
] ]
realModuleNameTestsForModuleRule : Test forModuleRule : Test
realModuleNameTestsForModuleRule = forModuleRule =
Test.describe "Scope.realModuleName (module rule)" Test.describe "module rule"
[ test "should indicate that module from which a function or value comes from, with knowledge of what is in other modules" <| [ test "should return the module that defined the value" <|
\() -> \() ->
"""module A exposing (..) """module A exposing (..)
import Bar as Baz exposing (baz) import Bar as Baz exposing (baz)
@ -35,7 +33,6 @@ import Http exposing (get)
localValue = 1 localValue = 1
a : SomeCustomType -> SomeTypeAlias -> SomeOtherTypeAlias -> NonExposedCustomType
a = localValue a = localValue
unknownValue unknownValue
exposedElement exposedElement
@ -57,10 +54,6 @@ a = localValue
|> Review.Test.expectErrors |> Review.Test.expectErrors
[ Review.Test.error [ Review.Test.error
{ message = """ { message = """
<nothing>.SomeCustomType -> <nothing>.SomeCustomType
<nothing>.SomeTypeAlias -> <nothing>.SomeTypeAlias
<nothing>.SomeOtherTypeAlias -> <nothing>.SomeOtherTypeAlias
<nothing>.NonExposedCustomType -> <nothing>.NonExposedCustomType
<nothing>.localValue -> <nothing>.localValue <nothing>.localValue -> <nothing>.localValue
<nothing>.unknownValue -> <nothing>.unknownValue <nothing>.unknownValue -> <nothing>.unknownValue
<nothing>.exposedElement -> <nothing>.exposedElement <nothing>.exposedElement -> <nothing>.exposedElement
@ -84,10 +77,10 @@ Http.get -> Http.get
] ]
realModuleNameTestsForProjectRule : Test forProjectRule : Test
realModuleNameTestsForProjectRule = forProjectRule =
Test.describe "Scope.realModuleName (project rule)" Test.describe "project rule"
[ test "should indicate that module from which a function or value comes from, with knowledge of what is in other modules" <| [ test "should return the module that defined the value" <|
\() -> \() ->
[ """module A exposing (..) [ """module A exposing (..)
import Bar as Baz exposing (baz) import Bar as Baz exposing (baz)
@ -103,7 +96,6 @@ localValue = 1
localValueValueToBeShadowed = 1 localValueValueToBeShadowed = 1
type Msg = SomeMsgToBeShadowed type Msg = SomeMsgToBeShadowed
a : SomeCustomType -> SomeTypeAlias -> SomeOtherTypeAlias -> NonExposedCustomType
a = localValue a = localValue
localValueValueToBeShadowed localValueValueToBeShadowed
SomeMsgToBeShadowed SomeMsgToBeShadowed
@ -111,7 +103,6 @@ a = localValue
Something.b Something.b
Something.c Something.c
Something.BAlias Something.BAlias
Something.Foo
Something.Bar Something.Bar
unknownValue unknownValue
exposedElement exposedElement
@ -151,10 +142,6 @@ c = 1
[ ( "A" [ ( "A"
, [ Review.Test.error , [ Review.Test.error
{ message = """ { message = """
<nothing>.SomeCustomType -> ExposesEverything.SomeCustomType
<nothing>.SomeTypeAlias -> ExposesEverything.SomeTypeAlias
<nothing>.SomeOtherTypeAlias -> ExposesSomeThings.SomeOtherTypeAlias
<nothing>.NonExposedCustomType -> <nothing>.NonExposedCustomType
<nothing>.localValue -> <nothing>.localValue <nothing>.localValue -> <nothing>.localValue
<nothing>.localValueValueToBeShadowed -> <nothing>.localValueValueToBeShadowed <nothing>.localValueValueToBeShadowed -> <nothing>.localValueValueToBeShadowed
<nothing>.SomeMsgToBeShadowed -> <nothing>.SomeMsgToBeShadowed <nothing>.SomeMsgToBeShadowed -> <nothing>.SomeMsgToBeShadowed
@ -162,7 +149,6 @@ c = 1
Something.b -> Something.B.b Something.b -> Something.B.b
Something.c -> Something.C.c Something.c -> Something.C.c
Something.BAlias -> Something.B.BAlias Something.BAlias -> Something.B.BAlias
Something.Foo -> Something.B.Foo
Something.Bar -> Something.B.Bar Something.Bar -> Something.B.Bar
<nothing>.unknownValue -> <nothing>.unknownValue <nothing>.unknownValue -> <nothing>.unknownValue
<nothing>.exposedElement -> ExposesSomeThings.exposedElement <nothing>.exposedElement -> ExposesSomeThings.exposedElement
@ -184,45 +170,13 @@ Http.get -> Http.get
} }
] ]
) )
, ( "ExposesSomeThings"
, [ Review.Test.error
{ message = ""
, details = [ "details" ]
, under = "module"
}
]
)
, ( "ExposesEverything"
, [ Review.Test.error
{ message = ""
, details = [ "details" ]
, under = "module"
}
]
)
, ( "Something.B"
, [ Review.Test.error
{ message = ""
, details = [ "details" ]
, under = "module"
}
]
)
, ( "Something.C"
, [ Review.Test.error
{ message = ""
, details = [ "details" ]
, under = "module"
}
]
)
] ]
] ]
type alias ModuleContext = type alias ModuleContext =
{ scope : Scope.ModuleContext { scope : Scope.ModuleContext
, text : String , texts : List String
} }
@ -242,7 +196,7 @@ projectRule =
{ fromProjectToModule = { fromProjectToModule =
\_ _ projectContext -> \_ _ projectContext ->
{ scope = Scope.fromProjectToModule projectContext.scope { scope = Scope.fromProjectToModule projectContext.scope
, text = "" , texts = []
} }
, fromModuleToProject = , fromModuleToProject =
\_ moduleNameNode moduleContext -> \_ moduleNameNode moduleContext ->
@ -255,7 +209,7 @@ projectRule =
moduleRule : Rule moduleRule : Rule
moduleRule = moduleRule =
Rule.newModuleRuleSchema "TestRule" { scope = Scope.initialModuleContext, text = "" } Rule.newModuleRuleSchema "TestRule" { scope = Scope.initialModuleContext, texts = [] }
|> Scope.addModuleVisitors |> Scope.addModuleVisitors
|> moduleVisitor |> moduleVisitor
|> Rule.fromModuleRuleSchema |> Rule.fromModuleRuleSchema
@ -264,71 +218,10 @@ moduleRule =
moduleVisitor : Rule.ModuleRuleSchema schemaState ModuleContext -> Rule.ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } ModuleContext moduleVisitor : Rule.ModuleRuleSchema schemaState ModuleContext -> Rule.ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } ModuleContext
moduleVisitor schema = moduleVisitor schema =
schema schema
|> Rule.withDeclarationVisitor declarationVisitor
|> Rule.withExpressionVisitor expressionVisitor |> Rule.withExpressionVisitor expressionVisitor
|> Rule.withFinalModuleEvaluation finalEvaluation |> Rule.withFinalModuleEvaluation finalEvaluation
declarationVisitor : Node Declaration -> Rule.Direction -> ModuleContext -> ( List nothing, ModuleContext )
declarationVisitor node direction context =
case ( direction, Node.value node ) of
( Rule.OnEnter, Declaration.FunctionDeclaration function ) ->
case function.signature |> Maybe.map (Node.value >> .typeAnnotation) of
Nothing ->
( [], context )
Just typeAnnotation ->
( [], { context | text = context.text ++ "\n" ++ typeAnnotationNames context.scope typeAnnotation } )
_ ->
( [], context )
typeAnnotationNames : Scope.ModuleContext -> Node TypeAnnotation -> String
typeAnnotationNames scope typeAnnotation =
case Node.value typeAnnotation of
TypeAnnotation.GenericType name ->
"<nothing>." ++ name ++ " -> <generic>"
TypeAnnotation.Typed (Node _ ( moduleName, typeName )) typeParameters ->
-- Elm.Type.Type (String.join "." moduleName ++ "." ++ typeName) (List.map syntaxTypeAnnotationToDocsType typeParameters)
let
nameInCode : String
nameInCode =
case moduleName of
[] ->
"<nothing>." ++ typeName
_ ->
String.join "." moduleName ++ "." ++ typeName
realName : String
realName =
case Scope.realModuleName scope typeName moduleName of
[] ->
"<nothing>." ++ typeName
moduleName_ ->
String.join "." moduleName_ ++ "." ++ typeName
in
nameInCode ++ " -> " ++ realName
TypeAnnotation.Unit ->
"unknown"
TypeAnnotation.Tupled typeAnnotationTypeAnnotationSyntaxElmNodeNodeSyntaxElmListList ->
"unknown"
TypeAnnotation.Record recordDefinitionTypeAnnotationSyntaxElm ->
"unknown"
TypeAnnotation.GenericRecord stringStringNodeNodeSyntaxElm recordDefinitionTypeAnnotationSyntaxElmNodeNodeSyntaxElm ->
"unknown"
TypeAnnotation.FunctionTypeAnnotation arg returnType ->
typeAnnotationNames scope arg ++ "\n" ++ typeAnnotationNames scope returnType
expressionVisitor : Node Expression -> Rule.Direction -> ModuleContext -> ( List nothing, ModuleContext ) expressionVisitor : Node Expression -> Rule.Direction -> ModuleContext -> ( List nothing, ModuleContext )
expressionVisitor node direction context = expressionVisitor node direction context =
case ( direction, Node.value node ) of case ( direction, Node.value node ) of
@ -345,14 +238,14 @@ expressionVisitor node direction context =
realName : String realName : String
realName = realName =
case Scope.realModuleName context.scope name moduleName of case Scope.moduleNameForValue context.scope name moduleName of
[] -> [] ->
"<nothing>." ++ name "<nothing>." ++ name
moduleName_ -> moduleName_ ->
String.join "." moduleName_ ++ "." ++ name String.join "." moduleName_ ++ "." ++ name
in in
( [], { context | text = context.text ++ "\n" ++ nameInCode ++ " -> " ++ realName } ) ( [], { context | texts = context.texts ++ [ nameInCode ++ " -> " ++ realName ] } )
_ -> _ ->
( [], context ) ( [], context )
@ -360,8 +253,15 @@ expressionVisitor node direction context =
finalEvaluation : ModuleContext -> List (Error {}) finalEvaluation : ModuleContext -> List (Error {})
finalEvaluation context = finalEvaluation context =
[ Rule.error { message = context.text, details = [ "details" ] } if List.isEmpty context.texts then
{ start = { row = 1, column = 1 } []
, end = { row = 1, column = 7 }
} else
] [ Rule.error
{ message = "\n" ++ String.join "\n" context.texts
, details = [ "details" ]
}
{ start = { row = 1, column = 1 }
, end = { row = 1, column = 7 }
}
]

View File

@ -465,7 +465,7 @@ registerUsedFunctionOrValue moduleName name moduleContext =
let let
realModuleName : ModuleName realModuleName : ModuleName
realModuleName = realModuleName =
Scope.realModuleName moduleContext.scope name moduleName Scope.moduleNameForValue moduleContext.scope name moduleName
in in
{ moduleContext { moduleContext
| usedFunctionsOrValues = | usedFunctionsOrValues =
@ -614,7 +614,7 @@ collectTypesUsedAsPhantomVariables scope phantomVariables node =
let let
realModuleNameOfPhantomContainer : ModuleName realModuleNameOfPhantomContainer : ModuleName
realModuleNameOfPhantomContainer = realModuleNameOfPhantomContainer =
Scope.realModuleName scope name moduleNameOfPhantomContainer Scope.moduleNameForType scope name moduleNameOfPhantomContainer
typesUsedInThePhantomVariablePosition : List ( ModuleName, CustomTypeName ) typesUsedInThePhantomVariablePosition : List ( ModuleName, CustomTypeName )
typesUsedInThePhantomVariablePosition = typesUsedInThePhantomVariablePosition =
@ -625,7 +625,7 @@ collectTypesUsedAsPhantomVariables scope phantomVariables node =
(\( _, index ) -> (\( _, index ) ->
case listAtIndex index params |> Maybe.map Node.value of case listAtIndex index params |> Maybe.map Node.value of
Just (TypeAnnotation.Typed (Node.Node _ ( moduleNameOfPhantomVariable, typeName )) _) -> Just (TypeAnnotation.Typed (Node.Node _ ( moduleNameOfPhantomVariable, typeName )) _) ->
Just ( Scope.realModuleName scope typeName moduleNameOfPhantomVariable, typeName ) Just ( Scope.moduleNameForType scope typeName moduleNameOfPhantomVariable, typeName )
_ -> _ ->
Nothing Nothing

View File

@ -559,4 +559,20 @@ type Msg = NoOp
] ]
) )
] ]
, test "should not report type constructors in confusing situations" <|
\() ->
[ """module A exposing (A(..))
type A = Foo | Bar
"""
, """module B exposing (B(..))
type B = A | B
"""
, """module C exposing (foo)
import A exposing (A(..))
import B exposing (B(..))
foo = [ (Foo, A), (Bar, B) ]
"""
]
|> Review.Test.runOnModulesWithProjectData project (rule [])
|> Review.Test.expectNoErrors
] ]

View File

@ -404,7 +404,7 @@ testFunctionName scope declaration =
|> Maybe.map (Node.value >> .typeAnnotation >> Node.value) |> Maybe.map (Node.value >> .typeAnnotation >> Node.value)
of of
Just (TypeAnnotation.Typed (Node _ ( moduleName, name )) _) -> Just (TypeAnnotation.Typed (Node _ ( moduleName, name )) _) ->
if name == "Test" && Scope.realModuleName scope name moduleName == [ "Test" ] then if name == "Test" && Scope.moduleNameForType scope name moduleName == [ "Test" ] then
function.declaration function.declaration
|> Node.value |> Node.value
|> .name |> .name
@ -464,7 +464,7 @@ collectTypesFromTypeAnnotation scope node =
collectTypesFromTypeAnnotation scope a ++ collectTypesFromTypeAnnotation scope b collectTypesFromTypeAnnotation scope a ++ collectTypesFromTypeAnnotation scope b
TypeAnnotation.Typed (Node _ ( moduleName, name )) params -> TypeAnnotation.Typed (Node _ ( moduleName, name )) params ->
( Scope.realModuleName scope name moduleName, name ) ( Scope.moduleNameForType scope name moduleName, name )
:: List.concatMap (collectTypesFromTypeAnnotation scope) params :: List.concatMap (collectTypesFromTypeAnnotation scope) params
TypeAnnotation.Record list -> TypeAnnotation.Record list ->
@ -498,7 +498,7 @@ expressionVisitor node direction moduleContext =
( Rule.OnEnter, Expression.FunctionOrValue moduleName name ) -> ( Rule.OnEnter, Expression.FunctionOrValue moduleName name ) ->
( [] ( []
, registerAsUsed , registerAsUsed
( Scope.realModuleName moduleContext.scope name moduleName, name ) ( Scope.moduleNameForValue moduleContext.scope name moduleName, name )
moduleContext moduleContext
) )

View File

@ -240,6 +240,19 @@ config = []
""" """
|> Review.Test.runWithProjectData package_ rule |> Review.Test.runWithProjectData package_ rule
|> Review.Test.expectNoErrors |> Review.Test.expectNoErrors
, test "should not report a function in a 'shadowed' module" <|
\() ->
[ """module Foo exposing (foo)
foo = 1
""", """module Bar exposing (bar)
bar = 2
""", """module Main exposing (main)
import Bar as Foo
import Foo
main = [ Foo.foo, Foo.bar ]
""" ]
|> Review.Test.runOnModulesWithProjectData application rule
|> Review.Test.expectNoErrors
] ]

View File

@ -2,7 +2,7 @@ module Scope exposing
( ModuleContext, addModuleVisitors, initialModuleContext ( ModuleContext, addModuleVisitors, initialModuleContext
, ProjectContext, addProjectVisitors , ProjectContext, addProjectVisitors
, initialProjectContext, fromProjectToModule, fromModuleToProject, foldProjectContexts , initialProjectContext, fromProjectToModule, fromModuleToProject, foldProjectContexts
, realModuleName , moduleNameForValue, moduleNameForType
) )
{-| Collect and infer information automatically for you {-| Collect and infer information automatically for you
@ -21,13 +21,13 @@ module Scope exposing
# Access # Access
@docs realModuleName @docs moduleNameForValue, moduleNameForType
-} -}
{- Copied over from https://github.com/jfmengels/elm-review-scope {- Copied over from https://github.com/jfmengels/elm-review-scope
Version: 0.1.1 Version: 0.2.0
Copyright (c) 2020, Jeroen Engels Copyright (c) 2020, Jeroen Engels
All rights reserved. All rights reserved.
@ -70,10 +70,13 @@ import Elm.Syntax.Node as Node exposing (Node(..))
import Elm.Syntax.Pattern as Pattern exposing (Pattern) import Elm.Syntax.Pattern as Pattern exposing (Pattern)
import Elm.Syntax.Range as Range exposing (Range) import Elm.Syntax.Range as Range exposing (Range)
import Elm.Syntax.Signature exposing (Signature) import Elm.Syntax.Signature exposing (Signature)
import Elm.Syntax.Type
import Elm.Syntax.TypeAlias
import Elm.Syntax.TypeAnnotation as TypeAnnotation exposing (TypeAnnotation) import Elm.Syntax.TypeAnnotation as TypeAnnotation exposing (TypeAnnotation)
import Elm.Type import Elm.Type
import Review.Project.Dependency as Dependency exposing (Dependency) import Review.Project.Dependency as Dependency exposing (Dependency)
import Review.Rule as Rule exposing (Direction) import Review.Rule as Rule exposing (Direction)
import Set exposing (Set)
@ -88,8 +91,10 @@ type ModuleContext
type alias InnerModuleContext = type alias InnerModuleContext =
{ scopes : Nonempty Scope { scopes : Nonempty Scope
, localTypes : Set String
, importAliases : Dict String (List ModuleName) , importAliases : Dict String (List ModuleName)
, importedFunctionOrTypes : Dict String (List String) , importedFunctions : Dict String (List String)
, importedTypes : Dict String (List String)
, dependenciesModules : Dict String Elm.Docs.Module , dependenciesModules : Dict String Elm.Docs.Module
, modules : Dict ModuleName Elm.Docs.Module , modules : Dict ModuleName Elm.Docs.Module
, exposesEverything : Bool , exposesEverything : Bool
@ -189,8 +194,10 @@ initialProjectContext =
fromProjectToModule : ProjectContext -> ModuleContext fromProjectToModule : ProjectContext -> ModuleContext
fromProjectToModule (ProjectContext projectContext) = fromProjectToModule (ProjectContext projectContext) =
{ scopes = nonemptyList_fromElement emptyScope { scopes = nonemptyList_fromElement emptyScope
, localTypes = Set.empty
, importAliases = Dict.empty , importAliases = Dict.empty
, importedFunctionOrTypes = Dict.empty , importedFunctions = Dict.empty
, importedTypes = Dict.empty
, dependenciesModules = projectContext.dependenciesModules , dependenciesModules = projectContext.dependenciesModules
, modules = projectContext.modules , modules = projectContext.modules
, exposesEverything = False , exposesEverything = False
@ -583,135 +590,140 @@ createFakeImport { moduleName, moduleAlias, exposingList } =
declarationListVisitor : List (Node Declaration) -> InnerModuleContext -> InnerModuleContext declarationListVisitor : List (Node Declaration) -> InnerModuleContext -> InnerModuleContext
declarationListVisitor declarations innerContext = declarationListVisitor declarations innerContext =
List.foldl registerDeclaration innerContext declarations List.foldl registerDeclaration innerContext declarations
|> (\newInnerContext -> List.foldl registerExposed newInnerContext declarations)
registerDeclaration : Node Declaration -> InnerModuleContext -> InnerModuleContext registerDeclaration : Node Declaration -> InnerModuleContext -> InnerModuleContext
registerDeclaration declaration innerContext = registerDeclaration declaration innerContext =
let
scope : Nonempty Scope
scope =
List.foldl
(\( variableType, nameNode ) accumulatorScope ->
registerVariable
{ variableType = variableType
, node = nameNode
}
(Node.value nameNode)
accumulatorScope
)
innerContext.scopes
(declarationNameNode declaration)
in
updateScope innerContext scope
declarationNameNode : Node Declaration -> List ( VariableType, Node String )
declarationNameNode (Node _ declaration) =
case declaration of
Declaration.FunctionDeclaration function ->
[ ( TopLevelVariable
, function.declaration
|> Node.value
|> .name
)
]
Declaration.CustomTypeDeclaration { name, constructors } ->
( TopLevelVariable, name )
:: List.map
(\constructor ->
( CustomTypeConstructor
, constructor
|> Node.value
|> .name
)
)
constructors
Declaration.AliasDeclaration alias_ ->
[ ( TopLevelVariable, alias_.name ) ]
Declaration.PortDeclaration port_ ->
[ ( Port, port_.name ) ]
Declaration.InfixDeclaration _ ->
[]
Declaration.Destructuring _ _ ->
[]
registerExposed : Node Declaration -> InnerModuleContext -> InnerModuleContext
registerExposed declaration innerContext =
case Node.value declaration of case Node.value declaration of
Declaration.FunctionDeclaration function -> Declaration.FunctionDeclaration function ->
let let
name : String nameNode : Node String
name = nameNode =
function.declaration function.declaration
|> Node.value |> Node.value
|> .name |> .name
|> Node.value
in in
if innerContext.exposesEverything || Dict.member name innerContext.exposedNames then innerContext
{ innerContext |> addToScope
| exposedValues = { variableType = TopLevelVariable
{ name = name , node = nameNode
, comment = "" }
, tipe = convertTypeSignatureToDocsType function.signature |> registerIfExposed (registerExposedValue function) (Node.value nameNode)
}
:: innerContext.exposedValues
}
else
innerContext
Declaration.CustomTypeDeclaration type_ ->
if innerContext.exposesEverything || Dict.member (Node.value type_.name) innerContext.exposedNames then
{ innerContext
| exposedUnions =
{ name = Node.value type_.name
, comment = ""
-- TODO
, args = []
, tags =
type_.constructors
-- TODO Constructor args?
|> List.map (\constructor -> ( Node.value (Node.value constructor).name, [] ))
}
:: innerContext.exposedUnions
}
else
innerContext
Declaration.AliasDeclaration alias_ -> Declaration.AliasDeclaration alias_ ->
if innerContext.exposesEverything || Dict.member (Node.value alias_.name) innerContext.exposedNames then { innerContext | localTypes = Set.insert (Node.value alias_.name) innerContext.localTypes }
{ innerContext |> addToScope
| exposedAliases = { variableType = TopLevelVariable
{ name = Node.value alias_.name , node = alias_.name
, comment = "" }
, args = [] |> registerIfExposed (registerExposedTypeAlias alias_) (Node.value alias_.name)
, tipe = Elm.Type.Tuple []
}
:: innerContext.exposedAliases
}
else Declaration.CustomTypeDeclaration { name, constructors } ->
List.foldl
(\constructor innerContext_ ->
let
constructorName : Node String
constructorName =
constructor |> Node.value |> .name
in
addToScope
{ variableType = CustomTypeConstructor
, node = constructorName
}
innerContext_
)
{ innerContext | localTypes = Set.insert (Node.value name) innerContext.localTypes }
constructors
|> registerIfExposed (registerExposedCustomType constructors) (Node.value name)
Declaration.PortDeclaration signature ->
addToScope
{ variableType = Port
, node = signature.name
}
innerContext innerContext
Declaration.PortDeclaration _ ->
innerContext
Declaration.InfixDeclaration _ -> Declaration.InfixDeclaration _ ->
-- TODO Support operators
-- I could use help adding this.
innerContext innerContext
Declaration.Destructuring _ _ -> Declaration.Destructuring _ _ ->
-- Not possible in 0.19 code
innerContext innerContext
addToScope : { variableType : VariableType, node : Node String } -> InnerModuleContext -> InnerModuleContext
addToScope variableData innerContext =
let
newScopes : Nonempty Scope
newScopes =
registerVariable
variableData
(Node.value variableData.node)
innerContext.scopes
in
{ innerContext | scopes = newScopes }
registerExposedValue : Expression.Function -> String -> InnerModuleContext -> InnerModuleContext
registerExposedValue function name innerContext =
{ innerContext
| exposedValues =
{ name = name
, comment =
case Maybe.map Node.value function.documentation of
Just str ->
str
Nothing ->
""
, tipe = convertTypeSignatureToDocsType function.signature
}
:: innerContext.exposedValues
}
registerExposedCustomType : List (Node Elm.Syntax.Type.ValueConstructor) -> String -> InnerModuleContext -> InnerModuleContext
registerExposedCustomType constructors name innerContext =
{ innerContext
| exposedUnions =
{ name = name
, comment = ""
-- TODO
, args = []
, tags =
constructors
-- TODO Constructor args?
|> List.map (\constructor -> ( Node.value (Node.value constructor).name, [] ))
}
:: innerContext.exposedUnions
}
registerExposedTypeAlias : Elm.Syntax.TypeAlias.TypeAlias -> String -> InnerModuleContext -> InnerModuleContext
registerExposedTypeAlias alias_ name innerContext =
{ innerContext
| exposedAliases =
{ name = name
, comment = ""
, args = []
, tipe = Elm.Type.Tuple []
}
:: innerContext.exposedAliases
}
registerIfExposed : (String -> InnerModuleContext -> InnerModuleContext) -> String -> InnerModuleContext -> InnerModuleContext
registerIfExposed registerFn name innerContext =
if innerContext.exposesEverything || Dict.member name innerContext.exposedNames then
registerFn name innerContext
else
innerContext
convertTypeSignatureToDocsType : Maybe (Node Signature) -> Elm.Type.Type convertTypeSignatureToDocsType : Maybe (Node Signature) -> Elm.Type.Type
convertTypeSignatureToDocsType maybeSignature = convertTypeSignatureToDocsType maybeSignature =
case maybeSignature |> Maybe.map (Node.value >> .typeAnnotation) of case maybeSignature |> Maybe.map (Node.value >> .typeAnnotation) of
@ -756,8 +768,8 @@ registerVariable variableInfo name scopes =
updateScope : InnerModuleContext -> Nonempty Scope -> InnerModuleContext updateScope : InnerModuleContext -> Nonempty Scope -> InnerModuleContext
updateScope context scopes = updateScope innerContext scopes =
{ context | scopes = scopes } { innerContext | scopes = scopes }
@ -810,7 +822,23 @@ registerImportAlias : Import -> InnerModuleContext -> InnerModuleContext
registerImportAlias import_ innerContext = registerImportAlias import_ innerContext =
case import_.moduleAlias of case import_.moduleAlias of
Nothing -> Nothing ->
innerContext let
moduleName : List String
moduleName =
Node.value import_.moduleName
in
case moduleName of
singleSegmentModuleName :: [] ->
{ innerContext
| importAliases =
Dict.update
singleSegmentModuleName
(\previousValue -> Just <| moduleName :: Maybe.withDefault [] previousValue)
innerContext.importAliases
}
_ ->
innerContext
Just alias_ -> Just alias_ ->
{ innerContext { innerContext
@ -864,8 +892,7 @@ registerImportExposed import_ innerContext =
List.concat List.concat
[ List.concatMap [ List.concatMap
(\union -> (\union ->
nameWithModuleName union List.map (\( name, _ ) -> ( name, moduleName )) union.tags
:: List.map (\( name, _ ) -> ( name, moduleName )) union.tags
) )
module_.unions module_.unions
, List.map nameWithModuleName module_.values , List.map nameWithModuleName module_.values
@ -873,10 +900,18 @@ registerImportExposed import_ innerContext =
, List.map nameWithModuleName module_.binops , List.map nameWithModuleName module_.binops
] ]
|> Dict.fromList |> Dict.fromList
exposedTypes : Dict String (List String)
exposedTypes =
List.concat
[ List.map nameWithModuleName module_.unions
, List.map nameWithModuleName module_.aliases
]
|> Dict.fromList
in in
{ innerContext { innerContext
| importedFunctionOrTypes = | importedFunctions = Dict.union innerContext.importedFunctions exposedValues
Dict.union innerContext.importedFunctionOrTypes exposedValues , importedTypes = Dict.union innerContext.importedTypes exposedTypes
} }
Exposing.Explicit topLevelExposeList -> Exposing.Explicit topLevelExposeList ->
@ -884,18 +919,25 @@ registerImportExposed import_ innerContext =
exposedValues : Dict String (List String) exposedValues : Dict String (List String)
exposedValues = exposedValues =
topLevelExposeList topLevelExposeList
|> List.concatMap (namesFromExposingList module_) |> List.concatMap (valuesFromExposingList module_)
|> List.map (\name -> ( name, moduleName ))
|> Dict.fromList
exposedTypes : Dict String (List String)
exposedTypes =
topLevelExposeList
|> List.filterMap typesFromExposingList
|> List.map (\name -> ( name, moduleName )) |> List.map (\name -> ( name, moduleName ))
|> Dict.fromList |> Dict.fromList
in in
{ innerContext { innerContext
| importedFunctionOrTypes = | importedFunctions = Dict.union innerContext.importedFunctions exposedValues
Dict.union innerContext.importedFunctionOrTypes exposedValues , importedTypes = Dict.union innerContext.importedTypes exposedTypes
} }
namesFromExposingList : Elm.Docs.Module -> Node TopLevelExpose -> List String valuesFromExposingList : Elm.Docs.Module -> Node TopLevelExpose -> List String
namesFromExposingList module_ topLevelExpose = valuesFromExposingList module_ topLevelExpose =
case Node.value topLevelExpose of case Node.value topLevelExpose of
Exposing.InfixExpose operator -> Exposing.InfixExpose operator ->
[ operator ] [ operator ]
@ -903,21 +945,40 @@ namesFromExposingList module_ topLevelExpose =
Exposing.FunctionExpose function -> Exposing.FunctionExpose function ->
[ function ] [ function ]
Exposing.TypeOrAliasExpose type_ -> Exposing.TypeOrAliasExpose name ->
[ type_ ] if List.any (\alias_ -> alias_.name == name) module_.aliases then
[ name ]
else
-- Type is a custom type
[]
Exposing.TypeExpose { name, open } -> Exposing.TypeExpose { name, open } ->
case open of case open of
Just _ -> Just _ ->
name module_.unions
:: (module_.unions |> List.filter (\union -> union.name == name)
|> List.filter (\union -> union.name == name) |> List.concatMap .tags
|> List.concatMap .tags |> List.map Tuple.first
|> List.map Tuple.first
)
Nothing -> Nothing ->
[ name ] []
typesFromExposingList : Node TopLevelExpose -> Maybe String
typesFromExposingList topLevelExpose =
case Node.value topLevelExpose of
Exposing.InfixExpose _ ->
Nothing
Exposing.FunctionExpose _ ->
Nothing
Exposing.TypeOrAliasExpose name ->
Just name
Exposing.TypeExpose { name } ->
Just name
unboxProjectContext : ProjectContext -> InnerProjectContext unboxProjectContext : ProjectContext -> InnerProjectContext
@ -1029,7 +1090,7 @@ collectNamesFromPattern pattern =
popScope : Node Expression -> Direction -> InnerModuleContext -> InnerModuleContext popScope : Node Expression -> Direction -> InnerModuleContext -> InnerModuleContext
popScope ((Node range value) as node) direction context = popScope node direction context =
let let
currentScope : Scope currentScope : Scope
currentScope = currentScope =
@ -1136,10 +1197,11 @@ findInList predicate list =
-- ACCESS -- ACCESS
{-| Get the name of the module where a function or type was defined. {-| Get the name of the module where a value was defined.
A value can be either a function, a constant, a custom type constructor or a type alias (used as a function).
- The second argument (`String`) is the function name - The second argument (`String`) is the name of the value
- The third argument (`List String`) is the module name that was used next to the function name where you found it - The third argument (`List String`) is the module name that was used next to the value's name where you found it
If the element was defined in the current module, then the result will be `[]`. If the element was defined in the current module, then the result will be `[]`.
@ -1147,7 +1209,7 @@ If the element was defined in the current module, then the result will be `[]`.
expressionVisitor node direction context = expressionVisitor node direction context =
case ( direction, Node.value node ) of case ( direction, Node.value node ) of
( Rule.OnEnter, Expression.FunctionOrValue moduleName "button" ) -> ( Rule.OnEnter, Expression.FunctionOrValue moduleName "button" ) ->
if Scope.realModuleName context.scope "button" moduleName == [ "Html" ] then if Scope.moduleNameForValue context.scope "button" moduleName == [ "Html" ] then
( [ createError node ], context ) ( [ createError node ], context )
else else
@ -1156,23 +1218,16 @@ If the element was defined in the current module, then the result will be `[]`.
_ -> _ ->
( [], context ) ( [], context )
**Known problems**:
- Does not make the distinction between types and functions/values/custom type constructors.
This will likely warrant splitting this function into two later on.
Help is welcome!
-} -}
realModuleName : ModuleContext -> String -> List String -> List String moduleNameForValue : ModuleContext -> String -> List String -> List String
realModuleName (ModuleContext context) functionOrType moduleName = moduleNameForValue (ModuleContext context) valueName moduleName =
case moduleName of case moduleName of
[] -> [] ->
if isInScope functionOrType context.scopes then if isInScope valueName context.scopes then
[] []
else else
Dict.get functionOrType context.importedFunctionOrTypes Dict.get valueName context.importedFunctions
|> Maybe.withDefault [] |> Maybe.withDefault []
_ :: [] -> _ :: [] ->
@ -1186,7 +1241,7 @@ realModuleName (ModuleContext context) functionOrType moduleName =
(\aliasedModuleName -> (\aliasedModuleName ->
case Dict.get aliasedModuleName context.modules of case Dict.get aliasedModuleName context.modules of
Just module_ -> Just module_ ->
isDeclaredInModule functionOrType module_ isValueDeclaredInModule valueName module_
Nothing -> Nothing ->
False False
@ -1207,18 +1262,71 @@ realModuleName (ModuleContext context) functionOrType moduleName =
moduleName moduleName
isDeclaredInModule : String -> Elm.Docs.Module -> Bool {-| Get the name of the module where a type was defined.
isDeclaredInModule functionOrType module_ = A type can be either a custom type or a type alias.
List.any (.name >> (==) functionOrType) module_.values
|| List.any (.name >> (==) functionOrType) module_.aliases - The second argument (`String`) is the name of the type
- The third argument (`List String`) is the module name that was used next to the type name where you found it
-}
moduleNameForType : ModuleContext -> String -> List String -> List String
moduleNameForType (ModuleContext context) typeName moduleName =
case moduleName of
[] ->
if Set.member typeName context.localTypes then
[]
else
Dict.get typeName context.importedTypes
|> Maybe.withDefault []
_ :: [] ->
case Dict.get (getModuleName moduleName) context.importAliases of
Just [ aliasedModuleName ] ->
aliasedModuleName
Just aliases ->
case
findInList
(\aliasedModuleName ->
case Dict.get aliasedModuleName context.modules of
Just module_ ->
isTypeDeclaredInModule typeName module_
Nothing ->
False
)
aliases
of
Just aliasedModuleName ->
aliasedModuleName
Nothing ->
List.head aliases
|> Maybe.withDefault moduleName
Nothing ->
moduleName
_ ->
moduleName
isValueDeclaredInModule : String -> Elm.Docs.Module -> Bool
isValueDeclaredInModule valueName module_ =
List.any (.name >> (==) valueName) module_.values
|| List.any (.name >> (==) valueName) module_.aliases
|| List.any || List.any
(\union -> (\union -> List.any (Tuple.first >> (==) valueName) union.tags)
(union.name == functionOrType)
|| List.any (Tuple.first >> (==) functionOrType) union.tags
)
module_.unions module_.unions
isTypeDeclaredInModule : String -> Elm.Docs.Module -> Bool
isTypeDeclaredInModule typeName module_ =
List.any (.name >> (==) typeName) module_.aliases
|| List.any (.name >> (==) typeName) module_.unions
isInScope : String -> Nonempty Scope -> Bool isInScope : String -> Nonempty Scope -> Bool
isInScope name scopes = isInScope name scopes =
nonemptyList_any (.names >> Dict.member name) scopes nonemptyList_any (.names >> Dict.member name) scopes