Compare commits

...

90 Commits

Author SHA1 Message Date
Jeroen Engels
be7c5d08a2 DEBUG 2024-04-07 23:29:21 +02:00
Jeroen Engels
ae605ee249 DEBUG 2024-04-07 23:26:54 +02:00
Jeroen Engels
49ad03d59d Use FilePattern 2024-04-07 23:26:53 +02:00
Jeroen Engels
ade886ddd1 Handle variable case 2024-04-07 23:23:23 +02:00
Jeroen Engels
c955278351 Add ModuleNameLookupTable to arguments 2024-04-07 23:23:23 +02:00
Jeroen Engels
00c344b88c Add variant UngraspableExpression 2024-04-07 23:23:23 +02:00
Jeroen Engels
5b7ffece53 Understand case expressions 2024-04-07 23:23:23 +02:00
Jeroen Engels
34ba41ae46 Understand if expressions 2024-04-07 23:23:23 +02:00
Jeroen Engels
c8bcd974e7 Add ClassFunction.fromExpression 2024-04-07 23:23:23 +02:00
Jeroen Engels
909f509971 Add test for if 2024-04-07 23:23:23 +02:00
Jeroen Engels
1a0b092946 Cleanup CSS rule 2024-04-07 23:23:23 +02:00
Jeroen Engels
b4d79d360b Move CSS class functions over to ClassFunctions 2024-04-07 23:23:23 +02:00
Jeroen Engels
2bd6bf8421 Remove unused CssFunctions 2024-04-07 23:23:23 +02:00
Jeroen Engels
982903435c DEBUG Add CSS rule to configuration 2024-04-07 23:23:23 +02:00
Jeroen Engels
eb3d948207 Add Class.none 2024-04-07 23:23:23 +02:00
Jeroen Engels
e7d0055cb8 Report error when function is missing some arguments 2024-04-07 23:23:23 +02:00
Jeroen Engels
c09fe19c99 Change CssFunctions data structure 2024-04-07 23:23:23 +02:00
Jeroen Engels
4af35c8d88 Add test 2024-04-07 23:23:23 +02:00
Jeroen Engels
4f0b79b0af Report stray classes 2024-04-07 23:23:23 +02:00
Jeroen Engels
e5e25719e5 Extract function 2024-04-07 23:23:23 +02:00
Jeroen Engels
d82bf7baf9 Collect locations where we find a class function 2024-04-07 23:23:23 +02:00
Jeroen Engels
93cb8f89d8 Add test 2024-04-07 23:23:23 +02:00
Jeroen Engels
51fd359c58 Report Html.Attributes.attribute "class" 2024-04-07 23:23:23 +02:00
Jeroen Engels
77e9ee4c0b Add documentation for Css.NoUnknownClasses 2024-04-07 23:23:23 +02:00
Jeroen Engels
8d0255af87 Extract ClassFunction 2024-04-07 23:23:23 +02:00
Jeroen Engels
4ced30b8a8 Rename rule 2024-04-07 23:23:23 +02:00
Jeroen Engels
28f6ffc213 Rename builder function 2024-04-07 23:23:23 +02:00
Jeroen Engels
687440cf8d Take globs by default 2024-04-07 23:23:23 +02:00
Jeroen Engels
a6c866b187 Clean up 2024-04-07 23:23:23 +02:00
Jeroen Engels
c250973325 Display a list of similar classes 2024-04-07 23:23:23 +02:00
Jeroen Engels
cc2f78f3ec Support parsing classes with dashes and underscore 2024-04-07 23:23:23 +02:00
Jeroen Engels
d78b9a3f49 Report when a CSS is not parsable 2024-04-07 23:23:23 +02:00
Jeroen Engels
d7ac221781 Allow configuring custom CSS functions 2024-04-07 23:23:23 +02:00
Jeroen Engels
d454814e70 Add Svg.class 2024-04-07 23:23:23 +02:00
Jeroen Engels
f9cee7c9a3 Support Html.Attributes.classList 2024-04-07 23:23:23 +02:00
Jeroen Engels
13e0d0b832 Report an error when encountering a non-literal CSS class 2024-04-07 23:23:23 +02:00
Jeroen Engels
2740b7b8eb Use appropriate cssUsingFunction 2024-04-07 23:23:23 +02:00
Jeroen Engels
cd9d1237e9 Define css functions 2024-04-07 23:23:23 +02:00
Jeroen Engels
4470445c3a Extract 2024-04-07 23:23:23 +02:00
Jeroen Engels
98851c28f8 Simplify parser even more 2024-04-07 23:23:23 +02:00
Jeroen Engels
e376deb41d Add naive and simplified CSS parser to get selectors 2024-04-07 23:23:23 +02:00
Jeroen Engels
2c583e664c Start CSS parser 2024-04-07 23:23:23 +02:00
Jeroen Engels
6ed6b6e4d3 Read CSS files 2024-04-07 23:23:23 +02:00
Jeroen Engels
c9f3524885 Support pipes 2024-04-07 23:23:23 +02:00
Jeroen Engels
be2be4fb3d Only pass range 2024-04-07 23:23:23 +02:00
Jeroen Engels
7dab184c00 Add tests for pipes 2024-04-07 23:23:23 +02:00
Jeroen Engels
fdc93a71df Don't report if in known classes 2024-04-07 23:23:23 +02:00
Jeroen Engels
3aee5f68fb Add configuration 2024-04-07 23:23:23 +02:00
Jeroen Engels
c7cb65fced Report CSS classes 2024-04-07 23:23:23 +02:00
Jeroen Engels
cedf1c240a Init NoUnknownCssClasses 2024-04-07 23:23:23 +02:00
Jeroen Engels
4ad1dcfcdd Rename string to pattern 2024-04-07 23:23:01 +02:00
Jeroen Engels
45842b2234 Use FilePattern when requesting files 2024-04-07 22:54:21 +02:00
Jeroen Engels
038819ae49 Remove TODOs 2024-04-07 22:30:41 +02:00
Jeroen Engels
04b3b9fa2a Remove parsing inside constructors 2024-04-07 22:29:36 +02:00
Jeroen Engels
4129685142 Copy parsing inside compact 2024-04-07 22:28:29 +02:00
Jeroen Engels
a3413facb2 Rename 2024-04-07 22:22:53 +02:00
Jeroen Engels
701eac7b95 Reverse lists at the correct location 2024-04-07 22:21:57 +02:00
Jeroen Engels
f9f0f1b0b5 Add fuzz test for folders 2024-04-07 22:18:49 +02:00
Jeroen Engels
98f08aa62a Format 2024-04-07 22:15:06 +02:00
Jeroen Engels
a70dae14d7 Fix toStrings 2024-04-07 22:14:55 +02:00
Jeroen Engels
e2eb764566 Specify fuzz test 2024-04-07 22:11:48 +02:00
Jeroen Engels
2d073c9317 describe 2024-04-07 20:04:56 +02:00
Jeroen Engels
be560d38be Make a record 2024-04-07 20:03:58 +02:00
Jeroen Engels
847decf1b4 Add excludeFoldersStrings 2024-04-07 20:00:54 +02:00
Jeroen Engels
67e94e484a Collect strings 2024-04-07 19:37:39 +02:00
Jeroen Engels
bdf9173cd0 fixup! WIP on a: a9eef342 Add fuzz test 2024-04-07 19:32:36 +02:00
Jeroen Engels
d18a7452c8 Include raw string 2024-04-07 19:32:14 +02:00
Jeroen Engels
2e92713315 Use a boolean 2024-04-07 19:29:50 +02:00
Jeroen Engels
a9eef34293 Add fuzz test 2024-04-07 19:27:48 +02:00
Jeroen Engels
2a0cc68767 Merge functions 2024-04-07 19:14:40 +02:00
Jeroen Engels
d620dde379 Rename 2024-04-07 19:02:13 +02:00
Jeroen Engels
8b7662cdfc Change record field order 2024-04-07 19:01:18 +02:00
Jeroen Engels
081aefae99 Rename 2024-04-07 18:59:37 +02:00
Jeroen Engels
348e70e256 Add missing dependencies 2024-04-07 18:13:26 +02:00
Jeroen Engels
47f662db3e Remove unused import 2024-04-07 17:46:58 +02:00
Jeroen Engels
955d2837de Cleanup 2024-04-07 17:34:49 +02:00
Jeroen Engels
d1ef356e3f Remove Debug.log 2024-04-07 17:27:34 +02:00
Jeroen Engels
56c17ba5fa Publicly expose Review.FilePattern 2024-04-07 17:27:00 +02:00
Jeroen Engels
3b62c5d831 Compact globs 2024-04-07 15:27:44 +02:00
Jeroen Engels
113cf7f061 Add TODOs 2024-04-07 09:50:26 +02:00
Jeroen Engels
abb2dce7a7 Support excluding folders 2024-04-07 09:46:18 +02:00
Jeroen Engels
56e75c931f Test 2024-04-07 09:40:17 +02:00
Jeroen Engels
7a230f309e Use an accumulator 2024-04-07 09:39:44 +02:00
Jeroen Engels
620abd615e Make recursive 2024-04-07 09:37:27 +02:00
Jeroen Engels
ca62e5159f Add tests 2024-04-07 09:37:06 +02:00
Jeroen Engels
cc6847f48b Support Include 2024-04-07 09:32:26 +02:00
Jeroen Engels
3d045ddf8a Turn patterns into globs 2024-04-07 09:32:18 +02:00
Jeroen Engels
078e2d0414 Add tests 2024-04-07 09:23:43 +02:00
Jeroen Engels
4245fc1fb0 Add match function 2024-04-06 10:49:03 +02:00
Jeroen Engels
aeec25f60a Rename ExtraFiles to FilePattern 2024-04-06 10:45:09 +02:00
14 changed files with 1880 additions and 59 deletions

View File

@ -9,6 +9,7 @@
"Review.ModuleNameLookupTable",
"Review.Project",
"Review.Project.Dependency",
"Review.FilePattern",
"Review.Fix",
"Review.Test",
"Review.Test.Dependencies",
@ -18,13 +19,13 @@
"dependencies": {
"elm/core": "1.0.2 <= v < 2.0.0",
"elm/json": "1.1.3 <= v < 2.0.0",
"elm/parser": "1.1.0 <= v < 2.0.0",
"elm/project-metadata-utils": "1.0.0 <= v < 2.0.0",
"elm/regex": "1.0.0 <= v < 2.0.0",
"elm-explorations/test": "2.0.1 <= v < 3.0.0",
"stil4m/elm-syntax": "7.2.7 <= v < 8.0.0"
},
"test-dependencies": {
"elm/parser": "1.1.0 <= v < 2.0.0",
"elm/regex": "1.0.0 <= v < 2.0.0",
"pzp1997/assoc-list": "1.0.0 <= v < 2.0.0"
}
}

View File

@ -34,7 +34,9 @@ import NoUnused.Parameters
import NoUnused.Patterns
import NoUnused.Variables
import Review.Rule as Rule exposing (Rule)
import Review.FilePattern as FilePattern
import NoUnused.CustomTypeConstructorArgs
import Css.NoUnknownClasses
config : List Rule
config =
@ -72,6 +74,13 @@ config =
, NoSimpleLetBody.rule
, NoPrematureLetComputation.rule
, NoForbiddenWords.rule [ "REPLACEME" ]
, Css.NoUnknownClasses.cssFiles [ FilePattern.include "src/**/*.css"
, FilePattern.exclude "**/Equalizer.css"
, FilePattern.excludeFolder "src/NotCss"
, FilePattern.include "src/NotCss/bar.css"
]
-- |> Css.NoUnknownClasses.withCssUsingFunctions cssUsingFunctions
|> Css.NoUnknownClasses.rule
]
|> List.map (Rule.ignoreErrorsForDirectories [ "src/Vendor/", "tests/Vendor/" ])
|> List.map (Rule.ignoreErrorsForFiles [ "tests/NoUnused/Patterns/NameVisitor.elm" ])

View File

@ -1,22 +0,0 @@
module Review.ExtraFiles exposing (ExtraFileRequest, exclude, excludeFolder, include)
type ExtraFileRequest
= Include String
| Exclude String
| ExcludeFolder String
include : String -> ExtraFileRequest
include =
Include
exclude : String -> ExtraFileRequest
exclude =
Exclude
excludeFolder : String -> ExtraFileRequest
excludeFolder =
ExcludeFolder

238
src/Review/FilePattern.elm Normal file
View File

@ -0,0 +1,238 @@
module Review.FilePattern exposing (FilePattern, compact, exclude, excludeFolder, include, match, toStrings)
import Glob exposing (Glob)
type FilePattern
= Include String
| Exclude String
| ExcludeFolder String
| InvalidGlob String
type alias Summary =
{ includeExclude : List CompactFilePattern
, excludedFolders : List Glob
, strings : List { pattern : String, included : Bool }
, excludedFoldersStrings : List String
}
toStrings : Summary -> { files : List { pattern : String, included : Bool }, excludedFolders : List String }
toStrings summary =
{ files = summary.strings
, excludedFolders = summary.excludedFoldersStrings
}
type CompactFilePattern
= CompactInclude (List Glob)
| CompactExclude (List Glob)
compact : List FilePattern -> Result (List String) Summary
compact filePatterns =
compactBase filePatterns
{ includeExclude = []
, excludedFolders = []
, strings = []
, excludedFoldersStrings = []
}
|> Result.map
(\summary ->
{ includeExclude = summary.includeExclude
, excludedFolders = summary.excludedFolders
, strings = List.reverse summary.strings
, excludedFoldersStrings = List.reverse summary.excludedFoldersStrings
}
)
compactBase : List FilePattern -> Summary -> Result (List String) Summary
compactBase filePatterns accSummary =
case filePatterns of
[] ->
Ok accSummary
(Include raw) :: rest ->
case Glob.fromString raw of
Ok pattern ->
compactHelp rest [ pattern ] True (addRawIncludeExclude raw True accSummary)
Err _ ->
Err (compactErrors rest [ raw ])
(Exclude raw) :: rest ->
case Glob.fromString raw of
Ok pattern ->
compactHelp rest [ pattern ] False (addRawIncludeExclude raw False accSummary)
Err _ ->
Err (compactErrors rest [ raw ])
(ExcludeFolder raw) :: rest ->
case Glob.fromString (toFolder raw) of
Ok pattern ->
compactBase rest
{ includeExclude = accSummary.includeExclude
, excludedFolders = pattern :: accSummary.excludedFolders
, strings = accSummary.strings
, excludedFoldersStrings = raw :: accSummary.excludedFoldersStrings
}
Err _ ->
Err (compactErrors rest [ raw ])
(InvalidGlob pattern) :: rest ->
Err (compactErrors rest [ pattern ])
compactHelp : List FilePattern -> List Glob -> Bool -> Summary -> Result (List String) Summary
compactHelp filePatterns accGlobs included accSummary =
case filePatterns of
[] ->
Ok
{ includeExclude =
(if included then
CompactInclude accGlobs
else
CompactExclude accGlobs
)
:: accSummary.includeExclude
, excludedFolders = accSummary.excludedFolders
, strings = accSummary.strings
, excludedFoldersStrings = accSummary.excludedFoldersStrings
}
(Include raw) :: rest ->
case Glob.fromString raw of
Ok pattern ->
if included then
compactHelp rest (pattern :: accGlobs) included (addRawIncludeExclude raw included accSummary)
else
compactHelp rest
[ pattern ]
True
{ includeExclude = CompactExclude accGlobs :: accSummary.includeExclude
, excludedFolders = accSummary.excludedFolders
, strings = { pattern = raw, included = True } :: accSummary.strings
, excludedFoldersStrings = accSummary.excludedFoldersStrings
}
Err _ ->
Err (compactErrors rest [ raw ])
(Exclude raw) :: rest ->
case Glob.fromString raw of
Ok pattern ->
if included then
compactHelp rest
[ pattern ]
False
{ includeExclude = CompactInclude accGlobs :: accSummary.includeExclude
, excludedFolders = accSummary.excludedFolders
, strings = { pattern = raw, included = False } :: accSummary.strings
, excludedFoldersStrings = accSummary.excludedFoldersStrings
}
else
compactHelp rest (pattern :: accGlobs) included (addRawIncludeExclude raw included accSummary)
Err _ ->
Err (compactErrors rest [ raw ])
(ExcludeFolder raw) :: rest ->
case Glob.fromString (toFolder raw) of
Ok pattern ->
compactHelp rest
accGlobs
included
{ includeExclude = accSummary.includeExclude
, excludedFolders = pattern :: accSummary.excludedFolders
, strings = accSummary.strings
, excludedFoldersStrings = raw :: accSummary.excludedFoldersStrings
}
Err _ ->
Err (compactErrors rest [ raw ])
(InvalidGlob invalidGlobStr) :: rest ->
Err (compactErrors rest [ invalidGlobStr ])
compactErrors : List FilePattern -> List String -> List String
compactErrors filePatterns accGlobStrings =
case filePatterns of
[] ->
List.reverse accGlobStrings
(InvalidGlob invalidGlobStr) :: rest ->
compactErrors rest (invalidGlobStr :: accGlobStrings)
_ :: rest ->
compactErrors rest accGlobStrings
addRawIncludeExclude : String -> Bool -> Summary -> Summary
addRawIncludeExclude string included summary =
{ includeExclude = summary.includeExclude
, excludedFolders = summary.excludedFolders
, strings = { pattern = string, included = included } :: summary.strings
, excludedFoldersStrings = summary.excludedFoldersStrings
}
include : String -> FilePattern
include =
Include
exclude : String -> FilePattern
exclude =
Exclude
excludeFolder : String -> FilePattern
excludeFolder =
ExcludeFolder
toFolder : String -> String
toFolder globStr =
if String.endsWith "/" globStr then
globStr ++ "**/*"
else
globStr ++ "/**/*"
match : Summary -> String -> Bool
match summary str =
if List.any (\folderGlob -> Glob.match folderGlob str) summary.excludedFolders then
False
else
matchHelp summary.includeExclude str
matchHelp : List CompactFilePattern -> String -> Bool
matchHelp filePatterns str =
case filePatterns of
[] ->
False
(CompactInclude globs) :: rest ->
if List.any (\glob -> Glob.match glob str) globs then
True
else
matchHelp rest str
(CompactExclude globs) :: rest ->
if List.any (\glob -> Glob.match glob str) globs then
False
else
matchHelp rest str

View File

@ -6,7 +6,7 @@ type RequestedData
{ moduleNameLookupTable : Bool
, sourceCodeExtractor : Bool
, ignoredFiles : Bool
, files : List String
, files : List { files : List { pattern : String, included : Bool }, excludedFolders : List String }
}
@ -35,7 +35,7 @@ combine maybeA maybeB =
a
withFiles : List String -> RequestedData -> RequestedData
withFiles : List { files : List { pattern : String, included : Bool }, excludedFolders : List String } -> RequestedData -> RequestedData
withFiles files ((RequestedData requested) as untouched) =
if List.isEmpty files then
untouched

View File

@ -326,6 +326,7 @@ import Review.ElmProjectEncoder
import Review.Error exposing (InternalError)
import Review.Exceptions as Exceptions exposing (Exceptions)
import Review.FilePath exposing (FilePath)
import Review.FilePattern as FilePattern exposing (FilePattern)
import Review.Fix as Fix exposing (Fix, FixResult(..))
import Review.Fix.FixProblem as FixProblem
import Review.Fix.FixedErrors as FixedErrors exposing (FixedErrors)
@ -414,7 +415,7 @@ type alias ModuleRuleSchemaData moduleContext =
type alias ExtraFileRequest =
Result (List String) (List StringableGlob)
Result (List String) (List { files : List { pattern : String, included : Bool }, excludedFolders : List String })
type alias StringableGlob =
@ -893,7 +894,7 @@ ruleKnowsAboutIgnoredFiles (Rule rule) =
{-| REPLACEME
-}
ruleRequestedFiles : Rule -> List String
ruleRequestedFiles : Rule -> List { files : List { pattern : String, included : Bool }, excludedFolders : List String }
ruleRequestedFiles (Rule rule) =
let
(RequestedData requestedData) =
@ -1314,7 +1315,7 @@ fromProjectRuleSchema (ProjectRuleSchema schema) =
(Maybe.map requestedDataFromContextCreator schema.moduleContextCreator)
(Maybe.map (.fromModuleToProject >> requestedDataFromContextCreator) schema.folder)
-- TODO Keep the original globs as strings and pass them here
|> RequestedData.withFiles (List.map .string extraFileGlobs)
|> RequestedData.withFiles extraFileGlobs
, providesFixes = schema.providesFixes
, ruleProjectVisitor =
Ok
@ -1907,21 +1908,17 @@ withReadmeProjectVisitor visitor (ProjectRuleSchema schema) =
{-| REPLACEME
-}
withExtraFilesProjectVisitor :
List String
List FilePattern
-> (List { fileKey : ExtraFileKey, path : String, content : String } -> projectContext -> ( List (Error { useErrorForModule : () }), projectContext ))
-> ProjectRuleSchema schemaState projectContext moduleContext
-> ProjectRuleSchema { schemaState | hasAtLeastOneVisitor : () } projectContext moduleContext
withExtraFilesProjectVisitor requestedFiles baseVisitor (ProjectRuleSchema schema) =
case parseGlobs requestedFiles of
Ok stringableGlobs ->
withExtraFilesProjectVisitor filePatterns baseVisitor (ProjectRuleSchema schema) =
case FilePattern.compact filePatterns of
Ok filePatternSummary ->
let
globs : List Glob
globs =
List.map .glob stringableGlobs
visitor : List { fileKey : ExtraFileKey, path : String, content : String } -> projectContext -> ( List (Error {}), projectContext )
visitor files context =
baseVisitor (List.filter (globMatch globs) files) context
baseVisitor (List.filter (\file -> FilePattern.match filePatternSummary file.path) files) context
|> Tuple.mapFirst removeErrorPhantomTypes
in
ProjectRuleSchema
@ -1930,9 +1927,9 @@ withExtraFilesProjectVisitor requestedFiles baseVisitor (ProjectRuleSchema schem
, extraFileRequest =
case schema.extraFileRequest of
Ok previous ->
Ok (previous ++ stringableGlobs)
Ok (FilePattern.toStrings filePatternSummary :: previous)
Err previous ->
Err _ ->
schema.extraFileRequest
}
@ -2465,21 +2462,17 @@ withElmJsonModuleVisitor visitor (ModuleRuleSchema schema) =
{-| REPLACEME
-}
withExtraFilesModuleVisitor :
List String
List FilePattern
-> (List { path : String, content : String } -> moduleContext -> moduleContext)
-> ModuleRuleSchema { schemaState | canCollectProjectData : () } moduleContext
-> ModuleRuleSchema { schemaState | canCollectProjectData : () } moduleContext
withExtraFilesModuleVisitor requestedFiles baseVisitor (ModuleRuleSchema schema) =
case parseGlobs requestedFiles of
Ok stringableGlobs ->
withExtraFilesModuleVisitor filePatterns baseVisitor (ModuleRuleSchema schema) =
case FilePattern.compact filePatterns of
Ok filePatternSummary ->
let
globs : List Glob
globs =
List.map .glob stringableGlobs
visitor : List { path : String, content : String } -> moduleContext -> moduleContext
visitor files context =
baseVisitor (List.filter (globMatch globs) files) context
baseVisitor (List.filter (\file -> FilePattern.match filePatternSummary file.path) files) context
in
ModuleRuleSchema
{ schema
@ -2487,9 +2480,9 @@ withExtraFilesModuleVisitor requestedFiles baseVisitor (ModuleRuleSchema schema)
, extraFileRequest =
case schema.extraFileRequest of
Ok previous ->
Ok (previous ++ stringableGlobs)
Ok (FilePattern.toStrings filePatternSummary :: previous)
Err previous ->
Err _ ->
schema.extraFileRequest
}

136
tests/Css/ClassFunction.elm Normal file
View File

@ -0,0 +1,136 @@
module Css.ClassFunction exposing
( CssArgument(..)
, fromLiteral
, baseCssFunctions
, Arguments, firstArgumentIsClass, htmlAttributesAttribute, htmlAttributesClassList
, smartFirstArgumentIsClass
)
{-|
@docs CssArgument
@docs fromLiteral
@docs baseCssFunctions
@docs Arguments, firstArgumentIsClass, htmlAttributesAttribute, htmlAttributesClassList
@docs smartFirstArgumentIsClass
-}
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import Elm.Syntax.Range exposing (Range)
import Review.ModuleNameLookupTable as ModuleNameLookupTable exposing (ModuleNameLookupTable)
type CssArgument
= Literal String
| Variable Range
| UngraspableExpression Range
| MissingArgument Int
type alias Arguments =
{ firstArgument : Node Expression
, restOfArguments : List (Node Expression)
, lookupTable : ModuleNameLookupTable
}
fromLiteral : Node Expression -> CssArgument
fromLiteral node =
case Node.value node of
Expression.Literal str ->
Literal str
_ ->
UngraspableExpression (Node.range node)
fromExpression : ModuleNameLookupTable -> Node Expression -> List CssArgument
fromExpression lookupTable node =
fromExpressionHelp lookupTable [ node ] []
fromExpressionHelp : ModuleNameLookupTable -> List (Node Expression) -> List CssArgument -> List CssArgument
fromExpressionHelp lookupTable nodes acc =
case nodes of
[] ->
acc
node :: rest ->
case Node.value node of
Expression.Literal str ->
fromExpressionHelp lookupTable rest (Literal str :: acc)
Expression.FunctionOrValue [] name ->
case ModuleNameLookupTable.moduleNameFor lookupTable node of
Just [] ->
fromExpressionHelp lookupTable rest (Variable (Node.range node) :: acc)
_ ->
fromExpressionHelp lookupTable rest (UngraspableExpression (Node.range node) :: acc)
Expression.ParenthesizedExpression expr ->
fromExpressionHelp lookupTable (expr :: rest) acc
Expression.IfBlock _ then_ else_ ->
fromExpressionHelp lookupTable (then_ :: else_ :: rest) acc
Expression.CaseExpression { cases } ->
fromExpressionHelp lookupTable (List.foldl (\( _, expr ) nodesAcc -> expr :: nodesAcc) rest cases) acc
_ ->
fromExpressionHelp lookupTable rest (UngraspableExpression (Node.range node) :: acc)
baseCssFunctions : List ( String, Arguments -> List CssArgument )
baseCssFunctions =
[ ( "Html.Attributes.class", \{ firstArgument } -> [ fromLiteral firstArgument ] )
, ( "Svg.Attributes.class", \{ firstArgument } -> [ fromLiteral firstArgument ] )
, ( "Html.Attributes.classList", htmlAttributesClassList )
, ( "Html.Attributes.attribute", htmlAttributesAttribute )
]
firstArgumentIsClass : Arguments -> List CssArgument
firstArgumentIsClass { firstArgument } =
[ fromLiteral firstArgument ]
smartFirstArgumentIsClass : Arguments -> List CssArgument
smartFirstArgumentIsClass { lookupTable, firstArgument } =
fromExpression lookupTable firstArgument
htmlAttributesAttribute : Arguments -> List CssArgument
htmlAttributesAttribute { firstArgument, restOfArguments } =
case Node.value firstArgument of
Expression.Literal "class" ->
case restOfArguments of
[] ->
[ MissingArgument 2 ]
classArgument :: _ ->
[ fromLiteral classArgument ]
_ ->
[]
htmlAttributesClassList : Arguments -> List CssArgument
htmlAttributesClassList { firstArgument } =
case Node.value firstArgument of
Expression.ListExpr list ->
List.map
(\element ->
case Node.value element of
Expression.TupledExpression [ first, _ ] ->
fromLiteral first
_ ->
Variable (Node.range element)
)
list
_ ->
[ fromLiteral firstArgument ]

View File

@ -0,0 +1,585 @@
module Css.NoUnknownClasses exposing
( withCssUsingFunctions
, addKnownClasses, cssFiles, rule
)
{-| Reports... REPLACEME
config =
[ NoUnknownCssClasses.rule
]
## Fail
import Html
import Html.Attributes
a =
Html.span
[ Html.Attributes.class "unknown" ]
[ Html.text "Some text" ]
## Success
```css
.red { color: red; }
.bold { font-weight: bold; }
```
import Html
import Html.Attributes
a =
Html.span
[ Html.Attributes.class "red bold" ]
[ Html.text "Some text" ]
## Class module
module Class exposing (Class, batch, fromString, none, toAttr)
{-| TODO
-}
import Html
import Html.Attributes
type Class
= Class (List String)
none : Class
none =
Class []
fromString : String -> Class
fromString class =
Class [ class ]
batch : List Class -> Class
batch classes =
classes
|> List.concatMap (\(Class classes) -> classes)
|> Class
toAttr : Class -> Html.Attribute never
toAttr (Class classes) =
Html.Attributes.class (String.join " " classes)
A nice benefit of using this approach is that if you have an element that accepts arbitrary styling through arguments,
you can pass a `Class` instead of a much more permissive `Html.Attribute msg`.
import Html
import Html.Attributes
import Html.Events
viewThing : Html.Attribute msg -> String -> Html msg
viewThing styling text =
Html.div
[ Html.id "thing"
, styling
]
[ Html.text text ]
exampleView =
viewThing
-- ❌ This attribute was misused to become an event handler!
(Html.Events.onClick ThisIsNotAStylingAttribute)
"Some text"
You can make sure this doesn't happen using this more restrictive `Class` approach.
import Class
import Html
viewThing : Class -> String -> Html msg
viewThing styling text =
Html.div
[ Html.id "thing"
, Class.toAttr styling
]
[ Html.text text ]
exampleView =
viewThing
(Class.fromString "red bold")
"Some text"
TODO Indicate how to configure the rule to greenlight this `Class.fromString`
## Configuring your own class functions
@docs withCssUsingFunctions
## When (not) to enable this rule
This rule is useful in web applications that use plain CSS files for the styling.
Given sufficiently enough information through [`addKnownClasses`](addKnownClasses), it could be useful
for frameworks such as Tailwind.
To work correctly, this rule does need some work from you to avoid having expressions.
This rule is useful when REPLACEME.
This rule is not useful when REPLACEME.
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/elm-review/example --rules NoUnknownCssClasses
```
-}
import Css.ClassFunction as ClassFunction exposing (CssArgument)
import Dict exposing (Dict)
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node(..))
import Elm.Syntax.Range exposing (Range)
import Levenshtein
import Parser exposing ((|.), (|=), Parser)
import RangeDict exposing (RangeDict)
import Review.FilePattern exposing (FilePattern)
import Review.ModuleNameLookupTable as ModuleNameLookupTable exposing (ModuleNameLookupTable)
import Review.Rule as Rule exposing (Rule)
import Set exposing (Set)
rule : Configuration -> Rule
rule (Configuration configuration) =
Rule.newProjectRuleSchema "Css.NoUnknownClasses" (initialProjectContext configuration.knownClasses)
|> Rule.withExtraFilesProjectVisitor configuration.cssFiles cssFilesVisitor
|> Rule.withModuleVisitor (moduleVisitor configuration.cssFunctions)
|> Rule.withModuleContextUsingContextCreator
{ fromProjectToModule = fromProjectToModule
, fromModuleToProject = fromModuleToProject
, foldProjectContexts = foldProjectContexts
}
|> Rule.fromProjectRuleSchema
type Configuration
= Configuration
{ knownClasses : Set String
, cssFiles : List FilePattern
, cssFunctions : CssFunctions
}
cssFiles : List FilePattern -> Configuration
cssFiles globs =
Configuration
{ knownClasses = Set.empty
, cssFiles = globs
, cssFunctions = Dict.fromList ClassFunction.baseCssFunctions
}
cssFilesVisitor : List { fileKey : Rule.ExtraFileKey, path : String, content : String } -> ProjectContext -> ( List (Rule.Error scope), ProjectContext )
cssFilesVisitor files context =
let
_ =
Debug.log "loaded files" (List.map .path files)
( errors, knownClasses ) =
List.foldl parseCssFile ( [], context.knownClasses ) files
in
( errors, { knownClasses = knownClasses } )
addKnownClasses : List String -> Configuration -> Configuration
addKnownClasses list (Configuration configuration) =
Configuration { configuration | knownClasses = List.foldl Set.insert configuration.knownClasses list }
{-| By default, only `Html.Attributes.class`, `Html.Attributes.classList` and `Svg.Attributes.class` are used
to find class usages in the application.
You may have your own trusted functions (such as the `Class` module suggested above) that are allowed to take
`String` arguments to be considered as CSS classes. You can use this function along with the
[`Css.ClassFunction` module](#TODO) to explain to the rule which arguments are/contain what should be considered
as CSS classes.
TODO Move this documentation into the `ClassFunction` module
This example is for adding `Class.fromAttr`.
import Css.ClassFunction
import Css.NoUnknownClasses
-- TODO Add missing imports
config =
[ Css.NoUnknownClasses.cssFiles whereYourCssFilesAre
|> Css.NoUnknownClasses.withCssUsingFunctions cssUsingFunctions
|> Css.NoUnknownClasses.rule
]
cssUsingFunctions : Dict ( ModuleName, String ) (ClassFunction.Arguments -> List ClassFunction.CssArgument)
cssUsingFunctions =
Dict.fromList [ ( ( [ "Class" ], "fromString" ), classFromStringFunction ) ]
classFromStringFunction : ClassFunction.Arguments -> List ClassFunction.CssArgument
classFromStringFunction { firstArgument } =
[ ClassFunction.fromLiteral firstArgument ]
Here is how `Html.Attributes.classList` is implemented (Reminder of an example usage: `Html.Attributes.classList [ ("some-class", booleanValue ) ]`):
import Css.NoUnknownClasses
import Css.ClassFunction
-- TODO Add missing imports
cssUsingFunctions : Dict ( ModuleName, String ) (ClassFunction.Arguments -> List ClassFunction.CssArgument)
cssUsingFunctions =
Dict.fromList
[ ( ( [ "Html", "Attributes" ], "classList" ), \{ firstArgument } -> htmlAttributesClassList firstArgument )
-- , ...
]
]
htmlAttributesClassList : Node Expression -> List ClassFunction.CssArgument
htmlAttributesClassList node =
case Node.value node of
Expression.ListExpr list ->
List.map
(\element ->
case Node.value element of
Expression.TupledExpression [ first, _ ] ->
ClassFunction.fromLiteral first
_ ->
ClassFunction.Variable (Node.range element)
)
list
_ ->
[ ClassFunction.fromLiteral node ]
-}
withCssUsingFunctions :
List ( String, ClassFunction.Arguments -> List CssArgument )
-> Configuration
-> Configuration
withCssUsingFunctions newFunctions (Configuration configuration) =
Configuration { configuration | cssFunctions = List.foldl (\( key, fn ) acc -> Dict.insert key fn acc) configuration.cssFunctions newFunctions }
type alias ProjectContext =
{ knownClasses : Set String
}
type alias ModuleContext =
{ lookupTable : ModuleNameLookupTable
, knownClasses : Set String
, functionOrValuesToIgnore : RangeDict ()
}
moduleVisitor : CssFunctions -> Rule.ModuleRuleSchema schema ModuleContext -> Rule.ModuleRuleSchema { schema | hasAtLeastOneVisitor : () } ModuleContext
moduleVisitor cssFunctions schema =
schema
|> Rule.withExpressionEnterVisitor (expressionVisitor cssFunctions)
initialProjectContext : Set String -> ProjectContext
initialProjectContext knownClasses =
{ knownClasses = knownClasses
}
fromProjectToModule : Rule.ContextCreator ProjectContext ModuleContext
fromProjectToModule =
Rule.initContextCreator
(\lookupTable projectContext ->
{ lookupTable = lookupTable
, knownClasses = projectContext.knownClasses
, functionOrValuesToIgnore = RangeDict.empty
}
)
|> Rule.withModuleNameLookupTable
fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext
fromModuleToProject =
Rule.initContextCreator
(\_ ->
{ knownClasses = Set.empty
}
)
foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext
foldProjectContexts new previous =
{ knownClasses = previous.knownClasses
}
expressionVisitor : CssFunctions -> Node Expression -> ModuleContext -> ( List (Rule.Error {}), ModuleContext )
expressionVisitor cssFunctions node context =
case Node.value node of
Expression.Application ((Node fnRange (Expression.FunctionOrValue _ name)) :: firstArg :: restOfArguments) ->
reportClasses cssFunctions context fnRange name firstArg restOfArguments
Expression.OperatorApplication "|>" _ firstArg (Node fnRange (Expression.FunctionOrValue _ name)) ->
reportClasses cssFunctions context fnRange name firstArg []
Expression.OperatorApplication "<|" _ (Node fnRange (Expression.FunctionOrValue _ name)) firstArg ->
reportClasses cssFunctions context fnRange name firstArg []
Expression.FunctionOrValue _ name ->
( reportStrayCssFunction cssFunctions context (Node.range node) name
, context
)
_ ->
( [], context )
reportStrayCssFunction : CssFunctions -> ModuleContext -> Range -> String -> List (Rule.Error {})
reportStrayCssFunction cssFunctions context range name =
if RangeDict.member range context.functionOrValuesToIgnore then
[]
else
case
ModuleNameLookupTable.moduleNameAt context.lookupTable range
|> Maybe.andThen (\moduleName -> getCssFunction cssFunctions name moduleName)
of
Just _ ->
[ Rule.error
{ message = "Class using function is used without arguments"
, details = [ "Having the function used without arguments confuses me and will prevent me from figuring out whether the classes passed to this function will be known or unknown. Please pass in all the arguments at the location." ]
}
range
]
Nothing ->
[]
getCssFunction : Dict String v -> String -> List String -> Maybe v
getCssFunction cssFunctions name moduleName =
Dict.get (String.join "." moduleName ++ "." ++ name) cssFunctions
type alias CssFunctions =
Dict String (ClassFunction.Arguments -> List CssArgument)
reportClasses : CssFunctions -> ModuleContext -> Range -> String -> Node Expression -> List (Node Expression) -> ( List (Rule.Error {}), ModuleContext )
reportClasses cssFunctions context fnRange name firstArgument restOfArguments =
case
ModuleNameLookupTable.moduleNameAt context.lookupTable fnRange
|> Maybe.andThen (\moduleName -> getCssFunction cssFunctions name moduleName)
of
Just cssFunction ->
( errorsForCssFunction context.knownClasses cssFunction fnRange { lookupTable = context.lookupTable, firstArgument = firstArgument, restOfArguments = restOfArguments }
, { context | functionOrValuesToIgnore = RangeDict.insert fnRange () context.functionOrValuesToIgnore }
)
Nothing ->
( [], context )
errorsForCssFunction :
Set String
-> (ClassFunction.Arguments -> List CssArgument)
-> Range
-> ClassFunction.Arguments
-> List (Rule.Error {})
errorsForCssFunction knownClasses cssFunction fnRange target =
cssFunction target
|> List.concatMap
(\arg ->
case arg of
ClassFunction.Literal class ->
unknownClasses
knownClasses
(Node.range target.firstArgument)
class
ClassFunction.Variable range ->
[ Rule.error
{ message = "Non-literal argument to CSS class function"
, details = [ "The argument given to this function is not a value that I could interpret. This makes it hard for me to figure out whether this was a known CSS class or not. Please transform this a string literal (\"my-class\")." ]
}
range
]
ClassFunction.UngraspableExpression range ->
[ Rule.error
{ message = "Non-literal argument to CSS class function"
, details = [ "The argument given to this function is not a value that I could interpret. This makes it hard for me to figure out whether this was a known CSS class or not. Please transform this a string literal (\"my-class\")." ]
}
range
]
ClassFunction.MissingArgument index ->
[ Rule.error
{ message = "Class using function is used without all of its CSS class arguments"
, details = [ "Having the function used without all of its arguments confuses me and will prevent me from figuring out whether the classes passed to this function will be known or unknown. Please pass in all the arguments at the location." ]
}
fnRange
]
)
reportError : Set String -> Range -> String -> Rule.Error {}
reportError knownClasses range name =
Rule.error
{ message = "Unknown CSS class \"" ++ name ++ "\""
, details =
"I could not find this class in CSS files. Have you made a typo?"
:: (if Set.isEmpty knownClasses then
[]
else
[ String.join "\n"
("Here are similarly-named classes:"
:: List.map (\str -> " - " ++ str) (similarClasses name knownClasses)
)
]
)
}
range
similarClasses : String -> Set String -> List String
similarClasses targetClass knownClasses =
Set.foldl
(\class ({ first, second } as untouched) ->
let
distance : Int
distance =
computeDistance class targetClass
in
if isSmallerDistance distance first then
{ first = Just { class = class, distance = distance }
, second = first
}
else if isSmallerDistance distance second then
{ first = first
, second = Just { class = class, distance = distance }
}
else
untouched
)
{ first = Nothing, second = Nothing }
knownClasses
|> (\{ first, second } -> List.filterMap (Maybe.map .class) [ first, second ])
isSmallerDistance : Int -> Maybe { a | distance : Int } -> Bool
isSmallerDistance distance maybeElement =
case maybeElement of
Just element ->
distance < element.distance
Nothing ->
True
computeDistance : String -> String -> Int
computeDistance a b =
Basics.min
(Levenshtein.distance a b)
(Levenshtein.distance b a)
unknownClasses : Set String -> Range -> String -> List (Rule.Error {})
unknownClasses knownClasses range classesStr =
unknownClassesHelp
knownClasses
range.start.row
range.start.column
1
(String.split " " classesStr)
[]
unknownClassesHelp : Set String -> Int -> Int -> Int -> List String -> List (Rule.Error {}) -> List (Rule.Error {})
unknownClassesHelp knownClasses row column offset classes errors =
case classes of
[] ->
errors
class :: rest ->
let
newErrors : List (Rule.Error {})
newErrors =
if Set.member class knownClasses then
errors
else
reportError
knownClasses
{ start = { row = row, column = column + offset }
, end = { row = row, column = column + offset + String.length class }
}
class
:: errors
in
unknownClassesHelp
knownClasses
row
column
(offset + String.length class + 1)
rest
newErrors
---
parseCssFile : { fileKey : Rule.ExtraFileKey, path : String, content : String } -> ( List (Rule.Error externalFile), Set String ) -> ( List (Rule.Error externalFile), Set String )
parseCssFile file ( errors, knownClasses ) =
case Parser.run cssParser file.content of
Ok cssClasses ->
( errors, Set.union cssClasses knownClasses )
Err _ ->
-- Create an error?
( Rule.errorForExtraFile file.fileKey
{ message = "Unable to parse CSS file `some-file.css`"
, details = [ "Please check that this file is syntactically correct. It is possible that I'm mistaken as my CSS parser is still very naive. Contributions are welcome to solve the issue." ]
}
{ start = { row = 1, column = 1 }, end = { row = 1, column = 100000 } }
:: errors
, knownClasses
)
cssParser : Parser (Set String)
cssParser =
Parser.loop Set.empty cssRule
cssRule : Set String -> Parser (Parser.Step (Set String) (Set String))
cssRule acc =
Parser.oneOf
[ Parser.succeed (\selector -> Parser.Loop (Set.insert selector acc))
|. Parser.token "."
|= (Parser.chompWhile (\c -> Char.isAlphaNum c || c == '-' || c == '_')
|> Parser.getChompedString
)
, Parser.end
|> Parser.map (\_ -> Parser.Done acc)
, Parser.succeed (\() -> Parser.Loop acc)
|. Parser.token "{"
|. Parser.chompUntil "}"
|. Parser.token "}"
|= Parser.spaces
, Parser.succeed (\() -> Parser.Loop acc)
|= Parser.chompIf (always True)
]

View File

@ -0,0 +1,387 @@
module Css.NoUnknownClassesTest exposing (all)
import Css.ClassFunction as ClassFunction exposing (CssArgument, fromLiteral)
import Css.NoUnknownClasses exposing (addKnownClasses, cssFiles, rule, withCssUsingFunctions)
import Elm.Syntax.Expression exposing (Expression)
import Elm.Syntax.Node exposing (Node)
import Review.Project as Project exposing (Project)
import Review.Test
import Review.Test.Dependencies
import Test exposing (Test, describe, test)
all : Test
all =
describe "NoUnknownCssClasses"
[ test "should not report an error when strings don't seem to be CSS classes" <|
\() ->
"""module A exposing (..)
import Html
import Html.Attributes as Attr
view model =
Html.span [] [ Html.text "ok" ]
"""
|> Review.Test.run (cssFiles [ "*.css" ] |> rule)
|> Review.Test.expectNoErrors
, test "should report an error when encountering an unknown CSS class through Html.Attributes.class" <|
\() ->
"""module A exposing (..)
import Html
import Html.Attributes as Attr
view model =
Html.span [ Attr.class "unknown" ] []
"""
|> Review.Test.run (cssFiles [ "*.css" ] |> addKnownClasses [ "known", "bar", "unknown2" ] |> rule)
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Unknown CSS class \"unknown\""
, details =
[ "I could not find this class in CSS files. Have you made a typo?"
, "Here are similarly-named classes:\n - unknown2\n - known"
]
, under = "unknown"
}
]
, test "should not report an error when encountering an CSS class specified in the configuration" <|
\() ->
"""module A exposing (..)
import Html
import Html.Attributes as Attr
view model =
Html.span [ Attr.class "known" ] []
"""
|> Review.Test.run (cssFiles [ "*.css" ] |> addKnownClasses [ "known" ] |> rule)
|> Review.Test.expectNoErrors
, test "should report an error when encountering an unknown CSS class through Html.Attributes.class in <| pipe" <|
\() ->
"""module A exposing (..)
import Html
import Html.Attributes as Attr
view model =
Html.span [ Attr.class <| "unknown" ] []
"""
|> Review.Test.run (cssFiles [ "*.css" ] |> rule)
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Unknown CSS class \"unknown\""
, details = [ "I could not find this class in CSS files. Have you made a typo?" ]
, under = "unknown"
}
]
, test "should report an error when encountering an unknown CSS class through Html.Attributes.class in |> pipe" <|
\() ->
"""module A exposing (..)
import Html
import Html.Attributes as Attr
view model =
Html.span [ "unknown" |> Attr.class ] []
"""
|> Review.Test.run (cssFiles [ "*.css" ] |> rule)
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Unknown CSS class \"unknown\""
, details = [ "I could not find this class in CSS files. Have you made a typo?" ]
, under = "unknown"
}
]
, test "should not report an error when encountering CSS classes found in specified files" <|
\() ->
"""module A exposing (..)
import Html
import Html.Attributes as Attr
view model =
Html.span [ "known red-faint under_score" |> Attr.class ] []
"""
|> Review.Test.runWithProjectData projectWithCssClasses (cssFiles [ "*.css" ] |> rule)
|> Review.Test.expectNoErrors
, test "should report an error when encountering a non-literal argument for Html.Attributes.class" <|
\() ->
"""module A exposing (..)
import Html
import Html.Attributes as Attr
view model =
Attr.class model.class
"""
|> Review.Test.run (cssFiles [ "*.css" ] |> rule)
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Non-literal argument to CSS class function"
, details = [ "The argument given to this function is not a value that I could interpret. This makes it hard for me to figure out whether this was a known CSS class or not. Please transform this a string literal (\"my-class\")." ]
, under = "model.class"
}
]
, test "should report an error when encountering a non-literal argument for Html.Attributes.classList" <|
\() ->
"""module A exposing (..)
import Html
import Html.Attributes as Attr
view model =
Attr.classList model.classList
"""
|> Review.Test.run (cssFiles [ "*.css" ] |> rule)
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Non-literal argument to CSS class function"
, details = [ "The argument given to this function is not a value that I could interpret. This makes it hard for me to figure out whether this was a known CSS class or not. Please transform this a string literal (\"my-class\")." ]
, under = "model.classList"
}
]
, test "should report an error when encountering non-literal CSS classes for Html.Attributes.classList" <|
\() ->
"""module A exposing (..)
import Html
import Html.Attributes as Attr
view model =
Attr.classList
[ ( "known", model.a )
, ( variable, model.b )
]
"""
|> Review.Test.run (cssFiles [ "*.css" ] |> addKnownClasses [ "known" ] |> rule)
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Non-literal argument to CSS class function"
, details = [ "The argument given to this function is not a value that I could interpret. This makes it hard for me to figure out whether this was a known CSS class or not. Please transform this a string literal (\"my-class\")." ]
, under = "variable"
}
]
, test "should report an error when encountering a non-literal argument to Html.Attributes.attribute \"class\"" <|
\() ->
"""module A exposing (..)
import Html
import Html.Attributes as Attr
view model =
Attr.attribute "class" model.class
"""
|> Review.Test.run (cssFiles [ "*.css" ] |> rule)
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Non-literal argument to CSS class function"
, details = [ "The argument given to this function is not a value that I could interpret. This makes it hard for me to figure out whether this was a known CSS class or not. Please transform this a string literal (\"my-class\")." ]
, under = "model.class"
}
]
, test "should not report an error when encountering a known class argument to Html.Attributes.attribute \"class\"" <|
\() ->
"""module A exposing (..)
import Html
import Html.Attributes as Attr
view model =
Attr.attribute "class" "known"
"""
|> Review.Test.run (cssFiles [ "*.css" ] |> addKnownClasses [ "known" ] |> rule)
|> Review.Test.expectNoErrors
, test "should not report an error when Html.Attributes.attribute is used with something else than \"class\"" <|
\() ->
"""module A exposing (..)
import Html
import Html.Attributes as Attr
view model =
Attr.attribute "id" model.id
"""
|> Review.Test.run (cssFiles [ "*.css" ] |> addKnownClasses [ "known" ] |> rule)
|> Review.Test.expectNoErrors
, test "should not report an error when Html.Attributes.attribute is used with a non-literal attribute name" <|
\() ->
"""module A exposing (..)
import Html
import Html.Attributes as Attr
view model =
Attr.attribute name model.name
"""
|> Review.Test.run (cssFiles [ "*.css" ] |> addKnownClasses [ "known" ] |> rule)
|> Review.Test.expectNoErrors
, test "should not report an error when encountering a literal CSS class with a custom CSS function" <|
\() ->
"""module A exposing (..)
import Class
view model =
Class.fromString "known"
"""
|> Review.Test.run
(cssFiles [ "*.css" ]
|> addKnownClasses [ "known" ]
|> withCssUsingFunctions [ ( "Class.fromString", classFromAttrFunction ) ]
|> rule
)
|> Review.Test.expectNoErrors
, test "should report an error when encountering a non-literal CSS class with a custom CSS function" <|
\() ->
"""module A exposing (..)
import Class
view model =
Class.fromString model.a
"""
|> Review.Test.run
(cssFiles [ "*.css" ]
|> addKnownClasses [ "known" ]
|> withCssUsingFunctions [ ( "Class.fromString", classFromAttrFunction ) ]
|> rule
)
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Non-literal argument to CSS class function"
, details = [ "The argument given to this function is not a value that I could interpret. This makes it hard for me to figure out whether this was a known CSS class or not. Please transform this a string literal (\"my-class\")." ]
, under = "model.a"
}
]
, test "should report an error when encountering a reference to class function outside of a function call" <|
\() ->
"""module A exposing (..)
import Html.Attributes
classListWithoutErrorsBeingReported =
Html.Attributes.classList
"""
|> Review.Test.run (cssFiles [ "*.css" ] |> rule)
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Class using function is used without arguments"
, details = [ "Having the function used without arguments confuses me and will prevent me from figuring out whether the classes passed to this function will be known or unknown. Please pass in all the arguments at the location." ]
, under = "Html.Attributes.classList"
}
]
, test "should report an error when encountering a class function application with less arguments than where their class arguments are" <|
\() ->
"""module A exposing (..)
import Html.Attributes
classFunctionWithoutErrorsBeingReported =
Html.Attributes.attribute "class"
"""
|> Review.Test.run (cssFiles [ "*.css" ] |> rule)
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Class using function is used without all of its CSS class arguments"
, details = [ "Having the function used without all of its arguments confuses me and will prevent me from figuring out whether the classes passed to this function will be known or unknown. Please pass in all the arguments at the location." ]
, under = "Html.Attributes.attribute"
}
]
, test "should report an error when being unable to parse a CSS file" <|
\() ->
"""module A exposing (..)
import Class
view model =
Class.fromString model.a
"""
|> Review.Test.runWithProjectData projectWithUnparsableCssClasses (cssFiles [ "*.css" ] |> rule)
|> Review.Test.expectErrorsForModules
[ ( "some-file.css"
, [ Review.Test.error
{ message = "Unable to parse CSS file `some-file.css`"
, details = [ "Please check that this file is syntactically correct. It is possible that I'm mistaken as my CSS parser is still very naive. Contributions are welcome to solve the issue." ]
, under = "-- First line"
}
]
)
]
, test "should report an error when encountering an if expression as an argument to Html.Attributes.class" <|
\() ->
"""module A exposing (..)
import Html.Attributes as Attr
view model =
Attr.class <| if model.condition then "a" else "b"
"""
|> Review.Test.run (cssFiles [ "*.css" ] |> rule)
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Non-literal argument to CSS class function"
, details = [ "The argument given to this function is not a value that I could interpret. This makes it hard for me to figure out whether this was a known CSS class or not. Please transform this a string literal (\"my-class\")." ]
, under = "if model.condition then \"a\" else \"b\""
}
]
, test "should understand if expressions as an argument to Html.Attributes.class" <|
\() ->
"""module A exposing (..)
import Html.Attributes as Attr
view model =
Attr.class <| if model.condition then "known" else nonLiteral
"""
|> Review.Test.run (cssFiles [ "*.css" ] |> addKnownClasses [ "known" ] |> withCssUsingFunctions [ ( "Html.Attributes.class", ClassFunction.smartFirstArgumentIsClass ) ] |> rule)
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Non-literal argument to CSS class function"
, details = [ "The argument given to this function is not a value that I could interpret. This makes it hard for me to figure out whether this was a known CSS class or not. Please transform this a string literal (\"my-class\")." ]
, under = "nonLiteral"
}
]
, test "should understand case expressions as an argument to Html.Attributes.class" <|
\() ->
"""module A exposing (..)
import Html.Attributes as Attr
view model =
Attr.class <|
case model.thing of
A -> "known"
B -> nonLiteral
"""
|> Review.Test.run (cssFiles [ "*.css" ] |> addKnownClasses [ "known" ] |> withCssUsingFunctions [ ( "Html.Attributes.class", ClassFunction.smartFirstArgumentIsClass ) ] |> rule)
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Non-literal argument to CSS class function"
, details = [ "The argument given to this function is not a value that I could interpret. This makes it hard for me to figure out whether this was a known CSS class or not. Please transform this a string literal (\"my-class\")." ]
, under = "nonLiteral"
}
]
]
classFromAttrFunction : ClassFunction.Arguments -> List CssArgument
classFromAttrFunction { firstArgument } =
[ fromLiteral firstArgument ]
projectWithCssClasses : Project
projectWithCssClasses =
Project.addExtraFiles
[ { path = "some-file.css"
, content = """-- First line
.known {
color: blue;
}
.red-faint {
color: red;
}
.under_score {
color: green;
}
"""
}
]
Review.Test.Dependencies.projectWithElmCore
projectWithUnparsableCssClasses : Project
projectWithUnparsableCssClasses =
Project.addExtraFiles
[ { path = "some-file.css"
, content = """-- First line
.known {
color: blue;
}
.red-faint {
color: red;
-- missing closing curly brace
"""
}
]
Review.Test.Dependencies.projectWithElmCore

View File

@ -45,6 +45,7 @@ elm-review --template jfmengels/elm-review/example --rules Docs.NoMissingChangel
import Elm.Project exposing (Project)
import Elm.Syntax.Range exposing (Range)
import Elm.Version
import Review.FilePattern as FilePattern
import Review.Fix as Fix
import Review.Rule as Rule exposing (Rule)
@ -55,7 +56,7 @@ rule : Configuration -> Rule
rule (Configuration { changelogPath }) =
Rule.newProjectRuleSchema "Docs.NoMissingChangelogEntry" initialProjectContext
|> Rule.withElmJsonProjectVisitor elmJsonVisitor
|> Rule.withExtraFilesProjectVisitor [ Maybe.withDefault defaultPath changelogPath ] (extraFilesVisitor changelogPath)
|> Rule.withExtraFilesProjectVisitor [ FilePattern.include (Maybe.withDefault defaultPath changelogPath) ] (extraFilesVisitor changelogPath)
|> Rule.providesFixesForProjectRule
|> Rule.fromProjectRuleSchema

348
tests/Levenshtein.elm Normal file
View File

@ -0,0 +1,348 @@
module Levenshtein exposing (distance)
{-| Levenshtein distance is a metric for measuring the difference between two strings.
@docs distance
Copied from
-}
import Array exposing (Array)
{-| Computes the Levenshtein distance between two strings.
-}
distance : String -> String -> Int
distance str1 str2 =
if str1 == str2 then
0
else
distanceHelper
(Array.fromList (String.toList str1))
(Array.fromList (String.toList str2))
distanceHelper : Array Char -> Array Char -> Int
distanceHelper arr1 arr2 =
let
calculateEditDistanceForChars : Table -> ( Int, Int ) -> ( Table, Int )
calculateEditDistanceForChars table ( i, j ) =
case ( Array.get (i - 1) arr1, Array.get (j - 1) arr2 ) of
( Just chr1, Just chr2 ) ->
let
( table1, dist1 ) =
tableFetch ( i - 1, j ) calculateEditDistanceForChars table
( table2, dist2 ) =
tableFetch ( i, j - 1 ) calculateEditDistanceForChars table1
( table3, dist3 ) =
tableFetch ( i - 1, j - 1 ) calculateEditDistanceForChars table2
in
( table3
-- Find the smallest of (dist1 + 1), (dist2 + 1), and (dist3 + distanceIndicator)
, if dist3 < dist1 then
if dist3 < dist2 then
if chr1 /= chr2 then
dist3 + 1
else
dist3
else
dist1 + 1
else if dist1 > dist2 then
dist2 + 1
else
dist1 + 1
)
_ ->
( table, max i j )
indecesForLastChars =
( Array.length arr1, Array.length arr2 )
in
calculateEditDistanceForChars (tableEmpty indecesForLastChars) indecesForLastChars
|> Tuple.second
--- tableElm
{-
The algorithm goes across the various combinations of characters in the two strings to be compared.
The result of every combination can be looked at like a grid:
- K I T T E N
- (0,0) (1,0) (2,0) (3,0) (4,0) (5,0) (6,0)
S (0,1) (1,1) (2,1) (3,1) (4,1) (5,1) (6,1)
I (0,2) (1,2) (2,2) (3,2) (4,2) (5,2) (6,2)
T (0,3) (1,3) (2,3) (3,3) (4,3) (5,3) (6,3)
T (0,4) (1,4) (2,4) (3,4) (4,4) (5,4) (6,4)
I (0,5) (1,5) (2,5) (3,5) (4,5) (5,5) (6,5)
N (0,6) (1,6) (2,6) (3,6) (4,6) (5,6) (6,6)
G (0,7) (1,7) (2,7) (3,7) (4,7) (5,7) (6,7)
Note that each row and column have room for one more element than the length of the string they represent.
To store the result for each comparison efficiently, we use a flat array, where the data looks like this:
[ (0,0), (1,0), (2,0), (3,0), (4,0), (5,0), (6,0), (0,1), (1,1), (2,1), (3,1), etc.]
Where the index to read from can be calculated from the key and the "width" of the grid.
-}
type Table
= Table Int (Array Int)
tableEmpty : ( Int, Int ) -> Table
tableEmpty ( sizeA, sizeB ) =
let
dimension =
sizeB + 1
arraySize =
(sizeA + 1) * dimension - 1
in
Table dimension (Array.repeat arraySize -1)
tableFetch : ( Int, Int ) -> (Table -> ( Int, Int ) -> ( Table, Int )) -> Table -> ( Table, Int )
tableFetch (( iKey, jKey ) as key) builder ((Table dimension distanceStore) as table) =
let
index =
iKey * dimension + jKey
in
case Array.get index distanceStore of
Just editDistance ->
if editDistance == -1 then
let
( Table _ newStore, actualEditDistance ) =
builder table key
in
( Table dimension (Array.set index actualEditDistance newStore), actualEditDistance )
else
( table, editDistance )
Nothing ->
-- Would only occur if we are out of bounds on the array. This should never happen
( table, -1 )
{- LICENSE
The code was copied from https://github.com/dasch/levenshtein
and including the change in https://github.com/dasch/levenshtein/pull/10
from https://github.com/KasMA1990/levenshtein
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-}

View File

@ -0,0 +1,143 @@
module Review.FilePatternTest exposing (all)
import Expect
import Fuzz
import Review.FilePattern as FilePattern exposing (FilePattern, excludeFolder, include)
import Test exposing (Test, describe, fuzz, test)
all : Test
all =
describe "Review.FilePattern"
[ matchTest
, toStringsTest
]
matchTest : Test
matchTest =
describe "match"
[ test "should return False when the list is empty" <|
\() ->
matchAgainst [] "some/file/path.ext"
|> Expect.equal False
, test "should return True when including the target file" <|
\() ->
matchAgainst
[ FilePattern.include "some/file/path.ext"
]
"some/file/path.ext"
|> Expect.equal True
, test "should return True when including through *" <|
\() ->
matchAgainst
[ FilePattern.include "some/file/**/*"
]
"some/file/path.ext"
|> Expect.equal True
, test "should return True when including through * with an extension" <|
\() ->
matchAgainst
[ FilePattern.include "some/file/**/*.ext"
]
"some/file/path.ext"
|> Expect.equal True
, test "should return True when including through **/*" <|
\() ->
matchAgainst
[ FilePattern.include "some/file/**/*"
]
"some/file/path.ext"
|> Expect.equal True
, test "should return False when including through **/* but excluding the target file" <|
\() ->
matchAgainst
[ FilePattern.include "some/file/**/*"
, FilePattern.exclude "some/file/path.ext"
]
"some/file/path.ext"
|> Expect.equal False
, test "should return True when excluding through **/* but re-including the target file" <|
\() ->
matchAgainst
[ FilePattern.exclude "some/file/**/*"
, FilePattern.include "some/file/path.ext"
]
"some/file/path.ext"
|> Expect.equal True
, test "should return False when excluding the folder even when re-including the target file" <|
\() ->
matchAgainst
[ FilePattern.excludeFolder "some"
, FilePattern.include "some/file/path.ext"
]
"some/file/path.ext"
|> Expect.equal False
, test "should return False when excluding the folder (with trailing /) even when re-including the target file" <|
\() ->
matchAgainst
[ FilePattern.excludeFolder "some/"
, FilePattern.include "some/file/path.ext"
]
"some/file/path.ext"
|> Expect.equal False
]
matchAgainst : List FilePattern -> String -> Bool
matchAgainst filePatterns str =
case FilePattern.compact filePatterns of
Ok filePatternCompact ->
FilePattern.match filePatternCompact str
Err globs ->
Debug.todo ("Invalid globs:\n" ++ String.join "\n" globs)
toStringsTest : Test
toStringsTest =
describe "toStrings"
[ fuzz
(Fuzz.list (Fuzz.map2 (\str included -> { pattern = str, included = included }) Fuzz.string Fuzz.bool))
"files should stay as before"
<|
\list ->
case
list
|> List.map
(\{ pattern, included } ->
if included then
FilePattern.include pattern
else
FilePattern.exclude pattern
)
|> FilePattern.compact
of
Ok summary ->
summary
|> FilePattern.toStrings
|> .files
|> Expect.equal list
Err _ ->
Expect.pass
, fuzz
(Fuzz.list Fuzz.string)
"excluded folders should stay as before"
<|
\list ->
case
list
|> List.map FilePattern.excludeFolder
|> FilePattern.compact
of
Ok summary ->
summary
|> FilePattern.toStrings
|> .excludedFolders
|> Expect.equal list
Err _ ->
Expect.pass
]

View File

@ -5,6 +5,7 @@ import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Node as Node exposing (Node(..))
import Elm.Syntax.Pattern exposing (Pattern)
import Review.FilePattern as FilePattern
import Review.Project as Project exposing (Project)
import Review.Rule as Rule exposing (Error, Rule)
import Review.Test
@ -32,8 +33,8 @@ all =
|> Rule.withElmJsonModuleVisitor (\_ context -> context ++ "\n1.2 - withElmJsonModuleVisitor")
|> Rule.withReadmeModuleVisitor (\_ context -> context ++ "\n2.1 - withReadmeModuleVisitor")
|> Rule.withReadmeModuleVisitor (\_ context -> context ++ "\n2.2 - withReadmeModuleVisitor")
|> Rule.withExtraFilesModuleVisitor [ "first.txt" ] (\files context -> context ++ "\n3.1 - withExtraFilesModuleVisitor " ++ (List.map .path files |> String.join ", "))
|> Rule.withExtraFilesModuleVisitor [ "last.txt" ] (\files context -> context ++ "\n3.2 - withExtraFilesModuleVisitor " ++ (List.map .path files |> String.join ", "))
|> Rule.withExtraFilesModuleVisitor [ FilePattern.include "first.txt" ] (\files context -> context ++ "\n3.1 - withExtraFilesModuleVisitor " ++ (List.map .path files |> String.join ", "))
|> Rule.withExtraFilesModuleVisitor [ FilePattern.include "last.txt" ] (\files context -> context ++ "\n3.2 - withExtraFilesModuleVisitor " ++ (List.map .path files |> String.join ", "))
|> Rule.withDirectDependenciesModuleVisitor (\_ context -> context ++ "\n4.1 - withDirectDependenciesModuleVisitor")
|> Rule.withDirectDependenciesModuleVisitor (\_ context -> context ++ "\n4.2 - withDirectDependenciesModuleVisitor")
|> Rule.withDependenciesModuleVisitor (\_ context -> context ++ "\n4.3 - withDependenciesModuleVisitor")

View File

@ -1,5 +1,6 @@
module Review.Rule.WithExtraFilesVisitorTest exposing (all)
import Review.FilePattern as FilePattern exposing (FilePattern)
import Review.Project as Project exposing (Project)
import Review.Rule as Rule exposing (Error, Rule)
import Review.Test
@ -20,7 +21,7 @@ all =
rule : Rule
rule =
createRule (Rule.withExtraFilesModuleVisitor [ "foo/some-file.css" ] extraFilesModuleVisitor)
createRule (Rule.withExtraFilesModuleVisitor [ FilePattern.include "foo/some-file.css" ] extraFilesModuleVisitor)
in
"""module A exposing (a)
a = 1
@ -44,7 +45,7 @@ a = 1
rule : Rule
rule =
createRule (Rule.withExtraFilesModuleVisitor [ "foo/some-file.css" ] extraFilesModuleVisitor)
createRule (Rule.withExtraFilesModuleVisitor [ FilePattern.include "foo/some-file.css" ] extraFilesModuleVisitor)
in
"""module A exposing (a)
a = 1
@ -69,8 +70,8 @@ a = 1
rule : Rule
rule =
createRule
(Rule.withExtraFilesModuleVisitor [ "a.txt", "c.txt" ] (reportsFileNames "A")
>> Rule.withExtraFilesModuleVisitor [ "b.txt" ] (reportsFileNames "B")
(Rule.withExtraFilesModuleVisitor [ FilePattern.include "a.txt", FilePattern.include "c.txt" ] (reportsFileNames "A")
>> Rule.withExtraFilesModuleVisitor [ FilePattern.include "b.txt" ] (reportsFileNames "B")
)
in
"""module A exposing (a)
@ -90,7 +91,7 @@ a = 1
\() ->
createRule
(Rule.withExtraFilesModuleVisitor
[ "** " ]
[ FilePattern.include "** " ]
(reportsFileNames "A")
)
|> Review.Test.expectConfigurationError