4
* Applies lint rules for Phutil libraries. We enforce three rules:
6
* # If you use a symbol, it must be defined somewhere.
7
* # If you define a symbol, it must not duplicate another definition.
8
* # If you define a class or interface in a file, it MUST be the only symbol
9
* defined in that file.
11
final class ArcanistPhutilLibraryLinter extends ArcanistLinter {
13
const LINT_UNKNOWN_SYMBOL = 1;
14
const LINT_DUPLICATE_SYMBOL = 2;
15
const LINT_ONE_CLASS_PER_FILE = 3;
17
public function getInfoName() {
18
return 'Phutil Library Linter';
21
public function getInfoDescription() {
23
'Make sure all the symbols use in a libphutil library are defined and '.
24
'known. This linter is specific to PHP source in libphutil libraries.');
27
public function getLinterConfigurationName() {
28
return 'phutil-library';
31
public function getLintNameMap() {
33
self::LINT_UNKNOWN_SYMBOL => pht('Unknown Symbol'),
34
self::LINT_DUPLICATE_SYMBOL => pht('Duplicate Symbol'),
35
self::LINT_ONE_CLASS_PER_FILE => pht('One Class Per File'),
39
public function getLinterName() {
43
public function getLinterPriority() {
47
public function willLintPaths(array $paths) {
48
if (!xhpast_is_available()) {
49
throw new Exception(xhpast_get_build_instructions());
52
// NOTE: For now, we completely ignore paths and just lint every library in
53
// its entirety. This is simpler and relatively fast because we don't do any
54
// detailed checks and all the data we need for this comes out of module
57
$bootloader = PhutilBootloader::getInstance();
58
$libs = $bootloader->getAllLibraries();
60
// Load the up-to-date map for each library, without loading the library
61
// itself. This means lint results will accurately reflect the state of
65
foreach ($libs as $lib) {
66
$root = phutil_get_library_root($lib);
69
$symbols[$lib] = id(new PhutilLibraryMapBuilder($root))
70
->buildFileSymbolMap();
71
} catch (XHPASTSyntaxErrorException $ex) {
72
// If the library contains a syntax error then there isn't much that we
78
$all_symbols = array();
79
foreach ($symbols as $library => $map) {
80
// Check for files which declare more than one class/interface in the same
81
// file, or mix function definitions with class/interface definitions. We
82
// must isolate autoloadable symbols to one per file so the autoloader
83
// can't end up in an unresolvable cycle.
84
foreach ($map as $file => $spec) {
85
$have = idx($spec, 'have', array());
88
idx($have, 'class', array()) +
89
idx($have, 'interface', array());
90
$have_functions = idx($have, 'function');
92
if ($have_functions && $have_classes) {
93
$function_list = implode(', ', array_keys($have_functions));
94
$class_list = implode(', ', array_keys($have_classes));
95
$this->raiseLintInLibrary(
99
self::LINT_ONE_CLASS_PER_FILE,
100
"File '{$file}' mixes function ({$function_list}) and ".
101
"class/interface ({$class_list}) definitions in the same file. ".
102
"A file which declares a class or an interface MUST ".
103
"declare nothing else.");
104
} else if (count($have_classes) > 1) {
105
$class_list = implode(', ', array_keys($have_classes));
106
$this->raiseLintInLibrary(
110
self::LINT_ONE_CLASS_PER_FILE,
111
"File '{$file}' declares more than one class or interface ".
112
"({$class_list}). A file which declares a class or interface MUST ".
113
"declare nothing else.");
117
// Check for duplicate symbols: two files providing the same class or
119
foreach ($map as $file => $spec) {
120
$have = idx($spec, 'have', array());
121
foreach (array('class', 'function', 'interface') as $type) {
122
$libtype = ($type == 'interface') ? 'class' : $type;
123
foreach (idx($have, $type, array()) as $symbol => $offset) {
124
if (empty($all_symbols[$libtype][$symbol])) {
125
$all_symbols[$libtype][$symbol] = array(
126
'library' => $library,
133
$osrc = $all_symbols[$libtype][$symbol]['file'];
134
$olib = $all_symbols[$libtype][$symbol]['library'];
136
$this->raiseLintInLibrary(
140
self::LINT_DUPLICATE_SYMBOL,
141
"Definition of {$type} '{$symbol}' in '{$file}' in library ".
142
"'{$library}' duplicates prior definition in '{$osrc}' in ".
143
"library '{$olib}'.");
149
$types = array('class', 'function', 'interface', 'class/interface');
150
foreach ($symbols as $library => $map) {
151
// Check for unknown symbols: uses of classes, functions or interfaces
152
// which are not defined anywhere. We reference the list of all symbols
153
// we built up earlier.
154
foreach ($map as $file => $spec) {
155
$need = idx($spec, 'need', array());
156
foreach ($types as $type) {
158
if ($type == 'interface' || $type == 'class/interface') {
161
foreach (idx($need, $type, array()) as $symbol => $offset) {
162
if (!empty($all_symbols[$libtype][$symbol])) {
163
// Symbol is defined somewhere.
167
$libphutil_root = dirname(phutil_get_library_root('phutil'));
169
$this->raiseLintInLibrary(
173
self::LINT_UNKNOWN_SYMBOL,
174
"Use of unknown {$type} '{$symbol}'. Common causes are:\n\n".
175
" - Your libphutil/ is out of date.\n".
176
" This is the most common cause.\n".
177
" Update this copy of libphutil: {$libphutil_root}\n".
179
" - Some other library is out of date.\n".
180
" Update the library this symbol appears in.\n".
182
" - This symbol is misspelled.\n".
183
" Spell the symbol name correctly.\n".
184
" Symbol name spelling is case-sensitive.\n".
186
" - This symbol was added recently.\n".
187
" Run `arc liberate` on the library it was added to.\n".
189
" - This symbol is external. Use `@phutil-external-symbol`.\n".
190
" Use `grep` to find usage examples of this directive.\n".
192
"*** ALTHOUGH USUALLY EASY TO FIX, THIS IS A SERIOUS ERROR.\n".
193
"*** THIS ERROR IS YOUR FAULT. YOU MUST RESOLVE IT.");
200
private function raiseLintInLibrary($library, $path, $offset, $code, $desc) {
201
$root = phutil_get_library_root($library);
203
$this->activePath = $root.'/'.$path;
204
$this->raiseLintAtOffset($offset, $code, $desc);
207
public function lintPath($path) {
211
public function getCacheGranularity() {
212
return self::GRANULARITY_GLOBAL;