Index: coder.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/coder/coder.module,v retrieving revision 1.88.2.59.2.1 diff -u -u -p -r1.88.2.59.2.1 coder.module --- coder.module 28 Sep 2008 03:05:01 -0000 1.88.2.59.2.1 +++ coder.module 28 Sep 2008 17:55:40 -0000 @@ -350,7 +350,6 @@ function _coder_settings_form($settings, } asort($system_links); - // Display what to review options. $form['coder_what'] = array( '#type' => 'fieldset', @@ -369,10 +368,11 @@ function _coder_settings_form($settings, '#default_value' => isset($settings['coder_core']) ? $settings['coder_core'] : 0, '#title' => t('core files (php, modules, and includes)'), ); + $ext = variable_get('coder_php_ext', array('inc', 'php', 'install', 'test')); $form['coder_what']['coder_includes'] = array( '#type' => 'checkbox', '#default_value' => isset($settings['coder_includes']) ? $settings['coder_includes'] : 0, - '#title' => t('include files (.inc, .php, .test, .js, and .install files)'), + '#title' => t('include files (@ext)', array('@ext' => implode(' | ', $ext))), ); $form['coder_what']['coder_includes_exclusion_fieldset'] = array( '#type' => 'fieldset', @@ -653,6 +653,25 @@ function coder_page_form_submit($form, & } /** + * Return the extensions used by the reviews, that aren's part of the default extensions. + */ +function _coder_get_reviews_extensions($defaults, $reviews) { + $extensions = array(); + foreach ($reviews as $review) { + foreach ($review['#rules'] as $rule) { + if (isset($rule['#filename'])) { + foreach ($rule['#filename'] as $ext) { + if (!in_array($ext, $defaults) && !in_array($ext, $extensions)) { + $extensions[] = $ext; + } + } + } + } + } + return $extensions; +} + +/** * Implementation of hook_form(). * * Implements coder's main form, in which a user can select reviews and @@ -723,6 +742,11 @@ function coder_page_form($form_state) { // Loop through the selected modules and themes. if (isset($system)) { + // Determine which file extensions to include, based on the rules. + $php_extensions = variable_get('coder_php_ext', array('inc', 'php', 'install', 'test')); + $include_extensions = _coder_get_reviews_extensions($php_extensions, $reviews); + $includes = array_merge($php_extensions, $include_extensions); + // Used to avoid duplicate includes. $dups = array(); $stats = array(); @@ -744,6 +768,8 @@ function coder_page_form($form_state) { '#severity' => $settings['coder_severity'], '#filename' => $filename, '#patch' => $patch, + '#php_extensions' => $php_extensions, + '#include_extensions' => $include_extensions, ); $results = do_coder_reviews($coder_args); $stats[$filename] = $results['#stats']; @@ -778,7 +804,8 @@ function coder_page_form($form_state) { $coder_args['#php_minor'] = 1; } $dups[$path] = 1; - $includefiles = drupal_system_listing('.*\.(inc|php|js|install|test)$', dirname($filename), 'filename', 0); + $regex = '\.('. implode('|', $includes) .')$'; + $includefiles = drupal_system_listing($regex, dirname($filename), 'filename', 0); $offset = strpos($filename, dirname($filename)); $stats[$filename]['#includes'] = _coder_page_form_includes($form, $coder_args, $name, $includefiles, $offset, $coder_includes_exclusions); } @@ -1010,6 +1037,23 @@ function do_coder_reviews($coder_args) { * Integer 1 if success. */ function _coder_read_and_parse_file(&$coder_args) { + // If this isn't a php file, then don't try to parse it. + $regex = '/\.('. implode('|', array_merge(array('module'), $coder_args['#php_extensions'])) .')$/'; + if (!preg_match($regex, $coder_args['#filename'])) { + if ((($filepath = realpath($coder_args['#filename'])) && file_exists($filepath))) { + $full_lines = file($filepath); + if ($full_lines[0] != ' $line) { + if (($line = trim($line, "\r\n")) != '') { + $all_lines[$lineno] = $line; + } + } + $coder_args['#all_lines'] = $all_lines; + return 1; + } + } + } + // Get the path to the module file. if (!empty($coder_args['#patch']) || !empty($coder_args['#test']) || (($filepath = realpath($coder_args['#filename'])) && file_exists($filepath))) { $in_php = 0; @@ -1018,7 +1062,7 @@ function _coder_read_and_parse_file(&$co if (!empty($coder_args['#patch'])) { $content = $coder_args['#patch']; - if (preg_match("/^\s*\*/", $content)) { + if (preg_match('/^\s*\*/', $content)) { $in_comment = '*'; } else { @@ -1434,16 +1478,17 @@ function do_coder_review($coder_args, $r foreach ($review['#rules'] as $rule) { // Ignore rules that don't match the file extension. $filename = empty($coder_args['#patch']) ? $coder_args['#filename'] : 'coder.patch'; - if (!empty($rule['#filename'])) { - $regex = '/'. $rule['#filename'] .'/i'; + if ($filename_includes = isset($rule['#filename']) ? $rule['#filename'] : (isset($rule['#filename-not']) ? $coder_args['#php_extensions'] : null)) { + $regex = '/\.('. implode('|', $filename_includes) .')$/i'; if (!preg_match($regex, $filename, $matches)) { continue; } } + // Ignore rules that do match the file extension, javascript files are excluded by default. - $not = isset($rule['#filename-not']) ? $rule['#filename-not'] : (empty($rule['#filename']) || !preg_match('/\\\.js\$/i', $rule['#filename'], $matches) ? '\.js$' : null); + $not = isset($rule['#filename-not']) ? $rule['#filename-not'] : (isset($rule['#filename']) ? null : $coder_args['#include_extensions']); if ($not) { - $regex = '/'. $not .'/i'; + $regex = '/\.('. implode('|', $not) .')$/i'; if (preg_match($regex, $filename, $matches)) { continue; } @@ -1457,23 +1502,21 @@ function do_coder_review($coder_args, $r $lines = $coder_args[$source]; } else { - $lines = $coder_args['#php_array_lines']; + $lines = isset($coder_args['#php_array_lines']) ? $coder_args['#php_array_lines'] : array(); } - 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; - } + 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; } } } @@ -1731,13 +1774,16 @@ function coder_simpletest() { * Helper function to run the review on the code snippet. */ function coder_test($code, $type, $severity = SEVERITY_MINOR) { + $reviews = coder_reviews(); + $ext = variable_get('coder_php_ext', array('inc', 'php', 'install', 'test')); $coder_args = array( '#severity' => $severity, '#filename' => 'snippet', '#test' => $code, + '#php_extensions' => $ext, + '#include_extensions' => _coder_get_reviews_extensions($ext, $reviews), ); _coder_read_and_parse_file($coder_args); - $reviews = coder_reviews(); $results = do_coder_review($coder_args, $reviews[$type]); unset($results['#stats']); return $results; Index: includes/coder_comment.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/coder/includes/coder_comment.inc,v retrieving revision 1.1.4.11 diff -u -u -p -r1.1.4.11 coder_comment.inc --- includes/coder_comment.inc 20 Aug 2008 16:08:47 -0000 1.1.4.11 +++ includes/coder_comment.inc 28 Sep 2008 17:55:40 -0000 @@ -17,7 +17,7 @@ function coder_comment_reviews() { '#value' => '$Id', '#case-sensitive' => TRUE, '#warning_callback' => '_coder_comment_Id_warning', - '#filename-not' => '\.patch$', + '#filename-not' => array('patch'), ), array( '#type' => 'regex', @@ -84,7 +84,7 @@ function coder_comment_reviews() { '#source' => 'comment', '#value' => '@'.'file', '#warning_callback' => '_coder_comment_missing_file_block', - '#filename-not' => '\.patch$', + '#filename-not' => array('patch'), ), array( '#type' => 'regex', Index: includes/coder_i18n.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/coder/includes/Attic/coder_i18n.inc,v retrieving revision 1.1.2.11 diff -u -u -p -r1.1.2.11 coder_i18n.inc --- includes/coder_i18n.inc 28 Sep 2008 02:49:30 -0000 1.1.2.11 +++ includes/coder_i18n.inc 28 Sep 2008 17:55:40 -0000 @@ -15,7 +15,7 @@ function coder_i18n_reviews() { array( '#type' => 'regex', '#value' => '[\s\(]l\s*\(\s*["\']', - '#filename-not' => '\.install$', + '#filename-not' => array('install'), '#warning_callback' => '_coder_i18n_l_without_t', ), array( @@ -33,7 +33,7 @@ function coder_i18n_reviews() { array( '#type' => 'regex', '#value' => '[\s\(]alert\s*\(\s*[\'"]', - '#filename' => '\.js$', + '#filename' => array('js'), '#warning' => 'Javascript strings should be passed through Drupal.t().', ), array( @@ -83,6 +83,11 @@ function coder_i18n_reviews() { '#source' => 'allphp', '#warning_callback' => '_coder_i18n_space_starts_or_ends_t', ), + array( + '#type' => 'callback', + '#filename' => array('po'), + '#value' => '_coder_translation_callback', + ), ); $review = array( '#title' => t('Internationalization'), @@ -92,6 +97,91 @@ function coder_i18n_reviews() { } /** + * Define the rule callbacks for style. + */ +function _coder_translation_callback(&$coder_args, $review, $rule, $lines, &$results) { + $severity_name = _coder_severity_name($coder_args, $review, $rule); + + // Parse the translation file into msgid/msgstr pairs. + $translations = array(); + foreach ($coder_args['#all_lines'] as $lineno => $line) { + if (preg_match('/^(msgid|msgstr) "(.*)"$/', $line, $matches)) { + if ($matches[1] == 'msgid') { + $msgid = $matches[2]; + } + elseif (!empty($msgid)) { + $translations[$lineno] = array('msgid' => $msgid, 'msgstr' => $matches[2]); + unset($msgid); + } + } + } + + // Check each translation. + foreach ($translations as $lineno => $translation) { + $msgid = $translation['msgid']; + $msgstr = $translation['msgstr']; + + // Check the translation first capitable letter. + if (ctype_upper($msgid[0]) != ctype_upper($msgstr[0])) { + $rule = array( + '#warning' => "The first letter in the translation text should have the same capitalization as it's original text.", + ); + _coder_error($results, $rule, $severity_name, $lineno, $msgstr); + } + + // Check punctuation characters. + if (preg_match_all('/[\.,:;?!]/', $msgid, $matches)) { + foreach ($matches[0] as $html) { + if (stripos($msgstr, $html) === FALSE) { + $rule = array( + '#warning' => 'Punctuation characters (.,:;?!) from the original text should exist in the translation.', + ); + _coder_error($results, $rule, $severity_name, $lineno, $msgstr); + } + } + } + if (preg_match_all('/[\.,:;?!]/', $msgstr, $matches)) { + foreach ($matches[0] as $html) { + if (stripos($msgid, $html) === FALSE) { + $rule = array( + '#warning' => 'Punctuations characters (.,:;?!) in the translation should also exist in the original text.', + ); + _coder_error($results, $rule, $severity_name, $lineno, $msgstr); + } + } + } + + // Check HTML tags. + if (preg_match_all('/<[^>]*>/', $msgid, $matches)) { + foreach ($matches[0] as $html) { + if (stripos($msgstr, $html) === FALSE) { + $rule = array( + '#warning' => 'HTML from the original text should also exist in the translation.' + ); + _coder_error($results, $rule, $severity_name, $lineno, $msgstr); + } + } + } + + // Check placeholders. + if (preg_match_all('/[\!\@\%]\w+/', $msgid, $matches)) { + foreach ($matches[0] as $placeholder) { + if (stripos($msgstr, $placeholder) === FALSE) { + $rule = array( + '#warning' => 'If placeholders like !name, @name or %name exist in the original text, they must also exist in the translation.', + ); + _coder_error($results, $rule, $severity_name, $lineno, $msgstr); + } + } + } + + // @TODO: Check translations for opening/closing tags if they contain HTML. + // @TODO: Quotation checks. + // @TODO: Parenthesis (()[]{}) checks. + } +} + +/** * Define the warning callbacks. */ Index: includes/coder_sql.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/coder/includes/Attic/coder_sql.inc,v retrieving revision 1.1.4.12 diff -u -u -p -r1.1.4.12 coder_sql.inc --- includes/coder_sql.inc 23 Sep 2008 13:51:50 -0000 1.1.4.12 +++ includes/coder_sql.inc 28 Sep 2008 17:55:40 -0000 @@ -49,7 +49,7 @@ function coder_sql_reviews() { ), array( '#type' => 'regex', - '#filename' => '\.install$', + '#filename' => array('install'), '#function' => '_update_[0-9]+$', '#source' => 'allphp', '#value' => 'db_query\s*\(\s*[\'"](UPDATE|INSERT|DELETE)\s+', Index: includes/coder_style.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/coder/includes/coder_style.inc,v retrieving revision 1.16.2.18.2.1 diff -u -u -p -r1.16.2.18.2.1 coder_style.inc --- includes/coder_style.inc 28 Sep 2008 03:05:01 -0000 1.16.2.18.2.1 +++ includes/coder_style.inc 28 Sep 2008 17:55:40 -0000 @@ -70,7 +70,7 @@ function coder_style_reviews() { array( '#type' => 'callback', '#source' => 'all', - '#value' => '_coder_style_callback', + '#value' => '_coder_style_closing_php_callback', '#warning' => 'the final ?> should be omitted from all code files', ), array( @@ -157,7 +157,7 @@ function coder_style_reviews() { /** * Define the rule callbacks for style. */ -function _coder_style_callback(&$coder_args, $review, $rule, $lines, &$results) { +function _coder_style_closing_php_callback(&$coder_args, $review, $rule, $lines, &$results) { for ($lineno = -1; $last = array_slice($lines, $lineno); $lineno --) { $lastline = $last[0][0]; if (preg_match('/\S/', $lastline)) {