Fixed trailing literals not being required

this was a problem with cmdalias, where just writing cmdalias alone would invoke cmdalias list.
In itself this wasn't particularly harmful, but it could've been a problem for other commands like timings.
This commit is contained in:
Dylan K. Taylor
2025-10-12 14:36:02 +01:00
parent 50976c20aa
commit a314d2dbb1

View File

@@ -33,9 +33,7 @@ use pocketmine\command\utils\InvalidCommandSyntaxException;
use pocketmine\lang\Translatable;
use pocketmine\permission\PermissionManager;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Utils;
use function array_filter;
use function array_map;
use function array_key_last;
use function count;
use function get_class;
use function implode;
@@ -43,8 +41,6 @@ use function is_string;
use function preg_match;
use function strlen;
use function strpos;
use function substr;
use function var_dump;
final class CommandOverload{
@@ -69,6 +65,11 @@ final class CommandOverload{
private \Closure $handler,
private bool $acceptsAliasUsed = false
){
foreach($this->parameters as $k => $parameter){
if(!is_string($parameter) && $parameter->consumesAllRemainingInputs() && $k !== array_key_last($this->parameters)){
throw new \InvalidArgumentException($parameter::class . " can only be used as the final argument, because it consumes all remaining inputs");
}
}
$permissions = is_string($permission) ? [$permission] : $permission;
if(count($permissions) === 0){
throw new \InvalidArgumentException("At least one permission must be provided");
@@ -83,24 +84,39 @@ final class CommandOverload{
//TODO: auto infer parameter infos if they aren't provided?
//TODO: allow the type of CommandSender to be constrained - this can be useful for player-only commands etc
$alwaysPresentArgs = self::alwaysPresentArgs($this->acceptsAliasUsed);
$nonInputParameters = self::alwaysPresentArgs($this->acceptsAliasUsed);
$literalCount = 0;
$inputParameters = [];
foreach($this->parameters as $parameter){
if(is_string($parameter)){
$literalCount++;
}else{
$inputParameters[] = new ParameterInfo(
$parameter->getCodeName(),
$parameter->getCodeType(),
byReference: false,
isOptional: false,
isVariadic: false,
);
}
}
$passableParameters = array_filter($this->parameters, is_object(...));
$expectedPrototype = new Prototype(
new ReturnInfo(new NamedType(BuiltInType::VOID), byReference: false),
...$alwaysPresentArgs,
...array_map(fn(Parameter $p) => new ParameterInfo(
$p->getCodeName(),
$p->getCodeType(),
byReference: false,
isOptional: false,
isVariadic: false,
), $passableParameters)
...$nonInputParameters,
...$inputParameters
);
$actualPrototype = Prototype::fromClosure($this->handler);
Utils::validateCallableSignature($expectedPrototype, $actualPrototype);
if(!$expectedPrototype->isSatisfiedBy($actualPrototype)){
//validateCallableSignature() not used because we want a custom error message
throw new \InvalidArgumentException("Expected handler signature $expectedPrototype from provided parameter info, but handler has signature $actualPrototype");
}
$this->requiredInputCount = $actualPrototype->getRequiredParameterCount() - count($alwaysPresentArgs);
//optionals are inferred from the prototype of the callable, not the parameter infos themselves
//contravariance allows them to be optional even if they're required in the prototype
//literals must always be provided
$this->requiredInputCount = $actualPrototype->getRequiredParameterCount() + $literalCount - count($nonInputParameters);
}
/**
@@ -171,14 +187,17 @@ final class CommandOverload{
public function invoke(CommandSender $sender, string $aliasUsed, string $commandLine) : void{
$offset = 0;
$args = [];
//skip preceding whitespace
$literals = 0;
//skip preceding whitespace
self::skipWhitespace($commandLine, $offset);
if($offset < strlen($commandLine)){
foreach($this->parameters as $parameter){
if(is_string($parameter)){
if(strpos($commandLine, $parameter, $offset) === $offset){
$offset += strlen($parameter);
$literals++;
}else{
throw new ParameterParseException("Literal \"$parameter\" expected");
}
@@ -200,7 +219,6 @@ final class CommandOverload{
if(is_string($parameter)){
throw new AssumptionFailedError();
}
var_dump(substr($commandLine, $offset));
throw new ParameterParseException("Parameter " . get_class($parameter) . " for \$" . $parameter->getCodeName() . " didn't stop on a whitespace character");
}
}
@@ -209,7 +227,7 @@ final class CommandOverload{
if($offset !== strlen($commandLine)){
throw new InvalidCommandSyntaxException("Too many arguments provided for overload");
}
if(count($args) < $this->requiredInputCount){
if(count($args) + $literals < $this->requiredInputCount){
throw new InvalidCommandSyntaxException("Not enough arguments provided for overload");
}
//Reflection magic here :)