Spaces:
Sleeping
Sleeping
| from .cognitive_complexity import ( | |
| compute_cognitive_complexity, | |
| ) | |
| from .call_graph import analyze_local_call_graph | |
| from .node_types import ( | |
| BOOLEAN_OPERATOR, | |
| BRANCH_TYPES, | |
| CASE_CLAUSE, | |
| CLASS_DEFINITION, | |
| COMPREHENSION_TYPES, | |
| CONDITIONAL_EXPRESSION, | |
| EXCEPT_CLAUSE, | |
| FOR_STATEMENT, | |
| FUNCTION_DEFINITION, | |
| IF_STATEMENT, | |
| LOOP_TYPES, | |
| NESTING_TYPES, | |
| NOT_OPERATOR, | |
| RETURN_STATEMENT, | |
| WHILE_STATEMENT, | |
| WITH_STATEMENT, | |
| ) | |
| from .parser import is_async_node, walk | |
| from .operational_features import extract_operational_features | |
| def _decision_cost(node) -> int: | |
| if node.type in { | |
| IF_STATEMENT, | |
| "elif_clause", | |
| FOR_STATEMENT, | |
| WHILE_STATEMENT, | |
| EXCEPT_CLAUSE, | |
| CONDITIONAL_EXPRESSION, | |
| CASE_CLAUSE, | |
| }: | |
| return 1 | |
| if node.type == BOOLEAN_OPERATOR: | |
| return sum(child.type in {"and", "or"} for child in node.children) | |
| if node.type == "if_clause" and node.parent and ( | |
| node.parent.type in COMPREHENSION_TYPES | |
| or node.parent.type == "for_in_clause" | |
| ): | |
| return 1 | |
| return 0 | |
| def _cyclomatic_for_scope(scope) -> int: | |
| complexity = 1 | |
| def visit(node): | |
| nonlocal complexity | |
| if node.type == "ERROR": | |
| return | |
| if node is not scope and node.type == FUNCTION_DEFINITION: | |
| return | |
| complexity += _decision_cost(node) | |
| for child in node.children: | |
| visit(child) | |
| visit(scope) | |
| return complexity | |
| def _max_cyclomatic(root_node) -> int: | |
| functions = [node for node in walk(root_node) if node.type == FUNCTION_DEFINITION] | |
| if not functions: | |
| return _cyclomatic_for_scope(root_node) | |
| return max(_cyclomatic_for_scope(function) for function in functions) | |
| def _max_nesting(root_node) -> int: | |
| maximum = 0 | |
| def visit(node, depth): | |
| nonlocal maximum | |
| if node.type == "ERROR": | |
| return | |
| new_depth = depth | |
| if node.type == "block" and node.parent and ( | |
| node.parent.type in NESTING_TYPES | |
| or node.parent.type in {FUNCTION_DEFINITION, CLASS_DEFINITION} | |
| ): | |
| new_depth += 1 | |
| maximum = max(maximum, new_depth) | |
| for child in node.children: | |
| visit(child, new_depth) | |
| visit(root_node, 0) | |
| return maximum | |
| def _docstring_lines(root_node) -> set[int]: | |
| lines = set() | |
| scopes = [root_node] + [ | |
| node | |
| for node in walk(root_node) | |
| if node.type in {FUNCTION_DEFINITION, CLASS_DEFINITION} | |
| ] | |
| for scope in scopes: | |
| body = scope if scope.type == "module" else scope.child_by_field_name("body") | |
| if body is None: | |
| continue | |
| statements = body.named_children | |
| if not statements: | |
| continue | |
| first = statements[0] | |
| if first.type != "expression_statement" or not first.named_children: | |
| continue | |
| if first.named_children[0].type != "string": | |
| continue | |
| lines.update(range(first.start_point.row, first.end_point.row + 1)) | |
| return lines | |
| def _sloc(root_node, code: str) -> int: | |
| docstrings = _docstring_lines(root_node) | |
| return sum( | |
| 1 | |
| for index, line in enumerate(code.splitlines()) | |
| if index not in docstrings | |
| and line.strip() | |
| and not line.strip().startswith("#") | |
| ) | |
| def _has_nested_loops(root_node) -> bool: | |
| for node in walk(root_node): | |
| if node.type not in LOOP_TYPES: | |
| continue | |
| for descendant in walk(node): | |
| if descendant is not node and ( | |
| descendant.type in LOOP_TYPES | |
| or descendant.type in COMPREHENSION_TYPES | |
| ): | |
| return True | |
| return False | |
| def _bool_complexity(root_node) -> int: | |
| def count(node): | |
| value = 0 | |
| if node.type == BOOLEAN_OPERATOR: | |
| value += sum(child.type in {"and", "or"} for child in node.children) | |
| elif node.type == NOT_OPERATOR: | |
| value += 1 | |
| return value + sum(count(child) for child in node.named_children) | |
| roots = [ | |
| node | |
| for node in walk(root_node) | |
| if node.type in {BOOLEAN_OPERATOR, NOT_OPERATOR} | |
| and ( | |
| node.parent is None | |
| or node.parent.type not in {BOOLEAN_OPERATOR, NOT_OPERATOR} | |
| ) | |
| ] | |
| return max((count(node) for node in roots), default=0) | |
| def _max_function_length(root_node, code: str) -> int: | |
| lines = code.splitlines() | |
| lengths = [] | |
| for function in walk(root_node): | |
| if function.type != FUNCTION_DEFINITION: | |
| continue | |
| extent = function | |
| if function.parent and function.parent.type == "decorated_definition": | |
| extent = function.parent | |
| start, end = extent.start_point.row, extent.end_point.row | |
| length = sum( | |
| 1 | |
| for line in lines[start : end + 1] | |
| if line.strip() and not line.strip().startswith("#") | |
| ) | |
| lengths.append(length) | |
| return max(lengths, default=0) | |
| def extract_structural(root_node, code: str) -> dict: | |
| nodes = list(walk(root_node)) | |
| call_graph = analyze_local_call_graph(root_node) | |
| operational = extract_operational_features(root_node) | |
| cognitive = compute_cognitive_complexity(root_node) | |
| branches = sum(node.type in BRANCH_TYPES for node in nodes) | |
| return { | |
| "cognitive_complexity": cognitive, | |
| "cyclomatic_complexity": _max_cyclomatic(root_node), | |
| "max_nesting_depth": _max_nesting(root_node), | |
| "sloc": _sloc(root_node, code), | |
| "num_branches": branches, | |
| "num_loops": sum(node.type in LOOP_TYPES for node in nodes), | |
| "has_nested_loops": _has_nested_loops(root_node), | |
| "has_recursion": call_graph["recursive_function_count"] > 0, | |
| "max_bool_expr_complexity": _bool_complexity(root_node), | |
| "num_function_defs": sum( | |
| node.type == FUNCTION_DEFINITION for node in nodes | |
| ), | |
| "num_classes": sum( | |
| node.type == CLASS_DEFINITION for node in nodes | |
| ), | |
| "max_function_length": _max_function_length(root_node, code), | |
| "num_returns": sum(node.type == RETURN_STATEMENT for node in nodes), | |
| "exception_handler_count": sum( | |
| node.type == EXCEPT_CLAUSE for node in nodes | |
| ), | |
| "has_async": any( | |
| node.type == "await" | |
| or ( | |
| node.type in {FUNCTION_DEFINITION, FOR_STATEMENT, WITH_STATEMENT} | |
| and is_async_node(node) | |
| ) | |
| for node in nodes | |
| ), | |
| "num_comprehensions": sum( | |
| node.type in COMPREHENSION_TYPES for node in nodes | |
| ), | |
| "complexity_per_branch": round(cognitive / max(branches, 1), 2), | |
| **operational, | |
| **{ | |
| key: value | |
| for key, value in call_graph.items() | |
| if not key.startswith("_") | |
| and key != "unresolved_local_call_count" | |
| }, | |
| } | |