api.drupal.org).', array('@api' => 'http://api.drupal.org')); default : return; } } /** * 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' => t('Code review'), 'page callback' => 'coder_page', 'access arguments' => array('view code review'), 'type' => MENU_NORMAL_ITEM, ); $items['coder/settings'] = array( 'title' => t('Selection Form'), 'page callback' => 'coder_page', 'access arguments' => array('view code review'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -2, ); $items['coder/default'] = array( 'title' => t('Default'), 'page callback' => 'coder_page', 'access arguments' => array('view code review'), 'type' => MENU_LOCAL_TASK, 'weight' => -1, ); $items['coder/core'] = array( 'title' => t('Core'), 'page callback' => 'coder_page', 'access arguments' => array('view code review'), 'type' => MENU_LOCAL_TASK, ); $items['coder/active'] = array( 'title' => t('Active'), 'page callback' => 'coder_page', 'access arguments' => array('view code review'), 'type' => MENU_LOCAL_TASK, ); $items['coder/all'] = array( 'title' => t('All'), 'page callback' => 'coder_page', 'access arguments' => array('view code review all'), 'type' => MENU_LOCAL_TASK, 'weight' => 1, ); $items['admin/settings/coder'] = array( 'title' => t('Code review'), 'description' => t('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') { 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'], ); // 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 and .php files)'), ); 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' => 'coder_checkboxes', ), ); 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' => 'coder_checkboxes', ), ); 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; } /** * Format checkbox field into columns. * * @param $form * Form sub-array to render, usually supplied as callback. */ function theme_coder_checkboxes($form) { $total = 0; foreach ($form as $element_id => $element) { if ($element_id[0] != '#') { $total ++; } } $total = (int) (($total % 3) ? (($total + 2) / 3) : ($total / 3)); $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['#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_values * 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 '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_modules'] = variable_get('coder_modules', array()); $settings['coder_themes'] = variable_get('coder_themes', array()); break; default: $settings['coder_includes'] = 1; // 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); $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); } // Loop through the selected modules and themes. if (isset($system)) { // Used to avoid duplicate includes. $dups = array(); $stats = array(); 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; } $coder_args = array( '#reviews' => $reviews, '#severity' => $settings['coder_severity'], '#filename' => $filename, ); $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"). $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)$', $path, 'filename', 0); $stats[$filename]['#includes'] = _coder_page_form_includes($form, $coder_args, $name, $includefiles, $offset); } } } } 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(); $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)); } } // 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. * @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) { $stats = array(); $coder_args['#name'] = $name; $weight = 0; foreach ($files as $file) { $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, * @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)) { // Load the cached results if they exist. $cache_key = 'coder:'. implode(':', array_keys($coder_args['#reviews'])) . $coder_args['#severity'] .':'. $coder_args['#filename']; $cache_mtime = filemtime(realpath($coder_args['#filename'])); 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); $results['#stats'] = $stats; // 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) { $results['#stats'] = $stats; 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)) { foreach ($stats as $key => $value) { $stats[$key] += $result['#stats'][$key]; } unset($result['#stats']); $results += $result; } } // Sort the results. ksort($results, SORT_NUMERIC); $results['#stats'] = $stats; } 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_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 ($filepath = realpath($coder_args['#filename'])) { // Read the file. $content = file_get_contents($filepath) ."\n"; $content_length = drupal_strlen($content); $in_comment = 0; $beginning_of_line = 0; $in_php = 0; $in_allphp = 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) != '') { $all_lines[$lineno] = $this_all_lines; } if (trim($this_php_lines) != '') { $php_lines[$lineno] = $this_php_lines; } if (trim($this_allphp_lines) != '') { $allphp_lines[$lineno] = $this_allphp_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; } } // 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) { // 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'])) { $regex = '/'. $rule['#value'] .'/'; if (!isset($rule['#case-sensitive'])) { $regex .= 'i'; } $function_regex = isset($rule['#function']) ? '/'. $rule['#function'] .'/' : ''; $current_function = ''; $paren = 0; $not_regex = isset($rule['#not']) ? '/'. $rule['#not'] .'/i' : ''; $never_regex = isset($rule['#never']) ? '/'. $rule['#never'] .'/i' : ''; 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 = '', $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); } /** * Does the actual saving of error to results array and generating its * unique numeric id. * * @param $results * Results array variable to save errors to. * @param $warning * Warning array/string to be themed, returned from '#warning_callback' or * is a translated string from the rule. See theme_coder_warning() for * array format. * @param $severity_name * String severity of error. * @param $lineno * Integer line number error occured on. * @param $line * String line contents. */ 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); $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; } // 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_checkboxes' => 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) { $output = '

'. basename($filename) .'

'; if (!empty($results)) { $output .= theme('item_list', $results); } $output .= '
'; return $output; } /** * 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 = '') { // 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"); }