#! /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))