2 # Copyright (c) 2017 The Bitcoin Core developers
3 # Distributed under the MIT software license, see the accompanying
4 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
5 """Check RPC argument consistency."""
7 from collections
import defaultdict
12 # Source files (relative to root) to scan for dispatch tables
15 "src/rpc/blockchain.cpp",
19 "src/rpc/rawtransaction.cpp",
20 "src/wallet/rpcwallet.cpp",
22 # Source file (relative to root) containing conversion mapping
23 SOURCE_CLIENT
= 'src/rpc/client.cpp'
24 # Argument names that should be ignored in consistency checks
25 IGNORE_DUMMY_ARGS
= {'dummy', 'arg0', 'arg1', 'arg2', 'arg3', 'arg4', 'arg5', 'arg6', 'arg7', 'arg8', 'arg9'}
28 def __init__(self
, name
, args
):
33 def __init__(self
, names
, idx
):
43 def process_commands(fname
):
44 """Find and parse dispatch table in implementation file `fname`."""
47 with
open(fname
, "r") as f
:
51 if re
.match("static const CRPCCommand .*\[\] =", line
):
54 if line
.startswith('};'):
56 elif '{' in line
and '"' in line
:
57 m
= re
.search('{ *("[^"]*"), *("[^"]*"), *&([^,]*), *{([^}]*)} *},', line
)
58 assert m
, 'No match to table expression: %s' % line
59 name
= parse_string(m
.group(2))
60 args_str
= m
.group(4).strip()
62 args
= [RPCArgument(parse_string(x
.strip()).split('|'), idx
) for idx
, x
in enumerate(args_str
.split(','))]
65 cmds
.append(RPCCommand(name
, args
))
66 assert not in_rpcs
, "Something went wrong with parsing the C++ file: update the regexps"
69 def process_mapping(fname
):
70 """Find and parse conversion table in implementation file `fname`."""
73 with
open(fname
, "r") as f
:
77 if line
== 'static const CRPCConvertParam vRPCConvertParams[] =':
80 if line
.startswith('};'):
82 elif '{' in line
and '"' in line
:
83 m
= re
.search('{ *("[^"]*"), *([0-9]+) *, *("[^"]*") *},', line
)
84 assert m
, 'No match to table expression: %s' % line
85 name
= parse_string(m
.group(1))
87 argname
= parse_string(m
.group(3))
88 cmds
.append((name
, idx
, argname
))
95 # Get all commands from dispatch tables
98 cmds
+= process_commands(os
.path
.join(root
, fname
))
102 cmds_by_name
[cmd
.name
] = cmd
104 # Get current convert mapping for client
105 client
= SOURCE_CLIENT
106 mapping
= set(process_mapping(os
.path
.join(root
, client
)))
108 print('* Checking consistency between dispatch tables and vRPCConvertParams')
110 # Check mapping consistency
112 for (cmdname
, argidx
, argname
) in mapping
:
114 rargnames
= cmds_by_name
[cmdname
].args
[argidx
].names
116 print('ERROR: %s argument %i (named %s in vRPCConvertParams) is not defined in dispatch table' % (cmdname
, argidx
, argname
))
119 if argname
not in rargnames
:
120 print('ERROR: %s argument %i is named %s in vRPCConvertParams but %s in dispatch table' % (cmdname
, argidx
, argname
, rargnames
), file=sys
.stderr
)
123 # Check for conflicts in vRPCConvertParams conversion
124 # All aliases for an argument must either be present in the
125 # conversion table, or not. Anything in between means an oversight
126 # and some aliases won't work.
129 convert
= [((cmd
.name
, arg
.idx
, argname
) in mapping
) for argname
in arg
.names
]
130 if any(convert
) != all(convert
):
131 print('ERROR: %s argument %s has conflicts in vRPCConvertParams conversion specifier %s' % (cmd
.name
, arg
.names
, convert
))
133 arg
.convert
= all(convert
)
135 # Check for conversion difference by argument name.
136 # It is preferable for API consistency that arguments with the same name
137 # have the same conversion, so bin by argument name.
138 all_methods_by_argname
= defaultdict(list)
139 converts_by_argname
= defaultdict(list)
142 for argname
in arg
.names
:
143 all_methods_by_argname
[argname
].append(cmd
.name
)
144 converts_by_argname
[argname
].append(arg
.convert
)
146 for argname
, convert
in converts_by_argname
.items():
147 if all(convert
) != any(convert
):
148 if argname
in IGNORE_DUMMY_ARGS
:
149 # these are testing or dummy, don't warn for them
151 print('WARNING: conversion mismatch for argument named %s (%s)' %
152 (argname
, list(zip(all_methods_by_argname
[argname
], converts_by_argname
[argname
]))))
157 if __name__
== '__main__':