~ubuntu-branches/ubuntu/vivid/phabricator/vivid-proposed

« back to all changes in this revision

Viewing changes to arcanist/src/lint/linter/ArcanistXHPASTLinter.php

  • Committer: Package Import Robot
  • Author(s): Richard Sellam
  • Date: 2014-10-23 20:49:26 UTC
  • mfrom: (0.2.1) (0.1.1)
  • Revision ID: package-import@ubuntu.com-20141023204926-vq80u1op4df44azb
Tags: 0~git20141023-1
Initial release (closes: #703046)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<?php
 
2
 
 
3
/**
 
4
 * Uses XHPAST to apply lint rules to PHP.
 
5
 */
 
6
final class ArcanistXHPASTLinter extends ArcanistBaseXHPASTLinter {
 
7
 
 
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;
 
52
 
 
53
  private $naminghook;
 
54
  private $switchhook;
 
55
  private $version;
 
56
  private $windowsVersion;
 
57
 
 
58
  public function getInfoName() {
 
59
    return 'XHPAST Lint';
 
60
  }
 
61
 
 
62
  public function getInfoDescription() {
 
63
    return pht('Use XHPAST to enforce coding conventions on PHP source files.');
 
64
  }
 
65
 
 
66
  public function getLintNameMap() {
 
67
    return array(
 
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',
 
112
    );
 
113
  }
 
114
 
 
115
  public function getLinterName() {
 
116
    return 'XHP';
 
117
  }
 
118
 
 
119
  public function getLinterConfigurationName() {
 
120
    return 'xhpast';
 
121
  }
 
122
 
 
123
  public function getLintSeverityMap() {
 
124
    $disabled = ArcanistLintSeverity::SEVERITY_DISABLED;
 
125
    $advice   = ArcanistLintSeverity::SEVERITY_ADVICE;
 
126
    $warning  = ArcanistLintSeverity::SEVERITY_WARNING;
 
127
 
 
128
    return array(
 
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,
 
153
    );
 
154
  }
 
155
 
 
156
  public function getLinterConfigurationOptions() {
 
157
    return parent::getLinterConfigurationOptions() + array(
 
158
      'xhpast.naminghook' => array(
 
159
        'type' => 'optional string',
 
160
        'help' => pht(
 
161
          'Name of a concrete subclass of ArcanistXHPASTLintNamingHook which '.
 
162
          'enforces more granular naming convention rules for symbols.'),
 
163
      ),
 
164
      'xhpast.switchhook' => array(
 
165
        'type' => 'optional string',
 
166
        'help' => pht(
 
167
          'Name of a concrete subclass of ArcanistXHPASTLintSwitchHook which '.
 
168
          'tunes the analysis of switch() statements for this linter.'),
 
169
      ),
 
170
      'xhpast.php-version' => array(
 
171
        'type' => 'optional string',
 
172
        'help' => pht('PHP version to target.'),
 
173
      ),
 
174
      'xhpast.php-version.windows' => array(
 
175
        'type' => 'optional string',
 
176
        'help' => pht('PHP version to target on Windows.'),
 
177
      ),
 
178
    );
 
179
  }
 
180
 
 
181
  public function setLinterConfigurationValue($key, $value) {
 
182
    switch ($key) {
 
183
      case 'xhpast.naminghook':
 
184
        $this->naminghook = $value;
 
185
        return;
 
186
      case 'xhpast.switchhook':
 
187
        $this->switchhook = $value;
 
188
        return;
 
189
      case 'xhpast.php-version':
 
190
        $this->version = $value;
 
191
        return;
 
192
      case 'xhpast.php-version.windows':
 
193
        $this->windowsVersion = $value;
 
194
        return;
 
195
    }
 
196
 
 
197
    return parent::setLinterConfigurationValue($key, $value);
 
198
  }
 
199
 
 
200
  public function getVersion() {
 
201
    // The version number should be incremented whenever a new rule is added.
 
202
    return '10';
 
203
  }
 
204
 
 
205
  protected function resolveFuture($path, Future $future) {
 
206
    $tree = $this->getXHPASTTreeForPath($path);
 
207
    if (!$tree) {
 
208
      $ex = $this->getXHPASTExceptionForPath($path);
 
209
      if ($ex instanceof XHPASTSyntaxErrorException) {
 
210
        $this->raiseLintAtLine(
 
211
          $ex->getErrorLine(),
 
212
          1,
 
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());
 
217
      }
 
218
      return;
 
219
    }
 
220
 
 
221
    $root = $tree->getRootNode();
 
222
 
 
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,
 
238
      ),
 
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,
 
244
      ),
 
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,
 
274
    );
 
275
 
 
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);
 
280
          break;
 
281
        }
 
282
      }
 
283
    }
 
284
  }
 
285
 
 
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();
 
291
 
 
292
      if ($operator !== '===' && $operator !== '!==') {
 
293
        continue;
 
294
      }
 
295
 
 
296
      $false = $expression->getChildByIndex(0);
 
297
      if ($false->getTypeName() === 'n_SYMBOL_NAME' &&
 
298
          $false->getConcreteString() === 'false') {
 
299
        $strstr = $expression->getChildByIndex(2);
 
300
      } else {
 
301
        $strstr = $false;
 
302
        $false = $expression->getChildByIndex(2);
 
303
        if ($false->getTypeName() !== 'n_SYMBOL_NAME' ||
 
304
            $false->getConcreteString() !== 'false') {
 
305
          continue;
 
306
        }
 
307
      }
 
308
 
 
309
      if ($strstr->getTypeName() !== 'n_FUNCTION_CALL') {
 
310
        continue;
 
311
      }
 
312
 
 
313
      $name = strtolower($strstr->getChildByIndex(0)->getConcreteString());
 
314
      if ($name === 'strstr' || $name === 'strchr') {
 
315
        $this->raiseLintAtNode(
 
316
          $strstr,
 
317
          self::LINT_SLOWNESS,
 
318
          'Use strpos() for checking if the string contains something.');
 
319
      } else if ($name === 'stristr') {
 
320
        $this->raiseLintAtNode(
 
321
          $strstr,
 
322
          self::LINT_SLOWNESS,
 
323
          'Use stripos() for checking if the string contains something.');
 
324
      }
 
325
    }
 
326
  }
 
327
 
 
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();
 
333
 
 
334
      if ($operator !== '===' && $operator !== '!==') {
 
335
        continue;
 
336
      }
 
337
 
 
338
      $zero = $expression->getChildByIndex(0);
 
339
      if ($zero->getTypeName() === 'n_NUMERIC_SCALAR' &&
 
340
          $zero->getConcreteString() === '0') {
 
341
        $strpos = $expression->getChildByIndex(2);
 
342
      } else {
 
343
        $strpos = $zero;
 
344
        $zero = $expression->getChildByIndex(2);
 
345
        if ($zero->getTypeName() !== 'n_NUMERIC_SCALAR' ||
 
346
            $zero->getConcreteString() !== '0') {
 
347
          continue;
 
348
        }
 
349
      }
 
350
 
 
351
      if ($strpos->getTypeName() !== 'n_FUNCTION_CALL') {
 
352
        continue;
 
353
      }
 
354
 
 
355
      $name = strtolower($strpos->getChildByIndex(0)->getConcreteString());
 
356
      if ($name === 'strpos') {
 
357
        $this->raiseLintAtNode(
 
358
          $strpos,
 
359
          self::LINT_SLOWNESS,
 
360
          'Use strncmp() for checking if the string starts with something.');
 
361
      } else if ($name === 'stripos') {
 
362
        $this->raiseLintAtNode(
 
363
          $strpos,
 
364
          self::LINT_SLOWNESS,
 
365
          'Use strncasecmp() for checking if the string starts with '.
 
366
            'something.');
 
367
      }
 
368
    }
 
369
  }
 
370
 
 
371
  private function lintPHPCompatibility(XHPASTNode $root) {
 
372
    if (!$this->version) {
 
373
      return;
 
374
    }
 
375
 
 
376
    $target = phutil_get_library_root('phutil').
 
377
      '/../resources/php_compat_info.json';
 
378
    $compat_info = phutil_json_decode(Filesystem::readFile($target));
 
379
 
 
380
    // Create a whitelist for symbols which are being used conditionally.
 
381
    $whitelist = array(
 
382
      'class'    => array(),
 
383
      'function' => array(),
 
384
    );
 
385
 
 
386
    $conditionals = $root->selectDescendantsOfType('n_IF');
 
387
    foreach ($conditionals as $conditional) {
 
388
      $condition = $conditional->getChildOfType(0, 'n_CONTROL_CONDITION');
 
389
      $function  = $condition->getChildByIndex(0);
 
390
 
 
391
      if ($function->getTypeName() != 'n_FUNCTION_CALL') {
 
392
        continue;
 
393
      }
 
394
 
 
395
      $function_token = $function
 
396
        ->getChildByIndex(0);
 
397
 
 
398
      if ($function_token->getTypeName() != 'n_SYMBOL_NAME') {
 
399
        // This may be `Class::method(...)` or `$var(...)`.
 
400
        continue;
 
401
      }
 
402
 
 
403
      $function_name = $function_token->getConcreteString();
 
404
 
 
405
      switch ($function_name) {
 
406
        case 'class_exists':
 
407
        case 'function_exists':
 
408
        case 'interface_exists':
 
409
          $type = null;
 
410
          switch ($function_name) {
 
411
            case 'class_exists':
 
412
              $type = 'class';
 
413
              break;
 
414
 
 
415
            case 'function_exists':
 
416
              $type = 'function';
 
417
              break;
 
418
 
 
419
            case 'interface_exists':
 
420
              $type = 'interface';
 
421
              break;
 
422
          }
 
423
 
 
424
          $params = $function->getChildOfType(1, 'n_CALL_PARAMETER_LIST');
 
425
          $symbol = $params->getChildByIndex(0);
 
426
 
 
427
          if (!$symbol->isStaticScalar()) {
 
428
            continue;
 
429
          }
 
430
 
 
431
          $symbol_name = $symbol->evalStatic();
 
432
          if (!idx($whitelist[$type], $symbol_name)) {
 
433
            $whitelist[$type][$symbol_name] = array();
 
434
          }
 
435
 
 
436
          $span = $conditional
 
437
            ->getChildByIndex(1)
 
438
            ->getTokens();
 
439
 
 
440
          $whitelist[$type][$symbol_name][] = range(
 
441
            head_key($span),
 
442
            last_key($span));
 
443
          break;
 
444
      }
 
445
    }
 
446
 
 
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);
 
452
 
 
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()))) {
 
458
            $whitelisted = true;
 
459
            break;
 
460
          }
 
461
        }
 
462
 
 
463
        if ($whitelisted) {
 
464
          continue;
 
465
        }
 
466
 
 
467
        $this->raiseLintAtNode(
 
468
          $node,
 
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(
 
478
              $param,
 
479
              self::LINT_PHP_COMPATIBILITY,
 
480
              "This codebase targets PHP {$this->version}, but parameter ".
 
481
              ($i + 1)." of `{$name}()` was not introduced until PHP ".
 
482
              "{$version}.");
 
483
          }
 
484
        }
 
485
      }
 
486
 
 
487
      if ($this->windowsVersion) {
 
488
        $windows = idx($compat_info['functions_windows'], $name);
 
489
 
 
490
        if ($windows === false) {
 
491
          $this->raiseLintAtNode(
 
492
            $node,
 
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(
 
498
            $node,
 
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}.");
 
503
        }
 
504
      }
 
505
    }
 
506
 
 
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()))) {
 
517
            $whitelisted = true;
 
518
            break;
 
519
          }
 
520
        }
 
521
 
 
522
        if ($whitelisted) {
 
523
          continue;
 
524
        }
 
525
 
 
526
        $this->raiseLintAtNode(
 
527
          $node,
 
528
          self::LINT_PHP_COMPATIBILITY,
 
529
          "This codebase targets PHP {$this->version}, but `{$name}` was not ".
 
530
          "introduced until PHP {$version['min']}.");
 
531
      }
 
532
    }
 
533
 
 
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(
 
543
          $node,
 
544
          self::LINT_PHP_COMPATIBILITY,
 
545
          "This codebase targets PHP {$this->version}, but `{$name}` was not ".
 
546
          "introduced until PHP {$version['min']}.");
 
547
      }
 
548
    }
 
549
 
 
550
    if (version_compare($this->version, '5.3.0') < 0) {
 
551
      $this->lintPHP53Features($root);
 
552
    } else {
 
553
      $this->lintPHP53Incompatibilities($root);
 
554
    }
 
555
 
 
556
    if (version_compare($this->version, '5.4.0') < 0) {
 
557
      $this->lintPHP54Features($root);
 
558
    } else {
 
559
      $this->lintPHP54Incompatibilities($root);
 
560
    }
 
561
  }
 
562
 
 
563
  private function lintPHP53Features(XHPASTNode $root) {
 
564
    $functions = $root->selectTokensOfType('T_FUNCTION');
 
565
    foreach ($functions as $function) {
 
566
      $next = $function->getNextToken();
 
567
      while ($next) {
 
568
        if ($next->isSemantic()) {
 
569
          break;
 
570
        }
 
571
        $next = $next->getNextToken();
 
572
      }
 
573
 
 
574
      if ($next) {
 
575
        if ($next->getTypeName() === '(') {
 
576
          $this->raiseLintAtToken(
 
577
            $function,
 
578
            self::LINT_PHP_COMPATIBILITY,
 
579
            "This codebase targets PHP {$this->version}, but anonymous ".
 
580
            "functions were not introduced until PHP 5.3.");
 
581
        }
 
582
      }
 
583
    }
 
584
 
 
585
    $namespaces = $root->selectTokensOfType('T_NAMESPACE');
 
586
    foreach ($namespaces as $namespace) {
 
587
      $this->raiseLintAtToken(
 
588
        $namespace,
 
589
        self::LINT_PHP_COMPATIBILITY,
 
590
        "This codebase targets PHP {$this->version}, but namespaces were not ".
 
591
        "introduced until PHP 5.3.");
 
592
    }
 
593
 
 
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.
 
596
 
 
597
    // TODO: We parse n_USE in a slightly crazy way right now; that would be
 
598
    // a better selector once it's fixed.
 
599
 
 
600
    $uses = $root->selectDescendantsOfType('n_USE_LIST');
 
601
    foreach ($uses as $use) {
 
602
      $this->raiseLintAtNode(
 
603
        $use,
 
604
        self::LINT_PHP_COMPATIBILITY,
 
605
        "This codebase targets PHP {$this->version}, but namespaces were not ".
 
606
        "introduced until PHP 5.3.");
 
607
    }
 
608
 
 
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') {
 
613
        continue;
 
614
      }
 
615
      if ($name->getConcreteString() === 'static') {
 
616
        $this->raiseLintAtNode(
 
617
          $name,
 
618
          self::LINT_PHP_COMPATIBILITY,
 
619
          "This codebase targets PHP {$this->version}, but `static::` was not ".
 
620
          "introduced until PHP 5.3.");
 
621
      }
 
622
    }
 
623
 
 
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(
 
629
          $ternary,
 
630
          self::LINT_PHP_COMPATIBILITY,
 
631
          "This codebase targets PHP {$this->version}, but short ternary was ".
 
632
          "not introduced until PHP 5.3.");
 
633
      }
 
634
    }
 
635
 
 
636
    $heredocs = $root->selectDescendantsOfType('n_HEREDOC');
 
637
    foreach ($heredocs as $heredoc) {
 
638
      if (preg_match('/^<<<[\'"]/', $heredoc->getConcreteString())) {
 
639
        $this->raiseLintAtNode(
 
640
          $heredoc,
 
641
          self::LINT_PHP_COMPATIBILITY,
 
642
          "This codebase targets PHP {$this->version}, but nowdoc was not ".
 
643
          "introduced until PHP 5.3.");
 
644
      }
 
645
    }
 
646
  }
 
647
 
 
648
  private function lintPHP53Incompatibilities(XHPASTNode $root) {}
 
649
 
 
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,
 
659
            pht(
 
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`.',
 
663
              'f()[...]',
 
664
              'idx()'));
 
665
          break;
 
666
      }
 
667
    }
 
668
  }
 
669
 
 
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);
 
674
 
 
675
      switch ($arg->getTypeName()) {
 
676
        case 'n_EMPTY':
 
677
          break;
 
678
 
 
679
        case 'n_NUMERIC_SCALAR':
 
680
          if ($arg->getConcreteString() != '0') {
 
681
            break;
 
682
          }
 
683
 
 
684
        default:
 
685
          $this->raiseLintAtNode(
 
686
            $break->getChildByIndex(0),
 
687
            self::LINT_PHP_COMPATIBILITY,
 
688
            pht(
 
689
              'The `%s` and `%s` statements no longer accept '.
 
690
              'variable arguments.',
 
691
              'break',
 
692
              'continue'));
 
693
          break;
 
694
      }
 
695
    }
 
696
  }
 
697
 
 
698
  private function lintImplicitFallthrough(XHPASTNode $root) {
 
699
    $hook_obj = null;
 
700
    $working_copy = $this->getEngine()->getWorkingCopy();
 
701
    if ($working_copy) {
 
702
      $hook_class = $this->switchhook
 
703
        ? $this->switchhook
 
704
        : $this->getDeprecatedConfiguration('lint.xhpast.switchhook');
 
705
      if ($hook_class) {
 
706
        $hook_obj = newv($hook_class, array());
 
707
        assert_instances_of(array($hook_obj), 'ArcanistXHPASTLintSwitchHook');
 
708
      }
 
709
    }
 
710
 
 
711
    $switches = $root->selectDescendantsOfType('n_SWITCH');
 
712
    foreach ($switches as $switch) {
 
713
      $blocks = array();
 
714
 
 
715
      $cases = $switch->selectDescendantsOfType('n_CASE');
 
716
      foreach ($cases as $case) {
 
717
        $blocks[] = $case;
 
718
      }
 
719
 
 
720
      $defaults = $switch->selectDescendantsOfType('n_DEFAULT');
 
721
      foreach ($defaults as $default) {
 
722
        $blocks[] = $default;
 
723
      }
 
724
 
 
725
 
 
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();
 
737
        }
 
738
 
 
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();
 
749
        }
 
750
 
 
751
        $lower_level_tokens += $different_scope_tokens;
 
752
 
 
753
        // Get all the trailing nonsemantic tokens, since we need to look for
 
754
        // "fallthrough" comments past the end of the semantic block.
 
755
 
 
756
        $tokens = $block->getTokens();
 
757
        $last = end($tokens);
 
758
        while ($last && $last = $last->getNextToken()) {
 
759
          if ($last->isSemantic()) {
 
760
            break;
 
761
          }
 
762
          $tokens[$last->getTokenID()] = $last;
 
763
        }
 
764
 
 
765
        $blocks[$key] = array(
 
766
          $tokens,
 
767
          $lower_level_tokens,
 
768
          $different_scope_tokens,
 
769
        );
 
770
      }
 
771
 
 
772
      foreach ($blocks as $token_lists) {
 
773
        list(
 
774
          $tokens,
 
775
          $lower_level_tokens,
 
776
          $different_scope_tokens) = $token_lists;
 
777
 
 
778
        // Test each block (case or default statement) to see if it's OK. It's
 
779
        // OK if:
 
780
        //
 
781
        //  - it is empty; or
 
782
        //  - it ends in break, return, throw, continue or exit at top level; or
 
783
        //  - it has a comment with "fallthrough" in its text.
 
784
 
 
785
        // Empty blocks are OK, so we start this at `true` and only set it to
 
786
        // false if we find a statement.
 
787
        $block_ok = true;
 
788
 
 
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;
 
792
 
 
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())) {
 
798
              $block_ok = true;
 
799
              break;
 
800
            }
 
801
            continue;
 
802
          }
 
803
 
 
804
          $tok_type = $token->getTypeName();
 
805
 
 
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.
 
810
            $block_ok = false;
 
811
            continue;
 
812
          }
 
813
 
 
814
          if ($tok_type === ';') {
 
815
            if ($statement_ok) {
 
816
              $statment_ok = false;
 
817
            } else {
 
818
              $block_ok = false;
 
819
            }
 
820
            continue;
 
821
          }
 
822
 
 
823
          if ($tok_type === 'T_BREAK'    ||
 
824
              $tok_type === 'T_CONTINUE') {
 
825
            if (empty($lower_level_tokens[$token_id])) {
 
826
              $statement_ok = true;
 
827
              $block_ok = true;
 
828
            }
 
829
            continue;
 
830
          }
 
831
 
 
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;
 
838
              $block_ok = true;
 
839
            }
 
840
            continue;
 
841
          }
 
842
        }
 
843
 
 
844
        if (!$block_ok) {
 
845
          $this->raiseLintAtToken(
 
846
            head($tokens),
 
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.");
 
852
        }
 
853
      }
 
854
    }
 
855
  }
 
856
 
 
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() != '{') {
 
861
        continue;
 
862
      }
 
863
      list($before, $after) = $list->getSurroundingNonsemanticTokens();
 
864
      if (!$before) {
 
865
        $first = head($tokens);
 
866
 
 
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() !== ')') {
 
872
          continue;
 
873
        }
 
874
 
 
875
        $this->raiseLintAtToken(
 
876
          $first,
 
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(
 
885
            $before,
 
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.',
 
889
            ' ');
 
890
        }
 
891
      }
 
892
    }
 
893
  }
 
894
 
 
895
  private function lintTautologicalExpressions(XHPASTNode $root) {
 
896
    $expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
 
897
 
 
898
    static $operators = array(
 
899
      '-'   => true,
 
900
      '/'   => true,
 
901
      '-='  => true,
 
902
      '/='  => true,
 
903
      '<='  => true,
 
904
      '<'   => true,
 
905
      '=='  => true,
 
906
      '===' => true,
 
907
      '!='  => true,
 
908
      '!==' => true,
 
909
      '>='  => true,
 
910
      '>'   => true,
 
911
    );
 
912
 
 
913
    static $logical = array(
 
914
      '||'  => true,
 
915
      '&&'  => true,
 
916
    );
 
917
 
 
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();
 
923
 
 
924
        if ($left === $right) {
 
925
          $this->raiseLintAtNode(
 
926
            $expr,
 
927
            self::LINT_TAUTOLOGICAL_EXPRESSION,
 
928
            'Both sides of this expression are identical, so it always '.
 
929
            'evaluates to a constant.');
 
930
        }
 
931
      }
 
932
 
 
933
      if (!empty($logical[$operator])) {
 
934
        $left = $expr->getChildByIndex(0)->getSemanticString();
 
935
        $right = $expr->getChildByIndex(2)->getSemanticString();
 
936
 
 
937
        // NOTE: These will be null to indicate "could not evaluate".
 
938
        $left = $this->evaluateStaticBoolean($left);
 
939
        $right = $this->evaluateStaticBoolean($right);
 
940
 
 
941
        if (($operator === '||' && ($left === true || $right === true)) ||
 
942
            ($operator === '&&' && ($left === false || $right === false))) {
 
943
          $this->raiseLintAtNode(
 
944
            $expr,
 
945
            self::LINT_TAUTOLOGICAL_EXPRESSION,
 
946
            'The logical value of this expression is static. Did you forget '.
 
947
            'to remove some debugging code?');
 
948
        }
 
949
      }
 
950
    }
 
951
  }
 
952
 
 
953
  /**
 
954
   * Statically evaluate a boolean value from an XHP tree.
 
955
   *
 
956
   * TODO: Improve this and move it to XHPAST proper?
 
957
   *
 
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.
 
961
   */
 
962
  private function evaluateStaticBoolean($string) {
 
963
    switch (strtolower($string)) {
 
964
      case '0':
 
965
      case 'null':
 
966
      case 'false':
 
967
        return false;
 
968
      case '1':
 
969
      case 'true':
 
970
        return true;
 
971
    }
 
972
    return null;
 
973
  }
 
974
 
 
975
 
 
976
  protected function lintCommentSpaces(XHPASTNode $root) {
 
977
    foreach ($root->selectTokensOfType('T_COMMENT') as $comment) {
 
978
      $value = $comment->getValue();
 
979
      if ($value[0] !== '#') {
 
980
        $match = null;
 
981
        if (preg_match('@^(/[/*]+)[^/*\s]@', $value, $match)) {
 
982
          $this->raiseLintAtOffset(
 
983
            $comment->getOffset(),
 
984
            self::LINT_COMMENT_SPACING,
 
985
            'Put space after comment start.',
 
986
            $match[1],
 
987
            $match[1].' ');
 
988
        }
 
989
      }
 
990
    }
 
991
  }
 
992
 
 
993
 
 
994
  protected function lintHashComments(XHPASTNode $root) {
 
995
    foreach ($root->selectTokensOfType('T_COMMENT') as $comment) {
 
996
      $value = $comment->getValue();
 
997
      if ($value[0] !== '#') {
 
998
        continue;
 
999
      }
 
1000
 
 
1001
      $this->raiseLintAtOffset(
 
1002
        $comment->getOffset(),
 
1003
        self::LINT_COMMENT_STYLE,
 
1004
        'Use "//" single-line comments, not "#".',
 
1005
        '#',
 
1006
        (preg_match('/^#\S/', $value) ? '// ' : '//'));
 
1007
    }
 
1008
  }
 
1009
 
 
1010
  /**
 
1011
   * Find cases where loops get nested inside each other but use the same
 
1012
   * iterator variable. For example:
 
1013
   *
 
1014
   *  COUNTEREXAMPLE
 
1015
   *  foreach ($list as $thing) {
 
1016
   *    foreach ($stuff as $thing) { // <-- Raises an error for reuse of $thing
 
1017
   *      // ...
 
1018
   *    }
 
1019
   *  }
 
1020
   *
 
1021
   */
 
1022
  private function lintReusedIterators(XHPASTNode $root) {
 
1023
    $used_vars = array();
 
1024
 
 
1025
    $for_loops = $root->selectDescendantsOfType('n_FOR');
 
1026
    foreach ($for_loops as $for_loop) {
 
1027
      $var_map = array();
 
1028
 
 
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;
 
1036
        }
 
1037
      }
 
1038
 
 
1039
      $used_vars[$for_loop->getID()] = $var_map;
 
1040
    }
 
1041
 
 
1042
    $foreach_loops = $root->selectDescendantsOfType('n_FOREACH');
 
1043
    foreach ($foreach_loops as $foreach_loop) {
 
1044
      $var_map = array();
 
1045
 
 
1046
      $foreach_expr = $foreach_loop->getChildOftype(0, 'n_FOREACH_EXPRESSION');
 
1047
 
 
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),
 
1053
      );
 
1054
      foreach ($possible_used_vars as $var) {
 
1055
        if ($var->getTypeName() === 'n_EMPTY') {
 
1056
          continue;
 
1057
        }
 
1058
        $name = $var->getConcreteString();
 
1059
        $name = trim($name, '&'); // Get rid of ref silliness.
 
1060
        $var_map[$name] = $var;
 
1061
      }
 
1062
 
 
1063
      $used_vars[$foreach_loop->getID()] = $var_map;
 
1064
    }
 
1065
 
 
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);
 
1071
 
 
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);
 
1076
        if ($shared) {
 
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.");
 
1084
 
 
1085
          $locations = array();
 
1086
          foreach ($shared as $var) {
 
1087
            $locations[] = $this->getOtherLocation($var->getOffset());
 
1088
          }
 
1089
          $message->setOtherLocations($locations);
 
1090
        }
 
1091
      }
 
1092
    }
 
1093
  }
 
1094
 
 
1095
  /**
 
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
 
1099
   * reference.
 
1100
   *
 
1101
   *  COUNTEREXAMPLE
 
1102
   *  foreach ($ar as &$a) {
 
1103
   *    // ...
 
1104
   *  }
 
1105
   *  $a = 1; // <-- Raises an error for using $a
 
1106
   *
 
1107
   */
 
1108
  protected function lintReusedIteratorReferences(XHPASTNode $root) {
 
1109
 
 
1110
    $fdefs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
 
1111
    $mdefs = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
 
1112
    $defs = $fdefs->add($mdefs);
 
1113
 
 
1114
    foreach ($defs as $def) {
 
1115
 
 
1116
      $body = $def->getChildByIndex(5);
 
1117
      if ($body->getTypeName() === 'n_EMPTY') {
 
1118
        // Abstract method declaration.
 
1119
        continue;
 
1120
      }
 
1121
 
 
1122
      $exclude = array();
 
1123
 
 
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;
 
1131
        }
 
1132
 
 
1133
        $unset_lists = $func_def->selectDescendantsOfType('n_UNSET_LIST');
 
1134
        foreach ($unset_lists as $unset_list) {
 
1135
          $exclude[$unset_list->getID()] = true;
 
1136
        }
 
1137
 
 
1138
        $foreaches = $func_def->selectDescendantsOfType('n_FOREACH');
 
1139
        foreach ($foreaches as $foreach) {
 
1140
          $exclude[$foreach->getID()] = true;
 
1141
        }
 
1142
      }
 
1143
 
 
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()])) {
 
1149
          continue;
 
1150
        }
 
1151
 
 
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;
 
1157
        }
 
1158
      }
 
1159
 
 
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()])) {
 
1165
          continue;
 
1166
        }
 
1167
 
 
1168
        $foreach_expr = $foreach->getChildOfType(0, 'n_FOREACH_EXPRESSION');
 
1169
        $var = $foreach_expr->getChildByIndex(2);
 
1170
        if ($var->getTypeName() !== 'n_VARIABLE_REFERENCE') {
 
1171
          continue;
 
1172
        }
 
1173
 
 
1174
        $reference = $var->getChildByIndex(0);
 
1175
        if ($reference->getTypeName() !== 'n_VARIABLE') {
 
1176
          continue;
 
1177
        }
 
1178
 
 
1179
        $reference_name = $this->getConcreteVariableString($reference);
 
1180
        $reference_vars[$reference_name][] = $reference->getOffset();
 
1181
        $exclude[$reference->getID()] = true;
 
1182
 
 
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;
 
1189
          }
 
1190
        }
 
1191
      }
 
1192
 
 
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() !== '=') {
 
1198
          continue;
 
1199
        }
 
1200
        $lval = $expr->getChildByIndex(0);
 
1201
        if ($lval->getTypeName() !== 'n_VARIABLE') {
 
1202
          continue;
 
1203
        }
 
1204
        $rval = $expr->getChildByIndex(2);
 
1205
        if ($rval->getTypeName() !== 'n_VARIABLE_REFERENCE') {
 
1206
          continue;
 
1207
        }
 
1208
 
 
1209
        // Counts as unsetting a variable
 
1210
        $concrete = $this->getConcreteVariableString($lval);
 
1211
        $unset_vars[$concrete][] = $lval->getOffset();
 
1212
        $exclude[$lval->getID()] = true;
 
1213
      }
 
1214
 
 
1215
      $all_vars = array();
 
1216
      $all = $body->selectDescendantsOfType('n_VARIABLE');
 
1217
      foreach ($all as $var) {
 
1218
        if (isset($exclude[$var->getID()])) {
 
1219
          continue;
 
1220
        }
 
1221
 
 
1222
        $name = $this->getConcreteVariableString($var);
 
1223
 
 
1224
        if (!isset($reference_vars[$name])) {
 
1225
          continue;
 
1226
        }
 
1227
 
 
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;
 
1233
          } else {
 
1234
            break;
 
1235
          }
 
1236
        }
 
1237
        if (!$reference_offset) {
 
1238
          continue;
 
1239
        }
 
1240
 
 
1241
        // Check if an unset exists between reference and usage of this
 
1242
        // variable
 
1243
        $warn = true;
 
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()) {
 
1248
                $warn = false;
 
1249
                break;
 
1250
            }
 
1251
          }
 
1252
        }
 
1253
        if ($warn) {
 
1254
          $this->raiseLintAtNode(
 
1255
            $var,
 
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, '.
 
1259
            'do not reuse.');
 
1260
        }
 
1261
      }
 
1262
 
 
1263
    }
 
1264
  }
 
1265
 
 
1266
  protected function lintVariableVariables(XHPASTNode $root) {
 
1267
    $vvars = $root->selectDescendantsOfType('n_VARIABLE_VARIABLE');
 
1268
    foreach ($vvars as $vvar) {
 
1269
      $this->raiseLintAtNode(
 
1270
        $vvar,
 
1271
        self::LINT_VARIABLE_VARIABLE,
 
1272
        'Rewrite this code to use an array. Variable variables are unclear '.
 
1273
        'and hinder static analysis.');
 
1274
    }
 
1275
  }
 
1276
 
 
1277
  private function lintUndeclaredVariables(XHPASTNode $root) {
 
1278
    // These things declare variables in a function:
 
1279
    //    Explicit parameters
 
1280
    //    Assignment
 
1281
    //    Assignment via list()
 
1282
    //    Static
 
1283
    //    Global
 
1284
    //    Lexical vars
 
1285
    //    Builtins ($this)
 
1286
    //    foreach()
 
1287
    //    catch
 
1288
    //
 
1289
    // These things make lexical scope unknowable:
 
1290
    //    Use of extract()
 
1291
    //    Assignment to variable variables ($$x)
 
1292
    //    Global with variable variables
 
1293
    //
 
1294
    // These things don't count as "using" a variable:
 
1295
    //    isset()
 
1296
    //    empty()
 
1297
    //    Static class variables
 
1298
    //
 
1299
    // The general approach here is to find each function/method declaration,
 
1300
    // then:
 
1301
    //
 
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.
 
1308
    //
 
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.
 
1313
    //
 
1314
    // TODO: Support functions defined inside other functions which is commonly
 
1315
    // used with anonymous functions.
 
1316
 
 
1317
    $fdefs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
 
1318
    $mdefs = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
 
1319
    $defs = $fdefs->add($mdefs);
 
1320
 
 
1321
    foreach ($defs as $def) {
 
1322
 
 
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;
 
1327
 
 
1328
      $declarations = array(
 
1329
        '$this'     => 0,
 
1330
      ) + array_fill_keys($this->getSuperGlobalNames(), 0);
 
1331
      $declaration_tokens = array();
 
1332
      $exclude_tokens = array();
 
1333
      $vars = array();
 
1334
 
 
1335
      // First up, find all the different kinds of declarations, as explained
 
1336
      // above. Put the tokens into the $vars array.
 
1337
 
 
1338
      $param_list = $def->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST');
 
1339
      $param_vars = $param_list->selectDescendantsOfType('n_VARIABLE');
 
1340
      foreach ($param_vars as $var) {
 
1341
        $vars[] = $var;
 
1342
      }
 
1343
 
 
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) {
 
1349
        $vars[] = $var;
 
1350
      }
 
1351
 
 
1352
      $body = $def->getChildByIndex(5);
 
1353
      if ($body->getTypeName() === 'n_EMPTY') {
 
1354
        // Abstract method declaration.
 
1355
        continue;
 
1356
      }
 
1357
 
 
1358
      $static_vars = $body
 
1359
        ->selectDescendantsOfType('n_STATIC_DECLARATION')
 
1360
        ->selectDescendantsOfType('n_VARIABLE');
 
1361
      foreach ($static_vars as $var) {
 
1362
        $vars[] = $var;
 
1363
      }
 
1364
 
 
1365
 
 
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') {
 
1371
            $vars[] = $var;
 
1372
          } else {
 
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.
 
1376
          }
 
1377
        }
 
1378
      }
 
1379
 
 
1380
      // Include "catch (Exception $ex)", but not variables in the body of the
 
1381
      // catch block.
 
1382
      $catches = $body->selectDescendantsOfType('n_CATCH');
 
1383
      foreach ($catches as $catch) {
 
1384
        $vars[] = $catch->getChildOfType(1, 'n_VARIABLE');
 
1385
      }
 
1386
 
 
1387
      $binary = $body->selectDescendantsOfType('n_BINARY_EXPRESSION');
 
1388
      foreach ($binary as $expr) {
 
1389
        if ($expr->getChildByIndex(1)->getConcreteString() !== '=') {
 
1390
          continue;
 
1391
        }
 
1392
        $lval = $expr->getChildByIndex(0);
 
1393
        if ($lval->getTypeName() === 'n_VARIABLE') {
 
1394
          $vars[] = $lval;
 
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) {
 
1402
            $vars[] = $var;
 
1403
          }
 
1404
        }
 
1405
 
 
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.
 
1409
        }
 
1410
      }
 
1411
 
 
1412
      $calls = $body->selectDescendantsOfType('n_FUNCTION_CALL');
 
1413
      foreach ($calls as $call) {
 
1414
        $name = strtolower($call->getChildByIndex(0)->getConcreteString());
 
1415
 
 
1416
        if ($name === 'empty' || $name === 'isset') {
 
1417
          $params = $call
 
1418
            ->getChildOfType(1, 'n_CALL_PARAMETER_LIST')
 
1419
            ->selectDescendantsOfType('n_VARIABLE');
 
1420
          foreach ($params as $var) {
 
1421
            $exclude_tokens[$var->getID()] = true;
 
1422
          }
 
1423
          continue;
 
1424
        }
 
1425
        if ($name !== 'extract') {
 
1426
          continue;
 
1427
        }
 
1428
        $scope_destroyed_at = min($scope_destroyed_at, $call->getOffset());
 
1429
        $this->raiseLintAtNode(
 
1430
          $call,
 
1431
          self::LINT_EXTRACT_USE,
 
1432
          'Avoid extract(). It is confusing and hinders static analysis.');
 
1433
      }
 
1434
 
 
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).
 
1439
 
 
1440
      foreach ($vars as $var) {
 
1441
        $concrete = $this->getConcreteVariableString($var);
 
1442
        $declarations[$concrete] = min(
 
1443
          idx($declarations, $concrete, PHP_INT_MAX),
 
1444
          $var->getOffset());
 
1445
        $declaration_tokens[$var->getID()] = true;
 
1446
      }
 
1447
 
 
1448
      // Excluded tokens are ones we don't "count" as being used, described
 
1449
      // above. Put them into $exclude_tokens.
 
1450
 
 
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;
 
1457
      }
 
1458
 
 
1459
 
 
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.
 
1463
 
 
1464
      $uses = array();
 
1465
 
 
1466
      $all_vars = $body->selectDescendantsOfType('n_VARIABLE');
 
1467
      $all = array();
 
1468
 
 
1469
      // NOTE: $all_vars is not a real array so we can't unset() it.
 
1470
      foreach ($all_vars as $var) {
 
1471
 
 
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
 
1474
        // to it.
 
1475
 
 
1476
        $concrete = $this->getConcreteVariableString($var);
 
1477
        $uses[$concrete][$var->getID()] = $var->getOffset();
 
1478
 
 
1479
        if (isset($declaration_tokens[$var->getID()])) {
 
1480
          // We know this is part of a declaration, so it's fine.
 
1481
          continue;
 
1482
        }
 
1483
        if (isset($exclude_tokens[$var->getID()])) {
 
1484
          // We know this is part of isset() or similar, so it's fine.
 
1485
          continue;
 
1486
        }
 
1487
 
 
1488
        $all[$var->getOffset()] = $concrete;
 
1489
      }
 
1490
 
 
1491
 
 
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
 
1494
      // local.
 
1495
 
 
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
 
1499
      // iterators.
 
1500
 
 
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');
 
1505
 
 
1506
        $foreach_vars = array();
 
1507
 
 
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();
 
1512
 
 
1513
        $key_var = $foreach_expr->getChildByIndex(1);
 
1514
        if ($key_var->getTypeName() === 'n_VARIABLE') {
 
1515
          $foreach_vars[] = $key_var;
 
1516
        }
 
1517
 
 
1518
        $value_var = $foreach_expr->getChildByIndex(2);
 
1519
        if ($value_var->getTypeName() === 'n_VARIABLE') {
 
1520
          $foreach_vars[] = $value_var;
 
1521
        } else {
 
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
 
1525
          // node.
 
1526
          $var = $value_var->getChildByIndex(0);
 
1527
          if ($var->getTypeName() === 'n_VARIABLE_VARIABLE') {
 
1528
            $var = $var->getChildByIndex(0);
 
1529
          }
 
1530
          $foreach_vars[] = $var;
 
1531
        }
 
1532
 
 
1533
        // Remove all uses of the iterators inside of the foreach() loop from
 
1534
        // the $uses map.
 
1535
 
 
1536
        foreach ($foreach_vars as $var) {
 
1537
          $concrete = $this->getConcreteVariableString($var);
 
1538
          $offset = $var->getOffset();
 
1539
 
 
1540
          foreach ($uses[$concrete] as $id => $use_offset) {
 
1541
            if (($use_offset >= $offset) && ($use_offset < $foreach_end)) {
 
1542
              unset($uses[$concrete][$id]);
 
1543
            }
 
1544
          }
 
1545
 
 
1546
          $all_foreach_vars[] = $var;
 
1547
        }
 
1548
      }
 
1549
 
 
1550
      foreach ($all_foreach_vars as $var) {
 
1551
        $concrete = $this->getConcreteVariableString($var);
 
1552
        $offset = $var->getOffset();
 
1553
 
 
1554
        // If a variable was declared before a foreach() and is used after
 
1555
        // it, raise a message.
 
1556
 
 
1557
        if (isset($declarations[$concrete])) {
 
1558
          if ($declarations[$concrete] < $offset) {
 
1559
            if (!empty($uses[$concrete]) &&
 
1560
                max($uses[$concrete]) > $offset) {
 
1561
              $message = $this->raiseLintAtNode(
 
1562
                $var,
 
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])),
 
1570
              ));
 
1571
            }
 
1572
          }
 
1573
        }
 
1574
 
 
1575
        // This is a declaration, exclude it from the "declare variables prior
 
1576
        // to use" check below.
 
1577
        unset($all[$var->getOffset()]);
 
1578
 
 
1579
        $vars[] = $var;
 
1580
      }
 
1581
 
 
1582
      // Now rebuild declarations to include foreach().
 
1583
 
 
1584
      foreach ($vars as $var) {
 
1585
        $concrete = $this->getConcreteVariableString($var);
 
1586
        $declarations[$concrete] = min(
 
1587
          idx($declarations, $concrete, PHP_INT_MAX),
 
1588
          $var->getOffset());
 
1589
        $declaration_tokens[$var->getID()] = true;
 
1590
      }
 
1591
 
 
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;
 
1596
          }
 
1597
        }
 
1598
      }
 
1599
 
 
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.
 
1603
 
 
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
 
1610
          // figure out.
 
1611
          continue;
 
1612
        }
 
1613
        if ($offset >= idx($declarations, $concrete, PHP_INT_MAX)) {
 
1614
          // The use appears after the variable is declared, so it's fine.
 
1615
          continue;
 
1616
        }
 
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.
 
1620
          continue;
 
1621
        }
 
1622
        $this->raiseLintAtOffset(
 
1623
          $offset,
 
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 '.
 
1627
          'variable name.',
 
1628
          $concrete);
 
1629
        $issued_warnings[$concrete] = true;
 
1630
      }
 
1631
    }
 
1632
  }
 
1633
 
 
1634
  private function getConcreteVariableString(XHPASTNode $var) {
 
1635
    $concrete = $var->getConcreteString();
 
1636
    // Strip off curly braces as in $obj->{$property}.
 
1637
    $concrete = trim($concrete, '{}');
 
1638
    return $concrete;
 
1639
  }
 
1640
 
 
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(
 
1647
            $token,
 
1648
            self::LINT_PHP_SHORT_TAG,
 
1649
            'Use the full form of the PHP open tag, "<?php".',
 
1650
            "<?php\n");
 
1651
        }
 
1652
        break;
 
1653
      } else if ($token->getTypeName() === 'T_OPEN_TAG_WITH_ECHO') {
 
1654
        $this->raiseLintAtToken(
 
1655
          $token,
 
1656
          self::LINT_PHP_ECHO_TAG,
 
1657
          'Avoid the PHP echo short form, "<?=".');
 
1658
        break;
 
1659
      } else {
 
1660
        if (!preg_match('/^#!/', $token->getValue())) {
 
1661
          $this->raiseLintAtToken(
 
1662
            $token,
 
1663
            self::LINT_PHP_OPEN_TAG,
 
1664
            'PHP files should start with "<?php", which may be preceded by '.
 
1665
            'a "#!" line for scripts.');
 
1666
        }
 
1667
        break;
 
1668
      }
 
1669
    }
 
1670
 
 
1671
    foreach ($root->selectTokensOfType('T_CLOSE_TAG') as $token) {
 
1672
      $this->raiseLintAtToken(
 
1673
        $token,
 
1674
        self::LINT_PHP_CLOSE_TAG,
 
1675
        'Do not use the PHP closing tag, "?>".');
 
1676
    }
 
1677
  }
 
1678
 
 
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
 
1682
    // override us.
 
1683
    $names = array();
 
1684
 
 
1685
    $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
 
1686
    foreach ($classes as $class) {
 
1687
      $name_token = $class->getChildByIndex(1);
 
1688
      $name_string = $name_token->getConcreteString();
 
1689
 
 
1690
      $names[] = array(
 
1691
        'class',
 
1692
        $name_string,
 
1693
        $name_token,
 
1694
        ArcanistXHPASTLintNamingHook::isUpperCamelCase($name_string)
 
1695
          ? null
 
1696
          : 'Follow naming conventions: classes should be named using '.
 
1697
            'UpperCamelCase.',
 
1698
      );
 
1699
    }
 
1700
 
 
1701
    $ifaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION');
 
1702
    foreach ($ifaces as $iface) {
 
1703
      $name_token = $iface->getChildByIndex(1);
 
1704
      $name_string = $name_token->getConcreteString();
 
1705
      $names[] = array(
 
1706
        'interface',
 
1707
        $name_string,
 
1708
        $name_token,
 
1709
        ArcanistXHPASTLintNamingHook::isUpperCamelCase($name_string)
 
1710
          ? null
 
1711
          : 'Follow naming conventions: interfaces should be named using '.
 
1712
            'UpperCamelCase.',
 
1713
      );
 
1714
    }
 
1715
 
 
1716
 
 
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') {
 
1721
        // Unnamed closure.
 
1722
        continue;
 
1723
      }
 
1724
      $name_string = $name_token->getConcreteString();
 
1725
      $names[] = array(
 
1726
        'function',
 
1727
        $name_string,
 
1728
        $name_token,
 
1729
        ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores(
 
1730
          ArcanistXHPASTLintNamingHook::stripPHPFunction($name_string))
 
1731
          ? null
 
1732
          : 'Follow naming conventions: functions should be named using '.
 
1733
            'lowercase_with_underscores.',
 
1734
      );
 
1735
    }
 
1736
 
 
1737
 
 
1738
    $methods = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
 
1739
    foreach ($methods as $method) {
 
1740
      $name_token = $method->getChildByIndex(2);
 
1741
      $name_string = $name_token->getConcreteString();
 
1742
      $names[] = array(
 
1743
        'method',
 
1744
        $name_string,
 
1745
        $name_token,
 
1746
        ArcanistXHPASTLintNamingHook::isLowerCamelCase(
 
1747
          ArcanistXHPASTLintNamingHook::stripPHPFunction($name_string))
 
1748
          ? null
 
1749
          : 'Follow naming conventions: methods should be named using '.
 
1750
            'lowerCamelCase.',
 
1751
      );
 
1752
    }
 
1753
 
 
1754
    $param_tokens = array();
 
1755
 
 
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');
 
1762
        }
 
1763
        $param_tokens[$name_token->getID()] = true;
 
1764
        $name_string = $name_token->getConcreteString();
 
1765
 
 
1766
        $names[] = array(
 
1767
          'parameter',
 
1768
          $name_string,
 
1769
          $name_token,
 
1770
          ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores(
 
1771
            ArcanistXHPASTLintNamingHook::stripPHPVariable($name_string))
 
1772
            ? null
 
1773
            : 'Follow naming conventions: parameters should be named using '.
 
1774
              'lowercase_with_underscores.',
 
1775
        );
 
1776
      }
 
1777
    }
 
1778
 
 
1779
 
 
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();
 
1786
        $names[] = array(
 
1787
          'constant',
 
1788
          $name_string,
 
1789
          $name_token,
 
1790
          ArcanistXHPASTLintNamingHook::isUppercaseWithUnderscores($name_string)
 
1791
            ? null
 
1792
            : 'Follow naming conventions: class constants should be named '.
 
1793
              'using UPPERCASE_WITH_UNDERSCORES.',
 
1794
        );
 
1795
      }
 
1796
    }
 
1797
 
 
1798
    $member_tokens = array();
 
1799
 
 
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') {
 
1804
          continue;
 
1805
        }
 
1806
 
 
1807
        $name_token = $prop->getChildByIndex(0);
 
1808
        $member_tokens[$name_token->getID()] = true;
 
1809
 
 
1810
        $name_string = $name_token->getConcreteString();
 
1811
        $names[] = array(
 
1812
          'member',
 
1813
          $name_string,
 
1814
          $name_token,
 
1815
          ArcanistXHPASTLintNamingHook::isLowerCamelCase(
 
1816
            ArcanistXHPASTLintNamingHook::stripPHPVariable($name_string))
 
1817
            ? null
 
1818
            : 'Follow naming conventions: class properties should be named '.
 
1819
              'using lowerCamelCase.',
 
1820
        );
 
1821
      }
 
1822
    }
 
1823
 
 
1824
    $superglobal_map = array_fill_keys(
 
1825
      $this->getSuperGlobalNames(),
 
1826
      true);
 
1827
 
 
1828
 
 
1829
    $fdefs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
 
1830
    $mdefs = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
 
1831
    $defs = $fdefs->add($mdefs);
 
1832
 
 
1833
    foreach ($defs as $def) {
 
1834
      $globals = $def->selectDescendantsOfType('n_GLOBAL_DECLARATION_LIST');
 
1835
      $globals = $globals->selectDescendantsOfType('n_VARIABLE');
 
1836
 
 
1837
      $globals_map = array();
 
1838
      foreach ($globals as $global) {
 
1839
        $global_string = $global->getConcreteString();
 
1840
        $globals_map[$global_string] = true;
 
1841
        $names[] = array(
 
1842
          'user',
 
1843
          $global_string,
 
1844
          $global,
 
1845
 
 
1846
          // No advice for globals, but hooks have an option to provide some.
 
1847
          null,
 
1848
        );
 
1849
      }
 
1850
 
 
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;
 
1863
        }
 
1864
      }
 
1865
 
 
1866
      $vars = $def->selectDescendantsOfType('n_VARIABLE');
 
1867
      foreach ($vars as $token_id => $var) {
 
1868
        if (isset($member_tokens[$token_id])) {
 
1869
          continue;
 
1870
        }
 
1871
        if (isset($param_tokens[$token_id])) {
 
1872
          continue;
 
1873
        }
 
1874
        if (isset($exclude_tokens[$token_id])) {
 
1875
          continue;
 
1876
        }
 
1877
 
 
1878
        $var_string = $var->getConcreteString();
 
1879
 
 
1880
        // Awkward artifact of "$o->{$x}".
 
1881
        $var_string = trim($var_string, '{}');
 
1882
 
 
1883
        if (isset($superglobal_map[$var_string])) {
 
1884
          continue;
 
1885
        }
 
1886
        if (isset($globals_map[$var_string])) {
 
1887
          continue;
 
1888
        }
 
1889
 
 
1890
        $names[] = array(
 
1891
          'variable',
 
1892
          $var_string,
 
1893
          $var,
 
1894
          ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores(
 
1895
            ArcanistXHPASTLintNamingHook::stripPHPVariable($var_string))
 
1896
              ? null
 
1897
              : 'Follow naming conventions: variables should be named using '.
 
1898
                'lowercase_with_underscores.',
 
1899
        );
 
1900
      }
 
1901
    }
 
1902
 
 
1903
    $engine = $this->getEngine();
 
1904
    $working_copy = $engine->getWorkingCopy();
 
1905
 
 
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
 
1910
        ? $this->naminghook
 
1911
        : $working_copy->getProjectConfig('lint.xhpast.naminghook');
 
1912
      if ($hook_class) {
 
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;
 
1918
        }
 
1919
      }
 
1920
    }
 
1921
 
 
1922
    // Raise anything we're left with.
 
1923
    foreach ($names as $k => $name_attrs) {
 
1924
      list($type, $name, $token, $result) = $name_attrs;
 
1925
      if ($result) {
 
1926
        $this->raiseLintAtNode(
 
1927
          $token,
 
1928
          self::LINT_NAMING_CONVENTIONS,
 
1929
          $result);
 
1930
      }
 
1931
    }
 
1932
  }
 
1933
 
 
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(
 
1944
            $method_name_token,
 
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 '.
 
1948
            'defined in.');
 
1949
        }
 
1950
      }
 
1951
    }
 
1952
  }
 
1953
 
 
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');
 
1960
 
 
1961
    $all_paren_groups = $calls
 
1962
      ->add($controls)
 
1963
      ->add($fors)
 
1964
      ->add($foreach)
 
1965
      ->add($decl);
 
1966
    foreach ($all_paren_groups as $group) {
 
1967
      $tokens = $group->getTokens();
 
1968
 
 
1969
      $token_o = array_shift($tokens);
 
1970
      $token_c = array_pop($tokens);
 
1971
      if ($token_o->getTypeName() !== '(') {
 
1972
        throw new Exception('Expected open paren!');
 
1973
      }
 
1974
      if ($token_c->getTypeName() !== ')') {
 
1975
        throw new Exception('Expected close paren!');
 
1976
      }
 
1977
 
 
1978
      $nonsem_o = $token_o->getNonsemanticTokensAfter();
 
1979
      $nonsem_c = $token_c->getNonsemanticTokensBefore();
 
1980
 
 
1981
      if (!$nonsem_o) {
 
1982
        continue;
 
1983
      }
 
1984
 
 
1985
      $raise = array();
 
1986
 
 
1987
      $string_o = implode('', mpull($nonsem_o, 'getValue'));
 
1988
      if (preg_match('/^[ ]+$/', $string_o)) {
 
1989
        $raise[] = array($nonsem_o, $string_o);
 
1990
      }
 
1991
 
 
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);
 
1996
        }
 
1997
      }
 
1998
 
 
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.',
 
2005
          $string,
 
2006
          '');
 
2007
      }
 
2008
    }
 
2009
  }
 
2010
 
 
2011
  private function lintSpaceAfterControlStatementKeywords(XHPASTNode $root) {
 
2012
    foreach ($root->getTokens() as $id => $token) {
 
2013
      switch ($token->getTypeName()) {
 
2014
        case 'T_IF':
 
2015
        case 'T_ELSE':
 
2016
        case 'T_FOR':
 
2017
        case 'T_FOREACH':
 
2018
        case 'T_WHILE':
 
2019
        case 'T_DO':
 
2020
        case 'T_SWITCH':
 
2021
          $after = $token->getNonsemanticTokensAfter();
 
2022
          if (empty($after)) {
 
2023
            $this->raiseLintAtToken(
 
2024
              $token,
 
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);
 
2030
 
 
2031
            // If we have an else clause with braces, $space may not be
 
2032
            // a single white space. e.g.,
 
2033
            //
 
2034
            //  if ($x)
 
2035
            //    echo 'foo'
 
2036
            //  else          // <- $space is not " " but "\n  ".
 
2037
            //    echo 'bar'
 
2038
            //
 
2039
            // We just require it starts with either a whitespace or a newline.
 
2040
            if ($token->getTypeName() === 'T_ELSE' ||
 
2041
                $token->getTypeName() === 'T_DO') {
 
2042
              break;
 
2043
            }
 
2044
 
 
2045
            if ($space->isAnyWhitespace() && $space->getValue() !== ' ') {
 
2046
              $this->raiseLintAtToken(
 
2047
                $space,
 
2048
                self::LINT_CONTROL_STATEMENT_SPACING,
 
2049
                'Convention: put a single space after control statements.',
 
2050
                ' ');
 
2051
            }
 
2052
          }
 
2053
          break;
 
2054
      }
 
2055
    }
 
2056
  }
 
2057
 
 
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();
 
2064
 
 
2065
      $replace = null;
 
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} ";
 
2072
      }
 
2073
 
 
2074
      if ($replace !== null) {
 
2075
        $this->raiseLintAtNode(
 
2076
          $operator,
 
2077
          self::LINT_BINARY_EXPRESSION_SPACING,
 
2078
          'Convention: logical and arithmetic operators should be '.
 
2079
          'surrounded by whitespace.',
 
2080
          $replace);
 
2081
      }
 
2082
    }
 
2083
 
 
2084
    $tokens = $root->selectTokensOfType(',');
 
2085
    foreach ($tokens as $token) {
 
2086
      $next = $token->getNextToken();
 
2087
      switch ($next->getTypeName()) {
 
2088
        case ')':
 
2089
        case 'T_WHITESPACE':
 
2090
          break;
 
2091
        default:
 
2092
          $this->raiseLintAtToken(
 
2093
            $token,
 
2094
            self::LINT_BINARY_EXPRESSION_SPACING,
 
2095
            'Convention: comma should be followed by space.',
 
2096
            ', ');
 
2097
          break;
 
2098
      }
 
2099
    }
 
2100
 
 
2101
    $tokens = $root->selectTokensOfType('T_DOUBLE_ARROW');
 
2102
    foreach ($tokens as $token) {
 
2103
      $prev = $token->getPrevToken();
 
2104
      $next = $token->getNextToken();
 
2105
 
 
2106
      $prev_type = $prev->getTypeName();
 
2107
      $next_type = $next->getTypeName();
 
2108
 
 
2109
      $prev_space = ($prev_type === 'T_WHITESPACE');
 
2110
      $next_space = ($next_type === 'T_WHITESPACE');
 
2111
 
 
2112
      $replace = null;
 
2113
      if (!$prev_space && !$next_space) {
 
2114
        $replace = ' => ';
 
2115
      } else if ($prev_space && !$next_space) {
 
2116
        $replace = '=> ';
 
2117
      } else if (!$prev_space && $next_space) {
 
2118
        $replace = ' =>';
 
2119
      }
 
2120
 
 
2121
      if ($replace !== null) {
 
2122
        $this->raiseLintAtToken(
 
2123
          $token,
 
2124
          self::LINT_BINARY_EXPRESSION_SPACING,
 
2125
          'Convention: double arrow should be surrounded by whitespace.',
 
2126
          $replace);
 
2127
      }
 
2128
    }
 
2129
 
 
2130
    $parameters = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER');
 
2131
    foreach ($parameters as $parameter) {
 
2132
      if ($parameter->getChildByIndex(2)->getTypeName() == 'n_EMPTY') {
 
2133
        continue;
 
2134
      }
 
2135
 
 
2136
      $operator = head($parameter->selectTokensOfType('='));
 
2137
      $before = $operator->getNonsemanticTokensBefore();
 
2138
      $after = $operator->getNonsemanticTokensAfter();
 
2139
 
 
2140
      $replace = null;
 
2141
      if (empty($before) && empty($after)) {
 
2142
        $replace = ' = ';
 
2143
      } else if (empty($before)) {
 
2144
        $replace = ' =';
 
2145
      } else if (empty($after)) {
 
2146
        $replace = '= ';
 
2147
      }
 
2148
 
 
2149
      if ($replace !== null) {
 
2150
        $this->raiseLintAtToken(
 
2151
          $operator,
 
2152
          self::LINT_BINARY_EXPRESSION_SPACING,
 
2153
          'Convention: logical and arithmetic operators should be '.
 
2154
          'surrounded by whitespace.',
 
2155
          $replace);
 
2156
      }
 
2157
    }
 
2158
  }
 
2159
 
 
2160
  private function lintSpaceAroundConcatenationOperators(XHPASTNode $root) {
 
2161
    $tokens = $root->selectTokensOfType('.');
 
2162
    foreach ($tokens as $token) {
 
2163
      $prev = $token->getPrevToken();
 
2164
      $next = $token->getNextToken();
 
2165
 
 
2166
      foreach (array('prev' => $prev, 'next' => $next) as $wtoken) {
 
2167
        if ($wtoken->getTypeName() !== 'T_WHITESPACE') {
 
2168
          continue;
 
2169
        }
 
2170
 
 
2171
        $value = $wtoken->getValue();
 
2172
        if (strpos($value, "\n") !== false) {
 
2173
          // If the whitespace has a newline, it's conventional.
 
2174
          continue;
 
2175
        }
 
2176
 
 
2177
        $next = $wtoken->getNextToken();
 
2178
        if ($next && $next->getTypeName() === 'T_COMMENT') {
 
2179
          continue;
 
2180
        }
 
2181
 
 
2182
        $this->raiseLintAtToken(
 
2183
          $wtoken,
 
2184
          self::LINT_CONCATENATION_OPERATOR,
 
2185
          'Convention: no spaces around "." (string concatenation) operator.',
 
2186
          '');
 
2187
      }
 
2188
    }
 
2189
  }
 
2190
 
 
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(
 
2200
            $defined,
 
2201
            self::LINT_DYNAMIC_DEFINE,
 
2202
            'First argument to define() must be a string literal.');
 
2203
        }
 
2204
      }
 
2205
    }
 
2206
  }
 
2207
 
 
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) {
 
2213
 
 
2214
        $attributes = $method
 
2215
          ->getChildByIndex(0, 'n_METHOD_MODIFIER_LIST')
 
2216
          ->selectDescendantsOfType('n_STRING');
 
2217
 
 
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;
 
2223
          }
 
2224
          if (strtolower($attribute->getConcreteString()) === 'abstract') {
 
2225
            $method_is_abstract = true;
 
2226
          }
 
2227
        }
 
2228
 
 
2229
        if ($method_is_abstract) {
 
2230
          continue;
 
2231
        }
 
2232
 
 
2233
        if (!$method_is_static) {
 
2234
          continue;
 
2235
        }
 
2236
 
 
2237
        $body = $method->getChildOfType(5, 'n_STATEMENT_LIST');
 
2238
 
 
2239
        $variables = $body->selectDescendantsOfType('n_VARIABLE');
 
2240
        foreach ($variables as $variable) {
 
2241
          if ($method_is_static &&
 
2242
              strtolower($variable->getConcreteString()) === '$this') {
 
2243
            $this->raiseLintAtNode(
 
2244
              $variable,
 
2245
              self::LINT_STATIC_THIS,
 
2246
              'You can not reference "$this" inside a static method.');
 
2247
          }
 
2248
        }
 
2249
      }
 
2250
    }
 
2251
  }
 
2252
 
 
2253
  /**
 
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
 
2257
   * wrong.
 
2258
   */
 
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(
 
2267
            $call,
 
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.');
 
2272
        }
 
2273
      }
 
2274
    }
 
2275
  }
 
2276
 
 
2277
  /**
 
2278
   * Exit is parsed as an expression, but using it as such is almost always
 
2279
   * wrong. That is, this is valid:
 
2280
   *
 
2281
   *    strtoupper(33 * exit - 6);
 
2282
   *
 
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:
 
2286
   *
 
2287
   *    exit(-1);
 
2288
   *    exit -1;
 
2289
   *
 
2290
   * The former exits with a failure code, the latter with a success code!
 
2291
   */
 
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(
 
2299
            $unary,
 
2300
            self::LINT_EXIT_EXPRESSION,
 
2301
            'Use exit as a statement, not an expression.');
 
2302
        }
 
2303
      }
 
2304
    }
 
2305
  }
 
2306
 
 
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.',
 
2319
          $trailing_text,
 
2320
          '');
 
2321
      }
 
2322
    }
 
2323
  }
 
2324
 
 
2325
  private function lintTODOComments(XHPASTNode $root) {
 
2326
    $comments = $root->selectTokensOfType('T_COMMENT') +
 
2327
                $root->selectTokensOfType('T_DOC_COMMENT');
 
2328
 
 
2329
    foreach ($comments as $token) {
 
2330
      $value = $token->getValue();
 
2331
      if ($token->getTypeName() === 'T_DOC_COMMENT') {
 
2332
        $regex = '/(TODO|@todo)/';
 
2333
      } else {
 
2334
        $regex = '/TODO/';
 
2335
      }
 
2336
 
 
2337
      $matches = null;
 
2338
      $preg = preg_match_all(
 
2339
        $regex,
 
2340
        $value,
 
2341
        $matches,
 
2342
        PREG_OFFSET_CAPTURE);
 
2343
 
 
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.',
 
2350
          $string);
 
2351
      }
 
2352
    }
 
2353
  }
 
2354
 
 
2355
  /**
 
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.
 
2359
   */
 
2360
  private function lintPrimaryDeclarationFilenameMatch(XHPASTNode $root) {
 
2361
    $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
 
2362
    $interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION');
 
2363
 
 
2364
    if (count($classes) + count($interfaces) !== 1) {
 
2365
      return;
 
2366
    }
 
2367
 
 
2368
    $declarations = count($classes) ? $classes : $interfaces;
 
2369
    $declarations->rewind();
 
2370
    $declaration = $declarations->current();
 
2371
 
 
2372
    $decl_name = $declaration->getChildByIndex(1);
 
2373
    $decl_string = $decl_name->getConcreteString();
 
2374
 
 
2375
    // Exclude strangely named classes, e.g. XHP tags.
 
2376
    if (!preg_match('/^\w+$/', $decl_string)) {
 
2377
      return;
 
2378
    }
 
2379
 
 
2380
    $rename = $decl_string.'.php';
 
2381
 
 
2382
    $path = $this->getActivePath();
 
2383
    $filename = basename($path);
 
2384
 
 
2385
    if ($rename === $filename) {
 
2386
      return;
 
2387
    }
 
2388
 
 
2389
    $this->raiseLintAtNode(
 
2390
      $decl_name,
 
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}'.");
 
2394
  }
 
2395
 
 
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() !== '+') {
 
2401
        continue;
 
2402
      }
 
2403
 
 
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(
 
2409
          $binop,
 
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.");
 
2413
      }
 
2414
    }
 
2415
  }
 
2416
 
 
2417
  /**
 
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.
 
2421
   */
 
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);
 
2430
 
 
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();
 
2436
            break;
 
2437
 
 
2438
          case 'n_SYMBOL_NAME':
 
2439
          case 'n_VARIABLE':
 
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();
 
2445
            break;
 
2446
 
 
2447
          default:
 
2448
            $key = null;
 
2449
            break;
 
2450
        }
 
2451
 
 
2452
        if ($key !== null) {
 
2453
          if (isset($nodes_by_key[$key])) {
 
2454
            $keys_warn[$key] = true;
 
2455
          }
 
2456
          $nodes_by_key[$key][] = $key_node;
 
2457
        }
 
2458
      }
 
2459
 
 
2460
      foreach ($keys_warn as $key => $_) {
 
2461
        $node = array_pop($nodes_by_key[$key]);
 
2462
        $message = $this->raiseLintAtNode(
 
2463
          $node,
 
2464
          self::LINT_DUPLICATE_KEYS_IN_ARRAY,
 
2465
          'Duplicate key in array initializer. PHP will ignore all '.
 
2466
            'but the last entry.');
 
2467
 
 
2468
        $locations = array();
 
2469
        foreach ($nodes_by_key[$key] as $node) {
 
2470
          $locations[] = $this->getOtherLocation($node->getOffset());
 
2471
        }
 
2472
        $message->setOtherLocations($locations);
 
2473
      }
 
2474
    }
 
2475
  }
 
2476
 
 
2477
  private function lintClosingCallParen(XHPASTNode $root) {
 
2478
    $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
 
2479
    $calls = $calls->add($root->selectDescendantsOfType('n_METHOD_CALL'));
 
2480
 
 
2481
    foreach ($calls as $call) {
 
2482
      // If the last parameter of a call is a HEREDOC, don't apply this rule.
 
2483
      $params = $call
 
2484
        ->getChildOfType(1, 'n_CALL_PARAMETER_LIST')
 
2485
        ->getChildren();
 
2486
 
 
2487
      if ($params) {
 
2488
        $last_param = last($params);
 
2489
        if ($last_param->getTypeName() === 'n_HEREDOC') {
 
2490
          continue;
 
2491
        }
 
2492
      }
 
2493
 
 
2494
      $tokens = $call->getTokens();
 
2495
      $last = array_pop($tokens);
 
2496
 
 
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.',
 
2504
          $trailing_text,
 
2505
          '');
 
2506
      }
 
2507
    }
 
2508
  }
 
2509
 
 
2510
  private function lintClosingDeclarationParen(XHPASTNode $root) {
 
2511
    $decs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
 
2512
    $decs = $decs->add($root->selectDescendantsOfType('n_METHOD_DECLARATION'));
 
2513
 
 
2514
    foreach ($decs as $dec) {
 
2515
      $params = $dec->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST');
 
2516
      $tokens = $params->getTokens();
 
2517
      $last = array_pop($tokens);
 
2518
 
 
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.',
 
2527
          $trailing_text,
 
2528
          '');
 
2529
      }
 
2530
    }
 
2531
  }
 
2532
 
 
2533
  private function lintKeywordCasing(XHPASTNode $root) {
 
2534
    $keywords = array();
 
2535
 
 
2536
    $symbols = $root->selectDescendantsOfType('n_SYMBOL_NAME');
 
2537
    foreach ($symbols as $symbol) {
 
2538
      $keywords[] = head($symbol->getTokens());
 
2539
    }
 
2540
 
 
2541
    $arrays = $root->selectDescendantsOfType('n_ARRAY_LITERAL');
 
2542
    foreach ($arrays as $array) {
 
2543
      $keywords[] = head($array->getTokens());
 
2544
    }
 
2545
 
 
2546
    $typehints = $root->selectDescendantsOfType('n_TYPE_NAME');
 
2547
    foreach ($typehints as $typehint) {
 
2548
      $keywords[] = head($typehint->getTokens());
 
2549
    }
 
2550
 
 
2551
    $new_invocations = $root->selectDescendantsOfType('n_NEW');
 
2552
    foreach ($new_invocations as $invocation) {
 
2553
      $keywords[] = head($invocation->getTokens());
 
2554
    }
 
2555
 
 
2556
    $class_declarations = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
 
2557
    foreach ($class_declarations as $declaration) {
 
2558
      $keywords[] = head($declaration->getTokens());
 
2559
    }
 
2560
 
 
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.
 
2567
 
 
2568
    static $keyword_map = array(
 
2569
      'true'  => 'true',
 
2570
      'false' => 'false',
 
2571
      'null'  => 'null',
 
2572
      'array' => 'array',
 
2573
      'new'   => 'new',
 
2574
      'class' => 'class',
 
2575
    );
 
2576
 
 
2577
    foreach ($keywords as $keyword) {
 
2578
      $value = $keyword->getValue();
 
2579
      $value_key = strtolower($value);
 
2580
      if (!isset($keyword_map[$value_key])) {
 
2581
        continue;
 
2582
      }
 
2583
      $expected_spelling = $keyword_map[$value_key];
 
2584
      if ($value !== $expected_spelling) {
 
2585
        $this->raiseLintAtToken(
 
2586
          $keyword,
 
2587
          self::LINT_KEYWORD_CASING,
 
2588
          "Convention: spell keyword '{$value}' as '{$expected_spelling}'.",
 
2589
          $expected_spelling);
 
2590
      }
 
2591
    }
 
2592
  }
 
2593
 
 
2594
  private function lintStrings(XHPASTNode $root) {
 
2595
    $nodes = $root->selectDescendantsOfTypes(array(
 
2596
      'n_CONCATENATION_LIST',
 
2597
      'n_STRING_SCALAR',
 
2598
    ));
 
2599
 
 
2600
    foreach ($nodes as $node) {
 
2601
      $strings = array();
 
2602
 
 
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);
 
2607
 
 
2608
        if ($node->getParentNode()->getTypeName() === 'n_CONCATENATION_LIST') {
 
2609
          continue;
 
2610
        }
 
2611
      }
 
2612
 
 
2613
      $valid = false;
 
2614
      $invalid_nodes = array();
 
2615
      $fixes = array();
 
2616
 
 
2617
      foreach ($strings as $string) {
 
2618
        $concrete_string = $string->getConcreteString();
 
2619
        $single_quoted = ($concrete_string[0] === "'");
 
2620
        $contents = substr($concrete_string, 1, -1);
 
2621
 
 
2622
        // Double quoted strings are allowed when the string contains the
 
2623
        // following characters.
 
2624
        static $allowed_chars = array(
 
2625
          '\n',
 
2626
          '\r',
 
2627
          '\t',
 
2628
          '\v',
 
2629
          '\e',
 
2630
          '\f',
 
2631
          '\'',
 
2632
          '\0',
 
2633
          '\1',
 
2634
          '\2',
 
2635
          '\3',
 
2636
          '\4',
 
2637
          '\5',
 
2638
          '\6',
 
2639
          '\7',
 
2640
          '\x',
 
2641
        );
 
2642
 
 
2643
        $contains_special_chars = false;
 
2644
        foreach ($allowed_chars as $allowed_char) {
 
2645
          if (strpos($contents, $allowed_char) !== false) {
 
2646
            $contains_special_chars = true;
 
2647
          }
 
2648
        }
 
2649
 
 
2650
        if (!$string->isConstantString()) {
 
2651
          $valid = true;
 
2652
        } else if ($contains_special_chars && !$single_quoted) {
 
2653
          $valid = true;
 
2654
        } else if (!$contains_special_chars && !$single_quoted) {
 
2655
          $invalid_nodes[] = $string;
 
2656
          $fixes[$string->getID()] = "'".str_replace('\"', '"', $contents)."'";
 
2657
        }
 
2658
      }
 
2659
 
 
2660
      if (!$valid) {
 
2661
        foreach ($invalid_nodes as $invalid_node) {
 
2662
          $this->raiseLintAtNode(
 
2663
            $invalid_node,
 
2664
            self::LINT_DOUBLE_QUOTE,
 
2665
            pht(
 
2666
              'String does not require double quotes. For consistency, '.
 
2667
              'prefer single quotes.'),
 
2668
            $fixes[$invalid_node->getID()]);
 
2669
        }
 
2670
      }
 
2671
    }
 
2672
  }
 
2673
 
 
2674
  protected function lintElseIfStatements(XHPASTNode $root) {
 
2675
    $tokens = $root->selectTokensOfType('T_ELSEIF');
 
2676
 
 
2677
    foreach ($tokens as $token) {
 
2678
      $this->raiseLintAtToken(
 
2679
        $token,
 
2680
        self::LINT_ELSEIF_USAGE,
 
2681
        pht('Usage of `else if` is preferred over `elseif`.'),
 
2682
        'else if');
 
2683
    }
 
2684
  }
 
2685
 
 
2686
  protected function lintSemicolons(XHPASTNode $root) {
 
2687
    $tokens = $root->selectTokensOfType(';');
 
2688
 
 
2689
    foreach ($tokens as $token) {
 
2690
      $prev = $token->getPrevToken();
 
2691
 
 
2692
      if ($prev->isAnyWhitespace()) {
 
2693
        $this->raiseLintAtToken(
 
2694
          $prev,
 
2695
          self::LINT_SEMICOLON_SPACING,
 
2696
          pht('Space found before semicolon.'),
 
2697
          '');
 
2698
      }
 
2699
    }
 
2700
  }
 
2701
 
 
2702
  protected function lintLanguageConstructParentheses(XHPASTNode $root) {
 
2703
    $nodes = $root->selectDescendantsOfTypes(array(
 
2704
      'n_INCLUDE_FILE',
 
2705
      'n_ECHO_LIST',
 
2706
    ));
 
2707
 
 
2708
    foreach ($nodes as $node) {
 
2709
      $child = head($node->getChildren());
 
2710
 
 
2711
      if ($child->getTypeName() === 'n_PARENTHETICAL_EXPRESSION') {
 
2712
        list($before, $after) = $child->getSurroundingNonsemanticTokens();
 
2713
 
 
2714
        $replace = preg_replace(
 
2715
          '/^\((.*)\)$/',
 
2716
          '$1',
 
2717
          $child->getConcreteString());
 
2718
 
 
2719
        if (!$before) {
 
2720
          $replace = ' '.$replace;
 
2721
        }
 
2722
 
 
2723
        $this->raiseLintAtNode(
 
2724
          $child,
 
2725
          self::LINT_LANGUAGE_CONSTRUCT_PAREN,
 
2726
          pht('Language constructs do not require parentheses.'),
 
2727
          $replace);
 
2728
      }
 
2729
    }
 
2730
  }
 
2731
 
 
2732
  protected function lintEmptyBlockStatements(XHPASTNode $root) {
 
2733
    $nodes = $root->selectDescendantsOfType('n_STATEMENT_LIST');
 
2734
 
 
2735
    foreach ($nodes as $node) {
 
2736
      $tokens = $node->getTokens();
 
2737
      $token = head($tokens);
 
2738
 
 
2739
      if (count($tokens) <= 2) {
 
2740
        continue;
 
2741
      }
 
2742
 
 
2743
      // Safety check... if the first token isn't an opening brace then
 
2744
      // there's nothing to do here.
 
2745
      if ($token->getTypeName() != '{') {
 
2746
        continue;
 
2747
      }
 
2748
 
 
2749
      $only_whitespace = true;
 
2750
      for ($token = $token->getNextToken();
 
2751
           $token && $token->getTypeName() != '}';
 
2752
           $token = $token->getNextToken()) {
 
2753
        $only_whitespace = $only_whitespace && $token->isAnyWhitespace();
 
2754
      }
 
2755
 
 
2756
      if (count($tokens) > 2 && $only_whitespace) {
 
2757
        $this->raiseLintAtNode(
 
2758
          $node,
 
2759
          self::LINT_EMPTY_STATEMENT,
 
2760
          pht(
 
2761
            "Braces for an empty block statement shouldn't ".
 
2762
            "contain only whitespace."),
 
2763
          '{}');
 
2764
      }
 
2765
    }
 
2766
  }
 
2767
 
 
2768
  protected function lintArraySeparator(XHPASTNode $root) {
 
2769
    $arrays = $root->selectDescendantsOfType('n_ARRAY_LITERAL');
 
2770
 
 
2771
    foreach ($arrays as $array) {
 
2772
      $value_list = $array->getChildOfType(0, 'n_ARRAY_VALUE_LIST');
 
2773
      $values = $value_list->getChildrenOfType('n_ARRAY_VALUE');
 
2774
 
 
2775
      if (!$values) {
 
2776
        // There is no need to check an empty array.
 
2777
        continue;
 
2778
      }
 
2779
 
 
2780
      $multiline = $array->getLineNumber() != $array->getEndLineNumber();
 
2781
 
 
2782
      $value = last($values);
 
2783
      $after = last($value->getTokens())->getNextToken();
 
2784
 
 
2785
      if ($multiline && (!$after || $after->getValue() != ',')) {
 
2786
        if ($value->getChildByIndex(1)->getTypeName() == 'n_HEREDOC') {
 
2787
          continue;
 
2788
        }
 
2789
 
 
2790
        $this->raiseLintAtNode(
 
2791
          $value,
 
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(
 
2797
          $after,
 
2798
          self::LINT_ARRAY_SEPARATOR,
 
2799
          pht('Single lined arrays should not have a trailing comma.'),
 
2800
          '');
 
2801
      }
 
2802
    }
 
2803
  }
 
2804
 
 
2805
  private function lintConstructorParentheses(XHPASTNode $root) {
 
2806
    $nodes = $root->selectDescendantsOfType('n_NEW');
 
2807
 
 
2808
    foreach ($nodes as $node) {
 
2809
      $class  = $node->getChildByIndex(0);
 
2810
      $params = $node->getChildByIndex(1);
 
2811
 
 
2812
      if ($params->getTypeName() == 'n_EMPTY') {
 
2813
        $this->raiseLintAtNode(
 
2814
          $class,
 
2815
          self::LINT_CONSTRUCTOR_PARENTHESES,
 
2816
          pht('Use parentheses when invoking a constructor.'),
 
2817
          $class->getConcreteString().'()');
 
2818
      }
 
2819
    }
 
2820
  }
 
2821
 
 
2822
  public function getSuperGlobalNames() {
 
2823
    return array(
 
2824
      '$GLOBALS',
 
2825
      '$_SERVER',
 
2826
      '$_GET',
 
2827
      '$_POST',
 
2828
      '$_FILES',
 
2829
      '$_COOKIE',
 
2830
      '$_SESSION',
 
2831
      '$_REQUEST',
 
2832
      '$_ENV',
 
2833
    );
 
2834
  }
 
2835
 
 
2836
}