Skip to content
coder.module 19.9 KiB
Newer Older
Doug Green's avatar
Doug Green committed
<?php
// $Id$

/** @file
 * Developer Module that assists with code review and version upgrade that
 * supports a plug-in extensible hook system so contributed modules can
 * define additional review standards.
 *
 * Built-in support for:
 * - Drupal Coding Standards - http://drupal.org/node/318
 * - Handle text in a secure fashion - http://drupal.org/node/28984
 * - Converting 4.6.x modules to 4.7.x - http://drupal.org/node/22218
 * - Converting 4.7.x modules to 5.x - http://drupal.org/node/64279
 *
 * Credit also to dries:
 * - http://cvs.drupal.org/viewcvs/drupal/drupal/scripts/code-style.pl
Doug Green's avatar
Doug Green committed
 */ 

/**
 * Implementation of hook_init().
 */
function coder_init() {
  // hook init is called even on cached pages, but we don't want to
  // actually do anything in that case.
  if (!function_exists('drupal_get_path')) {
    return;
  }

  // Load all our module plug-ins
  global $_coder_coders;
  $path = drupal_get_path('module', 'coder') .'/includes';
  $system_listing = function_exists('drupal_system_listing') ? 'drupal_system_listing' : 'system_listing';
  $files = $system_listing('coder_.*\.inc$', $path, 'name', 0);
Doug Green's avatar
Doug Green committed
  foreach ($files as $file) {
    $_coder_coders[] = $file->name;
Doug Green's avatar
Doug Green committed
  }
}

/**
 * Get all of the code review modules
 */
function _coder_reviews() {
  $reviews = array();

  // get the review definitions from the include directory
  global $_coder_coders;
  if ($_coder_coders) {
    foreach ($_coder_coders as $coder) {
Doug Green's avatar
Doug Green committed
      if (function_exists($function)) {
        if ($review = call_user_func($function)) {
          $reviews = array_merge($reviews, $review);
        }
      }
    }
  }

  // get the contributed module review definitions
  if ($review = module_invoke_all('reviews')) {
    $reviews = array_merge($reviews, $review);
  }

  return $reviews;
}

/**
 * Implementation of hook_help()
 */
function coder_help($section) { 
  switch ($section) { 
    case 'admin/modules#description': 
      return t('Developer Module that assists with code review and version upgrade');
    default :
      return;
  } 
} 

/**
 * Implementation of hook_cron().
 *
 * TODO: move some of the work here...
 */
function coder_cron() {
}

/**
 * Implementation of hook_perm().
 */
function coder_perm() {
  return array('view code review');
}

/**
 * Implementation of hook_menu().
 */
function coder_menu($may_cache) {
  $items = array();

  if ($may_cache) {
    $items[] = array(
      'title' => t('Code review'),
Doug Green's avatar
Doug Green committed
      'callback' => 'coder_page',
      'access' => user_access('view code review'),
    );
    if (function_exists('drupal_system_listing')) {
      $items[] = array(
        'path' => 'admin/settings/coder',
        'title' => t('Code review'),
        'description' => t('Select code review plugins and modules'),
        'callback' => 'drupal_get_form',
        'callback arguments' => 'coder_admin_settings',
        'access' => user_access('administer site configuration'),
        'type' => MENU_NORMAL_ITEM,
      );
    }
Doug Green's avatar
Doug Green committed
  }

  return $items;
}

/**
 * Implementation of hook_form_alter().
 */
function coder_form_alter($form_id, &$form) {
  if ($form_id == 'system_modules') {
    if (user_access('view code review')) {
      foreach ($form['name'] as $name => $data) {
        $form['name'][$name]['#value'] = l($data['#value'], "coder/$name");
function _coder_default_reviews() {
  return drupal_map_assoc(array('style', 'security'));
Doug Green's avatar
Doug Green committed
/**
 * Implementation of hook_settings().
 */
function coder_settings() {
  // get this variable once - do we want only active modules?
  $active = variable_get('coder_active_modules', 1);

  // create the list of review options from the coder review plug-ins
  $reviews = _coder_reviews();
  foreach ($reviews as $name => $review) {
    $review_options[$name] = $review['#title'];
  }

  // what review standards should be applied
  $form['coder_reviews_group'] = array(
    '#type' => 'fieldset',
    '#title' => t('Reviews'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  $form['coder_reviews_group']['coder_reviews'] = array(
    '#type' => 'checkboxes',
    '#options' => $review_options,
    '#description' => t('apply the checked coding reviews'),
    '#default_value' => variable_get('coder_reviews', _coder_default_reviews()),
Doug Green's avatar
Doug Green committed
  );
  
  // get the modules
  $sql = "SELECT name, status FROM {system} WHERE type = 'module'";
  if ($active == 1) {
    $sql .= " AND status=1";
  }
  $sql .= " ORDER BY weight ASC, filename ASC";
  $result = db_query($sql);
  while ($module = db_fetch_object($result)) {
    $name = $module->name;
    $display_name = (!$active && $module->status) ? $name .' (active)' : $name;
    $module_options[$module->name] = l($display_name, "coder/$name");
Doug Green's avatar
Doug Green committed
    if ($module->status) {
      $default_coder_modules[] = $module->name;
    }
  }

  // display active modules option
  $form['coder_modules_group'] = array(
    '#type' => 'fieldset',
    '#title' => t('Modules'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  $form['coder_modules_group']['coder_active_modules'] = array(
    '#type' => 'checkbox',
    '#default_value' => $active,
    '#title' => t('code review all active modules'),
  );

  // display the modules in a fieldset
  $form['coder_modules_group']['coder_modules_subgroup'] = array(
    '#type' => 'fieldset',
    '#title' => t('Select Specific Modules'),
    '#collapsible' => TRUE,
    '#collapsed' => $active,
  );
  $modules = $active ? $default_coder_modules : variable_get('coder_modules', $default_coder_modules);
  $form['coder_modules_group']['coder_modules_subgroup']['coder_modules'] = array(
    '#type' => 'checkboxes',
    '#options' => $module_options,
    '#description' => t('code review the selected modules only'),
    '#default_value' => $modules,
  );

  // display the showcode setting
  // should this even be an option???
    '#title' => t('Hide Source Code on warnings'),
    '#description' => t('Only display line numbers and error messages on warning messages, do not display the source code (for more compact reading)'),
    '#default_value' => variable_get('coder_nocode', 0),
Doug Green's avatar
Doug Green committed
  return $form;
}

/**
 * Implementation of settings page for Drupal 5
 */
function coder_admin_settings() {
  return system_settings_form(coder_settings());
}

Doug Green's avatar
Doug Green committed
/**
 * Implementation of code review page
 */
function coder_page() {
  // get this variable once - do we want only active modules?
  $active = variable_get('coder_active_modules', 1);

  // get this once - list of the reviews to perform
  $reviews = array();
  $avail_reviews = _coder_reviews();
  $selected_reviews = variable_get('coder_reviews', _coder_default_reviews());
Doug Green's avatar
Doug Green committed
  foreach ($selected_reviews as $name => $checked) {
    if ($checked) {
      $reviews[$name] = $avail_reviews[$name];
    }
  }

  // get the list of the modules
Doug Green's avatar
Doug Green committed
  $sql = "SELECT name, filename, status FROM {system} WHERE type = 'module'";
  if (!$requested_module && $active == 1) {
Doug Green's avatar
Doug Green committed
    $sql .= " AND status=1";
  }
  $result = db_query($sql);
  while ($module = db_fetch_object($result)) {
    if ($module->status) {
      $default_coder_modules[$module->name] = $module->name;
    }
    $files[$module->name] = $module->filename;
  }

  // determine which modules are selected
  if ($requested_module) {
    $modules = array($requested_module => $requested_module);
Doug Green's avatar
Doug Green committed
    $modules = $active ? $default_coder_modules : variable_get('coder_modules', $default_coder_modules);
  }
  if ($modules) {
    // loop through the selected modules, preparing the code review results
    $dups = array(); // used to avoid duplicate includes
Doug Green's avatar
Doug Green committed
    foreach ($modules as $name => $checked) {
      if ($checked) {
        // process this one file
        $filename = $files[$name];
        if (!$filename) {
          drupal_set_message(t('Code Review is only available on module files (%module.module not found)', array('%module' => $name)));
          continue;
        }
        $coder_args = array(
          '#reviews' => $reviews,
          '#name' => $name,
          '#filename' => $filename,
        );
        $results = do_coder_reviews($coder_args);
        $output .= theme('coder', $name, $filename, $results);

        // process the same directory include files
        if ($do_includes) {
          // NOTE: convert to the realpath here so system_listing
          // doesn't return additional paths (i.e., try "module").
Doug Green's avatar
Doug Green committed
          $path = str_replace('\\', '/', dirname(realpath($filename)));
          $offset = strpos($path, dirname($filename));
          if (!isset($dups[$path])) {
            $dups[$path] = 1;
            $system_listing = (function_exists('drupal_system_listing')) ? 'drupal_system_listing' : 'system_listing';
            $includefiles = $system_listing('.*\.inc$', $path, 'name', 0);
            foreach ($includefiles as $file) {
              $filename = drupal_substr($file->filename, $offset);
              $coder_args = array(
                '#reviews' => $reviews,
                '#name' => $name,
                '#filename' => $filename,
              );
              $results = do_coder_reviews($coder_args);
              $output .= theme('coder', $name, $filename, $results);
Doug Green's avatar
Doug Green committed
      }
    }

    // prepend any output with the list of code reviews performed
    if (!$output) {
      $output = t('No modules found');
    }
    foreach ($reviews as $review) {
      $items[] = l($review['#title'], $review['#link']);
Doug Green's avatar
Doug Green committed
    }
    $output = '<div class="reviews">'. theme('reviews', $items, t('The code was checked using the following review guidelines')) .'</div>'. $output;
Doug Green's avatar
Doug Green committed
  }
  return $output;
}

function do_coder_reviews($coder_args) {
  // read the file
  if (_coder_read_and_parse_file($coder_args)) {
    // do all of the code reviews
    foreach ($coder_args['#reviews'] as $review) {
      if ($result = do_coder_review($coder_args, $review)) {
        $results += $result;
      }
    }

    // always display something
    if (count($results) == 0) {
      _coder_error_msg($results, t('No Problems Found'));
    }
    else {
      ksort($results, SORT_NUMERIC);
    }
  }
  else {
    _coder_error_msg($results, t('Could not read the file'));
  }

  return $results;
}

function _coder_read_and_parse_file(&$coder_args) {
Doug Green's avatar
Doug Green committed
  // get the path to the module file
  if ($filepath = realpath($coder_args['#filename'])) {
Doug Green's avatar
Doug Green committed
    // read the file
    $content = file_get_contents($filepath);
    $content_length = drupal_strlen($content);

    // TODO: convert the content string to an array for faster usage

    // parse the file:
    // - strip comments
    // - strip quote content // - strip stuff not in php // - break into lines
    $lineno = 0;
    for ($pos = 0; $pos < $content_length; $pos ++) {
      // get the current character
      $char = $content[$pos];
      if ($char == "\n") {
        if ($in_comment == '/') { // end C++ style comments on newline
          unset($in_comment);
        }
        // assume that html inside quotes doesn't span newlines
        unset($in_quote_html);
        // remove blank lines now, so we avoid processing them over-and-over
        if (trim($all_lines[$lineno]) == '') {
          unset($all_lines[$lineno]);
        if (trim($php_lines[$lineno]) == '') {
          unset($php_lines[$lineno]);
        }
        if (trim($html_lines[$lineno]) == '') {
          unset($html_lines[$lineno]);
        $beginning_of_line = 1;
      $all_lines[$lineno] .= $char;

      if ($in_php) {
        // look for the ending php tag which tags precedence over everything
        if ($char == '?' && $content[$pos + 1] == '>') {
          unset($char);
          unset($in_php);
          $all_lines[$lineno] .= '>';
        // when in a quoted string, look for the trailing quote
        // strip characters in the string, replacing with '' or ""
        elseif ($in_quote) {
          if ($in_backslash) {
            unset($in_backslash);
          }
          elseif ($char == '\\') {
            $in_backslash = '\\';
          }
          elseif ($char == $in_quote && !$in_backslash) {
            unset($in_quote);
          }
          elseif ($char == '<') {
            $in_quote_html = '>';
          }
          if ($in_quote && $in_quote_html) {
            $html_lines[$lineno] .= $char;
          }
          if ($char == $in_quote_html) {
            unset($in_quote_html);
          }
          unset($char); // NOTE: trailing char output with starting one
        }
        elseif ($in_heredoc) {
          if ($beginning_of_line && $char == $in_heredoc[0] && substr($content, $pos, $in_heredoc_length) == $in_heredoc) {
            $all_lines[$lineno] .= substr($content, $pos + 1, $in_heredoc_length - 1);
            unset($in_heredoc);
            $pos += $in_heredoc_length;
          }
          elseif ($char == '<') {
            $in_heredoc_html = '>';
          }
          if ($in_heredoc && $in_heredoc_html) {
            $html_lines[$lineno] .= $char;
          }
          if ($char == $in_heredoc_html) {
            unset($in_heredoc_html);
          }
          unset($char);
        }

        // when in a comment look for the trailing comment
        elseif ($in_comment) {
          if ($in_comment == '*' && $char == '*' && $content[$pos + 1] == '/') {
            unset($in_comment);
            $all_lines[$lineno] .= '/';
            $pos ++;
          }
          unset($char); // don't add comments to php output
          switch ($char) {
            case '\'':
            case '"':
              if ($content[$pos - 1] != '\\') {
                $php_lines[$lineno] .= $char;
                $in_quote = $char;
              }
              break;

            case '/':
              $next_char = $content[$pos + 1];
              if ($next_char == '/' || $next_char == '*') {
                unset($char);
                $in_comment = $next_char;
                $all_lines[$lineno] .= $next_char;

            case '<':
              if ($content[$pos + 1] == '<' && $content[$pos + 2] == '<') {
                unset($char);
                $all_lines[$lineno] .= '<<';

                // get the heredoc word
                // read until the end-of-line
                for ($pos += 3; $pos < $content_length; $pos ++) {
                  $char = $content[$pos];
                  if ($char == "\n") {
                    $pos --;
                    if (preg_match('/^\s+(\w+)/', $heredoc, $match)) {
                      $in_heredoc = $match[1];
                      $in_heredoc_length = drupal_strlen($in_heredoc);
                    }
                    break;
                  }
                  $all_lines[$lineno] .= $char;
                  $heredoc .= $char;
                }
                unset($heredoc);

                // replace heredoc's with an empty string
                $php_lines[$lineno] .= "''";
                unset($char);
              }
              break;
        if (isset($char)) {
          $php_lines[$lineno] .= $char;
      else {
        switch ($char) {
          case '<':
            if ($content[$pos + 1] == '?') {
              if ($content[$pos + 2] == ' ') {
                $in_php = 1;
                $all_lines[$lineno] .= '? ';
                $pos += 2;
              }
              elseif (substr($content, $pos + 2, 3) == 'php') {
                $in_php = 1;
                $all_lines[$lineno] .= '?php';
                $pos += 4;
              }
              break;
            }
            // FALTHROUGH
          default:
            $html_lines[$lineno] .= $char;
            break;
        }
    // add the files lines to the arguments
    $coder_args['#all_lines'] = $all_lines;
    $coder_args['#php_lines'] = $php_lines;
    $coder_args['#html_lines'] = $html_lines;
    return 1;
function do_coder_review($coder_args, $review) {
Doug Green's avatar
Doug Green committed
  $results = array();
  if ($review['#rules']) {
    foreach ($review['#rules'] as $rule) {
      if (isset($rule['#original'])) { // deprecated
        $lines = $coder_args['#all_lines'];
      elseif (isset($rule['#source'])) { // all, html, comment, or php
        $source = '#'. $rule['#source'] .'_lines';
        $lines = $coder_args[$source];
      }
      else {
        $lines = $coder_args['#php_lines'];
      }
      if ($lines) {
        switch ($rule['#type']) {
          case 'regex':
            do_coder_review_regex($coder_args, $rule, $lines, $results);
            break;
          case 'grep':
            do_coder_review_grep($coder_args, $rule, $lines, $results);
            break;
          case 'callback':
            do_coder_review_callback($coder_args, $rule, $lines, $results);
            break;
        }
Doug Green's avatar
Doug Green committed
      }
    }
  }
  return $results;
}
Doug Green's avatar
Doug Green committed

function do_coder_review_regex(&$coder_args, $rule, $lines, &$results) {
Doug Green's avatar
Doug Green committed
  if ($regex = $rule['#value']) {
    $regex = '/'. $regex .'/i';
Doug Green's avatar
Doug Green committed
    foreach ($lines as $lineno => $line) {
      if (preg_match($regex, $line, $matches)) {
        // don't match some regex's
        if ($not = $rule['#not']) {
          foreach ($matches as $match) {
            if (preg_match('/'. $not .'/i', $match)) {
        $line = $coder_args['#all_lines'][$lineno];
        _coder_error($results, $rule, $lineno, $line);
function _coder_error(&$results, $rule, $lineno = -1, $line = '', $original = '') {
  if (isset($rule['#warning_callback'])) {
    if (function_exists($rule['#warning_callback'])) {
      $warning = $rule['#warning_callback']();
    }
    else { // if this happens, there is an error in the rule definition
      $warning = t('please <a href="@report">report</a> this !warning',
          '@report' => 'http://drupal.org/node/add/project_issue/coder/bug',
          '!warning' => $rule['#warning_callback'],
        )
      );
    }
  }
  else {
    $warning = t($rule['#warning']);
  }
  
  return _coder_error_msg($results, $warning, $lineno, $line);
}

function _coder_error_msg(&$results, $warning, $lineno = -1, $line = '') {
  // Note: The use of the $key allows multiple errors on one line.
  // This assumes that no line of source has more than 10000 lines of code
  // and that we have fewer than 10000 errors.
  $key = ($lineno + 1) * 10000 + ($_coder_errno ++);
  $results[$key] = theme('coder_warning', $warning, $lineno + 1, $line);
function do_coder_review_grep(&$coder_args, $rule, $lines, &$results) {
  if ($regex = $rule['#value']) {
    $regex = '/'. $regex .'/i';
    foreach ($lines as $lineno => $line) {
      if (preg_match($regex, $line)) {
        return;
      }
    }
function do_coder_review_callback(&$coder_args, $rule, $lines, &$results) {
  if ($function = $rule['#value']) {
    if (function_exists($function)) {
      call_user_func_array($function, array(&$coder_args, $rule, $lines, &$results));
Doug Green's avatar
Doug Green committed
/**
 * Theming functions below...
 */

Doug Green's avatar
Doug Green committed
function theme_reviews($items, $message = '') {
  return $message . theme('item_list', $items, NULL, 'ol');
}

Doug Green's avatar
Doug Green committed
function theme_coder($name, $filename, $results) {
  $title = ($_GET['q'] == "coder/$name") ? $filename : l($name, "coder/$name");
  $output  = '<div class="coder"><h2>'. $title .'</h2>';
  if (count($results)) {
    $output .= theme('item_list', $results);
  }
Doug Green's avatar
Doug Green committed
  $output .= '</div>';
  return $output;
Doug Green's avatar
Doug Green committed
}

function theme_coder_warning($warning, $lineno = 0, $line = '') {
Doug Green's avatar
Doug Green committed
  if ($lineno) {
    $warning = t('Line @number: !warning', array('@number' => $lineno, '!warning' => $warning));
    if ($line && !variable_get('coder_nocode', 0)) {
      $warning .= '<pre>'. check_plain($line) .'</pre>';
    }
Doug Green's avatar
Doug Green committed
  }
  return '<div class="coder_warning">'. $warning .'</div>';
Doug Green's avatar
Doug Green committed
}

function theme_drupalapi($function, $version = 'HEAD') {
  return l($function, "http://api.drupal.org/api/$version/function/$function");
}