mirror of
https://github.com/jfmengels/elm-review.git
synced 2024-08-16 19:40:22 +03:00
Compare commits
90 Commits
e310851dfa
...
be7c5d08a2
Author | SHA1 | Date | |
---|---|---|---|
|
be7c5d08a2 | ||
|
ae605ee249 | ||
|
49ad03d59d | ||
|
ade886ddd1 | ||
|
c955278351 | ||
|
00c344b88c | ||
|
5b7ffece53 | ||
|
34ba41ae46 | ||
|
c8bcd974e7 | ||
|
909f509971 | ||
|
1a0b092946 | ||
|
b4d79d360b | ||
|
2bd6bf8421 | ||
|
982903435c | ||
|
eb3d948207 | ||
|
e7d0055cb8 | ||
|
c09fe19c99 | ||
|
4af35c8d88 | ||
|
4f0b79b0af | ||
|
e5e25719e5 | ||
|
d82bf7baf9 | ||
|
93cb8f89d8 | ||
|
51fd359c58 | ||
|
77e9ee4c0b | ||
|
8d0255af87 | ||
|
4ced30b8a8 | ||
|
28f6ffc213 | ||
|
687440cf8d | ||
|
a6c866b187 | ||
|
c250973325 | ||
|
cc2f78f3ec | ||
|
d78b9a3f49 | ||
|
d7ac221781 | ||
|
d454814e70 | ||
|
f9cee7c9a3 | ||
|
13e0d0b832 | ||
|
2740b7b8eb | ||
|
cd9d1237e9 | ||
|
4470445c3a | ||
|
98851c28f8 | ||
|
e376deb41d | ||
|
2c583e664c | ||
|
6ed6b6e4d3 | ||
|
c9f3524885 | ||
|
be2be4fb3d | ||
|
7dab184c00 | ||
|
fdc93a71df | ||
|
3aee5f68fb | ||
|
c7cb65fced | ||
|
cedf1c240a | ||
|
4ad1dcfcdd | ||
|
45842b2234 | ||
|
038819ae49 | ||
|
04b3b9fa2a | ||
|
4129685142 | ||
|
a3413facb2 | ||
|
701eac7b95 | ||
|
f9f0f1b0b5 | ||
|
98f08aa62a | ||
|
a70dae14d7 | ||
|
e2eb764566 | ||
|
2d073c9317 | ||
|
be560d38be | ||
|
847decf1b4 | ||
|
67e94e484a | ||
|
bdf9173cd0 | ||
|
d18a7452c8 | ||
|
2e92713315 | ||
|
a9eef34293 | ||
|
2a0cc68767 | ||
|
d620dde379 | ||
|
8b7662cdfc | ||
|
081aefae99 | ||
|
348e70e256 | ||
|
47f662db3e | ||
|
955d2837de | ||
|
d1ef356e3f | ||
|
56c17ba5fa | ||
|
3b62c5d831 | ||
|
113cf7f061 | ||
|
abb2dce7a7 | ||
|
56e75c931f | ||
|
7a230f309e | ||
|
620abd615e | ||
|
ca62e5159f | ||
|
cc6847f48b | ||
|
3d045ddf8a | ||
|
078e2d0414 | ||
|
4245fc1fb0 | ||
|
aeec25f60a |
5
elm.json
5
elm.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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" ])
|
||||
|
@ -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
238
src/Review/FilePattern.elm
Normal 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
|
@ -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
|
||||
|
@ -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
136
tests/Css/ClassFunction.elm
Normal 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 ]
|
585
tests/Css/NoUnknownClasses.elm
Normal file
585
tests/Css/NoUnknownClasses.elm
Normal 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)
|
||||
]
|
387
tests/Css/NoUnknownClassesTest.elm
Normal file
387
tests/Css/NoUnknownClassesTest.elm
Normal 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
|
@ -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
348
tests/Levenshtein.elm
Normal 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.
|
||||
|
||||
-}
|
143
tests/Review/FilePatternTest.elm
Normal file
143
tests/Review/FilePatternTest.elm
Normal 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
|
||||
]
|
@ -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")
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user