Skip to content

Instantly share code, notes, and snippets.

@pete-murphy
Last active June 21, 2025 15:40
Show Gist options
  • Save pete-murphy/c348503245fb5e17d0f66b1780507d38 to your computer and use it in GitHub Desktop.
Save pete-murphy/c348503245fb5e17d0f66b1780507d38 to your computer and use it in GitHub Desktop.
Non-simple message data extractor and fixer

Non-simple message reporter

Simple messages have the shape Lang -> String

This rule is in a dirty/WIP state with lots of extraneous cruft, but declarationEnterVisitor is the significant part, and it serves two purposes:

  1. reports non-simple messages (despite the name of the rule / module) for use with Rule.withDataExtractor
  2. reports errors for messages whose key appears in out.json (these are messages that were found to have been edited unintentionally, the JSON is a map from message key / function name to original source that we should restore)
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment