mirror of
https://github.com/jfmengels/elm-review.git
synced 2024-12-26 03:04:48 +03:00
429 lines
12 KiB
Elm
429 lines
12 KiB
Elm
|
module Docs.NoMissing exposing
|
||
|
( rule
|
||
|
, What, everything, onlyExposed
|
||
|
, From, allModules, exposedModules
|
||
|
)
|
||
|
|
||
|
{-|
|
||
|
|
||
|
@docs rule
|
||
|
|
||
|
|
||
|
## Configuration
|
||
|
|
||
|
@docs What, everything, onlyExposed
|
||
|
@docs From, allModules, exposedModules
|
||
|
|
||
|
|
||
|
## When (not) to enable this rule
|
||
|
|
||
|
This rule is useful when you care about having a thoroughly or increasingly documented project.
|
||
|
It is also useful when you write Elm packages, in order to know about missing documentation before you publish.
|
||
|
|
||
|
|
||
|
## Try it out
|
||
|
|
||
|
You can try this rule out by running the following command:
|
||
|
|
||
|
```bash
|
||
|
elm-review --template jfmengels/elm-review-documentation/example --rules Docs.NoMissing
|
||
|
```
|
||
|
|
||
|
-}
|
||
|
|
||
|
import Docs.Utils.ExposedFromProject as ExposedFromProject
|
||
|
import Elm.Project
|
||
|
import Elm.Syntax.Declaration as 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 Elm.Syntax.Range as Range
|
||
|
import Review.Rule as Rule exposing (Error, Rule)
|
||
|
import Set exposing (Set)
|
||
|
|
||
|
|
||
|
{-| Reports missing or empty documentation for functions and types.
|
||
|
|
||
|
import Docs.NoMissing exposing (exposedModules, onlyExposed)
|
||
|
|
||
|
config =
|
||
|
[ Docs.NoMissing.rule
|
||
|
{ document = onlyExposed
|
||
|
, from = exposedModules
|
||
|
}
|
||
|
]
|
||
|
|
||
|
|
||
|
## Fail
|
||
|
|
||
|
The rule will report when documentation is missing
|
||
|
|
||
|
someFunction =
|
||
|
great Things
|
||
|
|
||
|
or when documentation is present but empty.
|
||
|
|
||
|
{-| -}
|
||
|
someOtherFunction =
|
||
|
other (great Things)
|
||
|
|
||
|
The reasoning for not allowing empty documentation is because of how people consume documentation can vary, and for some
|
||
|
of those ways, empty documentation doesn't lead to a nice experience. For instance, if you are looking at Elm code in
|
||
|
your IDE and want to lookup the definition of a function, an empty documentation will give you no information beyond the
|
||
|
type annotation.
|
||
|
|
||
|
When I write documentation for a module, I try to tell a story or somehow phrase it as a tutorial, so that people can
|
||
|
learn the easier concepts first, and gradually as they read more and learn more about the ideas and concepts, I will
|
||
|
assume that they read most of the documentation above.
|
||
|
|
||
|
But for every function or type, I also imagine that they'll be read on their own from an IDE for instance, and therefore
|
||
|
try to make the documentation as light as possible while giving a helpful description and an example, without relying
|
||
|
too much on the assumption that the user has read the rest of the module.
|
||
|
|
||
|
A common case where people don't give an example is when exposing functions such as `map2`, `map3`, `map4`, etc., usually
|
||
|
documented in that order and next to each other. While `map2` is usually properly documented, the following ones would
|
||
|
have empty documentation, which I believe would be because the author assumes that the user went through the documentation on
|
||
|
the package registry and has read the documentation for `map2`. But if someone unfamiliar with Elm or an API looks up
|
||
|
`map3`, they may have trouble finding the information they were looking for.
|
||
|
|
||
|
I would recommend to make the documentation for each element as understandable out of context as possible. At the very least,
|
||
|
I would advise to say something like "This function is like `map2` but with X arguments" with a link to `map2`, so that
|
||
|
relevant information _can_ be found without too much effort.
|
||
|
|
||
|
|
||
|
## Success
|
||
|
|
||
|
{-| someFunction does great things
|
||
|
-}
|
||
|
someFunction =
|
||
|
great Things
|
||
|
|
||
|
-}
|
||
|
rule : { document : What, from : From } -> Rule
|
||
|
rule configuration =
|
||
|
Rule.newModuleRuleSchema "Docs.NoMissing" initialContext
|
||
|
|> Rule.withElmJsonModuleVisitor elmJsonVisitor
|
||
|
|> Rule.withModuleDefinitionVisitor (moduleDefinitionVisitor configuration.from)
|
||
|
|> Rule.withCommentsVisitor commentsVisitor
|
||
|
|> Rule.withDeclarationEnterVisitor (declarationVisitor configuration.document)
|
||
|
|> Rule.fromModuleRuleSchema
|
||
|
|
||
|
|
||
|
type alias Context =
|
||
|
{ moduleNameNode : Node String
|
||
|
, exposedModules : Set String
|
||
|
, exposedElements : Exposed
|
||
|
, shouldBeReported : Bool
|
||
|
}
|
||
|
|
||
|
|
||
|
initialContext : Context
|
||
|
initialContext =
|
||
|
{ moduleNameNode = Node Range.emptyRange ""
|
||
|
, exposedModules = Set.empty
|
||
|
, exposedElements = EverythingIsExposed
|
||
|
, shouldBeReported = True
|
||
|
}
|
||
|
|
||
|
|
||
|
type Exposed
|
||
|
= EverythingIsExposed
|
||
|
| ExplicitList (Set String)
|
||
|
|
||
|
|
||
|
{-| Which elements from a module should be documented. Possible options are [`everything`](#everything) in a module or
|
||
|
only the exposed elements of a module ([`onlyExposed`](#onlyExposed)).
|
||
|
-}
|
||
|
type What
|
||
|
= Everything
|
||
|
| OnlyExposed
|
||
|
|
||
|
|
||
|
{-| Every function and type from a module should be documented. The module definition should also be documented.
|
||
|
-}
|
||
|
everything : What
|
||
|
everything =
|
||
|
Everything
|
||
|
|
||
|
|
||
|
{-| Only exposed functions and types from a module should be documented. The module definition should also be documented.
|
||
|
-}
|
||
|
onlyExposed : What
|
||
|
onlyExposed =
|
||
|
OnlyExposed
|
||
|
|
||
|
|
||
|
{-| Which modules should be documented. Possible options are [`allModules`](#allModules) of a project or
|
||
|
only the [`exposedModules`](#exposedModules) (only for packages).
|
||
|
-}
|
||
|
type From
|
||
|
= AllModules
|
||
|
| ExposedModules
|
||
|
|
||
|
|
||
|
{-| All modules from the project should be documented.
|
||
|
-}
|
||
|
allModules : From
|
||
|
allModules =
|
||
|
AllModules
|
||
|
|
||
|
|
||
|
{-| Only exposed modules from the project will need to be documented.
|
||
|
|
||
|
If your project is an application, you should not use this option. An application does not expose modules which would
|
||
|
mean there isn't any module to report errors for.
|
||
|
|
||
|
-}
|
||
|
exposedModules : From
|
||
|
exposedModules =
|
||
|
-- TODO Report a global error if used inside an application
|
||
|
ExposedModules
|
||
|
|
||
|
|
||
|
|
||
|
-- ELM.JSON VISITOR
|
||
|
|
||
|
|
||
|
elmJsonVisitor : Maybe Elm.Project.Project -> Context -> Context
|
||
|
elmJsonVisitor maybeProject context =
|
||
|
let
|
||
|
exposedModules_ : Set String
|
||
|
exposedModules_ =
|
||
|
case maybeProject of
|
||
|
Just project ->
|
||
|
ExposedFromProject.exposedModules project
|
||
|
|
||
|
_ ->
|
||
|
Set.empty
|
||
|
in
|
||
|
{ context | exposedModules = exposedModules_ }
|
||
|
|
||
|
|
||
|
|
||
|
-- MODULE DEFINITION VISITOR
|
||
|
|
||
|
|
||
|
moduleDefinitionVisitor : From -> Node Module -> Context -> ( List nothing, Context )
|
||
|
moduleDefinitionVisitor fromConfig node context =
|
||
|
let
|
||
|
moduleNameNode : Node String
|
||
|
moduleNameNode =
|
||
|
case Node.value node of
|
||
|
Module.NormalModule x ->
|
||
|
Node
|
||
|
(Node.range x.moduleName)
|
||
|
(Node.value x.moduleName |> String.join ".")
|
||
|
|
||
|
Module.PortModule x ->
|
||
|
Node
|
||
|
(Node.range x.moduleName)
|
||
|
(Node.value x.moduleName |> String.join ".")
|
||
|
|
||
|
Module.EffectModule x ->
|
||
|
Node
|
||
|
(Node.range x.moduleName)
|
||
|
(Node.value x.moduleName |> String.join ".")
|
||
|
|
||
|
shouldBeReported : Bool
|
||
|
shouldBeReported =
|
||
|
case fromConfig of
|
||
|
AllModules ->
|
||
|
True
|
||
|
|
||
|
ExposedModules ->
|
||
|
Set.member (Node.value moduleNameNode) context.exposedModules
|
||
|
|
||
|
exposed : Exposed
|
||
|
exposed =
|
||
|
case Node.value node |> Module.exposingList of
|
||
|
Exposing.All _ ->
|
||
|
EverythingIsExposed
|
||
|
|
||
|
Exposing.Explicit list ->
|
||
|
ExplicitList (List.map collectExposing list |> Set.fromList)
|
||
|
in
|
||
|
( []
|
||
|
, { context
|
||
|
| moduleNameNode = moduleNameNode
|
||
|
, shouldBeReported = shouldBeReported
|
||
|
, exposedElements = exposed
|
||
|
}
|
||
|
)
|
||
|
|
||
|
|
||
|
collectExposing : Node Exposing.TopLevelExpose -> String
|
||
|
collectExposing node =
|
||
|
case Node.value node of
|
||
|
Exposing.InfixExpose name ->
|
||
|
name
|
||
|
|
||
|
Exposing.FunctionExpose name ->
|
||
|
name
|
||
|
|
||
|
Exposing.TypeOrAliasExpose name ->
|
||
|
name
|
||
|
|
||
|
Exposing.TypeExpose exposedType ->
|
||
|
exposedType.name
|
||
|
|
||
|
|
||
|
|
||
|
-- COMMENTS VISITOR
|
||
|
|
||
|
|
||
|
commentsVisitor : List (Node String) -> Context -> ( List (Error {}), Context )
|
||
|
commentsVisitor comments context =
|
||
|
if context.shouldBeReported then
|
||
|
let
|
||
|
documentation : Maybe (Node String)
|
||
|
documentation =
|
||
|
findFirst (Node.value >> String.startsWith "{-|") comments
|
||
|
in
|
||
|
( checkModuleDocumentation documentation context.moduleNameNode
|
||
|
, context
|
||
|
)
|
||
|
|
||
|
else
|
||
|
( [], context )
|
||
|
|
||
|
|
||
|
findFirst : (a -> Bool) -> List a -> Maybe a
|
||
|
findFirst predicate list =
|
||
|
case list of
|
||
|
[] ->
|
||
|
Nothing
|
||
|
|
||
|
a :: rest ->
|
||
|
if predicate a then
|
||
|
Just a
|
||
|
|
||
|
else
|
||
|
findFirst predicate rest
|
||
|
|
||
|
|
||
|
|
||
|
-- DECLARATION VISITOR
|
||
|
|
||
|
|
||
|
declarationVisitor : What -> Node Declaration -> Context -> ( List (Error {}), Context )
|
||
|
declarationVisitor documentWhat node context =
|
||
|
if context.shouldBeReported then
|
||
|
( reportDeclarationDocumentation documentWhat context node
|
||
|
, context
|
||
|
)
|
||
|
|
||
|
else
|
||
|
( [], context )
|
||
|
|
||
|
|
||
|
reportDeclarationDocumentation : What -> Context -> Node Declaration -> List (Error {})
|
||
|
reportDeclarationDocumentation documentWhat context node =
|
||
|
case Node.value node of
|
||
|
Declaration.FunctionDeclaration { documentation, declaration } ->
|
||
|
let
|
||
|
nameNode : Node String
|
||
|
nameNode =
|
||
|
(Node.value declaration).name
|
||
|
in
|
||
|
if shouldBeDocumented documentWhat context (Node.value nameNode) then
|
||
|
checkElementDocumentation documentation nameNode
|
||
|
|
||
|
else
|
||
|
[]
|
||
|
|
||
|
Declaration.CustomTypeDeclaration { documentation, name } ->
|
||
|
if shouldBeDocumented documentWhat context (Node.value name) then
|
||
|
checkElementDocumentation documentation name
|
||
|
|
||
|
else
|
||
|
[]
|
||
|
|
||
|
Declaration.AliasDeclaration { documentation, name } ->
|
||
|
if shouldBeDocumented documentWhat context (Node.value name) then
|
||
|
checkElementDocumentation documentation name
|
||
|
|
||
|
else
|
||
|
[]
|
||
|
|
||
|
_ ->
|
||
|
[]
|
||
|
|
||
|
|
||
|
shouldBeDocumented : What -> Context -> String -> Bool
|
||
|
shouldBeDocumented documentWhat context name =
|
||
|
case documentWhat of
|
||
|
Everything ->
|
||
|
True
|
||
|
|
||
|
OnlyExposed ->
|
||
|
case context.exposedElements of
|
||
|
EverythingIsExposed ->
|
||
|
True
|
||
|
|
||
|
ExplicitList exposedElements ->
|
||
|
Set.member name exposedElements
|
||
|
|
||
|
|
||
|
checkModuleDocumentation : Maybe (Node String) -> Node String -> List (Error {})
|
||
|
checkModuleDocumentation documentation nameNode =
|
||
|
case documentation of
|
||
|
Just doc ->
|
||
|
if isDocumentationEmpty doc then
|
||
|
[ Rule.error
|
||
|
{ message = "The documentation for module `" ++ Node.value nameNode ++ "` is empty"
|
||
|
, details = [ "Empty documentation is not useful for the users. Please give explanations or examples." ]
|
||
|
}
|
||
|
(Node.range doc)
|
||
|
]
|
||
|
|
||
|
else
|
||
|
[]
|
||
|
|
||
|
Nothing ->
|
||
|
[ Rule.error
|
||
|
{ message = "Missing documentation for module `" ++ Node.value nameNode ++ "`"
|
||
|
, details = documentationErrorDetails
|
||
|
}
|
||
|
(Node.range nameNode)
|
||
|
]
|
||
|
|
||
|
|
||
|
documentationErrorDetails : List String
|
||
|
documentationErrorDetails =
|
||
|
[ "A module documentation summarizes what a module is for, the responsibilities it has and how to use it. Providing a good module documentation will be useful for your users or colleagues."
|
||
|
]
|
||
|
|
||
|
|
||
|
checkElementDocumentation : Maybe (Node String) -> Node String -> List (Error {})
|
||
|
checkElementDocumentation documentation nameNode =
|
||
|
case documentation of
|
||
|
Just doc ->
|
||
|
if isDocumentationEmpty doc then
|
||
|
[ Rule.error
|
||
|
{ message = "The documentation for `" ++ Node.value nameNode ++ "` is empty"
|
||
|
, details = [ "Empty documentation is not useful for the users. Please give explanations or examples." ]
|
||
|
}
|
||
|
(Node.range doc)
|
||
|
]
|
||
|
|
||
|
else
|
||
|
[]
|
||
|
|
||
|
Nothing ->
|
||
|
[ Rule.error
|
||
|
{ message = "Missing documentation for `" ++ Node.value nameNode ++ "`"
|
||
|
, details = [ "Documentation can help developers use this API." ]
|
||
|
}
|
||
|
(Node.range nameNode)
|
||
|
]
|
||
|
|
||
|
|
||
|
isDocumentationEmpty : Node String -> Bool
|
||
|
isDocumentationEmpty doc =
|
||
|
doc
|
||
|
|> Node.value
|
||
|
|> String.dropLeft 3
|
||
|
|> String.dropRight 2
|
||
|
|> String.trim
|
||
|
|> String.isEmpty
|