dart_analyzer
¶
Full name: tenets.core.analysis.implementations.dart_analyzer
dart_analyzer¶
Dart code analyzer with Flutter support.
This module provides comprehensive analysis for Dart source files, including support for Flutter-specific patterns, null safety, async programming, and modern Dart features.
Classes¶
DartAnalyzer¶
Bases: LanguageAnalyzer
Dart code analyzer with Flutter support.
Provides comprehensive analysis for Dart files including: - Import and export directives - Part and library declarations - Classes with mixins and extensions - Null safety features (?, !, late) - Async/await, Future, and Stream handling - Flutter widgets and lifecycle methods - Factory and named constructors - Extension methods - Annotations and metadata - Generics and type parameters
Supports Dart 2.x with null safety and Flutter framework patterns.
Initialize the Dart analyzer with logger.
Source code in tenets/core/analysis/implementations/dart_analyzer.py
Functions¶
extract_imports¶
Extract import, export, part, and library directives from Dart code.
Handles: - import statements: import 'package:flutter/material.dart'; - export statements: export 'src/widget.dart'; - part statements: part 'implementation.dart'; - part of statements: part of 'library.dart'; - library declarations: library my_library; - Conditional imports: import 'stub.dart' if (dart.library.io) 'io.dart'; - Show/hide clauses: import 'dart:math' show Random hide PI; - Deferred imports: import 'big_lib.dart' deferred as big;
PARAMETER | DESCRIPTION |
---|---|
content | Dart 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/dart_analyzer.py
def extract_imports(self, content: str, file_path: Path) -> List[ImportInfo]:
"""Extract import, export, part, and library directives from Dart code.
Handles:
- import statements: import 'package:flutter/material.dart';
- export statements: export 'src/widget.dart';
- part statements: part 'implementation.dart';
- part of statements: part of 'library.dart';
- library declarations: library my_library;
- Conditional imports: import 'stub.dart' if (dart.library.io) 'io.dart';
- Show/hide clauses: import 'dart:math' show Random hide PI;
- Deferred imports: import 'big_lib.dart' deferred as big;
Args:
content: Dart source code
file_path: Path to the file being analyzed
Returns:
List of ImportInfo objects with import details
"""
imports = []
lines = content.split("\n")
for i, line in enumerate(lines, 1):
# Skip comments
if line.strip().startswith("//"):
continue
# Import statements - handle more complex patterns with show/hide
# First, try to extract the basic import and parse show/hide separately
basic_import_pattern = r"^\s*import\s+['\"]([^'\"]+)['\"](?:\s+if\s*\([^)]+\)\s*['\"][^'\"]+['\"]*)?(?:\s+deferred)?(?:\s+as\s+(\w+))?(.*?);"
match = re.match(basic_import_pattern, line)
if match:
module_path = match.group(1)
alias = match.group(2)
show_hide_part = match.group(3) if match.group(3) else ""
# Parse show/hide clauses
show_symbols = []
hide_symbols = []
is_deferred = "deferred" in line
# Extract show clause
show_match = re.search(r"\bshow\s+([^;]+?)(?:\s+hide|$)", show_hide_part + " ")
if show_match:
show_symbols = self._parse_symbols(show_match.group(1))
# Extract hide clause
hide_match = re.search(r"\bhide\s+([^;]+?)(?:\s+show|$)", show_hide_part + " ")
if hide_match:
hide_symbols = self._parse_symbols(hide_match.group(1))
# Determine import type
import_type = "import"
is_package = module_path.startswith("package:")
is_dart_core = module_path.startswith("dart:")
is_relative = module_path.startswith("../") or module_path.startswith("./")
# Categorize the import
category = self._categorize_import(module_path)
imports.append(
ImportInfo(
module=module_path,
alias=alias,
line=i,
type=import_type,
is_relative=is_relative,
is_package=is_package,
is_dart_core=is_dart_core,
is_deferred=is_deferred,
category=category,
show_symbols=show_symbols if show_symbols else [],
hide_symbols=hide_symbols if hide_symbols else [],
)
)
# Export statements
export_pattern = r"""
^\s*export\s+
['"]([^'"]+)['"]\s*
(?:show\s+([^;]+))?\s*
(?:hide\s+([^;]+))?\s*
;
"""
match = re.match(export_pattern, line, re.VERBOSE)
if match:
module_path = match.group(1)
show_clause = match.group(2)
hide_clause = match.group(3)
imports.append(
ImportInfo(
module=module_path,
line=i,
type="export",
is_relative=not module_path.startswith("package:")
and not module_path.startswith("dart:"),
show_symbols=self._parse_symbols(show_clause) if show_clause else [],
hide_symbols=self._parse_symbols(hide_clause) if hide_clause else [],
category=self._categorize_import(module_path),
)
)
# Part statements
part_pattern = r"^\s*part\s+['\"]([^'\"]+)['\"]\s*;"
match = re.match(part_pattern, line)
if match:
imports.append(
ImportInfo(
module=match.group(1),
line=i,
type="part",
is_relative=True,
is_part_file=True,
)
)
# Part of statements
part_of_pattern = r"^\s*part\s+of\s+['\"]?([^'\";\s]+)['\"]?\s*;"
match = re.match(part_of_pattern, line)
if match:
imports.append(
ImportInfo(
module=match.group(1),
line=i,
type="part_of",
is_relative=False,
is_library_part=True,
)
)
# Library declaration
library_pattern = r"^\s*library\s+(\w+(?:\.\w+)*)\s*;"
match = re.match(library_pattern, line)
if match:
imports.append(
ImportInfo(
module=match.group(1),
line=i,
type="library",
is_relative=False,
is_library_declaration=True,
)
)
# Handle conditional, multi-line imports like:
# import 'stub.dart'
# if (dart.library.io) 'io_implementation.dart'
# if (dart.library.html) 'web_implementation.dart';
cond_import_pattern = (
r"import\s+['\"]([^'\"]+)['\"]\s*(?:\s*if\s*\([^)]+\)\s*['\"][^'\"]+['\"]\s*)+;"
)
for m in re.finditer(cond_import_pattern, content, re.MULTILINE):
first_module = m.group(1)
# Avoid duplicates if already added (e.g., if written in one line)
if not any(imp.module == first_module and imp.type == "import" for imp in imports):
imports.append(
ImportInfo(
module=first_module,
line=content[: m.start()].count("\n") + 1,
type="import",
is_relative=first_module.startswith("../") or first_module.startswith("./"),
is_package=first_module.startswith("package:"),
is_dart_core=first_module.startswith("dart:"),
category=self._categorize_import(first_module),
conditional=True,
)
)
return imports
extract_exports¶
Extract exported symbols from Dart code.
In Dart, exports include: - Public classes (not prefixed with _) - Public functions - Public variables and constants - Public typedefs - Public enums - Extension methods
PARAMETER | DESCRIPTION |
---|---|
content | Dart source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
RETURNS | DESCRIPTION |
---|---|
List[Dict[str, Any]] | List of exported symbols |
Source code in tenets/core/analysis/implementations/dart_analyzer.py
def extract_exports(self, content: str, file_path: Path) -> List[Dict[str, Any]]:
"""Extract exported symbols from Dart code.
In Dart, exports include:
- Public classes (not prefixed with _)
- Public functions
- Public variables and constants
- Public typedefs
- Public enums
- Extension methods
Args:
content: Dart source code
file_path: Path to the file being analyzed
Returns:
List of exported symbols
"""
exports = []
# Public classes (including abstract and mixins)
class_pattern = r"^\s*(?:abstract\s+)?(?:final\s+)?(?:base\s+)?(?:interface\s+)?(?:mixin\s+)?class\s+([A-Z]\w*)"
for match in re.finditer(class_pattern, content, re.MULTILINE):
class_name = match.group(1)
modifiers = []
match_str = match.group(0)
if "abstract" in match_str:
modifiers.append("abstract")
if "final" in match_str:
modifiers.append("final")
if "base" in match_str:
modifiers.append("base")
if "interface" in match_str:
modifiers.append("interface")
if "mixin" in match_str:
modifiers.append("mixin")
exports.append(
{
"name": class_name,
"type": "class",
"line": content[: match.start()].count("\n") + 1,
"modifiers": modifiers,
"is_public": True,
}
)
# Mixins
mixin_pattern = r"^\s*(?:base\s+)?mixin\s+([A-Z]\w*)"
for match in re.finditer(mixin_pattern, content, re.MULTILINE):
if not any(e["name"] == match.group(1) for e in exports): # Avoid duplicates
exports.append(
{
"name": match.group(1),
"type": "mixin",
"line": content[: match.start()].count("\n") + 1,
"is_public": True,
}
)
# Public functions (not starting with _), including async*, sync*
func_pattern = r"^\s*(?:Future<?[^>]*>?\s+|Stream<?[^>]*>?\s+|void\s+|[\w<>]+\s+)?([a-z]\w*)\s*(?:<[^>]+>)?\s*\([^\{]*\)\s*(?:(?:async|sync)\s*\*|async)?\s*(?:=>|\{)"
for match in re.finditer(func_pattern, content, re.MULTILINE):
func_name = match.group(1)
if not func_name.startswith("_"):
snippet = match.group(0)
exports.append(
{
"name": func_name,
"type": "function",
"line": content[: match.start()].count("\n") + 1,
"is_public": True,
"is_async": ("async" in snippet),
}
)
# Public variables and constants
var_pattern = r"^\s*(?:final\s+|const\s+|late\s+)?(?:static\s+)?(?:final\s+|const\s+)?(?:[\w<>?]+\s+)?([a-z]\w*)\s*(?:=|;)"
for match in re.finditer(var_pattern, content, re.MULTILINE):
var_name = match.group(1)
if not var_name.startswith("_") and var_name not in [
"if",
"for",
"while",
"return",
"class",
"import",
]:
var_type = "constant" if "const" in match.group(0) else "variable"
exports.append(
{
"name": var_name,
"type": var_type,
"line": content[: match.start()].count("\n") + 1,
"is_public": True,
"is_final": "final" in match.group(0),
"is_late": "late" in match.group(0),
}
)
# Enums
enum_pattern = r"^\s*enum\s+([A-Z]\w*)"
for match in re.finditer(enum_pattern, content, re.MULTILINE):
exports.append(
{
"name": match.group(1),
"type": "enum",
"line": content[: match.start()].count("\n") + 1,
"is_public": True,
}
)
# Typedefs
typedef_pattern = r"^\s*typedef\s+([A-Z]\w*)"
for match in re.finditer(typedef_pattern, content, re.MULTILINE):
exports.append(
{
"name": match.group(1),
"type": "typedef",
"line": content[: match.start()].count("\n") + 1,
"is_public": True,
}
)
# Extension methods
extension_pattern = r"^\s*extension\s+(?:([A-Z]\w*)\s+)?on\s+([A-Z]\w*)"
for match in re.finditer(extension_pattern, content, re.MULTILINE):
extension_name = match.group(1) or f"Extension on {match.group(2)}"
exports.append(
{
"name": extension_name,
"type": "extension",
"line": content[: match.start()].count("\n") + 1,
"on_type": match.group(2),
"is_public": True,
}
)
return exports
extract_structure¶
Extract code structure from Dart file.
Extracts: - Classes with inheritance, mixins, and interfaces - Constructors (default, named, factory) - Methods and getters/setters - Flutter widgets and lifecycle methods - Async functions and streams - Extension methods - Null safety features - Annotations
PARAMETER | DESCRIPTION |
---|---|
content | Dart 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/dart_analyzer.py
def extract_structure(self, content: str, file_path: Path) -> CodeStructure:
"""Extract code structure from Dart file.
Extracts:
- Classes with inheritance, mixins, and interfaces
- Constructors (default, named, factory)
- Methods and getters/setters
- Flutter widgets and lifecycle methods
- Async functions and streams
- Extension methods
- Null safety features
- Annotations
Args:
content: Dart source code
file_path: Path to the file being analyzed
Returns:
CodeStructure object with extracted elements
"""
structure = CodeStructure()
# Detect if it's a Flutter file
structure.is_flutter = self._is_flutter_file(content)
# Extract classes
class_pattern = r"""
^\s*(?:@\w+(?:\([^)]*\))?\s*)* # Annotations
(?:(abstract)\s+)?
(?:(final)\s+)?
(?:(base)\s+)?
(?:(interface)\s+)?
(?:(mixin)\s+)?
(?:(sealed)\s+)?
class\s+(\w+)
(?:<([^>\n{}]*?)>+)? # Generics (tolerant of nested '>')
(?:\s+extends\s+([^\{]+?))?
(?:\s+with\s+([^\{]+?))?
(?:\s+implements\s+([^\{]+?))?
\s*\{
"""
for match in re.finditer(class_pattern, content, re.VERBOSE | re.MULTILINE):
class_name = match.group(7)
# Extract class modifiers
modifiers = []
if match.group(1):
modifiers.append("abstract")
if match.group(2):
modifiers.append("final")
if match.group(3):
modifiers.append("base")
if match.group(4):
modifiers.append("interface")
if match.group(5):
modifiers.append("mixin")
if match.group(6):
modifiers.append("sealed")
# Parse inheritance
extends = match.group(9).strip() if match.group(9) else None
mixins = self._parse_type_list(match.group(10)) if match.group(10) else []
implements = self._parse_type_list(match.group(11)) if match.group(11) else []
# Check if it's a Flutter widget
is_widget = False
widget_type = None
if extends:
# Prefer concrete State<T> first to avoid misclassification
if re.search(r"\bState<", extends):
is_widget = True
widget_type = "state"
elif "StatelessWidget" in extends:
is_widget = True
widget_type = "stateless"
elif "StatefulWidget" in extends:
is_widget = True
widget_type = "stateful"
elif "InheritedWidget" in extends:
is_widget = True
widget_type = "inherited"
# Extract class body
class_body = self._extract_class_body(content, match.end())
if class_body:
# Extract constructors
constructors = self._extract_constructors(class_body, class_name)
# Extract methods
methods = self._extract_methods(class_body)
# Extract fields
fields = self._extract_fields(class_body)
# Extract getters/setters
properties = self._extract_properties(class_body)
else:
constructors = []
methods = []
fields = []
properties = []
class_info = ClassInfo(
name=class_name,
line=content[: match.start()].count("\n") + 1,
modifiers=modifiers,
generics=match.group(8),
bases=[extends] if extends else [],
mixins=mixins,
interfaces=implements,
constructors=constructors,
methods=methods,
fields=fields,
properties=properties,
is_widget=is_widget,
widget_type=widget_type,
is_sealed="sealed" in modifiers,
)
# Balance generics angle brackets if regex captured incomplete nested generics
if class_info.generics:
try:
opens = class_info.generics.count("<")
closes = class_info.generics.count(">")
if opens > closes:
class_info.generics = class_info.generics + (">" * (opens - closes))
except Exception:
pass
structure.classes.append(class_info)
# Fallback: capture classes with complex generic bounds that the primary regex may miss
try:
existing = {c.name for c in structure.classes}
complex_class_pattern = r"""^\s*
(?:(abstract|final|base|interface|mixin|sealed)\s+)*
class\s+(\w+)\s*<([^\n{]+)>\s*
(?:extends\s+([^\n{]+?))?\s*
(?:with\s+([^\n{]+?))?\s*
(?:implements\s+([^\n{]+?))?\s*\{
"""
for m in re.finditer(complex_class_pattern, content, re.MULTILINE | re.VERBOSE):
name = m.group(2)
if name in existing:
continue
modifiers_raw = m.group(1) or ""
modifiers = [mod for mod in modifiers_raw.split() if mod]
generics = m.group(3).strip()
extends = m.group(4).strip() if m.group(4) else None
mixins = self._parse_type_list(m.group(5)) if m.group(5) else []
implements = self._parse_type_list(m.group(6)) if m.group(6) else []
structure.classes.append(
ClassInfo(
name=name,
line=content[: m.start()].count("\n") + 1,
generics=generics,
bases=[extends] if extends else [],
mixins=mixins,
interfaces=implements,
constructors=[],
methods=[],
fields=[],
properties=[],
modifiers=modifiers,
)
)
except Exception:
pass
# Extract mixins (standalone)
mixin_pattern = r"^\s*(?:base\s+)?mixin\s+(\w+)(?:<([^>]+)>)?(?:\s+on\s+([^{]+))?\s*\{"
for match in re.finditer(mixin_pattern, content, re.MULTILINE):
mixin_name = match.group(1)
# Avoid duplicates with mixin classes
if not any(c.name == mixin_name for c in structure.classes):
structure.mixins.append(
{
"name": mixin_name,
"line": content[: match.start()].count("\n") + 1,
"generics": match.group(2),
"on_types": self._parse_type_list(match.group(3)) if match.group(3) else [],
}
)
# Extract top-level functions
func_pattern = r"""
^\s*(?:@\w+(?:\([^)]*\))?\s*)* # Annotations
(?:(Future|Stream)(?:<[^>]+>)?\s+)?
(?:(void|[\w<>?]+|\([^)]+\))\s+)? # Return type or record type
([a-zA-Z_]\w*)\s*
(?:<[^>]+>)?\s* # Generic parameters
\(([^)]*)\)\s*
(?:(?:async|sync)\s*\*|async)?\s* # async, async*, or sync*
(?:=>|\{)
"""
for match in re.finditer(func_pattern, content, re.VERBOSE | re.MULTILINE):
func_name = match.group(3)
# Skip if it's inside a class
if not self._is_top_level(content, match.start()):
continue
return_type = match.group(1) or match.group(2)
params = match.group(4)
span = content[match.start() : match.end()]
is_async = "async" in span
is_generator = "*" in span
func_info = FunctionInfo(
name=func_name,
line=content[: match.start()].count("\n") + 1,
return_type=return_type,
parameters=self._parse_parameters(params),
is_async=is_async,
is_generator=is_generator,
is_private=func_name.startswith("_"),
)
structure.functions.append(func_info)
# Extract enums (brace-aware, supports enhanced enums with methods)
enum_head_pattern = r"^\s*enum\s+(\w+)(?:\s*<[^>]+>)?(?:\s+implements\s+[^\{]+)?\s*\{"
for m in re.finditer(enum_head_pattern, content, re.MULTILINE):
enum_name = m.group(1)
enum_body = self._extract_block(content, m.end()) or ""
if enum_body is None:
continue
# Determine the values section: up to first top-level ';' if present
values_part = enum_body
depth = 0
cutoff = None
for i, ch in enumerate(enum_body):
if ch == "{":
depth += 1
elif ch == "}":
depth = max(0, depth - 1)
elif ch == ";" and depth == 0:
cutoff = i
break
if cutoff is not None:
values_part = enum_body[:cutoff]
values = self._parse_enum_values(values_part)
structure.enums.append(
{
"name": enum_name,
"line": content[: m.start()].count("\n") + 1,
"values": values,
"has_enhanced_features": ("(" in values_part) or (cutoff is not None),
}
)
# Extract extensions
extension_pattern = r"^\s*extension\s+(?:(\w+)\s+)?on\s+([^\{]+)\s*\{"
for match in re.finditer(extension_pattern, content, re.MULTILINE):
extension_name = match.group(1) or f"on {match.group(2)}"
on_type = match.group(2).strip()
structure.extensions.append(
{
"name": extension_name,
"line": content[: match.start()].count("\n") + 1,
"on_type": on_type,
}
)
# Extract typedefs
typedef_pattern = r"^\s*typedef\s+(\w+)(?:<[^>]+>)?\s*=\s*([^;]+);"
for match in re.finditer(typedef_pattern, content, re.MULTILINE):
structure.typedefs.append(
{
"name": match.group(1),
"line": content[: match.start()].count("\n") + 1,
"definition": match.group(2).strip(),
}
)
# Count null safety features
structure.nullable_types = len(re.findall(r"\w+\?(?:\s|,|\))", content))
structure.null_assertions = len(re.findall(r"\w+!(?:\.|;|\s|\))", content))
structure.late_variables = len(re.findall(r"\blate\s+", content))
structure.null_aware_operators = len(re.findall(r"\?\?|\?\.", content))
# Count async features
structure.async_functions = len(re.findall(r"\basync\s*(?:\*)?\s*(?:=>|\{)", content))
structure.await_expressions = len(re.findall(r"\bawait\s+", content))
structure.future_count = len(re.findall(r"\bFuture(?:\s*<|[.(])", content))
structure.stream_count = len(re.findall(r"\bStream(?:\s*<|[.(])", content))
# Detect test file
structure.is_test_file = (
"_test.dart" in file_path.name or file_path.parts and "test" in file_path.parts
)
# Detect main function
structure.has_main = bool(re.search(r"\bvoid\s+main\s*\(", content))
return structure
calculate_complexity¶
Calculate complexity metrics for Dart code.
Calculates: - Cyclomatic complexity - Cognitive complexity - Null safety complexity - Async complexity - Flutter-specific complexity - Class hierarchy depth
PARAMETER | DESCRIPTION |
---|---|
content | Dart 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/dart_analyzer.py
def calculate_complexity(self, content: str, file_path: Path) -> ComplexityMetrics:
"""Calculate complexity metrics for Dart code.
Calculates:
- Cyclomatic complexity
- Cognitive complexity
- Null safety complexity
- Async complexity
- Flutter-specific complexity
- Class hierarchy depth
Args:
content: Dart 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"\bwhile\b",
r"\bdo\b",
r"\bswitch\b",
r"\bcase\b",
r"\bcatch\b",
r"\b\?\s*[^:]+\s*:", # Ternary operator
r"\?\?", # Null coalescing
r"&&",
r"\|\|",
]
for keyword in decision_keywords:
complexity += len(re.findall(keyword, content))
metrics.cyclomatic = complexity
# Calculate cognitive complexity
cognitive = 0
nesting_level = 0
max_nesting = 0
lines = content.split("\n")
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"\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 = len([l for l in lines if l.strip() and not l.strip().startswith("//")])
metrics.comment_lines = len([l for l in lines if l.strip().startswith("//")])
metrics.comment_ratio = (
metrics.comment_lines / metrics.line_count if metrics.line_count > 0 else 0
)
# Count classes and methods
metrics.class_count = len(re.findall(r"\bclass\s+\w+", content))
metrics.mixin_count = len(re.findall(r"\bmixin\s+\w+", content))
metrics.method_count = len(
re.findall(
r"(?:^|\s)(?:Future|Stream|void|[\w<>]+)\s+\w+\s*\([^)]*\)\s*(?:async\s*)?(?:=>|\{)",
content,
)
)
# Null safety metrics
metrics.nullable_types = len(re.findall(r"\w+\?(?:\s|,|\))", content))
metrics.null_assertions = len(re.findall(r"\w+!(?:\.|;|\s|\))", content))
metrics.late_keywords = len(re.findall(r"\blate\s+", content))
metrics.null_aware_ops = len(re.findall(r"\?\?|\?\.|\?\.\?", content))
metrics.required_keywords = len(re.findall(r"\brequired\s+", content))
# Async metrics
metrics.async_functions = len(re.findall(r"\basync\s*(?:\*)?\s*(?:=>|\{)", content))
metrics.await_count = len(re.findall(r"\bawait\s+", content))
metrics.future_count = len(re.findall(r"\bFuture(?:\s*<|[.(])", content))
metrics.stream_count = len(re.findall(r"\bStream(?:\s*<|[.(])", content))
metrics.completer_count = len(re.findall(r"\bCompleter<", content))
# Flutter-specific metrics
if self._is_flutter_file(content):
metrics.widget_count = len(re.findall(r"\bWidget\b", content))
metrics.build_methods = len(re.findall(r"\bWidget\s+build\s*\(", content))
metrics.setstate_calls = len(re.findall(r"\bsetState\s*\(", content))
metrics.stateful_widgets = len(re.findall(r"extends\s+StatefulWidget", content))
metrics.stateless_widgets = len(re.findall(r"extends\s+StatelessWidget", content))
metrics.inherited_widgets = len(re.findall(r"extends\s+InheritedWidget", content))
# Flutter hooks and keys
metrics.keys_used = len(
re.findall(r"\bKey\s*\(|GlobalKey|ValueKey|ObjectKey|UniqueKey", content)
)
metrics.context_usage = len(re.findall(r"\bBuildContext\b", content))
# Exception handling metrics
metrics.try_blocks = len(re.findall(r"\btry\s*\{", content))
metrics.catch_blocks = len(re.findall(r"\bcatch\s*\(", content))
metrics.finally_blocks = len(re.findall(r"\bfinally\s*\{", content))
metrics.throw_statements = len(re.findall(r"\bthrow\s+", content))
metrics.rethrow_statements = len(re.findall(r"\brethrow\s*;", content))
# Type system metrics
metrics.generic_types = len(re.findall(r"<[\w\s,<>]+>", content))
metrics.type_parameters = len(re.findall(r"<\w+(?:\s+extends\s+\w+)?>", content))
metrics.dynamic_types = len(re.findall(r"\bdynamic\b", content))
metrics.var_declarations = len(re.findall(r"\bvar\s+\w+", content))
# Calculate maintainability index
import math
if metrics.code_lines > 0:
# Adjusted for Dart
null_safety_factor = 1 - (metrics.null_assertions * 0.01)
async_factor = 1 - (metrics.async_functions * 0.01)
flutter_factor = (
1 - (metrics.setstate_calls * 0.02) if hasattr(metrics, "setstate_calls") else 1
)
type_factor = 1 + ((metrics.nullable_types - metrics.dynamic_types) * 0.001)
mi = (
171
- 5.2 * math.log(max(1, complexity))
- 0.23 * complexity
- 16.2 * math.log(metrics.code_lines)
+ 10 * null_safety_factor
+ 5 * async_factor
+ 5 * flutter_factor
+ 5 * type_factor
)
metrics.maintainability_index = max(0, min(100, mi))
return metrics