Backport rules from elm-review-forbidden-words

This commit is contained in:
Jeroen Engels 2022-09-01 18:00:56 +02:00
parent bf3f9b1469
commit a37d5afedc
2 changed files with 655 additions and 0 deletions

334
tests/NoForbiddenWords.elm Normal file
View File

@ -0,0 +1,334 @@
module NoForbiddenWords exposing (rule)
{-|
@docs rule
-}
import Elm.Project as Project exposing (Project)
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Node exposing (Node(..))
import Elm.Syntax.Range as Range exposing (Range)
import Regex exposing (Regex)
import Review.Rule as Rule exposing (Rule)
{-| Forbid certain words in Elm comments, README and elm.json (package summary only).
config : List Rule
config =
[ NoForbiddenWords.rule [ "TODO", "- [ ]" ]
]
## Failure Examples
Based on the configured words `TODO` and `- [ ]` the following examples would fail:
-- TODO: Finish writing this function
Multi-line comments `{- ... -}` and documentation `{-| ... -}` also work:
{- Actions
- [ ] Documentation
- [ ] Tests
-}
-}
rule : List String -> Rule
rule words =
Rule.newProjectRuleSchema "NoForbiddenWords" ()
|> Rule.withElmJsonProjectVisitor (elmJsonVisitor words)
|> Rule.withReadmeProjectVisitor (readmeVisitor words)
|> Rule.withModuleVisitor (moduleVisitor words)
|> Rule.withModuleContext
{ fromModuleToProject = \_ _ () -> ()
, fromProjectToModule = \_ _ () -> ()
, foldProjectContexts = \() () -> ()
}
|> Rule.fromProjectRuleSchema
--- ELM.JSON
elmJsonVisitor : List String -> Maybe { elmJsonKey : Rule.ElmJsonKey, project : Project } -> () -> ( List (Rule.Error scope), () )
elmJsonVisitor words maybeElmJson () =
case maybeElmJson of
Nothing ->
( [], () )
Just elmJson ->
( checkElmJson words elmJson, () )
checkElmJson : List String -> { elmJsonKey : Rule.ElmJsonKey, project : Project } -> List (Rule.Error scope)
checkElmJson words { elmJsonKey, project } =
case project of
Project.Application _ ->
[]
Project.Package info ->
fastConcatMap (checkElmJsonSummary elmJsonKey info.summary) words
checkElmJsonSummary : Rule.ElmJsonKey -> String -> String -> List (Rule.Error scope)
checkElmJsonSummary elmJsonKey summary word =
summary
|> stringNode
|> ranges word
|> List.map (elmJsonSummaryError elmJsonKey word)
elmJsonSummaryError : Rule.ElmJsonKey -> String -> Range -> Rule.Error scope
elmJsonSummaryError elmJsonKey word rangeInSummary =
rawElmJsonSummaryError word rangeInSummary
|> Rule.errorForElmJson elmJsonKey
rawElmJsonSummaryError : String -> Range -> String -> { message : String, details : List String, range : Range }
rawElmJsonSummaryError word rangeInSummary rawElmJson =
{ message = "`" ++ word ++ "` is not allowed in elm.json summary."
, details =
[ "You should review your elm.json and make sure the forbidden word has been removed before publishing your code."
]
, range = rawElmJsonSummaryRange rangeInSummary rawElmJson
}
rawElmJsonSummaryRange : Range -> String -> Range
rawElmJsonSummaryRange rangeInSummary rawElmJson =
rawElmJson
|> jsonFieldLocation "summary"
|> Maybe.map (rangeAddLocation rangeInSummary)
|> Maybe.withDefault startRange
--- README
readmeVisitor : List String -> Maybe { readmeKey : Rule.ReadmeKey, content : String } -> () -> ( List (Rule.Error scope), () )
readmeVisitor words maybeReadme () =
case maybeReadme of
Nothing ->
( [], () )
Just readme ->
( fastConcatMap (checkForbiddenReadmeWord readme) words
, ()
)
checkForbiddenReadmeWord : { readmeKey : Rule.ReadmeKey, content : String } -> String -> List (Rule.Error scope)
checkForbiddenReadmeWord { readmeKey, content } word =
content
|> stringNode
|> ranges word
|> List.map (forbiddenReadmeWordError readmeKey word)
forbiddenReadmeWordError : Rule.ReadmeKey -> String -> Range -> Rule.Error scope
forbiddenReadmeWordError readmeKey word range =
Rule.errorForReadme readmeKey
{ message = "`" ++ word ++ "` is not allowed in README file."
, details =
[ "You should review this section and make sure the forbidden word has been removed before publishing your code."
]
}
range
--- MODULE
moduleVisitor :
List String
-> Rule.ModuleRuleSchema {} ()
-> Rule.ModuleRuleSchema { hasAtLeastOneVisitor : () } ()
moduleVisitor words schema =
schema
|> Rule.withSimpleCommentsVisitor (commentsVisitor words)
|> Rule.withSimpleDeclarationVisitor (declarationVisitor words)
commentsVisitor : List String -> List (Node String) -> List (Rule.Error {})
commentsVisitor words comments =
fastConcatMap (commentVisitor words) comments
declarationVisitor : List String -> Node Declaration -> List (Rule.Error {})
declarationVisitor words (Node _ declaration) =
case declaration of
Declaration.FunctionDeclaration { documentation } ->
documentation
|> Maybe.map (commentVisitor words)
|> Maybe.withDefault []
Declaration.CustomTypeDeclaration { documentation } ->
documentation
|> Maybe.map (commentVisitor words)
|> Maybe.withDefault []
Declaration.AliasDeclaration { documentation } ->
documentation
|> Maybe.map (commentVisitor words)
|> Maybe.withDefault []
_ ->
[]
commentVisitor : List String -> Node String -> List (Rule.Error {})
commentVisitor words comment =
fastConcatMap (checkForbiddenWord comment) words
checkForbiddenWord : Node String -> String -> List (Rule.Error {})
checkForbiddenWord comment word =
ranges word comment
|> List.map (forbiddenWordError word)
--- HELPERS
stringNode : String -> Node String
stringNode string =
Node startRange string
startRange : Range
startRange =
{ start = { row = 1, column = 1 }
, end = { row = 1, column = 1 }
}
rangeAddLocation : Range -> Range.Location -> Range
rangeAddLocation range start =
{ start =
{ row = start.row + range.start.row - 1
, column = start.column + range.start.column - 1
}
, end =
{ row = start.row + range.end.row - 1
, column = start.column + range.end.column - 1
}
}
ranges : String -> Node String -> List Range
ranges needle (Node range haystack) =
String.lines haystack
|> List.indexedMap (rangesInLine needle range.start)
|> fastConcat
rangesInLine : String -> Range.Location -> Int -> String -> List Range
rangesInLine needle start row line =
String.indexes needle line
|> List.map (rangeFromIndex needle start row)
rangeFromIndex : String -> Range.Location -> Int -> Int -> Range
rangeFromIndex needle start row index =
case row of
0 ->
{ start =
{ row = start.row
, column = start.column + index
}
, end =
{ row = start.row
, column = start.column + index + String.length needle
}
}
_ ->
{ start =
{ row = start.row + row
, column = index + 1
}
, end =
{ row = start.row + row
, column = index + 1 + String.length needle
}
}
jsonFieldLocation : String -> String -> Maybe Range.Location
jsonFieldLocation fieldName rawJson =
let
regex : Regex
regex =
jsonFieldRegex fieldName
in
String.lines rawJson
|> List.indexedMap (jsonFieldLocationsInLine regex)
|> fastConcat
|> List.head
jsonFieldLocationsInLine : Regex -> Int -> String -> List Range.Location
jsonFieldLocationsInLine regex row line =
Regex.find regex line
|> List.map (jsonFieldMatchLocation row)
jsonFieldMatchLocation : Int -> Regex.Match -> Range.Location
jsonFieldMatchLocation row { match, index } =
case row of
0 ->
{ row = 1
, column = 1 + index + String.length match
}
_ ->
{ row = row + 1
, column = index + 1 + String.length match
}
jsonFieldRegex : String -> Regex
jsonFieldRegex fieldName =
Regex.fromString ("\"" ++ fieldName ++ "\"\\s?:\\s?\"")
|> Maybe.withDefault Regex.never
forbiddenWordError : String -> Range -> Rule.Error {}
forbiddenWordError word range =
Rule.error
{ message = "`" ++ word ++ "` is not allowed in comments."
, details =
[ "You should review this comment and make sure the forbidden word has been removed before publishing your code."
]
}
range
--- List Performance
fastConcat : List (List a) -> List a
fastConcat =
List.foldr (++) []
fastConcatMap : (a -> List b) -> List a -> List b
fastConcatMap fn =
let
helper : a -> List b -> List b
helper item acc =
fn item ++ acc
in
List.foldr helper []

View File

@ -0,0 +1,321 @@
module Tests.NoForbiddenWords exposing (all)
import Elm.Project
import Json.Decode as Decode
import Json.Encode as Encode
import NoForbiddenWords exposing (rule)
import Review.Project as Project exposing (Project)
import Review.Rule
import Review.Test
import Test exposing (Test, describe, test)
all : Test
all =
describe "NoForbiddenWords"
[ test "with no words reports nothing" <|
\() ->
"""
module A exposing (..)
-- TODO: Write main
main = Debug.todo ""
"""
|> Review.Test.run (rule [])
|> Review.Test.expectNoErrors
, test "reports any found words" <|
\() ->
"""
module A exposing (..)
-- TODO: Write main
main = Debug.todo ""
"""
|> Review.Test.run (rule [ "TODO" ])
|> Review.Test.expectErrors
[ forbiddenWordError "TODO"
]
, test "reports any found words in multi-line comments" <|
\() ->
"""
module A exposing (..)
{- The entry point for our project.
TODO: Write main
-}
main = Debug.todo ""
"""
|> Review.Test.run (rule [ "TODO" ])
|> Review.Test.expectErrors
[ forbiddenWordError "TODO"
]
, test "reports `-- [ ]` as `- [ ]`" <|
\() ->
"""
module A exposing (..)
-- [ ] Documentation
main = Debug.todo ""
"""
|> Review.Test.run (rule [ "- [ ]" ])
|> Review.Test.expectErrors
[ forbiddenWordError "- [ ]"
]
, test "reports forbidden words in module documentation" <|
\() ->
"""
module A exposing (..)
{-| Module A
TODO: Write the documentation
-}
import Foo
main = Debug.todo ""
"""
|> Review.Test.run (rule [ "TODO" ])
|> Review.Test.expectErrors
[ forbiddenWordError "TODO"
]
, test "reports forbidden words in function documentation" <|
\() ->
"""
module A exposing (..)
{-| Module A
-}
import Foo
{-| Main
TODO: Write the documentation
-}
main = Debug.todo ""
"""
|> Review.Test.run (rule [ "TODO" ])
|> Review.Test.expectErrors
[ forbiddenWordError "TODO"
]
, test "reports forbidden words in type documentation" <|
\() ->
"""
module A exposing (..)
import Foo
{-| Page
TODO: Add more pages
-}
type Page
= Page
main = Debug.todo ""
"""
|> Review.Test.run (rule [ "TODO" ])
|> Review.Test.expectErrors
[ forbiddenWordError "TODO"
]
, test "reports forbidden words in type alias documentation" <|
\() ->
"""
module A exposing (..)
import Foo
{-| Page
TODO: Add footer
-}
type alias Page =
{ title : String, body : List (Html msg) }
main = Debug.todo ""
"""
|> Review.Test.run (rule [ "TODO" ])
|> Review.Test.expectErrors
[ forbiddenWordError "TODO"
]
, test "reports forbidden words in port documentation" <|
\() ->
"""
port module Ports exposing (..)
import Foo
{-| Save
TODO: Use Json.Encode.Value here.
-}
port save : String -> Cmd Msg
"""
|> Review.Test.run (rule [ "TODO" ])
|> Review.Test.expectErrors
[ forbiddenWordError "TODO"
]
, test "checks forbidden words in README.md" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addReadme
{ path = "README.md"
, content = """
# My Awesome Project
TODO: Write the readme
"""
}
in
"""
module A exposing (..)
a = 1"""
|> Review.Test.runWithProjectData project (rule [ "TODO" ])
|> Review.Test.expectErrorsForReadme
[ forbiddenWordErrorForReadme "TODO"
]
, test "forbidden words in README.md can be ignored" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addReadme
{ path = "README.md"
, content = """
# My Awesome Project
TODO: Write the readme
"""
}
in
"""
module A exposing (..)
a = 1"""
|> Review.Test.runWithProjectData project
(rule [ "TODO" ]
|> Review.Rule.ignoreErrorsForFiles [ "README.md" ]
)
|> Review.Test.expectNoErrors
, test "checks elm.json summary for forbidden words" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson
(packageElmJson
{ summary = "REPLACEME"
, formatted = True
}
)
in
"""
module A exposing (..)
a = 1"""
|> Review.Test.runWithProjectData project (rule [ "REPLACEME" ])
|> Review.Test.expectErrorsForElmJson
[ forbiddenWordErrorForElmJson "REPLACEME"
]
, test "checks minified elm.json summary for forbidden words" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson
(packageElmJson
{ summary = "REPLACEME"
, formatted = False
}
)
in
"""
module A exposing (..)
a = 1"""
|> Review.Test.runWithProjectData project (rule [ "REPLACEME" ])
|> Review.Test.expectErrorsForElmJson
[ forbiddenWordErrorForElmJson "REPLACEME"
]
]
packageElmJson : { summary : String, formatted : Bool } -> { path : String, raw : String, project : Elm.Project.Project }
packageElmJson { summary, formatted } =
let
spaces : number
spaces =
if formatted then
4
else
0
in
Encode.encode spaces
(Encode.object
[ ( "type", Encode.string "package" )
, ( "name", Encode.string "sparksp/elm-review-new-package" )
, ( "summary", Encode.string summary )
, ( "license", Encode.string "MIT" )
, ( "version", Encode.string "1.0.0" )
, ( "exposed-modules", Encode.list Encode.string [ "NoNewPackage" ] )
, ( "elm-version", Encode.string "0.19.0 <= v < 0.20.0" )
, ( "dependencies"
, Encode.object
[ ( "elm/code", Encode.string "1.0.5 <= v <= 2.0.0" )
]
)
, ( "test-dependencies", Encode.object [] )
]
)
|> createElmJson
forbiddenWordError : String -> Review.Test.ExpectedError
forbiddenWordError word =
Review.Test.error
{ message = "`" ++ word ++ "` is not allowed in comments."
, details =
[ "You should review this comment and make sure the forbidden word has been removed before publishing your code."
]
, under = word
}
forbiddenWordErrorForReadme : String -> Review.Test.ExpectedError
forbiddenWordErrorForReadme word =
Review.Test.error
{ message = "`" ++ word ++ "` is not allowed in README file."
, details =
[ "You should review this section and make sure the forbidden word has been removed before publishing your code."
]
, under = word
}
forbiddenWordErrorForElmJson : String -> Review.Test.ExpectedError
forbiddenWordErrorForElmJson word =
Review.Test.error
{ message = "`" ++ word ++ "` is not allowed in elm.json summary."
, details =
[ "You should review your elm.json and make sure the forbidden word has been removed before publishing your code."
]
, under = word
}
--- HELPERS
createElmJson : String -> { path : String, raw : String, project : Elm.Project.Project }
createElmJson rawElmJson =
{ path = "elm.json"
, raw = rawElmJson
, project = createElmJsonProject rawElmJson
}
createElmJsonProject : String -> Elm.Project.Project
createElmJsonProject rawElmJson =
case Decode.decodeString Elm.Project.decoder rawElmJson of
Ok project ->
project
Err error ->
Debug.todo ("[elm.json]: " ++ Debug.toString error)