Coverage for src / codeaudit / complexitycheck.py: 75%
57 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-09 09:33 +0200
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-09 09:33 +0200
1"""
2License GPLv3 or higher.
4(C) 2025 Created by Maikel Mardjan - https://nocomplexity.com/
6This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
8This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
10You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
12Simple Cyclomatic Complexity check for Python source files
13See the docs for in-depth and why this is a simple way , but good enough!
14"""
16import ast
17import warnings
18from pathlib import Path
21class ComplexityVisitor(ast.NodeVisitor):
22 def __init__(self):
23 self.complexity = 1 # Start with 1 for the entry point
25 def visit_If(self, node):
26 self.complexity += 1
27 self.generic_visit(node)
29 def visit_For(self, node):
30 self.complexity += 1
31 self.generic_visit(node)
33 def visit_While(self, node):
34 self.complexity += 1
35 self.generic_visit(node)
37 def visit_Try(self, node):
38 self.complexity += 1 # For the try block
39 self.generic_visit(node)
41 def visit_ExceptHandler(self, node):
42 self.complexity += 1 # For each except clause
43 self.generic_visit(node)
45 def visit_With(self, node):
46 self.complexity += 1 # For 'with' statements
47 self.generic_visit(node)
49 def visit_BoolOp(self, node):
50 # Count 'and' and 'or' operators
51 # Each 'and' or 'or' introduces a new predicate
52 self.complexity += len(node.values) - 1
53 self.generic_visit(node)
55 def visit_Match(self, node):
56 # A 'match' statement itself adds to complexity
57 self.complexity += 1
58 self.generic_visit(node)
60 def visit_MatchCase(self, node):
61 # Each 'case' in a match statement is a distinct path
62 # Note: The 'visit_Match' already adds 1. Each subsequent 'MatchCase'
63 # For simplicity and aligning with common CC interpretations where each distinct
64 # path adds 1, we'll increment for each MatchCase.
65 # So count each case as a separate branch point from the 'match' entry.
66 self.complexity += 1
67 self.generic_visit(node)
69 def visit_Assert(self, node):
70 # Assert statements introduce a potential exit point (branch)
71 self.complexity += 1
72 self.generic_visit(node)
75def calculate_complexity(code):
76 with warnings.catch_warnings():
77 warnings.simplefilter("ignore", category=SyntaxWarning)
78 tree = ast.parse(code)
80 visitor = ComplexityVisitor()
81 visitor.visit(tree)
82 return visitor.complexity
85def count_static_warnings_in_file(file_path, max_file_size=10_000_000):
86 """
87 Parses a Python source file using AST and counts the number of warnings raised (e.g., SyntaxWarning).
89 Args:
90 file_path (str or Path): Path to the Python source file.
91 max_file_size (int, optional): Maximum allowed file size in bytes (default: 10 MB).
93 Returns:
94 dict: {"warnings": int} - Number of static warnings detected during parsing.
95 Returns -1 if the file cannot be read or parsed.
96 """
97 try:
98 # Convert to Path and resolve
99 file_path = Path(file_path).expanduser().resolve()
101 # Security: Check file exists and is a regular file
102 if not file_path.is_file():
103 return -1
105 # Security: Max file size protection
106 if file_path.stat().st_size > max_file_size:
107 return -1
109 # Read source code safely
110 source = file_path.read_text(encoding="utf-8")
112 # Capture warnings during AST parsing
113 with warnings.catch_warnings(record=True) as caught_warnings:
114 warnings.simplefilter("always")
115 ast.parse(source, filename=str(file_path))
117 return {"warnings": len(caught_warnings)}
119 except (SyntaxError, UnicodeDecodeError, ValueError):
120 return -1