public function process()

in src/lint/linter/xhpast/rules/ArcanistPHPCompatibilityXHPASTLinterRule.php [12:277]


  public function process(XHPASTNode $root) {
    static $compat_info;

    if (!$this->version) {
      return;
    }

    if ($compat_info === null) {
      $target = phutil_get_library_root('phutil').
        '/../resources/php_compat_info.json';
      $compat_info = phutil_json_decode(Filesystem::readFile($target));
    }

    // Create a whitelist for symbols which are being used conditionally.
    $whitelist = array(
      'class'    => array(),
      'function' => array(),
      'constant' => array(),
    );

    $conditionals = $root->selectDescendantsOfType('n_IF');
    foreach ($conditionals as $conditional) {
      $condition = $conditional->getChildOfType(0, 'n_CONTROL_CONDITION');
      $function  = $condition->getChildByIndex(0);

      if ($function->getTypeName() != 'n_FUNCTION_CALL') {
        continue;
      }

      $function_token = $function
        ->getChildByIndex(0);

      if ($function_token->getTypeName() != 'n_SYMBOL_NAME') {
        // This may be `Class::method(...)` or `$var(...)`.
        continue;
      }

      $function_name = $function_token->getConcreteString();

      switch ($function_name) {
        case 'class_exists':
        case 'function_exists':
        case 'interface_exists':
        case 'defined':
          $type = null;
          switch ($function_name) {
            case 'class_exists':
              $type = 'class';
              break;

            case 'function_exists':
              $type = 'function';
              break;

            case 'interface_exists':
              $type = 'interface';
              break;

            case 'defined':
              $type = 'constant';
              break;
          }

          $params = $function->getChildOfType(1, 'n_CALL_PARAMETER_LIST');
          $symbol = $params->getChildByIndex(0);

          if (!$symbol->isStaticScalar()) {
            break;
          }

          $symbol_name = $symbol->evalStatic();
          if (!idx($whitelist[$type], $symbol_name)) {
            $whitelist[$type][$symbol_name] = array();
          }

          $span = $conditional
            ->getChildByIndex(1)
            ->getTokens();

          $whitelist[$type][$symbol_name][] = range(
            head_key($span),
            last_key($span));
          break;
      }
    }

    $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
    foreach ($calls as $call) {
      $node = $call->getChildByIndex(0);
      $name = $node->getConcreteString();

      $version = idx($compat_info['functions'], $name, array());
      $min = idx($version, 'php.min');
      $max = idx($version, 'php.max');

      $whitelisted = false;
      foreach (idx($whitelist['function'], $name, array()) as $range) {
        if (array_intersect($range, array_keys($node->getTokens()))) {
          $whitelisted = true;
          break;
        }
      }

      if ($whitelisted) {
        continue;
      }

      if ($min && version_compare($min, $this->version, '>')) {
        $this->raiseLintAtNode(
          $node,
          pht(
            'This codebase targets PHP %s, but `%s()` was not '.
            'introduced until PHP %s.',
            $this->version,
            $name,
            $min));
      } else if ($max && version_compare($max, $this->version, '<')) {
        $this->raiseLintAtNode(
          $node,
          pht(
            'This codebase targets PHP %s, but `%s()` was '.
            'removed in PHP %s.',
            $this->version,
            $name,
            $max));
      } else if (array_key_exists($name, $compat_info['params'])) {
        $params = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST');
        foreach (array_values($params->getChildren()) as $i => $param) {
          $version = idx($compat_info['params'][$name], $i);
          if ($version && version_compare($version, $this->version, '>')) {
            $this->raiseLintAtNode(
              $param,
              pht(
                'This codebase targets PHP %s, but parameter %d '.
                'of `%s()` was not introduced until PHP %s.',
                $this->version,
                $i + 1,
                $name,
                $version));
          }
        }
      }

      if ($this->windowsVersion) {
        $windows = idx($compat_info['functions_windows'], $name);

        if ($windows === false) {
          $this->raiseLintAtNode(
            $node,
            pht(
              'This codebase targets PHP %s on Windows, '.
              'but `%s()` is not available there.',
              $this->windowsVersion,
              $name));
        } else if (version_compare($windows, $this->windowsVersion, '>')) {
          $this->raiseLintAtNode(
            $node,
            pht(
              'This codebase targets PHP %s on Windows, '.
              'but `%s()` is not available there until PHP %s.',
              $this->windowsVersion,
              $name,
              $windows));
        }
      }
    }

    $classes = $root->selectDescendantsOfType('n_CLASS_NAME');
    foreach ($classes as $node) {
      $name = $node->getConcreteString();
      $version = idx($compat_info['interfaces'], $name, array());
      $version = idx($compat_info['classes'], $name, $version);
      $min = idx($version, 'php.min');
      $max = idx($version, 'php.max');

      $whitelisted = false;
      foreach (idx($whitelist['class'], $name, array()) as $range) {
        if (array_intersect($range, array_keys($node->getTokens()))) {
          $whitelisted = true;
          break;
        }
      }

      if ($whitelisted) {
        continue;
      }

      if ($min && version_compare($min, $this->version, '>')) {
        $this->raiseLintAtNode(
          $node,
          pht(
            'This codebase targets PHP %s, but `%s` was not '.
            'introduced until PHP %s.',
            $this->version,
            $name,
            $min));
      } else if ($max && version_compare($max, $this->version, '<')) {
        $this->raiseLintAtNode(
          $node,
          pht(
            'This codebase targets PHP %s, but `%s` was '.
            'removed in PHP %s.',
            $this->version,
            $name,
            $max));
      }
    }

    // TODO: Technically, this will include function names. This is unlikely to
    // cause any issues (unless, of course, there existed a function that had
    // the same name as some constant).
    $constants = $root->selectDescendantsOfTypes(array(
      'n_SYMBOL_NAME',
      'n_MAGIC_SCALAR',
    ));
    foreach ($constants as $node) {
      $name = $node->getConcreteString();
      $version = idx($compat_info['constants'], $name, array());
      $min = idx($version, 'php.min');
      $max = idx($version, 'php.max');

      $whitelisted = false;
      foreach (idx($whitelist['constant'], $name, array()) as $range) {
        if (array_intersect($range, array_keys($node->getTokens()))) {
          $whitelisted = true;
          break;
        }
      }

      if ($whitelisted) {
        continue;
      }

      if ($min && version_compare($min, $this->version, '>')) {
        $this->raiseLintAtNode(
          $node,
          pht(
            'This codebase targets PHP %s, but `%s` was not '.
            'introduced until PHP %s.',
            $this->version,
            $name,
            $min));
      } else if ($max && version_compare($max, $this->version, '<')) {
        $this->raiseLintAtNode(
          $node,
          pht(
            'This codebase targets PHP %s, but `%s` was '.
            'removed in PHP %s.',
            $this->version,
            $name,
            $max));
      }
    }

    if (version_compare($this->version, '5.3.0') < 0) {
      $this->lintPHP53Features($root);
    } else {
      $this->lintPHP53Incompatibilities($root);
    }

    if (version_compare($this->version, '5.4.0') < 0) {
      $this->lintPHP54Features($root);
    } else {
      $this->lintPHP54Incompatibilities($root);
    }
  }