diff --git a/cli/CLI.hs b/cli/CLI.hs index b854b7f..7475c48 100644 --- a/cli/CLI.hs +++ b/cli/CLI.hs @@ -7,16 +7,18 @@ import Prelude data Opts = Compile FilePath Locale | Flatten FilePath + | Lint FilePath getOpts :: IO Opts getOpts = execParser (info (opts <**> helper) (progDesc h)) where h = "Compile ICU messages into code." opts :: Parser Opts -opts = subparser - ( command "compile" (info (compile <**> helper) mempty) - <> command "flatten" (info (flatten <**> helper) mempty) - ) +opts = subparser . mconcat $ + [ command "compile" (info (compile <**> helper) mempty) + , command "flatten" (info (flatten <**> helper) mempty) + , command "lint" (info (lint <**> helper) mempty) + ] compile :: Parser Opts compile = Compile <$> pathp <*> localep @@ -24,6 +26,9 @@ compile = Compile <$> pathp <*> localep flatten :: Parser Opts flatten = Flatten <$> pathp +lint :: Parser Opts +lint = Lint <$> pathp + pathp :: Parser FilePath pathp = argument str (metavar "filepath") diff --git a/cli/Main.hs b/cli/Main.hs index ef2f14c..b344c2d 100644 --- a/cli/Main.hs +++ b/cli/Main.hs @@ -1,9 +1,11 @@ module Main where import CLI (Opts (..), getOpts) +import qualified Data.Map as M import qualified Data.Text as T import Intlc.Compiler (compileDataset, compileFlattened) import Intlc.Core +import Intlc.Linter import Intlc.Parser (parseDataset, printErr) import Intlc.Parser.Error (ParseFailure) import Prelude @@ -11,7 +13,14 @@ import Prelude main :: IO () main = getOpts >>= \case Compile path loc -> tryCompile loc =<< getParsed path - Flatten path -> either parserDie (putTextLn . compileFlattened) =<< getParsed path + Flatten path -> either parserDie (putTextLn . compileFlattened) =<< getParsed path + Lint path -> either parserDie lint =<< getParsed path + where lint = exit . M.mapMaybe (statusToMaybe . lintExternal . message) + exit sts + | M.size sts > 0 = die . T.unpack . ("Errors\n" <>) . M.foldrWithKey mkLine mempty $ sts + | otherwise = pure () + mkLine k es acc = acc <> "\n" <> k <> ": " <> e + where e = T.intercalate ", " . toList . fmap show $ es where tryCompile l = either parserDie (either compilerDie putTextLn . compileDataset l) parserDie = die . printErr compilerDie = die . T.unpack . ("Invalid keys:\n" <>) . T.intercalate "\n" . fmap ("\t" <>) . toList diff --git a/internal/CLI.hs b/internal/CLI.hs index b7e8e83..869178d 100644 --- a/internal/CLI.hs +++ b/internal/CLI.hs @@ -1,6 +1,3 @@ -{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} -{-# HLINT ignore "Use newtype instead of data" #-} - module CLI (Opts (..), getOpts) where import Options.Applicative diff --git a/internal/Main.hs b/internal/Main.hs index b2d8b07..ac747c7 100644 --- a/internal/Main.hs +++ b/internal/Main.hs @@ -26,13 +26,13 @@ main = getOpts >>= \case expandPlurals' = putTextLn . compileDataset . fmap (\x -> x { message = expandPlurals (message x) }) lint' :: Dataset Translation -> IO () - lint' = exit . M.mapMaybe (statusToMaybe . lint . message) + lint' = exit . M.mapMaybe (statusToMaybe . lintInternal . message) - exit :: Dataset (NonEmpty LintingError) -> IO () + exit :: Dataset (NonEmpty InternalLint) -> IO () exit sts | M.size sts > 0 = die . T.unpack . ("Errors\n" <>) . M.foldrWithKey mkLine mempty $ sts | otherwise = pure () - mkLine :: Text -> NonEmpty LintingError -> Text -> Text + mkLine :: Text -> NonEmpty InternalLint -> Text -> Text mkLine k es acc = acc <> "\n" <> k <> ": " <> e where e = T.intercalate ", " . toList . fmap show $ es diff --git a/intlc.cabal b/intlc.cabal index 82819af..2d370e0 100644 --- a/intlc.cabal +++ b/intlc.cabal @@ -23,6 +23,7 @@ common common , optics ^>=0.4 , relude ^>=1.0 , text ^>=1.2 + , these ^>=1.1 , validation ^>=1.1 mixins: base hiding (Prelude) diff --git a/lib/Intlc/Backend/ICU/Compiler.hs b/lib/Intlc/Backend/ICU/Compiler.hs index 3903c1a..a7b7e8d 100644 --- a/lib/Intlc/Backend/ICU/Compiler.hs +++ b/lib/Intlc/Backend/ICU/Compiler.hs @@ -8,8 +8,9 @@ module Intlc.Backend.ICU.Compiler where +import Data.These (These (..), mergeTheseWith) import Intlc.ICU -import Prelude hiding (Type) +import Prelude hiding (Type) compileMsg :: Message -> Text compileMsg (Message xs) = stream xs @@ -30,7 +31,7 @@ interp n (Time fmt) = "{" <> n <> ", time, " <> dateT interp n (Plural (Cardinal p)) = "{" <> n <> ", plural, " <> cardinalPlural p <> "}" interp n (Plural (Ordinal p)) = "{" <> n <> ", selectordinal, " <> ordinalPlural p <> "}" interp _ PluralRef = "#" -interp n (Select xs y) = "{" <> n <> ", select, " <> select xs y <> "}" +interp n (Select x) = "{" <> n <> ", select, " <> select x <> "}" interp n (Callback xs) = "<" <> n <> ">" <> stream xs <> " n <> ">" dateTimeFmt :: DateTimeFmt -> Text @@ -64,7 +65,7 @@ pluralRule Many = "many" pluralWildcard :: PluralWildcard -> Text pluralWildcard (PluralWildcard xs) = "other {" <> stream xs <> "}" -select :: NonEmpty SelectCase -> Maybe SelectWildcard -> Text -select xs mw = unwords . toList $ (case' <$> toList xs) <> foldMap (pure . wild) mw +select :: These (NonEmpty SelectCase) SelectWildcard -> Text +select = unwords . mergeTheseWith (toList . fmap case') (pure . wild) (<>) where case' (SelectCase n ys) = n <> " {" <> stream ys <> "}" wild (SelectWildcard ys) = "other {" <> stream ys <> "}" diff --git a/lib/Intlc/Backend/JavaScript/Language.hs b/lib/Intlc/Backend/JavaScript/Language.hs index 402091e..0ac4a82 100644 --- a/lib/Intlc/Backend/JavaScript/Language.hs +++ b/lib/Intlc/Backend/JavaScript/Language.hs @@ -1,5 +1,6 @@ module Intlc.Backend.JavaScript.Language where +import Data.These (These (..)) import Intlc.Core (Locale) import qualified Intlc.ICU as ICU import Prelude @@ -33,7 +34,7 @@ data MatchCond data MatchRet = LitMatchRet (NonEmpty Branch) - | NonLitMatchRet (NonEmpty Branch) Wildcard + | NonLitMatchRet [Branch] Wildcard | RecMatchRet (NonEmpty Branch) Match deriving (Show, Eq) @@ -60,32 +61,37 @@ fromInterp nraw t = x <- fromBoolCase True trueCase y <- fromBoolCase False falseCase pure . TMatch . Match n LitCond . LitMatchRet $ x :| [y] - ICU.String -> pure $ TStr n - ICU.Number -> pure $ TNum n - ICU.Date x -> pure $ TDate n x - ICU.Time x -> pure $ TTime n x - ICU.Plural x -> TMatch <$> fromPlural n x - ICU.PluralRef -> pure $ TNum n - ICU.Select cs (Just w) -> ((TMatch . Match n LitCond) .) . NonLitMatchRet <$> (fromSelectCase `mapM` cs) <*> fromSelectWildcard w - ICU.Select cs Nothing -> TMatch . Match n LitCond . LitMatchRet <$> (fromSelectCase `mapM` cs) - ICU.Callback xs -> TApply n <$> (fromToken `mapM` xs) + ICU.String -> pure $ TStr n + ICU.Number -> pure $ TNum n + ICU.Date x -> pure $ TDate n x + ICU.Time x -> pure $ TTime n x + ICU.Plural x -> TMatch <$> fromPlural n x + ICU.PluralRef -> pure $ TNum n + ICU.Select x -> case x of + (This cs) -> TMatch . Match n LitCond . LitMatchRet <$> ret + where ret = fromSelectCase `mapM` cs + (That w) -> TMatch . Match n LitCond <$> ret + where ret = NonLitMatchRet mempty <$> fromSelectWildcard w + (These cs w) -> TMatch . Match n LitCond <$> ret + where ret = NonLitMatchRet <$> (toList <$> fromSelectCase `mapM` cs) <*> fromSelectWildcard w + ICU.Callback xs -> TApply n <$> (fromToken `mapM` xs) where n = Ref nraw fromPlural :: Ref -> ICU.Plural -> ASTCompiler Match fromPlural r p = case p of ICU.Cardinal (ICU.LitPlural lcs mw) -> Match r LitCond <$> case mw of Nothing -> LitMatchRet <$> (fromExactPluralCase `mapM` lcs) - Just w -> NonLitMatchRet <$> (fromExactPluralCase `mapM` lcs) <*> fromPluralWildcard w + Just w -> NonLitMatchRet <$> (toList <$> fromExactPluralCase `mapM` lcs) <*> fromPluralWildcard w ICU.Cardinal (ICU.RulePlural rcs w) -> Match r CardinalPluralRuleCond <$> m - where m = NonLitMatchRet <$> (fromRulePluralCase `mapM` rcs) <*> fromPluralWildcard w + where m = NonLitMatchRet <$> (toList <$> fromRulePluralCase `mapM` rcs) <*> fromPluralWildcard w ICU.Cardinal (ICU.MixedPlural lcs rcs w) -> Match r LitCond <$> m where m = RecMatchRet <$> (fromExactPluralCase `mapM` lcs) <*> (Match r CardinalPluralRuleCond <$> im) - im = NonLitMatchRet <$> (fromRulePluralCase `mapM` rcs) <*> fromPluralWildcard w + im = NonLitMatchRet <$> (toList <$> fromRulePluralCase `mapM` rcs) <*> fromPluralWildcard w ICU.Ordinal (ICU.OrdinalPlural [] rcs w) -> Match r OrdinalPluralRuleCond <$> m - where m = NonLitMatchRet <$> (fromRulePluralCase `mapM` rcs) <*> fromPluralWildcard w + where m = NonLitMatchRet <$> (toList <$> fromRulePluralCase `mapM` rcs) <*> fromPluralWildcard w ICU.Ordinal (ICU.OrdinalPlural (lc:lcs) rcs w) -> Match r LitCond <$> m where m = RecMatchRet <$> ((:|) <$> fromExactPluralCase lc <*> (fromExactPluralCase `mapM` lcs)) <*> im - im = Match r OrdinalPluralRuleCond <$> (NonLitMatchRet <$> (fromRulePluralCase `mapM` rcs) <*> fromPluralWildcard w) + im = Match r OrdinalPluralRuleCond <$> (NonLitMatchRet <$> (toList <$> fromRulePluralCase `mapM` rcs) <*> fromPluralWildcard w) fromExactPluralCase :: ICU.PluralCase ICU.PluralExact -> ASTCompiler Branch fromExactPluralCase (ICU.PluralCase (ICU.PluralExact n) xs) = Branch n <$> (fromToken `mapM` xs) diff --git a/lib/Intlc/Backend/TypeScript/Language.hs b/lib/Intlc/Backend/TypeScript/Language.hs index 2868f28..12a5112 100644 --- a/lib/Intlc/Backend/TypeScript/Language.hs +++ b/lib/Intlc/Backend/TypeScript/Language.hs @@ -2,6 +2,7 @@ module Intlc.Backend.TypeScript.Language where import Data.List.NonEmpty (nub) import qualified Data.Map as M +import Data.These (These (..)) import qualified Intlc.ICU as ICU import Prelude @@ -48,21 +49,21 @@ fromToken ICU.Plaintext {} = mempty fromToken (ICU.Interpolation x y) = fromInterp x y fromInterp :: Text -> ICU.Type -> UncollatedArgs -fromInterp n (ICU.Bool xs ys) = (n, TBool) : (fromToken =<< xs) <> (fromToken =<< ys) -fromInterp n ICU.String = pure (n, TStr) -fromInterp n ICU.Number = pure (n, TNum) -fromInterp n ICU.Date {} = pure (n, TDate) -fromInterp n ICU.Time {} = pure (n, TDate) -fromInterp n (ICU.Plural x) = fromPlural n x +fromInterp n (ICU.Bool xs ys) = (n, TBool) : (fromToken =<< xs) <> (fromToken =<< ys) +fromInterp n ICU.String = pure (n, TStr) +fromInterp n ICU.Number = pure (n, TNum) +fromInterp n ICU.Date {} = pure (n, TDate) +fromInterp n ICU.Time {} = pure (n, TDate) +fromInterp n (ICU.Plural x) = fromPlural n x -- Plural references are treated as a no-op. -fromInterp _ ICU.PluralRef = mempty -fromInterp n (ICU.Select cs mw) = (n, t) : (fromSelectCase =<< toList cs) <> foldMap fromSelectWildcard mw +fromInterp _ ICU.PluralRef = mempty +fromInterp n (ICU.Select x) = case x of + (That w) -> (n, TStr) : fromSelectWildcard w + (These cs w) -> (n, TStr) : (fromSelectCase =<< toList cs) <> fromSelectWildcard w -- When there's no wildcard case we can compile to a union of string literals. - where t = case mw of - Just _ -> TStr - Nothing -> TStrLitUnion $ caseLit <$> cs - caseLit (ICU.SelectCase x _) = x -fromInterp n (ICU.Callback xs) = (n, TEndo) : (fromToken =<< xs) + (This cs) -> (n, TStrLitUnion (lit <$> cs)) : (fromSelectCase =<< toList cs) + where lit (ICU.SelectCase l _) = l +fromInterp n (ICU.Callback xs) = (n, TEndo) : (fromToken =<< xs) fromPlural :: Text -> ICU.Plural -> UncollatedArgs fromPlural n (ICU.Cardinal (ICU.LitPlural ls mw)) = (n, t) : (fromExactPluralCase =<< toList ls) <> foldMap fromPluralWildcard mw diff --git a/lib/Intlc/Compiler.hs b/lib/Intlc/Compiler.hs index 59f66df..4baa883 100644 --- a/lib/Intlc/Compiler.hs +++ b/lib/Intlc/Compiler.hs @@ -5,6 +5,7 @@ import Data.Foldable (elem) import Data.List.Extra (firstJust, unionBy) import qualified Data.Map as M import qualified Data.Text as T +import Data.These (These (..)) import Intlc.Backend.JavaScript.Compiler as JS import qualified Intlc.Backend.JSON.Compiler as JSON import qualified Intlc.Backend.TypeScript.Compiler as TS @@ -37,7 +38,7 @@ compileTranslation l k (Translation v be _) = case be of TypeScriptReact -> TS.compileNamedExport JSX l k v type ICUBool = (ICU.Stream, ICU.Stream) -type ICUSelect = (NonEmpty ICU.SelectCase, Maybe ICU.SelectWildcard) +type ICUSelect = These (NonEmpty ICU.SelectCase) ICU.SelectWildcard compileFlattened :: Dataset Translation -> Text compileFlattened = JSON.compileDataset . mapMsgs flatten @@ -52,11 +53,11 @@ mapTokens :: (ICU.Token -> ICU.Token) -> ICU.Stream -> ICU.Stream mapTokens f = fmap $ f >>> \case x@(ICU.Plaintext {}) -> x x@(ICU.Interpolation n t) -> case t of - ICU.Bool xs ys -> g $ ICU.Bool (h xs) (h ys) - ICU.Plural y -> g . ICU.Plural $ mapPluralStreams h y - ICU.Select ys mz -> g . uncurry ICU.Select $ mapSelectStreams h (ys, mz) - ICU.Callback ys -> g . ICU.Callback . h $ ys - _ -> x + ICU.Bool xs ys -> g $ ICU.Bool (h xs) (h ys) + ICU.Plural y -> g . ICU.Plural $ mapPluralStreams h y + ICU.Select y -> g . ICU.Select $ mapSelectStreams h y + ICU.Callback ys -> g . ICU.Callback . h $ ys + _ -> x where g = ICU.Interpolation n h = fmap f @@ -69,8 +70,8 @@ flatten (ICU.Message xs) = ICU.Message . flattenStream $ xs , mapPlural <$> extractFirstPlural ys ] mapBool (n, ls, boo, rs) = streamFromArg n . uncurry ICU.Bool $ mapBoolStreams (around ls rs) boo - mapSelect (n, ls, sel, rs) = streamFromArg n . uncurry ICU.Select $ mapSelectStreams (around ls rs) sel - mapPlural (n, ls, plu, rs) = streamFromArg n . ICU.Plural $ mapPluralStreams (around ls rs) plu + mapSelect (n, ls, sel, rs) = streamFromArg n . ICU.Select $ mapSelectStreams (around ls rs) sel + mapPlural (n, ls, plu, rs) = streamFromArg n . ICU.Plural $ mapPluralStreams (around ls rs) plu around ls rs = flattenStream . ICU.mergePlaintext . surround ls rs surround ls rs cs = ls <> cs <> rs streamFromArg n = pure . ICU.Interpolation n @@ -119,8 +120,8 @@ extractFirstArg f xs = firstJust arg (zip [0..] xs) extractFirstSelect :: ICU.Stream -> Maybe (Text, ICU.Stream, ICUSelect, ICU.Stream) extractFirstSelect = extractFirstArg $ \case - ICU.Select xs y -> Just (xs, y) - _ -> Nothing + ICU.Select x -> Just x + _ -> Nothing extractFirstPlural :: ICU.Stream -> Maybe (Text, ICU.Stream, ICU.Plural, ICU.Stream) extractFirstPlural = extractFirstArg $ \case @@ -131,7 +132,7 @@ mapBoolStreams :: (ICU.Stream -> ICU.Stream) -> ICUBool -> ICUBool mapBoolStreams f (xs, ys) = (f xs, f ys) mapSelectStreams :: (ICU.Stream -> ICU.Stream) -> ICUSelect -> ICUSelect -mapSelectStreams f (xs, mw) = (mapSelectCase f <$> xs, mapSelectWildcard f <$> mw) +mapSelectStreams f = bimap (fmap (mapSelectCase f)) (mapSelectWildcard f) mapSelectCase :: (ICU.Stream -> ICU.Stream) -> ICU.SelectCase -> ICU.SelectCase mapSelectCase f (ICU.SelectCase x ys) = ICU.SelectCase x (f ys) diff --git a/lib/Intlc/ICU.hs b/lib/Intlc/ICU.hs index 2ec5107..23054a6 100644 --- a/lib/Intlc/ICU.hs +++ b/lib/Intlc/ICU.hs @@ -5,7 +5,8 @@ module Intlc.ICU where -import Prelude hiding (Type) +import Data.These (These (..), mergeTheseWith) +import Prelude hiding (Type) newtype Message = Message Stream deriving (Show, Eq) @@ -39,11 +40,9 @@ getStream (Interpolation _ t) = case t of PluralRef -> Nothing Bool {trueCase, falseCase} -> Just $ trueCase <> falseCase Plural x -> Just $ getPluralStream x - Select cs mw -> Just $ ss <> ws - where ss = (\(SelectCase _ xs) -> xs) `concatMap` cs - ws = case mw of - Nothing -> [] - Just (SelectWildcard xs) -> xs + Select x -> Just . mergeTheseWith (concatMap f) g (<>) $ x + where f (SelectCase _ xs) = xs + g (SelectWildcard w) = w Callback xs -> Just xs getPluralStream :: Plural -> Stream @@ -90,7 +89,7 @@ data Type -- Plural hash references have their own distinct type rather than merely -- taking on `Number` to allow compilers to infer appropriately. | PluralRef - | Select (NonEmpty SelectCase) (Maybe SelectWildcard) + | Select (These (NonEmpty SelectCase) SelectWildcard) | Callback Stream deriving (Show, Eq) diff --git a/lib/Intlc/Linter.hs b/lib/Intlc/Linter.hs index 2f18ff8..0c9fa96 100644 --- a/lib/Intlc/Linter.hs +++ b/lib/Intlc/Linter.hs @@ -1,36 +1,61 @@ module Intlc.Linter where +import Data.These (These (..)) import Intlc.ICU -import Prelude hiding (Type) +import Prelude -data LintingError +data ExternalLint + = RedundantSelect + deriving (Eq, Show) + +data InternalLint = TooManyInterpolations deriving (Eq, Show) -data Status +data Status a = Success - | Failure (NonEmpty LintingError) + | Failure (NonEmpty a) deriving (Eq, Show) -statusToMaybe :: Status -> Maybe (NonEmpty LintingError) +type Rule a = Stream -> Maybe a + +statusToMaybe :: Status a -> Maybe (NonEmpty a) statusToMaybe Success = Nothing statusToMaybe (Failure xs) = Just xs -maybeToStatus :: Maybe (NonEmpty LintingError) -> Status +maybeToStatus :: Maybe (NonEmpty a) -> Status a maybeToStatus Nothing = Success maybeToStatus (Just xs) = Failure xs -interpolationsRule :: Stream -> Maybe LintingError +redundantSelectRule :: Rule ExternalLint +redundantSelectRule [] = Nothing +redundantSelectRule (x:xs) + | isRedundant x = Just RedundantSelect + | otherwise = redundantSelectRule xs + -- If there's only a wildcard it could have been a plain string instead. + where isRedundant (Interpolation _ (Select (That _w))) = True + isRedundant _ = False + +interpolationsRule :: Rule InternalLint interpolationsRule = go 0 where - go :: Int -> Stream -> Maybe LintingError + go :: Int -> Rule InternalLint go 2 _ = Just TooManyInterpolations go _ [] = Nothing go n (x:xs) = go n' $ maybeToMonoid mys <> xs where mys = getStream x n' = n + length mys -lint :: Message -> Status -lint (Message stream) = toStatus $ rules `flap` stream +lintWith :: [Rule a] -> Message -> Status a +lintWith rules (Message stream) = toStatus $ rules `flap` stream where toStatus = maybeToStatus . nonEmpty . catMaybes - rules = [interpolationsRule] + +lintExternal :: Message -> Status ExternalLint +lintExternal = lintWith + [ redundantSelectRule + ] + +lintInternal :: Message -> Status InternalLint +lintInternal = lintWith + [ interpolationsRule + ] diff --git a/lib/Intlc/Parser/ICU.hs b/lib/Intlc/Parser/ICU.hs index 6333191..56ea432 100644 --- a/lib/Intlc/Parser/ICU.hs +++ b/lib/Intlc/Parser/ICU.hs @@ -11,6 +11,7 @@ module Intlc.Parser.ICU where import qualified Control.Applicative.Combinators.NonEmpty as NE import qualified Data.Text as T +import Data.These (These (..)) import Data.Void () import Intlc.ICU import Intlc.Parser.Error (MessageParseErr (..), @@ -136,7 +137,7 @@ interp = between (char '{') (char '}') $ do string "plural" *> sep *> cardinalPluralCases <|> string "selectordinal" *> sep *> ordinalPluralCases ) - , uncurry Select <$> (string "select" *> sep *> selectCases) + , Select <$> (string "select" *> sep *> selectCases) ] withPluralCtx n = withReaderT (\x -> x { pluralCtxName = Just n }) @@ -157,10 +158,15 @@ boolCases = (,) <* hspace1 <*> (string "false" *> hspace1 *> caseBody) -selectCases :: Parser (NonEmpty SelectCase, Maybe SelectWildcard) -selectCases = (,) <$> cases <*> optional wildcard +selectCases :: Parser (These (NonEmpty SelectCase) SelectWildcard) +selectCases = choice + [ reconcile <$> cases <*> optional wildcard + , That <$> wildcard + ] where cases = NE.sepEndBy1 (SelectCase <$> (name <* hspace1) <*> caseBody) hspace1 wildcard = SelectWildcard <$> (string wildcardName *> hspace1 *> caseBody) + reconcile cs (Just w) = These cs w + reconcile cs Nothing = This cs name = try $ mfilter (/= wildcardName) ident wildcardName = "other" diff --git a/test/Intlc/Backend/TypeScriptSpec.hs b/test/Intlc/Backend/TypeScriptSpec.hs index 09de66d..7958e1f 100644 --- a/test/Intlc/Backend/TypeScriptSpec.hs +++ b/test/Intlc/Backend/TypeScriptSpec.hs @@ -1,6 +1,7 @@ module Intlc.Backend.TypeScriptSpec (spec) where import qualified Data.Text as T +import Data.These (These (..)) import Intlc.Backend.JavaScript.Compiler (InterpStrat (..)) import Intlc.Backend.TypeScript.Compiler (compileNamedExport, compileTypeof) @@ -28,7 +29,7 @@ golden strat compiler name msg = baseCfg spec :: Spec spec = describe "TypeScript compiler" $ do describe "golden" $ do - let msg = ICU.Message $ + let msg = ICU.Message [ ICU.Plaintext "Hello " , ICU.Interpolation "bold" (ICU.Callback (pure $ ICU.Interpolation "name" ICU.String @@ -48,11 +49,11 @@ spec = describe "TypeScript compiler" $ do , ICU.Plaintext ", and the time is " , ICU.Interpolation "currTime" (ICU.Time ICU.Full) , ICU.Plaintext ". And just to recap, your name is " - , ICU.Interpolation "name" (ICU.Select (fromList + , ICU.Interpolation "name" (ICU.Select . This . fromList $ [ ICU.SelectCase "Sam" [ICU.Plaintext "undoubtedly excellent"] , ICU.SelectCase "Ashley" [ICU.Plaintext "fairly good"] ] - ) Nothing) + ) , ICU.Plaintext ". Finally, you are " , ICU.Interpolation "isDev" (ICU.Bool { ICU.trueCase = [ICU.Plaintext "a software engineer"] @@ -92,13 +93,13 @@ spec = describe "TypeScript compiler" $ do -- Typechecking happens externally. it "typechecks nested selects" $ do golden TemplateLit (compileNamedExport TemplateLit (Locale "te-ST") "test") "nested-select" $ - ICU.Message [ICU.Interpolation "x" $ flip ICU.Select Nothing $ (fromList + ICU.Message [ICU.Interpolation "x" . ICU.Select . This $ fromList [ ICU.SelectCase "a" [] - , ICU.SelectCase "b" [ICU.Interpolation "x" $ flip ICU.Select Nothing $ (fromList + , ICU.SelectCase "b" [ICU.Interpolation "x" . ICU.Select . This $ fromList [ ICU.SelectCase "a" [] -- <-- without a workaround, TypeScript will have narrowed and reject this case , ICU.SelectCase "b" [] - ])] - ])] + ]] + ]] describe "collects nested arguments" $ do let args (TS.Lambda xs _) = xs @@ -106,7 +107,7 @@ spec = describe "TypeScript compiler" $ do let fromArgs = fromList it "in select" $ do - let x = flip ICU.Select Nothing . pure $ ICU.SelectCase "foo" [ICU.Interpolation "y" ICU.String] + let x = ICU.Select . This . pure $ ICU.SelectCase "foo" [ICU.Interpolation "y" ICU.String] let ys = [ ("x", pure (TS.TStrLitUnion (pure "foo"))) , ("y", pure TS.TStr) diff --git a/test/Intlc/CompilerSpec.hs b/test/Intlc/CompilerSpec.hs index 824f8e7..a744f3a 100644 --- a/test/Intlc/CompilerSpec.hs +++ b/test/Intlc/CompilerSpec.hs @@ -1,5 +1,6 @@ module Intlc.CompilerSpec (spec) where +import Data.These (These (..)) import Intlc.Compiler (compileDataset, compileFlattened, expandRules, flatten) import Intlc.Core (Backend (..), Locale (Locale), @@ -49,12 +50,12 @@ spec = describe "compiler" $ do let other = SelectWildcard [Plaintext "many dogs"] let otherf = SelectWildcard [Plaintext "I have many dogs"] - flatten (Message [Plaintext "I have ", Interpolation "thing" (Select (pure foo) (pure other))]) `shouldBe` - Message (pure $ Interpolation "thing" (Select (pure foof) (pure otherf))) + flatten (Message [Plaintext "I have ", Interpolation "thing" (Select $ These (pure foo) other)]) `shouldBe` + Message (pure $ Interpolation "thing" (Select $ These (pure foof) otherf)) it "without a wildcard" $ do - flatten (Message [Plaintext "I have ", Interpolation "thing" (Select (pure foo) empty)]) `shouldBe` - Message (pure $ Interpolation "thing" (Select (pure foof) empty)) + flatten (Message [Plaintext "I have ", Interpolation "thing" (Select $ This (pure foo))]) `shouldBe` + Message (pure $ Interpolation "thing" (Select $ This (pure foof))) it "flattens shallow plural" $ do let other = PluralWildcard [Plaintext "many dogs"] @@ -73,9 +74,9 @@ spec = describe "compiler" $ do (PluralWildcard [ Interpolation "count" Number , Plaintext " dogs, the newest of which is " - , Interpolation "name" $ Select + , Interpolation "name" . Select $ These (pure $ SelectCase "hodor" [Plaintext "Hodor"]) - (pure $ SelectWildcard [Plaintext "unknown"]) + (SelectWildcard [Plaintext "unknown"]) ] ) , Plaintext "!" @@ -84,14 +85,14 @@ spec = describe "compiler" $ do Interpolation "count" . Plural . Cardinal $ RulePlural (pure $ PluralCase One [Plaintext "I have a dog!"]) (PluralWildcard - [ Interpolation "name" $ Select + [ Interpolation "name" . Select $ These (pure $ SelectCase "hodor" [ Plaintext "I have " , Interpolation "count" Number , Plaintext " dogs, the newest of which is Hodor!" ] ) - (pure $ SelectWildcard + (SelectWildcard [ Plaintext "I have " , Interpolation "count" Number , Plaintext " dogs, the newest of which is unknown!" diff --git a/test/Intlc/EndToEndSpec.hs b/test/Intlc/EndToEndSpec.hs index 0c88da9..37610d4 100644 --- a/test/Intlc/EndToEndSpec.hs +++ b/test/Intlc/EndToEndSpec.hs @@ -6,12 +6,13 @@ import Intlc.Compiler (compileDataset, expandPlurals) import Intlc.Core (Locale (Locale)) import Intlc.Parser (parseDataset) import Intlc.Parser.Error (ParseFailure) -import Intlc.Parser.ICU (msg, emptyState, ParserState (endOfInput)) +import Intlc.Parser.ICU (ParserState (endOfInput), + emptyState, msg) import Prelude import System.FilePath ((<.>), ()) import Test.Hspec import Test.Hspec.Golden (Golden (..), defaultGolden) -import Text.Megaparsec (runParser, eof) +import Text.Megaparsec (eof, runParser) import Text.RawString.QQ (r) parseAndCompileDataset :: Text -> Either (NonEmpty Text) Text diff --git a/test/Intlc/LinterSpec.hs b/test/Intlc/LinterSpec.hs index b2c6c21..8b001a1 100644 --- a/test/Intlc/LinterSpec.hs +++ b/test/Intlc/LinterSpec.hs @@ -1,41 +1,63 @@ module Intlc.LinterSpec where +import Data.These (These (..)) import Intlc.ICU import Intlc.Linter import Prelude import Test.Hspec +lintWith' :: Rule a -> Message -> Status a +lintWith' = lintWith . pure + spec :: Spec spec = describe "linter" $ do - it "lints streams with 1 plain text token" $ do - lint (Message [Plaintext "yay"]) `shouldBe` Success + describe "external" $ do + describe "redundant select" $ do + let lint = lintWith' redundantSelectRule + + it "succeeds on select with any non-wildcard case" $ do + lint (Message [Interpolation "x" (Select $ This (pure $ SelectCase "y" []))]) + `shouldBe` Success + lint (Message [Interpolation "x" (Select $ These (pure $ SelectCase "y" []) (SelectWildcard []))]) + `shouldBe` Success + + it "fails on select with only a wildcard" $ do + lint (Message [Interpolation "x" (Select $ That (SelectWildcard []))]) + `shouldBe` Failure (pure RedundantSelect) + + describe "internal" $ do + describe "interpolations" $ do + let lint = lintWith' interpolationsRule + + it "lints streams with 1 plain text token" $ do + lint (Message [Plaintext "yay"]) `shouldBe` Success - it "lints streams with 2 or more plain text token" $ do - lint (Message [Plaintext "yay", Plaintext "Hello"]) `shouldBe` Success + it "lints streams with 2 or more plain text token" $ do + lint (Message [Plaintext "yay", Plaintext "Hello"]) `shouldBe` Success - it "lints streams with 1 simple interpolation" $ do - lint (Message [Interpolation "Hello" String]) `shouldBe` Success + it "lints streams with 1 simple interpolation" $ do + lint (Message [Interpolation "Hello" String]) `shouldBe` Success - it "lints streams with 1 complex interpolation" $ do - lint (Message [Interpolation "Hello" (Callback [])]) `shouldBe` Success + it "lints streams with 1 complex interpolation" $ do + lint (Message [Interpolation "Hello" (Callback [])]) `shouldBe` Success - it "lints streams with 1 complex interpolation and 1 simple interpolation" $ do - lint (Message [Interpolation "Hello" (Callback []), Plaintext "hello"]) `shouldBe` Success + it "lints streams with 1 complex interpolation and 1 simple interpolation" $ do + lint (Message [Interpolation "Hello" (Callback []), Plaintext "hello"]) `shouldBe` Success - it "does not lint streams with 2 or more complex interpolations" $ do - lint (Message [Interpolation "Hello" (Callback []), Interpolation "Hello" (Bool [] [])]) `shouldBe` Failure (pure TooManyInterpolations) + it "does not lint streams with 2 or more complex interpolations" $ do + lint (Message [Interpolation "Hello" (Callback []), Interpolation "Hello" (Bool [] [])]) `shouldBe` Failure (pure TooManyInterpolations) - it "does not lint nested streams" $ do - lint (Message [Interpolation "outer" (Callback [Interpolation "inner" (Callback [])])]) `shouldBe` Failure (pure TooManyInterpolations) + it "does not lint nested streams" $ do + lint (Message [Interpolation "outer" (Callback [Interpolation "inner" (Callback [])])]) `shouldBe` Failure (pure TooManyInterpolations) - it "does not lint complex interpolations with nested complex interpolations" $ do - lint (Message [Interpolation "outer" (Select (fromList [SelectCase "hello" [Interpolation "super_inner" (Callback [])]]) Nothing)]) `shouldBe` Failure (pure TooManyInterpolations) + it "does not lint complex interpolations with nested complex interpolations" $ do + lint (Message [Interpolation "outer" (Select (This (pure $ SelectCase "hello" [Interpolation "super_inner" (Callback [])])))]) `shouldBe` Failure (pure TooManyInterpolations) - it "stops iterating after encountering two stream-interpolations" $ do - let nested x = Interpolation "x" (Callback [x]) - let e = error "should not reach this item" + it "stops iterating after encountering two stream-interpolations" $ do + let nested x = Interpolation "x" (Callback [x]) + let e = error "should not reach this item" - lint (Message - [ nested (nested e) - , e - ]) `shouldBe` Failure (pure TooManyInterpolations) + lint (Message + [ nested (nested e) + , e + ]) `shouldBe` Failure (pure TooManyInterpolations) diff --git a/test/Intlc/Parser/ICUSpec.hs b/test/Intlc/Parser/ICUSpec.hs index 5291112..1d6579e 100644 --- a/test/Intlc/Parser/ICUSpec.hs +++ b/test/Intlc/Parser/ICUSpec.hs @@ -1,5 +1,6 @@ module Intlc.Parser.ICUSpec (spec) where +import Data.These (These (..)) import Intlc.ICU import Intlc.Parser.Error (MessageParseErr (..), ParseErr (FailedMsgParse), ParseFailure) @@ -7,7 +8,7 @@ import Intlc.Parser.ICU import Prelude import Test.Hspec import Test.Hspec.Megaparsec -import Text.Megaparsec (runParser, eof) +import Text.Megaparsec (eof, runParser) import Text.Megaparsec.Error (ErrorFancy (ErrorCustom)) parseWith :: ParserState -> Parser a -> Text -> Either ParseFailure a @@ -36,7 +37,7 @@ spec = describe "ICU parser" $ do parse msg "#" `shouldParse` Message [Plaintext "#"] parse msg "{x, select, y {#}}" `shouldParse` (Message . pure . Interpolation "x" $ - Select (pure $ SelectCase "y" (pure $ Plaintext "#")) Nothing) + Select (This . pure $ SelectCase "y" (pure $ Plaintext "#"))) it "parses as arg inside shallow plural" $ do let n = pure $ Interpolation "n" PluralRef @@ -192,14 +193,16 @@ spec = describe "ICU parser" $ do Cardinal (MixedPlural (pure $ PluralCase (PluralExact "0") [Plaintext "foo"]) (pure $ PluralCase Few [Plaintext "bar"]) (PluralWildcard [Plaintext "baz ", Interpolation "xyz" PluralRef])) describe "select" $ do + let selectCases' = selectCases <* eof + it "disallows wildcard not at the end" $ do - parse selectCases "foo {bar} other {baz}" `shouldParse` (pure (SelectCase "foo" [Plaintext "bar"]), Just (SelectWildcard [Plaintext "baz"])) - parse selectCases `shouldFailOn` "other {bar} foo {baz}" + parse selectCases' "foo {bar} other {baz}" `shouldParse` These (pure $ SelectCase "foo" [Plaintext "bar"]) (SelectWildcard [Plaintext "baz"]) + parse selectCases' `shouldFailOn` "other {bar} foo {baz}" it "tolerates empty cases" $ do - parse selectCases "x {} other {}" `shouldParse` (pure (SelectCase "x" []), Just (SelectWildcard [])) + parse selectCases' "x {} other {}" `shouldParse` These (pure $ SelectCase "x" []) (SelectWildcard []) - it "requires at least one non-wildcard case" $ do - parse selectCases "foo {bar}" `shouldParse` (pure (SelectCase "foo" [Plaintext "bar"]), Nothing) - parse selectCases "foo {bar} other {baz}" `shouldParse` (pure (SelectCase "foo" [Plaintext "bar"]), Just (SelectWildcard [Plaintext "baz"])) - parse selectCases `shouldFailOn` "other {foo}" + it "allows no non-wildcard case" $ do + parse selectCases' "foo {bar}" `shouldParse` This (pure $ SelectCase "foo" [Plaintext "bar"]) + parse selectCases' "foo {bar} other {baz}" `shouldParse` These (pure $ SelectCase "foo" [Plaintext "bar"]) (SelectWildcard [Plaintext "baz"]) + parse selectCases' "other {foo}" `shouldParse` That (SelectWildcard [Plaintext "foo"]) diff --git a/test/Intlc/Parser/JSONSpec.hs b/test/Intlc/Parser/JSONSpec.hs index a7abe11..0db3f3c 100644 --- a/test/Intlc/Parser/JSONSpec.hs +++ b/test/Intlc/Parser/JSONSpec.hs @@ -1,6 +1,7 @@ module Intlc.Parser.JSONSpec (spec) where import Intlc.Core +import qualified Intlc.ICU as ICU import Intlc.Parser (parseDataset) import Intlc.Parser.Error (JSONParseErr (..), MessageParseErr (..), ParseErr (..), ParseFailure) @@ -9,7 +10,6 @@ import Test.Hspec import Test.Hspec.Megaparsec import Text.Megaparsec (ErrorFancy (ErrorCustom), ParseError) import Text.RawString.QQ (r) -import qualified Intlc.ICU as ICU parse :: Text -> Either ParseFailure (Dataset Translation) parse = parseDataset "test"