csharp_analyzer
¶
Full name: tenets.core.analysis.implementations.csharp_analyzer
csharp_analyzer¶
C# code analyzer with Unity3D support.
This module provides comprehensive analysis for C# source files, including support for modern C# features, .NET patterns, and Unity3D specific constructs like MonoBehaviours, Coroutines, and Unity attributes.
Classes¶
CSharpAnalyzer¶
Bases: LanguageAnalyzer
C# code analyzer with Unity3D support.
Provides comprehensive analysis for C# files including: - Using directives and namespace analysis - Class, interface, struct, enum, and record extraction - Property and event analysis - Async/await and Task-based patterns - LINQ query detection - Attribute processing - Unity3D specific patterns (MonoBehaviour, Coroutines, etc.) - .NET Framework/Core detection - Nullable reference types (C# 8+) - Pattern matching (C# 7+)
Supports modern C# features and Unity3D development patterns.
Initialize the C# analyzer with logger.
Source code in tenets/core/analysis/implementations/csharp_analyzer.py
Functions¶
extract_imports¶
Extract using directives from C# code.
Handles: - using statements: using System.Collections.Generic; - using static: using static System.Math; - using aliases: using Project = PC.MyCompany.Project; - global using (C# 10+): global using System.Text; - Unity-specific usings
PARAMETER | DESCRIPTION |
---|---|
content | C# source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
RETURNS | DESCRIPTION |
---|---|
List[ImportInfo] | List of ImportInfo objects with import details |
Source code in tenets/core/analysis/implementations/csharp_analyzer.py
def extract_imports(self, content: str, file_path: Path) -> List[ImportInfo]:
"""Extract using directives from C# code.
Handles:
- using statements: using System.Collections.Generic;
- using static: using static System.Math;
- using aliases: using Project = PC.MyCompany.Project;
- global using (C# 10+): global using System.Text;
- Unity-specific usings
Args:
content: C# source code
file_path: Path to the file being analyzed
Returns:
List of ImportInfo objects with import details
"""
imports: List[ImportInfo] = []
lines = content.splitlines()
current_namespace: Optional[str] = None
seen_code = False # stop parsing usings after first non-using code element at top-level
# Pre-compile patterns (hot path in large files)
namespace_re = re.compile(r"^\s*namespace\s+([\w\.]+)")
alias_re = re.compile(r"^\s*(?:(global)\s+)?using\s+([\w\.]+)\s*=\s*([^;]+?)\s*;")
using_re = re.compile(r"^\s*(?:(global)\s+)?using\s+(?:(static)\s+)?([\w\.]+)\s*;")
decl_re = re.compile(
r"^\s*(?:public\s+)?(?:partial\s+)?(?:abstract\s+)?(?:sealed\s+)?(?:class|interface|struct|enum|delegate|record)\b"
)
for i, line in enumerate(lines, 1):
stripped = line.strip()
if not stripped:
continue
# Skip single-line comments
if stripped.startswith("//"):
continue
# Namespace (track for nested usings)
m = namespace_re.match(line)
if m:
current_namespace = m.group(1)
# Don't treat namespace declaration itself as code for stopping further usings
continue
# Stop scanning after first real code (class/interface/etc.) at top-level
if decl_re.match(line):
seen_code = True
if seen_code:
# Still allow usings inside namespace blocks (indented) – C# allows that
# Only break if this is a top-level code declaration and not inside a namespace context yet
if current_namespace is None:
break
# Using alias
m = alias_re.match(line)
if m:
is_global = m.group(1) == "global"
alias = m.group(2)
target = m.group(3).strip()
base_for_category = target.split("<", 1)[0].strip()
category = self._categorize_import(base_for_category)
is_unity = self._is_unity_import(base_for_category)
imports.append(
ImportInfo(
module=target,
alias=alias,
line=i,
type="global_using_alias" if is_global else "using_alias",
is_relative=False,
category=category,
is_unity=is_unity,
namespace_context=current_namespace,
)
)
continue
# Standard / static / global usings
m = using_re.match(line)
if m:
is_global = m.group(1) == "global"
is_static = m.group(2) == "static"
ns = m.group(3)
category = self._categorize_import(ns)
is_unity = self._is_unity_import(ns)
if is_global:
import_type = "global_using"
elif is_static:
import_type = "using_static"
else:
import_type = "using"
imports.append(
ImportInfo(
module=ns,
line=i,
type=import_type,
is_relative=False,
category=category,
is_unity=is_unity,
namespace_context=current_namespace,
)
)
continue
# .csproj dependency parsing
if file_path.suffix.lower() == ".csproj":
imports.extend(self._extract_csproj_dependencies(content))
return imports
extract_exports¶
Extract public members from C# code.
In C#, public members are accessible from other assemblies. This includes public classes, interfaces, structs, enums, delegates, etc.
PARAMETER | DESCRIPTION |
---|---|
content | C# source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
RETURNS | DESCRIPTION |
---|---|
List[Dict[str, Any]] | List of exported (public) symbols |
Source code in tenets/core/analysis/implementations/csharp_analyzer.py
def extract_exports(self, content: str, file_path: Path) -> List[Dict[str, Any]]:
"""Extract public members from C# code.
In C#, public members are accessible from other assemblies.
This includes public classes, interfaces, structs, enums, delegates, etc.
Args:
content: C# source code
file_path: Path to the file being analyzed
Returns:
List of exported (public) symbols
"""
exports = []
# Extract namespace
namespace_match = re.search(r"^\s*namespace\s+([\w\.]+)", content, re.MULTILINE)
namespace = namespace_match.group(1) if namespace_match else ""
# Public classes (including Unity MonoBehaviours)
class_pattern = r"^\s*(?:public\s+)?(?:partial\s+)?(?:abstract\s+)?(?:sealed\s+)?(?:static\s+)?class\s+(\w+)(?:\s*:\s*([\w\.,\s]+))?"
for match in re.finditer(class_pattern, content, re.MULTILINE):
class_name = match.group(1)
inheritance = match.group(2)
modifiers = []
if "abstract" in match.group(0):
modifiers.append("abstract")
if "sealed" in match.group(0):
modifiers.append("sealed")
if "static" in match.group(0):
modifiers.append("static")
if "partial" in match.group(0):
modifiers.append("partial")
# Check if it's a Unity component
is_unity_component = False
unity_base_class = None
if inheritance:
if "MonoBehaviour" in inheritance:
is_unity_component = True
unity_base_class = "MonoBehaviour"
elif "ScriptableObject" in inheritance:
is_unity_component = True
unity_base_class = "ScriptableObject"
elif "Editor" in inheritance:
is_unity_component = True
unity_base_class = "Editor"
exports.append(
{
"name": class_name,
"type": "class",
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
"modifiers": modifiers,
"inheritance": inheritance,
"is_unity_component": is_unity_component,
"unity_base_class": unity_base_class,
}
)
# Public interfaces
interface_pattern = r"^\s*(?:public\s+)?(?:partial\s+)?interface\s+(\w+)(?:<[^>]+>)?(?:\s*:\s*([\w\.,\s]+))?"
for match in re.finditer(interface_pattern, content, re.MULTILINE):
exports.append(
{
"name": match.group(1),
"type": "interface",
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
"extends": match.group(2),
}
)
# Public structs
struct_pattern = r"^\s*(?:public\s+)?(?:readonly\s+)?(?:ref\s+)?struct\s+(\w+)"
for match in re.finditer(struct_pattern, content, re.MULTILINE):
modifiers = []
if "readonly" in match.group(0):
modifiers.append("readonly")
if "ref" in match.group(0):
modifiers.append("ref")
exports.append(
{
"name": match.group(1),
"type": "struct",
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
"modifiers": modifiers,
}
)
# Public enums (support both 'enum' and 'enum class' styles)
enum_pattern = r"^\s*(?:public\s+)?enum(?:\s+class)?\s+(\w+)(?:\s*:\s*([\w\.]+))?"
for match in re.finditer(enum_pattern, content, re.MULTILINE):
enum_type = "enum_class" if "enum class" in match.group(0) else "enum"
exports.append(
{
"name": match.group(1),
"type": enum_type,
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
"base_type": match.group(2),
}
)
# Public delegates
delegate_pattern = r"^\s*(?:public\s+)?delegate\s+(\w+)\s+(\w+(?:<[^>]+>)?)\s*\([^)]*\)"
for match in re.finditer(delegate_pattern, content, re.MULTILINE):
exports.append(
{
"name": match.group(2),
"type": "delegate",
"return_type": match.group(1),
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
}
)
# Public records (C# 9+)
record_pattern = r"^\s*(?:public\s+)?record\s+(?:class\s+|struct\s+)?(\w+)"
for match in re.finditer(record_pattern, content, re.MULTILINE):
record_type = "record_struct" if "struct" in match.group(0) else "record"
exports.append(
{
"name": match.group(1),
"type": record_type,
"line": content[: match.start()].count("\n") + 1,
"namespace": namespace,
}
)
return exports
extract_structure¶
Extract code structure from C# file.
Extracts: - Namespace declarations - Classes with inheritance and interfaces - Properties with getters/setters - Methods including async methods - Events and delegates - Unity-specific components (MonoBehaviours, Coroutines) - LINQ queries - Attributes
PARAMETER | DESCRIPTION |
---|---|
content | C# source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
RETURNS | DESCRIPTION |
---|---|
CodeStructure | CodeStructure object with extracted elements |
Source code in tenets/core/analysis/implementations/csharp_analyzer.py
def extract_structure(self, content: str, file_path: Path) -> CodeStructure:
"""Extract code structure from C# file.
Extracts:
- Namespace declarations
- Classes with inheritance and interfaces
- Properties with getters/setters
- Methods including async methods
- Events and delegates
- Unity-specific components (MonoBehaviours, Coroutines)
- LINQ queries
- Attributes
Args:
content: C# source code
file_path: Path to the file being analyzed
Returns:
CodeStructure object with extracted elements
"""
structure = CodeStructure()
# Extract namespace
namespace_match = re.search(r"^\s*namespace\s+([\w\.]+)", content, re.MULTILINE)
if namespace_match:
structure.namespace = namespace_match.group(1)
# Detect if it's a Unity script
structure.is_unity_script = self._is_unity_script(content)
# Extract classes
# Capture any stacked attribute blocks immediately preceding the class declaration in a named group
# so we don't rely on a fragile backward scan that fails when the regex itself already consumed them.
class_pattern = (
r"(?:^|\n)\s*(?P<attr_block>(?:\[[^\]]+\]\s*)*)"
r"(?:(?P<visibility>public|private|protected|internal)\s+)?"
r"(?:(?P<partial>partial)\s+)?(?:(?P<abstract>abstract)\s+)?(?:(?P<sealed>sealed)\s+)?(?:(?P<static>static)\s+)?"
r"class\s+(?P<class_name>\w+)(?:<(?P<generics>[^>]+)>)?(?:\s*:\s*(?P<inheritance>[\w\.,\s<>]+))?"
)
for match in re.finditer(class_pattern, content):
attr_block = match.group("attr_block") or ""
class_name = match.group("class_name") or ""
generics = match.group("generics")
inheritance = match.group("inheritance")
# Prefer directly captured attribute block; fallback to legacy backward scan only if empty
attributes = self._extract_attributes(attr_block) if attr_block else []
if not attributes:
# Legacy backward scan (kept for robustness in edge cases where regex miss might occur)
start_line_index = content[: match.start()].count("\n")
lines = content.splitlines()
attr_lines: List[str] = []
line_cursor = start_line_index - 1
while line_cursor >= 0:
line_text = lines[line_cursor].strip()
if not line_text or not line_text.startswith("["):
break
attr_lines.insert(0, line_text)
line_cursor -= 1
if attr_lines:
attributes = self._extract_attributes("\n".join(attr_lines))
# Collect modifiers
modifiers: List[str] = []
for key in ["partial", "abstract", "sealed", "static"]:
if match.group(key):
modifiers.append(match.group(key))
visibility = match.group("visibility") or None
# Parse inheritance
bases = []
interfaces = []
is_monobehaviour = False
is_scriptable_object = False
if inheritance:
for item in inheritance.split(","):
item = item.strip()
if item == "MonoBehaviour":
is_monobehaviour = True
bases.append(item)
elif item == "ScriptableObject":
is_scriptable_object = True
bases.append(item)
elif item.startswith("I"): # Convention for interfaces
interfaces.append(item)
else:
bases.append(item)
# Find class body
class_body = self._extract_class_body(content, match.end())
# Extract class components
methods = []
properties = []
fields = []
events = []
unity_methods = []
coroutines = []
if class_body:
methods = self._extract_methods(class_body)
properties = self._extract_properties(class_body)
fields = self._extract_fields(class_body)
events = self._extract_events(class_body)
if is_monobehaviour or is_scriptable_object:
unity_methods = self._extract_unity_methods(class_body)
coroutines = self._extract_coroutines(class_body)
class_info = ClassInfo(
name=class_name,
line=content[: match.start()].count("\n") + 1,
generics=generics,
bases=bases,
interfaces=interfaces,
visibility=visibility,
modifiers=modifiers,
methods=methods,
properties=properties,
fields=fields,
events=events,
attributes=attributes,
is_monobehaviour=is_monobehaviour,
is_scriptable_object=is_scriptable_object,
unity_methods=unity_methods,
coroutines=coroutines,
)
structure.classes.append(class_info)
# Extract interfaces
interface_pattern = r"(?:^|\n)\s*(?:public\s+)?(?:partial\s+)?interface\s+(\w+)(?:<([^>]+)>)?(?:\s*:\s*([\w\.,\s<>]+))?"
for match in re.finditer(interface_pattern, content):
interface_name = match.group(1)
generics = match.group(2)
extends = match.group(3)
# Extract interface methods
interface_body = self._extract_class_body(content, match.end())
methods = self._extract_interface_methods(interface_body) if interface_body else []
structure.interfaces.append(
{
"name": interface_name,
"line": content[: match.start()].count("\n") + 1,
"generics": generics,
"extends": self._parse_interface_list(extends) if extends else [],
"methods": methods,
}
)
# Extract structs
struct_pattern = (
r"(?:^|\n)\s*(?:public\s+)?(?:readonly\s+)?(?:ref\s+)?struct\s+(\w+)(?:<([^>]+)>)?"
)
for match in re.finditer(struct_pattern, content):
struct_name = match.group(1)
generics = match.group(2)
modifiers = []
if "readonly" in match.group(0):
modifiers.append("readonly")
if "ref" in match.group(0):
modifiers.append("ref")
structure.structs.append(
{
"name": struct_name,
"line": content[: match.start()].count("\n") + 1,
"generics": generics,
"modifiers": modifiers,
}
)
# Extract enums
enum_pattern = r"(?:^|\n)\s*(?:public\s+)?enum\s+(\w+)(?:\s*:\s*(\w+))?"
for match in re.finditer(enum_pattern, content):
enum_name = match.group(1)
base_type = match.group(2)
# Extract enum values
enum_body = self._extract_class_body(content, match.end())
values = self._extract_enum_values(enum_body) if enum_body else []
structure.enums.append(
{
"name": enum_name,
"line": content[: match.start()].count("\n") + 1,
"base_type": base_type,
"values": values,
}
)
# Extract delegates
delegate_pattern = r"(?:^|\n)\s*(?:public\s+)?delegate\s+(\w+)\s+(\w+)\s*\(([^)]*)\)"
for match in re.finditer(delegate_pattern, content):
structure.delegates.append(
{
"return_type": match.group(1),
"name": match.group(2),
"parameters": self._parse_parameters(match.group(3)),
"line": content[: match.start()].count("\n") + 1,
}
)
# Extract global functions (rare in C# but possible)
structure.functions = self._extract_global_functions(content)
# Extract LINQ queries
structure.linq_queries = self._extract_linq_queries(content)
# Count async methods
structure.async_method_count = len(re.findall(r"\basync\s+(?:Task|ValueTask)", content))
# Count lambda expressions
structure.lambda_count = len(re.findall(r"=>\s*(?:\{|[^;{]+;)", content))
# Detect framework
structure.framework = self._detect_framework(content)
# Check for test file
structure.is_test_file = (
"Test" in file_path.name
or file_path.name.endswith("Tests.cs")
or file_path.name.endswith("Test.cs")
or any(part in ["Tests", "Test"] for part in file_path.parts)
)
return structure
calculate_complexity¶
Calculate complexity metrics for C# code.
Calculates: - Cyclomatic complexity - Cognitive complexity - Unity-specific complexity (Coroutines, Update methods) - Async/await complexity - LINQ complexity - Exception handling complexity
PARAMETER | DESCRIPTION |
---|---|
content | C# source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
RETURNS | DESCRIPTION |
---|---|
ComplexityMetrics | ComplexityMetrics object with calculated metrics |
Source code in tenets/core/analysis/implementations/csharp_analyzer.py
def calculate_complexity(self, content: str, file_path: Path) -> ComplexityMetrics:
"""Calculate complexity metrics for C# code.
Calculates:
- Cyclomatic complexity
- Cognitive complexity
- Unity-specific complexity (Coroutines, Update methods)
- Async/await complexity
- LINQ complexity
- Exception handling complexity
Args:
content: C# source code
file_path: Path to the file being analyzed
Returns:
ComplexityMetrics object with calculated metrics
"""
metrics = ComplexityMetrics()
# Calculate cyclomatic complexity
complexity = 1
decision_keywords = [
r"\bif\b",
r"\belse\s+if\b",
r"\belse\b",
r"\bfor\b",
r"\bforeach\b",
r"\bwhile\b",
r"\bdo\b",
r"\bswitch\b",
r"\bcase\b",
r"\bcatch\b",
r"\b&&\b",
r"\|\|",
r"\?\s*[^:]+\s*:", # Ternary operator
r"\?\?", # Null coalescing operator
r"\?\.(?!\s*\[)", # Null conditional operator (not including ?.[])
]
for keyword in decision_keywords:
complexity += len(re.findall(keyword, content))
# Add complexity for pattern matching (C# 7+)
# "is" patterns
complexity += len(re.findall(r"\bis\s+\w+\s+\w+", content))
# Switch statements with when filters
complexity += len(re.findall(r"\bswitch\s*\(.*\)\s*\{[\s\S]*?\bwhen\b", content))
# Switch expressions with when clauses (=> and when)
complexity += len(re.findall(r"\bswitch\s*\{[\s\S]*?=>[\s\S]*?\bwhen\b", content))
metrics.cyclomatic = complexity
# Calculate cognitive complexity
cognitive = 0
nesting_level = 0
max_nesting = 0
lines = content.splitlines()
for line in lines:
# Skip comments
if line.strip().startswith("//"):
continue
# Track nesting
opening_braces = line.count("{")
closing_braces = line.count("}")
nesting_level += opening_braces - closing_braces
max_nesting = max(max_nesting, nesting_level)
# Control structures with nesting penalty
control_patterns = [
(r"\bif\b", 1),
(r"\belse\s+if\b", 1),
(r"\belse\b", 0),
(r"\bfor\b", 1),
(r"\bforeach\b", 1),
(r"\bwhile\b", 1),
(r"\bdo\b", 1),
(r"\bswitch\b", 1),
(r"\btry\b", 1),
(r"\bcatch\b", 1),
]
for pattern, weight in control_patterns:
if re.search(pattern, line):
cognitive += weight * (1 + max(0, nesting_level - 1))
metrics.cognitive = cognitive
metrics.max_depth = max_nesting
# Count code elements
metrics.line_count = len(lines)
metrics.code_lines = self._count_code_lines(content)
metrics.comment_lines = self._count_comment_lines(content)
metrics.comment_ratio = (
metrics.comment_lines / metrics.line_count if metrics.line_count > 0 else 0
)
# Count classes, interfaces, etc.
metrics.class_count = len(re.findall(r"\bclass\s+\w+", content))
metrics.interface_count = len(re.findall(r"\binterface\s+\w+", content))
metrics.struct_count = len(re.findall(r"\bstruct\s+\w+", content))
metrics.enum_count = len(re.findall(r"\benum\s+\w+", content))
# Count methods
metrics.method_count = len(
re.findall(
r"(?:public|private|protected|internal)\s+(?:static\s+)?(?:async\s+)?(?:override\s+)?(?:virtual\s+)?(?:[\w<>\[\]]+)\s+\w+\s*\([^)]*\)\s*\{",
content,
)
)
# Property metrics
metrics.property_count = len(
re.findall(
r"(?:public|private|protected|internal)\s+(?:static\s+)?(?:[\w<>\[\]]+)\s+\w+\s*\{\s*(?:get|set)",
content,
)
)
metrics.auto_property_count = len(re.findall(r"\{\s*get;\s*(?:set;)?\s*\}", content))
# Exception handling metrics
metrics.try_blocks = len(re.findall(r"\btry\s*\{", content))
metrics.catch_blocks = len(
re.findall(r"\bcatch(?:\s+when\s*\([^)]*\))?\s*(?:\([^)]*\))?\s*\{", content)
)
metrics.finally_blocks = len(re.findall(r"\bfinally\s*\{", content))
# Count both "throw;" and "throw new ..." forms
metrics.throw_statements = len(re.findall(r"\bthrow\b", content))
# Async/await metrics
metrics.async_methods = len(re.findall(r"\basync\s+(?:Task|ValueTask)", content))
metrics.await_statements = len(re.findall(r"\bawait\s+", content))
# LINQ metrics
metrics.linq_queries = len(re.findall(r"\bfrom\s+\w+\s+in\s+", content))
metrics.linq_methods = len(
re.findall(
r"\.\s*(?:Where|Select|OrderBy|GroupBy|Join|Any|All|First|Last|Single)\s*\(",
content,
)
)
# Unity-specific metrics
if self._is_unity_script(content):
metrics.unity_components = len(
re.findall(r":\s*(?:MonoBehaviour|ScriptableObject)", content)
)
metrics.coroutines = len(re.findall(r"\bIEnumerator\s+\w+\s*\(", content))
metrics.unity_methods = len(
re.findall(
r"\b(?:Start|Update|FixedUpdate|LateUpdate|OnEnable|OnDisable|Awake|OnDestroy|OnCollision(?:Enter|Exit|Stay)?|OnTrigger(?:Enter|Exit|Stay)?)\s*\(",
content,
)
)
metrics.serialize_fields = len(re.findall(r"\[SerializeField\]", content))
metrics.unity_events = len(re.findall(r"\bUnityEvent(?:<[^>]+>)?\s+\w+", content))
# Attribute metrics
metrics.attribute_count = len(re.findall(r"\[[A-Z]\w*(?:\([^)]*\))?\]", content))
# Nullable reference types (C# 8+): properties and locals/params with ? type, plus #nullable enable
nullable_types = len(re.findall(r"[\w<>\[\]]+\?\s+\w+\s*[;=,)\}]", content))
metrics.nullable_refs = nullable_types + len(re.findall(r"#nullable\s+enable", content))
# Calculate maintainability index
import math
if metrics.code_lines > 0:
# Adjusted for C#
async_factor = 1 - (metrics.async_methods * 0.01)
unity_factor = 1 - (getattr(metrics, "coroutines", 0) * 0.02)
mi = (
171
- 5.2 * math.log(max(1, complexity))
- 0.23 * complexity
- 16.2 * math.log(metrics.code_lines)
+ 10 * async_factor
+ 10 * unity_factor
)
metrics.maintainability_index = max(0, min(100, mi))
return metrics