Newer
Older
<?php
// $Id$
/** @file
* Developer Module that assists with code review and version upgrade that
* supports a plug-in extensible hook system so contributed modules can
* define additional review standards.
*
* Built-in support for:
* - Drupal Coding Standards - http://drupal.org/node/318
* - Handle text in a secure fashion - http://drupal.org/node/28984
* - Converting 4.6.x modules to 4.7.x - http://drupal.org/node/22218
* - Converting 4.7.x modules to 5.x - http://drupal.org/node/64279
*
* Credit also to dries:
* - http://cvs.drupal.org/viewcvs/drupal/drupal/scripts/code-style.pl
*/
/**
* Implementation of hook_init().
*/
function coder_init() {
// hook init is called even on cached pages, but we don't want to
// actually do anything in that case.
if (!function_exists('drupal_get_path')) {
return;
}
// Load all our module plug-ins
Doug Green
committed
$path = drupal_get_path('module', 'coder') .'/includes';
$system_listing = function_exists('drupal_system_listing') ? 'drupal_system_listing' : 'system_listing';
$files = $system_listing('coder_.*\.inc$', $path, 'name', 0);
Doug Green
committed
require_once('./'. $file->filename);
}
}
/**
* Get all of the code review modules
*/
function _coder_reviews() {
$reviews = array();
// get the review definitions from the include directory
global $_coder_coders;
if ($_coder_coders) {
foreach ($_coder_coders as $coder) {
Doug Green
committed
$function = $coder .'_reviews';
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
if (function_exists($function)) {
if ($review = call_user_func($function)) {
$reviews = array_merge($reviews, $review);
}
}
}
}
// get the contributed module review definitions
if ($review = module_invoke_all('reviews')) {
$reviews = array_merge($reviews, $review);
}
return $reviews;
}
/**
* Implementation of hook_help()
*/
function coder_help($section) {
switch ($section) {
case 'admin/modules#description':
return t('Developer Module that assists with code review and version upgrade');
default :
return;
}
}
/**
* Implementation of hook_cron().
*
* TODO: move some of the work here...
*/
function coder_cron() {
}
/**
* Implementation of hook_perm().
*/
function coder_perm() {
return array('view code review');
}
/**
* Implementation of hook_menu().
*/
function coder_menu($may_cache) {
$items = array();
if ($may_cache) {
$items[] = array(
'path' => 'coder',
'title' => t('Code review'),
'callback' => 'coder_page',
'access' => user_access('view code review'),
);
if (function_exists('drupal_system_listing')) {
$items[] = array(
'path' => 'admin/settings/coder',
'title' => t('Code review'),
'description' => t('Select code review plugins and modules'),
'callback' => 'drupal_get_form',
'callback arguments' => 'coder_admin_settings',
'access' => user_access('administer site configuration'),
'type' => MENU_NORMAL_ITEM,
);
}
/**
* Implementation of hook_form_alter().
*/
function coder_form_alter($form_id, &$form) {
if ($form_id == 'system_modules') {
if (user_access('view code review')) {
foreach ($form['name'] as $name => $data) {
$form['name'][$name]['#value'] = l($data['#value'], "coder/$name");
function _coder_default_reviews() {
return drupal_map_assoc(array('style', 'security'));
/**
* Implementation of hook_settings().
*/
function coder_settings() {
// get this variable once - do we want only active modules?
$active = variable_get('coder_active_modules', 1);
// create the list of review options from the coder review plug-ins
$reviews = _coder_reviews();
foreach ($reviews as $name => $review) {
$review_options[$name] = $review['#title'];
}
// what review standards should be applied
$form['coder_reviews_group'] = array(
'#type' => 'fieldset',
'#title' => t('Reviews'),
'#collapsible' => TRUE,
'#collapsed' => FALSE,
);
$form['coder_reviews_group']['coder_reviews'] = array(
'#type' => 'checkboxes',
'#options' => $review_options,
'#description' => t('apply the checked coding reviews'),
'#default_value' => variable_get('coder_reviews', _coder_default_reviews()),
);
// get the modules
$sql = "SELECT name, status FROM {system} WHERE type = 'module'";
if ($active == 1) {
$sql .= " AND status=1";
}
$sql .= " ORDER BY weight ASC, filename ASC";
$result = db_query($sql);
while ($module = db_fetch_object($result)) {
$name = $module->name;
$display_name = (!$active && $module->status) ? $name .' (active)' : $name;
$module_options[$module->name] = l($display_name, "coder/$name");
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
if ($module->status) {
$default_coder_modules[] = $module->name;
}
}
// display active modules option
$form['coder_modules_group'] = array(
'#type' => 'fieldset',
'#title' => t('Modules'),
'#collapsible' => TRUE,
'#collapsed' => FALSE,
);
$form['coder_modules_group']['coder_active_modules'] = array(
'#type' => 'checkbox',
'#default_value' => $active,
'#title' => t('code review all active modules'),
);
// display the modules in a fieldset
$form['coder_modules_group']['coder_modules_subgroup'] = array(
'#type' => 'fieldset',
'#title' => t('Select Specific Modules'),
'#collapsible' => TRUE,
'#collapsed' => $active,
);
$modules = $active ? $default_coder_modules : variable_get('coder_modules', $default_coder_modules);
$form['coder_modules_group']['coder_modules_subgroup']['coder_modules'] = array(
'#type' => 'checkboxes',
'#options' => $module_options,
'#description' => t('code review the selected modules only'),
'#default_value' => $modules,
);
// display the showcode setting
// should this even be an option???
Doug Green
committed
/* $form['coder_nocode'] = array(
'#type' => 'checkbox',
Doug Green
committed
'#title' => t('Hide Source Code on warnings'),
'#description' => t('Only display line numbers and error messages on warning messages, do not display the source code (for more compact reading)'),
'#default_value' => variable_get('coder_nocode', 0),
Doug Green
committed
); */
/**
* Implementation of settings page for Drupal 5
*/
function coder_admin_settings() {
return system_settings_form(coder_settings());
}
/**
* Implementation of code review page
*/
function coder_page() {
// get this variable once - do we want only active modules?
$active = variable_get('coder_active_modules', 1);
// get this once - list of the reviews to perform
$reviews = array();
$avail_reviews = _coder_reviews();
$selected_reviews = variable_get('coder_reviews', _coder_default_reviews());
foreach ($selected_reviews as $name => $checked) {
if ($checked) {
$reviews[$name] = $avail_reviews[$name];
}
}
// get the list of the modules
$requested_module = arg(1);
$sql = "SELECT name, filename, status FROM {system} WHERE type = 'module'";
if (!$requested_module && $active == 1) {
$sql .= " AND status=1";
}
$result = db_query($sql);
while ($module = db_fetch_object($result)) {
if ($module->status) {
$default_coder_modules[$module->name] = $module->name;
}
$files[$module->name] = $module->filename;
}
// determine which modules are selected
if ($requested_module) {
$modules = array($requested_module => $requested_module);
$do_includes = 1;
$modules = $active ? $default_coder_modules : variable_get('coder_modules', $default_coder_modules);
}
if ($modules) {
// loop through the selected modules, preparing the code review results
$dups = array(); // used to avoid duplicate includes
foreach ($modules as $name => $checked) {
if ($checked) {
// process this one file
$filename = $files[$name];
if (!$filename) {
drupal_set_message(t('Code Review is only available on module files (%module.module not found)', array('%module' => $name)));
continue;
}
$coder_args = array(
'#reviews' => $reviews,
'#name' => $name,
'#filename' => $filename,
);
$results = do_coder_reviews($coder_args);
$output .= theme('coder', $name, $filename, $results);
// process the same directory include files
if ($do_includes) {
// NOTE: convert to the realpath here so system_listing
// doesn't return additional paths (i.e., try "module").
$path = str_replace('\\', '/', dirname(realpath($filename)));
$offset = strpos($path, dirname($filename));
if (!isset($dups[$path])) {
$dups[$path] = 1;
$system_listing = (function_exists('drupal_system_listing')) ? 'drupal_system_listing' : 'system_listing';
$includefiles = $system_listing('.*\.inc$', $path, 'name', 0);
foreach ($includefiles as $file) {
Doug Green
committed
$filename = drupal_substr($file->filename, $offset);
$coder_args = array(
'#reviews' => $reviews,
'#name' => $name,
'#filename' => $filename,
);
$results = do_coder_reviews($coder_args);
$output .= theme('coder', $name, $filename, $results);
}
}
// prepend any output with the list of code reviews performed
if (!$output) {
$output = t('No modules found');
}
foreach ($reviews as $review) {
$items[] = l($review['#title'], $review['#link']);
$output = '<div class="reviews">'. theme('reviews', $items, t('The code was checked using the following review guidelines')) .'</div>'. $output;
function do_coder_reviews($coder_args) {
$results = array();
// read the file
if (_coder_read_and_parse_file($coder_args)) {
// do all of the code reviews
foreach ($coder_args['#reviews'] as $review) {
if ($result = do_coder_review($coder_args, $review)) {
$results += $result;
}
}
// always display something
if (count($results) == 0) {
_coder_error_msg($results, t('No Problems Found'));
}
else {
ksort($results, SORT_NUMERIC);
}
}
else {
_coder_error_msg($results, t('Could not read the file'));
}
return $results;
}
function _coder_read_and_parse_file(&$coder_args) {
if ($filepath = realpath($coder_args['#filename'])) {
$content = file_get_contents($filepath);
$content_length = drupal_strlen($content);
// TODO: convert the content string to an array for faster usage
// parse the file:
// - strip comments
// - strip quote content // - strip stuff not in php // - break into lines
$lineno = 0;
for ($pos = 0; $pos < $content_length; $pos ++) {
// get the current character
$char = $content[$pos];
if ($char == "\n") {
if ($in_comment == '/') { // end C++ style comments on newline
unset($in_comment);
}
Doug Green
committed
// assume that html inside quotes doesn't span newlines
unset($in_quote_html);
Doug Green
committed
// remove blank lines now, so we avoid processing them over-and-over
if (trim($all_lines[$lineno]) == '') {
unset($all_lines[$lineno]);
Doug Green
committed
}
if (trim($php_lines[$lineno]) == '') {
unset($php_lines[$lineno]);
}
if (trim($html_lines[$lineno]) == '') {
unset($html_lines[$lineno]);
Doug Green
committed
}
$lineno ++;
continue;
}
if ($in_php) {
// look for the ending php tag which tags precedence over everything
if ($char == '?' && $content[$pos + 1] == '>') {
unset($char);
unset($in_php);
$pos ++;
}
// when in a quoted string, look for the trailing quote
// strip characters in the string, replacing with '' or ""
elseif ($in_quote) {
if ($in_backslash) {
unset($in_backslash);
}
elseif ($char == '\\') {
$in_backslash = '\\';
}
elseif ($char == $in_quote && !$in_backslash) {
unset($in_quote);
}
elseif ($char == '<') {
$in_quote_html = '>';
}
if ($in_quote && $in_quote_html) {
$html_lines[$lineno] .= $char;
}
if ($char == $in_quote_html) {
unset($in_quote_html);
}
unset($char); // NOTE: trailing char output with starting one
}
elseif ($in_heredoc) {
if ($beginning_of_line && $char == $in_heredoc[0] && substr($content, $pos, $in_heredoc_length) == $in_heredoc) {
$all_lines[$lineno] .= substr($content, $pos + 1, $in_heredoc_length - 1);
unset($in_heredoc);
$pos += $in_heredoc_length;
}
elseif ($char == '<') {
$in_heredoc_html = '>';
}
if ($in_heredoc && $in_heredoc_html) {
$html_lines[$lineno] .= $char;
}
if ($char == $in_heredoc_html) {
unset($in_heredoc_html);
}
unset($char);
}
// when in a comment look for the trailing comment
elseif ($in_comment) {
if ($in_comment == '*' && $char == '*' && $content[$pos + 1] == '/') {
unset($in_comment);
$pos ++;
}
unset($char); // don't add comments to php output
switch ($char) {
case '\'':
case '"':
if ($content[$pos - 1] != '\\') {
$php_lines[$lineno] .= $char;
$in_quote = $char;
}
break;
case '/':
$next_char = $content[$pos + 1];
if ($next_char == '/' || $next_char == '*') {
unset($char);
$in_comment = $next_char;
$pos ++;
}
break;
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
case '<':
if ($content[$pos + 1] == '<' && $content[$pos + 2] == '<') {
unset($char);
$all_lines[$lineno] .= '<<';
// get the heredoc word
// read until the end-of-line
for ($pos += 3; $pos < $content_length; $pos ++) {
$char = $content[$pos];
if ($char == "\n") {
$pos --;
if (preg_match('/^\s+(\w+)/', $heredoc, $match)) {
$in_heredoc = $match[1];
$in_heredoc_length = drupal_strlen($in_heredoc);
}
break;
}
$all_lines[$lineno] .= $char;
$heredoc .= $char;
}
unset($heredoc);
// replace heredoc's with an empty string
$php_lines[$lineno] .= "''";
unset($char);
}
break;
if (isset($char)) {
$php_lines[$lineno] .= $char;
else {
switch ($char) {
case '<':
if ($content[$pos + 1] == '?') {
if ($content[$pos + 2] == ' ') {
$in_php = 1;
$pos += 2;
}
elseif (substr($content, $pos + 2, 3) == 'php') {
$in_php = 1;
$pos += 4;
}
break;
}
// FALTHROUGH
default:
$html_lines[$lineno] .= $char;
break;
}
// add the files lines to the arguments
$coder_args['#php_lines'] = $php_lines;
$coder_args['#html_lines'] = $html_lines;
return 1;
function do_coder_review($coder_args, $review) {
$results = array();
if ($review['#rules']) {
foreach ($review['#rules'] as $rule) {
if (isset($rule['#original'])) { // deprecated
$lines = $coder_args['#all_lines'];
elseif (isset($rule['#source'])) { // all, html, comment, or php
$source = '#'. $rule['#source'] .'_lines';
$lines = $coder_args[$source];
}
else {
$lines = $coder_args['#php_lines'];
}
if ($lines) {
switch ($rule['#type']) {
case 'regex':
do_coder_review_regex($coder_args, $rule, $lines, $results);
break;
case 'grep':
do_coder_review_grep($coder_args, $rule, $lines, $results);
break;
case 'callback':
do_coder_review_callback($coder_args, $rule, $lines, $results);
break;
}
function do_coder_review_regex(&$coder_args, $rule, $lines, &$results) {
if (preg_match($regex, $line, $matches)) {
// don't match some regex's
if ($not = $rule['#not']) {
foreach ($matches as $match) {
$line = $coder_args['#all_lines'][$lineno];
_coder_error($results, $rule, $lineno, $line);
function _coder_error(&$results, $rule, $lineno = -1, $line = '', $original = '') {
Doug Green
committed
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 <a href="@report">report</a> this !warning',
Doug Green
committed
array(
'@report' => 'http://drupal.org/node/add/project_issue/coder/bug',
Doug Green
committed
'!warning' => $rule['#warning_callback'],
)
);
}
}
else {
$warning = t($rule['#warning']);
}
return _coder_error_msg($results, $warning, $lineno, $line);
}
function _coder_error_msg(&$results, $warning, $lineno = -1, $line = '') {
// Note: The use of the $key allows multiple errors on one line.
// This assumes that no line of source has more than 10000 lines of code
// and that we have fewer than 10000 errors.
Doug Green
committed
global $_coder_errno;
$key = ($lineno + 1) * 10000 + ($_coder_errno ++);
$results[$key] = theme('coder_warning', $warning, $lineno + 1, $line);
Doug Green
committed
}
function do_coder_review_grep(&$coder_args, $rule, $lines, &$results) {
foreach ($lines as $lineno => $line) {
if (preg_match($regex, $line)) {
return;
}
}
Doug Green
committed
_coder_error($results, $rule);
function do_coder_review_callback(&$coder_args, $rule, $lines, &$results) {
if ($function = $rule['#value']) {
if (function_exists($function)) {
call_user_func_array($function, array(&$coder_args, $rule, $lines, &$results));
}
}
}
function theme_reviews($items, $message = '') {
return $message . theme('item_list', $items, NULL, 'ol');
}
$title = ($_GET['q'] == "coder/$name") ? $filename : l($name, "coder/$name");
Doug Green
committed
$output = '<div class="coder"><h2>'. $title .'</h2>';
if (count($results)) {
$output .= theme('item_list', $results);
}
function theme_coder_warning($warning, $lineno = 0, $line = '') {
Doug Green
committed
$warning = t('Line @number: !warning', array('@number' => $lineno, '!warning' => $warning));
if ($line && !variable_get('coder_nocode', 0)) {
$warning .= '<pre>'. check_plain($line) .'</pre>';
}
Doug Green
committed
return '<div class="coder_warning">'. $warning .'</div>';
Doug Green
committed
function theme_drupalapi($function, $version = 'HEAD') {
return l($function, "http://api.drupal.org/api/$version/function/$function");
}