From 14132fbf292df2803f1d8d2cd1339ff8aac762c4 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Tue, 7 May 2024 00:55:52 +0200 Subject: [PATCH] Add example rule to report unused CSS classes --- src/Review/Rule.elm | 148 +++++++++- tests/Css/NoUnusedCssClasses.elm | 158 ++++++++++ tests/Css/NoUnusedCssClassesTest.elm | 415 +++++++++++++++++++++++++++ 3 files changed, 720 insertions(+), 1 deletion(-) create mode 100644 tests/Css/NoUnusedCssClasses.elm create mode 100644 tests/Css/NoUnusedCssClassesTest.elm diff --git a/src/Review/Rule.elm b/src/Review/Rule.elm index d1bd8aa4..07f808e6 100644 --- a/src/Review/Rule.elm +++ b/src/Review/Rule.elm @@ -1903,7 +1903,153 @@ doesn't analyze by default. The visitor function will be called with all the files matching the file patterns. -REPLACEME Make a rule that reports unused classes in CSS files. +The following example rule reads a project's `.css` files to extract all the mentioned CSS classes, +then finds calls to `Html.Attributes.class` in the Elm code (such as `Html.Attributes.class "big-red-button"`) +and reports errors when the classes given as argument are unknown. + + 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 Regex exposing (Regex) + import Review.FilePattern as FilePattern + import Review.Rule as Rule exposing (Rule) + import Set exposing (Set) + + rule : Rule + rule = + Rule.newProjectRuleSchema "NoUnusedCssClasses" initialProjectContext + |> Rule.withExtraFilesProjectVisitor cssFilesVisitor + [ FilePattern.include "**/*.css" ] + |> Rule.withModuleVisitor moduleVisitor + |> Rule.withModuleContextUsingContextCreator + { fromProjectToModule = fromProjectToModule + , fromModuleToProject = fromModuleToProject + , foldProjectContexts = foldProjectContexts + } + |> Rule.withFinalProjectEvaluation finalEvaluation + |> Rule.fromProjectRuleSchema + + moduleVisitor : Rule.ModuleRuleSchema {} ModuleContext -> Rule.ModuleRuleSchema { hasAtLeastOneVisitor : () } ModuleContext + moduleVisitor schema = + schema + |> Rule.withExpressionEnterVisitor expressionVisitor + + type alias ProjectContext = + { cssFiles : + Dict + String + { fileKey : Rule.ExtraFileKey + , classes : Set String + } + , usedCssClasses : Set String + } + + type alias ModuleContext = + { usedCssClasses : Set String + } + + initialProjectContext : ProjectContext + initialProjectContext = + { cssFiles = Dict.empty + , usedCssClasses = Set.empty + } + + fromProjectToModule : Rule.ContextCreator ProjectContext ModuleContext + fromProjectToModule = + Rule.initContextCreator (\_ -> { usedCssClasses = Set.empty }) + + fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext + fromModuleToProject = + Rule.initContextCreator + (\{ usedCssClasses } -> + { cssFiles = Dict.empty + , usedCssClasses = usedCssClasses + } + ) + + foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext + foldProjectContexts newContext previousContext = + { cssFiles = previousContext.cssFiles + , usedCssClasses = Set.union newContext.usedCssClasses previousContext.usedCssClasses + } + + cssClassRegex : Regex + cssClassRegex = + Regex.fromString "\\.([\\w-_]+)" + |> Maybe.withDefault Regex.never + + cssFilesVisitor : Dict String { fileKey : Rule.ExtraFileKey, content : String } -> ProjectContext -> ( List (Rule.Error { useErrorForModule : () }), ProjectContext ) + cssFilesVisitor files context = + ( [] + , { context + | cssFiles = + Dict.map + (\_ { fileKey, content } -> + { fileKey = fileKey + , classes = + Regex.find cssClassRegex content + |> List.map (\m -> String.dropLeft 1 m.match) + |> Set.fromList + } + ) + files + } + ) + + expressionVisitor : Node Expression -> ModuleContext -> ( List (Rule.Error {}), ModuleContext ) + expressionVisitor node context = + case Node.value node of + Expression.Application [ function, firstArg ] -> + case Node.value function of + Expression.FunctionOrValue [ "Html", "Attributes" ] "class" -> + case Node.value firstArg of + Expression.Literal stringLiteral -> + let + usedCssClasses : List String + usedCssClasses = + String.split " " stringLiteral + in + ( [] + , { context | usedCssClasses = List.foldl Set.insert context.usedCssClasses usedCssClasses } + ) + + _ -> + ( [], context ) + + _ -> + ( [], context ) + + _ -> + ( [], context ) + + finalEvaluation : ProjectContext -> List (Rule.Error { useErrorForModule : () }) + finalEvaluation context = + context.cssFiles + |> Dict.toList + |> List.filterMap (\( filePath, file ) -> reportUnusedClasses context.usedCssClasses filePath file) + + reportUnusedClasses : Set String -> String -> { a | fileKey : Rule.ExtraFileKey, classes : Set String } -> Maybe (Rule.Error { useErrorForModule : () }) + reportUnusedClasses usedCssClasses filePath { fileKey, classes } = + let + unusedClasses : Set String + unusedClasses = + Set.diff classes usedCssClasses + in + if Set.isEmpty unusedClasses then + Nothing + + else + Just + (Rule.errorForExtraFile fileKey + { message = "Found unused CSS classes in " ++ filePath + , details = + [ "This file declared the usage of some CSS classes for which I could not any usage in the Elm codebase. Please check that no typo was made in the name of the classes, and remove them if they still seem unused." + , "Here are the classes that seem unused: " ++ String.join " " (Set.toList unusedClasses) + ] + } + { start = { row = 1, column = 1 }, end = { row = 2, column = 1 } } + ) -} withExtraFilesProjectVisitor : diff --git a/tests/Css/NoUnusedCssClasses.elm b/tests/Css/NoUnusedCssClasses.elm new file mode 100644 index 00000000..0689a956 --- /dev/null +++ b/tests/Css/NoUnusedCssClasses.elm @@ -0,0 +1,158 @@ +module Css.NoUnusedCssClasses exposing (rule) + +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 Regex exposing (Regex) +import Review.FilePattern as FilePattern +import Review.Rule as Rule exposing (Rule) +import Set exposing (Set) + + +rule : Rule +rule = + Rule.newProjectRuleSchema "NoUnusedCssClasses" initialProjectContext + |> Rule.withExtraFilesProjectVisitor cssFilesVisitor + [ FilePattern.include "**/*.css" ] + |> Rule.withModuleVisitor moduleVisitor + |> Rule.withModuleContextUsingContextCreator + { fromProjectToModule = fromProjectToModule + , fromModuleToProject = fromModuleToProject + , foldProjectContexts = foldProjectContexts + } + |> Rule.withFinalProjectEvaluation finalEvaluation + |> Rule.fromProjectRuleSchema + + +moduleVisitor : Rule.ModuleRuleSchema {} ModuleContext -> Rule.ModuleRuleSchema { hasAtLeastOneVisitor : () } ModuleContext +moduleVisitor schema = + schema + |> Rule.withExpressionEnterVisitor expressionVisitor + + +type alias ProjectContext = + { cssFiles : + Dict + String + { fileKey : Rule.ExtraFileKey + , classes : Set String + } + , usedCssClasses : Set String + } + + +type alias ModuleContext = + { usedCssClasses : Set String + } + + +initialProjectContext : ProjectContext +initialProjectContext = + { cssFiles = Dict.empty + , usedCssClasses = Set.empty + } + + +fromProjectToModule : Rule.ContextCreator ProjectContext ModuleContext +fromProjectToModule = + Rule.initContextCreator (\_ -> { usedCssClasses = Set.empty }) + + +fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext +fromModuleToProject = + Rule.initContextCreator + (\{ usedCssClasses } -> + { cssFiles = Dict.empty + , usedCssClasses = usedCssClasses + } + ) + + +foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext +foldProjectContexts newContext previousContext = + { cssFiles = previousContext.cssFiles + , usedCssClasses = Set.union newContext.usedCssClasses previousContext.usedCssClasses + } + + +cssClassRegex : Regex +cssClassRegex = + Regex.fromString "\\.([\\w-_]+)" + |> Maybe.withDefault Regex.never + + +cssFilesVisitor : Dict String { fileKey : Rule.ExtraFileKey, content : String } -> ProjectContext -> ( List (Rule.Error { useErrorForModule : () }), ProjectContext ) +cssFilesVisitor files context = + ( [] + , { context + | cssFiles = + Dict.map + (\_ { fileKey, content } -> + { fileKey = fileKey + , classes = + Regex.find cssClassRegex content + |> List.map (\m -> String.dropLeft 1 m.match) + |> Set.fromList + } + ) + files + } + ) + + +expressionVisitor : Node Expression -> ModuleContext -> ( List (Rule.Error {}), ModuleContext ) +expressionVisitor node context = + case Node.value node of + Expression.Application [ function, firstArg ] -> + case Node.value function of + Expression.FunctionOrValue [ "Html", "Attributes" ] "class" -> + case Node.value firstArg of + Expression.Literal stringLiteral -> + let + usedCssClasses : List String + usedCssClasses = + String.split " " stringLiteral + in + ( [] + , { context | usedCssClasses = List.foldl Set.insert context.usedCssClasses usedCssClasses } + ) + + _ -> + ( [], context ) + + _ -> + ( [], context ) + + _ -> + ( [], context ) + + +finalEvaluation : ProjectContext -> List (Rule.Error { useErrorForModule : () }) +finalEvaluation context = + context.cssFiles + |> Dict.toList + |> List.filterMap (\( filePath, file ) -> reportUnusedClasses context.usedCssClasses filePath file) + + +reportUnusedClasses : Set String -> String -> { a | fileKey : Rule.ExtraFileKey, classes : Set String } -> Maybe (Rule.Error { useErrorForModule : () }) +reportUnusedClasses usedCssClasses filePath { fileKey, classes } = + let + unusedClasses : Set String + unusedClasses = + Set.diff classes usedCssClasses + in + if Set.isEmpty unusedClasses then + Nothing + + else + Just + (Rule.errorForExtraFile fileKey + { message = "Found unused CSS classes in " ++ filePath + , details = + [ "This file declared the usage of some CSS classes for which I could not any usage in the Elm codebase. Please check that no typo was made in the name of the classes, and remove them if they still seem unused." + , "Here are the classes that seem unused: " ++ String.join " " (Set.toList unusedClasses) + ] + } + { start = { row = 1, column = 1 }, end = { row = 2, column = 1 } } + ) diff --git a/tests/Css/NoUnusedCssClassesTest.elm b/tests/Css/NoUnusedCssClassesTest.elm new file mode 100644 index 00000000..9c5e5e82 --- /dev/null +++ b/tests/Css/NoUnusedCssClassesTest.elm @@ -0,0 +1,415 @@ +module Css.NoUnknownCssClassesTest exposing (all) + +import Css.ClassFunction as ClassFunction exposing (CssArgument, fromLiteral) +import Css.NoUnknownCssClasses exposing (addKnownClasses, cssFiles, rule, withCssUsingFunctions) +import Dict +import Elm.Syntax.Expression exposing (Expression) +import Elm.Syntax.Node exposing (Node) +import Review.FilePattern as FilePattern exposing (FilePattern) +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 [ FilePattern.include "*.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 [ FilePattern.include "*.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 [ FilePattern.include "*.css" ] |> addKnownClasses [ "known" ] |> rule) + |> Review.Test.expectNoErrors + , test "should not report an error when the class argument is empty" <| + \() -> + """module A exposing (..) +import Html +import Html.Attributes as Attr + +view model = + Html.span [ Attr.class "" ] [] +""" + |> Review.Test.run (cssFiles [ FilePattern.include "*.css" ] |> addKnownClasses [ "known" ] |> rule) + |> Review.Test.expectNoErrors + , test "should not report an error when the class argument is only made out of spaces" <| + \() -> + """module A exposing (..) +import Html +import Html.Attributes as Attr + +view model = + Html.span [ Attr.class " " ] [] +""" + |> Review.Test.run (cssFiles [ FilePattern.include "*.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 [ FilePattern.include "*.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 [ FilePattern.include "*.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 [ FilePattern.include "*.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 [ FilePattern.include "*.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 [ FilePattern.include "*.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 [ FilePattern.include "*.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 [ FilePattern.include "*.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 [ FilePattern.include "*.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 [ FilePattern.include "*.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 [ FilePattern.include "*.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 [ FilePattern.include "*.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 [ FilePattern.include "*.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 [ FilePattern.include "*.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 [ FilePattern.include "*.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 [ FilePattern.include "*.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 [ FilePattern.include "*.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 [ FilePattern.include "*.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 [ FilePattern.include "*.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 + (Dict.fromList + [ ( "some-file.css" + , """-- First line +.known { + color: blue; +} +.red-faint { + color: red; +} +.under_score { + color: green; +} +""" + ) + ] + ) + Review.Test.Dependencies.projectWithElmCore + + +projectWithUnparsableCssClasses : Project +projectWithUnparsableCssClasses = + Project.addExtraFiles + (Dict.fromList + [ ( "some-file.css" + , """-- First line +.known { + color: blue; +} +.red-faint { + color: red; +-- missing closing curly brace +""" + ) + ] + ) + Review.Test.Dependencies.projectWithElmCore