src-sniff.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. #! /usr/bin/env python
  2. # src-sniff.py: checks source code for patterns that look like common errors.
  3. # Copyright (C) 2007-2021 Free Software Foundation, Inc.
  4. #
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. # Many of these would probably be better as gnulib syntax checks, because
  19. # gnulib provides a way of disabling checks for particular files, and
  20. # has a wider range of checks. Indeed, many of these checks do in fact
  21. # check the same thing as "make syntax-check".
  22. import os.path
  23. import re
  24. import sys
  25. C_ISH_FILENAME = "\.(c|cc|h|cpp|cxx|hxx)$"
  26. C_ISH_FILENAME_RE = re.compile(C_ISH_FILENAME)
  27. C_MODULE_FILENAME_RE = re.compile("\.(c|cc|cpp|cxx)$")
  28. FIRST_INCLUDE = 'config.h'
  29. problems = 0
  30. def Problem(**kwargs):
  31. global problems
  32. problems += 1
  33. msg = kwargs['message']
  34. if kwargs['line']:
  35. location = "%(filename)s:%(line)d" % kwargs
  36. else:
  37. location = "%(filename)s" % kwargs
  38. detail = msg % kwargs
  39. print >>sys.stderr, "error: %s: %s" % (location, detail)
  40. class RegexSniffer(object):
  41. def __init__(self, source, message, regexflags=0):
  42. super(RegexSniffer, self).__init__()
  43. self._regex = re.compile(source, regexflags)
  44. self._msg = message
  45. def Sniff(self, text, filename, line):
  46. #print >>sys.stderr, ("Matching %s against %s"
  47. # % (text, self._regex.pattern))
  48. m = self._regex.search(text)
  49. if m:
  50. if line is None:
  51. line = 1 + m.string.count('\n', 1, m.start(0))
  52. args = {
  53. 'filename' : filename,
  54. 'line' : line,
  55. 'fulltext' : text,
  56. 'matchtext': m.group(0),
  57. 'message' : self._msg
  58. }
  59. Problem(**args)
  60. class RegexChecker(object):
  61. def __init__(self, regex, line_smells, file_smells):
  62. super(RegexChecker, self).__init__()
  63. self._regex = re.compile(regex)
  64. self._line_sniffers = [RegexSniffer(s[0],s[1]) for s in line_smells]
  65. self._file_sniffers = [RegexSniffer(s[0],s[1],re.S|re.M) for s in file_smells]
  66. def Check(self, filename, lines, fulltext):
  67. if self._regex.search(filename):
  68. # We recognise this type of file.
  69. for line_number, line_text in lines:
  70. for sniffer in self._line_sniffers:
  71. sniffer.Sniff(line_text, filename, line_number)
  72. for sniffer in self._file_sniffers:
  73. sniffer.Sniff(fulltext, filename, None)
  74. else:
  75. # We don't know how to check this file. Skip it.
  76. pass
  77. class MakefileRegexChecker(object):
  78. MAKEFILE_PRIORITY_LIST = ['Makefile.am', 'Makefile.in', 'Makefile']
  79. MAKEFILE_REGEX = ''.join(
  80. '|'.join(['(%s)' % pattern for pattern in MAKEFILE_PRIORITY_LIST]))
  81. def __init__(self, line_smells, file_smells):
  82. self._file_regex = re.compile(self.MAKEFILE_REGEX)
  83. self._rxc = RegexChecker(self.MAKEFILE_REGEX, line_smells, file_smells)
  84. def WantToCheck(self, filename):
  85. if not self._file_regex.search(filename):
  86. return False
  87. makefile_base = os.path.basename(filename)
  88. makefile_dir = os.path.dirname(filename)
  89. for base in self.MAKEFILE_PRIORITY_LIST:
  90. path = os.path.join(makefile_dir, base)
  91. if os.path.exists(path):
  92. if path == filename:
  93. # The first existing name in MAKEFILE_PRIORITY_LIST
  94. # is actually this file, so we want to check it.
  95. return True
  96. else:
  97. # These is another (source) Makefile we want to check
  98. # instead.
  99. return False
  100. # If we get to here we were asked about a file which either
  101. # doesn't exist or which doesn't look like anything in
  102. # MAKEFILE_PRIORITY_LIST. So give the go-ahead to check it.
  103. return True
  104. def Check(self, filename, lines, fulltext):
  105. if self.WantToCheck(filename):
  106. self._rxc.Check(filename, lines, fulltext)
  107. checkers = [
  108. # Check C-like languages for C code smells.
  109. RegexChecker(C_ISH_FILENAME_RE,
  110. # line smells
  111. [
  112. [r'^\s*#\s*define\s+(_[A-Z_]+)', "Don't use reserved macro names"],
  113. [r'(?<!\w)free \(\(', "don't cast the argument to free()"],
  114. [r'\*\) *x(m|c|re)alloc(?!\w)',"don't cast the result of x*alloc"],
  115. [r'\*\) *alloca(?!\w)',"don't cast the result of alloca"],
  116. [r'[ ] ',"found SPACE-TAB; remove the space"],
  117. [r'(?<!\w)([fs]?scanf|ato([filq]|ll))(?!\w)', 'do not use %(matchtext)s'],
  118. [r'error \(EXIT_SUCCESS',"passing EXIT_SUCCESS to error is confusing"],
  119. [r'file[s]ystem', "prefer writing 'file system' to 'filesystem'"],
  120. [r'HAVE''_CONFIG_H', "Avoid checking HAVE_CONFIG_H"],
  121. [r'HAVE_FCNTL_H', "Avoid checking HAVE_FCNTL_H"],
  122. [r'O_NDELAY', "Avoid using O_NDELAY"],
  123. [r'the\s*the', "'the"+" the' is probably not deliberate"],
  124. [r'(?<!\w)error \([^_"]*[^_]"[^"]*[a-z]{3}', "untranslated error message"],
  125. [r'^# *if\s+defined *\(', "useless parentheses in '#if defined'"],
  126. ],
  127. [
  128. [r'# *include <assert.h>(?!.*assert \()',
  129. "If you include <assert.h>, use assert()."],
  130. [r'# *include "quotearg.h"(?!.*(?<!\w)quotearg(_[^ ]+)? \()',
  131. "If you include \"quotearg.h\", use one of its functions."],
  132. [r'# *include "quote.h"(?!.*(?<!\w)quote(_[^ ]+)? \()',
  133. "If you include \"quote.h\", use one of its functions."],
  134. ]),
  135. # Check Makefiles for Makefile code smells.
  136. MakefileRegexChecker([ [r'^ ', "Spaces at start of makefile line"], ],
  137. []),
  138. # Check everything for whitespace problems.
  139. RegexChecker('', [], [[r'[ ]$',
  140. "trailing whitespace '%(matchtext)s'"],]),
  141. # Check everything for out of date addresses.
  142. RegexChecker('', [], [
  143. [r'675\s*Mass\s*Ave,\s*02139[^a-zA-Z]*USA',
  144. "out of date FSF address"],
  145. [r'59 Temple Place.*02111-?1307\s*USA',
  146. "out of date FSF address %(matchtext)s"],
  147. ]),
  148. # Check everything for GPL version regression
  149. RegexChecker('',
  150. [],
  151. [[r'G(nu |eneral )?P(ublic )?L(icense)?.{1,200}version [12]',
  152. "Out of date GPL version: %(matchtext)s"],
  153. ]),
  154. # Bourne shell code smells
  155. RegexChecker('\.sh$',
  156. [
  157. ['for\s*\w+\s*in.*;\s*do',
  158. # Solaris 10 /bin/sh rejects this, see Autoconf manual
  159. "for loops should not contain a 'do' on the same line."],
  160. ], []),
  161. ]
  162. # missing check: ChangeLog prefixes
  163. # missing: sc_always_defined_macros from coreutils
  164. # missing: sc_tight_scope
  165. def Warning(filename, desc):
  166. print >> sys.stderr, "warning: %s: %s" % (filename, desc)
  167. def BuildIncludeList(text):
  168. """Build a list of included files, with line numbers.
  169. Args:
  170. text: the full text of the source file
  171. Returns:
  172. [ ('config.h',32), ('assert.h',33), ... ]
  173. """
  174. include_re = re.compile(r'# *include +[<"](.*)[>"]')
  175. includes = []
  176. last_include_pos = 1
  177. line = 1
  178. for m in include_re.finditer(text):
  179. header = m.group(1)
  180. # Count only the number of lines between the last include and
  181. # this one. Counting them from the beginning would be quadratic.
  182. line += m.string.count('\n', last_include_pos, m.start(0))
  183. last_include_pos = m.end()
  184. includes.append( (header,line) )
  185. return includes
  186. def CheckStatHeader(filename, lines, fulltext):
  187. stat_hdr_re = re.compile(r'# *include .*<sys/stat.h>')
  188. # It's OK to have a pointer though.
  189. stat_use_re = re.compile(r'struct stat\W *[^*]')
  190. for line in lines:
  191. m = stat_use_re.search(line[1])
  192. if m:
  193. msg = "If you use struct stat, you must #include <sys/stat.h> first"
  194. Problem(filename = filename, line = line[0], message = msg)
  195. # Diagnose only once
  196. break
  197. m = stat_hdr_re.search(line[1])
  198. if m:
  199. break
  200. def CheckFirstInclude(filename, lines, fulltext):
  201. includes = BuildIncludeList(fulltext)
  202. #print "Include map:"
  203. #for name, line in includes:
  204. # print "%s:%d: %s" % (filename, line, name)
  205. if includes:
  206. actual_first_include = includes[0][0]
  207. else:
  208. actual_first_include = None
  209. if actual_first_include and actual_first_include != FIRST_INCLUDE:
  210. if FIRST_INCLUDE in [inc[0] for inc in includes]:
  211. msg = ("%(actual_first_include)s is the first included file, "
  212. "but %(required_first_include)s should be included first")
  213. Problem(filename=filename, line=includes[0][1], message=msg,
  214. actual_first_include=actual_first_include,
  215. required_first_include = FIRST_INCLUDE)
  216. if FIRST_INCLUDE not in [inc[0] for inc in includes]:
  217. Warning(filename,
  218. "%s should be included by most files" % FIRST_INCLUDE)
  219. def SniffSourceFile(filename, lines, fulltext):
  220. if C_MODULE_FILENAME_RE.search(filename):
  221. CheckFirstInclude(filename, lines, fulltext)
  222. CheckStatHeader (filename, lines, fulltext)
  223. for checker in checkers:
  224. checker.Check(filename, lines, fulltext)
  225. def main(args):
  226. "main program"
  227. for srcfile in args[1:]:
  228. f = open(srcfile)
  229. line_number = 1
  230. lines = []
  231. for line in f.readlines():
  232. lines.append( (line_number, line) )
  233. line_number += 1
  234. fulltext = ''.join([line[1] for line in lines])
  235. SniffSourceFile(srcfile, lines, fulltext)
  236. f.close()
  237. if problems:
  238. return 1
  239. else:
  240. return 0
  241. if __name__ == "__main__":
  242. sys.exit(main(sys.argv))