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
|
#!/usr/bin/env php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Shirabe\PluginClassifier\VendorPackages;
if ($argc < 2) {
fwrite(STDERR, <<<USAGE
usage: query <target>...
<target> is any of:
a PHP source file composer/src/Composer/Util/Git.php
a Rust source file crates/shirabe/src/util/git.rs
a class name Composer\\Util\\Git, Git, git
Prints the plugin-boundary classification from report.json,
generating the report first if it does not exist.
USAGE);
exit(2);
}
$reportPath = __DIR__ . '/report.json';
if (!is_file($reportPath)) {
fwrite(STDERR, "report.json not found; running classify...\n");
exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(__DIR__ . '/classify') . ' > /dev/null', $_, $status);
if (!is_file($reportPath)) {
fwrite(STDERR, "classify failed to produce report.json (exit $status)\n");
exit(1);
}
if ($status !== 0) {
fwrite(STDERR, "note: classify exited with $status (violations present); report was still written\n");
}
}
$report = json_decode(file_get_contents($reportPath), true);
if (!is_array($report)) {
fwrite(STDERR, "cannot parse $reportPath\n");
exit(1);
}
/** @var array<string, array<string, mixed>> */
$classes = [];
foreach ($report['classes'] as $row) {
$classes[$row['fqcn']] = $row;
}
/** @var array<string, array<string, mixed>> */
$vendorTypes = [];
foreach ($report['vendorTypes'] as $row) {
$vendorTypes[$row['fqcn']] = $row;
}
/** Path-segment form used to match Rust paths: no underscores, lowercase. */
function normalizeSegment(string $segment): string
{
return strtolower(str_replace('_', '', $segment));
}
/** @return list<string> matched FQCNs */
function resolveTarget(string $target, array $classes): array
{
// Rust source file: match normalized path segments against FQCNs,
// because the snake_case mapping is not textually reversible
// (io_interface.rs -> IOInterface).
if (str_ends_with($target, '.rs')) {
$path = preg_replace('#^.*?crates/shirabe/src/#', '', str_replace('\\', '/', $target));
$segments = explode('/', substr($path, 0, -strlen('.rs')));
$wanted = array_map(normalizeSegment(...), $segments);
$matches = [];
foreach ($classes as $fqcn => $_) {
$fqcnSegments = explode('\\', $fqcn);
array_shift($fqcnSegments); // Composer\
if (array_map(normalizeSegment(...), $fqcnSegments) === $wanted) {
$matches[] = $fqcn;
}
}
return $matches;
}
// PHP source file: the path under src/Composer/ is the FQCN.
if (str_ends_with($target, '.php')) {
$path = str_replace('\\', '/', $target);
$pos = strpos($path, 'src/Composer/');
if ($pos !== false) {
$fqcn = 'Composer\\' . str_replace('/', '\\', substr($path, $pos + strlen('src/Composer/'), -strlen('.php')));
if (isset($classes[$fqcn])) {
return [$fqcn];
}
}
// Fall back to the basename as a short class name (also covers
// vendor files and paths given without src/Composer/).
$target = basename($path, '.php');
}
// Class name: exact FQCN first, else case-insensitive, else by short
// name (last segment).
$name = ltrim($target, '\\');
if (isset($classes[$name])) {
return [$name];
}
$matches = [];
foreach ($classes as $fqcn => $_) {
if (strcasecmp($fqcn, $name) === 0) {
$matches[] = $fqcn;
}
}
if ($matches !== []) {
return $matches;
}
foreach ($classes as $fqcn => $_) {
$parts = explode('\\', $fqcn);
if (strcasecmp(end($parts), $name) === 0) {
$matches[] = $fqcn;
}
}
return $matches;
}
function printRow(array $row): void
{
echo $row['fqcn'], "\n";
echo ' category: ', $row['category'];
if (isset($row['computedCategory'])) {
echo ' (computed: ', $row['computedCategory'], ', overridden)';
}
echo "\n";
echo ' direction: ', $row['direction'] ?? '-', "\n";
echo ' reachable: ', $row['reachable'] ? 'yes' : 'no', "\n";
foreach ($row['reasons'] as $reason) {
echo ' reason: ', $reason, "\n";
}
foreach ($row['attributes'] as $key => $value) {
echo ' ', $key, ': ', is_array($value) ? implode(', ', $value) : var_export($value, true), "\n";
}
if (isset($row['methods'])) {
$mutators = array_values(array_filter($row['methods'], static fn (array $m) => $m['purity'] === 'mutator'));
echo ' methods: ', count($row['methods']), ' public/protected, ', count($mutators), " mutator\n";
foreach ($mutators as $m) {
echo ' mutator: ', $m['name'], "\n";
}
}
}
$exit = 0;
foreach (array_slice($argv, 1) as $i => $target) {
if ($i > 0) {
echo "\n";
}
$matches = resolveTarget($target, $classes);
if ($matches !== []) {
foreach ($matches as $j => $fqcn) {
if ($j > 0) {
echo "\n";
}
printRow($classes[$fqcn]);
}
continue;
}
// Vendor types: reached ones carry a direction; any other name under a
// known vendor prefix classifies at package granularity.
$name = ltrim($target, '\\');
if (isset($vendorTypes[$name])) {
$row = $vendorTypes[$name];
echo $row['fqcn'], "\n";
echo ' category: ', $row['category'], ' (vendor package ', $row['package'], ")\n";
echo ' direction: ', $row['direction'], "\n";
continue;
}
$vendor = VendorPackages::lookup($name);
if ($vendor !== null) {
echo $name, "\n";
echo ' category: ', $vendor['category'], ' (vendor package ', $vendor['package'], ", wholesale)\n";
continue;
}
fwrite(STDERR, "not found: $target\n");
$exit = 1;
}
exit($exit);
|