#! /usr/bin/env python # src-sniff.py: checks source code for patterns that look like common errors. # Copyright (C) 2007-2023 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 . # 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'(?(?!.*assert \()', "If you include , use assert()."], [r'# *include "quotearg.h"(?!.*(?> 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 .*') # 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 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))