Skip to content

Commit

Permalink
Add example rule to report unused CSS classes
Browse files Browse the repository at this point in the history
  • Loading branch information
jfmengels committed May 6, 2024
1 parent bf40c54 commit 14132fb
Show file tree
Hide file tree
Showing 3 changed files with 720 additions and 1 deletion.
148 changes: 147 additions & 1 deletion src/Review/Rule.elm
Original file line number Diff line number Diff line change
Expand Up @@ -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 :
Expand Down
158 changes: 158 additions & 0 deletions tests/Css/NoUnusedCssClasses.elm
Original file line number Diff line number Diff line change
@@ -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 } }
)
Loading

0 comments on commit 14132fb

Please sign in to comment.