Enhancements to the code smell detector
[findutils.git] / build-aux / src-sniff.py
blobb571021f783f20c571ea618b2e24d298445c2d7f
1 #! /usr/bin/env python
4 import re
5 import sys
7 C_ISH_FILENAME = "\.(c|cc|h|cpp|cxx|hxx)$"
8 C_ISH_FILENAME_RE = re.compile(C_ISH_FILENAME)
9 C_MODULE_FILENAME_RE = re.compile("\.(c|cc|cpp|cxx)$")
10 FIRST_INCLUDE = 'config.h'
11 problems = 0
14 def Problem(**kwargs):
15 global problems
16 problems += 1
17 msg = kwargs['message']
18 if kwargs['line']:
19 location = "%(filename)s:%(line)d" % kwargs
20 else:
21 location = "%(filename)s" % kwargs
22 detail = msg % kwargs
23 print >>sys.stderr, "error: %s: %s" % (location, detail)
26 class RegexSniffer(object):
27 def __init__(self, source, message):
28 super(RegexSniffer, self).__init__()
29 self._regex = re.compile(source)
30 self._msg = message
31 def Sniff(self, text, filename, line):
32 m = self._regex.search(text)
33 if m:
34 if line is None:
35 line = 1 + m.string.count('\n', 1, m.start(0))
36 args = {
37 'filename' : filename,
38 'line' : line,
39 'fulltext' : text,
40 'matchtext': m.group(0),
41 'message' : self._msg
43 Problem(**args)
46 class RegexChecker(object):
47 def __init__(self, regex, line_smells, file_smells):
48 super(RegexChecker, self).__init__(self)
49 self._regex = re.compile(regex)
50 self._line_sniffers = [RegexSniffer(s[0],s[1]) for s in line_smells]
51 self._file_sniffers = [RegexSniffer(s[0],s[1]) for s in file_smells]
52 def Check(self, filename, lines, fulltext):
53 if self._regex.search(filename):
54 # We recognise this type of file.
55 for line_number, line_text in lines:
56 for sniffer in self._line_sniffers:
57 sniffer.Sniff(line_text, filename, line_number)
58 for sniffer in self._file_sniffers:
59 sniffer.Sniff(fulltext, filename, None)
60 else:
61 # We don't know how to check this file. Skip it.
62 pass
65 checkers = [
66 # Check C-like languages for C code smells.
67 RegexChecker(C_ISH_FILENAME_RE,
68 # line smells
70 [r'(?<!\w)free \(\(', "don't cast the argument to free()"],
71 [r'\*\) *x(m|c|re)alloc(?!\w)',"don't cast the result of x*alloc"],
72 [r'\*\) *alloca(?!\w)',"don't cast the result of alloca"],
73 [r'[ ] ',"found SPACE-TAB; remove the space"],
74 [r'(?<!\w)([fs]?scanf|ato([filq]|ll))(?!\w)',
75 'do not use *scan''f, ato''f, ato''i, ato''l, ato''ll, ato''q, or ss''canf'],
76 [r'error \(EXIT_SUCCESS',"passing EXIT_SUCCESS to error is confusing"],
77 [r'file[s]ystem', "prefer writing 'file system' to 'filesystem'"],
78 [r'HAVE''_CONFIG_H', "Avoid checking HAVE_CONFIG_H"],
79 # [r'HAVE_FCNTL_H', "Avoid checking HAVE_FCNTL_H"],
80 [r'O_NDELAY', "Avoid using O_NDELAY"],
81 [r'the *the', "'the the' is probably not deliberate"],
82 [r'(?<!\w)error \([^_"]*[^_]"[^"]*[a-z]{3}', "untranslated error message"],
83 [r'^# *if\s+defined *\(', "useless parentheses in '#if defined'"],
87 [r'# *include <assert.h>(?!.*assert \()',
88 "If you include <assert.h>, use assert()."],
89 [r'# *include "quotearg.h"(?!.*(?<!\w)quotearg(_[^ ]+)? \()',
90 "If you include \"quotearg.h\", use one of its functions."],
91 [r'# *include "quote.h"(?!.*(?<!\w)quote(_[^ ]+)? \()',
92 "If you include \"quote.h\", use one of its functions."],
93 ]),
94 # Check Makefiles for Makefile code smells.
95 RegexChecker('(^|/)[Mm]akefile(.am|.in)?',
96 [ [r'^ ', "Spaces at start of line"], ],
97 []),
98 # Check everything for whitespace problems.
99 # RegexChecker('', [], [[r'\s$', "trailing whitespace"],]),
100 # Check everything for out of date addresses.
101 RegexChecker('', [], [
102 [r'675\s*Mass\s*Ave,\s*02139[^a-zA-Z]*USA',
103 "out of date FSF address"],
104 [r'59 Temple Place.*02111-?1307\s*USA',
105 "out of date FSF address"],
110 # missing check: ChangeLog prefixes
111 # missing: sc_always_defined_macros from coreutils
112 # missing: sc_tight_scope
115 def Warning(filename, desc):
116 print >> sys.stderr, "warning: %s: %s" % (filename, desc)
119 def BuildIncludeList(text):
120 """Build a list of included files, with line numbers.
121 Args:
122 text: the full text of the source file
123 Returns:
124 [ ('config.h',32), ('assert.h',33), ... ]
126 include_re = re.compile(r'# *include +[<"](.*)[>"]')
127 includes = []
128 last_include_pos = 1
129 line = 1
130 for m in include_re.finditer(text):
131 header = m.group(1)
132 # Count only the number of lines between the last include and
133 # this one. Counting them from the beginning would be quadratic.
134 line += m.string.count('\n', last_include_pos, m.start(0))
135 last_include_pos = m.end()
136 includes.append( (header,line) )
137 return includes
140 def CheckStatHeader(filename, lines, fulltext):
141 stat_hdr_re = re.compile(r'# *include .*<sys/stat.h>')
142 # It's OK to have a pointer though.
143 stat_use_re = re.compile(r'struct stat\W *[^*]')
144 for line in lines:
145 m = stat_use_re.search(line[1])
146 if m:
147 msg = "If you use struct stat, you must #include <sys/stat.h> first"
148 Problem(filename = filename, line = line[0], message = msg)
149 # Diagnose only once
150 break
151 m = stat_hdr_re.search(line[1])
152 if m:
153 break
155 def CheckFirstInclude(filename, lines, fulltext):
156 includes = BuildIncludeList(fulltext)
157 #print "Include map:"
158 #for name, line in includes:
159 # print "%s:%d: %s" % (filename, line, name)
160 if includes:
161 actual_first_include = includes[0][0]
162 else:
163 actual_first_include = None
164 if actual_first_include and actual_first_include != FIRST_INCLUDE:
165 if FIRST_INCLUDE in [inc[0] for inc in includes]:
166 msg = ("%(actual_first_include)s is the first included file, "
167 "but %(required_first_include)s should be included first")
168 Problem(filename=filename, line=includes[0][1], message=msg,
169 actual_first_include=actual_first_include,
170 required_first_include = FIRST_INCLUDE)
171 if FIRST_INCLUDE not in [inc[0] for inc in includes]:
172 Warning(filename,
173 "%s should be included by most files" % FIRST_INCLUDE)
176 def SniffSourceFile(filename, lines, fulltext):
177 if C_MODULE_FILENAME_RE.search(filename):
178 CheckFirstInclude(filename, lines, fulltext)
179 CheckStatHeader (filename, lines, fulltext)
180 for checker in checkers:
181 checker.Check(filename, lines, fulltext)
184 def main(args):
185 "main program"
186 for srcfile in args[1:]:
187 f = open(srcfile)
188 line_number = 1
189 lines = []
190 for line in f.readlines():
191 lines.append( (line_number, line) )
192 line_number += 1
193 fulltext = ''.join([line[1] for line in lines])
194 SniffSourceFile(srcfile, lines, fulltext)
195 f.close()
196 if problems:
197 return 1
198 else:
199 return 0
202 if __name__ == "__main__":
203 sys.exit(main(sys.argv))