Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php
13466 views
1
<?php
2
3
final class PhabricatorJavelinLinter extends ArcanistLinter {
4
5
private $symbols = array();
6
7
private $symbolsBinary;
8
private $haveWarnedAboutBinary;
9
10
const LINT_PRIVATE_ACCESS = 1;
11
const LINT_MISSING_DEPENDENCY = 2;
12
const LINT_UNNECESSARY_DEPENDENCY = 3;
13
const LINT_UNKNOWN_DEPENDENCY = 4;
14
const LINT_MISSING_BINARY = 5;
15
16
public function getInfoName() {
17
return pht('Javelin Linter');
18
}
19
20
public function getInfoDescription() {
21
return pht(
22
'This linter is intended for use with the Javelin JS library and '.
23
'extensions. Use `%s` to run Javelin rules on Javascript source files.',
24
'javelinsymbols');
25
}
26
27
private function getBinaryPath() {
28
if ($this->symbolsBinary === null) {
29
list($err, $stdout) = exec_manual('which javelinsymbols');
30
$this->symbolsBinary = ($err ? false : rtrim($stdout));
31
}
32
return $this->symbolsBinary;
33
}
34
35
public function willLintPaths(array $paths) {
36
if (!$this->getBinaryPath()) {
37
return;
38
}
39
40
$root = dirname(phutil_get_library_root('phabricator'));
41
require_once $root.'/scripts/__init_script__.php';
42
43
$futures = array();
44
foreach ($paths as $path) {
45
if ($this->shouldIgnorePath($path)) {
46
continue;
47
}
48
49
$future = $this->newSymbolsFuture($path);
50
$futures[$path] = $future;
51
}
52
53
foreach (id(new FutureIterator($futures))->limit(8) as $path => $future) {
54
$this->symbols[$path] = $future->resolvex();
55
}
56
}
57
58
public function getLinterName() {
59
return 'JAVELIN';
60
}
61
62
public function getLinterConfigurationName() {
63
return 'javelin';
64
}
65
66
public function getLintSeverityMap() {
67
return array(
68
self::LINT_MISSING_BINARY => ArcanistLintSeverity::SEVERITY_WARNING,
69
);
70
}
71
72
public function getLintNameMap() {
73
return array(
74
self::LINT_PRIVATE_ACCESS =>
75
pht('Private Method/Member Access'),
76
self::LINT_MISSING_DEPENDENCY =>
77
pht('Missing Javelin Dependency'),
78
self::LINT_UNNECESSARY_DEPENDENCY =>
79
pht('Unnecessary Javelin Dependency'),
80
self::LINT_UNKNOWN_DEPENDENCY =>
81
pht('Unknown Javelin Dependency'),
82
self::LINT_MISSING_BINARY =>
83
pht('`%s` Not In Path', 'javelinsymbols'),
84
);
85
}
86
87
public function getCacheGranularity() {
88
return parent::GRANULARITY_REPOSITORY;
89
}
90
91
public function getCacheVersion() {
92
$version = '0';
93
$binary_path = $this->getBinaryPath();
94
if ($binary_path) {
95
$version .= '-'.md5_file($binary_path);
96
}
97
return $version;
98
}
99
100
private function shouldIgnorePath($path) {
101
return preg_match('@/__tests__/|externals/javelin/docs/@', $path);
102
}
103
104
public function lintPath($path) {
105
if ($this->shouldIgnorePath($path)) {
106
return;
107
}
108
109
if (!$this->symbolsBinary) {
110
if (!$this->haveWarnedAboutBinary) {
111
$this->haveWarnedAboutBinary = true;
112
// TODO: Write build documentation for the Javelin binaries and point
113
// the user at it.
114
$this->raiseLintAtLine(
115
1,
116
0,
117
self::LINT_MISSING_BINARY,
118
pht(
119
"The '%s' binary in the Javelin project is not available in %s, ".
120
"so the Javelin linter can't run. This isn't a big concern, ".
121
"but means some Javelin problems can't be automatically detected.",
122
'javelinsymbols',
123
'$PATH'));
124
}
125
return;
126
}
127
128
list($uses, $installs) = $this->getUsedAndInstalledSymbolsForPath($path);
129
foreach ($uses as $symbol => $line) {
130
$parts = explode('.', $symbol);
131
foreach ($parts as $part) {
132
if ($part[0] == '_' && $part[1] != '_') {
133
$base = implode('.', array_slice($parts, 0, 2));
134
if (!array_key_exists($base, $installs)) {
135
$this->raiseLintAtLine(
136
$line,
137
0,
138
self::LINT_PRIVATE_ACCESS,
139
pht(
140
"This file accesses private symbol '%s' across file ".
141
"boundaries. You may only access private members and methods ".
142
"from the file where they are defined.",
143
$symbol));
144
}
145
break;
146
}
147
}
148
}
149
150
$external_classes = array();
151
foreach ($uses as $symbol => $line) {
152
$parts = explode('.', $symbol);
153
$class = implode('.', array_slice($parts, 0, 2));
154
if (!array_key_exists($class, $external_classes) &&
155
!array_key_exists($class, $installs)) {
156
$external_classes[$class] = $line;
157
}
158
}
159
160
$celerity = CelerityResourceMap::getNamedInstance('phabricator');
161
162
$path = preg_replace(
163
'@^externals/javelinjs/src/@',
164
'webroot/rsrc/js/javelin/',
165
$path);
166
$need = $external_classes;
167
168
$resource_name = substr($path, strlen('webroot/'));
169
$requires = $celerity->getRequiredSymbolsForName($resource_name);
170
if (!$requires) {
171
$requires = array();
172
}
173
174
foreach ($requires as $key => $requires_symbol) {
175
$requires_name = $celerity->getResourceNameForSymbol($requires_symbol);
176
if ($requires_name === null) {
177
$this->raiseLintAtLine(
178
0,
179
0,
180
self::LINT_UNKNOWN_DEPENDENCY,
181
pht(
182
"This file %s component '%s', but it does not exist. ".
183
"You may need to rebuild the Celerity map.",
184
'@requires',
185
$requires_symbol));
186
unset($requires[$key]);
187
continue;
188
}
189
190
if (preg_match('/\\.css$/', $requires_name)) {
191
// If JS requires CSS, just assume everything is fine.
192
unset($requires[$key]);
193
} else {
194
$symbol_path = 'webroot/'.$requires_name;
195
list($ignored, $req_install) = $this->getUsedAndInstalledSymbolsForPath(
196
$symbol_path);
197
if (array_intersect_key($req_install, $external_classes)) {
198
$need = array_diff_key($need, $req_install);
199
unset($requires[$key]);
200
}
201
}
202
}
203
204
foreach ($need as $class => $line) {
205
$this->raiseLintAtLine(
206
$line,
207
0,
208
self::LINT_MISSING_DEPENDENCY,
209
pht(
210
"This file uses '%s' but does not @requires the component ".
211
"which installs it. You may need to rebuild the Celerity map.",
212
$class));
213
}
214
215
foreach ($requires as $component) {
216
$this->raiseLintAtLine(
217
0,
218
0,
219
self::LINT_UNNECESSARY_DEPENDENCY,
220
pht(
221
"This file %s component '%s' but does not use anything it provides.",
222
'@requires',
223
$component));
224
}
225
}
226
227
private function loadSymbols($path) {
228
if (empty($this->symbols[$path])) {
229
$this->symbols[$path] = $this->newSymbolsFuture($path)->resolvex();
230
}
231
return $this->symbols[$path];
232
}
233
234
private function newSymbolsFuture($path) {
235
$future = new ExecFuture('javelinsymbols # %s', $path);
236
$future->write($this->getData($path));
237
return $future;
238
}
239
240
private function getUsedAndInstalledSymbolsForPath($path) {
241
list($symbols) = $this->loadSymbols($path);
242
$symbols = trim($symbols);
243
244
$uses = array();
245
$installs = array();
246
if (empty($symbols)) {
247
// This file has no symbols.
248
return array($uses, $installs);
249
}
250
251
$symbols = explode("\n", trim($symbols));
252
foreach ($symbols as $line) {
253
$matches = null;
254
if (!preg_match('/^([?+\*])([^:]*):(\d+)$/', $line, $matches)) {
255
throw new Exception(
256
pht('Received malformed output from `%s`.', 'javelinsymbols'));
257
}
258
$type = $matches[1];
259
$symbol = $matches[2];
260
$line = $matches[3];
261
262
switch ($type) {
263
case '?':
264
$uses[$symbol] = $line;
265
break;
266
case '+':
267
$installs['JX.'.$symbol] = $line;
268
break;
269
}
270
}
271
272
$contents = $this->getData($path);
273
274
$matches = null;
275
$count = preg_match_all(
276
'/@javelin-installs\W+(\S+)/',
277
$contents,
278
$matches,
279
PREG_PATTERN_ORDER);
280
281
if ($count) {
282
foreach ($matches[1] as $symbol) {
283
$installs[$symbol] = 0;
284
}
285
}
286
287
return array($uses, $installs);
288
}
289
290
}
291
292