-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathclass.legacy_surface.php
More file actions
132 lines (123 loc) · 4.85 KB
/
Copy pathclass.legacy_surface.php
File metadata and controls
132 lines (123 loc) · 4.85 KB
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
<?php declare(strict_types=1);
/**
* LEGACY_SURFACE
* Static analyzer that recovers the top-level define() constant surface of a set of
* PHP files WITHOUT executing them. The v6 config files — and even the
* sample.config*.php templates — cannot be include()d in isolation: they pull in the
* logger, the autoloader and core_functions, start a session and call setlocale
* (spec §5.10). Tokenizing with token_get_all is the only safe way to read their
* constant surface, and it is the same technique the Phase-4 migration tool
* (install/migrate_config_v7.php) will use.
*
* First consumer: the boot-diff gate (test/server/unit/boot_diff_gate_Test.php).
*
* The tokenizer primitives (is_define_call / collect_value / unquote / next_meaningful_index /
* prev_meaningful) are shared with migration_extractor via the define_scanner trait.
*/
require_once __DIR__ . '/trait.define_scanner.php';
final class legacy_surface {
use define_scanner;
/**
* EXTRACT
* @param string[] $files absolute paths to PHP files to scan
* @return array<string,array{kind:string,value:mixed,file:string}>
* name => ['kind'=>'literal'|'runtime', 'value'=>mixed|null, 'file'=>string].
* A name's FIRST definition wins (matches PHP's first-define()-wins runtime).
*/
public static function extract(array $files) : array {
$out = [];
foreach ($files as $file) {
$src = file_get_contents($file);
if ($src === false) {
throw new \RuntimeException("legacy_surface: cannot read {$file}");
}
foreach (self::scan($src, $file) as $name => $info) {
if (!array_key_exists($name, $out)) {
$out[$name] = $info; // first definition wins
}
}
}
return $out;
}//end extract
/**
* SCAN one file's source for top-level define('NAME', VALUE) calls.
* @return array<string,array{kind:string,value:mixed,file:string}>
*/
private static function scan(string $src, string $file) : array {
$tokens = token_get_all($src);
$n = count($tokens);
$found = [];
for ($i = 0; $i < $n; $i++) {
$t = $tokens[$i];
if (!is_array($t) || !self::is_define_call($t)) {
continue;
}
// reject method/static calls and function declarations: $o->define(), C::define(), function define()
$prev = self::prev_meaningful($tokens, $i);
if (is_array($prev) && in_array($prev[0], [
T_OBJECT_OPERATOR, T_DOUBLE_COLON, T_NULLSAFE_OBJECT_OPERATOR, T_FUNCTION,
], true)) {
continue;
}
$open = self::next_meaningful_index($tokens, $i);
if ($open === null || $tokens[$open] !== '(') {
continue;
}
$name_idx = self::next_meaningful_index($tokens, $open);
if ($name_idx === null
|| !is_array($tokens[$name_idx])
|| $tokens[$name_idx][0] !== T_CONSTANT_ENCAPSED_STRING) {
continue; // dynamic define name — not part of the static surface
}
$name = self::unquote($tokens[$name_idx][1]);
$comma = self::next_meaningful_index($tokens, $name_idx);
if ($comma === null || $tokens[$comma] !== ',') {
continue;
}
[$value_tokens, $end] = self::collect_value($tokens, $comma + 1);
$found[$name] = self::classify($value_tokens) + ['file' => $file];
$i = $end; // resume after this define()'s closing paren
}
return $found;
}//end scan
/**
* CLASSIFY — a single scalar literal (with an optional leading +/- on a number)
* is a 'literal' with its parsed value; anything else is 'runtime'.
* @return array{kind:string,value:mixed}
*/
private static function classify(array $value_tokens) : array {
$meaningful = array_values(array_filter($value_tokens, static function ($t) : bool {
if (is_array($t)) {
return !in_array($t[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true);
}
return true;
}));
// optional single leading +/- before a numeric literal
if (count($meaningful) === 2
&& !is_array($meaningful[0])
&& ($meaningful[0] === '-' || $meaningful[0] === '+')
&& is_array($meaningful[1])
&& in_array($meaningful[1][0], [T_LNUMBER, T_DNUMBER], true)) {
$num = $meaningful[1][0] === T_LNUMBER ? (int) $meaningful[1][1] : (float) $meaningful[1][1];
return ['kind' => 'literal', 'value' => $meaningful[0] === '-' ? -$num : $num];
}
if (count($meaningful) === 1 && is_array($meaningful[0])) {
$tok = $meaningful[0];
switch ($tok[0]) {
case T_CONSTANT_ENCAPSED_STRING:
return ['kind' => 'literal', 'value' => self::unquote($tok[1])];
case T_LNUMBER:
return ['kind' => 'literal', 'value' => (int) $tok[1]];
case T_DNUMBER:
return ['kind' => 'literal', 'value' => (float) $tok[1]];
case T_STRING:
$low = strtolower($tok[1]);
if ($low === 'true') { return ['kind' => 'literal', 'value' => true]; }
if ($low === 'false') { return ['kind' => 'literal', 'value' => false]; }
if ($low === 'null') { return ['kind' => 'literal', 'value' => null]; }
break; // bare constant ref → runtime
}
}
return ['kind' => 'runtime', 'value' => null];
}//end classify
}