Skip to content

Commit 88cf6b3

Browse files
committed
Make UCM command arg checking stricter
- distinguish between “parameters” and “arguments” – a command has a fixed number of parameters, each of which maps to some number of arguments (from 0 to many, depending on the parameter) - change the type of parameters to eliminate invalid parameter structures - require `InputPattern` parameters to cover all arguments (there are a number of commands whose parameters were under-specified).
1 parent 5060887 commit 88cf6b3

File tree

7 files changed

+543
-460
lines changed

7 files changed

+543
-460
lines changed

unison-cli/src/Unison/Codebase/Editor/HandleInput.hs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -714,18 +714,31 @@ loop e = do
714714
DebugFuzzyOptionsI command args -> do
715715
Cli.Env {codebase} <- ask
716716
currentBranch <- Branch.withoutTransitiveLibs <$> Cli.getCurrentBranch0
717-
case Map.lookup command InputPatterns.patternMap of
718-
Just (IP.InputPattern {args = argTypes}) -> do
719-
zip argTypes args & Monoid.foldMapM \case
720-
((argName, _, IP.ArgumentType {fzfResolver = Just IP.FZFResolver {getOptions}}), "_") -> do
721-
pp <- Cli.getCurrentProjectPath
722-
results <- liftIO $ getOptions codebase pp currentBranch
723-
Cli.respond (DebugDisplayFuzzyOptions argName (Text.unpack <$> results))
724-
((_, _, IP.ArgumentType {fzfResolver = Nothing}), "_") -> do
725-
Cli.respond DebugFuzzyOptionsNoResolver
726-
_ -> pure ()
727-
Nothing -> do
728-
Cli.respond DebugFuzzyOptionsNoResolver
717+
maybe
718+
(Cli.respond $ DebugFuzzyOptionsNoCommand command)
719+
( \IP.InputPattern {params} ->
720+
either (Cli.respond . DebugFuzzyOptionsIncorrectArgs) snd $
721+
IP.foldArgs
722+
( \(paramName, IP.ParameterType {fzfResolver}) arg ->
723+
( *>
724+
if arg == "_"
725+
then
726+
maybe
727+
(Cli.respond DebugFuzzyOptionsNoResolver)
728+
( \IP.FZFResolver {getOptions} -> do
729+
pp <- Cli.getCurrentProjectPath
730+
results <- liftIO $ getOptions codebase pp currentBranch
731+
Cli.respond (DebugDisplayFuzzyOptions paramName (Text.unpack <$> results))
732+
)
733+
fzfResolver
734+
else pure ()
735+
)
736+
)
737+
(pure ())
738+
params
739+
args
740+
)
741+
$ Map.lookup command InputPatterns.patternMap
729742
DebugFormatI -> do
730743
env <- ask
731744
void $ runMaybeT do

unison-cli/src/Unison/Codebase/Editor/Output.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,8 @@ data Output
354354
| DisplayDebugCompletions [Completion.Completion]
355355
| DisplayDebugLSPNameCompletions [(Text, Name, LabeledDependency)]
356356
| DebugDisplayFuzzyOptions Text [String {- arg description, options -}]
357+
| DebugFuzzyOptionsIncorrectArgs (NonEmpty String)
358+
| DebugFuzzyOptionsNoCommand String
357359
| DebugFuzzyOptionsNoResolver
358360
| DebugTerm (Bool {- verbose mode -}) (Either (Text {- A builtin hash -}) (Term Symbol Ann))
359361
| DebugDecl (Either (Text {- A builtin hash -}) (DD.Decl Symbol Ann)) (Maybe ConstructorId {- If 'Just' we're debugging a constructor of the given decl -})
@@ -611,6 +613,8 @@ isFailure o = case o of
611613
DisplayDebugCompletions {} -> False
612614
DisplayDebugLSPNameCompletions {} -> False
613615
DebugDisplayFuzzyOptions {} -> False
616+
DebugFuzzyOptionsIncorrectArgs {} -> True
617+
DebugFuzzyOptionsNoCommand {} -> True
614618
DebugFuzzyOptionsNoResolver {} -> True
615619
DebugTerm {} -> False
616620
DebugDecl {} -> False

unison-cli/src/Unison/CommandLine.hs

Lines changed: 77 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ import Control.Lens hiding (aside)
1515
import Control.Monad.Except
1616
import Control.Monad.Trans.Except
1717
import Data.List (isPrefixOf, isSuffixOf)
18+
import Data.List.NonEmpty (NonEmpty)
1819
import Data.Map qualified as Map
19-
import Data.Semialign qualified as Align
2020
import Data.Text qualified as Text
2121
import Data.Text.IO qualified as Text
22-
import Data.These (These (..))
2322
import Data.Vector qualified as Vector
2423
import System.FilePath (takeFileName)
24+
import Text.Numeral (defaultInflection)
25+
import Text.Numeral.Language.ENG qualified as Numeral
2526
import Text.Regex.TDFA ((=~))
2627
import Unison.Codebase (Codebase)
2728
import Unison.Codebase.Branch (Branch0)
@@ -83,35 +84,45 @@ parseInput codebase projPath currentProjectRoot numberedArgs patterns segments =
8384
case segments of
8485
[] -> throwE ""
8586
command : args -> case Map.lookup command patterns of
86-
Just pat@(InputPattern {parse, help}) -> do
87+
Just pat@(InputPattern {params, help, parse}) -> do
8788
let expandedNumbers :: InputPattern.Arguments
8889
expandedNumbers =
8990
foldMap (\arg -> maybe [Left arg] (fmap pure) $ expandNumber numberedArgs arg) args
90-
lift (fzfResolve codebase projPath getCurrentBranch0 pat expandedNumbers) >>= \case
91-
Left (NoFZFResolverForArgumentType _argDesc) -> throwError help
92-
Left (NoFZFOptions argDesc) -> throwError (noCompletionsMessage argDesc)
93-
Left FZFCancelled -> pure Nothing
94-
Right resolvedArgs -> do
95-
parsedInput <-
96-
except
97-
. first
98-
( \msg ->
99-
P.warnCallout $
100-
P.wrap "Sorry, I wasn’t sure how to process your request:"
101-
<> P.newline
102-
<> P.newline
103-
<> P.indentN 2 msg
104-
<> P.newline
105-
<> P.newline
106-
<> P.wrap
107-
( "You can run"
108-
<> IPs.makeExample IPs.help [fromString command]
109-
<> "for more information on using"
110-
<> IPs.makeExampleEOS pat []
111-
)
112-
)
113-
$ parse resolvedArgs
114-
pure $ Just (Left command : resolvedArgs, parsedInput)
91+
lift (fzfResolve codebase projPath getCurrentBranch0 params expandedNumbers)
92+
>>= either
93+
( \case
94+
NoFZFResolverForArgumentType _argDesc -> throwError help
95+
NoFZFOptions argDesc -> throwError (noCompletionsMessage argDesc)
96+
FZFCancelled -> pure Nothing
97+
FZFOversaturated extraArgs -> do
98+
let showNum n = fromMaybe (tShow n) $ Numeral.us_cardinal defaultInflection n
99+
maxCount <- maybe (throwError . P.text $ "Internal error: fuzzy finder complained that there are " <> showNum (length extraArgs) <> " too many arguments provided, but the command apparently allows an unbounded number of arguments.") pure $ InputPattern.maxArgs params
100+
let foundCount = showNum $ maxCount + length extraArgs
101+
throwError . P.text $
102+
"I expected no more than " <> showNum maxCount <> " arguments, but received " <> foundCount <> "."
103+
)
104+
( \resolvedArgs -> do
105+
parsedInput <-
106+
except
107+
. first
108+
( \msg ->
109+
P.warnCallout $
110+
P.wrap "Sorry, I wasn’t sure how to process your request:"
111+
<> P.newline
112+
<> P.newline
113+
<> P.indentN 2 msg
114+
<> P.newline
115+
<> P.newline
116+
<> P.wrap
117+
( "You can run"
118+
<> IPs.makeExample IPs.help [fromString command]
119+
<> "for more information on using"
120+
<> IPs.makeExampleEOS pat []
121+
)
122+
)
123+
$ parse resolvedArgs
124+
pure $ Just (Left command : resolvedArgs, parsedInput)
125+
)
115126
Nothing ->
116127
throwE
117128
. warn
@@ -151,50 +162,62 @@ expandNumber numberedArgs s =
151162
_ -> Nothing
152163

153164
data FZFResolveFailure
154-
= NoFZFResolverForArgumentType InputPattern.ArgumentDescription
155-
| NoFZFOptions Text {- argument description -}
165+
= NoFZFResolverForArgumentType InputPattern.ParameterDescription
166+
| NoFZFOptions
167+
-- | argument description
168+
Text
156169
| FZFCancelled
170+
| -- | More arguments were provided than the command supports.
171+
FZFOversaturated
172+
-- | The arguments that couldn’t be assigned to a parameter.
173+
(NonEmpty InputPattern.Argument)
157174

158-
fzfResolve :: Codebase IO Symbol Ann -> PP.ProjectPath -> (IO (Branch0 IO)) -> InputPattern -> InputPattern.Arguments -> IO (Either FZFResolveFailure InputPattern.Arguments)
159-
fzfResolve codebase ppCtx getCurrentBranch pat args = runExceptT do
175+
fzfResolve ::
176+
Codebase IO Symbol Ann ->
177+
PP.ProjectPath ->
178+
(IO (Branch0 IO)) ->
179+
InputPattern.Parameters ->
180+
InputPattern.Arguments ->
181+
IO (Either FZFResolveFailure InputPattern.Arguments)
182+
fzfResolve codebase ppCtx getCurrentBranch params args = runExceptT do
160183
-- We resolve args in two steps, first we check that all arguments that will require a fzf
161184
-- resolver have one, and only if so do we prompt the user to actually do a fuzzy search.
162185
-- Otherwise, we might ask the user to perform a search only to realize we don't have a resolver
163186
-- for a later arg.
164-
argumentResolvers :: [ExceptT FZFResolveFailure IO InputPattern.Arguments] <-
165-
(Align.align (InputPattern.args pat) args)
166-
& traverse \case
167-
This (argName, opt, InputPattern.ArgumentType {fzfResolver})
168-
| opt == InputPattern.Required || opt == InputPattern.OnePlus ->
169-
case fzfResolver of
170-
Nothing -> throwError $ NoFZFResolverForArgumentType argName
171-
Just fzfResolver -> pure $ fuzzyFillArg opt argName fzfResolver
172-
| otherwise -> pure $ pure []
173-
That arg -> pure $ pure [arg]
174-
These _ arg -> pure $ pure [arg]
187+
let argumentResolvers :: [ExceptT FZFResolveFailure IO InputPattern.Arguments] =
188+
either
189+
(pure . throwError . FZFOversaturated)
190+
( \(InputPattern.Parameters {requiredParams, trailingParams}, args) ->
191+
args
192+
<> map (meh False) requiredParams
193+
<> case trailingParams of
194+
InputPattern.Optional _ _ -> mempty
195+
InputPattern.OnePlus p -> pure $ meh True p
196+
)
197+
$ InputPattern.foldArgs (\(_, _) arg acc -> pure [arg] : acc) mempty params args
175198
argumentResolvers & foldMapM id
176199
where
177-
fuzzyFillArg :: InputPattern.IsOptional -> Text -> InputPattern.FZFResolver -> ExceptT FZFResolveFailure IO InputPattern.Arguments
178-
fuzzyFillArg opt argDesc InputPattern.FZFResolver {getOptions} = do
200+
meh :: Bool -> InputPattern.Parameter -> ExceptT FZFResolveFailure IO InputPattern.Arguments
201+
meh allowMulti (argName, InputPattern.ParameterType {fzfResolver}) =
202+
maybe
203+
(throwError $ NoFZFResolverForArgumentType argName)
204+
(fuzzyFillArg allowMulti argName)
205+
fzfResolver
206+
207+
fuzzyFillArg :: Bool -> Text -> InputPattern.FZFResolver -> ExceptT FZFResolveFailure IO InputPattern.Arguments
208+
fuzzyFillArg allowMulti argDesc InputPattern.FZFResolver {getOptions} = do
179209
currentBranch <- Branch.withoutTransitiveLibs <$> liftIO getCurrentBranch
180210
options <- liftIO $ getOptions codebase ppCtx currentBranch
181-
when (null options) $ throwError $ NoFZFOptions argDesc
211+
when (null options) . throwError $ NoFZFOptions argDesc
182212
liftIO $ Text.putStrLn (FZFResolvers.fuzzySelectHeader argDesc)
183213
results <-
184-
liftIO (Fuzzy.fuzzySelect Fuzzy.defaultOptions {Fuzzy.allowMultiSelect = multiSelectForOptional opt} id options)
214+
liftIO (Fuzzy.fuzzySelect Fuzzy.defaultOptions {Fuzzy.allowMultiSelect = allowMulti} id options)
185215
`whenNothingM` throwError FZFCancelled
186216
-- If the user triggered the fuzzy finder, but selected nothing, abort the command rather than continuing execution
187217
-- with no arguments.
188218
if null results
189219
then throwError FZFCancelled
190220
else pure (Left . Text.unpack <$> results)
191221

192-
multiSelectForOptional :: InputPattern.IsOptional -> Bool
193-
multiSelectForOptional = \case
194-
InputPattern.Required -> False
195-
InputPattern.Optional -> False
196-
InputPattern.OnePlus -> True
197-
InputPattern.ZeroPlus -> True
198-
199222
prompt :: String
200223
prompt = "> "

unison-cli/src/Unison/CommandLine/Completion.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ haskelineTabComplete patterns codebase authedHTTPClient ppCtx = Line.completeWor
8383
case words $ reverse prev of
8484
h : t -> fromMaybe (pure []) $ do
8585
p <- Map.lookup h patterns
86-
argType <- IP.argType p (length t)
87-
pure $ IP.suggestions argType word codebase authedHTTPClient ppCtx
86+
paramType <- IP.paramType (IP.params p) (length t)
87+
pure $ IP.suggestions paramType word codebase authedHTTPClient ppCtx
8888
_ -> pure []
8989

9090
-- | Things which we may want to complete for.

0 commit comments

Comments
 (0)