4
* Uses XHPAST to apply lint rules to PHP.
6
final class ArcanistXHPASTLinter extends ArcanistBaseXHPASTLinter {
8
const LINT_PHP_SYNTAX_ERROR = 1;
9
const LINT_UNABLE_TO_PARSE = 2;
10
const LINT_VARIABLE_VARIABLE = 3;
11
const LINT_EXTRACT_USE = 4;
12
const LINT_UNDECLARED_VARIABLE = 5;
13
const LINT_PHP_SHORT_TAG = 6;
14
const LINT_PHP_ECHO_TAG = 7;
15
const LINT_PHP_CLOSE_TAG = 8;
16
const LINT_NAMING_CONVENTIONS = 9;
17
const LINT_IMPLICIT_CONSTRUCTOR = 10;
18
const LINT_DYNAMIC_DEFINE = 12;
19
const LINT_STATIC_THIS = 13;
20
const LINT_PREG_QUOTE_MISUSE = 14;
21
const LINT_PHP_OPEN_TAG = 15;
22
const LINT_TODO_COMMENT = 16;
23
const LINT_EXIT_EXPRESSION = 17;
24
const LINT_COMMENT_STYLE = 18;
25
const LINT_CLASS_FILENAME_MISMATCH = 19;
26
const LINT_TAUTOLOGICAL_EXPRESSION = 20;
27
const LINT_PLUS_OPERATOR_ON_STRINGS = 21;
28
const LINT_DUPLICATE_KEYS_IN_ARRAY = 22;
29
const LINT_REUSED_ITERATORS = 23;
30
const LINT_BRACE_FORMATTING = 24;
31
const LINT_PARENTHESES_SPACING = 25;
32
const LINT_CONTROL_STATEMENT_SPACING = 26;
33
const LINT_BINARY_EXPRESSION_SPACING = 27;
34
const LINT_ARRAY_INDEX_SPACING = 28;
35
const LINT_IMPLICIT_FALLTHROUGH = 30;
36
const LINT_REUSED_AS_ITERATOR = 32;
37
const LINT_COMMENT_SPACING = 34;
38
const LINT_SLOWNESS = 36;
39
const LINT_CLOSING_CALL_PAREN = 37;
40
const LINT_CLOSING_DECL_PAREN = 38;
41
const LINT_REUSED_ITERATOR_REFERENCE = 39;
42
const LINT_KEYWORD_CASING = 40;
43
const LINT_DOUBLE_QUOTE = 41;
44
const LINT_ELSEIF_USAGE = 42;
45
const LINT_SEMICOLON_SPACING = 43;
46
const LINT_CONCATENATION_OPERATOR = 44;
47
const LINT_PHP_COMPATIBILITY = 45;
48
const LINT_LANGUAGE_CONSTRUCT_PAREN = 46;
49
const LINT_EMPTY_STATEMENT = 47;
50
const LINT_ARRAY_SEPARATOR = 48;
51
const LINT_CONSTRUCTOR_PARENTHESES = 49;
56
private $windowsVersion;
58
public function getInfoName() {
62
public function getInfoDescription() {
63
return pht('Use XHPAST to enforce coding conventions on PHP source files.');
66
public function getLintNameMap() {
68
self::LINT_PHP_SYNTAX_ERROR => 'PHP Syntax Error!',
69
self::LINT_UNABLE_TO_PARSE => 'Unable to Parse',
70
self::LINT_VARIABLE_VARIABLE => 'Use of Variable Variable',
71
self::LINT_EXTRACT_USE => 'Use of extract()',
72
self::LINT_UNDECLARED_VARIABLE => 'Use of Undeclared Variable',
73
self::LINT_PHP_SHORT_TAG => 'Use of Short Tag "<?"',
74
self::LINT_PHP_ECHO_TAG => 'Use of Echo Tag "<?="',
75
self::LINT_PHP_CLOSE_TAG => 'Use of Close Tag "?>"',
76
self::LINT_NAMING_CONVENTIONS => 'Naming Conventions',
77
self::LINT_IMPLICIT_CONSTRUCTOR => 'Implicit Constructor',
78
self::LINT_DYNAMIC_DEFINE => 'Dynamic define()',
79
self::LINT_STATIC_THIS => 'Use of $this in Static Context',
80
self::LINT_PREG_QUOTE_MISUSE => 'Misuse of preg_quote()',
81
self::LINT_PHP_OPEN_TAG => 'Expected Open Tag',
82
self::LINT_TODO_COMMENT => 'TODO Comment',
83
self::LINT_EXIT_EXPRESSION => 'Exit Used as Expression',
84
self::LINT_COMMENT_STYLE => 'Comment Style',
85
self::LINT_CLASS_FILENAME_MISMATCH => 'Class-Filename Mismatch',
86
self::LINT_TAUTOLOGICAL_EXPRESSION => 'Tautological Expression',
87
self::LINT_PLUS_OPERATOR_ON_STRINGS => 'Not String Concatenation',
88
self::LINT_DUPLICATE_KEYS_IN_ARRAY => 'Duplicate Keys in Array',
89
self::LINT_REUSED_ITERATORS => 'Reuse of Iterator Variable',
90
self::LINT_BRACE_FORMATTING => 'Brace placement',
91
self::LINT_PARENTHESES_SPACING => 'Spaces Inside Parentheses',
92
self::LINT_CONTROL_STATEMENT_SPACING => 'Space After Control Statement',
93
self::LINT_BINARY_EXPRESSION_SPACING => 'Space Around Binary Operator',
94
self::LINT_ARRAY_INDEX_SPACING => 'Spacing Before Array Index',
95
self::LINT_IMPLICIT_FALLTHROUGH => 'Implicit Fallthrough',
96
self::LINT_REUSED_AS_ITERATOR => 'Variable Reused As Iterator',
97
self::LINT_COMMENT_SPACING => 'Comment Spaces',
98
self::LINT_SLOWNESS => 'Slow Construct',
99
self::LINT_CLOSING_CALL_PAREN => 'Call Formatting',
100
self::LINT_CLOSING_DECL_PAREN => 'Declaration Formatting',
101
self::LINT_REUSED_ITERATOR_REFERENCE => 'Reuse of Iterator References',
102
self::LINT_KEYWORD_CASING => 'Keyword Conventions',
103
self::LINT_DOUBLE_QUOTE => 'Unnecessary Double Quotes',
104
self::LINT_ELSEIF_USAGE => 'ElseIf Usage',
105
self::LINT_SEMICOLON_SPACING => 'Semicolon Spacing',
106
self::LINT_CONCATENATION_OPERATOR => 'Concatenation Spacing',
107
self::LINT_PHP_COMPATIBILITY => 'PHP Compatibility',
108
self::LINT_LANGUAGE_CONSTRUCT_PAREN => 'Language Construct Parentheses',
109
self::LINT_EMPTY_STATEMENT => 'Empty Block Statement',
110
self::LINT_ARRAY_SEPARATOR => 'Array Separator',
111
self::LINT_CONSTRUCTOR_PARENTHESES => 'Constructor Parentheses',
115
public function getLinterName() {
119
public function getLinterConfigurationName() {
123
public function getLintSeverityMap() {
124
$disabled = ArcanistLintSeverity::SEVERITY_DISABLED;
125
$advice = ArcanistLintSeverity::SEVERITY_ADVICE;
126
$warning = ArcanistLintSeverity::SEVERITY_WARNING;
129
self::LINT_TODO_COMMENT => $disabled,
130
self::LINT_UNABLE_TO_PARSE => $warning,
131
self::LINT_NAMING_CONVENTIONS => $warning,
132
self::LINT_PREG_QUOTE_MISUSE => $advice,
133
self::LINT_BRACE_FORMATTING => $warning,
134
self::LINT_PARENTHESES_SPACING => $warning,
135
self::LINT_CONTROL_STATEMENT_SPACING => $warning,
136
self::LINT_BINARY_EXPRESSION_SPACING => $warning,
137
self::LINT_ARRAY_INDEX_SPACING => $warning,
138
self::LINT_IMPLICIT_FALLTHROUGH => $warning,
139
self::LINT_SLOWNESS => $warning,
140
self::LINT_COMMENT_SPACING => $advice,
141
self::LINT_CLOSING_CALL_PAREN => $warning,
142
self::LINT_CLOSING_DECL_PAREN => $warning,
143
self::LINT_REUSED_ITERATOR_REFERENCE => $warning,
144
self::LINT_KEYWORD_CASING => $warning,
145
self::LINT_DOUBLE_QUOTE => $advice,
146
self::LINT_ELSEIF_USAGE => $advice,
147
self::LINT_SEMICOLON_SPACING => $advice,
148
self::LINT_CONCATENATION_OPERATOR => $warning,
149
self::LINT_LANGUAGE_CONSTRUCT_PAREN => $warning,
150
self::LINT_EMPTY_STATEMENT => $advice,
151
self::LINT_ARRAY_SEPARATOR => $advice,
152
self::LINT_CONSTRUCTOR_PARENTHESES => $advice,
156
public function getLinterConfigurationOptions() {
157
return parent::getLinterConfigurationOptions() + array(
158
'xhpast.naminghook' => array(
159
'type' => 'optional string',
161
'Name of a concrete subclass of ArcanistXHPASTLintNamingHook which '.
162
'enforces more granular naming convention rules for symbols.'),
164
'xhpast.switchhook' => array(
165
'type' => 'optional string',
167
'Name of a concrete subclass of ArcanistXHPASTLintSwitchHook which '.
168
'tunes the analysis of switch() statements for this linter.'),
170
'xhpast.php-version' => array(
171
'type' => 'optional string',
172
'help' => pht('PHP version to target.'),
174
'xhpast.php-version.windows' => array(
175
'type' => 'optional string',
176
'help' => pht('PHP version to target on Windows.'),
181
public function setLinterConfigurationValue($key, $value) {
183
case 'xhpast.naminghook':
184
$this->naminghook = $value;
186
case 'xhpast.switchhook':
187
$this->switchhook = $value;
189
case 'xhpast.php-version':
190
$this->version = $value;
192
case 'xhpast.php-version.windows':
193
$this->windowsVersion = $value;
197
return parent::setLinterConfigurationValue($key, $value);
200
public function getVersion() {
201
// The version number should be incremented whenever a new rule is added.
205
protected function resolveFuture($path, Future $future) {
206
$tree = $this->getXHPASTTreeForPath($path);
208
$ex = $this->getXHPASTExceptionForPath($path);
209
if ($ex instanceof XHPASTSyntaxErrorException) {
210
$this->raiseLintAtLine(
213
self::LINT_PHP_SYNTAX_ERROR,
214
'This file contains a syntax error: '.$ex->getMessage());
215
} else if ($ex instanceof Exception) {
216
$this->raiseLintAtPath(self::LINT_UNABLE_TO_PARSE, $ex->getMessage());
221
$root = $tree->getRootNode();
223
$method_codes = array(
224
'lintStrstrUsedForCheck' => self::LINT_SLOWNESS,
225
'lintStrposUsedForStart' => self::LINT_SLOWNESS,
226
'lintImplicitFallthrough' => self::LINT_IMPLICIT_FALLTHROUGH,
227
'lintBraceFormatting' => self::LINT_BRACE_FORMATTING,
228
'lintTautologicalExpressions' => self::LINT_TAUTOLOGICAL_EXPRESSION,
229
'lintCommentSpaces' => self::LINT_COMMENT_SPACING,
230
'lintHashComments' => self::LINT_COMMENT_STYLE,
231
'lintReusedIterators' => self::LINT_REUSED_ITERATORS,
232
'lintReusedIteratorReferences' => self::LINT_REUSED_ITERATOR_REFERENCE,
233
'lintVariableVariables' => self::LINT_VARIABLE_VARIABLE,
234
'lintUndeclaredVariables' => array(
235
self::LINT_EXTRACT_USE,
236
self::LINT_REUSED_AS_ITERATOR,
237
self::LINT_UNDECLARED_VARIABLE,
239
'lintPHPTagUse' => array(
240
self::LINT_PHP_SHORT_TAG,
241
self::LINT_PHP_ECHO_TAG,
242
self::LINT_PHP_OPEN_TAG,
243
self::LINT_PHP_CLOSE_TAG,
245
'lintNamingConventions' => self::LINT_NAMING_CONVENTIONS,
246
'lintSurpriseConstructors' => self::LINT_IMPLICIT_CONSTRUCTOR,
247
'lintParenthesesShouldHugExpressions' => self::LINT_PARENTHESES_SPACING,
248
'lintSpaceAfterControlStatementKeywords' =>
249
self::LINT_CONTROL_STATEMENT_SPACING,
250
'lintSpaceAroundBinaryOperators' => self::LINT_BINARY_EXPRESSION_SPACING,
251
'lintDynamicDefines' => self::LINT_DYNAMIC_DEFINE,
252
'lintUseOfThisInStaticMethods' => self::LINT_STATIC_THIS,
253
'lintPregQuote' => self::LINT_PREG_QUOTE_MISUSE,
254
'lintExitExpressions' => self::LINT_EXIT_EXPRESSION,
255
'lintArrayIndexWhitespace' => self::LINT_ARRAY_INDEX_SPACING,
256
'lintTODOComments' => self::LINT_TODO_COMMENT,
257
'lintPrimaryDeclarationFilenameMatch' =>
258
self::LINT_CLASS_FILENAME_MISMATCH,
259
'lintPlusOperatorOnStrings' => self::LINT_PLUS_OPERATOR_ON_STRINGS,
260
'lintDuplicateKeysInArray' => self::LINT_DUPLICATE_KEYS_IN_ARRAY,
261
'lintClosingCallParen' => self::LINT_CLOSING_CALL_PAREN,
262
'lintClosingDeclarationParen' => self::LINT_CLOSING_DECL_PAREN,
263
'lintKeywordCasing' => self::LINT_KEYWORD_CASING,
264
'lintStrings' => self::LINT_DOUBLE_QUOTE,
265
'lintElseIfStatements' => self::LINT_ELSEIF_USAGE,
266
'lintSemicolons' => self::LINT_SEMICOLON_SPACING,
267
'lintSpaceAroundConcatenationOperators' =>
268
self::LINT_CONCATENATION_OPERATOR,
269
'lintPHPCompatibility' => self::LINT_PHP_COMPATIBILITY,
270
'lintLanguageConstructParentheses' => self::LINT_LANGUAGE_CONSTRUCT_PAREN,
271
'lintEmptyBlockStatements' => self::LINT_EMPTY_STATEMENT,
272
'lintArraySeparator' => self::LINT_ARRAY_SEPARATOR,
273
'lintConstructorParentheses' => self::LINT_CONSTRUCTOR_PARENTHESES,
276
foreach ($method_codes as $method => $codes) {
277
foreach ((array)$codes as $code) {
278
if ($this->isCodeEnabled($code)) {
279
call_user_func(array($this, $method), $root);
286
private function lintStrstrUsedForCheck(XHPASTNode $root) {
287
$expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
288
foreach ($expressions as $expression) {
289
$operator = $expression->getChildOfType(1, 'n_OPERATOR');
290
$operator = $operator->getConcreteString();
292
if ($operator !== '===' && $operator !== '!==') {
296
$false = $expression->getChildByIndex(0);
297
if ($false->getTypeName() === 'n_SYMBOL_NAME' &&
298
$false->getConcreteString() === 'false') {
299
$strstr = $expression->getChildByIndex(2);
302
$false = $expression->getChildByIndex(2);
303
if ($false->getTypeName() !== 'n_SYMBOL_NAME' ||
304
$false->getConcreteString() !== 'false') {
309
if ($strstr->getTypeName() !== 'n_FUNCTION_CALL') {
313
$name = strtolower($strstr->getChildByIndex(0)->getConcreteString());
314
if ($name === 'strstr' || $name === 'strchr') {
315
$this->raiseLintAtNode(
318
'Use strpos() for checking if the string contains something.');
319
} else if ($name === 'stristr') {
320
$this->raiseLintAtNode(
323
'Use stripos() for checking if the string contains something.');
328
private function lintStrposUsedForStart(XHPASTNode $root) {
329
$expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
330
foreach ($expressions as $expression) {
331
$operator = $expression->getChildOfType(1, 'n_OPERATOR');
332
$operator = $operator->getConcreteString();
334
if ($operator !== '===' && $operator !== '!==') {
338
$zero = $expression->getChildByIndex(0);
339
if ($zero->getTypeName() === 'n_NUMERIC_SCALAR' &&
340
$zero->getConcreteString() === '0') {
341
$strpos = $expression->getChildByIndex(2);
344
$zero = $expression->getChildByIndex(2);
345
if ($zero->getTypeName() !== 'n_NUMERIC_SCALAR' ||
346
$zero->getConcreteString() !== '0') {
351
if ($strpos->getTypeName() !== 'n_FUNCTION_CALL') {
355
$name = strtolower($strpos->getChildByIndex(0)->getConcreteString());
356
if ($name === 'strpos') {
357
$this->raiseLintAtNode(
360
'Use strncmp() for checking if the string starts with something.');
361
} else if ($name === 'stripos') {
362
$this->raiseLintAtNode(
365
'Use strncasecmp() for checking if the string starts with '.
371
private function lintPHPCompatibility(XHPASTNode $root) {
372
if (!$this->version) {
376
$target = phutil_get_library_root('phutil').
377
'/../resources/php_compat_info.json';
378
$compat_info = phutil_json_decode(Filesystem::readFile($target));
380
// Create a whitelist for symbols which are being used conditionally.
383
'function' => array(),
386
$conditionals = $root->selectDescendantsOfType('n_IF');
387
foreach ($conditionals as $conditional) {
388
$condition = $conditional->getChildOfType(0, 'n_CONTROL_CONDITION');
389
$function = $condition->getChildByIndex(0);
391
if ($function->getTypeName() != 'n_FUNCTION_CALL') {
395
$function_token = $function
396
->getChildByIndex(0);
398
if ($function_token->getTypeName() != 'n_SYMBOL_NAME') {
399
// This may be `Class::method(...)` or `$var(...)`.
403
$function_name = $function_token->getConcreteString();
405
switch ($function_name) {
407
case 'function_exists':
408
case 'interface_exists':
410
switch ($function_name) {
415
case 'function_exists':
419
case 'interface_exists':
424
$params = $function->getChildOfType(1, 'n_CALL_PARAMETER_LIST');
425
$symbol = $params->getChildByIndex(0);
427
if (!$symbol->isStaticScalar()) {
431
$symbol_name = $symbol->evalStatic();
432
if (!idx($whitelist[$type], $symbol_name)) {
433
$whitelist[$type][$symbol_name] = array();
440
$whitelist[$type][$symbol_name][] = range(
447
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
448
foreach ($calls as $call) {
449
$node = $call->getChildByIndex(0);
450
$name = $node->getConcreteString();
451
$version = idx($compat_info['functions'], $name);
453
if ($version && version_compare($version['min'], $this->version, '>')) {
454
// Check if whitelisted.
455
$whitelisted = false;
456
foreach (idx($whitelist['function'], $name, array()) as $range) {
457
if (array_intersect($range, array_keys($node->getTokens()))) {
467
$this->raiseLintAtNode(
469
self::LINT_PHP_COMPATIBILITY,
470
"This codebase targets PHP {$this->version}, but `{$name}()` was ".
471
"not introduced until PHP {$version['min']}.");
472
} else if (array_key_exists($name, $compat_info['params'])) {
473
$params = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST');
474
foreach (array_values($params->getChildren()) as $i => $param) {
475
$version = idx($compat_info['params'][$name], $i);
476
if ($version && version_compare($version, $this->version, '>')) {
477
$this->raiseLintAtNode(
479
self::LINT_PHP_COMPATIBILITY,
480
"This codebase targets PHP {$this->version}, but parameter ".
481
($i + 1)." of `{$name}()` was not introduced until PHP ".
487
if ($this->windowsVersion) {
488
$windows = idx($compat_info['functions_windows'], $name);
490
if ($windows === false) {
491
$this->raiseLintAtNode(
493
self::LINT_PHP_COMPATIBILITY,
494
"This codebase targets PHP {$this->windowsVersion} on Windows, ".
495
"but `{$name}()` is not available there.");
496
} else if (version_compare($windows, $this->windowsVersion, '>')) {
497
$this->raiseLintAtNode(
499
self::LINT_PHP_COMPATIBILITY,
500
"This codebase targets PHP {$this->windowsVersion} on Windows, ".
501
"but `{$name}()` is not available there until PHP ".
502
"{$this->windowsVersion}.");
507
$classes = $root->selectDescendantsOfType('n_CLASS_NAME');
508
foreach ($classes as $node) {
509
$name = $node->getConcreteString();
510
$version = idx($compat_info['interfaces'], $name);
511
$version = idx($compat_info['classes'], $name, $version);
512
if ($version && version_compare($version['min'], $this->version, '>')) {
513
// Check if whitelisted.
514
$whitelisted = false;
515
foreach (idx($whitelist['class'], $name, array()) as $range) {
516
if (array_intersect($range, array_keys($node->getTokens()))) {
526
$this->raiseLintAtNode(
528
self::LINT_PHP_COMPATIBILITY,
529
"This codebase targets PHP {$this->version}, but `{$name}` was not ".
530
"introduced until PHP {$version['min']}.");
534
// TODO: Technically, this will include function names. This is unlikely to
535
// cause any issues (unless, of course, there existed a function that had
536
// the same name as some constant).
537
$constants = $root->selectDescendantsOfType('n_SYMBOL_NAME');
538
foreach ($constants as $node) {
539
$name = $node->getConcreteString();
540
$version = idx($compat_info['constants'], $name);
541
if ($version && version_compare($version['min'], $this->version, '>')) {
542
$this->raiseLintAtNode(
544
self::LINT_PHP_COMPATIBILITY,
545
"This codebase targets PHP {$this->version}, but `{$name}` was not ".
546
"introduced until PHP {$version['min']}.");
550
if (version_compare($this->version, '5.3.0') < 0) {
551
$this->lintPHP53Features($root);
553
$this->lintPHP53Incompatibilities($root);
556
if (version_compare($this->version, '5.4.0') < 0) {
557
$this->lintPHP54Features($root);
559
$this->lintPHP54Incompatibilities($root);
563
private function lintPHP53Features(XHPASTNode $root) {
564
$functions = $root->selectTokensOfType('T_FUNCTION');
565
foreach ($functions as $function) {
566
$next = $function->getNextToken();
568
if ($next->isSemantic()) {
571
$next = $next->getNextToken();
575
if ($next->getTypeName() === '(') {
576
$this->raiseLintAtToken(
578
self::LINT_PHP_COMPATIBILITY,
579
"This codebase targets PHP {$this->version}, but anonymous ".
580
"functions were not introduced until PHP 5.3.");
585
$namespaces = $root->selectTokensOfType('T_NAMESPACE');
586
foreach ($namespaces as $namespace) {
587
$this->raiseLintAtToken(
589
self::LINT_PHP_COMPATIBILITY,
590
"This codebase targets PHP {$this->version}, but namespaces were not ".
591
"introduced until PHP 5.3.");
594
// NOTE: This is only "use x;", in anonymous functions the node type is
595
// n_LEXICAL_VARIABLE_LIST even though both tokens are T_USE.
597
// TODO: We parse n_USE in a slightly crazy way right now; that would be
598
// a better selector once it's fixed.
600
$uses = $root->selectDescendantsOfType('n_USE_LIST');
601
foreach ($uses as $use) {
602
$this->raiseLintAtNode(
604
self::LINT_PHP_COMPATIBILITY,
605
"This codebase targets PHP {$this->version}, but namespaces were not ".
606
"introduced until PHP 5.3.");
609
$statics = $root->selectDescendantsOfType('n_CLASS_STATIC_ACCESS');
610
foreach ($statics as $static) {
611
$name = $static->getChildByIndex(0);
612
if ($name->getTypeName() != 'n_CLASS_NAME') {
615
if ($name->getConcreteString() === 'static') {
616
$this->raiseLintAtNode(
618
self::LINT_PHP_COMPATIBILITY,
619
"This codebase targets PHP {$this->version}, but `static::` was not ".
620
"introduced until PHP 5.3.");
624
$ternaries = $root->selectDescendantsOfType('n_TERNARY_EXPRESSION');
625
foreach ($ternaries as $ternary) {
626
$yes = $ternary->getChildByIndex(1);
627
if ($yes->getTypeName() === 'n_EMPTY') {
628
$this->raiseLintAtNode(
630
self::LINT_PHP_COMPATIBILITY,
631
"This codebase targets PHP {$this->version}, but short ternary was ".
632
"not introduced until PHP 5.3.");
636
$heredocs = $root->selectDescendantsOfType('n_HEREDOC');
637
foreach ($heredocs as $heredoc) {
638
if (preg_match('/^<<<[\'"]/', $heredoc->getConcreteString())) {
639
$this->raiseLintAtNode(
641
self::LINT_PHP_COMPATIBILITY,
642
"This codebase targets PHP {$this->version}, but nowdoc was not ".
643
"introduced until PHP 5.3.");
648
private function lintPHP53Incompatibilities(XHPASTNode $root) {}
650
private function lintPHP54Features(XHPASTNode $root) {
651
$indexes = $root->selectDescendantsOfType('n_INDEX_ACCESS');
652
foreach ($indexes as $index) {
653
switch ($index->getChildByIndex(0)->getTypeName()) {
654
case 'n_FUNCTION_CALL':
655
case 'n_METHOD_CALL':
656
$this->raiseLintAtNode(
657
$index->getChildByIndex(1),
658
self::LINT_PHP_COMPATIBILITY,
660
'The `%s` syntax was not introduced until PHP 5.4, but this '.
661
'codebase targets an earlier version of PHP. You can rewrite '.
662
'this expression using `%s`.',
670
private function lintPHP54Incompatibilities(XHPASTNode $root) {
671
$breaks = $root->selectDescendantsOfTypes(array('n_BREAK', 'n_CONTINUE'));
672
foreach ($breaks as $break) {
673
$arg = $break->getChildByIndex(0);
675
switch ($arg->getTypeName()) {
679
case 'n_NUMERIC_SCALAR':
680
if ($arg->getConcreteString() != '0') {
685
$this->raiseLintAtNode(
686
$break->getChildByIndex(0),
687
self::LINT_PHP_COMPATIBILITY,
689
'The `%s` and `%s` statements no longer accept '.
690
'variable arguments.',
698
private function lintImplicitFallthrough(XHPASTNode $root) {
700
$working_copy = $this->getEngine()->getWorkingCopy();
702
$hook_class = $this->switchhook
704
: $this->getDeprecatedConfiguration('lint.xhpast.switchhook');
706
$hook_obj = newv($hook_class, array());
707
assert_instances_of(array($hook_obj), 'ArcanistXHPASTLintSwitchHook');
711
$switches = $root->selectDescendantsOfType('n_SWITCH');
712
foreach ($switches as $switch) {
715
$cases = $switch->selectDescendantsOfType('n_CASE');
716
foreach ($cases as $case) {
720
$defaults = $switch->selectDescendantsOfType('n_DEFAULT');
721
foreach ($defaults as $default) {
722
$blocks[] = $default;
726
foreach ($blocks as $key => $block) {
727
// Collect all the tokens in this block which aren't at top level.
728
// We want to ignore "break", and "continue" in these blocks.
729
$lower_level = $block->selectDescendantsOfType('n_WHILE');
730
$lower_level->add($block->selectDescendantsOfType('n_DO_WHILE'));
731
$lower_level->add($block->selectDescendantsOfType('n_FOR'));
732
$lower_level->add($block->selectDescendantsOfType('n_FOREACH'));
733
$lower_level->add($block->selectDescendantsOfType('n_SWITCH'));
734
$lower_level_tokens = array();
735
foreach ($lower_level as $lower_level_block) {
736
$lower_level_tokens += $lower_level_block->getTokens();
739
// Collect all the tokens in this block which aren't in this scope
740
// (because they're inside class, function or interface declarations).
741
// We want to ignore all of these tokens.
742
$decls = $block->selectDescendantsOfType('n_FUNCTION_DECLARATION');
743
$decls->add($block->selectDescendantsOfType('n_CLASS_DECLARATION'));
744
// For completeness; these can't actually have anything.
745
$decls->add($block->selectDescendantsOfType('n_INTERFACE_DECLARATION'));
746
$different_scope_tokens = array();
747
foreach ($decls as $decl) {
748
$different_scope_tokens += $decl->getTokens();
751
$lower_level_tokens += $different_scope_tokens;
753
// Get all the trailing nonsemantic tokens, since we need to look for
754
// "fallthrough" comments past the end of the semantic block.
756
$tokens = $block->getTokens();
757
$last = end($tokens);
758
while ($last && $last = $last->getNextToken()) {
759
if ($last->isSemantic()) {
762
$tokens[$last->getTokenID()] = $last;
765
$blocks[$key] = array(
768
$different_scope_tokens,
772
foreach ($blocks as $token_lists) {
776
$different_scope_tokens) = $token_lists;
778
// Test each block (case or default statement) to see if it's OK. It's
782
// - it ends in break, return, throw, continue or exit at top level; or
783
// - it has a comment with "fallthrough" in its text.
785
// Empty blocks are OK, so we start this at `true` and only set it to
786
// false if we find a statement.
789
// Keeps track of whether the current statement is one that validates
790
// the block (break, return, throw, continue) or something else.
791
$statement_ok = false;
793
foreach ($tokens as $token_id => $token) {
794
if (!$token->isSemantic()) {
795
// Liberally match "fall" in the comment text so that comments like
796
// "fallthru", "fall through", "fallthrough", etc., are accepted.
797
if (preg_match('/fall/i', $token->getValue())) {
804
$tok_type = $token->getTypeName();
806
if ($tok_type === 'T_FUNCTION' ||
807
$tok_type === 'T_CLASS' ||
808
$tok_type === 'T_INTERFACE') {
809
// These aren't statements, but mark the block as nonempty anyway.
814
if ($tok_type === ';') {
816
$statment_ok = false;
823
if ($tok_type === 'T_BREAK' ||
824
$tok_type === 'T_CONTINUE') {
825
if (empty($lower_level_tokens[$token_id])) {
826
$statement_ok = true;
832
if ($tok_type === 'T_RETURN' ||
833
$tok_type === 'T_THROW' ||
834
$tok_type === 'T_EXIT' ||
835
($hook_obj && $hook_obj->checkSwitchToken($token))) {
836
if (empty($different_scope_tokens[$token_id])) {
837
$statement_ok = true;
845
$this->raiseLintAtToken(
847
self::LINT_IMPLICIT_FALLTHROUGH,
848
"This 'case' or 'default' has a nonempty block which does not ".
849
"end with 'break', 'continue', 'return', 'throw' or 'exit'. Did ".
850
"you forget to add one of those? If you intend to fall through, ".
851
"add a '// fallthrough' comment to silence this warning.");
857
private function lintBraceFormatting(XHPASTNode $root) {
858
foreach ($root->selectDescendantsOfType('n_STATEMENT_LIST') as $list) {
859
$tokens = $list->getTokens();
860
if (!$tokens || head($tokens)->getValue() != '{') {
863
list($before, $after) = $list->getSurroundingNonsemanticTokens();
865
$first = head($tokens);
867
// Only insert the space if we're after a closing parenthesis. If
868
// we're in a construct like "else{}", other rules will insert space
869
// after the 'else' correctly.
870
$prev = $first->getPrevToken();
871
if (!$prev || $prev->getValue() !== ')') {
875
$this->raiseLintAtToken(
877
self::LINT_BRACE_FORMATTING,
878
'Put opening braces on the same line as control statements and '.
879
'declarations, with a single space before them.',
880
' '.$first->getValue());
881
} else if (count($before) === 1) {
882
$before = reset($before);
883
if ($before->getValue() !== ' ') {
884
$this->raiseLintAtToken(
886
self::LINT_BRACE_FORMATTING,
887
'Put opening braces on the same line as control statements and '.
888
'declarations, with a single space before them.',
895
private function lintTautologicalExpressions(XHPASTNode $root) {
896
$expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
898
static $operators = array(
913
static $logical = array(
918
foreach ($expressions as $expr) {
919
$operator = $expr->getChildByIndex(1)->getConcreteString();
920
if (!empty($operators[$operator])) {
921
$left = $expr->getChildByIndex(0)->getSemanticString();
922
$right = $expr->getChildByIndex(2)->getSemanticString();
924
if ($left === $right) {
925
$this->raiseLintAtNode(
927
self::LINT_TAUTOLOGICAL_EXPRESSION,
928
'Both sides of this expression are identical, so it always '.
929
'evaluates to a constant.');
933
if (!empty($logical[$operator])) {
934
$left = $expr->getChildByIndex(0)->getSemanticString();
935
$right = $expr->getChildByIndex(2)->getSemanticString();
937
// NOTE: These will be null to indicate "could not evaluate".
938
$left = $this->evaluateStaticBoolean($left);
939
$right = $this->evaluateStaticBoolean($right);
941
if (($operator === '||' && ($left === true || $right === true)) ||
942
($operator === '&&' && ($left === false || $right === false))) {
943
$this->raiseLintAtNode(
945
self::LINT_TAUTOLOGICAL_EXPRESSION,
946
'The logical value of this expression is static. Did you forget '.
947
'to remove some debugging code?');
954
* Statically evaluate a boolean value from an XHP tree.
956
* TODO: Improve this and move it to XHPAST proper?
958
* @param string The "semantic string" of a single value.
959
* @return mixed ##true## or ##false## if the value could be evaluated
960
* statically; ##null## if static evaluation was not possible.
962
private function evaluateStaticBoolean($string) {
963
switch (strtolower($string)) {
976
protected function lintCommentSpaces(XHPASTNode $root) {
977
foreach ($root->selectTokensOfType('T_COMMENT') as $comment) {
978
$value = $comment->getValue();
979
if ($value[0] !== '#') {
981
if (preg_match('@^(/[/*]+)[^/*\s]@', $value, $match)) {
982
$this->raiseLintAtOffset(
983
$comment->getOffset(),
984
self::LINT_COMMENT_SPACING,
985
'Put space after comment start.',
994
protected function lintHashComments(XHPASTNode $root) {
995
foreach ($root->selectTokensOfType('T_COMMENT') as $comment) {
996
$value = $comment->getValue();
997
if ($value[0] !== '#') {
1001
$this->raiseLintAtOffset(
1002
$comment->getOffset(),
1003
self::LINT_COMMENT_STYLE,
1004
'Use "//" single-line comments, not "#".',
1006
(preg_match('/^#\S/', $value) ? '// ' : '//'));
1011
* Find cases where loops get nested inside each other but use the same
1012
* iterator variable. For example:
1015
* foreach ($list as $thing) {
1016
* foreach ($stuff as $thing) { // <-- Raises an error for reuse of $thing
1022
private function lintReusedIterators(XHPASTNode $root) {
1023
$used_vars = array();
1025
$for_loops = $root->selectDescendantsOfType('n_FOR');
1026
foreach ($for_loops as $for_loop) {
1029
// Find all the variables that are assigned to in the for() expression.
1030
$for_expr = $for_loop->getChildOfType(0, 'n_FOR_EXPRESSION');
1031
$bin_exprs = $for_expr->selectDescendantsOfType('n_BINARY_EXPRESSION');
1032
foreach ($bin_exprs as $bin_expr) {
1033
if ($bin_expr->getChildByIndex(1)->getConcreteString() === '=') {
1034
$var = $bin_expr->getChildByIndex(0);
1035
$var_map[$var->getConcreteString()] = $var;
1039
$used_vars[$for_loop->getID()] = $var_map;
1042
$foreach_loops = $root->selectDescendantsOfType('n_FOREACH');
1043
foreach ($foreach_loops as $foreach_loop) {
1046
$foreach_expr = $foreach_loop->getChildOftype(0, 'n_FOREACH_EXPRESSION');
1048
// We might use one or two vars, i.e. "foreach ($x as $y => $z)" or
1049
// "foreach ($x as $y)".
1050
$possible_used_vars = array(
1051
$foreach_expr->getChildByIndex(1),
1052
$foreach_expr->getChildByIndex(2),
1054
foreach ($possible_used_vars as $var) {
1055
if ($var->getTypeName() === 'n_EMPTY') {
1058
$name = $var->getConcreteString();
1059
$name = trim($name, '&'); // Get rid of ref silliness.
1060
$var_map[$name] = $var;
1063
$used_vars[$foreach_loop->getID()] = $var_map;
1066
$all_loops = $for_loops->add($foreach_loops);
1067
foreach ($all_loops as $loop) {
1068
$child_for_loops = $loop->selectDescendantsOfType('n_FOR');
1069
$child_foreach_loops = $loop->selectDescendantsOfType('n_FOREACH');
1070
$child_loops = $child_for_loops->add($child_foreach_loops);
1072
$outer_vars = $used_vars[$loop->getID()];
1073
foreach ($child_loops as $inner_loop) {
1074
$inner_vars = $used_vars[$inner_loop->getID()];
1075
$shared = array_intersect_key($outer_vars, $inner_vars);
1077
$shared_desc = implode(', ', array_keys($shared));
1078
$message = $this->raiseLintAtNode(
1079
$inner_loop->getChildByIndex(0),
1080
self::LINT_REUSED_ITERATORS,
1081
"This loop reuses iterator variables ({$shared_desc}) from an ".
1082
"outer loop. You might be clobbering the outer iterator. Change ".
1083
"the inner loop to use a different iterator name.");
1085
$locations = array();
1086
foreach ($shared as $var) {
1087
$locations[] = $this->getOtherLocation($var->getOffset());
1089
$message->setOtherLocations($locations);
1096
* Find cases where a foreach loop is being iterated using a variable
1097
* reference and the same variable is used outside of the loop without
1098
* calling unset() or reassigning the variable to another variable
1102
* foreach ($ar as &$a) {
1105
* $a = 1; // <-- Raises an error for using $a
1108
protected function lintReusedIteratorReferences(XHPASTNode $root) {
1110
$fdefs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
1111
$mdefs = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
1112
$defs = $fdefs->add($mdefs);
1114
foreach ($defs as $def) {
1116
$body = $def->getChildByIndex(5);
1117
if ($body->getTypeName() === 'n_EMPTY') {
1118
// Abstract method declaration.
1124
// Exclude uses of variables, unsets, and foreach loops
1125
// within closures - they are checked on their own
1126
$func_defs = $body->selectDescendantsOfType('n_FUNCTION_DECLARATION');
1127
foreach ($func_defs as $func_def) {
1128
$vars = $func_def->selectDescendantsOfType('n_VARIABLE');
1129
foreach ($vars as $var) {
1130
$exclude[$var->getID()] = true;
1133
$unset_lists = $func_def->selectDescendantsOfType('n_UNSET_LIST');
1134
foreach ($unset_lists as $unset_list) {
1135
$exclude[$unset_list->getID()] = true;
1138
$foreaches = $func_def->selectDescendantsOfType('n_FOREACH');
1139
foreach ($foreaches as $foreach) {
1140
$exclude[$foreach->getID()] = true;
1144
// Find all variables that are unset within the scope
1145
$unset_vars = array();
1146
$unset_lists = $body->selectDescendantsOfType('n_UNSET_LIST');
1147
foreach ($unset_lists as $unset_list) {
1148
if (isset($exclude[$unset_list->getID()])) {
1152
$unset_list_vars = $unset_list->selectDescendantsOfType('n_VARIABLE');
1153
foreach ($unset_list_vars as $var) {
1154
$concrete = $this->getConcreteVariableString($var);
1155
$unset_vars[$concrete][] = $var->getOffset();
1156
$exclude[$var->getID()] = true;
1160
// Find all reference variables in foreach expressions
1161
$reference_vars = array();
1162
$foreaches = $body->selectDescendantsOfType('n_FOREACH');
1163
foreach ($foreaches as $foreach) {
1164
if (isset($exclude[$foreach->getID()])) {
1168
$foreach_expr = $foreach->getChildOfType(0, 'n_FOREACH_EXPRESSION');
1169
$var = $foreach_expr->getChildByIndex(2);
1170
if ($var->getTypeName() !== 'n_VARIABLE_REFERENCE') {
1174
$reference = $var->getChildByIndex(0);
1175
if ($reference->getTypeName() !== 'n_VARIABLE') {
1179
$reference_name = $this->getConcreteVariableString($reference);
1180
$reference_vars[$reference_name][] = $reference->getOffset();
1181
$exclude[$reference->getID()] = true;
1183
// Exclude uses of the reference variable within the foreach loop
1184
$foreach_vars = $foreach->selectDescendantsOfType('n_VARIABLE');
1185
foreach ($foreach_vars as $var) {
1186
$name = $this->getConcreteVariableString($var);
1187
if ($name === $reference_name) {
1188
$exclude[$var->getID()] = true;
1193
// Allow usage if the reference variable is assigned to another
1194
// reference variable
1195
$binary = $body->selectDescendantsOfType('n_BINARY_EXPRESSION');
1196
foreach ($binary as $expr) {
1197
if ($expr->getChildByIndex(1)->getConcreteString() !== '=') {
1200
$lval = $expr->getChildByIndex(0);
1201
if ($lval->getTypeName() !== 'n_VARIABLE') {
1204
$rval = $expr->getChildByIndex(2);
1205
if ($rval->getTypeName() !== 'n_VARIABLE_REFERENCE') {
1209
// Counts as unsetting a variable
1210
$concrete = $this->getConcreteVariableString($lval);
1211
$unset_vars[$concrete][] = $lval->getOffset();
1212
$exclude[$lval->getID()] = true;
1215
$all_vars = array();
1216
$all = $body->selectDescendantsOfType('n_VARIABLE');
1217
foreach ($all as $var) {
1218
if (isset($exclude[$var->getID()])) {
1222
$name = $this->getConcreteVariableString($var);
1224
if (!isset($reference_vars[$name])) {
1228
// Find the closest reference offset to this variable
1229
$reference_offset = null;
1230
foreach ($reference_vars[$name] as $offset) {
1231
if ($offset < $var->getOffset()) {
1232
$reference_offset = $offset;
1237
if (!$reference_offset) {
1241
// Check if an unset exists between reference and usage of this
1244
if (isset($unset_vars[$name])) {
1245
foreach ($unset_vars[$name] as $unset_offset) {
1246
if ($unset_offset > $reference_offset &&
1247
$unset_offset < $var->getOffset()) {
1254
$this->raiseLintAtNode(
1256
self::LINT_REUSED_ITERATOR_REFERENCE,
1257
'This variable was used already as a by-reference iterator '.
1258
'variable. Such variables survive outside the foreach loop, '.
1266
protected function lintVariableVariables(XHPASTNode $root) {
1267
$vvars = $root->selectDescendantsOfType('n_VARIABLE_VARIABLE');
1268
foreach ($vvars as $vvar) {
1269
$this->raiseLintAtNode(
1271
self::LINT_VARIABLE_VARIABLE,
1272
'Rewrite this code to use an array. Variable variables are unclear '.
1273
'and hinder static analysis.');
1277
private function lintUndeclaredVariables(XHPASTNode $root) {
1278
// These things declare variables in a function:
1279
// Explicit parameters
1281
// Assignment via list()
1289
// These things make lexical scope unknowable:
1291
// Assignment to variable variables ($$x)
1292
// Global with variable variables
1294
// These things don't count as "using" a variable:
1297
// Static class variables
1299
// The general approach here is to find each function/method declaration,
1302
// 1. Identify all the variable declarations, and where they first occur
1303
// in the function/method declaration.
1304
// 2. Identify all the uses that don't really count (as above).
1305
// 3. Everything else must be a use of a variable.
1306
// 4. For each variable, check if any uses occur before the declaration
1307
// and warn about them.
1309
// We also keep track of where lexical scope becomes unknowable (e.g.,
1310
// because the function calls extract() or uses dynamic variables,
1311
// preventing us from keeping track of which variables are defined) so we
1312
// can stop issuing warnings after that.
1314
// TODO: Support functions defined inside other functions which is commonly
1315
// used with anonymous functions.
1317
$fdefs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
1318
$mdefs = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
1319
$defs = $fdefs->add($mdefs);
1321
foreach ($defs as $def) {
1323
// We keep track of the first offset where scope becomes unknowable, and
1324
// silence any warnings after that. Default it to INT_MAX so we can min()
1325
// it later to keep track of the first problem we encounter.
1326
$scope_destroyed_at = PHP_INT_MAX;
1328
$declarations = array(
1330
) + array_fill_keys($this->getSuperGlobalNames(), 0);
1331
$declaration_tokens = array();
1332
$exclude_tokens = array();
1335
// First up, find all the different kinds of declarations, as explained
1336
// above. Put the tokens into the $vars array.
1338
$param_list = $def->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST');
1339
$param_vars = $param_list->selectDescendantsOfType('n_VARIABLE');
1340
foreach ($param_vars as $var) {
1344
// This is PHP5.3 closure syntax: function () use ($x) {};
1345
$lexical_vars = $def
1346
->getChildByIndex(4)
1347
->selectDescendantsOfType('n_VARIABLE');
1348
foreach ($lexical_vars as $var) {
1352
$body = $def->getChildByIndex(5);
1353
if ($body->getTypeName() === 'n_EMPTY') {
1354
// Abstract method declaration.
1358
$static_vars = $body
1359
->selectDescendantsOfType('n_STATIC_DECLARATION')
1360
->selectDescendantsOfType('n_VARIABLE');
1361
foreach ($static_vars as $var) {
1366
$global_vars = $body
1367
->selectDescendantsOfType('n_GLOBAL_DECLARATION_LIST');
1368
foreach ($global_vars as $var_list) {
1369
foreach ($var_list->getChildren() as $var) {
1370
if ($var->getTypeName() === 'n_VARIABLE') {
1373
// Dynamic global variable, i.e. "global $$x;".
1374
$scope_destroyed_at = min($scope_destroyed_at, $var->getOffset());
1375
// An error is raised elsewhere, no need to raise here.
1380
// Include "catch (Exception $ex)", but not variables in the body of the
1382
$catches = $body->selectDescendantsOfType('n_CATCH');
1383
foreach ($catches as $catch) {
1384
$vars[] = $catch->getChildOfType(1, 'n_VARIABLE');
1387
$binary = $body->selectDescendantsOfType('n_BINARY_EXPRESSION');
1388
foreach ($binary as $expr) {
1389
if ($expr->getChildByIndex(1)->getConcreteString() !== '=') {
1392
$lval = $expr->getChildByIndex(0);
1393
if ($lval->getTypeName() === 'n_VARIABLE') {
1395
} else if ($lval->getTypeName() === 'n_LIST') {
1396
// Recursivey grab everything out of list(), since the grammar
1397
// permits list() to be nested. Also note that list() is ONLY valid
1398
// as an lval assignments, so we could safely lift this out of the
1399
// n_BINARY_EXPRESSION branch.
1400
$assign_vars = $lval->selectDescendantsOfType('n_VARIABLE');
1401
foreach ($assign_vars as $var) {
1406
if ($lval->getTypeName() === 'n_VARIABLE_VARIABLE') {
1407
$scope_destroyed_at = min($scope_destroyed_at, $lval->getOffset());
1408
// No need to raise here since we raise an error elsewhere.
1412
$calls = $body->selectDescendantsOfType('n_FUNCTION_CALL');
1413
foreach ($calls as $call) {
1414
$name = strtolower($call->getChildByIndex(0)->getConcreteString());
1416
if ($name === 'empty' || $name === 'isset') {
1418
->getChildOfType(1, 'n_CALL_PARAMETER_LIST')
1419
->selectDescendantsOfType('n_VARIABLE');
1420
foreach ($params as $var) {
1421
$exclude_tokens[$var->getID()] = true;
1425
if ($name !== 'extract') {
1428
$scope_destroyed_at = min($scope_destroyed_at, $call->getOffset());
1429
$this->raiseLintAtNode(
1431
self::LINT_EXTRACT_USE,
1432
'Avoid extract(). It is confusing and hinders static analysis.');
1435
// Now we have every declaration except foreach(), handled below. Build
1436
// two maps, one which just keeps track of which tokens are part of
1437
// declarations ($declaration_tokens) and one which has the first offset
1438
// where a variable is declared ($declarations).
1440
foreach ($vars as $var) {
1441
$concrete = $this->getConcreteVariableString($var);
1442
$declarations[$concrete] = min(
1443
idx($declarations, $concrete, PHP_INT_MAX),
1445
$declaration_tokens[$var->getID()] = true;
1448
// Excluded tokens are ones we don't "count" as being used, described
1449
// above. Put them into $exclude_tokens.
1451
$class_statics = $body
1452
->selectDescendantsOfType('n_CLASS_STATIC_ACCESS');
1453
$class_static_vars = $class_statics
1454
->selectDescendantsOfType('n_VARIABLE');
1455
foreach ($class_static_vars as $var) {
1456
$exclude_tokens[$var->getID()] = true;
1460
// Find all the variables in scope, and figure out where they are used.
1461
// We want to find foreach() iterators which are both declared before and
1462
// used after the foreach() loop.
1466
$all_vars = $body->selectDescendantsOfType('n_VARIABLE');
1469
// NOTE: $all_vars is not a real array so we can't unset() it.
1470
foreach ($all_vars as $var) {
1472
// Be strict since it's easier; we don't let you reuse an iterator you
1473
// declared before a loop after the loop, even if you're just assigning
1476
$concrete = $this->getConcreteVariableString($var);
1477
$uses[$concrete][$var->getID()] = $var->getOffset();
1479
if (isset($declaration_tokens[$var->getID()])) {
1480
// We know this is part of a declaration, so it's fine.
1483
if (isset($exclude_tokens[$var->getID()])) {
1484
// We know this is part of isset() or similar, so it's fine.
1488
$all[$var->getOffset()] = $concrete;
1492
// Do foreach() last, we want to handle implicit redeclaration of a
1493
// variable already in scope since this probably means we're ovewriting a
1496
// NOTE: Processing foreach expressions in order allows programs which
1497
// reuse iterator variables in other foreach() loops -- this is fine. We
1498
// have a separate warning to prevent nested loops from reusing the same
1501
$foreaches = $body->selectDescendantsOfType('n_FOREACH');
1502
$all_foreach_vars = array();
1503
foreach ($foreaches as $foreach) {
1504
$foreach_expr = $foreach->getChildOfType(0, 'n_FOREACH_EXPRESSION');
1506
$foreach_vars = array();
1508
// Determine the end of the foreach() loop.
1509
$foreach_tokens = $foreach->getTokens();
1510
$last_token = end($foreach_tokens);
1511
$foreach_end = $last_token->getOffset();
1513
$key_var = $foreach_expr->getChildByIndex(1);
1514
if ($key_var->getTypeName() === 'n_VARIABLE') {
1515
$foreach_vars[] = $key_var;
1518
$value_var = $foreach_expr->getChildByIndex(2);
1519
if ($value_var->getTypeName() === 'n_VARIABLE') {
1520
$foreach_vars[] = $value_var;
1522
// The root-level token may be a reference, as in:
1523
// foreach ($a as $b => &$c) { ... }
1524
// Reach into the n_VARIABLE_REFERENCE node to grab the n_VARIABLE
1526
$var = $value_var->getChildByIndex(0);
1527
if ($var->getTypeName() === 'n_VARIABLE_VARIABLE') {
1528
$var = $var->getChildByIndex(0);
1530
$foreach_vars[] = $var;
1533
// Remove all uses of the iterators inside of the foreach() loop from
1536
foreach ($foreach_vars as $var) {
1537
$concrete = $this->getConcreteVariableString($var);
1538
$offset = $var->getOffset();
1540
foreach ($uses[$concrete] as $id => $use_offset) {
1541
if (($use_offset >= $offset) && ($use_offset < $foreach_end)) {
1542
unset($uses[$concrete][$id]);
1546
$all_foreach_vars[] = $var;
1550
foreach ($all_foreach_vars as $var) {
1551
$concrete = $this->getConcreteVariableString($var);
1552
$offset = $var->getOffset();
1554
// If a variable was declared before a foreach() and is used after
1555
// it, raise a message.
1557
if (isset($declarations[$concrete])) {
1558
if ($declarations[$concrete] < $offset) {
1559
if (!empty($uses[$concrete]) &&
1560
max($uses[$concrete]) > $offset) {
1561
$message = $this->raiseLintAtNode(
1563
self::LINT_REUSED_AS_ITERATOR,
1564
'This iterator variable is a previously declared local '.
1565
'variable. To avoid overwriting locals, do not reuse them '.
1566
'as iterator variables.');
1567
$message->setOtherLocations(array(
1568
$this->getOtherLocation($declarations[$concrete]),
1569
$this->getOtherLocation(max($uses[$concrete])),
1575
// This is a declaration, exclude it from the "declare variables prior
1576
// to use" check below.
1577
unset($all[$var->getOffset()]);
1582
// Now rebuild declarations to include foreach().
1584
foreach ($vars as $var) {
1585
$concrete = $this->getConcreteVariableString($var);
1586
$declarations[$concrete] = min(
1587
idx($declarations, $concrete, PHP_INT_MAX),
1589
$declaration_tokens[$var->getID()] = true;
1592
foreach (array('n_STRING_SCALAR', 'n_HEREDOC') as $type) {
1593
foreach ($body->selectDescendantsOfType($type) as $string) {
1594
foreach ($string->getStringVariables() as $offset => $var) {
1595
$all[$string->getOffset() + $offset - 1] = '$'.$var;
1600
// Issue a warning for every variable token, unless it appears in a
1601
// declaration, we know about a prior declaration, we have explicitly
1602
// exlcuded it, or scope has been made unknowable before it appears.
1604
$issued_warnings = array();
1605
foreach ($all as $offset => $concrete) {
1606
if ($offset >= $scope_destroyed_at) {
1607
// This appears after an extract() or $$var so we have no idea
1608
// whether it's legitimate or not. We raised a harshly-worded warning
1609
// when scope was made unknowable, so just ignore anything we can't
1613
if ($offset >= idx($declarations, $concrete, PHP_INT_MAX)) {
1614
// The use appears after the variable is declared, so it's fine.
1617
if (!empty($issued_warnings[$concrete])) {
1618
// We've already issued a warning for this variable so we don't need
1619
// to issue another one.
1622
$this->raiseLintAtOffset(
1624
self::LINT_UNDECLARED_VARIABLE,
1625
'Declare variables prior to use (even if you are passing them '.
1626
'as reference parameters). You may have misspelled this '.
1629
$issued_warnings[$concrete] = true;
1634
private function getConcreteVariableString(XHPASTNode $var) {
1635
$concrete = $var->getConcreteString();
1636
// Strip off curly braces as in $obj->{$property}.
1637
$concrete = trim($concrete, '{}');
1641
private function lintPHPTagUse(XHPASTNode $root) {
1642
$tokens = $root->getTokens();
1643
foreach ($tokens as $token) {
1644
if ($token->getTypeName() === 'T_OPEN_TAG') {
1645
if (trim($token->getValue()) === '<?') {
1646
$this->raiseLintAtToken(
1648
self::LINT_PHP_SHORT_TAG,
1649
'Use the full form of the PHP open tag, "<?php".',
1653
} else if ($token->getTypeName() === 'T_OPEN_TAG_WITH_ECHO') {
1654
$this->raiseLintAtToken(
1656
self::LINT_PHP_ECHO_TAG,
1657
'Avoid the PHP echo short form, "<?=".');
1660
if (!preg_match('/^#!/', $token->getValue())) {
1661
$this->raiseLintAtToken(
1663
self::LINT_PHP_OPEN_TAG,
1664
'PHP files should start with "<?php", which may be preceded by '.
1665
'a "#!" line for scripts.');
1671
foreach ($root->selectTokensOfType('T_CLOSE_TAG') as $token) {
1672
$this->raiseLintAtToken(
1674
self::LINT_PHP_CLOSE_TAG,
1675
'Do not use the PHP closing tag, "?>".');
1679
private function lintNamingConventions(XHPASTNode $root) {
1680
// We're going to build up a list of <type, name, token, error> tuples
1681
// and then try to instantiate a hook class which has the opportunity to
1685
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
1686
foreach ($classes as $class) {
1687
$name_token = $class->getChildByIndex(1);
1688
$name_string = $name_token->getConcreteString();
1694
ArcanistXHPASTLintNamingHook::isUpperCamelCase($name_string)
1696
: 'Follow naming conventions: classes should be named using '.
1701
$ifaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION');
1702
foreach ($ifaces as $iface) {
1703
$name_token = $iface->getChildByIndex(1);
1704
$name_string = $name_token->getConcreteString();
1709
ArcanistXHPASTLintNamingHook::isUpperCamelCase($name_string)
1711
: 'Follow naming conventions: interfaces should be named using '.
1717
$functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
1718
foreach ($functions as $function) {
1719
$name_token = $function->getChildByIndex(2);
1720
if ($name_token->getTypeName() === 'n_EMPTY') {
1724
$name_string = $name_token->getConcreteString();
1729
ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores(
1730
ArcanistXHPASTLintNamingHook::stripPHPFunction($name_string))
1732
: 'Follow naming conventions: functions should be named using '.
1733
'lowercase_with_underscores.',
1738
$methods = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
1739
foreach ($methods as $method) {
1740
$name_token = $method->getChildByIndex(2);
1741
$name_string = $name_token->getConcreteString();
1746
ArcanistXHPASTLintNamingHook::isLowerCamelCase(
1747
ArcanistXHPASTLintNamingHook::stripPHPFunction($name_string))
1749
: 'Follow naming conventions: methods should be named using '.
1754
$param_tokens = array();
1756
$params = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER_LIST');
1757
foreach ($params as $param_list) {
1758
foreach ($param_list->getChildren() as $param) {
1759
$name_token = $param->getChildByIndex(1);
1760
if ($name_token->getTypeName() === 'n_VARIABLE_REFERENCE') {
1761
$name_token = $name_token->getChildOfType(0, 'n_VARIABLE');
1763
$param_tokens[$name_token->getID()] = true;
1764
$name_string = $name_token->getConcreteString();
1770
ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores(
1771
ArcanistXHPASTLintNamingHook::stripPHPVariable($name_string))
1773
: 'Follow naming conventions: parameters should be named using '.
1774
'lowercase_with_underscores.',
1780
$constants = $root->selectDescendantsOfType(
1781
'n_CLASS_CONSTANT_DECLARATION_LIST');
1782
foreach ($constants as $constant_list) {
1783
foreach ($constant_list->getChildren() as $constant) {
1784
$name_token = $constant->getChildByIndex(0);
1785
$name_string = $name_token->getConcreteString();
1790
ArcanistXHPASTLintNamingHook::isUppercaseWithUnderscores($name_string)
1792
: 'Follow naming conventions: class constants should be named '.
1793
'using UPPERCASE_WITH_UNDERSCORES.',
1798
$member_tokens = array();
1800
$props = $root->selectDescendantsOfType('n_CLASS_MEMBER_DECLARATION_LIST');
1801
foreach ($props as $prop_list) {
1802
foreach ($prop_list->getChildren() as $token_id => $prop) {
1803
if ($prop->getTypeName() === 'n_CLASS_MEMBER_MODIFIER_LIST') {
1807
$name_token = $prop->getChildByIndex(0);
1808
$member_tokens[$name_token->getID()] = true;
1810
$name_string = $name_token->getConcreteString();
1815
ArcanistXHPASTLintNamingHook::isLowerCamelCase(
1816
ArcanistXHPASTLintNamingHook::stripPHPVariable($name_string))
1818
: 'Follow naming conventions: class properties should be named '.
1819
'using lowerCamelCase.',
1824
$superglobal_map = array_fill_keys(
1825
$this->getSuperGlobalNames(),
1829
$fdefs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
1830
$mdefs = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
1831
$defs = $fdefs->add($mdefs);
1833
foreach ($defs as $def) {
1834
$globals = $def->selectDescendantsOfType('n_GLOBAL_DECLARATION_LIST');
1835
$globals = $globals->selectDescendantsOfType('n_VARIABLE');
1837
$globals_map = array();
1838
foreach ($globals as $global) {
1839
$global_string = $global->getConcreteString();
1840
$globals_map[$global_string] = true;
1846
// No advice for globals, but hooks have an option to provide some.
1851
// Exclude access of static properties, since lint will be raised at
1852
// their declaration if they're invalid and they may not conform to
1853
// variable rules. This is slightly overbroad (includes the entire
1854
// rhs of a "Class::..." token) to cover cases like "Class:$x[0]". These
1855
// variables are simply made exempt from naming conventions.
1856
$exclude_tokens = array();
1857
$statics = $def->selectDescendantsOfType('n_CLASS_STATIC_ACCESS');
1858
foreach ($statics as $static) {
1859
$rhs = $static->getChildByIndex(1);
1860
$rhs_vars = $def->selectDescendantsOfType('n_VARIABLE');
1861
foreach ($rhs_vars as $var) {
1862
$exclude_tokens[$var->getID()] = true;
1866
$vars = $def->selectDescendantsOfType('n_VARIABLE');
1867
foreach ($vars as $token_id => $var) {
1868
if (isset($member_tokens[$token_id])) {
1871
if (isset($param_tokens[$token_id])) {
1874
if (isset($exclude_tokens[$token_id])) {
1878
$var_string = $var->getConcreteString();
1880
// Awkward artifact of "$o->{$x}".
1881
$var_string = trim($var_string, '{}');
1883
if (isset($superglobal_map[$var_string])) {
1886
if (isset($globals_map[$var_string])) {
1894
ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores(
1895
ArcanistXHPASTLintNamingHook::stripPHPVariable($var_string))
1897
: 'Follow naming conventions: variables should be named using '.
1898
'lowercase_with_underscores.',
1903
$engine = $this->getEngine();
1904
$working_copy = $engine->getWorkingCopy();
1906
if ($working_copy) {
1907
// If a naming hook is configured, give it a chance to override the
1908
// default results for all the symbol names.
1909
$hook_class = $this->naminghook
1911
: $working_copy->getProjectConfig('lint.xhpast.naminghook');
1913
$hook_obj = newv($hook_class, array());
1914
foreach ($names as $k => $name_attrs) {
1915
list($type, $name, $token, $default) = $name_attrs;
1916
$result = $hook_obj->lintSymbolName($type, $name, $default);
1917
$names[$k][3] = $result;
1922
// Raise anything we're left with.
1923
foreach ($names as $k => $name_attrs) {
1924
list($type, $name, $token, $result) = $name_attrs;
1926
$this->raiseLintAtNode(
1928
self::LINT_NAMING_CONVENTIONS,
1934
private function lintSurpriseConstructors(XHPASTNode $root) {
1935
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
1936
foreach ($classes as $class) {
1937
$class_name = $class->getChildByIndex(1)->getConcreteString();
1938
$methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION');
1939
foreach ($methods as $method) {
1940
$method_name_token = $method->getChildByIndex(2);
1941
$method_name = $method_name_token->getConcreteString();
1942
if (strtolower($class_name) === strtolower($method_name)) {
1943
$this->raiseLintAtNode(
1945
self::LINT_IMPLICIT_CONSTRUCTOR,
1946
'Name constructors __construct() explicitly. This method is a '.
1947
'constructor because it has the same name as the class it is '.
1954
private function lintParenthesesShouldHugExpressions(XHPASTNode $root) {
1955
$calls = $root->selectDescendantsOfType('n_CALL_PARAMETER_LIST');
1956
$controls = $root->selectDescendantsOfType('n_CONTROL_CONDITION');
1957
$fors = $root->selectDescendantsOfType('n_FOR_EXPRESSION');
1958
$foreach = $root->selectDescendantsOfType('n_FOREACH_EXPRESSION');
1959
$decl = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER_LIST');
1961
$all_paren_groups = $calls
1966
foreach ($all_paren_groups as $group) {
1967
$tokens = $group->getTokens();
1969
$token_o = array_shift($tokens);
1970
$token_c = array_pop($tokens);
1971
if ($token_o->getTypeName() !== '(') {
1972
throw new Exception('Expected open paren!');
1974
if ($token_c->getTypeName() !== ')') {
1975
throw new Exception('Expected close paren!');
1978
$nonsem_o = $token_o->getNonsemanticTokensAfter();
1979
$nonsem_c = $token_c->getNonsemanticTokensBefore();
1987
$string_o = implode('', mpull($nonsem_o, 'getValue'));
1988
if (preg_match('/^[ ]+$/', $string_o)) {
1989
$raise[] = array($nonsem_o, $string_o);
1992
if ($nonsem_o !== $nonsem_c) {
1993
$string_c = implode('', mpull($nonsem_c, 'getValue'));
1994
if (preg_match('/^[ ]+$/', $string_c)) {
1995
$raise[] = array($nonsem_c, $string_c);
1999
foreach ($raise as $warning) {
2000
list($tokens, $string) = $warning;
2001
$this->raiseLintAtOffset(
2002
reset($tokens)->getOffset(),
2003
self::LINT_PARENTHESES_SPACING,
2004
'Parentheses should hug their contents.',
2011
private function lintSpaceAfterControlStatementKeywords(XHPASTNode $root) {
2012
foreach ($root->getTokens() as $id => $token) {
2013
switch ($token->getTypeName()) {
2021
$after = $token->getNonsemanticTokensAfter();
2022
if (empty($after)) {
2023
$this->raiseLintAtToken(
2025
self::LINT_CONTROL_STATEMENT_SPACING,
2026
'Convention: put a space after control statements.',
2027
$token->getValue().' ');
2028
} else if (count($after) === 1) {
2029
$space = head($after);
2031
// If we have an else clause with braces, $space may not be
2032
// a single white space. e.g.,
2036
// else // <- $space is not " " but "\n ".
2039
// We just require it starts with either a whitespace or a newline.
2040
if ($token->getTypeName() === 'T_ELSE' ||
2041
$token->getTypeName() === 'T_DO') {
2045
if ($space->isAnyWhitespace() && $space->getValue() !== ' ') {
2046
$this->raiseLintAtToken(
2048
self::LINT_CONTROL_STATEMENT_SPACING,
2049
'Convention: put a single space after control statements.',
2058
private function lintSpaceAroundBinaryOperators(XHPASTNode $root) {
2059
$expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
2060
foreach ($expressions as $expression) {
2061
$operator = $expression->getChildByIndex(1);
2062
$operator_value = $operator->getConcreteString();
2063
list($before, $after) = $operator->getSurroundingNonsemanticTokens();
2066
if (empty($before) && empty($after)) {
2067
$replace = " {$operator_value} ";
2068
} else if (empty($before)) {
2069
$replace = " {$operator_value}";
2070
} else if (empty($after)) {
2071
$replace = "{$operator_value} ";
2074
if ($replace !== null) {
2075
$this->raiseLintAtNode(
2077
self::LINT_BINARY_EXPRESSION_SPACING,
2078
'Convention: logical and arithmetic operators should be '.
2079
'surrounded by whitespace.',
2084
$tokens = $root->selectTokensOfType(',');
2085
foreach ($tokens as $token) {
2086
$next = $token->getNextToken();
2087
switch ($next->getTypeName()) {
2089
case 'T_WHITESPACE':
2092
$this->raiseLintAtToken(
2094
self::LINT_BINARY_EXPRESSION_SPACING,
2095
'Convention: comma should be followed by space.',
2101
$tokens = $root->selectTokensOfType('T_DOUBLE_ARROW');
2102
foreach ($tokens as $token) {
2103
$prev = $token->getPrevToken();
2104
$next = $token->getNextToken();
2106
$prev_type = $prev->getTypeName();
2107
$next_type = $next->getTypeName();
2109
$prev_space = ($prev_type === 'T_WHITESPACE');
2110
$next_space = ($next_type === 'T_WHITESPACE');
2113
if (!$prev_space && !$next_space) {
2115
} else if ($prev_space && !$next_space) {
2117
} else if (!$prev_space && $next_space) {
2121
if ($replace !== null) {
2122
$this->raiseLintAtToken(
2124
self::LINT_BINARY_EXPRESSION_SPACING,
2125
'Convention: double arrow should be surrounded by whitespace.',
2130
$parameters = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER');
2131
foreach ($parameters as $parameter) {
2132
if ($parameter->getChildByIndex(2)->getTypeName() == 'n_EMPTY') {
2136
$operator = head($parameter->selectTokensOfType('='));
2137
$before = $operator->getNonsemanticTokensBefore();
2138
$after = $operator->getNonsemanticTokensAfter();
2141
if (empty($before) && empty($after)) {
2143
} else if (empty($before)) {
2145
} else if (empty($after)) {
2149
if ($replace !== null) {
2150
$this->raiseLintAtToken(
2152
self::LINT_BINARY_EXPRESSION_SPACING,
2153
'Convention: logical and arithmetic operators should be '.
2154
'surrounded by whitespace.',
2160
private function lintSpaceAroundConcatenationOperators(XHPASTNode $root) {
2161
$tokens = $root->selectTokensOfType('.');
2162
foreach ($tokens as $token) {
2163
$prev = $token->getPrevToken();
2164
$next = $token->getNextToken();
2166
foreach (array('prev' => $prev, 'next' => $next) as $wtoken) {
2167
if ($wtoken->getTypeName() !== 'T_WHITESPACE') {
2171
$value = $wtoken->getValue();
2172
if (strpos($value, "\n") !== false) {
2173
// If the whitespace has a newline, it's conventional.
2177
$next = $wtoken->getNextToken();
2178
if ($next && $next->getTypeName() === 'T_COMMENT') {
2182
$this->raiseLintAtToken(
2184
self::LINT_CONCATENATION_OPERATOR,
2185
'Convention: no spaces around "." (string concatenation) operator.',
2191
private function lintDynamicDefines(XHPASTNode $root) {
2192
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
2193
foreach ($calls as $call) {
2194
$name = $call->getChildByIndex(0)->getConcreteString();
2195
if (strtolower($name) === 'define') {
2196
$parameter_list = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST');
2197
$defined = $parameter_list->getChildByIndex(0);
2198
if (!$defined->isStaticScalar()) {
2199
$this->raiseLintAtNode(
2201
self::LINT_DYNAMIC_DEFINE,
2202
'First argument to define() must be a string literal.');
2208
private function lintUseOfThisInStaticMethods(XHPASTNode $root) {
2209
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
2210
foreach ($classes as $class) {
2211
$methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION');
2212
foreach ($methods as $method) {
2214
$attributes = $method
2215
->getChildByIndex(0, 'n_METHOD_MODIFIER_LIST')
2216
->selectDescendantsOfType('n_STRING');
2218
$method_is_static = false;
2219
$method_is_abstract = false;
2220
foreach ($attributes as $attribute) {
2221
if (strtolower($attribute->getConcreteString()) === 'static') {
2222
$method_is_static = true;
2224
if (strtolower($attribute->getConcreteString()) === 'abstract') {
2225
$method_is_abstract = true;
2229
if ($method_is_abstract) {
2233
if (!$method_is_static) {
2237
$body = $method->getChildOfType(5, 'n_STATEMENT_LIST');
2239
$variables = $body->selectDescendantsOfType('n_VARIABLE');
2240
foreach ($variables as $variable) {
2241
if ($method_is_static &&
2242
strtolower($variable->getConcreteString()) === '$this') {
2243
$this->raiseLintAtNode(
2245
self::LINT_STATIC_THIS,
2246
'You can not reference "$this" inside a static method.');
2254
* preg_quote() takes two arguments, but the second one is optional because
2255
* it is possible to use (), [] or {} as regular expression delimiters. If
2256
* you don't pass a second argument, you're probably going to get something
2259
private function lintPregQuote(XHPASTNode $root) {
2260
$function_calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
2261
foreach ($function_calls as $call) {
2262
$name = $call->getChildByIndex(0)->getConcreteString();
2263
if (strtolower($name) === 'preg_quote') {
2264
$parameter_list = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST');
2265
if (count($parameter_list->getChildren()) !== 2) {
2266
$this->raiseLintAtNode(
2268
self::LINT_PREG_QUOTE_MISUSE,
2269
'If you use pattern delimiters that require escaping (such as //, '.
2270
'but not ()) then you should pass two arguments to preg_quote(), '.
2271
'so that preg_quote() knows which delimiter to escape.');
2278
* Exit is parsed as an expression, but using it as such is almost always
2279
* wrong. That is, this is valid:
2281
* strtoupper(33 * exit - 6);
2283
* When exit is used as an expression, it causes the program to terminate with
2284
* exit code 0. This is likely not what is intended; these statements have
2285
* different effects:
2290
* The former exits with a failure code, the latter with a success code!
2292
private function lintExitExpressions(XHPASTNode $root) {
2293
$unaries = $root->selectDescendantsOfType('n_UNARY_PREFIX_EXPRESSION');
2294
foreach ($unaries as $unary) {
2295
$operator = $unary->getChildByIndex(0)->getConcreteString();
2296
if (strtolower($operator) === 'exit') {
2297
if ($unary->getParentNode()->getTypeName() !== 'n_STATEMENT') {
2298
$this->raiseLintAtNode(
2300
self::LINT_EXIT_EXPRESSION,
2301
'Use exit as a statement, not an expression.');
2307
private function lintArrayIndexWhitespace(XHPASTNode $root) {
2308
$indexes = $root->selectDescendantsOfType('n_INDEX_ACCESS');
2309
foreach ($indexes as $index) {
2310
$tokens = $index->getChildByIndex(0)->getTokens();
2311
$last = array_pop($tokens);
2312
$trailing = $last->getNonsemanticTokensAfter();
2313
$trailing_text = implode('', mpull($trailing, 'getValue'));
2314
if (preg_match('/^ +$/', $trailing_text)) {
2315
$this->raiseLintAtOffset(
2316
$last->getOffset() + strlen($last->getValue()),
2317
self::LINT_ARRAY_INDEX_SPACING,
2318
'Convention: no spaces before index access.',
2325
private function lintTODOComments(XHPASTNode $root) {
2326
$comments = $root->selectTokensOfType('T_COMMENT') +
2327
$root->selectTokensOfType('T_DOC_COMMENT');
2329
foreach ($comments as $token) {
2330
$value = $token->getValue();
2331
if ($token->getTypeName() === 'T_DOC_COMMENT') {
2332
$regex = '/(TODO|@todo)/';
2338
$preg = preg_match_all(
2342
PREG_OFFSET_CAPTURE);
2344
foreach ($matches[0] as $match) {
2345
list($string, $offset) = $match;
2346
$this->raiseLintAtOffset(
2347
$token->getOffset() + $offset,
2348
self::LINT_TODO_COMMENT,
2349
'This comment has a TODO.',
2356
* Lint that if the file declares exactly one interface or class,
2357
* the name of the file matches the name of the class,
2358
* unless the classname is funky like an XHP element.
2360
private function lintPrimaryDeclarationFilenameMatch(XHPASTNode $root) {
2361
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
2362
$interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION');
2364
if (count($classes) + count($interfaces) !== 1) {
2368
$declarations = count($classes) ? $classes : $interfaces;
2369
$declarations->rewind();
2370
$declaration = $declarations->current();
2372
$decl_name = $declaration->getChildByIndex(1);
2373
$decl_string = $decl_name->getConcreteString();
2375
// Exclude strangely named classes, e.g. XHP tags.
2376
if (!preg_match('/^\w+$/', $decl_string)) {
2380
$rename = $decl_string.'.php';
2382
$path = $this->getActivePath();
2383
$filename = basename($path);
2385
if ($rename === $filename) {
2389
$this->raiseLintAtNode(
2391
self::LINT_CLASS_FILENAME_MISMATCH,
2392
"The name of this file differs from the name of the class or interface ".
2393
"it declares. Rename the file to '{$rename}'.");
2396
private function lintPlusOperatorOnStrings(XHPASTNode $root) {
2397
$binops = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
2398
foreach ($binops as $binop) {
2399
$op = $binop->getChildByIndex(1);
2400
if ($op->getConcreteString() !== '+') {
2404
$left = $binop->getChildByIndex(0);
2405
$right = $binop->getChildByIndex(2);
2406
if (($left->getTypeName() === 'n_STRING_SCALAR') ||
2407
($right->getTypeName() === 'n_STRING_SCALAR')) {
2408
$this->raiseLintAtNode(
2410
self::LINT_PLUS_OPERATOR_ON_STRINGS,
2411
"In PHP, '.' is the string concatenation operator, not '+'. This ".
2412
"expression uses '+' with a string literal as an operand.");
2418
* Finds duplicate keys in array initializers, as in
2419
* array(1 => 'anything', 1 => 'foo'). Since the first entry is ignored,
2420
* this is almost certainly an error.
2422
private function lintDuplicateKeysInArray(XHPASTNode $root) {
2423
$array_literals = $root->selectDescendantsOfType('n_ARRAY_LITERAL');
2424
foreach ($array_literals as $array_literal) {
2425
$nodes_by_key = array();
2426
$keys_warn = array();
2427
$list_node = $array_literal->getChildByIndex(0);
2428
foreach ($list_node->getChildren() as $array_entry) {
2429
$key_node = $array_entry->getChildByIndex(0);
2431
switch ($key_node->getTypeName()) {
2432
case 'n_STRING_SCALAR':
2433
case 'n_NUMERIC_SCALAR':
2434
// Scalars: array(1 => 'v1', '1' => 'v2');
2435
$key = 'scalar:'.(string)$key_node->evalStatic();
2438
case 'n_SYMBOL_NAME':
2440
case 'n_CLASS_STATIC_ACCESS':
2441
// Constants: array(CONST => 'v1', CONST => 'v2');
2442
// Variables: array($a => 'v1', $a => 'v2');
2443
// Class constants and vars: array(C::A => 'v1', C::A => 'v2');
2444
$key = $key_node->getTypeName().':'.$key_node->getConcreteString();
2452
if ($key !== null) {
2453
if (isset($nodes_by_key[$key])) {
2454
$keys_warn[$key] = true;
2456
$nodes_by_key[$key][] = $key_node;
2460
foreach ($keys_warn as $key => $_) {
2461
$node = array_pop($nodes_by_key[$key]);
2462
$message = $this->raiseLintAtNode(
2464
self::LINT_DUPLICATE_KEYS_IN_ARRAY,
2465
'Duplicate key in array initializer. PHP will ignore all '.
2466
'but the last entry.');
2468
$locations = array();
2469
foreach ($nodes_by_key[$key] as $node) {
2470
$locations[] = $this->getOtherLocation($node->getOffset());
2472
$message->setOtherLocations($locations);
2477
private function lintClosingCallParen(XHPASTNode $root) {
2478
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
2479
$calls = $calls->add($root->selectDescendantsOfType('n_METHOD_CALL'));
2481
foreach ($calls as $call) {
2482
// If the last parameter of a call is a HEREDOC, don't apply this rule.
2484
->getChildOfType(1, 'n_CALL_PARAMETER_LIST')
2488
$last_param = last($params);
2489
if ($last_param->getTypeName() === 'n_HEREDOC') {
2494
$tokens = $call->getTokens();
2495
$last = array_pop($tokens);
2497
$trailing = $last->getNonsemanticTokensBefore();
2498
$trailing_text = implode('', mpull($trailing, 'getValue'));
2499
if (preg_match('/^\s+$/', $trailing_text)) {
2500
$this->raiseLintAtOffset(
2501
$last->getOffset() - strlen($trailing_text),
2502
self::LINT_CLOSING_CALL_PAREN,
2503
'Convention: no spaces before closing parenthesis in calls.',
2510
private function lintClosingDeclarationParen(XHPASTNode $root) {
2511
$decs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
2512
$decs = $decs->add($root->selectDescendantsOfType('n_METHOD_DECLARATION'));
2514
foreach ($decs as $dec) {
2515
$params = $dec->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST');
2516
$tokens = $params->getTokens();
2517
$last = array_pop($tokens);
2519
$trailing = $last->getNonsemanticTokensBefore();
2520
$trailing_text = implode('', mpull($trailing, 'getValue'));
2521
if (preg_match('/^\s+$/', $trailing_text)) {
2522
$this->raiseLintAtOffset(
2523
$last->getOffset() - strlen($trailing_text),
2524
self::LINT_CLOSING_DECL_PAREN,
2525
'Convention: no spaces before closing parenthesis in function and '.
2526
'method declarations.',
2533
private function lintKeywordCasing(XHPASTNode $root) {
2534
$keywords = array();
2536
$symbols = $root->selectDescendantsOfType('n_SYMBOL_NAME');
2537
foreach ($symbols as $symbol) {
2538
$keywords[] = head($symbol->getTokens());
2541
$arrays = $root->selectDescendantsOfType('n_ARRAY_LITERAL');
2542
foreach ($arrays as $array) {
2543
$keywords[] = head($array->getTokens());
2546
$typehints = $root->selectDescendantsOfType('n_TYPE_NAME');
2547
foreach ($typehints as $typehint) {
2548
$keywords[] = head($typehint->getTokens());
2551
$new_invocations = $root->selectDescendantsOfType('n_NEW');
2552
foreach ($new_invocations as $invocation) {
2553
$keywords[] = head($invocation->getTokens());
2556
$class_declarations = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
2557
foreach ($class_declarations as $declaration) {
2558
$keywords[] = head($declaration->getTokens());
2561
// NOTE: Although PHP generally allows arbitrary casing for all language
2562
// keywords, it's exceedingly rare for anyone to type, e.g., "CLASS" or
2563
// "cLaSs" in the wild. This list just attempts to cover unconventional
2564
// spellings which see some level of use, not all keywords exhaustively.
2565
// There is no token or node type which spans all keywords, so this is
2566
// significantly simpler.
2568
static $keyword_map = array(
2577
foreach ($keywords as $keyword) {
2578
$value = $keyword->getValue();
2579
$value_key = strtolower($value);
2580
if (!isset($keyword_map[$value_key])) {
2583
$expected_spelling = $keyword_map[$value_key];
2584
if ($value !== $expected_spelling) {
2585
$this->raiseLintAtToken(
2587
self::LINT_KEYWORD_CASING,
2588
"Convention: spell keyword '{$value}' as '{$expected_spelling}'.",
2589
$expected_spelling);
2594
private function lintStrings(XHPASTNode $root) {
2595
$nodes = $root->selectDescendantsOfTypes(array(
2596
'n_CONCATENATION_LIST',
2600
foreach ($nodes as $node) {
2603
if ($node->getTypeName() === 'n_CONCATENATION_LIST') {
2604
$strings = $node->selectDescendantsOfType('n_STRING_SCALAR');
2605
} else if ($node->getTypeName() === 'n_STRING_SCALAR') {
2606
$strings = array($node);
2608
if ($node->getParentNode()->getTypeName() === 'n_CONCATENATION_LIST') {
2614
$invalid_nodes = array();
2617
foreach ($strings as $string) {
2618
$concrete_string = $string->getConcreteString();
2619
$single_quoted = ($concrete_string[0] === "'");
2620
$contents = substr($concrete_string, 1, -1);
2622
// Double quoted strings are allowed when the string contains the
2623
// following characters.
2624
static $allowed_chars = array(
2643
$contains_special_chars = false;
2644
foreach ($allowed_chars as $allowed_char) {
2645
if (strpos($contents, $allowed_char) !== false) {
2646
$contains_special_chars = true;
2650
if (!$string->isConstantString()) {
2652
} else if ($contains_special_chars && !$single_quoted) {
2654
} else if (!$contains_special_chars && !$single_quoted) {
2655
$invalid_nodes[] = $string;
2656
$fixes[$string->getID()] = "'".str_replace('\"', '"', $contents)."'";
2661
foreach ($invalid_nodes as $invalid_node) {
2662
$this->raiseLintAtNode(
2664
self::LINT_DOUBLE_QUOTE,
2666
'String does not require double quotes. For consistency, '.
2667
'prefer single quotes.'),
2668
$fixes[$invalid_node->getID()]);
2674
protected function lintElseIfStatements(XHPASTNode $root) {
2675
$tokens = $root->selectTokensOfType('T_ELSEIF');
2677
foreach ($tokens as $token) {
2678
$this->raiseLintAtToken(
2680
self::LINT_ELSEIF_USAGE,
2681
pht('Usage of `else if` is preferred over `elseif`.'),
2686
protected function lintSemicolons(XHPASTNode $root) {
2687
$tokens = $root->selectTokensOfType(';');
2689
foreach ($tokens as $token) {
2690
$prev = $token->getPrevToken();
2692
if ($prev->isAnyWhitespace()) {
2693
$this->raiseLintAtToken(
2695
self::LINT_SEMICOLON_SPACING,
2696
pht('Space found before semicolon.'),
2702
protected function lintLanguageConstructParentheses(XHPASTNode $root) {
2703
$nodes = $root->selectDescendantsOfTypes(array(
2708
foreach ($nodes as $node) {
2709
$child = head($node->getChildren());
2711
if ($child->getTypeName() === 'n_PARENTHETICAL_EXPRESSION') {
2712
list($before, $after) = $child->getSurroundingNonsemanticTokens();
2714
$replace = preg_replace(
2717
$child->getConcreteString());
2720
$replace = ' '.$replace;
2723
$this->raiseLintAtNode(
2725
self::LINT_LANGUAGE_CONSTRUCT_PAREN,
2726
pht('Language constructs do not require parentheses.'),
2732
protected function lintEmptyBlockStatements(XHPASTNode $root) {
2733
$nodes = $root->selectDescendantsOfType('n_STATEMENT_LIST');
2735
foreach ($nodes as $node) {
2736
$tokens = $node->getTokens();
2737
$token = head($tokens);
2739
if (count($tokens) <= 2) {
2743
// Safety check... if the first token isn't an opening brace then
2744
// there's nothing to do here.
2745
if ($token->getTypeName() != '{') {
2749
$only_whitespace = true;
2750
for ($token = $token->getNextToken();
2751
$token && $token->getTypeName() != '}';
2752
$token = $token->getNextToken()) {
2753
$only_whitespace = $only_whitespace && $token->isAnyWhitespace();
2756
if (count($tokens) > 2 && $only_whitespace) {
2757
$this->raiseLintAtNode(
2759
self::LINT_EMPTY_STATEMENT,
2761
"Braces for an empty block statement shouldn't ".
2762
"contain only whitespace."),
2768
protected function lintArraySeparator(XHPASTNode $root) {
2769
$arrays = $root->selectDescendantsOfType('n_ARRAY_LITERAL');
2771
foreach ($arrays as $array) {
2772
$value_list = $array->getChildOfType(0, 'n_ARRAY_VALUE_LIST');
2773
$values = $value_list->getChildrenOfType('n_ARRAY_VALUE');
2776
// There is no need to check an empty array.
2780
$multiline = $array->getLineNumber() != $array->getEndLineNumber();
2782
$value = last($values);
2783
$after = last($value->getTokens())->getNextToken();
2785
if ($multiline && (!$after || $after->getValue() != ',')) {
2786
if ($value->getChildByIndex(1)->getTypeName() == 'n_HEREDOC') {
2790
$this->raiseLintAtNode(
2792
self::LINT_ARRAY_SEPARATOR,
2793
pht('Multi-lined arrays should have trailing commas.'),
2794
$value->getConcreteString().',');
2795
} else if (!$multiline && $after && $after->getValue() == ',') {
2796
$this->raiseLintAtToken(
2798
self::LINT_ARRAY_SEPARATOR,
2799
pht('Single lined arrays should not have a trailing comma.'),
2805
private function lintConstructorParentheses(XHPASTNode $root) {
2806
$nodes = $root->selectDescendantsOfType('n_NEW');
2808
foreach ($nodes as $node) {
2809
$class = $node->getChildByIndex(0);
2810
$params = $node->getChildByIndex(1);
2812
if ($params->getTypeName() == 'n_EMPTY') {
2813
$this->raiseLintAtNode(
2815
self::LINT_CONSTRUCTOR_PARENTHESES,
2816
pht('Use parentheses when invoking a constructor.'),
2817
$class->getConcreteString().'()');
2822
public function getSuperGlobalNames() {