{-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE TypeFamilies #-} -- | Checks that Descript source is well-formed before it's interpreted. -- If the source isn't well-formed, generates user-friendly problems. module Descript.BasicInj.Process.Validate ( validateForRefactor , validate , validate_ , validate' , validateForRefactorIn , validateIn , validateIn_ , validateIn' ) where import qualified Descript.BasicInj.Traverse.Term as T import Descript.BasicInj.Traverse import qualified Descript.BasicInj.Data.Value.In as In import qualified Descript.BasicInj.Data.Value.Out as Out import qualified Descript.BasicInj.Data.Type as RecordType (head) import Descript.BasicInj.Data import qualified Descript.Misc.Build.Process.Validate.Term as Term import Descript.Misc import Data.Semigroup as S import Data.Monoid as M import Data.List import Core.Data.List import Core.Data.List.Assoc import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as NonEmpty import Core.Control.Monad.Trans import Prelude hiding (head, mod) newtype ValidateAll an = ValidateAll (AModule ()) newtype ValidateRecords an = ValidateRecords (RecordCtx ()) newtype ValidatePropPaths an = ValidatePropPaths (In.Value ()) instance (Semigroup an) => Fold (ValidateAll an) where type Res (ValidateAll an) = [Problem an] type FAnn (ValidateAll an) = an fonTerm T.Program (ValidateAll extra) prog = foldTerm T.Query (ValidateRecords recordCtx') (query prog) where recordCtx' = recordCtx_ M.<> recordCtx extra recordCtx_ = remAnns $ recordCtx $ amodule $ module' prog fonTerm T.AModule (ValidateAll extra) mod = foldTerm T.ReduceCtx (ValidateRecords recordCtx') (reduceCtx mod) where recordCtx' = recordCtx_ M.<> recordCtx extra recordCtx_ = remAnns $ recordCtx mod fonTerm T.ImportCtx (ValidateAll _) (ImportCtx _ _ idecls) = validateImportDeclsNoDups idecls fonTerm T.RecordCtx (ValidateAll extra) (RecordCtx _ decls) = validateRecordDeclsNoDups extraDecls decls where extraDecls = recordCtxDecls $ recordCtx extra fonTerm T.Reducer (ValidateAll _) (Reducer _ input' output') = foldTerm T.Output (ValidatePropPaths input_) output' where input_ = remAnns input' fonTerm T.GenRecord (ValidateAll _) (Record _ _ props) = validatePropsNoDups props fonTerm T.InjApp (ValidateAll _) injApp = validateInjAppExists injApp fonTerm _ _ _ = mempty instance (Semigroup an) => Fold (ValidateRecords an) where type Res (ValidateRecords an) = [Problem an] type FAnn (ValidateRecords an) = an fonTerm T.GenRecord (ValidateRecords recordCtx') record = validateRecordConforms recordCtx' record fonTerm _ _ _ = mempty instance (Semigroup an) => Fold (ValidatePropPaths an) where type Res (ValidatePropPaths an) = [Problem an] type FAnn (ValidatePropPaths an) = an fonTerm T.PropPath (ValidatePropPaths input') path = validatePathExists input' path fonTerm _ _ _ = mempty -- | If the node is valid, returns an empty success. -- Otherwise returns a refactor failure. validateForRefactor :: (Monad u) => DirtyDepd Source SrcAnn -> RefactorResultT u () validateForRefactor = hoist . mapError RefactorValidateError . validate_ -- | If the source is valid, returns a success containing it. -- Otherwise returns a failure containing all the problems. validate :: (Ord an, Semigroup an) => DirtyDepd Source an -> Result [Problem an] (Depd Source an) validate dx | null problems = Success $ mapDep dirtyVal dx | otherwise = Failure problems where problems = validate' dx -- | If the source is valid, returns an empty success. -- Otherwise returns a failure containing all the problems. -- Useful in do notation. validate_ :: (Ord an, Semigroup an) => DirtyDepd Source an -> Result [Problem an] () validate_ dx | null problems = Success () | otherwise = Failure problems where problems = validate' dx -- | Finds all problems within the source. If the source has no -- problems, it's well formed and can be interpreted. validate' :: (Ord an, Semigroup an) => DirtyDepd Source an -> [Problem an] validate' (Depd (Dirty derrs extra) x) = dprobs ++ rprobs where dprobs = map ProblemDepFail derrs rprobs = validateIn' T.Source extra x -- | If the node is valid, returns an empty success. -- Otherwise returns a refactor failure. validateForRefactorIn :: (Monad u) => TTerm t -> AModule () -> t SrcAnn -> RefactorResultT u () validateForRefactorIn term extra = hoist . mapError RefactorValidateError . validateIn_ term extra -- | If the node is valid, returns a success containing it. -- Otherwise returns a failure containing all the problems. -- Uses context (e.g. declared records) from the module. validateIn :: (Ord an, Semigroup an) => TTerm t -> AModule () -> t an -> Result [Problem an] (t an) validateIn term extra x | null problems = Success x | otherwise = Failure problems where problems = validateIn' term extra x -- | If the node is valid, returns an empty success. -- Otherwise returns a failure containing all the problems. -- Useful in do notation. -- Uses context (e.g. declared records) from the module. validateIn_ :: (Ord an, Semigroup an) => TTerm t -> AModule () -> t an -> Result [Problem an] () validateIn_ term extra x | null problems = Success () | otherwise = Failure problems where problems = validateIn' term extra x -- | Finds all problems within the node. If the node has no -- problems, it's well formed and can be interpreted. -- Uses context (e.g. declared records) from the module. validateIn' :: (Ord an, Semigroup an) => TTerm t -> AModule () -> t an -> [Problem an] validateIn' term extra = sortOn getAnn . foldTerm term (ValidateAll extra) validateImportDeclsNoDups :: (Semigroup an) => [ImportDecl an] -> [Problem an] validateImportDeclsNoDups decls = concat $ zipWith validateImportDeclNoConflict otherDeclss decls where otherDeclss = inits $ map remAnns decls validateRecordDeclsNoDups :: (Semigroup an) => [RecordDecl ()] -> [RecordDecl an] -> [Problem an] validateRecordDeclsNoDups extraDecls decls = concat $ zipWith validateRecordDeclNoDup otherDeclss decls where otherDeclss = map (extraDecls ++) $ inits $ map remAnns decls validatePropsNoDups :: (Semigroup an, FwdPrintable v, GenPropVal v) => [GenProperty v an] -> [Problem an] validatePropsNoDups props = concat $ zipWith validatePropNoDup otherPropss props where otherPropss = inits $ map remAnns props validateImportDeclNoConflict :: (Semigroup an) => [ImportDecl ()] -> ImportDecl an -> [Problem an] validateImportDeclNoConflict prevDecls decl | any (`importsConflict` decl) prevDecls = [Conflict (getAnn decl) Term.Import $ pprintStr decl] | otherwise = [] validateRecordDeclNoDup :: (Semigroup an) => [RecordDecl ()] -> RecordDecl an -> [Problem an] validateRecordDeclNoDup prevDecls decl | any (`declsConflict` decl) prevDecls = [Duplicate (getAnn decl) Term.RecordDecl $ pprintStr decl] | otherwise = [] validatePropNoDup :: (Semigroup an, FwdPrintable v, GenPropVal v) => [GenProperty v ()] -> GenProperty v an -> [Problem an] validatePropNoDup prevProps prop | any (`propsConflict` prop) prevProps = [Duplicate (getAnn prop) Term.Property $ pprintStr prop] | otherwise = [] -- | Whether both imports are redundant and/or could break each other. importsConflict :: ImportDecl an1 -> ImportDecl an2 -> Bool ImportDecl _ xPath xISrcs xIDsts `importsConflict` ImportDecl _ yPath yISrcs yIDsts = xPath =@= yPath && (isPermBy (=@=) xISrcs yISrcs || overlapsBy (=@=) xIDsts yIDsts) -- | Whether both declarations' types conflict. declsConflict :: RecordDecl an1 -> RecordDecl an2 -> Bool RecordDecl _ x `declsConflict` RecordDecl _ y = x `typesConflict` y -- | Whether both types have the same head - whether both types can't -- exist in the same context. typesConflict :: RecordType an1 -> RecordType an2 -> Bool x `typesConflict` y = xHead =@= yHead && xHead /@= undefinedFSym where xHead = RecordType.head x yHead = RecordType.head y -- | Whether both properties conflict - whether they can't be in the -- same record. propsConflict :: GenProperty v1 an1 -> GenProperty v2 an2 -> Bool x `propsConflict` y = xKey =@= yKey && xKey /@= undefinedSym where xKey = propertyKey x yKey = propertyKey y -- | Validates that the record's type was defined, and it conforms. validateRecordConforms :: (Semigroup an) => RecordCtx () -> GenRecord v an -> [Problem an] validateRecordConforms ctx (Record ann head' properties') = case recordTypeFor ctx head_ of Nothing -> [UndeclaredRecord (getAnn head') $ pprintStr head'] Just (RecordType _ _ typeProps) -> validatePropsComplete typeProps ann properties' ++ validatePropsFit typeProps properties' where head_ = remAnns head' validatePropsComplete :: (Semigroup an) => [Symbol ()] -> an -> [GenProperty v an] -> [Problem an] validatePropsComplete typeProps ann properties' = case filter (not . (`assocMember` properties')) typeProps_ of [] -> [] missingProps -> [IncompleteRecord ann $ map pprintStr missingProps] where typeProps_ = map remAnns typeProps validatePropsFit :: (Semigroup an) => [Symbol ()] -> [GenProperty v an] -> [Problem an] validatePropsFit declProps properties' = case deleteFirstsBy' (=@=) declProps propKeys of [] -> [] extraKeys@(x : xs) -> [OvercompleteRecord extraKeysAnn $ extraKeyPrs] where extraKeyPrs = map pprintStr extraKeys extraKeysAnn = sconcat $ NonEmpty.map getAnn extraKeys' extraKeys' = x :| xs where propKeys = map propertyKey properties' validatePathExists :: (Semigroup an) => In.Value () -> PropPath an -> [Problem an] validatePathExists input' (PropPath _ elems) = validateSubpathExists (Just input') $ NonEmpty.toList elems validateSubpathExists :: (Semigroup an) => Maybe (In.Value ()) -> [PathElem an] -> [Problem an] validateSubpathExists _ [] = [] validateSubpathExists input' ((PathElem _ keyRef' headRef') : elems) = case recWithHead headRef_ =<< input' of Nothing -> [UndeclaredPathElemHead (getAnn headRef') $ pprintStr headRef'] Just inRec -> case lookupProp keyRef_ inRec of Nothing -> [UndeclaredPathElemKey (getAnn keyRef') $ pprintStr keyRef'] Just subInput -> validateSubpathExists subInput' elems where subInput' = In.optValToMaybeVal subInput where headRef_ = remAnns headRef' keyRef_ = remAnns keyRef' validateInjAppExists :: (Semigroup an) => Out.InjApp an -> [Problem an] validateInjAppExists (Out.InjApp _ funcId' _) = case lookupFunc funcId' of Nothing -> [UndefinedInjFunc (getAnn funcId') $ pprintStr funcId'] Just _ -> [] -- TODO Check params. Requires refactor move into `ValidatePropPaths` recordTypeFor :: (Semigroup an) => RecordCtx an -> FSymbol an -> Maybe (RecordType an) recordTypeFor (RecordCtx _ decls) head' = find (recordTypeMatches head') $ map recordDeclType decls recordTypeMatches :: (Semigroup an) => FSymbol an -> RecordType an -> Bool recordTypeMatches head' decl = head' =@= RecordType.head decl