filename); $_coder_coders[] = $file->name; } } /** * 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) { $function = $coder .'_reviews'; 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_cron(). */ function coder_cron() { if ($use_cache = variable_get('coder_cache', 1)) { // TODO: move some of the work here... is this really worth it? } } /** * 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( 'path' => 'coder', 'title' => t('Code review'), 'callback' => 'coder_page', 'access' => user_access('view code review'), 'type' => MENU_NORMAL_ITEM, ); $items[] = array( 'path' => 'coder/core', 'title' => t('Core'), 'callback' => 'coder_page', 'access' => user_access('view code review'), 'type' => MENU_NORMAL_ITEM, ); $items[] = array( 'path' => 'coder/active', 'title' => t('Active'), 'callback' => 'coder_page', 'access' => user_access('view code review'), 'type' => MENU_NORMAL_ITEM, ); $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'), ); } 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"); } } } } /** * Helper functions for settings form */ function _coder_default_reviews() { return drupal_map_assoc(array('style', 'security')); } function _coder_settings_form($settings, &$modules, &$files) { // add the javascript $path = drupal_get_path('module', 'coder'); drupal_add_js($path .'/coder.js'); // create the list of review options from the coder review plug-ins $reviews = _coder_reviews(); foreach ($reviews as $name => $review) { $review_options[$name] = l($review['#title'], $review['#link']); } // 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' => $settings['coder_reviews'], ); $form['coder_reviews_group']['coder_severity'] = array( '#type' => 'radios', '#options' => array( 1 => 'minor (most)', 5 => 'normal', 9 => 'critical (fewest)' ), '#description' => t('show warnings at or above the severity warning level'), '#default_value' => $settings['coder_severity'], ); // get the modules $sql = "SELECT name, filename, status FROM {system} WHERE type = 'module' ORDER BY weight ASC, filename ASC"; $result = db_query($sql); while ($module = db_fetch_object($result)) { $display_name = $module->name; if ($module->status) { $display_name .= t(' (active)'); $active_modules[$module->name] = $module->name; } if (_coder_is_drupal_core_module($module)) { $display_name .= t(' (core)'); $core_modules[$module->name] = $module->name; } $all_modules[$module->name] = l($display_name, "coder/$module->name"); $files[$module->name] = $module->filename; } // 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' => $settings['coder_active_modules'], '#title' => t('code review all active modules'), ); $form['coder_modules_group']['coder_core_modules'] = array( '#type' => 'checkbox', '#default_value' => $settings['coder_core_modules'], '#title' => t('code review core files (php, modules, and includes)'), ); $form['coder_modules_group']['coder_includes'] = array( '#type' => 'checkbox', '#default_value' => $settings['coder_includes'], '#title' => t('review all include files (.inc and .php files)'), ); if (arg(0) == 'admin') { $form['coder_modules_group']['coder_cache'] = array( '#type' => 'checkbox', '#default_value' => $settings['coder_cache'], '#title' => t('use the experimental coder cache'), ); } // display the modules in a fieldset $form['coder_modules_group']['coder_modules_subgroup'] = array( '#type' => 'fieldset', '#title' => t('Select Specific Modules'), '#collapsible' => TRUE, '#collapsed' => TRUE, ); if ($settings['coder_active_modules']) { if ($settings['coder_core_modules']) { $modules = array_intersect($active_modules, $core_modules); } else { $modules = $active_modules; } } elseif ($settings['coder_core_modules']) { $modules = $core_modules; } elseif (!is_array($settings['coder_modules'])) { $modules = $active_modules; } else { $modules = $settings['coder_modules']; } foreach ($all_modules as $name => $checkbox) { $classes = array(); if (in_array($name, $active_modules)) { $classes[] = "coder-active"; } if (in_array($name, $core_modules)) { $classes[] = "coder-core"; } $form['coder_modules_group']['coder_modules_subgroup']["coder_modules-$name"] = array( '#type' => 'checkbox', '#title' => $checkbox, '#default_value' => isset($modules[$name]), '#attributes' => array('class' => implode(' ', $classes)), ); } return $form; } /** * Implementation of settings page for Drupal 5 */ function coder_admin_settings() { $settings = _coder_get_default_settings(); $form = _coder_settings_form($settings, $modules, $files); $form['#submit']['coder_settings_form_submit'] = array(); $form['#submit']['system_settings_form_submit'] = array(); return system_settings_form($form); } function coder_settings_form_submit($form_id, &$form_values) { variable_set('coder_modules', _coder_settings_modules($form_values)); } function _coder_settings_modules(&$form_values) { $modules = array(); foreach ($form_values as $key => $value) { if (substr($key, 0, 14) == 'coder_modules-') { if ($value == 1) { $module = substr($key, 14); $modules[$module] = 1; } unset($form_values[$key]); } } return $modules; } function coder_page_form_submit($form_id, $form_values) { // HELP: is there a better way to get these to coder_page_form()??? return FALSE; } /** * Implementation of code review page */ function coder_page() { return drupal_get_form('coder_page_form'); } function _coder_get_default_settings($args = '') { $settings['coder_reviews'] = variable_get('coder_reviews', _coder_default_reviews()); $settings['coder_severity'] = variable_get('coder_severity', 5); $settings['coder_cache'] = variable_get('coder_cache', 1); // determine any options based on the passed in URL, switch ($args) { case '': $settings['coder_active_modules'] = variable_get('coder_active_modules', 1); $settings['coder_core_modules'] = variable_get('coder_core_modules', 0); $settings['coder_includes'] = variable_get('coder_includes', 0); $settings['coder_modules'] = variable_get('coder_modules', ''); break; case 'active': $settings['coder_active_modules'] = 1; break; case 'core': $settings['coder_core_modules'] = 1; $settings['coder_includes'] = 1; break; default: $settings['coder_includes'] = 1; $settings['coder_modules'] = array($args => $args); break; } return $settings; } function coder_page_form() { // HELP: is there a better way to get these from coder_page_form_submit()??? $form_values = $_POST; if (isset($form_values['op'])) { $settings = $form_values; $settings['coder_modules'] = _coder_settings_modules($form_values); drupal_set_title(t('Code review (submitted options)')); } else { $options = arg(1); $settings = _coder_get_default_settings($options); if ($options) { drupal_set_title(t('Code review (@options)', array('@options' => isset($options) ? $options : 'default options'))); } } // get this once - list of the reviews to perform $reviews = array(); $avail_reviews = _coder_reviews(); $selected_reviews = $settings['coder_reviews']; foreach ($selected_reviews as $name => $checked) { if ($checked) { $reviews[$name] = $avail_reviews[$name]; } } if ($coder_form = _coder_settings_form($settings, $modules, $files)) { // add style sheet $path = drupal_get_path('module', 'coder'); drupal_add_css($path .'/coder.css', 'module'); // code review non-module core files $module_weight = 0; if ($settings['coder_core_modules']) { $coder_args = array( '#reviews' => $reviews, '#severity' => $settings['coder_severity'], '#filename' => $filename, ); $form['core_php'] = array( '#type' => 'fieldset', '#title' => 'core (php)', '#collapsible' => TRUE, '#collapsed' => TRUE, '#weight' => ++ $module_weight, ); $phpfiles = file_scan_directory('.', '.*\.php', array('.', '..', 'CVS'), 0, FALSE, 'name', 0); _coder_page_form_includes($form, $coder_args, 'core_php', $phpfiles, 2); $form['core_includes'] = array( '#type' => 'fieldset', '#title' => 'core (includes)', '#collapsible' => TRUE, '#collapsed' => TRUE, '#weight' => ++ $module_weight, ); $includefiles = drupal_system_listing('.*\.inc$', 'includes', 'name', 0); _coder_page_form_includes($form, $coder_args, 'core_includes', $includefiles, 0); } // loop through the selected modules, preparing the code review results $dups = array(); // used to avoid duplicate includes 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, '#severity' => $settings['coder_severity'], '#name' => $name, '#filename' => $filename, ); $results = do_coder_reviews($coder_args); // output the results in a collapsible fieldset $form[$name] = array( '#type' => 'fieldset', '#title' => $filename, '#collapsible' => TRUE, '#collapsed' => TRUE, '#weight' => ++ $module_weight, ); if (count($results) == 0) { $results[] = t('No Problems Found'); } else { $form[$name]['#collapsed'] = FALSE; } $form[$name]['output'] = array( '#value' => theme('coder', $name, $filename, $results), '#weight' => -1, ); // process the same directory include files if ($settings['coder_includes']) { // NOTE: convert to the realpath here so drupal_system_listing // doesn't return additional paths (i.e., try "module"). $path = str_replace('\\', '/', dirname(realpath($filename))); $offset = strpos($path, dirname($filename)); if (!isset($dups[$path])) { $dups[$path] = 1; $includefiles = drupal_system_listing('.*\.(inc|php)$', $path, 'name', 0); _coder_page_form_includes($form, $coder_args, $name, $includefiles, $offset); } } } } // prepend any output with the list of code reviews performed if (!$form) { $form = array('#value' => t('No modules found')); } // prepend the settings form $form['settings'] = array( '#type' => 'fieldset', '#title' => t('Settings'), '#collapsible' => TRUE, '#collapsed' => TRUE, '#weight' => -1, '#prefix' => t('
Expand the Settings below to select options for this code review, or change the default settings for all code reviews.
', array('@default' => url('admin/settings/coder'))), ); $form['settings'][] = $coder_form; $form['settings']['submit'] = array( '#type' => 'submit', '#value' => t('Submit'), ); } return $form; } function _coder_page_form_includes(&$form, $coder_args, $name, $files, $offset) { foreach ($files as $file) { $filename = drupal_substr($file->filename, $offset); $coder_args['#filename'] = $filename; $results = do_coder_reviews($coder_args); // output the results in a collapsible fieldset $form[$name][$filename] = array( '#type' => 'fieldset', '#title' => $filename, '#collapsible' => TRUE, '#collapsed' => TRUE, '#weight' => ++ $weight, ); if (count($results) == 0) { $results[] = t('No Problems Found'); } else { $form[$name][$filename]['#collapsed'] = FALSE; $form[$name]['#collapsed'] = FALSE; } $form[$name][$filename]['output'] = array( '#value' => theme('coder', $name, $filename, $results), ); } } function _coder_modified() { static $_coder_mtime; if (!isset($_coder_mtime)) { $path = drupal_get_path('module', 'coder'); $includefiles = drupal_system_listing('.*\.(inc|module)$', $path, 'name', 0); $_coder_mtime = 0; foreach ($includefiles as $file) { $mtime = filemtime(realpath($file->filename)); if ($mtime > $_coder_mtime) { $_coder_mtime = $mtime; } } } return $_coder_mtime; } function do_coder_reviews($coder_args) { // the cache is still experimental, so users must enable it if ($use_cache = variable_get('coder_cache', 1)) { // cache the results because: $cache_key = 'coder:'. implode(':', array_keys($coder_args['#reviews'])) . $coder_args['#severity'] .':'. $coder_args['#filename']; $cache_mtime = filemtime(realpath($coder_args['#filename'])); if ($cache_serialized_results = cache_get($cache_key)) { $cache_results = unserialize($cache_serialized_results->data); if ($cache_results['mtime'] == $cache_mtime && _coder_modified() < $cache_serialized_results->created) { return $cache_results['results']; } } } $results = array(); // skip php include files when the user requested severity is above minor if (drupal_substr($coder_args['#filename'], -4) == '.php') { if ($coder_args['#severity'] > 1) { return $results; } } // 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; } } // sort the results ksort($results, SORT_NUMERIC); } else { _coder_error_msg($results, t('Could not read the file'), 'critical'); } // save the results in the cache if ($use_cache) { $cache_results = array( 'mtime' => $cache_mtime, 'results' => $results, ); cache_set($cache_key, 'cache', serialize($cache_results)); } return $results; } function _coder_read_and_parse_file(&$coder_args) { // get the path to the module file if ($filepath = realpath($coder_args['#filename'])) { // read the file $content = file_get_contents($filepath); $content_length = drupal_strlen($content); // 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]); } $lineno ++; $beginning_of_line = 1; continue; } $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] .= '>'; $pos ++; } // 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 } else { 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; $pos ++; } break; 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 _coder_severity($severity_name, $default_value = 5) { // NOTE: implemented this way in hopes that it is faster than a php switch if (!isset($severity_names)) { $severity_names = array( 'minor' => 1, 'normal' => 5, 'critical' => 9, ); } if (isset($severity_names[$severity_name])) { return $severity_names[$severity_name]; } return $default_value; } function _coder_severity_name($coder_args, $review, $rule) { // NOTE: warnings in php includes are suspicious because // php includes are frequently 3rd party products if (substr($coder_args['#filename'], -4) == '.php') { return 'minor'; } // get the severity as defined by the rule if (isset($rule['#severity'])) { return $rule['#severity']; } // if it's not defined in the rule, then it can be defined by the review if (isset($review['#severity'])) { return $review['#severity']; } // use the default return 'normal'; } function do_coder_review($coder_args, $review) { $results = array(); if ($review['#rules']) { // get the review's severity, used when the rule severity is not defined $default_severity = _coder_severity($review['#severity']); foreach ($review['#rules'] as $rule) { // perform the review if above the user requested severity $severity = _coder_severity($rule['#severity'], $default_severity); if ($severity >= $coder_args['#severity']) { 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, $review, $rule, $lines, $results); break; case 'grep': do_coder_review_grep($coder_args, $review, $rule, $lines, $results); break; case 'callback': do_coder_review_callback($coder_args, $review, $rule, $lines, $results); break; } } } } } return $results; } function do_coder_review_regex(&$coder_args, $review, $rule, $lines, &$results) { if ($regex = $rule['#value']) { $regex = '/'. $regex .'/i'; 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)) { continue 2; } } } $line = $coder_args['#all_lines'][$lineno]; $severity_name = _coder_severity_name($coder_args, $review, $rule); _coder_error($results, $rule, $severity_name, $lineno, $line); } } } } function _coder_error(&$results, $rule, $severity_name, $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 report this !warning', array( '@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, $severity_name, $lineno, $line); } function _coder_error_msg(&$results, $warning, $severity_name, $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. global $_coder_errno; $key = ($lineno + 1) * 10000 + ($_coder_errno ++); $results[$key] = theme('coder_warning', $warning, $severity_name, $lineno + 1, $line); } function do_coder_review_grep(&$coder_args, $review, $rule, $lines, &$results) { if ($regex = $rule['#value']) { $regex = '/'. $regex .'/i'; foreach ($lines as $lineno => $line) { if (preg_match($regex, $line)) { return; } } $severity_name = _coder_severity_name($coder_args, $review, $rule); _coder_error($results, $rule, $severity_name); } } function do_coder_review_callback(&$coder_args, $review, $rule, $lines, &$results) { if ($function = $rule['#value']) { if (function_exists($function)) { call_user_func_array($function, array(&$coder_args, $review, $rule, $lines, &$results)); } } } function _coder_is_drupal_core_module($module) { static $core; if (!isset($core)) { $core = array( 'aggregator' => 1, 'archive' => 1, 'block' => 1, 'blog' => 1, 'blogapi' => 1, 'book' => 1, 'color' => 1, 'comment' => 1, 'contact' => 1, 'drupal' => 1, 'filter' => 1, 'forum' => 1, 'help' => 1, 'legacy' => 1, 'locale' => 1, 'menu' => 1, 'node' => 1, 'page' => 1, 'path' => 1, 'ping' => 1, 'poll' => 1, 'profile' => 1, 'search' => 1, 'statistics' => 1, 'system' => 1, 'taxonomy' => 1, 'throttle' => 1, 'tracker' => 1, 'upload' => 1, 'user' => 1, 'watchdog' => 1, ); } return $core[$module->name]; } /** * Theming functions below... */ function theme_coder($name, $filename, $results) { $output = '

'. basename($filename) .'

'; if (count($results)) { $output .= theme('item_list', $results); } $output .= '
'; return $output; } function theme_coder_warning($warning, $severity_name, $lineno = 0, $line = '') { if ($lineno) { $warning = t('Line @number: !warning', array('@number' => $lineno, '!warning' => $warning)); if ($line) { $warning .= '
'. check_plain($line) .'
'; } } $class = "coder-warning"; if ($severity_name) { $class .= " coder-$severity_name"; } $path = drupal_get_path('module', 'coder'); $img = theme('image', $path ."/images/$severity_name.png", t('severity: @severity', array('@severity' => $severity_name)), '', array('align' => 'right', 'class' => 'coder'), FALSE); return '
'. $img . $warning .'
'; } function theme_drupalapi($function, $version = 'HEAD') { return l($function, "http://api.drupal.org/api/$version/function/$function"); }