api.drupal.org).', array('@api' => 'http://api.drupal.org')); case 'drush:coder': return t('coder [summary] [@reviews] [minor|major|critical] [active|core|all|default|] [no-]', array('@reviews' => implode('|', array_keys(_coder_reviews())))); } } /** * Get all of the code review modules, including contributions. */ function _coder_reviews() { return module_invoke_all('reviews'); } /** * Implementation of hook_reviews(). */ function coder_reviews() { global $_coder_reviews; if (!isset($_coder_reviews)) { $_coder_reviews = array(); $path = drupal_get_path('module', 'coder') .'/includes'; $files = drupal_system_listing('coder_.*\.inc$', $path, 'filename', 0); foreach ($files as $file) { require_once('./'. $file->filename); $function = $file->name .'_reviews'; if (function_exists($function)) { if ($review = call_user_func($function)) { $_coder_reviews = array_merge($_coder_reviews, $review); } } } } return $_coder_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', 'view code review all'); } /** * Implementation of hook_menu(). */ function coder_menu() { $items = array(); $items['coder'] = array( 'title' => 'Code review', 'page callback' => 'coder_page', 'access arguments' => array('view code review'), 'type' => MENU_NORMAL_ITEM, ); $items['coder/settings'] = array( 'title' => 'Selection Form', 'page callback' => 'coder_page', 'access arguments' => array('view code review'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -2, ); $items['coder/default'] = array( 'title' => 'Default', 'page callback' => 'coder_page', 'access arguments' => array('view code review'), 'type' => MENU_LOCAL_TASK, 'weight' => -1, ); $items['coder/core'] = array( 'title' => 'Core', 'page callback' => 'coder_page', 'access arguments' => array('view code review'), 'type' => MENU_LOCAL_TASK, ); $items['coder/active'] = array( 'title' => 'Active', 'page callback' => 'coder_page', 'access arguments' => array('view code review'), 'type' => MENU_LOCAL_TASK, ); $items['coder/all'] = array( 'title' => 'All', 'page callback' => 'coder_page', 'access arguments' => array('view code review all'), 'type' => MENU_LOCAL_TASK, 'weight' => 1, ); $items['coder/patches'] = array( 'title' => t('Patches'), 'page callback' => 'coder_page', 'access arguments' => array('view code review'), 'type' => MENU_LOCAL_TASK, 'weight' => 2, ); $items['admin/settings/coder'] = array( 'title' => 'Code review', 'description' => 'Select code review plugins and modules', 'page callback' => 'drupal_get_form', 'page arguments' => array('coder_admin_settings'), 'access arguments' => array('administer site configuration'), ); return $items; } /** * Implementation of hook_form_alter(). * * Modify the module display view by adding a Coder Review link to every * module description. */ function coder_form_alter(&$form, $form_state, $form_id) { if ($form_id == 'system_modules' && (!isset($form['#theme']) || $form['#theme'] != 'confirm_form')) { if (user_access('view code review')) { $path = drupal_get_path('module', 'coder'); drupal_add_css($path .'/coder.css', 'module'); foreach ($form['name'] as $name => $data) { $description = isset($form['description'][$name]['#value']) ? $form['description'][$name]['#value'] : $data['#value']; $form['description'][$name]['#value'] = $description .' ('. l(t('Code Review'), "coder/$name") .')'; } } } } /** * Helper functions for settings form. */ function _coder_default_reviews() { return drupal_map_assoc(array('style', 'sql', 'comment', 'security')); } /** * Build settings form API array for coder. * * Generates a form with the default reviews and default modules/themes to * run Coder on. * * @note * Actual forms may have additional sections added to them, this * is simply a base. * * @param $settings * Settings array for coder in the format of _coder_get_default_settings(). * @param $system * Array of module and theme information, in form string theme/module * name => boolean TRUE if checked by coder already. * @param $files * Associative array of files, in form string theme/module name => string * filename to check. * @return * Array for form API for the settings box. */ function _coder_settings_form($settings, &$system, &$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. // Maintain a secondary list based on #title only, to make sorting possible. $reviews = _coder_reviews(); foreach ($reviews as $name => $review) { $review_options[$name] = isset($review['#link']) ? l($review['#title'], $review['#link']) : $review['#title']; if (isset($review['#description'])) { $review_options[$name] .= ' ('. $review['#description'] .')'; } $review_sort[$name] = $review['#title']; } // Sort the reviews by #title. asort($review_sort); foreach ($review_sort as $name => $review) { $review_sort[$name] = $review_options[$name]; } // What reviews should be used? $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_sort, '#description' => t('apply the checked coding reviews'), '#default_value' => $settings['coder_reviews'], ); // What severities should be used? $form['coder_reviews_group']['coder_severity'] = array( '#type' => 'radios', '#options' => array( SEVERITY_MINOR => 'minor (most)', SEVERITY_NORMAL => 'normal', SEVERITY_CRITICAL => 'critical (fewest)' ), '#description' => t('show warnings at or above the severity warning level'), '#default_value' => $settings['coder_severity'], ); if ($settings['coder_patches']) { // Display what to review options. $form['coder_what'] = array( '#type' => 'fieldset', '#title' => t('What to review'), '#collapsible' => TRUE, '#collapsed' => FALSE, ); $form['coder_what']['coder_patch_help'] = array( '#value' => '

'. t('All patches must be in unified format. It\'s also preferable for the patch to be created with the "-p" option, which shows the function closest to the code change. Without this, some function dependent tests may not be triggered. Coder offers no guarantee that the patch will apply cleanly or will function correctly.') .'

', '#weight' => -4, ); $form['coder_what']['coder_patch_link'] = array( '#type' => 'textfield', '#title' => t('Link to patch'), '#default_value' => isset($settings['coder_patch_link']) ? $settings['coder_patch_link'] : '', '#weight' => -3, ); $form['coder_what']['coder_patch_help'] = array( '#value' => '

'. t('Or') .'

', '#weight' => -2, ); $form['coder_what']['coder_patch_text'] = array( '#type' => 'textarea', '#title' => t('Patch text'), '#rows' => 20, '#weight' => -1, '#default_value' => isset($settings['coder_patch_text']) ? $settings['coder_patch_text'] : '', '#attributes' => array('wrap' => 'OFF'), ); $form['coder_patches'] = array( '#type' => 'value', '#value' => 1, ); $in_patch = 0; $patch = $link_contents = $textarea_contents = ''; $patches = array(); if (!empty($settings['coder_patch_link'])) { $link_contents = file_get_contents($settings['coder_patch_link']); } if (!empty($settings['coder_patch_text'])) { $textarea_contents = $settings['coder_patch_text']; } $patch_contents = $link_contents ."\n". $textarea_contents; $lines = preg_split("/(\r\n|\n)/", $patch_contents); foreach ($lines as $line) { if ($line == '') { continue; } if (preg_match("/^\+\+\+\s+([\w\.\-\/]+\s)/", $line, $matches)) { if ($patch) { $patches[$filename .": ". $patch_line_numbers] = $patch; $system[$filename .": ". $patch_line_numbers] = $filename; $patch = ''; } $filename = $matches[1]; } else if (preg_match("/^(@@\s+\-\d+,\d+\s+\+\d+,\d+\s+@@)\s*(function\s([\w]+).*?)*$/", $line, $matches)) { if ($patch) { $patches[$filename .": ". $patch_line_numbers] = $patch; $system[$filename .": ". $patch_line_numbers] = $filename; $patch = ''; } if ($matches[3]) { $current_function = $matches[3]; $patch = 'function '. $current_function ."() {\n"; } $patch_line_numbers = $matches[1]; } else if (preg_match("/^\+[^\+]+/", $line)) { $patch .= ltrim($line, '+') ."\n"; $in_patch = 1; } else if (preg_match("/^\s/", $line)) { $patch .= substr($line, 1) ."\n"; $in_patch = 1; } else { $in_patch = 0; } } if ($patch) { $patches[$filename .": ". $patch_line_numbers] = $patch; $system[$filename .": ". $patch_line_numbers] = $filename; $patch = ''; } $files = $patches; } else { // Get the modules and theme. $sql = 'SELECT name, filename, type, status FROM {system} WHERE type=\'module\' OR type=\'theme\' ORDER BY weight ASC, filename ASC'; $result = db_query($sql); $system_modules = array(); $system_themes = array(); while ($system = db_fetch_object($result)) { $display_name = $system->name; if ($system->status) { $display_name .= t(' (active)'); $system_active[$system->name] = $system->name; } if (_coder_is_drupal_core($system)) { $display_name .= t(' (core)'); $system_core[$system->name] = $system->name; } if ($system->type == 'module') { $system_modules[$system->name] = $system->name; } else { $system_themes[$system->name] = $system->name; } $system_links[$system->name] = l($display_name, "coder/$system->name"); $files[$system->name] = $system->filename; } asort($system_links); // Display what to review options. $form['coder_what'] = array( '#type' => 'fieldset', '#title' => t('What to review'), '#collapsible' => TRUE, '#collapsed' => FALSE, ); // NOTE: Should rename var. $form['coder_what']['coder_active_modules'] = array( '#type' => 'checkbox', '#default_value' => isset($settings['coder_active_modules']) ? $settings['coder_active_modules'] : 0, '#title' => t('active modules and themes'), ); $form['coder_what']['coder_core'] = array( '#type' => 'checkbox', '#default_value' => isset($settings['coder_core']) ? $settings['coder_core'] : 0, '#title' => t('core files (php, modules, and includes)'), ); $form['coder_what']['coder_includes'] = array( '#type' => 'checkbox', '#default_value' => isset($settings['coder_includes']) ? $settings['coder_includes'] : 0, '#title' => t('include files (.inc, .php, .test, and .install files)'), ); $form['coder_what']['coder_includes_exclusion_fieldset'] = array( '#type' => 'fieldset', '#title' => t('include file exclusions'), '#collapsible' => TRUE, '#collapsed' => TRUE, ); $form['coder_what']['coder_includes_exclusion_fieldset']['coder_includes_exclusions'] = array( '#rows' => 3, '#type' => 'textarea', '#default_value' => $settings['coder_includes_exclusions'], '#description' => t('List file names or paths, one per line, which should be excluded (only valid if "include files" is checked above). For example, modules/system/*.php will exclude all php files in the modules/system directory.'), ); if (arg(0) == 'admin') { $form['coder_what']['coder_cache'] = array( '#type' => 'checkbox', '#default_value' => isset($settings['coder_cache']) ? $settings['coder_cache'] : 0, '#title' => t('use the coder cache'), ); } // Display the modules in a fieldset. $form['coder_what']['coder_modules'] = array( '#type' => 'fieldset', '#title' => t('Select specific modules'), '#collapsible' => TRUE, '#collapsed' => TRUE, 'checkboxes' => array( '#theme' => 'cols', '#cols' => 2, ), ); if (isset($settings['coder_all'])) { $modules = $system_modules; } elseif (isset($settings['coder_active_modules']) && $settings['coder_active_modules']) { if (isset($settings['coder_core']) && $settings['coder_core']) { $modules = array_intersect($system_active, $system_core); $modules = array_intersect($modules, $system_modules); } else { $modules = array_intersect($system_active, $system_modules); } } elseif (isset($settings['coder_core']) && $settings['coder_core']) { $modules = array_intersect($system_core, $system_modules); } elseif (isset($settings['coder_active_modules']) && $settings['coder_active_modules']) { $modules = array_intersect($system_active, $system_modules); } else { $modules = isset($settings['coder_modules']) && is_array($settings['coder_modules']) ? $settings['coder_modules'] : array(); } // Display the themes in a fieldset. $form['coder_what']['coder_themes'] = array( '#type' => 'fieldset', '#title' => t('Select specific themes'), '#collapsible' => TRUE, '#collapsed' => TRUE, 'checkboxes' => array( '#theme' => 'cols', ), ); if (isset($settings['coder_all'])) { $themes = $system_themes; } elseif (isset($settings['coder_active_modules']) && $settings['coder_active_modules']) { if (isset($settings['coder_core']) && $settings['coder_core']) { $themes = array_intersect($system_active, $system_core); $themes = array_intersect($themes, $system_themes); } else { $themes = array_intersect($system_active, $system_themes); } } elseif (isset($settings['coder_core']) && $settings['coder_core']) { $themes = array_intersect($system_core, $system_themes); } elseif (isset($settings['coder_active_modules']) && $settings['coder_active_modules']) { $themes = array_intersect($system_active, $system_themes); } else { $themes = isset($settings['coder_themes']) && is_array($settings['coder_themes']) ? $settings['coder_themes'] : array(); } foreach ($system_links as $name => $link) { $classes = array(); if (in_array($name, $system_active)) { $classes[] = 'coder-active'; } if (in_array($name, $system_core)) { $classes[] = 'coder-core'; } if (in_array($name, $system_themes)) { $type = 'theme'; $default_value = isset($themes[$name]); } else { $type = 'module'; $default_value = isset($modules[$name]); } $form['coder_what']["coder_${type}s"]['checkboxes']["coder_${type}s-$name"] = array( '#type' => 'checkbox', '#title' => $link, '#default_value' => $default_value, '#attributes' => array('class' => implode(' ', $classes)), ); } $system = array_merge($modules, $themes); } return $form; } if (!function_exists('theme_cols')) { /** * Implement theme_cols to theme the radiobuttons and checkboxes form * elements in a table column. */ function theme_cols($form) { $total = 0; $cols = isset($form['#cols']) ? $form['#cols'] : 3; foreach ($form as $element_id => $element) { if ($element_id[0] != '#') { $total ++; } } $total = (int) (($total % $cols) ? (($total + $cols - 1) / $cols) : ($total / $cols)); $pos = 0; $rows = array(); foreach ($form as $element_id => $element) { if ($element_id[0] != '#') { $pos ++; $row = $pos % $total; $col = $pos / $total; if (!isset($rows[$row])) { $rows[$row] = array(); } $rows[$row][$col] = drupal_render($element); } } return theme('table', array(), $rows); } } /** * Implementation of settings page for Drupal 5. */ function coder_admin_settings() { $settings = _coder_get_default_settings(); $form = _coder_settings_form($settings, $system, $files); $form['help'] = array( '#type' => 'markup', '#value' => t('After setting these defaults, use coder to perform code reviews.', array('@url' => url('coder'))), '#weight' => -1, ); $form['#submit'][] = 'coder_settings_form_submit'; return system_settings_form($form); } /** * Callback function for settings page in Drupal 5. */ function coder_settings_form_submit($form, &$form_state) { $form_state['storage'] = $form_state['values']; variable_set('coder_modules', _coder_settings_array($form_state, 'module')); variable_set('coder_themes', _coder_settings_array($form_state, 'theme')); } /** * Generate settings array for either modules or themes. * * @param $form_state * Form array passed to submit function (note: entries that are processed. * are removed for efficiency's sake). * @param $type * String type to generate settings for, either 'module' or 'theme'. * @return * Settings lookup array in form module/theme name => 1 */ function _coder_settings_array(&$form_state, $type) { $typekey = "coder_{$type}s-"; $typelen = strlen($typekey); $systems = array(); foreach ($form_state['storage'] as $key => $value) { if (substr($key, 0, $typelen) == $typekey) { if ($value == 1) { $system = substr($key, $typelen); $systems[$system] = 1; } unset($form_state['storage'][$key]); } } return $systems; } /** * Implementation of code review page. */ function coder_page() { $output = '
'. coder_help('coder#disclaimer', array()) .'
'; $output .= drupal_get_form('coder_page_form'); return $output; } /** * Returns a active settings array for coder. * * @note * The name is a misnomer, but is a largely correct characterization * for most of Coder's settings as the variables usually do not exist. * * @param $args * String settings argument, can be 'settings', 'active', 'core', 'all' * and 'default'. * @return * Associative array of settings in form setting name => setting value. */ function _coder_get_default_settings($args = 'default') { $settings['coder_reviews'] = variable_get('coder_reviews', _coder_default_reviews()); $settings['coder_severity'] = variable_get('coder_severity', SEVERITY_NORMAL); $settings['coder_cache'] = variable_get('coder_cache', 1); // Determine any options based on the passed in URL. switch ($args) { case 'settings': $settings['coder_includes'] = 1; break; case 'active': $settings['coder_active_modules'] = 1; break; case 'core': $settings['coder_core'] = 1; $settings['coder_includes'] = 1; break; case 'all': $settings['coder_core'] = 1; $settings['coder_includes'] = 1; $settings['coder_all'] = 1; break; case 'patches': $settings['coder_patches'] = 1; break; case 'default': $settings['coder_active_modules'] = variable_get('coder_active_modules', 1); $settings['coder_core'] = variable_get('coder_core', 0); $settings['coder_includes'] = variable_get('coder_includes', 0); $settings['coder_includes_exclusions'] = variable_get('coder_includes_exclusions', ''); $settings['coder_modules'] = variable_get('coder_modules', array()); $settings['coder_themes'] = variable_get('coder_themes', array()); break; default: $settings['coder_includes'] = 1; $settings['coder_includes_exclusions'] = variable_get('coder_includes_exclusions', ''); // TODO: Does this need to go into coder_themes sometimes? $settings['coder_modules'] = array($args => $args); break; } return $settings; } /** * Implementation of hook_submit(). */ function coder_page_form_submit($form, &$form_state) { $form_state['storage'] = $form_state['values']; } /** * Implementation of hook_form(). * * Implements coder's main form, in which a user can select reviews and * modules/themes to run them on. */ function coder_page_form($form_state) { if (isset($form_state['storage'])) { $settings = $form_state['storage']; $settings['coder_modules'] = _coder_settings_array($form_state, 'module'); $settings['coder_themes'] = _coder_settings_array($form_state, 'theme'); 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, $system, $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 (isset($settings['coder_core']) && $settings['coder_core']) { $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, 0, $settings['coder_includes_exclusions']); $form['core_includes'] = array( '#type' => 'fieldset', '#title' => 'core (includes)', '#collapsible' => TRUE, '#collapsed' => TRUE, '#weight' => ++ $module_weight, ); $includefiles = drupal_system_listing('.*\.inc$', 'includes', 'filename', 0); _coder_page_form_includes($form, $coder_args, 'core_includes', $includefiles, 0, $settings['coder_includes_exclusions']); } // Loop through the selected modules and themes. if (isset($system)) { // Used to avoid duplicate includes. $dups = array(); $stats = array(); $patch = ''; foreach ($system as $name => $checked) { if ($checked) { // Process this one file. $filename = $files[$name]; if (!$filename) { drupal_set_message(t('Code Review file for %module not found', array('%module' => $name))); continue; } if ($settings['coder_patches']) { $patch = $filename; $filename = $name; } $coder_args = array( '#reviews' => $reviews, '#severity' => $settings['coder_severity'], '#filename' => $filename, '#patch' => $patch, ); $results = do_coder_reviews($coder_args); $stats[$filename] = $results['#stats']; unset($results['#stats']); // Output the results in a collapsible fieldset. $form[$name] = array( '#type' => 'fieldset', '#title' => $filename, '#collapsible' => TRUE, '#collapsed' => TRUE, '#weight' => ++ $module_weight, ); if (empty($results)) { $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 (!empty($settings['coder_includes'])) { // NOTE: Convert to the realpath here so drupal_system_listing // doesn't return additional paths (i.e., try "module"). if ($path = str_replace('\\', '/', dirname(realpath($filename)))) { $offset = strpos($path, dirname($filename)); if (!isset($dups[$path])) { if (substr($filename, -7) == '.module') { $coder_args['#php_minor'] = 1; } $dups[$path] = 1; $includefiles = drupal_system_listing('.*\.(inc|php|install|test)$', $path, 'filename', 0); $stats[$filename]['#includes'] = _coder_page_form_includes($form, $coder_args, $name, $includefiles, $offset, $settings['coder_includes_exclusions']); } } } } } if (count($stats)) { $summary = array('files' => 0, 'minor' => 0, 'normal' => 0, 'critical' => 0); foreach ($stats as $stat) { if (isset($stat['#includes'])) { foreach ($stat['#includes'] as $includestat) { $summary['files'] ++; $summary['minor'] += $includestat['minor']; $summary['normal'] += $includestat['normal']; $summary['critical'] += $includestat['critical']; } } $summary['files'] ++; } $display = array(); if ($settings['coder_patches']) { $display[] = t('Coder found @count patches', array('@count' => count($stats))); } else { $display[] = t('Coder found @count projects', array('@count' => count($stats))); $display[] = t('@count files', array('@count' => $summary['files'])); } foreach (array('critical', 'normal', 'minor') as $severity_name) { if ($summary[$severity_name] > 0) { $display[] = t('@count %severity_name warnings', array('@count' => $summary[$severity_name], '%severity_name' => $severity_name)); } } drupal_set_message(implode(', ', $display)); if (function_exists('_coder_drush_is_option') && _coder_drush_is_option('drush')) { print coder_print_drush_messages(); } } } // Prepend the settings form. $form['settings'] = array( '#type' => 'fieldset', '#title' => t('Selection form'), '#collapsible' => TRUE, '#collapsed' => isset($form), '#weight' => -1, ); if ($form['settings']['#collapsed']) { $form['settings']['#prefix'] = t('
Use the Selection Form to select options for this code review, or change the Default Settings and use the Default tab above.
', array('@settings' => url('admin/settings/coder'), '@default' => url('coder/default'))); } $form['settings'][] = $coder_form; $form['settings']['submit'] = array( '#type' => 'submit', '#value' => t('Submit'), ); } return $form; } /** * Add results to form array for display on form page. * * @param $form * Form array variable to be modified. * @param $coder_args * Coder settings, see do_coder_reviews() for details. * @param $name * Name of form element. * @param $files * Array of file objects to check and display the results of, see * file_scan_directory(). * @param $offset * Integer offset to munge filenames with. * @param $exclusions * Exclusions that should be ignored in $files. * @return * Statistics array in form: string filename => array value of * '#stats' from do_coder_reviews(). */ function _coder_page_form_includes(&$form, $coder_args, $name, $files, $offset, $exclusions) { $stats = array(); $coder_args['#name'] = $name; $weight = 0; $exclusions = str_replace(array("\r\n", "\r", "\n", '/', '*', '.'), array('|', '|', '|', '\\/', '.*', '\\.'), $exclusions); foreach ($files as $file) { if (!empty($exclusions) && preg_match("/$exclusions/", $file->filename)) { continue; // don't review this files. } $filename = drupal_substr($file->filename, $offset); $coder_args['#filename'] = $filename; $results = do_coder_reviews($coder_args); $stats[$filename] = $results['#stats']; unset($results['#stats']); // Output the results in a collapsible fieldset. $form[$name][$filename] = array( '#type' => 'fieldset', '#title' => $filename, '#collapsible' => TRUE, '#collapsed' => TRUE, '#weight' => ++ $weight, ); if (empty($results)) { $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), ); } return $stats; } /** * Return last modification timestamp of coder and all of its dependencies. */ function _coder_modified() { static $_coder_mtime; if (!isset($_coder_mtime)) { $path = drupal_get_path('module', 'coder'); $includefiles = drupal_system_listing('.*\.(inc|module)$', $path .'/includes', 'filename', 0); $_coder_mtime = filemtime(realpath($path .'/coder.module')); foreach ($includefiles as $file) { $mtime = filemtime(realpath($file->filename)); if ($mtime > $_coder_mtime) { $_coder_mtime = $mtime; } } } return $_coder_mtime; } /** * Perform batch coder reviews for multiple files. * * @param $coder_args * Array of coder arguments, valid arguments are: * - '#reviews' => array list of reviews to perform, see _coder_reviews(); * - '#severity' => integer magic number, see constants SEVERITY_*; * - '#filename' => string filename to check, * - '#patch' => string patch lines to check, * @return * Array of results, in form: * - '#stats' => Array with error counts for all severities, in form * 'minor' => integer count, 'normal' => integer count; * 'critical' => integer count; * - integer ID => HTML error for display. */ function do_coder_reviews($coder_args) { if ($use_cache = variable_get('coder_cache', 1) && empty($coder_args['#patch'])) { // Load the cached results if they exist. $cache_key = 'coder:'. implode(':', array_keys($coder_args['#reviews'])) . $coder_args['#severity'] .':'. $coder_args['#filename']; if (file_exists($filepath = realpath($coder_args['#filename']))) { $cache_mtime = filemtime($filepath); if ($cache_results = cache_get($cache_key)) { if ($cache_results->data['mtime'] == $cache_mtime && _coder_modified() < $cache_results->created) { return $cache_results->data['results']; } } } } $results = array('#stats' => array('minor' => 0, 'normal' => 0, 'critical' => 0)); // Skip php include files when the user requested severity is above minor. if (isset($coder_args['#php_minor']) && 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. $errors = array(); foreach ($coder_args['#reviews'] as $review) { if ($result = do_coder_review($coder_args, $review)) { foreach (array('critical', 'normal', 'minor') as $severity_level) { if (isset($result['#stats'][$severity_level])) { $results['#stats'][$severity_level] += $result['#stats'][$severity_level]; } } $errors += $result; } } // Theme the error messages. foreach ($errors as $key => $error) { if (is_numeric($key)) { $results[$key] = theme('coder_warning_msg', $error); } } // Sort the results. ksort($results, SORT_NUMERIC); } else { $results[] = theme('coder_warning', 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_results); } return $results; } /** * Parse and read a file into a format easy to validate. * * @param $coder_args * Coder arguments array variable to add file lines of code (with * trailing newlines. The following array indices are added: '#all_lines', * '#php_lines', '#allphp_lines', '#html_lines', '#quote_lines', * '#doublequote_lines', '#comment_lines'. Their names should be * self explanatory. * @return * Integer 1 if success. */ function _coder_read_and_parse_file(&$coder_args) { // Get the path to the module file. if (!empty($coder_args['#patch']) || (($filepath = realpath($coder_args['#filename'])) && file_exists($filepath))) { $in_php = 0; $in_allphp = 0; $in_comment = 0; if (empty($coder_args['#patch'])) { $content = file_get_contents($filepath) ."\n"; } else { $content = $coder_args['#patch']; if (preg_match("/^\s*\*/", $content)) { $in_comment = '*'; } else { $content = preg_replace('/^(function\s.*?(\r\n|\n)+)(\s*\*)/', '${1}/*', $content); $in_php = 1; $in_allphp = 1; } } $content_length = drupal_strlen($content); $in_comment = 0; $beginning_of_line = 0; $in_quote_html = 0; $in_backslash = 0; $in_quote = 0; $in_heredoc = 0; $in_heredoc_html = ''; $heredoc = ''; $all_lines = array(); $php_lines = array(); $allphp_lines = array(); $html_lines = array(); $quote_lines = array(); $doublequote_lines = array(); $comment_lines = array(); $this_all_lines = ''; $this_php_lines = ''; $this_allphp_lines = ''; $this_html_lines = ''; $this_quote_lines = ''; $this_doublequote_lines = ''; $this_comment_lines = ''; // 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 && $in_comment == '/') { // End C++ style comments on newline. $in_comment = 0; } // Assume that html inside quotes doesn't span newlines. $in_quote_html = 0; // Remove blank lines now, so we avoid processing them over-and-over. if ($this_all_lines != '') { if (trim($this_all_lines, "\r\n") != '') { $all_lines[$lineno] = $this_all_lines; } if (trim($this_php_lines, "\r\n") != '') { $php_lines[$lineno] = $this_php_lines; } if (trim($this_allphp_lines, "\r\n") != '') { $allphp_lines[$lineno] = $this_allphp_lines; } if (trim($this_html_lines, "\r\n") != '') { $html_lines[$lineno] = $this_html_lines; } if (trim($this_quote_lines, "\r\n") != '') { $quote_lines[$lineno] = $this_quote_lines; } if (trim($this_doublequote_lines, "\r\n") != '') { $doublequote_lines[$lineno] = $this_doublequote_lines; } if (trim($this_comment_lines, "\r\n") != '') { $comment_lines[$lineno] = $this_comment_lines; } } // Save this line and start a new line. $lineno ++; $this_all_lines = ''; $this_php_lines = ''; $this_allphp_lines = ''; $this_html_lines = ''; $this_quote_lines = ''; $this_doublequote_lines = ''; $this_comment_lines = ''; $beginning_of_line = 1; continue; } if ($this_all_lines != '') { $beginning_of_line = 0; } $this_all_lines .= $char; if ($in_php || $in_allphp) { // When in a quoted string, look for the trailing quote // strip characters in the string, replacing with '' or "". if ($in_quote) { if ($in_backslash) { $in_backslash = 0; } elseif ($char == '\\') { $in_backslash = 1; } elseif ($char == $in_quote && !$in_backslash) { $in_quote = 0; } elseif ($char == '<') { $in_quote_html = '>'; } if ($in_quote) { $this_quote_lines .= $char; if ($in_quote == '"') { $this_doublequote_lines .= $char; } if ($in_quote_html) { $this_html_lines .= $char; } } if ($char == $in_quote_html) { $in_quote_html = 0; } $this_allphp_lines .= $char; 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) { $this_all_lines .= substr($content, $pos + 1, $in_heredoc_length - 1); $in_heredoc = 0; $pos += $in_heredoc_length; } elseif ($char == '<') { $in_heredoc_html = '>'; } if ($in_heredoc && $in_heredoc_html) { $this_html_lines .= $char; } if ($in_heredoc_html && $char == $in_heredoc_html) { $in_heredoc_html = ''; } unset($char); } // Look for the ending php tag. elseif ($char == '?' && $content[$pos + 1] == '>') { unset($char); $in_php = 0; $in_allphp = 0; $this_all_lines .= '>'; $pos ++; } // When in a comment look for the trailing comment. elseif ($in_comment) { $this_comment_lines .= $char; if ($in_comment == '*' && $char == '*' && $content[$pos + 1] == '/') { $in_comment = 0; $this_all_lines .= '/'; $this_comment_lines .= '/'; $pos ++; } unset($char); // Don't add comments to php output. } else { switch ($char) { case '\'': case '"': if ($content[$pos - 1] != '\\') { $this_php_lines .= $char; $in_quote = $char; } break; case '/': $next_char = $content[$pos + 1]; if ($next_char == '/' || $next_char == '*') { unset($char); $in_comment = $next_char; $this_all_lines .= $next_char; $this_comment_lines .= '/'. $next_char; $pos ++; } break; case '<': if ($content[$pos + 1] == '<' && $content[$pos + 2] == '<') { unset($char); $this_all_lines .= '<<'; // 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; } $this_all_lines .= $char; $heredoc .= $char; } $heredoc = ''; // Replace heredoc's with an empty string. $this_php_lines .= '\'\''; $this_allphp_lines .= '\'\''; unset($char); } break; } } if (isset($char)) { $this_php_lines .= $char; $this_allphp_lines .= $char; } } else { switch ($char) { case '<': if ($content[$pos + 1] == '?') { if ($content[$pos + 2] == ' ') { $in_php = 1; $in_allphp = 1; $this_all_lines .= '? '; $pos += 2; } elseif (substr($content, $pos + 2, 3) == 'php') { $in_php = 1; $in_allphp = 1; $this_all_lines .= '?php'; $pos += 4; } break; } // FALLTHROUGH default: $this_html_lines .= $char; break; } } } if (trim($this_all_lines) != '') { $all_lines[$lineno] = $this_all_lines; } if (trim($this_php_lines) != '') { $php_lines[$lineno] = $this_php_lines; } if (trim($this_html_lines) != '') { $html_lines[$lineno] = $this_html_lines; } if (trim($this_quote_lines) != '') { $quote_lines[$lineno] = $this_quote_lines; } if (trim($this_doublequote_lines) != '') { $doublequote_lines[$lineno] = $this_doublequote_lines; } if (trim($this_comment_lines) != '') { $comment_lines[$lineno] = $this_comment_lines; } // Add the files lines to the arguments. $coder_args['#all_lines'] = $all_lines; $coder_args['#php_lines'] = $php_lines; $coder_args['#allphp_lines'] = $allphp_lines; $coder_args['#html_lines'] = $html_lines; $coder_args['#quote_lines'] = $quote_lines; $coder_args['#doublequote_lines'] = $doublequote_lines; $coder_args['#comment_lines'] = $comment_lines; return 1; } } /** * Return the integer severity magic number for a string severity. * * @param $severity_name * String severity name 'minor', 'normal', or 'critical'. * @param $default_value * Integer magic number to use if severity string is not recognized. * @return * Integer magic number, see SEVERITY_* constants. */ function _coder_severity($severity_name, $default_value = SEVERITY_NORMAL) { // NOTE: Implemented this way in hopes that it is faster than a php switch. if (!isset($severity_names)) { $severity_names = array( 'minor' => SEVERITY_MINOR, 'normal' => SEVERITY_NORMAL, 'critical' => SEVERITY_CRITICAL, ); } if (isset($severity_names[$severity_name])) { return $severity_names[$severity_name]; } return $default_value; } /** * Return string severity for a given error. * * @param $coder_args * Coder settings array, see do_coder_reviews(). * @param $review * Review array, see hook_reviews(), contains rule arrays. * @param $rule * Rule array that was triggered, see individual entries from hook_reviews(). * @return * String severity of error. */ function _coder_severity_name($coder_args, $review, $rule) { // NOTE: Warnings in php includes are suspicious because // php includes are frequently 3rd party products. if (isset($coder_args['#php_minor']) && 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'; } /** * Perform code review for a review array. * * @param $coder_args * Array coder settings, must have been prepared with _coder_read_and_parse_file(), * see do_coder_reviews() for format. * @param $review * Review array, see hook_review(). * @return * Array results, see do_coder_reviews() return value for format. */ function do_coder_review($coder_args, $review) { $results = array('#stats' => array('minor' => 0, 'normal' => 0, 'critical' => 0)); if ($review['#rules']) { // Get the review's severity, used when the rule severity is not defined. $default_severity = isset($review['#severity']) ? _coder_severity($review['#severity']) : SEVERITY_NORMAL; foreach ($review['#rules'] as $rule) { // Ignore rules based on file extensions. $filename = empty($coder_args['#patch']) ? $coder_args['#filename'] : 'coder.patch'; if (isset($rule['#filename'])) { $regex = '/'. $rule['#filename'] .'/i'; if (!preg_match($regex, $filename, $matches)) { continue; } } if (isset($rule['#filename-not'])) { $regex = '/'. $rule['#filename-not'] .'/i'; if (preg_match($regex, $filename, $matches)) { continue; } } // Perform the review if above the user requested severity. $severity = _coder_severity(isset($rule['#severity']) ? $rule['#severity'] : '', $default_severity); if ($severity >= $coder_args['#severity']) { if (isset($rule['#original'])) { // Deprecated. $lines = $coder_args['#all_lines']; } elseif (isset($rule['#source'])) { // Values: all, html, comment, allphp 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 'grep_invert': do_coder_review_grep_invert($coder_args, $review, $rule, $lines, $results); break; case 'callback': do_coder_review_callback($coder_args, $review, $rule, $lines, $results); break; } } } } } return $results; } /** * Implements do_coder_review_* for regex match. * * @param $coder_args * Coder settings array variable, see do_coder_review() for format. * @param $review * Review array the current rule belongs to, used by _coder_severity_name(). * @param $rule * Rule array being checked. * @param $lines * Pertinent source file lines according to rule's '#source' value. * @param $results * Results array variable to save errors to. */ function do_coder_review_regex(&$coder_args, $review, $rule, $lines, &$results) { if (isset($rule['#value'])) { $regexflags = isset($rule['#case-sensitive']) ? '' : 'i'; $regex = '/'. $rule['#value'] .'/'. $regexflags; $function_regex = isset($rule['#function']) ? '/'. $rule['#function'] .'/' : ''; $current_function = ''; $paren = 0; $not_regex = isset($rule['#not']) ? '/'. $rule['#not'] .'/'. $regexflags : ''; $never_regex = isset($rule['#never']) ? '/'. $rule['#never'] .'/'. $regexflags : ''; foreach ($lines as $lineno => $line) { // Some rules apply only within certain functions. if ($function_regex) { if (preg_match('/function (\w+)\(/', $line, $match)) { $current_function = $match[1]; } if (preg_match('/([{}])/', $line, $match)) { $paren += ($match[0] == '{') ? 1 : -1; } if ($paren < 0 || $current_function == '' || !preg_match($function_regex, $current_function)) { continue; } } if (preg_match($regex, $line, $matches)) { // Don't match some regex's. if ($not_regex) { foreach ($matches as $match) { if (preg_match($not_regex, $match)) { continue 2; } } } if ($never_regex) { if (preg_match($never_regex, $coder_args['#all_lines'][$lineno])) { continue; } } $line = $coder_args['#all_lines'][$lineno]; $severity_name = _coder_severity_name($coder_args, $review, $rule); _coder_error($results, $rule, $severity_name, $lineno, $line); } } } } /** * Builds an error message based on the rule that failed and other information. * * @param $results * Results array variable to save errors to. * @param $rule * Rule array that triggered the error. * @param $severity_name * String severity of error as detected by _coder_severity_name(). * @param $lineno * Line number of error. * @param $line * Contents of line that triggered error. * @param $original * Deprecated. */ function _coder_error(&$results, $rule, $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] = array('rule' => $rule, 'severity_name' => $severity_name, 'lineno' => $lineno, 'line' => $line); $results['#stats'][$severity_name] ++; } /** * Search for a string. * * @note * See do_coder_review_regex() for arguments. */ function do_coder_review_grep(&$coder_args, $review, $rule, $lines, &$results) { if (isset($rule['#value'])) { foreach ($lines as $lineno => $line) { if (_coder_search_string($line, $rule)) { $line = $coder_args['#all_lines'][$lineno]; $severity_name = _coder_severity_name($coder_args, $review, $rule); _coder_error($results, $rule, $severity_name, $lineno, $line); } } } } /** * Search for potentially missing string. * * @note * See do_coder_review_regex() for arguments. */ function do_coder_review_grep_invert(&$coder_args, $review, $rule, $lines, &$results) { if (isset($rule['#value'])) { foreach ($lines as $lineno => $line) { if (_coder_search_string($line, $rule)) { return; } } $severity_name = _coder_severity_name($coder_args, $review, $rule); _coder_error($results, $rule, $severity_name); } } /** * Allow for an arbitrary callback function to perform a review. * * @note * See do_coder_review_regex() for arguments. */ 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)); } } } /** * Search for a string. * * Uses the fastest available php function for searching. * * @param $line * Haystack. * @param $rule * Rule to process. * @return * TRUE if needle is in haystack. */ function _coder_search_string($line, $rule) { static $php5; if (!isset($php5)) { if (function_exists('stripos')) { $php5 = TRUE; } else { $php5 = FALSE; } } // Case-sensitive search with strpos() (supported everywhere). if (isset($rule['#case-sensitive'])) { return strpos($line, $rule['#value']) !== FALSE; } // Case-insensitive search with stripos() (supported in PHP 5). if ($php5 && !isset($rule['#case-sensitive'])) { return stripos($line, $rule['#value']) !== FALSE; } // Case-insensitive search. $regex = '/'. preg_quote($rule['#value']) .'/i'; return preg_match($regex, $line); } /** * Return true if $module is in Drupal core. */ function _coder_is_drupal_core($module) { static $core; if (!isset($core)) { $core = array( // Modules: 'aggregator' => 1, 'block' => 1, 'blog' => 1, 'blogapi' => 1, 'book' => 1, 'color' => 1, 'comment' => 1, 'contact' => 1, 'dblog' => 1, 'filter' => 1, 'forum' => 1, 'help' => 1, 'locale' => 1, 'menu' => 1, 'node' => 1, 'openid' => 1, 'path' => 1, 'php' => 1, 'ping' => 1, 'poll' => 1, 'profile' => 1, 'search' => 1, 'statistics' => 1, 'syslog' => 1, 'system' => 1, 'taxonomy' => 1, 'throttle' => 1, 'tracker' => 1, 'translation' => 1, 'trigger' => 1, 'update' => 1, 'upload' => 1, 'user' => 1, // Themes: 'bluemarine' => 1, 'chameleon' => 1, 'garland' => 1, 'marvin' => 1, 'minnelli' => 1, 'pushbutton' => 1, ); } return isset($core[$module->name]) ? 1 : 0; } /** * Implementation of hook_simpletest(). */ function coder_simpletest() { return array_keys(file_scan_directory(drupal_get_path('module', 'coder') .'/tests', '\.test')); } /** * Helper function to run the review on the code snippet. */ function coder_test($code, $review, $severity = SEVERITY_MINOR) { $coder_args = array( '#severity' => $severity, '#filename' => 'snippet', '#patch' => $code, ); _coder_read_and_parse_file($coder_args); return do_coder_review($coder_args, $reviews['style']); } // Theming functions /** * Implementation of hook_theme(). */ function coder_theme() { return array( 'coder' => array('arguments' => array('name', 'filename', 'results')), 'coder_warning' => array('arguments' => array('warning', 'severity_name', 'lineno', 'line')), 'coder_warning_msg' => array('arguments' => array('error')), 'cols' => array('arguments' => array('form')), 'drupalapi' => array('arguments' => array('function', 'version')), ); } /** * Format coder form and results. * * @param $name * Name of module/theme checked, not used. * @param $filename * String filename checked. * @param $results * Array list of results HTML to display. See do_coder_reviews() for format. */ function theme_coder($name, $filename, $results) { // theme the output for the Drupal shell if (function_exists('_coder_drush_is_option') && _coder_drush_is_option('drush')) { return theme_drush_coder($name, $filename, $results); } $output = '

'. basename($filename) .'

'; if (!empty($results)) { $output .= theme('item_list', $results); } $output .= '
'; return $output; } /** * Format a coder warning to be included in results, creating the text. * * @param $results * Results array variable to save errors to. * @param $error * Error array from _coder_error(). */ function theme_coder_warning_msg($error) { if (isset($error['rule']['#warning_callback'])) { if (function_exists($error['rule']['#warning_callback'])) { $warning = $error['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' => $error['rule']['#warning_callback'], ) ); } } else { $warning = t($error['rule']['#warning']); } // @TODO: we can combine theme_coder_warning() and theme_coder_warning_msg(), // But let's do this on the 7.x upgrade in case anyone's actually using it. return theme('coder_warning', $warning, $error['severity_name'], $error['lineno'], $error['line']); } /** * Format a coder warning to be included in results. * * @param $warning * Either summary warning description, or an array in format: * - '#warning' => Summary warning description; * - '#description' => Detailed warning description; * - '#link' => Link to an explanatory document. * @param $severity_name * String severity name. * @param $lineno * Integer line number of error. * @param $line * String contents of line. */ function theme_coder_warning($warning, $severity_name, $lineno = 0, $line = '') { // theme the output for the Drupal shell if (function_exists('_coder_drush_is_option') && _coder_drush_is_option('drush')) { return theme_drush_coder_warning($warning, $severity_name, $lineno, $line); } // Extract description from warning. if (is_array($warning)) { $description = isset($warning['#description']) ? $warning['#description'] : ''; $link = $warning['#link']; $warning = $warning['#warning']; if (isset($link)) { $warning .= ' ('. l('Drupal Docs', $link) .')'; } } 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'); $title = t('severity: @severity', array('@severity' => $severity_name)); $img = theme('image', $path ."/images/$severity_name.png", $title, $title, array('align' => 'right', 'class' => 'coder'), FALSE); if (!empty($description)) { $img .= theme('image', $path .'/images/more.png', t('click to read more'), '', array('align' => 'right', 'class' => 'coder-more'), FALSE); $warning .= '
Explanation: '. $description .'
'; } return '
'. $img . $warning .'
'; } /** * Format link to Drupal API. * * @param $function * Function to link to. * @param $version * Version to link to. */ function theme_drupalapi($function, $version = '') { return l($function, "http://api.drupal.org/api/function/$function/$version"); } /** * Format link to PHP documentation. * * @param $function * Function to link to. */ function theme_phpapi($function) { return l($function, "http://us.php.net/$function"); }