1#!/usr/bin/env python 2# -*- coding: utf-8 -*- 3# 4# Copyright 2017, Data61 5# Commonwealth Scientific and Industrial Research Organisation (CSIRO) 6# ABN 41 687 119 230. 7# 8# This software may be distributed and modified according to the terms of 9# the BSD 2-Clause license. Note that NO WARRANTY is provided. 10# See "LICENSE_BSD2.txt" for details. 11# 12# @TAG(DATA61_BSD) 13# 14 15''' 16CAmkES source linter. 17 18This tool is designed to recognise various Python programming idioms used in 19the CAmkES sources and report likely typos in them. It is fairly limited right 20now, but feel free to extend it to detect more idioms. 21''' 22 23import argparse, ast, codecs, sys 24 25def main(argv): 26 parser = argparse.ArgumentParser( 27 description='check source files for mistaken CAmkES idioms') 28 parser.add_argument('filename', help='file to check', 29 type=argparse.FileType('r')) 30 options = parser.parse_args(argv[1:]) 31 32 # Parse the input as UTF-8 as all the CAmkES sources are intended to be 33 # UTF-8-clean. 34 with codecs.open(options.filename.name, 'rt', 'utf-8') as f: 35 st = ast.parse(f.read().encode('utf-8')) 36 37 result = 0 38 39 # Walk the AST of the input file looking for functions. 40 for node in (x for x in ast.walk(st) if isinstance(x, ast.FunctionDef)): 41 42 # Extract the names of the arguments to this function. 43 args = [x.id for x in node.args.args] 44 45 # Scan any leading assertions in the function's body. 46 for stmt in node.body: 47 if not isinstance(stmt, ast.Assert): 48 break 49 50 # Extract the body of the assertion. 51 test = stmt.test 52 53 # Now try to recognise two idioms: 54 # 1. `assert foo is None or isinstance(foo, ...)`; and 55 # 2. `assert isinstance(foo, ...)`. 56 # These idioms are used in the CAmkES sources to perform runtime 57 # type-checking of function parameters. Any deviation from the 58 # above templates likely indicates a typo. 59 60 # In the following variable, we'll store the node corresponding to 61 # the `isinstance` call in one of the two above idioms. 62 insttest = None 63 64 # Try to recognise case (1). 65 if isinstance(test, ast.BoolOp) and \ 66 isinstance(test.op, ast.Or) and \ 67 isinstance(test.values[0], ast.Compare) and \ 68 isinstance(test.values[0].left, ast.Name) and \ 69 len(test.values[0].ops) == 1 and \ 70 isinstance(test.values[0].ops[0], ast.Is) and \ 71 len(test.values[0].comparators) == 1 and \ 72 isinstance(test.values[0].comparators[0], ast.Name): 73 74 if test.values[0].left.id not in args: 75 sys.stderr.write('%s:%d: leading assertion references ' 76 '`%s` that is not a function argument\n' % 77 (options.filename.name, test.values[0].left.lineno, 78 test.values[0].left.id)) 79 result |= -1 80 81 if test.values[0].comparators[0].id != 'None': 82 sys.stderr.write('%s:%d: leading `is` assertion against ' 83 '`%s` instead of `None` as expected\n' % 84 (options.filename.name, 85 test.values[0].comparators[0].lineno, 86 test.values[0].comparators[0].id)) 87 result |= -1 88 89 if isinstance(test.values[1], ast.Call) and \ 90 test.values[1].func.id == 'isinstance': 91 insttest = test.values[1] 92 93 # Try to recognise case (2). 94 if isinstance(test, ast.Call) and \ 95 isinstance(test.func, ast.Name) and \ 96 test.func.id == 'isinstance': 97 insttest = test 98 99 # Check the `isinstance` contents. 100 if insttest is not None: 101 if len(insttest.args) != 2: 102 sys.stderr.write('%s:%d: %d arguments to `isinstance` ' 103 'instead of 2 as expected\n' % (options.filename.name, 104 insttest.lineno, len(insttest.args))) 105 result |= -1 106 107 elif not isinstance(insttest.args[0], (ast.Name, ast.Attribute)): 108 sys.stderr.write('%s:%d: unexpected first argument to ' 109 '`isinstance`\n' % (options.filename.name, 110 insttest.args[0].lineno)) 111 result |= -1 112 113 elif isinstance(insttest.args[0], ast.Name) and \ 114 insttest.args[0].id not in args: 115 sys.stderr.write('%s:%d: leading assertion references ' 116 '`%s` that is not a function argument\n' % 117 (options.filename.name, insttest.args[0].lineno, 118 insttest.args[0].id)) 119 result |= -1 120 121 elif isinstance(insttest.args[0], ast.Attribute) and \ 122 (not isinstance(insttest.args[0].value, ast.Name) or 123 insttest.args[0].value.id != 'self'): 124 sys.stderr.write('%s:%d: leading assertion references ' 125 '`%s.%s` that is not a function argument\n' % 126 (options.filename.name, insttest.args[0].lineno, 127 insttest.args[0].value.id, insttest.args[0].attr)) 128 result |= -1 129 130 return result 131 132if __name__ == '__main__': 133 sys.exit(main(sys.argv)) 134