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_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( 'path' => 'admin/coder', 'title' => t('Code review'), '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, ); } } 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'], "admin/coder/$name"); } } } } function _coder_default_reviews() { return drupal_map_assoc(array('drupal', 'security')); } /** * 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()), ); // 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; if (!$active && $module->status) { $name .= ' (active)'; } $module_options[$module->name] = l($name, "admin/coder/$name"); 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??? /* $form['coder_nocode'] = array( '#type' => 'checkbox', '#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), ); */ return $form; } /** * Implementation of settings page for Drupal 5 */ function coder_admin_settings() { return system_settings_form(coder_settings()); } /** * 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()); foreach ($selected_reviews as $name => $checked) { if ($checked) { $reviews[$name] = $avail_reviews[$name]; } } // get the list of the modules $requested_module = arg(2); $sql = "SELECT name, filename, status FROM {system} WHERE type = 'module'"; if (!$requested_module && $active == 1) { $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); $do_includes = 1; } else { $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 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"). $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); } } } } } // 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']); } $output = '
'. theme('reviews', $items, t('The code was checked using the following review guidelines')) .'
'. $output; } return $output; } function do_coder_reviews($coder_args) { $results = array(); // get the path to the module file $filename = $coder_args['#filename']; if ($filename && $filepath = realpath($filename)) { // read the file $orig_lines = file($filepath); $lines = $orig_lines; // strip single line ?_> <_?php // NOTE: I can't put the real strings here in the comments because // php processes them inside comments and breaks the file $lines = preg_replace('/\?'.'>[^<]*(<\?php)/', '', $lines); // strip html code inside multi-line ?_> <_?php tags foreach ($lines as $lineno => $line) { if (isset($in_html)) { // catch end-of multi-line embedded html if (preg_match("/^[^<]*<\?php(.*)/", $line, $match)) { $line = $match[1]; unset($in_html); } else { $line = ''; } } if (preg_match('/^([^>]*)\?'.'>.*/', $line, $match)) { // start-of multi-line embedded html $line = $match[1]; $in_html = 1; } // save the new line, without the comments $lines[$lineno] = $line; } // replace strings inside quotes with less complicated strings // many thanks to http://ad.hominem.org/log/2005/05/quoted_strings.php foreach (array('\'', '"') as $c) { $regex = '['. $c .']([^'. $c .'\\\\]*(?:\\\\.[^'. $c .'\\\\]*)*)['. $c .']'; $lines = preg_replace("/$regex/", "$c$c", $lines); } // strip single line comments // NOTE: do this after quote processing // to avoid removing comments that are actually in quotes $lines = preg_replace('/(\/\/[^:].*|\/\*.*\*\/)/', '', $lines); // remove multi-line quotes and comments (original code from code-style.pl) foreach ($lines as $lineno => $line) { if (isset($in_quote)) { // catch end-of multi-line quotes if (preg_match("/^[^\\$in_quote]*$in_quote/", $line)) { // HELP: This doesn't make sense to me, but appears to work. // Can this be simplified to look line the php code? // Why don't I need to put the quote in the preg_replace replacement // argument? Where's the extra quote coming from? $line = $quote_line . preg_replace("/^[^\\$in_quote]*$in_quote/", '', $line, 1); unset($in_quote); } else { $line = ''; } } elseif (isset($in_comment)) { // catch end-of multi-line comments if (preg_match('/.*\*\//', $line)) { $line = preg_replace('/.*\*\//', '', $line); unset($in_comment); } else { $line = ''; } } if (preg_match('/[^\']\'[^\']*$/', $line)) { // start-of multi-line single-quote $quote_line = rtrim(preg_replace('/([^\'])\'.*/', '$1\'', $line, 1)); $line = ''; $in_quote = '\''; } elseif (preg_match('/[^"]"[^"]*$/', $line)) { // start of multi-line double-quote $quote_line = rtrim(preg_replace('/([^"])[^"]".*/', '$1"', $line, 1)); $line = ''; $in_quote = '"'; } if (preg_match('/\/\*.*/', $line)) { // start of multi-line comment $line = preg_replace('/\/\*.*/', '', $line); $in_comment = 1; } // save the new line, without the comments $lines[$lineno] = $line; } // add the files lines to the arguments $coder_args['#orig_lines'] = $orig_lines; $coder_args['#lines'] = $lines; // now 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); } return $results; } function do_coder_review($coder_args, $review) { $results = array(); if ($review['#rules']) { foreach ($review['#rules'] as $rule) { $lines = $coder_args[isset($rule['#original']) ? '#orig_lines' : '#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; } } } return $results; } function do_coder_review_regex(&$coder_args, $rule, $lines, &$results) { if ($regex = $rule['#value']) { $regex = '/'. $regex .'/i'; foreach ($lines as $lineno => $line) { if (preg_match($regex, $line, $matches)) { // NOTE: I'd prefer to do this with regex strings, but there are a // couple in coder_drupal.inc that I just can't figure out with this. // don't match some regex's if ($not = $rule['#not']) { foreach ($matches as $match) { if (preg_match('/'. $not .'/i', $match)) { continue 2; } } } $orig_line = $coder_args['#orig_lines'][$lineno]; _coder_error($results, $rule, $lineno, $orig_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 !report this !warning', array( '!report' => l('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. global $_coder_errno; $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; } } _coder_error($results, $rule); } } 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)); } } } /** * Theming functions below... */ function theme_reviews($items, $message = '') { return $message . theme('item_list', $items, NULL, 'ol'); } function theme_coder($name, $filename, $results) { $title = ($_GET['q'] == "admin/coder/$name") ? $filename : l($name, "admin/coder/$name"); $output = '

'. $title .'

'; if (count($results)) { $output .= theme('item_list', $results); } $output .= '
'; return $output; } function theme_coder_warning($warning, $lineno = 0, $line = '') { if ($lineno) { $warning = t('Line @number: !warning', array('@number' => $lineno, '!warning' => $warning)); if ($line && !variable_get('coder_nocode', 0)) { $warning .= '
'. check_plain($line) .'
'; } } return '
'. $warning .'
'; } function theme_drupalapi($function, $version = 'HEAD') { return l($function, "http://api.drupal.org/api/$version/function/$function"); }