1"""Use the Python pygments library to perform extra checks on C++ grammar."""
2
3from pygments import token
4from pygments.lexers.compiled import CppLexer
5import os
6
7
8def check_header_file(fh_name, project_name, errors):
9    """Check a single C++ header file"""
10    _check_file(fh_name, project_name, True, errors)
11
12
13def check_cpp_file(fh_name, project_name, errors):
14    """Check a single C++ source file"""
15    _check_file(fh_name, project_name, False, errors)
16
17
18def _check_file(fh_name, project_name, header, errors):
19    fh, filename = fh_name
20    s = tokenize_file(fh)
21    check_tokens(s, filename, project_name, header, errors)
22
23
24def tokenize_file(fh):
25    """Use the Python pygments library to tokenize a C++ file"""
26    code = fh.read()
27    c = CppLexer()
28    scan = []
29    for (index, tok, value) in c.get_tokens_unprocessed(code):
30        scan.append((tok, value))
31    return scan
32
33
34def check_tokens(scan, filename, project_name, header, errors):
35    if filename.find("test_") == -1:
36        # we don't do it for python tests
37        check_comment_header(scan, filename, errors)
38    if header:
39        # Handle older versions of pygments which concatenate \n and # tokens
40        if len(scan) >= 3 and scan[2][0] == token.Comment.Preproc \
41           and scan[2][1] == '\n#':
42            scan[2] = (token.Comment.Preproc, '#')
43            scan.insert(2, (token.Comment.Text, '\n'))
44        check_header_start_end(scan, filename, project_name, errors)
45
46
47def check_comment_header(scan, filename, errors):
48    if len(scan) < 1 or scan[0][0] not in (token.Comment,
49                                           token.Comment.Multiline):
50        errors.append('%s:1: First line should be a comment with a copyright '
51                      'notice and a description of the file' % filename)
52
53
54def have_header_guard(scan):
55    return len(scan) >= 11 \
56        and scan[4][0] == token.Comment.Preproc \
57        and scan[4][1].startswith('ifndef') \
58        and scan[7][0] == token.Comment.Preproc \
59        and scan[7][1].startswith('define') \
60        and scan[-3][0] == token.Comment.Preproc \
61        and scan[-3][1].startswith('endif') \
62        and scan[-2][0] in (token.Comment, token.Comment.Multiline)
63
64
65def get_header_guard(filename, project_name):
66    """Get prefix and suffix for header guard"""
67    guard_prefix = project_name.replace(".", "").upper()
68    guard_suffix = os.path.split(filename)[1].replace(".", "_").upper()
69    return guard_prefix, guard_suffix
70
71
72def check_header_start_end(scan, filename, project_name, errors):
73    guard_prefix, guard_suffix = get_header_guard(filename, project_name)
74    header_guard = guard_prefix + '_' + guard_suffix
75    if len(scan) < 11:
76        bad = True
77    else:
78        bad = False
79        if not scan[4][0] == token.Comment.Preproc:
80            bad = True
81        if not scan[4][1].startswith('ifndef'):
82            errors.append('%s:%d: Header guard missing #ifndef.'
83                          % (filename, 1))
84            bad = True
85        if not scan[7][0] == token.Comment.Preproc:
86            bad = True
87        if not scan[7][1].startswith('define'):
88            errors.append('%s:%d: Header guard missing #define.'
89                          % (filename, 1))
90            bad = True
91        if not scan[-3][0] == token.Comment.Preproc \
92           and not scan[-4][0] == token.Comment.Preproc:
93            bad = True
94        if not scan[-3][1].startswith('endif') \
95           and not scan[-4][1].startswith('endif'):
96            errors.append('%s:%d: Header guard missing #endif.'
97                          % (filename, 1))
98            bad = True
99        if not scan[-2][0] in (token.Comment, token.Comment.Multiline) \
100           and not scan[-3][0] in (token.Comment, token.Comment.Multiline):
101            errors.append('%s:%d: Header guard missing closing comment.'
102                          % (filename, 1))
103            bad = True
104
105        guard = scan[4][1][7:]
106        if not guard.startswith(guard_prefix):
107            errors.append('%s:%d: Header guard does not start with "%s".'
108                          % (filename, 1, guard_prefix))
109            bad = True
110        if not guard.replace("_", "").endswith(guard_suffix.replace("_", "")):
111            errors.append('%s:%d: Header guard does not end with "%s".'
112                          % (filename, 1, guard_suffix))
113            bad = True
114        if not scan[7][1] == 'define ' + guard:
115            errors.append('%s:%d: Header guard does not define "%s".'
116                          % (filename, 1, guard))
117            bad = True
118        if not scan[-2][1] == '/* %s */' % guard \
119           and not scan[-3][1] == '/* %s */' % guard:
120            errors.append('%s:%d: Header guard close does not have a '
121                          'comment of "/* %s */".' % (filename, 1, guard))
122            bad = True
123    if bad:
124        errors.append('%s:%d: Missing or incomplete header guard.'
125                      % (filename, 1) + """
126Header files should start with a comment, then a blank line, then the rest
127of the file wrapped with a header guard. This must start with %s
128and end with %s - in between can be placed extra qualifiers, e.g. for a
129namespace. For example,
130
131/** Copyright and file description */
132
133#ifndef %s
134#define %s
135...
136#endif /* %s */
137""" % (guard_prefix, guard_suffix, header_guard, header_guard, header_guard))
138