123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- #! /usr/bin/env python
- # src-sniff.py: checks source code for patterns that look like common errors.
- # Copyright (C) 2007-2021 Free Software Foundation, Inc.
- #
- #
- # This program is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <https://www.gnu.org/licenses/>.
- # Many of these would probably be better as gnulib syntax checks, because
- # gnulib provides a way of disabling checks for particular files, and
- # has a wider range of checks. Indeed, many of these checks do in fact
- # check the same thing as "make syntax-check".
- import os.path
- import re
- import sys
- C_ISH_FILENAME = "\.(c|cc|h|cpp|cxx|hxx)$"
- C_ISH_FILENAME_RE = re.compile(C_ISH_FILENAME)
- C_MODULE_FILENAME_RE = re.compile("\.(c|cc|cpp|cxx)$")
- FIRST_INCLUDE = 'config.h'
- problems = 0
- def Problem(**kwargs):
- global problems
- problems += 1
- msg = kwargs['message']
- if kwargs['line']:
- location = "%(filename)s:%(line)d" % kwargs
- else:
- location = "%(filename)s" % kwargs
- detail = msg % kwargs
- print >>sys.stderr, "error: %s: %s" % (location, detail)
- class RegexSniffer(object):
- def __init__(self, source, message, regexflags=0):
- super(RegexSniffer, self).__init__()
- self._regex = re.compile(source, regexflags)
- self._msg = message
- def Sniff(self, text, filename, line):
- #print >>sys.stderr, ("Matching %s against %s"
- # % (text, self._regex.pattern))
- m = self._regex.search(text)
- if m:
- if line is None:
- line = 1 + m.string.count('\n', 1, m.start(0))
- args = {
- 'filename' : filename,
- 'line' : line,
- 'fulltext' : text,
- 'matchtext': m.group(0),
- 'message' : self._msg
- }
- Problem(**args)
- class RegexChecker(object):
- def __init__(self, regex, line_smells, file_smells):
- super(RegexChecker, self).__init__()
- self._regex = re.compile(regex)
- self._line_sniffers = [RegexSniffer(s[0],s[1]) for s in line_smells]
- self._file_sniffers = [RegexSniffer(s[0],s[1],re.S|re.M) for s in file_smells]
- def Check(self, filename, lines, fulltext):
- if self._regex.search(filename):
- # We recognise this type of file.
- for line_number, line_text in lines:
- for sniffer in self._line_sniffers:
- sniffer.Sniff(line_text, filename, line_number)
- for sniffer in self._file_sniffers:
- sniffer.Sniff(fulltext, filename, None)
- else:
- # We don't know how to check this file. Skip it.
- pass
- class MakefileRegexChecker(object):
- MAKEFILE_PRIORITY_LIST = ['Makefile.am', 'Makefile.in', 'Makefile']
- MAKEFILE_REGEX = ''.join(
- '|'.join(['(%s)' % pattern for pattern in MAKEFILE_PRIORITY_LIST]))
- def __init__(self, line_smells, file_smells):
- self._file_regex = re.compile(self.MAKEFILE_REGEX)
- self._rxc = RegexChecker(self.MAKEFILE_REGEX, line_smells, file_smells)
- def WantToCheck(self, filename):
- if not self._file_regex.search(filename):
- return False
- makefile_base = os.path.basename(filename)
- makefile_dir = os.path.dirname(filename)
- for base in self.MAKEFILE_PRIORITY_LIST:
- path = os.path.join(makefile_dir, base)
- if os.path.exists(path):
- if path == filename:
- # The first existing name in MAKEFILE_PRIORITY_LIST
- # is actually this file, so we want to check it.
- return True
- else:
- # These is another (source) Makefile we want to check
- # instead.
- return False
- # If we get to here we were asked about a file which either
- # doesn't exist or which doesn't look like anything in
- # MAKEFILE_PRIORITY_LIST. So give the go-ahead to check it.
- return True
- def Check(self, filename, lines, fulltext):
- if self.WantToCheck(filename):
- self._rxc.Check(filename, lines, fulltext)
- checkers = [
- # Check C-like languages for C code smells.
- RegexChecker(C_ISH_FILENAME_RE,
- # line smells
- [
- [r'^\s*#\s*define\s+(_[A-Z_]+)', "Don't use reserved macro names"],
- [r'(?<!\w)free \(\(', "don't cast the argument to free()"],
- [r'\*\) *x(m|c|re)alloc(?!\w)',"don't cast the result of x*alloc"],
- [r'\*\) *alloca(?!\w)',"don't cast the result of alloca"],
- [r'[ ] ',"found SPACE-TAB; remove the space"],
- [r'(?<!\w)([fs]?scanf|ato([filq]|ll))(?!\w)', 'do not use %(matchtext)s'],
- [r'error \(EXIT_SUCCESS',"passing EXIT_SUCCESS to error is confusing"],
- [r'file[s]ystem', "prefer writing 'file system' to 'filesystem'"],
- [r'HAVE''_CONFIG_H', "Avoid checking HAVE_CONFIG_H"],
- [r'HAVE_FCNTL_H', "Avoid checking HAVE_FCNTL_H"],
- [r'O_NDELAY', "Avoid using O_NDELAY"],
- [r'the\s*the', "'the"+" the' is probably not deliberate"],
- [r'(?<!\w)error \([^_"]*[^_]"[^"]*[a-z]{3}', "untranslated error message"],
- [r'^# *if\s+defined *\(', "useless parentheses in '#if defined'"],
- ],
- [
- [r'# *include <assert.h>(?!.*assert \()',
- "If you include <assert.h>, use assert()."],
- [r'# *include "quotearg.h"(?!.*(?<!\w)quotearg(_[^ ]+)? \()',
- "If you include \"quotearg.h\", use one of its functions."],
- [r'# *include "quote.h"(?!.*(?<!\w)quote(_[^ ]+)? \()',
- "If you include \"quote.h\", use one of its functions."],
- ]),
- # Check Makefiles for Makefile code smells.
- MakefileRegexChecker([ [r'^ ', "Spaces at start of makefile line"], ],
- []),
- # Check everything for whitespace problems.
- RegexChecker('', [], [[r'[ ]$',
- "trailing whitespace '%(matchtext)s'"],]),
- # Check everything for out of date addresses.
- RegexChecker('', [], [
- [r'675\s*Mass\s*Ave,\s*02139[^a-zA-Z]*USA',
- "out of date FSF address"],
- [r'59 Temple Place.*02111-?1307\s*USA',
- "out of date FSF address %(matchtext)s"],
- ]),
- # Check everything for GPL version regression
- RegexChecker('',
- [],
- [[r'G(nu |eneral )?P(ublic )?L(icense)?.{1,200}version [12]',
- "Out of date GPL version: %(matchtext)s"],
- ]),
- # Bourne shell code smells
- RegexChecker('\.sh$',
- [
- ['for\s*\w+\s*in.*;\s*do',
- # Solaris 10 /bin/sh rejects this, see Autoconf manual
- "for loops should not contain a 'do' on the same line."],
- ], []),
- ]
- # missing check: ChangeLog prefixes
- # missing: sc_always_defined_macros from coreutils
- # missing: sc_tight_scope
- def Warning(filename, desc):
- print >> sys.stderr, "warning: %s: %s" % (filename, desc)
- def BuildIncludeList(text):
- """Build a list of included files, with line numbers.
- Args:
- text: the full text of the source file
- Returns:
- [ ('config.h',32), ('assert.h',33), ... ]
- """
- include_re = re.compile(r'# *include +[<"](.*)[>"]')
- includes = []
- last_include_pos = 1
- line = 1
- for m in include_re.finditer(text):
- header = m.group(1)
- # Count only the number of lines between the last include and
- # this one. Counting them from the beginning would be quadratic.
- line += m.string.count('\n', last_include_pos, m.start(0))
- last_include_pos = m.end()
- includes.append( (header,line) )
- return includes
- def CheckStatHeader(filename, lines, fulltext):
- stat_hdr_re = re.compile(r'# *include .*<sys/stat.h>')
- # It's OK to have a pointer though.
- stat_use_re = re.compile(r'struct stat\W *[^*]')
- for line in lines:
- m = stat_use_re.search(line[1])
- if m:
- msg = "If you use struct stat, you must #include <sys/stat.h> first"
- Problem(filename = filename, line = line[0], message = msg)
- # Diagnose only once
- break
- m = stat_hdr_re.search(line[1])
- if m:
- break
- def CheckFirstInclude(filename, lines, fulltext):
- includes = BuildIncludeList(fulltext)
- #print "Include map:"
- #for name, line in includes:
- # print "%s:%d: %s" % (filename, line, name)
- if includes:
- actual_first_include = includes[0][0]
- else:
- actual_first_include = None
- if actual_first_include and actual_first_include != FIRST_INCLUDE:
- if FIRST_INCLUDE in [inc[0] for inc in includes]:
- msg = ("%(actual_first_include)s is the first included file, "
- "but %(required_first_include)s should be included first")
- Problem(filename=filename, line=includes[0][1], message=msg,
- actual_first_include=actual_first_include,
- required_first_include = FIRST_INCLUDE)
- if FIRST_INCLUDE not in [inc[0] for inc in includes]:
- Warning(filename,
- "%s should be included by most files" % FIRST_INCLUDE)
- def SniffSourceFile(filename, lines, fulltext):
- if C_MODULE_FILENAME_RE.search(filename):
- CheckFirstInclude(filename, lines, fulltext)
- CheckStatHeader (filename, lines, fulltext)
- for checker in checkers:
- checker.Check(filename, lines, fulltext)
- def main(args):
- "main program"
- for srcfile in args[1:]:
- f = open(srcfile)
- line_number = 1
- lines = []
- for line in f.readlines():
- lines.append( (line_number, line) )
- line_number += 1
- fulltext = ''.join([line[1] for line in lines])
- SniffSourceFile(srcfile, lines, fulltext)
- f.close()
- if problems:
- return 1
- else:
- return 0
- if __name__ == "__main__":
- sys.exit(main(sys.argv))
|