1#!/usr/bin/env python3
2#
3# cppcheck addon for naming conventions
4# An enhanced version. Configuration is taken from a json file
5# It supports to check for type-based prefixes in function or variable names.
6#
7# Example usage (variable name must start with lowercase, function name must start with uppercase):
8# $ cppcheck --dump path-to-src/
9# $ python namingng.py test.c.dump
10#
11# JSON format:
12#
13# {
14#    "RE_VARNAME": "[a-z]*[a-zA-Z0-9_]*\\Z",
15#    "RE_PRIVATE_MEMBER_VARIABLE": null,
16#    "RE_FUNCTIONNAME": "[a-z0-9A-Z]*\\Z",
17#    "var_prefixes": {"uint32_t": "ui32"},
18#    "function_prefixes": {"uint16_t": "ui16",
19#                          "uint32_t": "ui32"}
20# }
21#
22# RE_VARNAME, RE_PRIVATE_MEMBER_VARIABLE and RE_FUNCTIONNAME are regular expressions to cover the basic names
23# In var_prefixes and function_prefixes there are the variable-type/prefix pairs
24
25import cppcheckdata
26import sys
27import re
28import argparse
29import json
30
31
32# Auxiliary class
33class DataStruct:
34    def __init__(self, file, linenr, string):
35        self.file = file
36        self.linenr = linenr
37        self.str = string
38
39
40def reportError(filename, linenr, severity, msg):
41    message = "[{filename}:{linenr}] ( {severity} ) naming.py: {msg}\n".format(
42        filename=filename,
43        linenr=linenr,
44        severity=severity,
45        msg=msg
46    )
47    sys.stderr.write(message)
48    return message
49
50
51def loadConfig(configfile):
52    with open(configfile) as fh:
53        data = json.load(fh)
54    return data
55
56
57def checkTrueRegex(data, expr, msg, errors):
58    res = re.match(expr, data.str)
59    if res:
60        errors.append(reportError(data.file, data.linenr, 'style', msg))
61
62
63def checkFalseRegex(data, expr, msg, errors):
64    res = re.match(expr, data.str)
65    if not res:
66        errors.append(reportError(data.file, data.linenr, 'style', msg))
67
68
69def evalExpr(conf, exp, mockToken, msgType, errors):
70    if isinstance(conf, dict):
71        if conf[exp][0]:
72            msg = msgType + ' ' + mockToken.str + ' violates naming convention : ' + conf[exp][1]
73            checkTrueRegex(mockToken, exp, msg, errors)
74        elif ~conf[exp][0]:
75            msg = msgType + ' ' + mockToken.str + ' violates naming convention : ' + conf[exp][1]
76            checkFalseRegex(mockToken, exp, msg, errors)
77        else:
78            msg = msgType + ' ' + mockToken.str + ' violates naming convention : ' + conf[exp][0]
79            checkFalseRegex(mockToken, exp, msg, errors)
80    else:
81        msg = msgType + ' ' + mockToken.str + ' violates naming convention'
82        checkFalseRegex(mockToken, exp, msg, errors)
83
84
85def process(dumpfiles, configfile, debugprint=False):
86
87    errors = []
88
89    conf = loadConfig(configfile)
90
91    for afile in dumpfiles:
92        if not afile[-5:] == '.dump':
93            continue
94        print('Checking ' + afile + '...')
95        data = cppcheckdata.CppcheckData(afile)
96
97        # Check File naming
98        if "RE_FILE" in conf and conf["RE_FILE"]:
99            mockToken = DataStruct(afile[:-5], "0", afile[afile.rfind('/') + 1:-5])
100            msgType = 'File name'
101            for exp in conf["RE_FILE"]:
102                evalExpr(conf["RE_FILE"], exp, mockToken, msgType, errors)
103
104        # Check Namespace naming
105        if "RE_NAMESPACE" in conf and conf["RE_NAMESPACE"]:
106            for tk in data.rawTokens:
107                if tk.str == 'namespace':
108                    mockToken = DataStruct(tk.next.file, tk.next.linenr, tk.next.str)
109                    msgType = 'Namespace'
110                    for exp in conf["RE_NAMESPACE"]:
111                        evalExpr(conf["RE_NAMESPACE"], exp, mockToken, msgType, errors)
112
113        for cfg in data.configurations:
114            print('Checking %s, config %s...' % (afile, cfg.name))
115            if "RE_VARNAME" in conf and conf["RE_VARNAME"]:
116                for var in cfg.variables:
117                    if var.nameToken and var.access != 'Global' and var.access != 'Public' and var.access != 'Private':
118                        prev = var.nameToken.previous
119                        varType = prev.str
120                        while "*" in varType and len(varType.replace("*", "")) == 0:
121                            prev = prev.previous
122                            varType = prev.str + varType
123
124                        if debugprint:
125                            print("Variable Name: " + str(var.nameToken.str))
126                            print("original Type Name: " + str(var.nameToken.valueType.originalTypeName))
127                            print("Type Name: " + var.nameToken.valueType.type)
128                            print("Sign: " + str(var.nameToken.valueType.sign))
129                            print("variable type: " + varType)
130                            print("\n")
131                            print("\t-- {} {}".format(varType, str(var.nameToken.str)))
132
133                        if conf["skip_one_char_variables"] and len(var.nameToken.str) == 1:
134                            continue
135                        if varType in conf["var_prefixes"]:
136                            if not var.nameToken.str.startswith(conf["var_prefixes"][varType]):
137                                errors.append(reportError(
138                                                    var.typeStartToken.file,
139                                                    var.typeStartToken.linenr,
140                                                    'style',
141                                                    'Variable ' +
142                                                    var.nameToken.str +
143                                                    ' violates naming convention'))
144
145                        mockToken = DataStruct(var.typeStartToken.file, var.typeStartToken.linenr, var.nameToken.str)
146                        msgType = 'Variable'
147                        for exp in conf["RE_VARNAME"]:
148                            evalExpr(conf["RE_VARNAME"], exp, mockToken, msgType, errors)
149
150            # Check Private Variable naming
151            if "RE_PRIVATE_MEMBER_VARIABLE" in conf and conf["RE_PRIVATE_MEMBER_VARIABLE"]:
152                # TODO: Not converted yet
153                for var in cfg.variables:
154                    if (var.access is None) or var.access != 'Private':
155                        continue
156                    mockToken = DataStruct(var.typeStartToken.file, var.typeStartToken.linenr, var.nameToken.str)
157                    msgType = 'Private member variable'
158                    for exp in conf["RE_PRIVATE_MEMBER_VARIABLE"]:
159                        evalExpr(conf["RE_PRIVATE_MEMBER_VARIABLE"], exp, mockToken, msgType, errors)
160
161            # Check Public Member Variable naming
162            if "RE_PUBLIC_MEMBER_VARIABLE" in conf and conf["RE_PUBLIC_MEMBER_VARIABLE"]:
163                for var in cfg.variables:
164                    if (var.access is None) or var.access != 'Public':
165                        continue
166                    mockToken = DataStruct(var.typeStartToken.file, var.typeStartToken.linenr, var.nameToken.str)
167                    msgType = 'Public member variable'
168                    for exp in conf["RE_PUBLIC_MEMBER_VARIABLE"]:
169                        evalExpr(conf["RE_PUBLIC_MEMBER_VARIABLE"], exp, mockToken, msgType, errors)
170
171            # Check Global Variable naming
172            if "RE_GLOBAL_VARNAME" in conf and conf["RE_GLOBAL_VARNAME"]:
173                for var in cfg.variables:
174                    if (var.access is None) or var.access != 'Global':
175                        continue
176                    mockToken = DataStruct(var.typeStartToken.file, var.typeStartToken.linenr, var.nameToken.str)
177                    msgType = 'Public member variable'
178                    for exp in conf["RE_GLOBAL_VARNAME"]:
179                        evalExpr(conf["RE_GLOBAL_VARNAME"], exp, mockToken, msgType, errors)
180
181            # Check Functions naming
182            if "RE_FUNCTIONNAME" in conf and conf["RE_FUNCTIONNAME"]:
183                for token in cfg.tokenlist:
184                    if token.function:
185                        if token.function.type == 'Constructor' or token.function.type == 'Destructor':
186                            continue
187                        retval = token.previous.str
188                        prev = token.previous
189                        while "*" in retval and len(retval.replace("*", "")) == 0:
190                            prev = prev.previous
191                            retval = prev.str + retval
192                        if debugprint:
193                            print("\t:: {} {}".format(retval, token.function.name))
194
195                        if retval and retval in conf["function_prefixes"]:
196                            if not token.function.name.startswith(conf["function_prefixes"][retval]):
197                                errors.append(reportError(
198                                    token.file, token.linenr, 'style', 'Function ' + token.function.name + ' violates naming convention'))
199                        mockToken = DataStruct(token.file, token.linenr, token.function.name)
200                        msgType = 'Function'
201                        for exp in conf["RE_FUNCTIONNAME"]:
202                            evalExpr(conf["RE_FUNCTIONNAME"], exp, mockToken, msgType, errors)
203
204            # Check Class naming
205            if "RE_CLASS_NAME" in conf and conf["RE_CLASS_NAME"]:
206                for fnc in cfg.functions:
207                    # Check if it is Constructor/Destructor
208                    if fnc.type == 'Constructor' or fnc.type == 'Destructor':
209                        mockToken = DataStruct(fnc.tokenDef.file, fnc.tokenDef.linenr, fnc.name)
210                        msgType = 'Class ' + fnc.type
211                        for exp in conf["RE_CLASS_NAME"]:
212                            evalExpr(conf["RE_CLASS_NAME"], exp, mockToken, msgType, errors)
213    return errors
214
215
216if __name__ == "__main__":
217    parser = argparse.ArgumentParser(description='Naming verification')
218    parser.add_argument('dumpfiles', type=str, nargs='+',
219                        help='A set of dumpfiles to process')
220    parser.add_argument("--debugprint", action="store_true", default=False,
221                        help="Add debug prints")
222    parser.add_argument("--configfile", type=str, default="naming.json",
223                        help="Naming check config file")
224    parser.add_argument("--verify", action="store_true", default=False,
225                        help="verify this script. Must be executed in test folder !")
226
227    args = parser.parse_args()
228    errors = process(args.dumpfiles, args.configfile, args.debugprint)
229
230    if args.verify:
231        print(errors)
232        if len(errors) < 6:
233            print("Not enough errors found")
234            sys.exit(1)
235        target = [
236         '[namingng_test.c:8] ( style ) naming.py: Variable badui32 violates naming convention\n',
237         '[namingng_test.c:11] ( style ) naming.py: Variable a violates naming convention\n',
238         '[namingng_test.c:29] ( style ) naming.py: Variable badui32 violates naming convention\n',
239         '[namingng_test.c:20] ( style ) naming.py: Function ui16bad_underscore violates naming convention\n',
240         '[namingng_test.c:25] ( style ) naming.py: Function u32Bad violates naming convention\n',
241         '[namingng_test.c:37] ( style ) naming.py: Function Badui16 violates naming convention\n']
242        diff = set(errors) - set(target)
243        if len(diff):
244            print("Not the right errors found {}".format(str(diff)))
245            sys.exit(1)
246        print("Verification done\n")
247        sys.exit(0)
248
249    if len(errors):
250        print('Found errors: {}'.format(len(errors)))
251        sys.exit(1)
252
253    sys.exit(0)
254