mirror of
https://github.com/jfmengels/elm-review.git
synced 2024-12-26 03:04:48 +03:00
Rework the documentation for Lint.Rule
This commit is contained in:
parent
854e0412db
commit
b49df6c83e
@ -1,13 +1,17 @@
|
||||
module Lint.Rule exposing
|
||||
( Rule, Schema
|
||||
, newSchema, fromSchema
|
||||
, withSimpleModuleDefinitionVisitor, withSimpleImportVisitor, withSimpleExpressionVisitor, withSimpleDeclarationVisitor
|
||||
, withInitialContext, withModuleDefinitionVisitor, withImportVisitor, withExpressionVisitor, withDeclarationVisitor, withFinalEvaluation
|
||||
, withSimpleModuleDefinitionVisitor, withSimpleImportVisitor, withSimpleDeclarationVisitor, withSimpleExpressionVisitor
|
||||
, withInitialContext, withModuleDefinitionVisitor, withImportVisitor, withDeclarationVisitor, withExpressionVisitor, withFinalEvaluation
|
||||
, name, analyzer
|
||||
)
|
||||
|
||||
{-| This module contains functions that are used for writing rules.
|
||||
|
||||
TODO Explain how traversal works, and what an AST is.
|
||||
TODO Explain the order of traversal: moduleDefinition -> imports -> declarations + expressions -> final evaluation
|
||||
TODO Explain that and why people need to look at the documentation for elm-syntax.
|
||||
|
||||
|
||||
# Definition
|
||||
|
||||
@ -17,8 +21,8 @@ module Lint.Rule exposing
|
||||
# Writing rules
|
||||
|
||||
@docs newSchema, fromSchema
|
||||
@docs withSimpleModuleDefinitionVisitor, withSimpleImportVisitor, withSimpleExpressionVisitor, withSimpleDeclarationVisitor
|
||||
@docs withInitialContext, withModuleDefinitionVisitor, withImportVisitor, withExpressionVisitor, withDeclarationVisitor, withFinalEvaluation
|
||||
@docs withSimpleModuleDefinitionVisitor, withSimpleImportVisitor, withSimpleDeclarationVisitor, withSimpleExpressionVisitor
|
||||
@docs withInitialContext, withModuleDefinitionVisitor, withImportVisitor, withDeclarationVisitor, withExpressionVisitor, withFinalEvaluation
|
||||
|
||||
|
||||
# ACCESS
|
||||
@ -39,7 +43,8 @@ import Lint.Internal.Accumulate exposing (accumulateList)
|
||||
import Lint.Internal.DeclarationVisitor as DeclarationVisitor
|
||||
|
||||
|
||||
{-| Shortcut to a lint rule
|
||||
{-| Represents a construct able to analyze a `File` and report unwanted patterns.
|
||||
See [`newSchema`](#newSchema), and [`fromSchema`](#fromSchema) for how to create one.
|
||||
-}
|
||||
type Rule
|
||||
= Rule
|
||||
@ -48,15 +53,15 @@ type Rule
|
||||
}
|
||||
|
||||
|
||||
{-| Represents a `rule` that will be enforced.
|
||||
{-| Represents a Schema for a [`Rule`](#Rule). Create one using [`newSchema`](#newSchema).
|
||||
|
||||
import Lint.Rule as Rule
|
||||
import Lint.Rule as Rule exposing (Rule)
|
||||
|
||||
rule : Rule
|
||||
rule =
|
||||
Rule.newSchema "NoDebug"
|
||||
|> Rule.withExpressionVisitor expressionVisitor
|
||||
|> Rule.fromSchema
|
||||
rule : Rule
|
||||
rule =
|
||||
Rule.newSchema "NoDebug"
|
||||
|> Rule.withExpressionVisitor expressionVisitor
|
||||
|> Rule.fromSchema
|
||||
|
||||
-}
|
||||
type Schema context
|
||||
@ -71,15 +76,34 @@ type Schema context
|
||||
}
|
||||
|
||||
|
||||
{-| Creates a new schema for a rule. Will require calling `fromSchema` to create a usable `Rule`.
|
||||
{-| Creates a new schema for a rule. Will require calling [`fromSchema`](#fromSchema)
|
||||
to create a usable [`Rule`](#Rule). Use "with\*" functions from this module, like
|
||||
[`withSimpleExpressionVisitor`](#withSimpleExpressionVisitor) or [`withSimpleImportVisitor`](#withSimpleImportVisitor)
|
||||
to make it report something.
|
||||
|
||||
import Lint.Rule as Rule
|
||||
import Lint.Rule as Rule exposing (Rule)
|
||||
|
||||
rule : Rule
|
||||
rule =
|
||||
|> Rule.withExpressionVisitor expressionVisitor
|
||||
Rule.newSchema "NoDebug"
|
||||
|> Rule.fromSchema
|
||||
rule : Rule
|
||||
rule =
|
||||
Rule.newSchema "NoDebug"
|
||||
|> Rule.withSimpleExpressionVisitor expressionVisitor
|
||||
|> Rule.withSimpleImportVisitor importVisitor
|
||||
|> Rule.fromSchema
|
||||
|
||||
If you wish to build a [`Rule`](#Rule) that collects data as the file gets traversed,
|
||||
take a look at [`withInitialContext`](#withInitialContext) and "with\*" functions without
|
||||
"Simple" in their name, like [`withExpressionVisitor`](#withExpressionVisitor),
|
||||
[`withImportVisitor`](#withImportVisitor) or [`withFinalEvaluation`](#withFinalEvaluation).
|
||||
|
||||
import Lint.Rule as Rule exposing (Rule)
|
||||
|
||||
rule : Rule
|
||||
rule =
|
||||
Rule.newSchema "NoUnusedVariables"
|
||||
|> Rule.withInitialContext { declaredVariables = [], usedVariables = [] }
|
||||
|> Rule.withExpressionVisitor expressionVisitor
|
||||
|> Rule.withImportVisitor importVisitor
|
||||
|> Rule.fromSchema
|
||||
|
||||
-}
|
||||
newSchema : String -> Schema ()
|
||||
@ -95,6 +119,8 @@ newSchema name_ =
|
||||
}
|
||||
|
||||
|
||||
{-| Create a [`Rule`](#Rule) from a configured [`Schema`](#Schema).
|
||||
-}
|
||||
fromSchema : Schema context -> Rule
|
||||
fromSchema (Schema schema) =
|
||||
Rule
|
||||
@ -118,48 +144,122 @@ makeFinalEvaluation finalEvaluationFn ( previousErrors, previousContext ) =
|
||||
++ previousErrors
|
||||
|
||||
|
||||
{-| Add a visitor to the [`Schema`](#Schema) which will visit the `File`'s [module definition](https://package.elm-lang.org/packages/stil4m/elm-syntax/latest/Elm-Syntax-Module) (`module SomeModuleName exposing (a, b)`) and report patterns.
|
||||
|
||||
-- RULES WITH ANALYSIS
|
||||
The following example forbids having `_` in any part of a module name.
|
||||
|
||||
import Elm.Syntax.Module as Module exposing (Module)
|
||||
import Elm.Syntax.Node as Node exposing (Node)
|
||||
import Lint.Error as Error exposing (Error)
|
||||
import Lint.Rule as Rule exposing (Rule)
|
||||
|
||||
withInitialContext : context -> Schema () -> Schema context
|
||||
withInitialContext initialContext_ (Schema schema) =
|
||||
Schema
|
||||
{ name = schema.name
|
||||
, initialContext = initialContext_
|
||||
, moduleDefinitionVisitor = \node context -> ( [], context )
|
||||
, importVisitor = \node context -> ( [], context )
|
||||
, expressionVisitor = \node direction context -> ( [], context )
|
||||
, declarationVisitor = \node direction context -> ( [], context )
|
||||
, finalEvaluationFn = \context -> []
|
||||
}
|
||||
rule : Rule
|
||||
rule =
|
||||
Rule.newSchema "NoUnderscoreInModuleName"
|
||||
|> Rule.withSimpleModuleDefinitionVisitor moduleDefinitionVisitor
|
||||
|> Rule.fromSchema
|
||||
|
||||
moduleDefinitionVisitor : Node Module -> List Error
|
||||
moduleDefinitionVisitor node =
|
||||
if List.any (String.contains "") (Node.value node |> Module.moduleName) then
|
||||
[ Error.create "Do not use `_` in a module name" (Node.range node) ]
|
||||
|
||||
else
|
||||
[]
|
||||
|
||||
Note: `withSimpleModuleDefinitionVisitor` is a simplified version of [`withModuleDefinitionVisitor`](#withModuleDefinitionVisitor),
|
||||
which isn't passed a `context` and doesn't return one. You can use `withSimpleModuleDefinitionVisitor` even if you use "non-simple with\*" functions.
|
||||
|
||||
-}
|
||||
withSimpleModuleDefinitionVisitor : (Node Module -> List Error) -> Schema context -> Schema context
|
||||
withSimpleModuleDefinitionVisitor visitor (Schema schema) =
|
||||
Schema { schema | moduleDefinitionVisitor = \node context -> ( visitor node, context ) }
|
||||
|
||||
|
||||
{-| Add a visitor to the [`Schema`](#Schema) which will visit the `File`'s [import statements](https://package.elm-lang.org/packages/stil4m/elm-syntax/latest/Elm-Syntax-Import) (`import Html as H exposing (div)`) in order of their definition and report patterns.
|
||||
|
||||
The following example forbids using the core Html package and suggests using
|
||||
`elm-css` or `elm-ui` instead.
|
||||
|
||||
import Elm.Syntax.Import exposing (Import)
|
||||
import Elm.Syntax.Node as Node exposing (Node)
|
||||
import Lint.Error as Error exposing (Error)
|
||||
import Lint.Rule as Rule exposing (Rule)
|
||||
|
||||
rule : Rule
|
||||
rule =
|
||||
Rule.newSchema "NoCoreHtml"
|
||||
|> Rule.withSimpleImportVisitor importVisitor
|
||||
|> Rule.fromSchema
|
||||
|
||||
importVisitor : Node Import -> List Error
|
||||
importVisitor node =
|
||||
let
|
||||
moduleName : List String
|
||||
moduleName =
|
||||
node
|
||||
|> Node.value
|
||||
|> .moduleName
|
||||
|> Node.value
|
||||
in
|
||||
case moduleName of
|
||||
[ "Html" ] ->
|
||||
[ Error.create "Use `elm-css` or `elm-ui` instead of the core HTML package." (Node.range node) ]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
|
||||
Note: `withSimpleImportVisitor` is a simplified version of [`withImportVisitor`](#withImportVisitor),
|
||||
which isn't passed a `context` and doesn't return one. You can use `withSimpleImportVisitor` even if you use "non-simple with\*" functions.
|
||||
|
||||
-}
|
||||
withSimpleImportVisitor : (Node Import -> List Error) -> Schema context -> Schema context
|
||||
withSimpleImportVisitor visitor (Schema schema) =
|
||||
Schema { schema | importVisitor = \node context -> ( visitor node, context ) }
|
||||
|
||||
|
||||
withSimpleExpressionVisitor : (Node Expression -> List Error) -> Schema context -> Schema context
|
||||
withSimpleExpressionVisitor visitor (Schema schema) =
|
||||
Schema
|
||||
{ schema
|
||||
| expressionVisitor =
|
||||
\node direction context ->
|
||||
case direction of
|
||||
Direction.Enter ->
|
||||
( visitor node, context )
|
||||
{-| Add a visitor to the [`Schema`](#Schema) which will visit the `File`'s
|
||||
[declaration statements](https://package.elm-lang.org/packages/stil4m/elm-syntax/latest/Elm-Syntax-Declaration)
|
||||
(`someVar = add 1 2`, `type Bool = True | False`, `port output : Json.Encode.Value -> Cmd msg`)
|
||||
and report patterns. The declarations will be visited in the order of their definition.
|
||||
|
||||
Direction.Exit ->
|
||||
( [], context )
|
||||
}
|
||||
The following example forbids declaring a function or a value without a type
|
||||
annotation.
|
||||
|
||||
import Elm.Syntax.Declaration exposing (Declaration(..))
|
||||
import Elm.Syntax.Node as Node exposing (Node)
|
||||
import Lint.Error as Error exposing (Error)
|
||||
import Lint.Rule as Rule exposing (Rule)
|
||||
|
||||
rule : Rule
|
||||
rule =
|
||||
Rule.newSchema "NoMissingTypeAnnotation"
|
||||
|> Rule.withSimpleDeclarationVisitor declarationVisitor
|
||||
|> Rule.fromSchema
|
||||
|
||||
declarationVisitor : Node Declaration -> List Error
|
||||
declarationVisitor node =
|
||||
case Node.value node of
|
||||
FunctionDeclaration { signature, declaration } ->
|
||||
case signature of
|
||||
Just _ ->
|
||||
[]
|
||||
|
||||
Nothing ->
|
||||
let
|
||||
functionName : String
|
||||
functionName =
|
||||
declaration |> Node.value |> .name |> Node.value
|
||||
in
|
||||
[ Error.create ("Missing type annotation for `" ++ functionName ++ "`.") (Node.range node) ]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
|
||||
Note: `withSimpleDeclarationVisitor` is a simplified version of [`withDeclarationVisitor`](#withDeclarationVisitor),
|
||||
which isn't passed a `Direction` (it will only be called on `Direction.Enter`) and a `context` and doesn't return a context. You can use `withSimpleDeclarationVisitor` even if you use "non-simple with\*" functions.
|
||||
|
||||
-}
|
||||
withSimpleDeclarationVisitor : (Node Declaration -> List Error) -> Schema context -> Schema context
|
||||
withSimpleDeclarationVisitor visitor (Schema schema) =
|
||||
Schema
|
||||
@ -175,26 +275,505 @@ withSimpleDeclarationVisitor visitor (Schema schema) =
|
||||
}
|
||||
|
||||
|
||||
{-| Add a visitor to the [`Schema`](#Schema) which will visit the `File`'s
|
||||
[expressions](https://package.elm-lang.org/packages/stil4m/elm-syntax/latest/Elm-Syntax-Expression)
|
||||
(`1`, `True`, `add 1 2`, `1 + 2`). The expressions are visited in pre-order
|
||||
depth-first search, meaning that an expression will be visited, then it's first
|
||||
child, the first child's children (and so on), then the second child (and so on).
|
||||
|
||||
The following example forbids using the Debug module.
|
||||
|
||||
import Elm.Syntax.Expression exposing (Expression(..))
|
||||
import Elm.Syntax.Node as Node exposing (Node)
|
||||
import Lint.Error as Error exposing (Error)
|
||||
import Lint.Rule as Rule exposing (Rule)
|
||||
|
||||
rule : Rule
|
||||
rule =
|
||||
Rule.newSchema "NoDebug"
|
||||
|> Rule.withSimpleExpressionVisitor expressionVisitor
|
||||
|> Rule.fromSchema
|
||||
|
||||
expressionVisitor : Node Expression -> List Error
|
||||
expressionVisitor node =
|
||||
case Node.value node of
|
||||
FunctionOrValue moduleName fnName ->
|
||||
if List.member "Debug" moduleName then
|
||||
[ Error.create "Forbidden use of Debug" (Node.range node) ]
|
||||
|
||||
else
|
||||
[]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
|
||||
Note: `withSimpleExpressionVisitor` is a simplified version of [`withExpressionVisitor`](#withExpressionVisitor),
|
||||
which isn't passed a `Direction` (it will only be called on `Direction.Enter`) and a `context` and doesn't return a context. You can use `withSimpleExpressionVisitor` even if you use "non-simple with\*" functions.
|
||||
|
||||
-}
|
||||
withSimpleExpressionVisitor : (Node Expression -> List Error) -> Schema context -> Schema context
|
||||
withSimpleExpressionVisitor visitor (Schema schema) =
|
||||
Schema
|
||||
{ schema
|
||||
| expressionVisitor =
|
||||
\node direction context ->
|
||||
case direction of
|
||||
Direction.Enter ->
|
||||
( visitor node, context )
|
||||
|
||||
Direction.Exit ->
|
||||
( [], context )
|
||||
}
|
||||
|
||||
|
||||
{-| Adds an initial `context` to start collecting data during your traversal.
|
||||
|
||||
In some cases, you can't just report a pattern when you see it, but you want to
|
||||
not report or report differently depending on information located in a different
|
||||
part of the file. In that case, you collect data as the nodes in the file get
|
||||
traversed and store it in what we'll call a `context`. This `context` will be
|
||||
available and updated by non-"simple" "with\*" functions, like
|
||||
[`withExpressionVisitor`](#withExpressionVisitor) or [`withImportVisitor`](#withImportVisitor).
|
||||
|
||||
Once the file has been traversed and you have collected all the data available
|
||||
from the file, you can report some final errors using [`withFinalEvaluation`](#withFinalEvaluation).
|
||||
|
||||
A few use examples:
|
||||
|
||||
- You want to report the use of `Debug.log`: and if you see a call using a `log`
|
||||
function, you need to check whether `log` was defined in the file, or imported
|
||||
using `import Debug exposing (log)` or `import Debug exposing (..)`.
|
||||
- You wish to report unused variables, so you need to register the declared and
|
||||
imported variables, and note when they get used.
|
||||
- You noticed plenty of bad or inconsistent uses of the `Html.button` function,
|
||||
so you built a nice `Ui.Button` module. You now want to forbid all uses of
|
||||
`Html.button`, except in the `Ui.Button` module.
|
||||
|
||||
The `context` you choose needs to be of the same type for all visitors. In practice,
|
||||
it's similar to a `Model` for a rule.
|
||||
|
||||
The following example forbids calling `Rule.newSchema` with a name that is not
|
||||
the same as the module's name (forbidding `Rule.newSchema "NoSomething"` when the
|
||||
module name is `Lint.Rule.NoSomethingElse`).
|
||||
|
||||
-- module Lint.Rule.NoDifferentNameForRuleAndModuleName exposing (rule)
|
||||
|
||||
|
||||
import Elm.Syntax.Expression exposing (Expression(..))
|
||||
import Elm.Syntax.Module as Module exposing (Module)
|
||||
import Elm.Syntax.Node as Node exposing (Node)
|
||||
import Lint.Direction as Direction exposing (Direction)
|
||||
import Lint.Error as Error exposing (Error)
|
||||
import Lint.Rule as Rule exposing (Rule)
|
||||
import List.Extra
|
||||
|
||||
type alias Context =
|
||||
-- Contains the module name's last part
|
||||
Maybe String
|
||||
|
||||
rule : Rule
|
||||
rule =
|
||||
Rule.newSchema "NoDifferentNameForRuleAndModuleName"
|
||||
|> Rule.withInitialContext Nothing
|
||||
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
|
||||
|> Rule.withExpressionVisitor expressionVisitor
|
||||
|> Rule.fromSchema
|
||||
|
||||
moduleDefinitionVisitor : Node Module -> Context -> ( List Error, Context )
|
||||
moduleDefinitionVisitor node context =
|
||||
let
|
||||
moduleLastName : Maybe String
|
||||
moduleLastName =
|
||||
node
|
||||
|> Node.value
|
||||
|> Module.moduleName
|
||||
|> List.Extra.last
|
||||
in
|
||||
( [], moduleLastName )
|
||||
|
||||
expressionVisitor : Node Expression -> Direction -> Context -> ( List Error, Context )
|
||||
expressionVisitor node direction context =
|
||||
case ( direction, Node.value node ) of
|
||||
( Direction.Enter, Application (function :: ruleNameNode :: _) ) ->
|
||||
case ( Node.value function, Node.value ruleNameNode ) of
|
||||
( FunctionOrValue [ "Rule" ] "newSchema", Literal ruleName ) ->
|
||||
if Just ruleName /= context then
|
||||
let
|
||||
suggestedName : String
|
||||
suggestedName =
|
||||
case context of
|
||||
Just name ->
|
||||
" (`" ++ name ++ "`)"
|
||||
|
||||
Nothing ->
|
||||
""
|
||||
in
|
||||
( [ Error.create
|
||||
("Rule name should be the same as the module name" ++ suggestedName)
|
||||
(Node.range ruleNameNode)
|
||||
]
|
||||
, context
|
||||
)
|
||||
|
||||
else
|
||||
( [], context )
|
||||
|
||||
_ ->
|
||||
( [], context )
|
||||
|
||||
_ ->
|
||||
( [], context )
|
||||
|
||||
Note that due to implementation details, `withInitialContext` needs to be chained
|
||||
right after [`newSchema`](#newSchema) just like in the example above, as previous
|
||||
"with\*" functions will be ignored.
|
||||
|
||||
-}
|
||||
withInitialContext : context -> Schema () -> Schema context
|
||||
withInitialContext initialContext_ (Schema schema) =
|
||||
Schema
|
||||
{ name = schema.name
|
||||
, initialContext = initialContext_
|
||||
, moduleDefinitionVisitor = \node context -> ( [], context )
|
||||
, importVisitor = \node context -> ( [], context )
|
||||
, expressionVisitor = \node direction context -> ( [], context )
|
||||
, declarationVisitor = \node direction context -> ( [], context )
|
||||
, finalEvaluationFn = \context -> []
|
||||
}
|
||||
|
||||
|
||||
{-| Add a visitor to the [`Schema`](#Schema) which will visit the `File`'s
|
||||
[module definition](https://package.elm-lang.org/packages/stil4m/elm-syntax/latest/Elm-Syntax-Module) (`module SomeModuleName exposing (a, b)`), collect data in the `context` and/or report patterns.
|
||||
|
||||
The following example forbids the use of `Debug` except in some files, determined
|
||||
by a configuration which could look like `( Critical, NoDebugExceptInSomeModules.rule ["Some.Module"] )`
|
||||
|
||||
import Elm.Syntax.Expression exposing (Expression(..))
|
||||
import Elm.Syntax.Module as Module exposing (Module)
|
||||
import Elm.Syntax.Node as Node exposing (Node)
|
||||
import Lint.Direction as Direction exposing (Direction)
|
||||
import Lint.Error as Error exposing (Error)
|
||||
import Lint.Rule as Rule exposing (Rule)
|
||||
|
||||
type Context
|
||||
= DebugIsAllowed
|
||||
| DebugIsForbidden
|
||||
|
||||
rule : List String -> Rule
|
||||
rule allowedModuleNames =
|
||||
Rule.newSchema "NoDebugExceptInSomeModules"
|
||||
|> Rule.withInitialContext DebugIsForbidden
|
||||
|> Rule.withModuleDefinitionVisitor
|
||||
(moduleDefinitionVisitor <| List.map (String.split ".") allowedModuleNames)
|
||||
|> Rule.withExpressionVisitor expressionVisitor
|
||||
|> Rule.fromSchema
|
||||
|
||||
moduleDefinitionVisitor : List (List String) -> Node Module -> Context -> ( List Error, Context )
|
||||
moduleDefinitionVisitor allowedModuleNames node context =
|
||||
if List.member (Node.value node |> Module.moduleName) allowedModuleNames then
|
||||
( [], DebugIsAllowed )
|
||||
|
||||
else
|
||||
( [], DebugIsForbidden )
|
||||
|
||||
expressionVisitor : Node Expression -> Direction -> Context -> ( List Error, Context )
|
||||
expressionVisitor node direction context =
|
||||
case ( direction, context ) of
|
||||
( Direction.Enter, DebugIsAllowed ) ->
|
||||
( [], context )
|
||||
|
||||
( Direction.Enter, DebugIsForbidden ) ->
|
||||
case Node.value node of
|
||||
FunctionOrValue moduleName fnName ->
|
||||
if List.member "Debug" moduleName then
|
||||
( [ Error.create "Forbidden use of Debug" (Node.range node) ], context )
|
||||
|
||||
else
|
||||
( [], context )
|
||||
|
||||
_ ->
|
||||
( [], context )
|
||||
|
||||
( _, _ ) ->
|
||||
( [], context )
|
||||
|
||||
Note: If you do not need to collect data in this visitor, you may wish to use the
|
||||
simpler [`withSimpleModuleDefinitionVisitor`](#withSimpleModuleDefinitionVisitor).
|
||||
|
||||
-}
|
||||
withModuleDefinitionVisitor : (Node Module -> context -> ( List Error, context )) -> Schema context -> Schema context
|
||||
withModuleDefinitionVisitor visitor (Schema schema) =
|
||||
Schema { schema | moduleDefinitionVisitor = visitor }
|
||||
|
||||
|
||||
{-| Add a visitor to the [`Schema`](#Schema) which will visit the `File`'s
|
||||
[import statements](https://package.elm-lang.org/packages/stil4m/elm-syntax/latest/Elm-Syntax-Import)
|
||||
(`import Html as H exposing (div)`) in order of their definition, collect data
|
||||
in the `context` and/or report patterns.
|
||||
|
||||
The following example forbids importing both `Element` (`elm-ui`) and
|
||||
`Html.Styled` (`elm-css`).
|
||||
|
||||
import Elm.Syntax.Import exposing (Import)
|
||||
import Elm.Syntax.Node as Node exposing (Node)
|
||||
import Lint.Error as Error exposing (Error)
|
||||
import Lint.Rule as Rule exposing (Rule)
|
||||
|
||||
type alias Context =
|
||||
{ elmUiWasImported : Bool
|
||||
, elmCssWasImported : Bool
|
||||
}
|
||||
|
||||
rule : Rule
|
||||
rule =
|
||||
Rule.newSchema "NoUsingBothHtmlAndHtmlStyled"
|
||||
|> Rule.withInitialContext { elmUiWasImported = False, elmCssWasImported = False }
|
||||
|> Rule.withImportVisitor importVisitor
|
||||
|> Rule.fromSchema
|
||||
|
||||
importVisitor : Node Import -> Context -> ( List Error, Context )
|
||||
importVisitor node context =
|
||||
case Node.value node |> .moduleName |> Node.value of
|
||||
[ "Element" ] ->
|
||||
if context.elmCssWasImported then
|
||||
( [ Error.create "Do not use both `elm-ui` and `elm-css`" (Node.range node) ]
|
||||
, { context | elmUiWasImported = True }
|
||||
)
|
||||
|
||||
else
|
||||
( [ Error.create "Do not use both `elm-ui` and `elm-css`" (Node.range node) ]
|
||||
, { context | elmUiWasImported = True }
|
||||
)
|
||||
|
||||
[ "Html", "Styled" ] ->
|
||||
if context.elmUiWasImported then
|
||||
( [ Error.create "Do not use both `elm-ui` and `elm-css`" (Node.range node) ]
|
||||
, { context | elmCssWasImported = True }
|
||||
)
|
||||
|
||||
else
|
||||
( [ Error.create "Do not use both `elm-ui` and `elm-css`" (Node.range node) ]
|
||||
, { context | elmCssWasImported = True }
|
||||
)
|
||||
|
||||
_ ->
|
||||
( [], context )
|
||||
|
||||
This example was written in a different way in the example for [`withFinalEvaluation`](#withFinalEvaluation).
|
||||
|
||||
Note: If you do not need to collect or use the `context` in this visitor, you may wish to use the
|
||||
simpler [`withSimpleImportVisitor`](#withSimpleImportVisitor).
|
||||
|
||||
-}
|
||||
withImportVisitor : (Node Import -> context -> ( List Error, context )) -> Schema context -> Schema context
|
||||
withImportVisitor visitor (Schema schema) =
|
||||
Schema { schema | importVisitor = visitor }
|
||||
|
||||
|
||||
withExpressionVisitor : (Node Expression -> Direction -> context -> ( List Error, context )) -> Schema context -> Schema context
|
||||
withExpressionVisitor visitor (Schema schema) =
|
||||
Schema { schema | expressionVisitor = visitor }
|
||||
{-| Add a visitor to the [`Schema`](#Schema) which will visit the `File`'s
|
||||
[declaration statements](https://package.elm-lang.org/packages/stil4m/elm-syntax/latest/Elm-Syntax-Declaration)
|
||||
(`someVar = add 1 2`, `type Bool = True | False`, `port output : Json.Encode.Value -> Cmd msg`),
|
||||
collect data and/or report patterns. The declarations will be visited in the order of their definition.
|
||||
|
||||
The following example forbids declaring a function or a value without a type
|
||||
annotation.
|
||||
|
||||
import Elm.Syntax.Declaration exposing (Declaration(..))
|
||||
import Elm.Syntax.Exposing as Exposing
|
||||
import Elm.Syntax.Module as Module exposing (Module)
|
||||
import Elm.Syntax.Node as Node exposing (Node)
|
||||
import Lint.Direction as Direction exposing (Direction)
|
||||
import Lint.Error as Error exposing (Error)
|
||||
import Lint.Rule as Rule exposing (Rule)
|
||||
|
||||
type ExposedFunctions
|
||||
= All
|
||||
| OnlySome (List String)
|
||||
|
||||
rule : Rule
|
||||
rule =
|
||||
Rule.newSchema "NoMissingDocumentationForExposedFunctions"
|
||||
|> Rule.withInitialContext (OnlySome [])
|
||||
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
|
||||
|> Rule.withDeclarationVisitor declarationVisitor
|
||||
|> Rule.fromSchema
|
||||
|
||||
moduleDefinitionVisitor : Node Module -> ExposedFunctions -> ( List Error, ExposedFunctions )
|
||||
moduleDefinitionVisitor node context =
|
||||
case Node.value node |> Module.exposingList of
|
||||
Exposing.All _ ->
|
||||
( [], All )
|
||||
|
||||
Exposing.Explicit exposedValues ->
|
||||
( [], OnlySome (List.filterMap exposedFunctionName exposedValues) )
|
||||
|
||||
exposedFunctionName : Node Exposing.TopLevelExpose -> Maybe String
|
||||
exposedFunctionName value =
|
||||
case Node.value value of
|
||||
Exposing.FunctionExpose functionName ->
|
||||
Just functionName
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
|
||||
declarationVisitor : Node Declaration -> Direction -> ExposedFunctions -> ( List Error, ExposedFunctions )
|
||||
declarationVisitor node direction context =
|
||||
case ( direction, Node.value node ) of
|
||||
( Direction.Enter, FunctionDeclaration { documentation, declaration } ) ->
|
||||
let
|
||||
functionName : String
|
||||
functionName =
|
||||
Node.value declaration |> .name |> Node.value
|
||||
in
|
||||
if documentation == Nothing && isExposed context functionName then
|
||||
( [ Error.create "Exposed function is missing a type annotation" (Node.range node) ], context )
|
||||
|
||||
else
|
||||
( [], context )
|
||||
|
||||
_ ->
|
||||
( [], context )
|
||||
|
||||
isExposed : ExposedFunctions -> String -> Bool
|
||||
isExposed exposedFunctions name =
|
||||
case exposedFunctions of
|
||||
All ->
|
||||
True
|
||||
|
||||
OnlySome exposedList ->
|
||||
List.member name exposedList
|
||||
|
||||
Note: If you do not need to collect or use the `context` in this visitor, you may wish to use the
|
||||
simpler [`withSimpleDeclarationVisitor`](#withSimpleDeclarationVisitor).
|
||||
|
||||
-}
|
||||
withDeclarationVisitor : (Node Declaration -> Direction -> context -> ( List Error, context )) -> Schema context -> Schema context
|
||||
withDeclarationVisitor visitor (Schema schema) =
|
||||
Schema { schema | declarationVisitor = visitor }
|
||||
|
||||
|
||||
{-| Add a visitor to the [`Schema`](#Schema) which will visit the `File`'s
|
||||
[expressions](https://package.elm-lang.org/packages/stil4m/elm-syntax/latest/Elm-Syntax-Expression)
|
||||
(`1`, `True`, `add 1 2`, `1 + 2`), collect data in the `context` and/or report patterns.
|
||||
The expressions are visited in pre-order depth-first search, meaning that an
|
||||
expression will be visited, then it's first child, the first child's children
|
||||
(and so on), then the second child (and so on).
|
||||
|
||||
The following example forbids the use of `Debug.log` even when it's imported like
|
||||
`import Debug exposing (log)`.
|
||||
module Main exposing (Context(..), expressionVisitor, importVisitor, rule)
|
||||
|
||||
import Elm.Syntax.Exposing as Exposing exposing (TopLevelExpose(..))
|
||||
import Elm.Syntax.Expression exposing (Expression(..))
|
||||
import Elm.Syntax.Import exposing (Import)
|
||||
import Elm.Syntax.Node as Node exposing (Node)
|
||||
import Lint.Direction as Direction exposing (Direction)
|
||||
import Lint.Error as Error exposing (Error)
|
||||
import Lint.Rule as Rule exposing (Rule)
|
||||
|
||||
type Context
|
||||
= DebugLogWasNotImported
|
||||
| DebugLogWasImported
|
||||
|
||||
rule : Rule
|
||||
rule =
|
||||
Rule.newSchema "NoDebugEvenIfImported"
|
||||
|> Rule.withInitialContext DebugLogWasNotImported
|
||||
|> Rule.withImportVisitor importVisitor
|
||||
|> Rule.withExpressionVisitor expressionVisitor
|
||||
|> Rule.fromSchema
|
||||
|
||||
importVisitor : Node Import -> Context -> ( List Error, Context )
|
||||
importVisitor node context =
|
||||
case ( Node.value node |> .moduleName |> Node.value, (Node.value node).exposingList |> Maybe.map Node.value ) of
|
||||
( [ "Debug" ], Just (Exposing.All _) ) ->
|
||||
( [], DebugLogWasImported )
|
||||
|
||||
( [ "Debug" ], Just (Exposing.Explicit exposedFunctions) ) ->
|
||||
let
|
||||
isLogFunction : Node Exposing.TopLevelExpose -> Bool
|
||||
isLogFunction exposeNode =
|
||||
case Node.value exposeNode of
|
||||
FunctionExpose "log" ->
|
||||
True
|
||||
|
||||
_ ->
|
||||
False
|
||||
in
|
||||
if List.any isLogFunction exposedFunctions then
|
||||
( [], DebugLogWasImported )
|
||||
|
||||
else
|
||||
( [], DebugLogWasNotImported )
|
||||
|
||||
_ ->
|
||||
( [], DebugLogWasNotImported )
|
||||
|
||||
expressionVisitor : Node Expression -> Direction -> Context -> ( List Error, Context )
|
||||
expressionVisitor node direction context =
|
||||
case context of
|
||||
DebugLogWasNotImported ->
|
||||
( [], context )
|
||||
|
||||
DebugLogWasImported ->
|
||||
case ( direction, Node.value node ) of
|
||||
( Direction.Enter, FunctionOrValue [] "log" ) ->
|
||||
( [ Error.create "Forbidden use of Debug.log" (Node.range node) ], context )
|
||||
|
||||
_ ->
|
||||
( [], context )
|
||||
|
||||
Note: If you do not need to collect or use the `context` in this visitor, you may wish to use the
|
||||
simpler [`withSimpleExpressionVisitor`](#withSimpleExpressionVisitor).
|
||||
|
||||
-}
|
||||
withExpressionVisitor : (Node Expression -> Direction -> context -> ( List Error, context )) -> Schema context -> Schema context
|
||||
withExpressionVisitor visitor (Schema schema) =
|
||||
Schema { schema | expressionVisitor = visitor }
|
||||
|
||||
|
||||
{-| Add a function that makes a final evaluation based only on the data that was
|
||||
collected in the `context`. This can be useful if you can't or if it is hard to
|
||||
determine something as you traverse the file.
|
||||
|
||||
This example was written in a different way in the example for [`withFinalEvaluation`](#withFinalEvaluation).
|
||||
|
||||
The following example forbids importing both `Element` (`elm-ui`) and
|
||||
`Html.Styled` (`elm-css`). Note that this is the same one written in the example
|
||||
for [`withImportVisitor`](#withImportVisitor), but using `withFinalEvaluation`.
|
||||
|
||||
import Dict as Dict exposing (Dict)
|
||||
import Elm.Syntax.Import exposing (Import)
|
||||
import Elm.Syntax.Node as Node exposing (Node)
|
||||
import Elm.Syntax.Range exposing (Range)
|
||||
import Lint.Error as Error exposing (Error)
|
||||
import Lint.Rule as Rule exposing (Rule)
|
||||
|
||||
type alias Context =
|
||||
Dict (List String) Range
|
||||
|
||||
rule : Rule
|
||||
rule =
|
||||
Rule.newSchema "NoUsingBothHtmlAndHtmlStyled"
|
||||
|> Rule.withInitialContext Dict.empty
|
||||
|> Rule.withImportVisitor importVisitor
|
||||
|> Rule.withFinalEvaluation finalEvaluation
|
||||
|> Rule.fromSchema
|
||||
|
||||
importVisitor : Node Import -> Context -> ( List Error, Context )
|
||||
importVisitor node context =
|
||||
( [], Dict.insert (Node.value node |> .moduleName |> Node.value) (Node.range node) context )
|
||||
|
||||
finalEvaluation : Context -> List Error
|
||||
finalEvaluation context =
|
||||
case ( Dict.get [ "Element" ] context, Dict.get [ "Html", "Styled" ] context ) of
|
||||
( Just elmUiRange, Just _ ) ->
|
||||
[ Error.create "Do not use both `elm-ui` and `elm-css`" elmUiRange ]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
|
||||
-}
|
||||
withFinalEvaluation : (context -> List Error) -> Schema context -> Schema context
|
||||
withFinalEvaluation visitor (Schema schema) =
|
||||
Schema { schema | finalEvaluationFn = visitor }
|
||||
@ -204,11 +783,15 @@ withFinalEvaluation visitor (Schema schema) =
|
||||
-- ACCESS
|
||||
|
||||
|
||||
{-| Get the name of a [`Rule`](#Rule).
|
||||
-}
|
||||
name : Rule -> String
|
||||
name (Rule rule) =
|
||||
rule.name
|
||||
|
||||
|
||||
{-| Get the analyzer function of a [`Rule`](#Rule).
|
||||
-}
|
||||
analyzer : Rule -> (File -> List Error)
|
||||
analyzer (Rule rule) =
|
||||
rule.analyzer
|
||||
|
Loading…
Reference in New Issue
Block a user