@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Make chart function argument parsing modular/flexible with 900 pages of error messages

Summary:
Depends on D20444. Ref T13279. Instead of ad-hoc parsing and messages, formalize chart function arguments.

Also, add a whole lot of extra type checking.

Test Plan: Built and charted various functions with various valid and invalid argument lists, got sensible-seeming errors and results.

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: yelirekim

Maniphest Tasks: T13279

Differential Revision: https://secure.phabricator.com/D20445

+368 -76
+4
src/__phutil_library_map__.php
··· 2652 2652 'PhabricatorChartAxis' => 'applications/fact/chart/PhabricatorChartAxis.php', 2653 2653 'PhabricatorChartDataQuery' => 'applications/fact/chart/PhabricatorChartDataQuery.php', 2654 2654 'PhabricatorChartFunction' => 'applications/fact/chart/PhabricatorChartFunction.php', 2655 + 'PhabricatorChartFunctionArgument' => 'applications/fact/chart/PhabricatorChartFunctionArgument.php', 2656 + 'PhabricatorChartFunctionArgumentParser' => 'applications/fact/chart/PhabricatorChartFunctionArgumentParser.php', 2655 2657 'PhabricatorChatLogApplication' => 'applications/chatlog/application/PhabricatorChatLogApplication.php', 2656 2658 'PhabricatorChatLogChannel' => 'applications/chatlog/storage/PhabricatorChatLogChannel.php', 2657 2659 'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php', ··· 8629 8631 'PhabricatorChartAxis' => 'Phobject', 8630 8632 'PhabricatorChartDataQuery' => 'Phobject', 8631 8633 'PhabricatorChartFunction' => 'Phobject', 8634 + 'PhabricatorChartFunctionArgument' => 'Phobject', 8635 + 'PhabricatorChartFunctionArgumentParser' => 'Phobject', 8632 8636 'PhabricatorChatLogApplication' => 'PhabricatorApplication', 8633 8637 'PhabricatorChatLogChannel' => array( 8634 8638 'PhabricatorChatLogDAO',
+44 -2
src/applications/fact/chart/PhabricatorChartFunction.php
··· 7 7 private $yAxis; 8 8 private $limit; 9 9 10 + private $argumentParser; 11 + 10 12 final public function getFunctionKey() { 11 13 return $this->getPhobjectClassConstant('FUNCTIONKEY', 32); 12 14 } ··· 19 21 } 20 22 21 23 final public function setArguments(array $arguments) { 22 - $this->newArguments($arguments); 24 + $parser = $this->getArgumentParser(); 25 + $parser->setRawArguments($arguments); 26 + 27 + $specs = $this->newArguments(); 28 + 29 + if (!is_array($specs)) { 30 + throw new Exception( 31 + pht( 32 + 'Expected "newArguments()" in class "%s" to return a list of '. 33 + 'argument specifications, got %s.', 34 + get_class($this), 35 + phutil_describe_type($specs))); 36 + } 37 + 38 + assert_instances_of($specs, 'PhabricatorChartFunctionArgument'); 39 + 40 + foreach ($specs as $spec) { 41 + $parser->addArgument($spec); 42 + } 43 + 44 + $parser->setHaveAllArguments(true); 45 + $parser->parseArguments(); 46 + 23 47 return $this; 24 48 } 25 49 26 - abstract protected function newArguments(array $arguments); 50 + abstract protected function newArguments(); 51 + 52 + final protected function newArgument() { 53 + return new PhabricatorChartFunctionArgument(); 54 + } 55 + 56 + final protected function getArgument($key) { 57 + return $this->getArgumentParser()->getArgumentValue($key); 58 + } 59 + 60 + final protected function getArgumentParser() { 61 + if (!$this->argumentParser) { 62 + $parser = id(new PhabricatorChartFunctionArgumentParser()) 63 + ->setFunction($this); 64 + 65 + $this->argumentParser = $parser; 66 + } 67 + return $this->argumentParser; 68 + } 27 69 28 70 public function loadData() { 29 71 return;
+129
src/applications/fact/chart/PhabricatorChartFunctionArgument.php
··· 1 + <?php 2 + 3 + final class PhabricatorChartFunctionArgument 4 + extends Phobject { 5 + 6 + private $name; 7 + private $type; 8 + 9 + public function setName($name) { 10 + $this->name = $name; 11 + return $this; 12 + } 13 + 14 + public function getName() { 15 + return $this->name; 16 + } 17 + 18 + public function setType($type) { 19 + $types = array( 20 + 'fact-key' => true, 21 + 'function' => true, 22 + 'number' => true, 23 + ); 24 + 25 + if (!isset($types[$type])) { 26 + throw new Exception( 27 + pht( 28 + 'Chart function argument type "%s" is unknown. Valid types '. 29 + 'are: %s.', 30 + $type, 31 + implode(', ', array_keys($types)))); 32 + } 33 + 34 + $this->type = $type; 35 + return $this; 36 + } 37 + 38 + public function getType() { 39 + return $this->type; 40 + } 41 + 42 + public function newValue($value) { 43 + switch ($this->getType()) { 44 + case 'fact-key': 45 + if (!is_string($value)) { 46 + throw new Exception( 47 + pht( 48 + 'Value for "fact-key" argument must be a string, got %s.', 49 + phutil_describe_type($value))); 50 + } 51 + 52 + $facts = PhabricatorFact::getAllFacts(); 53 + $fact = idx($facts, $value); 54 + if (!$fact) { 55 + throw new Exception( 56 + pht( 57 + 'Fact key "%s" is not a known fact key.', 58 + $value)); 59 + } 60 + 61 + return $fact; 62 + case 'function': 63 + // If this is already a function object, just return it. 64 + if ($value instanceof PhabricatorChartFunction) { 65 + return $value; 66 + } 67 + 68 + if (!is_array($value)) { 69 + throw new Exception( 70 + pht( 71 + 'Value for "function" argument must be a function definition, '. 72 + 'formatted as a list, like: [fn, arg1, arg, ...]. Actual value '. 73 + 'is %s.', 74 + phutil_describe_type($value))); 75 + } 76 + 77 + if (!phutil_is_natural_list($value)) { 78 + throw new Exception( 79 + pht( 80 + 'Value for "function" argument must be a natural list, not '. 81 + 'a dictionary. Actual value is "%s".', 82 + phutil_describe_type($value))); 83 + } 84 + 85 + if (!$value) { 86 + throw new Exception( 87 + pht( 88 + 'Value for "function" argument must be a list with a function '. 89 + 'name; got an empty list.')); 90 + } 91 + 92 + $function_name = array_shift($value); 93 + 94 + if (!is_string($function_name)) { 95 + throw new Exception( 96 + pht( 97 + 'Value for "function" argument must be a natural list '. 98 + 'beginning with a function name as a string. The first list '. 99 + 'item has the wrong type, %s.', 100 + phutil_describe_type($function_name))); 101 + } 102 + 103 + $functions = PhabricatorChartFunction::getAllFunctions(); 104 + if (!isset($functions[$function_name])) { 105 + throw new Exception( 106 + pht( 107 + 'Function "%s" is unknown. Valid functions are: %s', 108 + $function_name, 109 + implode(', ', array_keys($functions)))); 110 + } 111 + 112 + return id(clone $functions[$function_name]) 113 + ->setArguments($value); 114 + case 'number': 115 + if (!is_float($value) && !is_int($value)) { 116 + throw new Exception( 117 + pht( 118 + 'Value for "number" argument must be an integer or double, '. 119 + 'got %s.', 120 + phutil_describe_type($value))); 121 + } 122 + 123 + return $value; 124 + } 125 + 126 + throw new PhutilMethodNotImplementedException(); 127 + } 128 + 129 + }
+154
src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php
··· 1 + <?php 2 + 3 + final class PhabricatorChartFunctionArgumentParser 4 + extends Phobject { 5 + 6 + private $function; 7 + private $rawArguments; 8 + private $unconsumedArguments; 9 + private $haveAllArguments = false; 10 + private $unparsedArguments; 11 + private $argumentMap = array(); 12 + private $argumentPosition = 0; 13 + private $argumentValues = array(); 14 + 15 + public function setFunction(PhabricatorChartFunction $function) { 16 + $this->function = $function; 17 + return $this; 18 + } 19 + 20 + public function getFunction() { 21 + return $this->function; 22 + } 23 + 24 + public function setRawArguments(array $arguments) { 25 + $this->rawArguments = $arguments; 26 + $this->unconsumedArguments = $arguments; 27 + } 28 + 29 + public function addArgument(PhabricatorChartFunctionArgument $spec) { 30 + $name = $spec->getName(); 31 + if (!strlen($name)) { 32 + throw new Exception( 33 + pht( 34 + 'Chart function "%s" emitted an argument specification with no '. 35 + 'argument name. Argument specifications must have unique names.', 36 + $this->getFunctionArgumentSignature())); 37 + } 38 + 39 + $type = $spec->getType(); 40 + if (!strlen($type)) { 41 + throw new Exception( 42 + pht( 43 + 'Chart function "%s" emitted an argument specification ("%s") with '. 44 + 'no type. Each argument specification must have a valid type.', 45 + $name)); 46 + } 47 + 48 + if (isset($this->argumentMap[$name])) { 49 + throw new Exception( 50 + pht( 51 + 'Chart function "%s" emitted multiple argument specifications '. 52 + 'with the same name ("%s"). Each argument specification must have '. 53 + 'a unique name.', 54 + $this->getFunctionArgumentSignature(), 55 + $name)); 56 + } 57 + 58 + $this->argumentMap[$name] = $spec; 59 + $this->unparsedArguments[] = $spec; 60 + 61 + return $this; 62 + } 63 + 64 + public function parseArgument( 65 + PhabricatorChartFunctionArgument $spec) { 66 + $this->addArgument($spec); 67 + return $this->parseArguments(); 68 + } 69 + 70 + public function setHaveAllArguments($have_all) { 71 + $this->haveAllArguments = $have_all; 72 + return $this; 73 + } 74 + 75 + public function parseArguments() { 76 + $have_count = count($this->rawArguments); 77 + $want_count = count($this->argumentMap); 78 + 79 + if ($this->haveAllArguments) { 80 + if ($want_count !== $have_count) { 81 + throw new Exception( 82 + pht( 83 + 'Function "%s" expects %s argument(s), but %s argument(s) were '. 84 + 'provided.', 85 + $this->getFunctionArgumentSignature(), 86 + $want_count, 87 + $have_count)); 88 + } 89 + } 90 + 91 + while ($this->unparsedArguments) { 92 + $argument = array_shift($this->unparsedArguments); 93 + $name = $argument->getName(); 94 + 95 + if (!$this->unconsumedArguments) { 96 + throw new Exception( 97 + pht( 98 + 'Function "%s" expects at least %s argument(s), but only %s '. 99 + 'argument(s) were provided.', 100 + $this->getFunctionArgumentSignature(), 101 + $want_count, 102 + $have_count)); 103 + } 104 + 105 + $raw_argument = array_shift($this->unconsumedArguments); 106 + $this->argumentPosition++; 107 + 108 + try { 109 + $value = $argument->newValue($raw_argument); 110 + } catch (Exception $ex) { 111 + throw new Exception( 112 + pht( 113 + 'Argument "%s" (in position "%s") to function "%s" is '. 114 + 'invalid: %s', 115 + $name, 116 + $this->argumentPosition, 117 + $this->getFunctionArgumentSignature(), 118 + $ex->getMessage())); 119 + } 120 + 121 + $this->argumentValues[$name] = $value; 122 + } 123 + } 124 + 125 + public function getArgumentValue($key) { 126 + if (!array_key_exists($key, $this->argumentValues)) { 127 + throw new Exception( 128 + pht( 129 + 'Function "%s" is requesting an argument ("%s") that it did '. 130 + 'not define.', 131 + $this->getFunctionArgumentSignature(), 132 + $key)); 133 + } 134 + 135 + return $this->argumentValues[$key]; 136 + } 137 + 138 + private function getFunctionArgumentSignature() { 139 + $argument_list = array(); 140 + foreach ($this->argumentMap as $key => $spec) { 141 + $argument_list[] = $key; 142 + } 143 + 144 + if (!$this->haveAllArguments) { 145 + $argument_list[] = '...'; 146 + } 147 + 148 + return sprintf( 149 + '%s(%s)', 150 + $this->getFunction()->getFunctionKey(), 151 + implode(', ', $argument_list)); 152 + } 153 + 154 + }
+9 -19
src/applications/fact/chart/PhabricatorConstantChartFunction.php
··· 7 7 8 8 private $value; 9 9 10 - protected function newArguments(array $arguments) { 11 - if (count($arguments) !== 1) { 12 - throw new Exception( 13 - pht( 14 - 'Chart function "constant(...)" expects one argument, got %s. '. 15 - 'Pass a constant.', 16 - count($arguments))); 17 - } 18 - 19 - if (!is_int($arguments[0])) { 20 - throw new Exception( 21 - pht( 22 - 'First argument for "fact(...)" is invalid: expected int, '. 23 - 'got %s.', 24 - phutil_describe_type($arguments[0]))); 25 - } 26 - 27 - $this->value = $arguments[0]; 10 + protected function newArguments() { 11 + return array( 12 + $this->newArgument() 13 + ->setName('n') 14 + ->setType('number'), 15 + ); 28 16 } 29 17 30 18 public function getDatapoints(PhabricatorChartDataQuery $query) { 31 19 $x_min = $query->getMinimumValue(); 32 20 $x_max = $query->getMaximumValue(); 33 21 22 + $value = $this->getArgument('n'); 23 + 34 24 $points = array(); 35 25 $steps = $this->newLinearSteps($x_min, $x_max, 2); 36 26 foreach ($steps as $step) { 37 27 $points[] = array( 38 28 'x' => $step, 39 - 'y' => $this->value, 29 + 'y' => $value, 40 30 ); 41 31 } 42 32
+9 -28
src/applications/fact/chart/PhabricatorFactChartFunction.php
··· 5 5 6 6 const FUNCTIONKEY = 'fact'; 7 7 8 - private $factKey; 9 8 private $fact; 10 9 private $datapoints; 11 10 12 - protected function newArguments(array $arguments) { 13 - if (count($arguments) !== 1) { 14 - throw new Exception( 15 - pht( 16 - 'Chart function "fact(...)" expects one argument, got %s. '. 17 - 'Pass the key for a fact.', 18 - count($arguments))); 19 - } 11 + protected function newArguments() { 12 + $key_argument = $this->newArgument() 13 + ->setName('fact-key') 14 + ->setType('fact-key'); 20 15 21 - if (!is_string($arguments[0])) { 22 - throw new Exception( 23 - pht( 24 - 'First argument for "fact(...)" is invalid: expected string, '. 25 - 'got %s.', 26 - phutil_describe_type($arguments[0]))); 27 - } 16 + $parser = $this->getArgumentParser(); 17 + $parser->parseArgument($key_argument); 28 18 29 - $facts = PhabricatorFact::getAllFacts(); 30 - $fact = idx($facts, $arguments[0]); 31 - 32 - if (!$fact) { 33 - throw new Exception( 34 - pht( 35 - 'Argument to "fact(...)" is invalid: "%s" is not a known fact '. 36 - 'key.', 37 - $arguments[0])); 38 - } 19 + $fact = $this->getArgument('fact-key'); 20 + $this->fact = $fact; 39 21 40 - $this->factKey = $arguments[0]; 41 - $this->fact = $fact; 22 + return $fact->getFunctionArguments(); 42 23 } 43 24 44 25 public function loadData() {
+10 -20
src/applications/fact/chart/PhabricatorSinChartFunction.php
··· 5 5 6 6 const FUNCTIONKEY = 'sin'; 7 7 8 - private $argument; 9 - 10 - protected function newArguments(array $arguments) { 11 - if (count($arguments) !== 1) { 12 - throw new Exception( 13 - pht( 14 - 'Chart function "sin(..)" expects one argument, got %s.', 15 - count($arguments))); 16 - } 17 - 18 - $argument = $arguments[0]; 19 - 20 - if (!($argument instanceof PhabricatorChartFunction)) { 21 - throw new Exception( 22 - pht( 23 - 'Argument to chart function should be a function, got %s.', 24 - phutil_describe_type($argument))); 25 - } 8 + protected function newArguments() { 9 + return array( 10 + $this->newArgument() 11 + ->setName('x') 12 + ->setType('function'), 13 + ); 14 + } 26 15 27 - $this->argument = $argument; 16 + protected function assignArguments(array $arguments) { 17 + $this->argument = $arguments[0]; 28 18 } 29 19 30 20 public function getDatapoints(PhabricatorChartDataQuery $query) { 31 - $points = $this->argument->getDatapoints($query); 21 + $points = $this->getArgument('x')->getDatapoints($query); 32 22 33 23 foreach ($points as $key => $point) { 34 24 $points[$key]['y'] = sin(deg2rad($points[$key]['y']));
+2 -7
src/applications/fact/chart/PhabricatorXChartFunction.php
··· 5 5 6 6 const FUNCTIONKEY = 'x'; 7 7 8 - protected function newArguments(array $arguments) { 9 - if (count($arguments) !== 0) { 10 - throw new Exception( 11 - pht( 12 - 'Chart function "x()" expects zero arguments, got %s.', 13 - count($arguments))); 14 - } 8 + protected function newArguments() { 9 + return array(); 15 10 } 16 11 17 12 public function getDatapoints(PhabricatorChartDataQuery $query) {
+3
src/applications/fact/controller/PhabricatorFactChartController.php
··· 23 23 $x_function = id(new PhabricatorXChartFunction()) 24 24 ->setArguments(array()); 25 25 26 + $functions[] = id(new PhabricatorConstantChartFunction()) 27 + ->setArguments(array(360)); 28 + 26 29 $functions[] = id(new PhabricatorSinChartFunction()) 27 30 ->setArguments(array($x_function)); 28 31
+4
src/applications/fact/fact/PhabricatorFact.php
··· 37 37 38 38 abstract protected function newTemplateDatapoint(); 39 39 40 + final public function getFunctionArguments() { 41 + return array(); 42 + } 43 + 40 44 }