#!/usr/bin/php -qC
<?php
#
# type: cli
# description: Transform "type hints" into function parameter type assert() statements.
# version: 0.1.0
#
# Duplicates parameter value-type hints into static lists of assertions atop
# each function body. Accepts in-comment "type hints". Can also remove them
# again. It recognizes scalar-ish types, arrays and resources:
#
# function xyz (/*int*/ $a, /*string*/ $s, /*bool*/ $b)
# {
# assert('is_int($a) /*typehint*/');
# assert('is_string($s) || $s instanceof SplString /*typehint*/');
# assert('is_bool($b) /*typehint*/');
#
# Even allows a few basic expressions or comparisons within assertions:
#
# function cmp (/*int,>0*/ $i, /*str,strlen($)<32*/, $s)
#
# Such type assertions can be turned off at runtime, be configured to generate
# E_WARNINGs instead, or permit custom handlers. Can be selectively displaced
# once PHP7 "type hints" become available (implicit type casts or runtime errors
# preclude any traceable assertions of course).
#
# To add assertions:
# php-assert-hints files/*.php
#
# Remove them:
# php-assert-hints --rm files/*.php
#
// Loop over input files
list($files, $opts) = argv();
$files or exit("Usage:\n php-assert-hints *.php\n");
array_map(
[new FuncDeclRewriteMap($opts), "on"],
$files
);
/**
* Transforms input files
*
*/
class FuncDeclRewriteMap {
// Parameter types to assert conditions, some aliases
public $types = [
"int" => "is_int($)",
"integer" => "is_integer($)",
"long" => "is_long($)",
"bool" => "is_boolean($)",
"boolean" => "is_boolean($)",
"str" => "is_string($) || $ instanceof SplString",
"string" => "is_string($) || $ instanceof SplString",
"float" => "is_float($)",
"real" => "is_real($)",
"double" => "is_double($)",
"numeric" => "is_numeric($)",
"array" => "is_array($) || $ instanceof Traversable || $ instanceof ArrayObject",
"scalar" => "is_scalar($)",
"resource" => "is_resource($) || $ instanceof PDO",
"callable" => "is_callable($)",
"isset" => "isset($)", # only makes sense for references
"=null" => " || is_null($)", # added for =NULL defaults
];
public $only_rm = false;
// Retain --rm cmdline flag
public function __construct (/*array*/ $opts) {
$this->only_rm = preg_grep("/^-+(rm?|de?l?)$/i", $opts);
}
// Update file in-place
public function on (/*string*/ $fn) {
$src = file_get_contents($fn);
if ($update = $this->rewrite($src)) {
// check that only lines containing assert() have been added/removed/changed
$diff = array_diff(preg_split("/\R/", $src), preg_split("/\R/", $src));
if (preg_grep("/^\s*assert\(/mi", $diff, PREG_GREP_INVERT)) {
fwrite(STDERR, "Invalid rewrite (changed too much) for '$fn'\n");
}
// write back
else {
file_put_contents($fn, $update);
}
}
elseif (preg_last_error()) {
fwrite(STDERR, "Probable regex/UTF8 error with '$fn'\n");
}
}
// Update source
public function rewrite(/*string*/ $src) {
// remove prior assertions
$src = preg_replace(self::RX_ASSERT, "", $src);
if ($this->only_rm) {
return $src;
}
// find function bodies, and inject new assert() lists
$src = preg_replace_callback(self::RX_FUNC, [$this, "rx_func"], $src);
return $src;
}
// Regexps
const RX_ASSERT = "/
^\h* assert\(\' [^{'}]+ \/\*\h*type\h*hint\h*\*\/ \'\);\h*\R
/mix";
const RX_FUNC = "/
(?<indent> \h*)
(?:(?:public|private|protected|static)\s+)*
function
\s*
(?<name> [\w\pL]+)
\s*
\( (?<params> [^{}]* ) \)
(?<rettype> \s*:\s* \w+)?
\s*
\{ (\h*\R)?
/mix";
const RX_PARAM = "/
(?:
(?: \/\*\s*)?
(?<type>\w+)
(?<expr>(?:[,\s]*[\w()$]*[<>!=]\s*[-+\d.]+[\w()]*)*)
(?: \s*\*\/\s*|\s+)
)?
(\s* \&)?
\s* (?<name>[$][\w\pL]+)
(?<def> \s*=\s* NULL)?
/mix";
// Append to function declaration headers
public function rx_func (/*array*/ $match) {
// whole function declaration header
$decl = $match[0];
$ws = preg_replace("/\S/", " ", $match["indent"]) . " ";
// list parameters
if (preg_match_all(self::RX_PARAM, $match["params"], $params, PREG_SET_ORDER)) {
foreach ($params as $p) {
// recognized type prefix?
if ($type = strtolower($p["type"]) and isset($this->types[$type])) {
// copy type assertion
$expr = $this->types[$type];
// add expression checks
if (!empty($p["expr"])) {
$expr = join(" && ", array_merge(
["($expr)"],
array_map(
function($add_x) {
return is_int(strpos($add_x, "$"))
? "($add_x)"
: "(\$ {$add_x})";
},
array_filter(preg_split("/\s*,\s*/", $p["expr"]))
)
));
}
// add null alternative
if (isset($p["def"]) && stristr($p["def"], "NULL")) {
$expr .= $this->types["=null"];
}
// substitute param name and just append to function declaration header
$expr = preg_replace("/[\$](?!\w)/", $p["name"], $expr);
$decl .= "{$ws}assert('$expr /*typehint*/');\n";
}
}
}
return $decl;
}
}
// Split files from -options
function argv(/*int*/ $x=NULL) {
$argv = array_slice($_SERVER["argv"], 1);
$opts = preg_grep("/^-+(rm?|de?l?)/i", $argv);
return [array_diff($argv, $opts), $opts];
}
#include <ispect.ph>