Start working on a design for multi-file rules

This commit is contained in:
Jeroen Engels 2019-10-17 00:20:19 +02:00
parent 76d7a785b3
commit 894e3ac1c9
2 changed files with 511 additions and 0 deletions

355
src/ApiDesign.elm Normal file
View File

@ -0,0 +1,355 @@
module ApiDesign exposing
( Rule, Schema
, newSchema, fromSchema
, withSimpleModuleDefinitionVisitor, withSimpleImportVisitor, withSimpleDeclarationVisitor, withSimpleExpressionVisitor
, withInitialContext, withModuleDefinitionVisitor, withImportVisitor, Direction(..), withDeclarationVisitor, withDeclarationListVisitor, withExpressionVisitor, withFinalEvaluation
, withElmJsonVisitor
, withFixes
, newMultiSchema, withProjectWideElmJsonVisitor
, Error, error, errorMessage, errorDetails, errorRange, errorFixes
, name, Analyzer(..)
)
{-| Api Design
Problems to fix:
- [ ] Be able to have a list containing single file and multi-file rules.
Users should not have to care if the rule is for a single or multiple files
- [ ] Be able to create a multi-file rule
- [ ] Be able to specify a multi-file rule's file visitor in a nice way
- [ ] Using the same functions as for a single rule
- [ ] Forbid specifying a name
- [ ] Forbid specifying an elmJsonVisitor
- [ ] Figure if it makes sense to have a finalEvaluationFn, and potentially forbid it?
- [ ] Be able to run both types of rules and get a list of errors
- [ ] Find great type and functions names
- [ ] Make a nice API for when the multifile context is different as the file visitor's
- [ ] Make a nice API for when the multifile context is the same as the file visitor's
- [ ] Add a way to test multi-file rules
- [ ] Make sure that the order does not matter by running a rule several
times with a different order for the files every time.
Errors
- [ ] Define a way to report errors in other files?
A FileKey/FileId similar to a Navigation.Key? It has no useful meaning, but
makes it so you can't give an error to a non-existing file or file you haven't visited.
- [ ] Define a way to report errors in elm.json?
- [ ] Get rid of Review.Error
- [ ] Need to be able to create an error without a file in Review.Rule
## Definition
@docs Rule, Schema
## Creating a Rule
@docs newSchema, fromSchema
## Builder functions without context
@docs withSimpleModuleDefinitionVisitor, withSimpleImportVisitor, withSimpleDeclarationVisitor, withSimpleExpressionVisitor
## Builder functions with context
@docs withInitialContext, withModuleDefinitionVisitor, withImportVisitor, Direction, withDeclarationVisitor, withDeclarationListVisitor, withExpressionVisitor, withFinalEvaluation
## Builder functions to analyze the project's data
@docs withElmJsonVisitor
## Automatic fixing
For more information on automatic fixing, read the documentation for [`Review.Fix`](./Review-Fix).
@docs withFixes
## Project-wide rules
@docs newMultiSchema, withProjectWideElmJsonVisitor
## Errors
@docs Error, error, errorMessage, errorDetails, errorRange, errorFixes
# ACCESS
@docs name, Analyzer
-}
import Elm.Project
import Elm.Syntax.Declaration exposing (Declaration(..))
import Elm.Syntax.Expression exposing (Expression(..), Function, LetDeclaration(..))
import Elm.Syntax.File exposing (File)
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Infix exposing (InfixDirection(..))
import Elm.Syntax.Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node)
import Elm.Syntax.Range exposing (Range)
import Review.Fix exposing (Fix)
import Review.Project exposing (Project)
type Rule
= Rule
{ name : String
, analyzer : Analyzer
}
type Analyzer
= Single (Project -> File -> List Error)
| Multi (Project -> List File -> List Error) -- List ( File, List Error )?
type Direction
= OnEnter
| OnExit
type Error
= Error
{ message : String
, details : List String
, range : Range
, fixes : Maybe (List Fix)
}
type Schema configurationState context
= Schema
{ name : String
, initialContext : context
, elmJsonVisitor : Maybe Elm.Project.Project -> context -> context
, moduleDefinitionVisitor : Node Module -> context -> ( List Error, context )
, importVisitor : Node Import -> context -> ( List Error, context )
, declarationListVisitor : List (Node Declaration) -> context -> ( List Error, context )
, declarationVisitor : Node Declaration -> Direction -> context -> ( List Error, context )
, expressionVisitor : Node Expression -> Direction -> context -> ( List Error, context )
, finalEvaluationFn : context -> List Error
}
type MultiSchema context
= MultiSchema
{ name : String
, initialContext : context
, elmJsonVisitor : Maybe (Maybe Elm.Project.Project -> context -> context)
, fileVisitor : SimpleSchema context
, finalEvaluationFn : context -> List Error
}
newMultiSchema :
String
->
{ initialContext : context
, fileVisitor : SimpleSchema context
, mergeContexts : context -> context -> context
, finalEvaluation : context -> List Error
}
-> MultiSchema context
newMultiSchema name_ { initialContext, fileVisitor, mergeContexts, finalEvaluation } =
MultiSchema
{ name = name_
, initialContext = initialContext
, elmJsonVisitor = Nothing
, fileVisitor = fileVisitor
, finalEvaluationFn = finalEvaluation
}
withProjectWideElmJsonVisitor : (Maybe Elm.Project.Project -> context -> context) -> MultiSchema context -> MultiSchema context
withProjectWideElmJsonVisitor visitor (MultiSchema schema) =
MultiSchema { schema | elmJsonVisitor = Just visitor }
type SimpleSchema context
= SimpleSchema
{ moduleDefinitionVisitor : Node Module -> context -> ( List Error, context )
, importVisitor : Node Import -> context -> ( List Error, context )
, declarationListVisitor : List (Node Declaration) -> context -> ( List Error, context )
, declarationVisitor : Node Declaration -> Direction -> context -> ( List Error, context )
, expressionVisitor : Node Expression -> Direction -> context -> ( List Error, context )
, finalEvaluationFn : context -> List Error
}
newSchema : String -> Schema { hasNoVisitor : () } ()
newSchema name_ =
Schema
{ name = name_
, initialContext = ()
, elmJsonVisitor = \elmJson context -> context
, moduleDefinitionVisitor = \node context -> ( [], context )
, importVisitor = \node context -> ( [], context )
, declarationListVisitor = \declarationNodes context -> ( [], context )
, declarationVisitor = \node direction context -> ( [], context )
, expressionVisitor = \node direction context -> ( [], context )
, finalEvaluationFn = \context -> []
}
fromSchema : Schema { hasAtLeastOneVisitor : () } context -> Rule
fromSchema (Schema schema) =
Rule
{ name = schema.name
, analyzer =
Single
(\project file ->
Debug.todo ""
)
}
withSimpleModuleDefinitionVisitor : (Node Module -> List Error) -> Schema anything context -> Schema { hasAtLeastOneVisitor : () } context
withSimpleModuleDefinitionVisitor visitor (Schema schema) =
Schema { schema | moduleDefinitionVisitor = \node context -> ( visitor node, context ) }
withSimpleImportVisitor : (Node Import -> List Error) -> Schema anything context -> Schema { hasAtLeastOneVisitor : () } context
withSimpleImportVisitor visitor (Schema schema) =
Schema { schema | importVisitor = \node context -> ( visitor node, context ) }
withSimpleDeclarationVisitor : (Node Declaration -> List Error) -> Schema anything context -> Schema { hasAtLeastOneVisitor : () } context
withSimpleDeclarationVisitor visitor (Schema schema) =
Schema
{ schema
| declarationVisitor =
\node direction context ->
case direction of
OnEnter ->
( visitor node, context )
OnExit ->
( [], context )
}
withSimpleExpressionVisitor : (Node Expression -> List Error) -> Schema anything context -> Schema { hasAtLeastOneVisitor : () } context
withSimpleExpressionVisitor visitor (Schema schema) =
Schema
{ schema
| expressionVisitor =
\node direction context ->
case direction of
OnEnter ->
( visitor node, context )
OnExit ->
( [], context )
}
withInitialContext : context -> Schema { hasNoVisitor : () } () -> Schema { hasNoVisitor : () } context
withInitialContext initialContext_ (Schema schema) =
Schema
{ name = schema.name
, initialContext = initialContext_
, elmJsonVisitor = \elmJson context -> context
, moduleDefinitionVisitor = \node context -> ( [], context )
, importVisitor = \node context -> ( [], context )
, declarationListVisitor = \declarationNodes context -> ( [], context )
, declarationVisitor = \node direction context -> ( [], context )
, expressionVisitor = \node direction context -> ( [], context )
, finalEvaluationFn = \context -> []
}
withElmJsonVisitor : (Maybe Elm.Project.Project -> context -> context) -> Schema anything context -> Schema anything context
withElmJsonVisitor visitor (Schema schema) =
Schema { schema | elmJsonVisitor = visitor }
withModuleDefinitionVisitor : (Node Module -> context -> ( List Error, context )) -> Schema anything context -> Schema { hasAtLeastOneVisitor : () } context
withModuleDefinitionVisitor visitor (Schema schema) =
Schema { schema | moduleDefinitionVisitor = visitor }
withImportVisitor : (Node Import -> context -> ( List Error, context )) -> Schema anything context -> Schema { hasAtLeastOneVisitor : () } context
withImportVisitor visitor (Schema schema) =
Schema { schema | importVisitor = visitor }
withDeclarationVisitor : (Node Declaration -> Direction -> context -> ( List Error, context )) -> Schema anything context -> Schema { hasAtLeastOneVisitor : () } context
withDeclarationVisitor visitor (Schema schema) =
Schema { schema | declarationVisitor = visitor }
withDeclarationListVisitor : (List (Node Declaration) -> context -> ( List Error, context )) -> Schema anything context -> Schema { hasAtLeastOneVisitor : () } context
withDeclarationListVisitor visitor (Schema schema) =
Schema { schema | declarationListVisitor = visitor }
withExpressionVisitor : (Node Expression -> Direction -> context -> ( List Error, context )) -> Schema anything context -> Schema { hasAtLeastOneVisitor : () } context
withExpressionVisitor visitor (Schema schema) =
Schema { schema | expressionVisitor = visitor }
withFinalEvaluation : (context -> List Error) -> Schema { hasAtLeastOneVisitor : () } context -> Schema { hasAtLeastOneVisitor : () } context
withFinalEvaluation visitor (Schema schema) =
Schema { schema | finalEvaluationFn = visitor }
withFixes : List Fix -> Error -> Error
withFixes fixes (Error err) =
if List.isEmpty fixes then
Error { err | fixes = Nothing }
else
Error { err | fixes = Just fixes }
-- ERROR
error : { message : String, details : List String } -> Range -> Error
error { message, details } range =
Error
{ message = message
, details = details
, range = range
, fixes = Nothing
}
errorMessage : Error -> String
errorMessage (Error err) =
err.message
errorDetails : Error -> List String
errorDetails (Error err) =
err.details
errorRange : Error -> Range
errorRange (Error err) =
err.range
errorFixes : Error -> Maybe (List Fix)
errorFixes (Error err) =
err.fixes
-- ACCESS
name : Rule -> String
name (Rule rule) =
rule.name

156
src/NoUnusedModules.elm Normal file
View File

@ -0,0 +1,156 @@
module NoUnusedModules exposing (rule)
{-| Forbid the use of unused dependencies in your project.
# Rule
@docs rule
-}
import Dict exposing (Dict)
import Elm.Module
import Elm.Project exposing (Project)
import Elm.Syntax.File exposing (File)
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node(..))
import Elm.Syntax.Range exposing (Range)
import Review.Rule as Rule exposing (Error, Rule)
import Set exposing (Set)
{-| Forbid the use of the [`Debug`](https://package.elm-lang.org/packages/elm/core/latest/Debug) module before it goes into production or fails in the CI.
config =
[ NoDebug.rule
]
## Fail
if Debug.log "condition" condition then
a
else
b
if condition then
Debug.todo "Nooo!"
else
value
## Success
if condition then
a
else
b
# When (not) to use this rule
You may not want to enable this rule if you are developing an application and do
not care about having extraneous dependencies.
-}
rule : Rule
rule =
Rule.newSchema "NoUnusedModules"
{ initialContext =
{ modules = Dict.empty
, usedModules = Set.empty
}
, fileVisitor = fileVisitor
, mergeContexts =
\contextA contextB ->
{ modules = Dict.union contextA.modules contextB.modules
, usedModules = Set.union contextA.usedModules contextB.usedModules
}
, finalEvaluation = finalEvaluationForProject
}
|> Rule.withElmJsonVisitor elmJsonVisitor
|> Rule.fromSchema
fileVisitor context =
Rule.schemaForFile
|> Rule.withInitialContext context
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
|> Rule.withImportVisitor importVisitor
error : { file : File, moduleNameLocation : Range } -> List String -> Error
error { file, range } moduleName =
Rule.errorForFile file
{ message = "`" ++ String.join "." moduleName ++ "` is never used."
, details = [ "This module is never used. You may want to remove it to keep your project clean, and maybe detect some unused dependencies in your project." ]
}
range
type alias Context =
{ modules : Dict (List String) { file : File, moduleNameLocation : Range }
, usedModules : Set (List String)
}
elmJsonVisitor : Maybe Project -> Context -> Context
elmJsonVisitor maybeProject context =
let
exposedModules : List String
exposedModules =
case maybeProject of
Just (Elm.Project.Package { exposed }) ->
case exposed of
Elm.Project.ExposedList names ->
names
|> List.map Elm.Module.toString
Elm.Project.ExposedDict fakeDict ->
fakeDict
|> List.concatMap Tuple.second
|> List.map Elm.Module.toString
_ ->
[]
in
{ context | usedModules = Set.fromList exposedModules }
moduleDefinitionVisitor : Node Module -> Context -> ( List Error, Context )
moduleDefinitionVisitor node context =
let
(Node range moduleName) =
node |> Node.value |> .moduleName
in
( []
, { context | modules = Dict.insert moduleName range context.modules }
)
importVisitor : Node Import -> Context -> ( List Error, Context )
importVisitor node context =
let
moduleName : List String
moduleName =
node
|> Node.value
|> .moduleName
|> Node.value
in
( []
, { context | usedModules = Set.insert (Node.value moduleName) context.usedModules }
)
finalEvaluationForProject : Context -> List Error
finalEvaluationForProject { modules, usedModules } =
modules
|> Dict.filter (\moduleName _ -> not <| Set.member moduleName usedModules)
|> Dict.toList
|> List.map (\moduleName range -> [])