/[drupal]/contributions/modules/coder_tough_love/coder_tough_love.module
ViewVC logotype

Diff of /contributions/modules/coder_tough_love/coder_tough_love.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph | View Patch Patch

revision 1.1, Wed Nov 19 16:26:31 2008 UTC revision 1.1.2.1, Wed Nov 19 16:26:31 2008 UTC
# Line 0  Line 1 
1    <?php
2    // $Id: coder_security.inc,v 1.11.4.1.4.12 2008/07/20 14:36:23 snpower Exp $
3    
4    /**
5     * @file
6     * Supplementary Coder module to provide Morbus' style complaints.
7     *
8     * @todo get rid of coder_tough_love_remove_known_keys once concat bug fixed.
9     */
10    
11    /**
12     * Implementation of hook_menu().
13     */
14    function coder_tough_love_menu() {
15      $items['admin/settings/coder_tough_love'] = array(
16        'access arguments'  => array('administer site configuration'),
17        'description'       => 'Configure Coder Tough Love with these settings.',
18        'page callback'     => 'drupal_get_form',
19        'page arguments'    => array('coder_tough_love_settings'),
20        'title'             => 'Code tough love',
21      );
22    
23      return $items;
24    }
25    
26    /**
27     * Implementation of hook_reviews().
28     */
29    function coder_tough_love_reviews() {
30      $rules = array(
31    
32        // Doxygen errors.
33        array(
34          '#source' => 'comment',
35          '#type' => 'regex',
36          '#value' => '(@param|@return) (?!unknown_type)unknown',
37          '#warning' => t('Proper documentation is needed for this mysterious blackhole.'),
38        ),
39        array(
40          '#source' => 'comment',
41          '#type' => 'regex',
42          '#value' => '[\/\*]\s+(TODO|BUG)',
43          '#warning' => t('Doxygen uses @todo and @bug to markup things to be done.'),
44        ),
45        array(
46          '#source' => 'comment',
47          '#type' => 'regex',
48          '#value' => '(^|\s+)\/\/[^\s]',
49          '#warning' => t('Separate comments from comment syntax by a space.'),
50        ),
51        array(
52          '#source' => 'all',
53          '#type' => 'callback',
54          '#value' => '_coder_tough_love_doxygen_params_explained',
55          '#warning' => t('If you define a @param or @return, you should document it as well.'),
56        ),
57        array(
58          '#source' => 'all',
59          '#type' => 'callback',
60          '#value' => '_coder_tough_love_doxygen_function_empty_comment',
61          '#warning' => t('Remove the empty commented line in your function documentation.'),
62        ),
63        array(
64          '#source' => 'all',
65          '#type' => 'callback',
66          '#value' => '_coder_tough_love_doxygen_function_long_line',
67          '#warning' => t('Function documentation should be less than 80 characters per line.'),
68        ),
69        array(
70          '#source' => 'all',
71          '#type' => 'callback',
72          '#value' => '_coder_tough_love_doxygen_function_one_line_summary',
73          '#warning_callback' => '_coder_tough_love_doxygen_function_one_line_summary_warning',
74        ),
75        array(
76          '#source' => 'comment',
77          '#type' => 'regex',
78          '#value' => '(@param|@return) (array|int|mixed|string|unknown_type)',
79          '#warning_callback' => '_coder_tough_love_doxygen_param_types_warning',
80        ),
81        array(
82          '#source' => 'comment',
83          '#type' => 'regex',
84          '#value' => '(@param|@return) (?!array|int|mixed|string|unknown_type).*? .*?',
85          '#warning_callback' => '_coder_tough_love_doxygen_param_description_warning',
86        ),
87    
88        // Grammar, spelling, and capitalization errors.
89        array(
90          '#type' => 'regex',
91          '#value' => '[^valid_email_address]email',
92          '#warning' => t('Core uses "e-mail" in end-user text and "mail" elsewhere (database, function names, etc.)'),
93        ),
94        array(
95          '#type' => 'regex',
96          '#value' => 'web-?server',
97          '#warning' => t('Core uses "web server" in end-user text.'),
98        ),
99        array(
100          '#case-sensitive' => TRUE,
101          '#source' => 'quote',
102          '#type' => 'regex',
103          '#value' => '[A-Z]\w+ Module',
104          '#warning' => t('"Module" should rarely be capitalized as part of a module\'s proper name.'),
105        ),
106        array(
107          '#case-sensitive' => TRUE,
108          '#source' => 'quote',
109          '#type' => 'callback',
110          '#value' => '_coder_tough_love_sentence_style',
111          '#warning' => t('Use sentence case, not title case, for end-user strings. (!link)', array('!link' => l(t('Wikipedia'), 'http://en.wikipedia.org/wiki/Capitalization#Headings_and_publication_titles'))),
112        ),
113        array(
114          '#case-sensitive' => TRUE,
115          '#source' => 'quote',
116          '#type' => 'callback',
117          '#value' => '_coder_tough_love_pspell_check',
118          '#warning' => t('An unknown or misspelled word has been detected on this line.'),
119        ),
120    
121        // Miscellany
122        array(
123          '#type' => 'regex',
124          '#value' => '\s*else\s+if\s*\(',
125          '#warning' => t('Use "elseif" not "else if".'),
126        ),
127        array(
128          '#source' => 'quote',
129          '#type' => 'regex',
130          '#value' => '<(a|img|table)(\s|>)',
131          '#warning' => t('Use the matching Drupal theme functions, not raw HTML.'),
132        ),
133        array(
134          '#type' => 'regex',
135          '#value' => '[^format_date]date\(',
136          '#warning' => t('Use Drupal\'s format_date(), not PHP\'s default date().'),
137        ),
138        array(
139          '#type' => 'regex',
140          '#value' => '\s+(chop|close|die|dir|diskfreespace|doubleval|fputs|ini_alter|is_(double|integer|long|real|writeable)|join|magic_quotes_runtime|pos|rewind|show_source|sizeof|strchr)\(',
141          '#warning' => t('Use PHP\'s master function, not an alias.'),
142        ),
143        array(
144          '#source' => 'all',
145          '#type' => 'callback',
146          '#value' => '_coder_tough_love_admin_menu_descriptions',
147          '#warning' => t('Administrative menu items should have a description.'),
148        ),
149    
150      );
151    
152      $review = array(
153        '#title' => 'Coder Tough Love',
154        '#rules' => $rules,
155        '#description' => t('obsessive eyeballing from Morbus'),
156      );
157    
158      return array('coder_tough_love' => $review);
159    }
160    
161    /**
162     * Administrative menu items should have a #description.
163     */
164    function _coder_tough_love_admin_menu_descriptions(&$coder_args, $review, $rule, $lines, &$results) {
165      foreach ($lines as $line_number => $line) {
166        $line = implode(' ', $line); // concat'd parts.
167    
168        // are we inside a function that ends in _menu?
169        if (preg_match('/^function .*?_menu\(\)/', $line)) {
170          $potentially_inside_a_hook_menu = 1;
171        }
172    
173        // if we're inside, look at the line and check for an admin path.
174        if ($potentially_inside_a_hook_menu && preg_match('/\[[\'"]admin\//', $line)) {
175          $potentially_found_an_admin_path = $line_number; // used in the error.
176        }
177    
178        // now, keep looking for a 'description' element for this admin path.
179        if ($potentially_inside_a_hook_menu && $potentially_found_an_admin_path) {
180          if (preg_match('/[\'"]description[\'"]\s*=>/', $line)) {
181            $saw_a_description = 1;
182          }
183        }
184    
185        // if we've found any sort of ");", we assume the menu's array is done.
186        if ($potentially_inside_a_hook_menu && $potentially_found_an_admin_path && preg_match("/\);/", $line)) {
187          if (!$saw_a_description) {
188            _coder_error($results, $rule, _coder_severity_name($coder_args, $review, $rule), $potentially_found_an_admin_path);
189          }
190    
191          // reset for the next iteration, bub.
192          $saw_a_description = $potentially_found_an_admin_path = 0;
193        }
194    
195        // are we inside a function that ends in _menu?
196        if (preg_match('/^(function .*?[^_menu]\(\)|\/\*\*)/', $line)) {
197          $potentially_inside_a_hook_menu = 0;
198        }
199      }
200    }
201    
202    /**
203     * If someone has defined Doxygen's param or return, they should document them.
204     */
205    function _coder_tough_love_doxygen_params_explained(&$coder_args, $review, $rule, $lines, &$results) {
206      foreach ($lines as $line_number => $line) {
207        $line = implode(' ', $line); // concat'd parts.
208        if (preg_match('/^ \* (@param|@return)/', $line)) {
209          // check the next line to see if there's SOMETHING written.
210          if (!preg_match('/^ \*   .*/', implode(' ', $lines[$line_number + 1]))) {
211            _coder_error($results, $rule, _coder_severity_name($coder_args, $review, $rule), $line_number, $line);
212          }
213        }
214      }
215    }
216    
217    /**
218     * Empty comments are not needed in a Doxygen block.
219     */
220    function _coder_tough_love_doxygen_function_empty_comment(&$coder_args, $review, $rule, $lines, &$results) {
221      foreach ($lines as $line_number => $line) {
222        $line = implode(' ', $line); // concat'd parts.
223    
224        if (preg_match('/^ \*\/$/', $line)) {
225          $previous_line = implode(' ', $lines[$line_number - 1]);
226          if (preg_match('/^ \*(\s*)$/', $previous_line)) {
227            _coder_error($results, $rule, _coder_severity_name($coder_args, $review, $rule), $line_number - 1);
228          }
229        }
230      }
231    }
232    
233    /**
234     * Function documentation should be less than 80 characters per line.
235     */
236    function _coder_tough_love_doxygen_function_long_line(&$coder_args, $review, $rule, $lines, &$results) {
237      foreach ($lines as $line_number => $line) {
238        $line = implode(' ', $line); // concat'd parts.
239    
240        if (preg_match('/^ \*/', $line) && drupal_strlen($line) > 80) {
241          _coder_error($results, $rule, _coder_severity_name($coder_args, $review, $rule), $line_number);
242        }
243      }
244    }
245    
246    /**
247     * The first line of a function Doxygen should be a brief summary.
248     */
249    function _coder_tough_love_doxygen_function_one_line_summary(&$coder_args, $review, $rule, $lines, &$results) {
250      foreach ($lines as $line_number => $line) {
251        $line = implode(' ', $line); // concat'd parts.
252        if (preg_match('/^\/\*\*$/', $line)) {
253          $first_line_exists = $second_line_is_ok = 0;
254    
255          // check the next line to see if there's SOMETHING written.
256          if (preg_match('/^ \* (\w+)/', implode(' ', $lines[$line_number + 1]))) {
257            $first_line_exists = $line_number + 1; // used in _coder_error().
258          }
259    
260          // if the line after THAT is NOT a blank line or end of Doxygen, there's a problem.
261          if ($first_line_exists && !preg_match('/^ (\*\/|\*)$/', implode(' ', $lines[$line_number + 2]))) {
262            _coder_error($results, $rule, _coder_severity_name($coder_args, $review, $rule), $first_line_exists);
263            $first_line_exists = 0; // reset for the next iteration.
264          }
265        }
266      }
267    }
268    
269    function _coder_tough_love_doxygen_function_one_line_summary_warning() {
270      return array(
271        '#warning' => t('Function summaries should be one line only.'),
272        '#link' => 'http://drupal.org/node/1354',
273      );
274    }
275    
276    function _coder_tough_love_doxygen_param_types_warning() {
277      return array(
278        '#warning' => t('@param and @return syntax does not indicate the data type.'),
279        '#link' => 'http://drupal.org/node/1354',
280      );
281    }
282    
283    function _coder_tough_love_doxygen_param_description_warning() {
284      return array(
285        '#warning' => t('@param and @return descriptions begin indented on the next line.'),
286        '#link' => 'http://drupal.org/node/1354',
287      );
288    }
289    
290    /**
291     * Check the passed quote for spelling errors.
292     */
293    function _coder_tough_love_pspell_check(&$coder_args, $review, $rule, $lines, &$results) {
294      if (function_exists('pspell_new')) {
295        static $pspell_link;
296    
297        if (!$pspell_link) {
298          $pspell_link = pspell_new("en_US");
299          $learnables = explode(',', variable_get('coder_tough_love_pspell_personal', _coder_tough_love_settings_pspell_personal_default()));
300          foreach ($learnables as $learnable) { // one day, we'll use a literal dictionary, not session.
301            pspell_add_to_session($pspell_link, drupal_strtolower(trim($learnable)));
302          }
303        }
304    
305        foreach ($lines as $line_number => $line) {
306          $line = implode(' ', $line); // concat'd parts.
307          $found_unknown_word = 0; // start fresh please.
308          $line = coder_tough_love_remove_known_keys($line);
309          $line = coder_tough_love_remove_non_words($line);
310          $line = coder_tough_love_remove_sql_queries($line);
311          $line = trim($line); // any fidgety whitespace.
312    
313          $words = explode(' ', $line);
314          foreach ($words as $word) {
315            if (!pspell_check($pspell_link, drupal_strtolower(trim($word)))) {
316              $found_unknown_word = 1; // don't spit the error yet...
317            }
318          }
319    
320          if ($found_unknown_word) { // ...we spit the error here so we report just once per line.
321            _coder_error($results, $rule, _coder_severity_name($coder_args, $review, $rule), $line_number, $line);
322          }
323        }
324      }
325    }
326    
327    /**
328     * Look for Sentences That Look Like This and yell about 'em.
329     */
330    function _coder_tough_love_sentence_style(&$coder_args, $review, $rule, $lines, &$results) {
331      $filename_to_title_caps = ucwords(str_replace('_', ' ', preg_replace('/(.*?)(\..*)/', '\1', basename($coder_args['#filename']))));
332    
333      foreach ($lines as $line_number => $line) {
334        $line = implode(' ', $line); // concat'd parts.
335        $line = coder_tough_love_remove_known_keys($line);
336        $line = trim($line); // any fidgety whitespace left.
337    
338        // if this line has only one word, skip it.
339        if (count(explode(' ', $line)) == 1) {
340          continue;
341        }
342    
343        // remove potential module names from the string, as determined by
344        // taking the filename and uppercasing it (ie., bio_visibility.module
345        // turns into "Bio Visibility"). This is primarily so that we don't give
346        // a false positive on a module's configuration menu definition.
347        $line = str_replace($filename_to_title_caps, '', $line);
348    
349        // remove the first word which should be capitalized anyways.
350        $line = preg_replace('/^[A-Z]\w+/', '', $line);
351    
352        // common stop words.
353        $rarely_capitalizeds = array(
354          'a', 'an', 'the', 'and', 'or', 'nor', 'for', 'but', 'so', 'yet',
355          'to', 'of', 'by', 'at', 'for', 'but', 'in', 'with', 'has',
356        ); // and now go ahead and remove common stop words.
357        foreach ($rarely_capitalizeds as $rarely_capitalized) {
358          // we could've put the \bs above, then passed the array, but it
359          // looked ugly. i like readability better than performance personally.
360          $line = preg_replace('/\b'. $rarely_capitalized .'\b/', '', $line);
361        }
362    
363        // normalize any remaining spaces down to one.
364        $line = preg_replace('/\s+/', ' ', trim($line));
365    
366        // if the number of words remaining is the same number of capitalized
367        // words, there's a good chance that the string is title-cased. note
368        // that we don't use word boundaries for the regexp because it considers
369        // the / in "api/v2/Group.php" a word boundary (properly - \W vs. \w).
370        $num_words = count(explode(' ', $line));
371        $num_capitalized_words = preg_match_all('/(^| )[A-Z]/', $line, $matches);
372    
373        if ($num_words == $num_capitalized_words) {
374          _coder_error($results, $rule, _coder_severity_name($coder_args, $review, $rule), $line_number);
375        }
376      }
377    }
378    
379    /**
380     * Remove known array keys that coder's "quote" #source gives us.
381     */
382    function coder_tough_love_remove_known_keys($line) {
383      return preg_replace('/^#?(access|callback|description|path|prefix|suffix|title|type|value)/', '', $line);
384    }
385    
386    /**
387     * Given a "quote" #source, try to remove non-words from the results.
388     */
389    function coder_tough_love_remove_non_words($line) {
390      // attempt to remove ending punctuation from a sentence.
391      $line = preg_replace("/(\w)[:;,\.\?\!](\s|$)/", "\1", $line); // remove ending punctuation.
392      $line = preg_replace("/\s+([@%!\$].*?)(\s|$)/", ' ', $line); // remove %placeholders in line.
393      $line = preg_replace("/\.\w+/", '', $line); // filename extensions leftover from previous.
394    
395      $words = preg_split("/\s+/", $line);
396      foreach ($words as $word) {
397        if (preg_match("/[^A-Z]/i", $word)) {
398          $word = preg_quote($word, '/');
399          $line = preg_replace("/(\s|^)$word(\s|$)/", ' ', $line);
400        }
401      }
402    
403      return $line;
404    }
405    
406    /**
407     * Given a "quote" #source, try to determine, and wipe-out, a SQL queries.
408     */
409    function coder_tough_love_remove_sql_queries($line) {
410      if (preg_match('/^(INSERT|UPDATE|DELETE|SELECT)/', trim($line))) {
411        return ''; // remove the entire line. there's nothing good here.
412      }
413    
414      return $line;
415    }
416    
417    /**
418     * Configures Coder Tough Love.
419     */
420    function coder_tough_love_settings() {
421      $form = array(); // helLOO, nurse!
422    
423      if (function_exists('pspell_new')) {
424        $form['coder_tough_love_pspell_personal'] = array(
425          '#default_value'    => variable_get('coder_tough_love_pspell_personal', _coder_tough_love_settings_pspell_personal_default()),
426          '#description'      => t('Comma-separated list of words to teach Pspell. Words with hyphens are not currently allowed.'),
427          '#title'            => t('Pspell custom dictionary'),
428          '#type'             => 'textarea',
429        );
430    
431        return system_settings_form($form);
432      }
433      else { // one day, we may be a bit more stringent with what to turn on. but, for now...
434        drupal_set_message(t('Pspell is not enabled, so there are no settings to configure.'));
435      }
436    }
437    
438    /**
439     * Default set of teachable words for Pspell.
440     */
441    function _coder_tough_love_settings_pspell_personal_default() {
442      return
443        'api, asc, baseURL, bio, cck, checkbox, checkboxes, CiviCRM, colspan, desc, ' .
444        'devel, Doxygen, Drupal, enctype, Facebook, fieldgroup, fieldset, geocode, ' .
445        'geocoding, Google, href, HTML, imagefield, irc, javascript, json, metadata, Morbus, ' .
446        'mysql, mysqli, ncount, nid, null, pathauto, pgsql, pid, PO, presave, regex, signup, ' .
447        'simplenews, tablename, textarea, textfield, tid, timestamp, trellon, uid, Unix, ' .
448        'URL, URLs, varchar, vid, wiki, Wikipedia, XHTML';
449    }
450    

Legend:
Removed from v.1.1  
changed lines
  Added in v.1.1.2.1

  ViewVC Help
Powered by ViewVC 1.1.2