|
module ReportSimpleMessages exposing (rule) |
|
|
|
{-| Run with |
|
|
|
elm-review ./src/Well/I18n\_Deprecated.elm --extract --report=json --rules ReportSimpleMessages | jq -r '.extracts.ReportSimpleMessages' |
|
|
|
-} |
|
|
|
import Dict exposing (Dict) |
|
import Elm.Syntax.Declaration as Declaration exposing (Declaration) |
|
import Elm.Syntax.Expression as Expression |
|
import Elm.Syntax.ModuleName exposing (ModuleName) |
|
import Elm.Syntax.Node as Node exposing (Node) |
|
import Elm.Syntax.Pattern as Pattern |
|
import Elm.Syntax.Range exposing (Range) |
|
import Elm.Syntax.TypeAnnotation exposing (TypeAnnotation(..)) |
|
import Json.Decode |
|
import Json.Encode as Encode |
|
import Maybe.Extra |
|
import Review.FilePattern as FilePattern |
|
import Review.Fix as Fix |
|
import Review.ModuleNameLookupTable exposing (ModuleNameLookupTable) |
|
import Review.Rule as Rule exposing (Error, ExtraFileKey, Rule) |
|
|
|
|
|
rule : Rule |
|
rule = |
|
Rule.newProjectRuleSchema "ReportSimpleMessages" initContext |
|
|> Rule.withExtraFilesProjectVisitor extraFilesVisitor |
|
[ FilePattern.include "temp/out.json" ] |
|
|> Rule.withModuleVisitor |
|
moduleVisitor |
|
|> Rule.withModuleContextUsingContextCreator |
|
{ fromProjectToModule = fromProjectToModule |
|
, fromModuleToProject = fromModuleToProject |
|
, foldProjectContexts = foldProjectContexts |
|
} |
|
|> Rule.withDataExtractor dataExtractor |
|
|> Rule.fromProjectRuleSchema |
|
|
|
|
|
extraFilesVisitor : |
|
Dict |
|
String |
|
{ fileKey : ExtraFileKey |
|
, content : String |
|
} |
|
-> ProjectContext |
|
-> ( List (Error { useErrorForModule : () }), ProjectContext ) |
|
extraFilesVisitor dict projectContext = |
|
let |
|
maybeContent = |
|
dict |
|
|> Dict.values |
|
|> List.map .content |
|
|> List.head |
|
|
|
maybeDecoded = |
|
maybeContent |
|
|> Maybe.map (Json.Decode.decodeString (Json.Decode.dict Json.Decode.string)) |
|
in |
|
case maybeDecoded of |
|
Just (Ok decoded) -> |
|
( [], { projectContext | originalSourceByKey = decoded } ) |
|
|
|
_ -> |
|
( [], projectContext ) |
|
|
|
|
|
type alias ProjectContext = |
|
{ functions : List SimplifiedFunction |
|
, originalSourceByKey : Dict String String |
|
} |
|
|
|
|
|
type alias ModuleContext = |
|
{ lookupTable : ModuleNameLookupTable |
|
, moduleName : ModuleName |
|
, functions : List SimplifiedFunction |
|
, extractSourceCode : Range -> String |
|
, originalSourceByKey : Dict String String |
|
} |
|
|
|
|
|
type alias SimplifiedFunction = |
|
{ name : String |
|
, result : Maybe String |
|
} |
|
|
|
|
|
initContext : ProjectContext |
|
initContext = |
|
{ functions = [] |
|
, originalSourceByKey = Dict.empty |
|
} |
|
|
|
|
|
fromProjectToModule : Rule.ContextCreator ProjectContext ModuleContext |
|
fromProjectToModule = |
|
Rule.initContextCreator |
|
(\moduleName lookupTable extractSourceCode projectContext -> |
|
{ lookupTable = lookupTable |
|
, moduleName = moduleName |
|
, functions = [] |
|
, extractSourceCode = extractSourceCode |
|
, originalSourceByKey = projectContext.originalSourceByKey |
|
} |
|
) |
|
|> Rule.withModuleName |
|
|> Rule.withModuleNameLookupTable |
|
|> Rule.withSourceCodeExtractor |
|
|
|
|
|
fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext |
|
fromModuleToProject = |
|
Rule.initContextCreator |
|
(\_ moduleContext -> |
|
{ functions = moduleContext.functions |
|
, originalSourceByKey = Dict.empty |
|
} |
|
) |
|
|> Rule.withModuleName |
|
|
|
|
|
foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext |
|
foldProjectContexts newContext previousContext = |
|
{ functions = newContext.functions ++ previousContext.functions |
|
, originalSourceByKey = Dict.union newContext.originalSourceByKey previousContext.originalSourceByKey |
|
} |
|
|
|
|
|
moduleVisitor : |
|
Rule.ModuleRuleSchema {} ModuleContext |
|
-> Rule.ModuleRuleSchema { hasAtLeastOneVisitor : () } ModuleContext |
|
moduleVisitor schema = |
|
schema |
|
|> Rule.withDeclarationEnterVisitor declarationEnterVisitor |
|
|
|
|
|
{-| Extract a simplified signature from a function annotation |
|
|
|
The last argument in the list is the return type of the function. |
|
|
|
-} |
|
signatureList : Node TypeAnnotation -> List String |
|
signatureList typeAnnotation = |
|
let |
|
go f typeAnn = |
|
case typeAnn |> Node.value of |
|
FunctionTypeAnnotation arg returnType -> |
|
let |
|
argStr = |
|
case Node.value arg of |
|
Typed argNodeModAndName _ -> |
|
argNodeModAndName |> Node.value |> Tuple.second |
|
|
|
GenericType arg_ -> |
|
arg_ |
|
|
|
Record _ -> |
|
"{ .. }" |
|
|
|
GenericRecord r _ -> |
|
"{ " ++ Node.value r ++ " | .. }" |
|
|
|
Unit -> |
|
"()" |
|
|
|
Tupled _ -> |
|
"(..)" |
|
|
|
FunctionTypeAnnotation _ _ -> |
|
"f" |
|
in |
|
go (f << (::) argStr) returnType |
|
|
|
Typed arg _ -> |
|
let |
|
argName = |
|
arg |> Node.value |> Tuple.second |
|
in |
|
f << (::) argName |
|
|
|
GenericType arg -> |
|
f << (::) arg |
|
|
|
Record _ -> |
|
f << (::) "{ .. }" |
|
|
|
GenericRecord r _ -> |
|
f << (::) ("{ " ++ Node.value r ++ " | .. }") |
|
|
|
Unit -> |
|
f |
|
|
|
Tupled _ -> |
|
f << (::) "(..)" |
|
in |
|
go identity typeAnnotation [] |
|
|
|
|
|
{-| A "simple" message is a function with signature `Lang -> String` |
|
-} |
|
isSimpleMessage : Expression.Function -> Bool |
|
isSimpleMessage = |
|
.signature |
|
>> Maybe.map Node.value |
|
>> Maybe.map (\signature -> signatureList signature.typeAnnotation) |
|
>> (\sigList -> |
|
case sigList of |
|
Just [ "Lang", "String" ] -> |
|
True |
|
|
|
_ -> |
|
False |
|
) |
|
|
|
|
|
declarationEnterVisitor : Node Declaration -> ModuleContext -> ( List (Error {}), ModuleContext ) |
|
declarationEnterVisitor node context = |
|
let |
|
fns_ = |
|
case Node.value node of |
|
Declaration.FunctionDeclaration function -> |
|
if not (isSimpleMessage function) then |
|
let |
|
z = |
|
function.declaration |
|
|> Node.value |
|
|> .expression |
|
|> Node.value |
|
|> (\exp -> |
|
case exp of |
|
Expression.CaseExpression { cases } -> |
|
cases |
|
|> List.filterMap |
|
(\( patternNode, exprNode ) -> |
|
case patternNode |> Node.value of |
|
Pattern.NamedPattern { name } _ -> |
|
if name == "En" then |
|
let |
|
range = |
|
Node.range exprNode |
|
in |
|
Just ( range, context.extractSourceCode range ) |
|
|
|
else |
|
Nothing |
|
|
|
_ -> |
|
Nothing |
|
) |
|
|
|
_ -> |
|
[] |
|
) |
|
-- There should be exactly one match for the "En" case |
|
|> List.head |
|
in |
|
[ { name = function.declaration |> Node.value |> .name |> Node.value |
|
, result = z |> Maybe.map Tuple.second |
|
, range = z |> Maybe.map Tuple.first |
|
} |
|
] |
|
|
|
else |
|
[] |
|
|
|
_ -> |
|
[] |
|
|
|
fns = |
|
fns_ |> List.map (\f -> { name = f.name, result = f.result }) |
|
|
|
errors = |
|
fns_ |
|
|> List.filterMap |
|
(\f -> |
|
Just |
|
(\range orig -> |
|
{ range = range |
|
, orig = orig |
|
} |
|
) |
|
|> Maybe.Extra.andMap f.range |
|
|> Maybe.Extra.andMap (Dict.get f.name context.originalSourceByKey) |
|
) |
|
|> List.map |
|
(\f -> |
|
Rule.errorWithFix |
|
{ message = "Found message with original source" |
|
, details = [] |
|
} |
|
f.range |
|
[ Fix.replaceRangeBy f.range f.orig |
|
] |
|
) |
|
in |
|
( errors, { context | functions = fns ++ context.functions } ) |
|
|
|
|
|
dataExtractor : ProjectContext -> Encode.Value |
|
dataExtractor projectContext = |
|
projectContext.functions |
|
|> List.map (\d -> ( d.name, d.result )) |
|
|> Dict.fromList |
|
|> Encode.dict identity (Maybe.map Encode.string >> Maybe.withDefault Encode.null) |