├── .gitignore ├── CONTRIBUTORS ├── README.md ├── TODO.txt ├── base.vim ├── build.py ├── complexity.py ├── grammar ├── count_nodes.py ├── everything.py └── python_ast_node_types.txt ├── linum.el ├── pycomplexity.el ├── pycomplexity.vim ├── doc │ └── complexity.txt └── ftplugin │ └── python │ └── complexity.vim ├── runtests.py └── tests ├── __init__.py ├── manual ├── test_function_on_first_line.py ├── test_functions.py ├── test_high_complexity_module.py └── test_update_function.py ├── test_comprehensions.py ├── test_conditionals.py ├── test_exceptions.py ├── test_integration.py ├── test_loops.py ├── test_scoped_objects.py ├── test_simple_statements.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /envs 3 | /.ropeproject 4 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Original vim script by Gary Bernhardt 2 | Emacs support added by Ignas Mikalajūnas 3 | 4 | Patches contributed by: 5 | - Godefroid Chapelle 6 | - Steve Bedford 7 | - Chris Clark 8 | - Peter Prohaska 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pycomplexity 2 | 3 | Scripts to show cyclomatic complexity of Python code in [Vim][VimHome] and [Emacs][EmacsHome]. 4 | 5 | Original vim script by **Gary Bernhardt**. Emacs support added by **Ignas Mikalajūnas**. 6 | 7 | Patches contributed by: 8 | 9 | * Godefroid Chapelle 10 | * Steve Bedford 11 | * Chris Clark 12 | * Peter Prohaska 13 | 14 | 15 | ## vim plugin 16 | Vim plugin is in *pycomplexity.vim* directory 17 | ![vim python complexity][VimScreenshot] 18 | 19 | ### install vim plugin with NeoBundle 20 | If you're using [NeoBundle][NeoBundleRepository] plugin manager you can add this into ~/.vimrc: 21 | 22 | ```viml 23 | NeoBundle 'garybernhardt/pycomplexity', {'rtp': 'pycomplexity.vim/'} 24 | " optional F6 mapping to fire :Complexity command 25 | nnoremap :Complexity 26 | ``` 27 | 28 | [VimHome]:http://www.vim.org/ 29 | [EmacsHome]:http://www.gnu.org/software/emacs/ 30 | [NeoBundleRepository]:https://github.com/Shougo/neobundle.vim 31 | [VimScreenshot]:http://blog.extracheese.org/images/vim_complexity.png 32 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | Should catching multiple exception types in an "except" increase complexity? 2 | 3 | Handle nested functions. 4 | -------------------------------------------------------------------------------- /base.vim: -------------------------------------------------------------------------------- 1 | " complexity.vim 2 | " Gary Bernhardt (http://blog.extracheese.org) 3 | " 4 | " This will add cyclomatic complexity annotations to your source code. It is 5 | " no longer wrong (as previous versions were!) 6 | 7 | if !has('signs') 8 | finish 9 | endif 10 | if !has('python') 11 | finish 12 | endif 13 | python << endpython 14 | import vim 15 | %(python_source)s 16 | endpython 17 | 18 | function! ShowComplexity() 19 | python << END 20 | show_complexity() 21 | END 22 | " no idea why it is needed to update colors each time 23 | " to actually see the colors 24 | hi low_complexity guifg=#004400 guibg=#004400 ctermfg=2 ctermbg=2 25 | hi medium_complexity guifg=#bbbb00 guibg=#bbbb00 ctermfg=3 ctermbg=3 26 | hi high_complexity guifg=#ff2222 guibg=#ff2222 ctermfg=1 ctermbg=1 27 | endfunction 28 | 29 | hi SignColumn guifg=fg guibg=bg 30 | hi low_complexity guifg=#004400 guibg=#004400 ctermfg=2 ctermbg=2 31 | hi medium_complexity guifg=#bbbb00 guibg=#bbbb00 ctermfg=3 ctermbg=3 32 | hi high_complexity guifg=#ff2222 guibg=#ff2222 ctermfg=1 ctermbg=1 33 | sign define low_complexity text=XX texthl=low_complexity 34 | sign define medium_complexity text=XX texthl=medium_complexity 35 | sign define high_complexity text=XX texthl=high_complexity 36 | 37 | autocmd! BufReadPost,BufWritePost,FileReadPost,FileWritePost *.py call ShowComplexity() 38 | 39 | -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | if __name__ == '__main__': 4 | py_src = file('complexity.py').read() 5 | vim_src = file('base.vim').read() 6 | combined_src = vim_src % dict(python_source=py_src) 7 | file('complexity.vim', 'w').write(combined_src) 8 | 9 | -------------------------------------------------------------------------------- /complexity.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import os 4 | import compiler#{{{ 5 | from compiler.visitor import ASTVisitor 6 | 7 | def compute_code_complexity(code): 8 | return ModuleComplexity(code).compute_complexity() 9 | 10 | #}}} 11 | 12 | class ASTComplexity(ASTVisitor): 13 | def __init__(self, node): 14 | ASTVisitor.__init__(self) 15 | self.score = 1 16 | self._in_conditional = False 17 | self.results = ComplexityResults() 18 | self.process_root_node(node) 19 | 20 | def process_root_node(self, node): 21 | for child in node.getChildNodes(): 22 | compiler.walk(child, self, walker=self) 23 | 24 | def dispatch_children(self, node):#{{{ 25 | for child in node.getChildNodes(): 26 | self.dispatch(child) 27 | 28 | def visitFunction(self, node): 29 | score=ASTComplexity(node).score 30 | score = ComplexityScore(name=node.name, 31 | type_='function', 32 | score=score, 33 | start_line=node.lineno, 34 | end_line=self.highest_line_in_node(node)) 35 | self.results.add(score) 36 | 37 | def visitClass(self, node): 38 | complexity = ASTComplexity(node) 39 | self.results.add(ComplexityScore( 40 | name=node.name, 41 | type_='class', 42 | score=complexity.score, 43 | start_line=node.lineno, 44 | end_line=self.highest_line_in_node(node))) 45 | for score in complexity.results.ordered_by_line(): 46 | score.name = '%s.%s' % (node.name, score.name) 47 | self.results.add(score) 48 | 49 | def highest_line_in_node(self, node, highest=0): 50 | children = node.getChildNodes() 51 | if node.lineno > highest: 52 | highest = node.lineno 53 | child_lines = map(self.highest_line_in_node, 54 | node.getChildNodes()) 55 | lines = [node.lineno] + child_lines 56 | return max(lines) 57 | 58 | def visitIf(self, node): 59 | tests = self._tests_for_if(node) 60 | self.score += len(tests) 61 | self._in_conditional = True 62 | for test in tests: 63 | self.dispatch(test) 64 | self._in_conditional = False 65 | self.dispatch_children(node) 66 | 67 | def _tests_for_if(self, if_node): 68 | try: 69 | return [test for test, body in if_node.tests] 70 | except AttributeError: 71 | return [if_node.test] 72 | 73 | visitGenExprIf = visitListCompIf = visitIfExp = visitIf 74 | 75 | def __processDecisionPoint(self, node): 76 | self.score += 1 77 | self.dispatch_children(node) 78 | 79 | visitFor = visitGenExprFor \ 80 | = visitListCompFor \ 81 | = visitWhile = __processDecisionPoint 82 | 83 | def _visit_logical_operator(self, node): 84 | self.dispatch_children(node) 85 | if self._in_conditional: 86 | self.score += len(node.getChildren()) - 1 87 | 88 | visitAnd = _visit_logical_operator 89 | visitOr = _visit_logical_operator 90 | 91 | def visitTryExcept(self, node): 92 | self.dispatch_children(node) 93 | self.score += len(node.handlers) 94 | #}}} 95 | 96 | class ModuleComplexity: 97 | def __init__(self, code): 98 | self.code = code 99 | self.node = compiler.parse(code) 100 | 101 | def compute_complexity(self): 102 | complexity = ASTComplexity(self.node) 103 | self.add_module_results(complexity, self.code) 104 | return complexity 105 | 106 | def add_module_results(self, complexity, code_or_node): 107 | end_line = max(1, code_or_node.count('\n') + 1) 108 | complexity.results.add( 109 | ComplexityScore(name='', 110 | type_='module', 111 | score=complexity.score, 112 | start_line=1, 113 | end_line=end_line)) 114 | 115 | class ComplexityResults:#{{{ 116 | def __init__(self): 117 | self._scores = [] 118 | 119 | def add(self, score): 120 | self._scores.append(score) 121 | 122 | def ordered_by_line(self): 123 | OBJECT_SORT_PRIORITY = ['module', 'function', 'class'] 124 | def sort_key(score): 125 | return (score.start_line, 126 | OBJECT_SORT_PRIORITY.index(score.type_)) 127 | return sorted(self._scores, key=sort_key) 128 | 129 | def named(self, name): 130 | return [s for s in self._scores if s.name == name][0] 131 | 132 | 133 | class ComplexityScore: 134 | def __init__(self, name, type_, score, start_line, end_line): 135 | self.name = name 136 | self.type_ = type_ 137 | self.score = score 138 | self.start_line = start_line 139 | self.end_line = end_line 140 | 141 | def __repr__(self): 142 | return ( 143 | 'ComplexityScore(name=%s, score=%s, start_line=%s, end_line=%s)' 144 | % (repr(self.name), 145 | repr(self.score), 146 | repr(self.start_line), 147 | repr(self.end_line))) 148 | 149 | 150 | def complexity_name(complexity): 151 | if complexity > 14: 152 | return 'high_complexity' 153 | elif complexity > 7: 154 | return 'medium_complexity' 155 | else: 156 | return 'low_complexity' 157 | 158 | 159 | def show_complexity(): 160 | current_file = vim.current.buffer.name 161 | try: 162 | scores = compute_scores_for(current_file) 163 | except (IndentationError, SyntaxError): 164 | return 165 | 166 | old_complexities = get_old_complexities(current_file) 167 | new_complexities = compute_new_complexities(scores) 168 | line_changes = compute_line_changes(old_complexities, new_complexities) 169 | update_line_markers(line_changes) 170 | 171 | 172 | def compute_scores_for(filename=None, code=None): 173 | if filename: 174 | code = open(filename).read() 175 | scores = compute_code_complexity(code).results.ordered_by_line() 176 | return scores 177 | 178 | 179 | def get_old_complexities(current_file): 180 | lines = list_current_signs(current_file) 181 | 182 | old_complexities = {} 183 | for line in lines: 184 | if '=' not in line: 185 | continue 186 | 187 | tokens = line.split() 188 | variables = dict(token.split('=') for token in tokens) 189 | line = int(variables['line']) 190 | complexity = variables['name'] 191 | old_complexities[line] = complexity 192 | 193 | return old_complexities 194 | 195 | 196 | def list_current_signs(current_file): 197 | vim.command('redir => s:complexity_sign_list') 198 | vim.command('silent sign place file=%s' % current_file) 199 | vim.command('redir END') 200 | 201 | sign_list = vim.eval('s:complexity_sign_list') 202 | lines = [line.strip() for line in sign_list.split('\n')] 203 | return lines 204 | 205 | 206 | def compute_line_changes(cached_complexities, new_scores): 207 | changes = {} 208 | for line, complexity in new_scores.iteritems(): 209 | if complexity != cached_complexities.get(line, None): 210 | changes[line] = complexity 211 | 212 | return changes 213 | 214 | 215 | def compute_new_complexities(scores): 216 | new_scores = {} 217 | for score in scores: 218 | for line in range(score.start_line, score.end_line + 1): 219 | new_scores[line] = complexity_name(score.score) 220 | return new_scores 221 | 222 | 223 | def update_line_markers(line_changes): 224 | filename = vim.current.buffer.name 225 | for line, complexity in line_changes.iteritems(): 226 | vim.command(':sign unplace %i' % line) 227 | vim.command(':sign place %i line=%i name=%s file=%s' % 228 | (line, line, complexity, filename))#}}} 229 | 230 | 231 | def main(): 232 | if sys.stdin.isatty() and len(sys.argv) < 2: 233 | print "Missing filename" 234 | return 235 | 236 | if len(sys.argv) > 1: 237 | file_name = sys.argv[1] 238 | content = None 239 | else: 240 | file_name = None 241 | content = sys.stdin.read() 242 | 243 | try: 244 | for score in compute_scores_for(file_name, content): 245 | print score.start_line, score.end_line, score.score, score.type_ 246 | except: 247 | pass 248 | 249 | 250 | if __name__ == '__main__': 251 | if 'vim' not in globals(): 252 | main() 253 | -------------------------------------------------------------------------------- /grammar/count_nodes.py: -------------------------------------------------------------------------------- 1 | import compiler 2 | from compiler.visitor import ASTVisitor 3 | 4 | 5 | class NodeVisitor(ASTVisitor): 6 | def __init__(self, code, stats=None, description=None): 7 | ASTVisitor.__init__(self) 8 | ast = compiler.parse(code) 9 | self.node_types = set() 10 | self.visit_node(ast.node) 11 | #for child in ast.getChildNodes(): 12 | #compiler.walk(child, self, walker=self) 13 | all_types = set(line.strip() 14 | for line 15 | in file('python_ast_node_types.txt').readlines()) 16 | self.untouched_nodes = sorted(all_types - self.node_types) 17 | 18 | def visit_node(self, node): 19 | self.node_types.add(node.__class__.__name__) 20 | for child in node.getChildNodes(): 21 | self.visit_node(child) 22 | 23 | 24 | visitor = NodeVisitor(file('everything.py').read()) 25 | print 'Nodes not touched: %s' % visitor.untouched_nodes 26 | 27 | -------------------------------------------------------------------------------- /grammar/everything.py: -------------------------------------------------------------------------------- 1 | # This file aims to contain every Python syntax node. It's almost there. 2 | from __future__ import with_statement 3 | 4 | a + a 5 | a and b 6 | a.x = a 7 | assert a 8 | a = a 9 | a, a = a 10 | [a, a] = a 11 | 'a' 12 | a += a 13 | `a` 14 | a & a 15 | a | a 16 | a ^ a 17 | break 18 | a() 19 | class a: pass 20 | a < a 21 | continue 22 | @a 23 | def a(): pass 24 | a(a=a) 25 | {} 26 | a / a 27 | a[...] 28 | exec a 29 | a // a 30 | for a in a: pass 31 | from a import a 32 | (a for a in a if a) 33 | a.a 34 | global a 35 | if a: pass 36 | import a 37 | lambda: a 38 | a << a 39 | [] 40 | [a for a in a] 41 | [a for a in a if a] 42 | a % a 43 | a * a 44 | not a 45 | a or a 46 | a ** a 47 | raise a 48 | print a, 49 | print a 50 | return a 51 | a >> a 52 | a[a:a] 53 | a[a:a:a] 54 | a - a 55 | try: 56 | a 57 | except: 58 | a 59 | finally: 60 | a 61 | () 62 | +a 63 | -a 64 | while a: pass 65 | with a as a: 66 | pass 67 | yield a 68 | 69 | # Thankst to Josh Lee (jleedev) for pointing this missing node out. 70 | ~x 71 | 72 | -------------------------------------------------------------------------------- /grammar/python_ast_node_types.txt: -------------------------------------------------------------------------------- 1 | Add 2 | And 3 | AssAttr 4 | AssList 5 | AssName 6 | AssTuple 7 | Assert 8 | Assign 9 | AugAssign 10 | Backquote 11 | Bitand 12 | Bitor 13 | Bitxor 14 | Break 15 | CallFunc 16 | Class 17 | Compare 18 | Const 19 | Continue 20 | Decorators 21 | Dict 22 | Discard 23 | Div 24 | Ellipsis 25 | Expression 26 | Exec 27 | FloorDiv 28 | For 29 | From 30 | Function 31 | GenExpr 32 | GenExprFor 33 | GenExprIf 34 | GenExprInner 35 | Getattr 36 | Global 37 | If 38 | Import 39 | Invert 40 | Keyword 41 | Lambda 42 | LeftShift 43 | List 44 | ListComp 45 | ListCompFor 46 | ListCompIf 47 | Mod 48 | Module 49 | Mul 50 | Name 51 | Not 52 | Or 53 | Pass 54 | Power 55 | Print 56 | Printnl 57 | Raise 58 | Return 59 | RightShift 60 | Slice 61 | Sliceobj 62 | Stmt 63 | Sub 64 | Subscript 65 | TryExcept 66 | TryFinally 67 | Tuple 68 | UnaryAdd 69 | UnarySub 70 | While 71 | With 72 | Yield 73 | -------------------------------------------------------------------------------- /linum.el: -------------------------------------------------------------------------------- 1 | ;;; linum.el --- Display line numbers to the left of buffers 2 | 3 | ;; Copyright (C) 2007, 2008 Markus Triska 4 | 5 | ;; Author: Markus Triska 6 | ;; Keywords: convenience 7 | 8 | ;; This file is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation; either version 3, or (at your option) 11 | ;; any later version. 12 | 13 | ;; This file is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with GNU Emacs; see the file GPL.txt . If not, write to 20 | ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 21 | ;; Boston, MA 02110-1301, USA. 22 | 23 | ;;; Commentary: 24 | 25 | ;; Display line numbers for the current buffer. Copy linum.el to your 26 | ;; load-path and add to your .emacs: 27 | 28 | ;; (require 'linum) 29 | 30 | ;; Then toggle display of line numbers with M-x linum-mode. To enable 31 | ;; line numbering in all buffers, use M-x global-linum-mode. 32 | 33 | ;;; Code: 34 | 35 | (defconst linum-version "0.9wza") 36 | 37 | (defvar linum-overlays nil "Overlays used in this buffer.") 38 | (defvar linum-available nil "Overlays available for reuse.") 39 | (defvar linum-before-numbering-hook nil 40 | "Functions run in each buffer before line numbering starts.") 41 | 42 | (mapc #'make-variable-buffer-local '(linum-overlays linum-available)) 43 | 44 | (defgroup linum nil 45 | "Show line numbers to the left of buffers" 46 | :group 'convenience) 47 | 48 | ;;;###autoload 49 | (defcustom linum-format 'dynamic 50 | "Format used to display line numbers. Either a format string 51 | like \"%7d\", 'dynamic to adapt the width as needed, or a 52 | function that is called with a line number as its argument and 53 | should evaluate to a string to be shown on that line. See also 54 | `linum-before-numbering-hook'." 55 | :group 'linum 56 | :type 'sexp) 57 | 58 | (defface linum 59 | '((t :inherit (shadow default))) 60 | "Face for displaying line numbers in the display margin." 61 | :group 'linum) 62 | 63 | (defcustom linum-eager t 64 | "Whether line numbers should be updated after each command. 65 | The conservative setting `nil' might miss some buffer changes, 66 | and you have to scroll or press C-l to update the numbers." 67 | :group 'linum 68 | :type 'boolean) 69 | 70 | (defcustom linum-delay nil 71 | "Delay updates to give Emacs a chance for other changes." 72 | :group 'linum 73 | :type 'boolean) 74 | 75 | ;;;###autoload 76 | (define-minor-mode linum-mode 77 | "Toggle display of line numbers in the left marginal area." 78 | :lighter "" ; for desktop.el 79 | (if linum-mode 80 | (progn 81 | (if linum-eager 82 | (add-hook 'post-command-hook (if linum-delay 83 | 'linum-schedule 84 | 'linum-update-current) nil t) 85 | (add-hook 'after-change-functions 'linum-after-change nil t)) 86 | (add-hook 'window-scroll-functions 'linum-after-scroll nil t) 87 | ;; mistake in Emacs: window-size-change-functions cannot be local 88 | (add-hook 'window-size-change-functions 'linum-after-size) 89 | (add-hook 'change-major-mode-hook 'linum-delete-overlays nil t) 90 | (add-hook 'window-configuration-change-hook 91 | 'linum-after-config nil t) 92 | (linum-update-current)) 93 | (remove-hook 'post-command-hook 'linum-update-current t) 94 | (remove-hook 'post-command-hook 'linum-schedule t) 95 | (remove-hook 'window-size-change-functions 'linum-after-size) 96 | (remove-hook 'window-scroll-functions 'linum-after-scroll t) 97 | (remove-hook 'after-change-functions 'linum-after-change t) 98 | (remove-hook 'window-configuration-change-hook 'linum-after-config t) 99 | (remove-hook 'change-major-mode-hook 'linum-delete-overlays t) 100 | (linum-delete-overlays))) 101 | 102 | ;;;###autoload 103 | (define-globalized-minor-mode global-linum-mode linum-mode linum-on) 104 | 105 | (defun linum-on () 106 | (unless (minibufferp) 107 | (linum-mode 1))) 108 | 109 | (defun linum-delete-overlays () 110 | "Delete all overlays displaying line numbers for this buffer." 111 | (mapc #'delete-overlay linum-overlays) 112 | (setq linum-overlays nil) 113 | (dolist (w (get-buffer-window-list (current-buffer) nil t)) 114 | (set-window-margins w 0))) 115 | 116 | (defun linum-update-current () 117 | "Update line numbers for the current buffer." 118 | (linum-update (current-buffer))) 119 | 120 | (defun linum-update (buffer) 121 | "Update line numbers for all windows displaying BUFFER." 122 | (with-current-buffer buffer 123 | (when linum-mode 124 | (setq linum-available linum-overlays) 125 | (setq linum-overlays nil) 126 | (save-excursion 127 | (mapc #'linum-update-window 128 | (get-buffer-window-list buffer nil 'visible))) 129 | (mapc #'delete-overlay linum-available) 130 | (setq linum-available nil)))) 131 | 132 | (defun linum-update-window (win) 133 | "Update line numbers for the portion visible in window WIN." 134 | (goto-char (window-start win)) 135 | (let ((line (line-number-at-pos)) 136 | (limit (window-end win t)) 137 | (fmt (cond ((stringp linum-format) linum-format) 138 | ((eq linum-format 'dynamic) 139 | (let ((w (length (number-to-string 140 | (count-lines (point-min) (point-max)))))) 141 | (concat "%" (number-to-string w) "d"))))) 142 | (width 0)) 143 | (run-hooks 'linum-before-numbering-hook) 144 | ;; Create an overlay (or reuse an existing one) for each 145 | ;; line visible in this window, if necessary. 146 | (while (and (not (eobp)) (<= (point) limit)) 147 | (let* ((str (if fmt 148 | (propertize (format fmt line) 'face 'linum) 149 | (funcall linum-format line))) 150 | (visited (catch 'visited 151 | (dolist (o (overlays-in (point) (point))) 152 | (when (string= (overlay-get o 'linum-str) str) 153 | (unless (memq o linum-overlays) 154 | (push o linum-overlays)) 155 | (setq linum-available (delete o linum-available)) 156 | (throw 'visited t)))))) 157 | (setq width (max width (length str))) 158 | (unless visited 159 | (let ((ov (if (null linum-available) 160 | (make-overlay (point) (point)) 161 | (move-overlay (pop linum-available) (point) (point))))) 162 | (push ov linum-overlays) 163 | (overlay-put ov 'before-string 164 | (propertize " " 'display `((margin left-margin) ,str))) 165 | (overlay-put ov 'linum-str str)))) 166 | (forward-line) 167 | (setq line (1+ line))) 168 | (set-window-margins win width))) 169 | 170 | (defun linum-after-change (beg end len) 171 | ;; update overlays on deletions, and after newlines are inserted 172 | (when (or (= beg end) 173 | (= end (point-max)) 174 | ;; TODO: use string-match-p with CVS or new release 175 | (string-match "\n" (buffer-substring-no-properties beg end))) 176 | (linum-update-current))) 177 | 178 | (defun linum-after-scroll (win start) 179 | (linum-update (window-buffer win))) 180 | 181 | (defun linum-after-size (frame) 182 | (linum-after-config)) 183 | 184 | (defun linum-schedule () 185 | ;; schedule an update; the delay gives Emacs a chance for display changes 186 | (run-with-idle-timer 0 nil #'linum-update-current)) 187 | 188 | (defun linum-after-config () 189 | (walk-windows (lambda (w) (linum-update (window-buffer w))) nil 'visible)) 190 | 191 | (provide 'linum) 192 | ;;; linum.el ends here 193 | -------------------------------------------------------------------------------- /pycomplexity.el: -------------------------------------------------------------------------------- 1 | ;;; pycomplexity.el --- Display python code complexity to the left of buffers 2 | 3 | ;; Copyright (C) 2009 Ignas Mikalajunas 4 | 5 | ;; Author: Ignas Mikalajunas 6 | ;; Keywords: convenience 7 | 8 | ;; This file is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation; either version 3, or (at your option) 11 | ;; any later version. 12 | 13 | ;; This file is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with GNU Emacs; see the file GPL.txt . If not, write to 20 | ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 21 | ;; Boston, MA 02110-1301, USA. 22 | 23 | ;;; Commentary: 24 | 25 | ;; Display complexity information for the current buffer. 26 | 27 | ;; Add to your .emacs: 28 | 29 | ;; (add-to-list 'load-path "~/.site-lisp/pycomplexity/") 30 | 31 | ;; (require 'linum) 32 | ;; (require 'pycomplexity) 33 | ;; (add-hook 'python-mode-hook 34 | ;; (function (lambda () 35 | ;; (pycomplexity-mode) 36 | ;; (linum-mode)))) 37 | 38 | ;;; Code: 39 | 40 | (defconst pycomplexity-version "0.1") 41 | 42 | 43 | (defvar complexity-last-change 0 "Time last change to some python buffer happened.") 44 | (defvar complexity-data nil "Calcuated code complexity information for this buffer.") 45 | (make-variable-buffer-local 'complexity-data) 46 | 47 | (defgroup pycomplexity nil 48 | "Show complexity information to the left of buffers" 49 | :group 'convenience) 50 | 51 | (defface pycomplexity-complexity-low 52 | '((t (:background "green" 53 | :foreground "green"))) 54 | "Face that marks simple code " 55 | :group 'pycomplexity) 56 | 57 | (defface pycomplexity-complexity-normal 58 | '((t (:background "yellow" 59 | :foreground "yellow"))) 60 | "Face that marks normal code " 61 | :group 'pycomplexity) 62 | 63 | (defface pycomplexity-complexity-high 64 | '((t (:background "red" 65 | :foreground "red"))) 66 | "Face that marks complex code " 67 | :group 'pycomplexity) 68 | 69 | (defcustom pycomplexity-delay 5 70 | "Update coverage information once in this many seconds." 71 | :group 'pycomplexity 72 | :type 'int) 73 | 74 | (defcustom pycomplexity-python "python" 75 | "Python interpreter used to run the complexity calculation script." 76 | :group 'pycomplexity 77 | :type 'string) 78 | 79 | (defcustom pycomplexity-script 80 | (expand-file-name "complexity.py" 81 | (file-name-directory (or load-file-name buffer-file-name))) 82 | "Pycomplexity python script." 83 | :group 'pycomplexity 84 | :type 'string) 85 | 86 | ;;;###autoload 87 | (define-minor-mode pycomplexity-mode 88 | "Toggle display complexity of the python code you are editing." 89 | :lighter "" ; for desktop.el 90 | (if pycomplexity-mode 91 | (progn 92 | (add-hook 'after-change-functions 'pycomplexity-on-change nil t) 93 | (add-hook 'after-save-hook 'pycomplexity-on-change-force nil t) 94 | (setf linum-format 'pycomplexity-line-format) 95 | (pycomplexity-on-change-force)) 96 | (setf linum-format 'dynamic) 97 | (remove-hook 'after-change-functions 'pycomplexity-on-change t))) 98 | 99 | (defun pycomplexity-get-complexity (line data) 100 | (multiple-value-bind (face str complexity) 101 | (loop for info in data 102 | for from = (first info) 103 | for to = (second info) 104 | for complexity = (third info) 105 | when (and (>= line from) 106 | (<= line to)) 107 | return (cond ((> complexity 14) (values 'pycomplexity-complexity-high "h" complexity)) 108 | ((> complexity 7) (values 'pycomplexity-complexity-normal "n" complexity)) 109 | (t (values 'pycomplexity-complexity-low "l" complexity))) 110 | when (< line from) 111 | return (values 'default " " 0)) 112 | (if face (values face str complexity) 113 | (values 'default " " 0)))) 114 | 115 | (defun pycomplexity-line-format (line) 116 | (multiple-value-bind (face str complexity) 117 | (pycomplexity-get-complexity line complexity-data) 118 | (propertize str 'face face 119 | 'help-echo (format "Complexity of this function is %d" complexity)))) 120 | 121 | 122 | (defun pycomplexity-make-buffer-copy () 123 | (let* ((source-file-name buffer-file-name) 124 | (file-name (flymake-create-temp-inplace source-file-name "complexity"))) 125 | (make-directory (file-name-directory file-name) 1) 126 | (write-region nil nil file-name nil 566) 127 | file-name)) 128 | 129 | (defun pycomplexity-get-raw-complexity-data (file-name) 130 | (shell-command-to-string (format "%s %s %s" 131 | pycomplexity-python 132 | pycomplexity-script 133 | file-name))) 134 | 135 | (defun pycomplexity-on-change-force (&optional beg end len) 136 | (pycomplexity-on-change beg end len t)) 137 | 138 | (defun pycomplexity-on-change (&optional beg end len force) 139 | (let ((since-last-change (- (float-time) complexity-last-change))) 140 | (when (or (> since-last-change pycomplexity-delay) force) 141 | (setf complexity-last-change (float-time)) 142 | (let* ((temp-source-file-name (pycomplexity-make-buffer-copy)) 143 | (result (pycomplexity-get-raw-complexity-data temp-source-file-name)) 144 | (data (loop 145 | for line in (save-match-data (split-string result "[\n\r]+")) 146 | for parsed-line = (loop for item in (split-string line) 147 | when item collect (read item)) 148 | when (and parsed-line 149 | (equal (car (last parsed-line)) 'function)) 150 | collect (subseq parsed-line 0 3)))) 151 | (when data (setf complexity-data data)) 152 | (delete-file temp-source-file-name))))) 153 | 154 | (provide 'pycomplexity) 155 | ;;; pycomplexity.el ends here 156 | -------------------------------------------------------------------------------- /pycomplexity.vim/doc/complexity.txt: -------------------------------------------------------------------------------- 1 | *complexity.txt* Adds cyclomatic complexity annotations to your source code. 2 | 3 | ============================================================================== 4 | CONTENTS *Complexity-contents* 5 | 6 | 1. Intro ............................ |ComplexityIntro| 7 | 2. Usage ............................ |ComplexityUsage| 8 | 3. License ........................ |ComplexityLicense| 9 | 4. Credits ........................ |ComplexityCredits| 10 | 11 | ============================================================================== 12 | 1. Intro *ComplexityIntro* 13 | 14 | A Vim plugin to annotate your source code with Vim's column signs with color 15 | codes. 16 | 17 | Three levels of complexity are reported: low, normal and high. Each one of them 18 | represents a color in the cyclomatic complexity level: 19 | 20 | * low = Green 21 | * normal = Orange 22 | * high = Red 23 | 24 | ============================================================================== 25 | 2. Usage *ComplexityUsage* 26 | 27 | This plugin provides a single command:: 28 | 29 | :Complexity 30 | 31 | It toggles on or off the signs displayed on the current buffer being edited. It 32 | also has the option to include a variable in your Vim configuration file to have 33 | Complexity run always. The default is to be OFF. You can assign this variable 34 | like: 35 | 36 | let g:complexity_always_on = 1 37 | 38 | 39 | ============================================================================== 40 | 3. License *ComplexityLicense* 41 | 42 | TODO 43 | 44 | ============================================================================== 45 | 2. Credits *ComplexityCredits* 46 | 47 | TODO 48 | -------------------------------------------------------------------------------- /pycomplexity.vim/ftplugin/python/complexity.vim: -------------------------------------------------------------------------------- 1 | " complexity.vim 2 | " Gary Bernhardt (http://blog.extracheese.org) 3 | " 4 | " This will add cyclomatic complexity annotations to your source code. It is 5 | " no longer wrong (as previous versions were!) 6 | 7 | if !has('signs') 8 | finish 9 | endif 10 | if !has('python') 11 | finish 12 | endif 13 | 14 | if exists("g:loaded_complexity") || &cp 15 | finish 16 | endif 17 | 18 | function! s:ClearSigns() 19 | sign unplace * 20 | endfunction 21 | 22 | function! s:ToggleComplexity() 23 | if exists("g:complexity_is_displaying") && g:complexity_is_displaying 24 | call s:ClearSigns() 25 | let g:complexity_is_displaying = 0 26 | else 27 | call s:ShowComplexity() 28 | endif 29 | endfunction 30 | 31 | python << endpython 32 | import vim 33 | #!/usr/bin/env python 34 | import sys 35 | import os 36 | import compiler#{{{ 37 | from compiler.visitor import ASTVisitor 38 | 39 | def compute_code_complexity(code): 40 | return ModuleComplexity(code).compute_complexity() 41 | 42 | #}}} 43 | 44 | class ASTComplexity(ASTVisitor): 45 | def __init__(self, node): 46 | ASTVisitor.__init__(self) 47 | self.score = 1 48 | self._in_conditional = False 49 | self.results = ComplexityResults() 50 | self.process_root_node(node) 51 | 52 | def process_root_node(self, node): 53 | for child in node.getChildNodes(): 54 | compiler.walk(child, self, walker=self) 55 | 56 | def dispatch_children(self, node):#{{{ 57 | for child in node.getChildNodes(): 58 | self.dispatch(child) 59 | 60 | def visitFunction(self, node): 61 | score=ASTComplexity(node).score 62 | score = ComplexityScore(name=node.name, 63 | type_='function', 64 | score=score, 65 | start_line=node.lineno, 66 | end_line=self.highest_line_in_node(node)) 67 | self.results.add(score) 68 | 69 | def visitClass(self, node): 70 | complexity = ASTComplexity(node) 71 | self.results.add(ComplexityScore( 72 | name=node.name, 73 | type_='class', 74 | score=complexity.score, 75 | start_line=node.lineno, 76 | end_line=self.highest_line_in_node(node))) 77 | for score in complexity.results.ordered_by_line(): 78 | score.name = '%s.%s' % (node.name, score.name) 79 | self.results.add(score) 80 | 81 | def highest_line_in_node(self, node, highest=0): 82 | children = node.getChildNodes() 83 | if node.lineno > highest: 84 | highest = node.lineno 85 | child_lines = map(self.highest_line_in_node, 86 | node.getChildNodes()) 87 | lines = [node.lineno] + child_lines 88 | return max(lines) 89 | 90 | def visitIf(self, node): 91 | tests = self._tests_for_if(node) 92 | self.score += len(tests) 93 | self._in_conditional = True 94 | for test in tests: 95 | self.dispatch(test) 96 | self._in_conditional = False 97 | self.dispatch_children(node) 98 | 99 | def _tests_for_if(self, if_node): 100 | try: 101 | return [test for test, body in if_node.tests] 102 | except AttributeError: 103 | return [if_node.test] 104 | 105 | visitGenExprIf = visitListCompIf = visitIfExp = visitIf 106 | 107 | def __processDecisionPoint(self, node): 108 | self.score += 1 109 | self.dispatch_children(node) 110 | 111 | visitFor = visitGenExprFor \ 112 | = visitListCompFor \ 113 | = visitWhile = __processDecisionPoint 114 | 115 | def _visit_logical_operator(self, node): 116 | self.dispatch_children(node) 117 | if self._in_conditional: 118 | self.score += len(node.getChildren()) - 1 119 | 120 | visitAnd = _visit_logical_operator 121 | visitOr = _visit_logical_operator 122 | 123 | def visitTryExcept(self, node): 124 | self.dispatch_children(node) 125 | self.score += len(node.handlers) 126 | #}}} 127 | 128 | class ModuleComplexity: 129 | def __init__(self, code): 130 | self.code = code 131 | self.node = compiler.parse(code) 132 | 133 | def compute_complexity(self): 134 | complexity = ASTComplexity(self.node) 135 | self.add_module_results(complexity, self.code) 136 | return complexity 137 | 138 | def add_module_results(self, complexity, code_or_node): 139 | end_line = max(1, code_or_node.count('\n') + 1) 140 | complexity.results.add( 141 | ComplexityScore(name='', 142 | type_='module', 143 | score=complexity.score, 144 | start_line=1, 145 | end_line=end_line)) 146 | 147 | class ComplexityResults:#{{{ 148 | def __init__(self): 149 | self._scores = [] 150 | 151 | def add(self, score): 152 | self._scores.append(score) 153 | 154 | def ordered_by_line(self): 155 | OBJECT_SORT_PRIORITY = ['module', 'function', 'class'] 156 | def sort_key(score): 157 | return (score.start_line, 158 | OBJECT_SORT_PRIORITY.index(score.type_)) 159 | return sorted(self._scores, key=sort_key) 160 | 161 | def named(self, name): 162 | return [s for s in self._scores if s.name == name][0] 163 | 164 | 165 | class ComplexityScore: 166 | def __init__(self, name, type_, score, start_line, end_line): 167 | self.name = name 168 | self.type_ = type_ 169 | self.score = score 170 | self.start_line = start_line 171 | self.end_line = end_line 172 | 173 | def __repr__(self): 174 | return ( 175 | 'ComplexityScore(name=%s, score=%s, start_line=%s, end_line=%s)' 176 | % (repr(self.name), 177 | repr(self.score), 178 | repr(self.start_line), 179 | repr(self.end_line))) 180 | 181 | 182 | def complexity_name(complexity): 183 | if complexity > 14: 184 | return 'high_complexity' 185 | elif complexity > 7: 186 | return 'medium_complexity' 187 | else: 188 | return 'low_complexity' 189 | 190 | 191 | def show_complexity(): 192 | current_file = vim.current.buffer.name 193 | try: 194 | scores = compute_scores_for(current_file) 195 | except (IndentationError, SyntaxError): 196 | return 197 | 198 | old_complexities = get_old_complexities(current_file) 199 | new_complexities = compute_new_complexities(scores) 200 | line_changes = compute_line_changes(old_complexities, new_complexities) 201 | update_line_markers(line_changes) 202 | 203 | 204 | def compute_scores_for(filename=None, code=None): 205 | if filename: 206 | code = open(filename).read() 207 | scores = compute_code_complexity(code).results.ordered_by_line() 208 | return scores 209 | 210 | 211 | def get_old_complexities(current_file): 212 | lines = list_current_signs(current_file) 213 | 214 | old_complexities = {} 215 | for line in lines: 216 | if '=' not in line: 217 | continue 218 | 219 | tokens = line.split() 220 | variables = dict(token.split('=') for token in tokens) 221 | line = int(variables['line']) 222 | complexity = variables['name'] 223 | old_complexities[line] = complexity 224 | 225 | return old_complexities 226 | 227 | 228 | def list_current_signs(current_file): 229 | vim.command('redir => s:complexity_sign_list') 230 | vim.command('silent sign place file=%s' % current_file) 231 | vim.command('redir END') 232 | 233 | sign_list = vim.eval('s:complexity_sign_list') 234 | lines = [line.strip() for line in sign_list.split('\n')] 235 | return lines 236 | 237 | 238 | def compute_line_changes(cached_complexities, new_scores): 239 | changes = {} 240 | for line, complexity in new_scores.iteritems(): 241 | if complexity != cached_complexities.get(line, None): 242 | changes[line] = complexity 243 | 244 | return changes 245 | 246 | 247 | def compute_new_complexities(scores): 248 | new_scores = {} 249 | for score in scores: 250 | for line in range(score.start_line, score.end_line + 1): 251 | new_scores[line] = complexity_name(score.score) 252 | return new_scores 253 | 254 | 255 | def update_line_markers(line_changes): 256 | filename = vim.current.buffer.name 257 | for line, complexity in line_changes.iteritems(): 258 | vim.command(':sign unplace %i' % line) 259 | vim.command(':sign place %i line=%i name=%s file=%s' % 260 | (line, line, complexity, filename))#}}} 261 | 262 | 263 | def main(): 264 | if sys.stdin.isatty() and len(sys.argv) < 2: 265 | print "Missing filename" 266 | return 267 | 268 | if len(sys.argv) > 1: 269 | file_name = sys.argv[1] 270 | content = None 271 | else: 272 | file_name = None 273 | content = sys.stdin.read() 274 | 275 | try: 276 | for score in compute_scores_for(file_name, content): 277 | print score.start_line, score.end_line, score.score, score.type_ 278 | except: 279 | pass 280 | 281 | 282 | if __name__ == '__main__': 283 | if 'vim' not in globals(): 284 | main() 285 | 286 | endpython 287 | 288 | function! s:ShowComplexity() 289 | python << END 290 | show_complexity() 291 | END 292 | let g:complexity_is_displaying = 1 293 | " no idea why it is needed to update colors each time 294 | " to actually see the colors 295 | hi low_complexity guifg=#004400 guibg=#004400 ctermfg=2 ctermbg=2 296 | hi medium_complexity guifg=#bbbb00 guibg=#bbbb00 ctermfg=3 ctermbg=3 297 | hi high_complexity guifg=#ff2222 guibg=#ff2222 ctermfg=1 ctermbg=1 298 | endfunction 299 | 300 | hi SignColumn guifg=fg guibg=bg 301 | hi low_complexity guifg=#004400 guibg=#004400 ctermfg=2 ctermbg=2 302 | hi medium_complexity guifg=#bbbb00 guibg=#bbbb00 ctermfg=3 ctermbg=3 303 | hi high_complexity guifg=#ff2222 guibg=#ff2222 ctermfg=1 ctermbg=1 304 | sign define low_complexity text=XX texthl=low_complexity 305 | sign define medium_complexity text=XX texthl=medium_complexity 306 | sign define high_complexity text=XX texthl=high_complexity 307 | 308 | if exists("g:complexity_always_on") && g:complexity_always_on 309 | autocmd! BufReadPost,BufWritePost,FileReadPost,FileWritePost *.py call s:ShowComplexity() 310 | call s:ShowComplexity() 311 | endif 312 | 313 | command! Complexity call s:ToggleComplexity() 314 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | import nose 6 | 7 | 8 | if __name__ == '__main__': 9 | nose_args = sys.argv + [r'-m', 10 | r'((?:^|[b_.-])(:?[Tt]est|When|describe|should|it))', 11 | r'--with-doctest', 12 | r'--doctest-extension='] 13 | nose.run(argv=nose_args) 14 | 15 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/manual/test_function_on_first_line.py: -------------------------------------------------------------------------------- 1 | def RED(): 2 | a if a else a 3 | a if a else a 4 | a if a else a 5 | a if a else a 6 | a if a else a 7 | a if a else a 8 | a if a else a 9 | a if a else a 10 | a if a else a 11 | a if a else a 12 | a if a else a 13 | a if a else a 14 | a if a else a 15 | a if a else a 16 | a if a else a 17 | a if a else a 18 | a if a else a 19 | a if a else a 20 | a if a else a 21 | a if a else a 22 | a if a else a 23 | a if a else a 24 | a if a else a 25 | a if a else a 26 | a if a else a 27 | a if a else a 28 | a if a else a 29 | 30 | -------------------------------------------------------------------------------- /tests/manual/test_functions.py: -------------------------------------------------------------------------------- 1 | def one(): 2 | pass 3 | 4 | def two(): 5 | a if a else a 6 | 7 | def three(): 8 | a if a else a if a else a 9 | 10 | def four(): 11 | a if a else a if a else a if a else a 12 | 13 | def five(): 14 | a if a else a if a else a if a else a if a else a 15 | 16 | def six(): 17 | a if a else a if a else a if a else a if a else a if a else a 18 | 19 | def seven(): 20 | a if a else a if a else a if a else a if a else a if a else a if a else a 21 | 22 | def eight(): 23 | a if a else a if a else a if a else a if a else a if a else a if a else a if a else a 24 | 25 | def nine(): 26 | a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a 27 | 28 | def ten(): 29 | a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a 30 | 31 | def eleven(): 32 | a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a 33 | 34 | def twelve(): 35 | a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a 36 | 37 | def thirteen(): 38 | a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a 39 | 40 | def fourteen(): 41 | a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a 42 | 43 | def fifteen(): 44 | a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a 45 | 46 | -------------------------------------------------------------------------------- /tests/manual/test_high_complexity_module.py: -------------------------------------------------------------------------------- 1 | a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a if a else a 2 | 3 | -------------------------------------------------------------------------------- /tests/manual/test_update_function.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | x if x else x if x else x if x else x if x else x if x else x if x else x 3 | # Delete this line, then save. The function should go from yellow to 4 | # green. Then undo and save. The *whole thing* should go back to yellow. 5 | x if x else x 6 | pass 7 | pass 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /tests/test_comprehensions.py: -------------------------------------------------------------------------------- 1 | from tests.utils import complexity 2 | 3 | 4 | class describe_list_comprehensions: 5 | def test_list_comprehension(self): 6 | assert complexity("[x for x in y]").score == 2 7 | 8 | def test_list_comprehension_with_inline_conditional(self): 9 | assert complexity("[x if y else z for x in x]").score == 3 10 | 11 | def test_nested_list_comprehensions(self): 12 | assert complexity("[x for x in [y for y in z]]").score == 3 13 | 14 | def test_list_comprehensions_with_multiple_fors(self): 15 | assert complexity("[x for x in y for y in z]").score == 3 16 | 17 | def test_list_comprehension_with_conditional(self): 18 | assert complexity("[x for x in y if x]").score == 3 19 | 20 | def test_list_comprehension_with_multiple_conditionals(self): 21 | assert complexity("[x for x in y if x and y]").score == 4 22 | 23 | def test_list_comprehension_with_multiple_conditionals_and_fors(self): 24 | assert complexity( 25 | """ 26 | [x for x in x 27 | for y in y 28 | if x and y] 29 | """).score == 5 30 | 31 | 32 | class describe_generator_expression: 33 | def test_generator_expression(self): 34 | assert complexity("(x for x in y)").score == 2 35 | 36 | def test_with_inline_conditional(self): 37 | assert complexity("(x if y else z for x in x)").score == 3 38 | 39 | def test_nested(self): 40 | assert complexity("(x for x in (y for y in z))").score == 3 41 | 42 | def test_with_multiple_fors(self): 43 | assert complexity("(x for x in y for y in z)").score == 3 44 | 45 | def test_with_conditional(self): 46 | assert complexity("(x for x in y if x)").score == 3 47 | 48 | def test_with_multiple_conditionals(self): 49 | assert complexity("(x for x in y if x and y)").score == 4 50 | 51 | def test_with_multiple_conditionals_and_fors(self): 52 | assert complexity( 53 | """ 54 | (x for x in x 55 | for y in y 56 | if x and y) 57 | """).score == 5 58 | 59 | -------------------------------------------------------------------------------- /tests/test_conditionals.py: -------------------------------------------------------------------------------- 1 | from tests.utils import complexity 2 | 3 | 4 | class describe_conditionals: 5 | def test_simple_branch(self): 6 | assert complexity( 7 | """ 8 | if x: 1 9 | # implicit else 10 | """).score == 2 11 | 12 | def test_branch_with_else(self): 13 | assert complexity( 14 | """ 15 | if x: 1 16 | else: 2 17 | """).score == 2 18 | 19 | def test_branch_with_else_if(self): 20 | assert complexity( 21 | """ 22 | if x: 1 23 | elif y: 2 24 | # implicit else 25 | """).score == 3 26 | 27 | def test_branch_with_else_if_and_else(self): 28 | assert complexity( 29 | """ 30 | if x: 1 31 | elif y: 2 32 | else: 3 33 | """).score == 3 34 | 35 | def test_child_nodes_of_ifs(self): 36 | assert complexity( 37 | """ 38 | if x: 39 | if y: 1 40 | else: 2 41 | else: 3 42 | """).score == 3 43 | 44 | def test_child_nodes_of_elses(self): 45 | assert complexity( 46 | """ 47 | if x: 1 48 | else: 49 | if y: 1 50 | # implicit else 51 | """).score == 3 52 | 53 | def test_compound_conditionals(self): 54 | assert complexity( 55 | """ 56 | if x or y: 1 57 | """).score == 3 58 | 59 | def test_chained_compound_conditionals(self): 60 | assert complexity( 61 | """ 62 | if a or b or c and d and e: 1 63 | """).score == 6 64 | 65 | def test_nested_compound_conditionals(self): 66 | assert complexity( 67 | """ 68 | if x or (y or z): 1 69 | """).score == 4 70 | 71 | def test_logical_operator_inside_conditional_but_outside_test(self): 72 | assert complexity( 73 | """ 74 | if x: 75 | x and y 76 | """).score == 2 77 | 78 | 79 | class describe_inline_conditionals: 80 | def test_inline_conditionals(self): 81 | assert complexity("b if c else d").score == 2 82 | 83 | def test_nested_inline_conditionals(self): 84 | assert complexity( 85 | """ 86 | (b 87 | if c 88 | else (d 89 | if e 90 | else f)) 91 | """).score == 3 92 | 93 | def test_logical_operator_in_inline_conditional(self): 94 | assert complexity("a if b and c else d").score == 3 95 | 96 | -------------------------------------------------------------------------------- /tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | from tests.utils import complexity 2 | 3 | 4 | class describe_exception_handling: 5 | def test_try(self): 6 | assert complexity( 7 | """ 8 | try: 1 9 | except: 2 10 | """).score == 2 11 | 12 | def test_try_with_multiple_excepts(self): 13 | assert complexity( 14 | """ 15 | try: 1 16 | except A: 2 17 | except B: 3 18 | except C: 4 19 | """).score == 4 20 | 21 | def test_try_with_multiple_exception_types_in_one_except(self): 22 | assert complexity( 23 | """ 24 | try: 1 25 | except (A, B): 2 26 | """).score == 2 27 | 28 | def test_try_with_child_nodes(self): 29 | assert complexity( 30 | """ 31 | try: 32 | if x: 1 33 | else: 2 34 | except: 2 35 | """).score == 3 36 | 37 | def test_try_with_finally(self): 38 | assert complexity( 39 | """ 40 | try: 1 41 | except: 2 42 | finally: 3 43 | """).score == 2 44 | 45 | def test_try_with_else(self): 46 | assert complexity( 47 | """ 48 | try: 1 49 | except: 2 50 | else: 3 51 | """).score == 2 52 | 53 | def test_try_with_finally_and_child_nodes(self): 54 | # Try/finally/else/except are all deceiving. The try and finally don't 55 | # add any paths because they both always happen. An except adds one 56 | # (it can either happen or not), but an else doesn't (it's equivalent 57 | # to adding the code after the line in the try: that threw the 58 | # exception, so it doesn't add a path). 59 | assert complexity( 60 | """ 61 | try: 62 | if a: 1 63 | else: 2 64 | except: 65 | if a: 1 66 | else: 2 67 | else: 68 | if a: 1 69 | else: 2 70 | finally: 71 | if a: 1 72 | else: 2 73 | """).score == 6 74 | 75 | -------------------------------------------------------------------------------- /tests/test_integration.py: -------------------------------------------------------------------------------- 1 | from tests.utils import complexity 2 | 3 | 4 | class describe_integration: 5 | def test_multiple_ifs_in_a_for_loop(self): 6 | assert complexity( 7 | """ 8 | for x in y: 9 | if x: pass 10 | # implicit else 11 | if y: pass 12 | # implicit else 13 | """).score == 4 14 | 15 | def test_lambdas_in_a_function(self): 16 | assert complexity( 17 | """ 18 | def foo(): 19 | x = lambda: x if x else x 20 | y if y else y 21 | """).results.named('foo').score == 3 22 | 23 | def test_a_big_hairy_mess(self): 24 | assert complexity( 25 | """ 26 | while True: #1 27 | if x and y or (z and w): #4 28 | try: 29 | x or y 30 | raise x 31 | except: #1 32 | 5 33 | finally: 34 | break 35 | else: 36 | [x for x in [x and y for x in y if z or w]] #5 37 | try: 38 | return 39 | except A: #1 40 | return 41 | except B: #1 42 | (y for y in z) #1 43 | finally: 44 | raise (x for x in z) #1 45 | """).score == 15 46 | 47 | def test_module_stat_comes_before_function_stat(self): 48 | stats = complexity("def foo(): pass\npass").results 49 | stat_names = [stat.name for stat in stats.ordered_by_line()] 50 | assert stat_names == ['', 'foo'] 51 | 52 | def test_class_stat_comes_before_module_stat(self): 53 | stats = complexity("class Foo: pass\npass").results 54 | stat_names = [stat.name for stat in stats.ordered_by_line()] 55 | assert stat_names == ['', 'Foo'] 56 | 57 | -------------------------------------------------------------------------------- /tests/test_loops.py: -------------------------------------------------------------------------------- 1 | from tests.utils import complexity 2 | 3 | 4 | class describe_for_loops: 5 | def test_for_loops(self): 6 | assert complexity( 7 | """ 8 | for x in y: 1 9 | # implicit else 10 | """).score == 2 11 | 12 | def test_else_clauses_on_for_loops(self): 13 | assert complexity( 14 | """ 15 | for x in y: 1 16 | else: 2 17 | """).score == 2 18 | 19 | def test_child_nodes_of_for_loops(self): 20 | assert complexity( 21 | """ 22 | for x in y: 23 | if x: 1 24 | else: 2 25 | # implicit else 26 | """).score == 3 27 | 28 | def test_child_nodes_in_for_loop_else_clauses(self): 29 | assert complexity( 30 | """ 31 | for x in y: 1 32 | else: 33 | if x: 2 34 | else: 3 35 | """).score == 3 36 | 37 | def test_break_statements_in_for_loops(self): 38 | # This seems like it should be more complex than an if with "pass"es, 39 | # but it's not. The break just reroutes the "if" path: instead of 40 | # going to the end of the loop and back up top, it goes straight back 41 | # up. 42 | assert complexity( 43 | """ 44 | for x in y: 45 | if x: 46 | break 47 | """).score == 3 48 | 49 | def test_break_statements_in_for_loops_with_else_clauses(self): 50 | # A "break" in a for loop skips the "else". My intuitive 51 | # interpretation is that this should increase CC by one. However, it's 52 | # basically a GOTO, and GOTOs don't increase the CC. Drawing the graph 53 | # out seems to confirm that a "break" with an "else" does not add a 54 | # path. 55 | assert complexity( 56 | """ 57 | for x in y: 58 | if x: 59 | break 60 | else: 61 | pass 62 | """).score == 3 63 | 64 | def test_continue_statement_in_for_loop(self): 65 | assert complexity( 66 | """ 67 | for x in y: 68 | if x: 69 | continue 70 | """).score == 3 71 | 72 | 73 | # These are basically identical to the "for" loop tests, but abstracting them 74 | # to remove the duplication would be just as long and more confusing. 75 | class describe_while_loops: 76 | def test_while_loops(self): 77 | assert complexity( 78 | """ 79 | while x: 1 80 | # implicit else 81 | """).score == 2 82 | 83 | def test_else_clauses_on_while_loops(self): 84 | assert complexity( 85 | """ 86 | while x: 1 87 | else: 2 88 | """).score == 2 89 | 90 | def test_child_nodes_of_while_loops(self): 91 | assert complexity( 92 | """ 93 | while x: 94 | if x: 1 95 | else: 2 96 | # implicit else 97 | """).score == 3 98 | 99 | def test_child_nodes_in_while_loop_else_clauses(self): 100 | assert complexity( 101 | """ 102 | while x: 1 103 | else: 104 | if x: 2 105 | else: 3 106 | """).score == 3 107 | 108 | def test_break_statements_in_while_loops(self): 109 | # See discussion for "for" loops above. 110 | assert complexity( 111 | """ 112 | while x: 113 | if x: 114 | break 115 | """).score == 3 116 | 117 | def test_break_statements_in_while_loops_with_else_clauses(self): 118 | # See discussion for for loops above. 119 | assert complexity( 120 | """ 121 | while x: 122 | if x: 123 | break 124 | else: 125 | pass 126 | """).score == 3 127 | 128 | def test_continue_statement_in_while_loop(self): 129 | assert complexity( 130 | """ 131 | while x: 132 | if x: 133 | continue 134 | """).score == 3 135 | 136 | -------------------------------------------------------------------------------- /tests/test_scoped_objects.py: -------------------------------------------------------------------------------- 1 | from tests.utils import complexity 2 | 3 | 4 | class describe_modules: 5 | def test_that_they_are_scored(self): 6 | assert complexity( 7 | """ 8 | a if a else a 9 | """).results.named('').score == 2 10 | assert complexity( 11 | """ 12 | 0 if x else 1 if y else 2 13 | """).results.named('').score == 3 14 | 15 | def test_that_they_know_their_names(self): 16 | assert complexity("").results.named('').name == '' 17 | 18 | def test_that_they_know_their_line_range(self): 19 | stats = complexity("").results.named('') 20 | assert stats.start_line == 1 21 | assert stats.end_line == 1 22 | 23 | stats = complexity( 24 | """ 25 | a 26 | """).results.named('') 27 | print '-%s-' % ( 28 | """ 29 | a 30 | """) 31 | assert stats.start_line == 1 32 | assert stats.end_line == 3 33 | 34 | def test_module_with_function_in_it(self): 35 | assert complexity( 36 | """ 37 | a if a else a 38 | def foo(): 39 | a if a else a 40 | a if a else a 41 | """).results.named('').score == 3 42 | 43 | 44 | class describe_functions: 45 | def test_that_they_are_scored(self): 46 | assert complexity( 47 | """ 48 | def foo(): 49 | 0 if x else 1 50 | """).results.named('foo').score == 2 51 | assert complexity( 52 | """ 53 | def foo(): 54 | 0 if x else 1 if y else 2 55 | """).results.named('foo').score == 3 56 | 57 | def test_that_they_know_their_names(self): 58 | assert complexity( 59 | """ 60 | def foo(): pass 61 | """).results.named('foo').name == 'foo' 62 | 63 | def test_that_they_know_their_line_range(self): 64 | stats = complexity("def foo(): pass").results.named('foo') 65 | assert stats.start_line == 1 66 | assert stats.end_line == 1 67 | 68 | stats = complexity( 69 | """ 70 | def foo(): pass 71 | """).results.named('foo') 72 | assert stats.start_line == 2 73 | assert stats.end_line == 2 74 | 75 | 76 | class describe_classes: 77 | def test_that_they_are_scored(self): 78 | assert complexity( 79 | """ 80 | class Foo: 81 | 0 if x else 1 82 | """).results.named('Foo').score == 2 83 | assert complexity( 84 | """ 85 | class Foo: 86 | 0 if x else 1 if y else 2 87 | """).results.named('Foo').score == 3 88 | 89 | def test_that_they_know_their_names(self): 90 | assert complexity( 91 | """ 92 | class Foo: pass 93 | """).results.named('Foo').name == 'Foo' 94 | 95 | def test_that_they_know_their_line_range(self): 96 | stats = complexity("class Foo: pass").results.named('Foo') 97 | assert stats.start_line == 1 98 | assert stats.end_line == 1 99 | 100 | stats = complexity( 101 | """ 102 | class Foo: 103 | pass 104 | """).results.named('Foo') 105 | assert stats.start_line == 2 106 | assert stats.end_line == 3 107 | 108 | def test_that_they_include_code_interspersed_with_methods(self): 109 | stats = complexity( 110 | """ 111 | class Foo: 112 | 0 if x else 1 113 | def foo(self): pass 114 | 0 if x else 1 115 | """).results.named('Foo') 116 | assert stats.score == 3 117 | assert stats.end_line == 5 118 | 119 | 120 | 121 | class describe_methods: 122 | def test_that_they_are_scored(self): 123 | assert complexity( 124 | """ 125 | class Foo: 126 | def foo(): 127 | 0 if x else 1 128 | """).results.named('Foo.foo').score == 2 129 | assert complexity( 130 | """ 131 | class Foo: 132 | def foo(): 133 | 0 if x else 1 if y else 2 134 | """).results.named('Foo.foo').score == 3 135 | 136 | def test_that_they_know_their_names(self): 137 | assert complexity( 138 | """ 139 | class Foo: 140 | def foo(): pass 141 | """).results.named('Foo.foo').name == 'Foo.foo' 142 | 143 | def test_that_they_know_their_line_range(self): 144 | stats = complexity( 145 | """ 146 | class Foo(): 147 | def foo(): 148 | pass 149 | """).results.named('Foo.foo') 150 | assert stats.start_line == 3 151 | assert stats.end_line == 4 152 | 153 | stats = complexity( 154 | """ 155 | pass 156 | class Foo: 157 | def foo(): 158 | pass 159 | pass 160 | """).results.named('Foo.foo') 161 | assert stats.start_line == 4 162 | assert stats.end_line == 6 163 | 164 | -------------------------------------------------------------------------------- /tests/test_simple_statements.py: -------------------------------------------------------------------------------- 1 | from tests.utils import complexity 2 | 3 | 4 | class describe_simple_statements: 5 | def test_pass(self): 6 | assert complexity('pass').score == 1 7 | 8 | def test_statement_sequence(self): 9 | assert complexity( 10 | """ 11 | pass 12 | pass 13 | """).score == 1 14 | 15 | def test_constant(self): 16 | assert complexity("1").score == 1 17 | 18 | def test_assignment(self): 19 | assert complexity("x = 1").score == 1 20 | 21 | def test_name(self): 22 | assert complexity("a").score == 1 23 | 24 | def test_sequence_of_names(self): 25 | assert complexity( 26 | """ 27 | a 28 | b 29 | c 30 | """).score == 1 31 | 32 | def test_logical_operators(self): 33 | assert complexity('a and b or (c or d and not e)').score == 1 34 | 35 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | 3 | from complexity import compute_code_complexity 4 | 5 | 6 | def complexity(code): 7 | return compute_code_complexity(dedent(code)) 8 | 9 | --------------------------------------------------------------------------------