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");
}