1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
| #!/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>
|