Language Support
This document describes the languages and syntax features supported by Hotspots.
Supported Languages
Hotspots supports multi-language analysis with consistent metrics across all languages:
- ECMAScript - TypeScript, JavaScript, and React (JSX/TSX) with full feature parity
- Go - Full Go language support including goroutines, defer, select, and channels
- Java - Full Java language support (Java 8+) including lambdas, streams, try-with-resources, and switch expressions (Java 14+)
- Python - Full Python language support including async/await, comprehensions, context managers, and match statements
- Rust - Full Rust language support including match expressions, ? operator, unwrap/expect, and panic
Analysis metrics (CC, ND, FO, NS, LRS) are computed consistently across all supported languages.
ECMAScript (TypeScript/JavaScript/React)
Supported File Extensions
TypeScript:
.ts- TypeScript source files.mts- TypeScript ES modules.cts- TypeScript CommonJS modules.d.ts- Type declaration files (excluded from analysis)
TypeScript with JSX (React):
.tsx- TypeScript React components.mtsx- TypeScript React ES modules.ctsx- TypeScript React CommonJS modules
JavaScript:
.js- JavaScript source files.mjs- JavaScript ES modules.cjs- JavaScript CommonJS modules
JavaScript with JSX (React):
.jsx- JavaScript React components.mjsx- JavaScript React ES modules.cjsx- JavaScript React CommonJS modules
Parser Configuration
hotspots uses swc_ecma_parser version 33.0.0 with automatic language detection:
- TypeScript files (.ts, .mts, .cts): Parsed with TypeScript syntax support (no JSX)
- TSX files (.tsx, .mtsx, .ctsx): Parsed with TypeScript + JSX syntax support
- JavaScript files (.js, .mjs, .cjs): Parsed with JavaScript syntax support (no JSX)
- JSX files (.jsx, .mjsx, .cjsx): Parsed with JavaScript + JSX syntax support
- Experimental Decorators: Disabled
- ES Version: ES2022
- Declaration Files:
.d.tsfiles are excluded from analysis
Supported Features
Common Features (TypeScript & JavaScript)
The following features are supported in both TypeScript and JavaScript:
Function Forms
All function forms are supported and analyzed:
- Function declarations
- Function expressions
- Arrow functions
- Class methods (instance and static)
- Object literal methods
- Getter/setter methods
Control Flow
if/elsestatementsswitchstatementsforloops (standard,for...in,for...of)whileloopsdo...whileloopstry/catch/finallyblocks- Labeled
breakandcontinue
ES2022 Features
- Optional chaining (
?.) - Nullish coalescing (
??) - Async/await
- Generators (
function*) - Private class fields (
#field) - Static class blocks
- Top-level
await - Class static initialization blocks
TypeScript-Only Features
When analyzing TypeScript files, the following additional features are supported:
- Type Annotations: Function parameters, return types, variable types
- Advanced Types: Union (
|), intersection (&), generics, conditional types, mapped types - Type Declarations: Interfaces, type aliases, enums, namespaces
- Class Modifiers:
public,private,protected,abstract - Type Assertions:
as,<>syntax
Note: Type annotations do not affect complexity metrics. A TypeScript function and its JavaScript equivalent (with types removed) will have identical LRS scores.
Explicitly Ignored Constructs
The following constructs are parsed but ignored in analysis (not counted as functions):
- Interfaces: Type-only declarations without runtime behavior
- Type aliases: Type-only declarations
- Overload signatures without bodies: Declaration-only function signatures
- Ambient declarations:
declarestatements
JSX/TSX Support
How JSX is Analyzed
Hotspots analyzes React components intelligently:
JSX Elements do NOT inflate complexity:
- Simple JSX markup (
<div>,<h1>, etc.) does not increase complexity metrics - JSX is treated as structured output, similar to template literals
- A component that just returns JSX has the same complexity as a function that returns a value
Control Flow in JSX Expressions IS counted:
- Ternary operators:
{condition ? <A/> : <B/>}increases CC - Logical AND:
{condition && <Component/>}increases CC - Map/filter with callbacks: Each callback function analyzed separately
Example:
// Simple component - LRS = 1.0 (no complexity added by JSX elements)
function SimpleComponent() {
return (
<div>
<h1>Title</h1>
<p>Content</p>
</div>
);
}
// Conditional component - LRS > 1.0 (ternary adds CC)
function ConditionalComponent({ isActive }) {
return (
<div>
{isActive ? <span>Active</span> : <span>Inactive</span>}
</div>
);
}Event Handlers and Callbacks
Anonymous functions in JSX (event handlers, map callbacks) are analyzed as separate functions:
function ItemList({ items }) {
return (
<div>
{items.map((item) => ( // This arrow function analyzed separately
<div key={item.id} onClick={() => console.log(item)}> // This too
{item.name}
</div>
))}
</div>
);
}
// Produces 3 function reports: ItemList, map callback, onClick callbackMetric Parity
Critical invariant: TypeScript, JavaScript, JSX, and TSX files with identical structure produce identical complexity metrics.
Example - these two functions have identical LRS:
TypeScript:
function calculate(x: number, y: number): number {
if (x > 0 && y > 0 || x < 0) {
return x + y;
}
return x * y;
}JavaScript:
function calculate(x, y) {
if (x > 0 && y > 0 || x < 0) {
return x + y;
}
return x * y;
}Both yield: CC=5, ND=1, FO=0, NS=1, LRS=5.48
Unsupported ECMAScript Features
The following features are not yet supported:
- Experimental Decorators: Standard decorators may be supported in future versions.
- Generator Functions (
function*): Encountering a generator will emit an error and skip that function. - Vue Single File Components:
.vuefiles are not supported yet. - Svelte Components:
.sveltefiles are not supported yet.
See Known Limitations below for full details.
Error Handling
JSX in Wrong File Extension
If JSX syntax is encountered in a .ts or .js file:
- The parser will emit a parse error
- Use
.tsxfor TypeScript + JSX - Use
.jsxfor JavaScript + JSX - Analysis of that file will abort with a clear error message
Generator Functions
If a generator function (function*) is encountered:
- Analysis of that specific function is skipped
- An error is emitted for that function
- Analysis continues with remaining functions
Parse Errors
General parse errors are reported with:
- Error message from the parser
- Context indicating the failure point
Implementation Details
The parser automatically detects the file type based on extension and selects the appropriate syntax configuration (defined in hotspots-core/src/parser.rs):
TypeScript files (.ts, .mts, .cts):
Syntax::Typescript(TsSyntax {
tsx: false, // No JSX support yet
decorators: false, // No experimental decorators
dts: is_dts, // Enable for .d.ts files
..Default::default()
})JavaScript files (.js, .mjs, .cjs):
Syntax::Es(EsSyntax {
jsx: false, // No JSX support yet
decorators: false, // No experimental decorators
..Default::default()
})Go Language Support
Supported File Extensions
Go:
.go- Go source files
Parser
Hotspots uses tree-sitter-go version 0.23.2 for parsing Go source files. Tree-sitter provides:
- Error-tolerant parsing
- Precise syntax tree representation
- Fast incremental parsing
Supported Features
Function Forms
All Go function forms are analyzed:
- Function declarations (
func name() {}) - Methods (receiver functions) (
func (t *Type) method() {}) - Value receiver methods (
func (t Type) method() {}) - Generic functions (Go 1.18+) (
func name[T any]() {})
Note: Anonymous functions (closures) are not yet supported.
Control Flow
All Go control flow constructs are fully supported:
Conditionals:
ifstatementsif/elsechainsif/else if/elseladders
Loops:
forloops (traditional 3-clause)- Range loops (
for _, item := range items) - While-style loops (
for condition {}) - Infinite loops (
for {}) breakandcontinuestatements
Switch Statements:
- Expression switches (
switch x { ... }) - Type switches (
switch x.(type) { ... }) - Tagless switches (
switch { case x > 0: ... }) - Fallthrough support
- Multiple values per case
Select Statements:
- Channel select (
select { case <-ch: ... }) - Non-blocking select with
default - Send and receive cases
Go-Specific Constructs
Defer:
deferstatements are counted as non-structured exits (NS)- Multiple defers are counted separately
- Defer contributes to fan-out if it calls a named function
Goroutines:
gostatements are counted as fan-out (FO)- Each unique goroutine spawn is counted once
- Example:
go doWork()increases FO by 1
Panic/Recover:
panic()calls are counted as non-structured exits (NS)recover()calls are counted as fan-out (FO)
Channels:
- Channel operations in select statements contribute to cyclomatic complexity (CC)
- Channel sends/receives themselves don't directly affect metrics
Metric Calculation
Go metrics are calculated using the same principles as other languages, with Go-specific adaptations:
Cyclomatic Complexity (CC)
Base formula: CC = E - N + 2 (from Control Flow Graph)
Additional contributions:
- Each switch/select case: +1
- Each boolean operator (
&&,||): +1 - Each
ifstatement: counted in CFG - Each loop: counted in CFG
Example:
func process(x int) {
if x > 0 && x < 100 { // CC: +1 (if) +1 (&&) = +2
switch x {
case 1: // CC: +1
doA()
case 2: // CC: +1
doB()
default: // CC: +1
doC()
}
}
}
// Total CC = 1 (base) + 1 (if) + 1 (&&) + 3 (switch cases) = 6Nesting Depth (ND)
Maximum depth of nested control structures:
ifstatementsforloops (all variants)switchstatements (all types)selectstatements
Example:
func nested() {
if x > 0 { // Depth 1
for i := 0; i < 10; i++ { // Depth 2
if i > 5 { // Depth 3
select {
case <-ch: // Depth 4
// code
}
}
}
}
}
// ND = 4Fan-Out (FO)
Count of unique function calls and goroutine spawns:
- Regular function calls
- Method calls
gostatements (each unique goroutine)
Example:
func fanOut() {
doWork() // FO: +1
doWork() // FO: +0 (duplicate)
doOther() // FO: +1
go doAsync() // FO: +1 (goroutine)
go doAsync() // FO: +0 (duplicate goroutine)
}
// Total FO = 3 (doWork, doOther, go doAsync)Non-Structured Exits (NS)
Count of exits that don't follow normal control flow:
returnstatements (excluding final tail return)deferstatementspanic()calls- Approximated via expression statements with calls
Example:
func exits(x int) int {
defer cleanup() // NS: +1
if x < 0 {
return -1 // NS: +1 (early return)
}
if x == 0 {
panic("zero") // NS: +1 (panic)
}
return x * 2 // NS: +0 (final tail return excluded)
}
// Total NS = 3Go-Specific Examples
Defer and Goroutines
func processAsync(items []Item) {
defer cleanup() // NS: +1, FO: +1 (cleanup)
for _, item := range items {
go func(i Item) { // FO: +1 (unique goroutine)
process(i) // FO: +1 (process)
}(item)
}
}
// CC=2 (base + loop), ND=1, FO=3, NS=1Select Statement
func selectExample(ch1, ch2 chan int) {
select {
case v := <-ch1: // CC: +1
handle(v) // FO: +1
case ch2 <- 42: // CC: +1
log() // FO: +1
default: // CC: +1
timeout() // FO: +1
}
}
// CC=4 (base + 3 cases), ND=1, FO=3, NS=0Type Switch
func typeSwitch(x interface{}) {
switch v := x.(type) {
case int: // CC: +1
handleInt(v)
case string: // CC: +1
handleString(v)
default: // CC: +1
handleOther(v)
}
}
// CC=4, ND=1, FO=3, NS=0Implementation Details
The Go parser is implemented in hotspots-core/src/language/go/:
parser.rs- Tree-sitter-based parsercfg_builder.rs- Control Flow Graph builder- Metrics extracted in
hotspots-core/src/metrics.rs(extract_go_metrics())
Unsupported Features
The following Go features are not yet supported:
- Anonymous functions/closures - Will be added in future release
- Function literals - Same as above
- Label handling - Labeled breaks/continues to specific loops
Error Handling
Parse errors in Go files are handled gracefully:
- Tree-sitter provides error-tolerant parsing
- Functions with parse errors are skipped
- Analysis continues with remaining valid functions
- Error messages indicate the failure point
Rust Language Support
Supported File Extensions
Rust:
.rs- Rust source files
Parser
Hotspots uses syn version 2.0 for parsing Rust source files. Syn is the same parser used by rustc and provides:
- Accurate Rust grammar support
- Full feature coverage (syn's
fullfeature enabled) - Precise source location tracking
- Graceful error handling
Supported Features
Function Forms
All Rust function forms are analyzed:
- Function declarations (
fn name() {}) - Methods (in
implblocks) (impl Type { fn method(&self) {} }) - Associated functions (
impl Type { fn new() -> Self {} }) - Async functions (
async fn name() {})
Note: Closures and anonymous functions are not yet supported.
Control Flow
All Rust control flow constructs are fully supported:
Conditionals:
ifexpressionsif/elsechainsif/else if/elseladders
Loops:
loopexpressions (infinite loops)whileloopsforloops (iterator-based)breakandcontinuestatements
Match Expressions:
- Match expressions (
match x { ... }) - Pattern matching with guards
- Match arms with multiple patterns
- Exhaustive matching
Rust-Specific Constructs
Question Mark Operator (?):
- The
?operator is counted as a non-structured exit (NS) - Multiple
?in the same expression are counted separately - Works with both
OptionandResulttypes
Unwrap/Expect:
.unwrap()calls are counted as non-structured exits (NS).expect()calls are counted as non-structured exits (NS)- These represent potential panic points
Panic:
panic!()macro invocations are counted as non-structured exits (NS)- Multiple panic sites are counted separately
Macros:
- Macro invocations (e.g.,
println!()) are counted as fan-out (FO) - Macros are not expanded; treated as function calls
Metric Calculation
Rust metrics are calculated using the same principles as other languages, with Rust-specific adaptations:
Cyclomatic Complexity (CC)
Base formula: CC = E - N + 2 (from Control Flow Graph)
Additional contributions:
- Each match arm: +1
- Each boolean operator (
&&,||): +1 - Each
ifexpression: counted in CFG - Each loop: counted in CFG
Example:
fn process(x: i32) {
if x > 0 && x < 100 { // CC: +1 (if) +1 (&&) = +2
match x {
1 => do_a(), // CC: +1
2 => do_b(), // CC: +1
_ => do_c(), // CC: +1
}
}
}
// Total CC = 1 (base) + 1 (if) + 1 (&&) + 3 (match arms) = 6Nesting Depth (ND)
Maximum depth of nested control structures:
ifexpressionsloop/while/forloopsmatchexpressions
Example:
fn nested() {
if x > 0 { // Depth 1
for i in 0..10 { // Depth 2
if i > 5 { // Depth 3
match i { // Depth 4
6 => {},
_ => {},
}
}
}
}
}
// ND = 4Fan-Out (FO)
Count of unique function calls, method calls, and macro invocations:
- Regular function calls
- Method calls
- Macro invocations (e.g.,
println!,panic!)
Example:
fn fan_out() {
do_work(); // FO: +1
do_work(); // FO: +0 (duplicate)
do_other(); // FO: +1
println!("hi"); // FO: +1 (macro)
}
// Total FO = 3 (do_work, do_other, println!)Non-Structured Exits (NS)
Count of exits that don't follow normal control flow:
returnstatements (excluding final tail return)?operator usages.unwrap()calls.expect()callspanic!()macro invocations
Example:
fn exits(x: Option<i32>) -> Option<i32> {
let value = x?; // NS: +1 (?)
if value < 0 {
return None; // NS: +1 (early return)
}
if value == 0 {
panic!("zero"); // NS: +1 (panic)
}
Some(value * 2) // NS: +0 (final tail expression excluded)
}
// Total NS = 3Rust-Specific Examples
Match Expressions
fn handle_result(res: Result<i32, String>) -> i32 {
match res {
Ok(n) if n > 0 => n * 2, // CC: +1
Ok(n) => n, // CC: +1
Err(_) => -1, // CC: +1
}
}
// CC=4 (base + 3 match arms), ND=1, FO=0, NS=0Question Mark Operator
fn parse_and_process(input: &str) -> Result<i32, String> {
let num = input.parse::<i32>()?; // NS: +1 (?)
let result = validate(num)?; // NS: +1 (?)
Ok(result * 2)
}
// CC=1, ND=0, FO=2 (parse, validate), NS=2Unwrap and Panic
fn risky_operation(opt: Option<i32>) -> i32 {
let value = opt.unwrap(); // NS: +1 (unwrap)
if value < 0 {
panic!("negative"); // NS: +1 (panic)
}
value
}
// CC=2 (base + if), ND=1, FO=1 (panic!), NS=2Implementation Details
The Rust parser is implemented in hotspots-core/src/language/rust/:
parser.rs- syn-based parser with full Rust supportcfg_builder.rs- Control Flow Graph builder- Metrics extracted in
hotspots-core/src/metrics.rs(extract_rust_metrics())
Unsupported Features
The following Rust features are not yet supported:
- Closures - Anonymous functions and closures
- If let / While let - Pattern matching in conditional expressions
- Async blocks - Async closures and blocks
These features will be added in future releases.
Error Handling
Parse errors in Rust files are handled gracefully:
- syn provides detailed error messages
- Functions with parse errors are skipped
- Analysis continues with remaining valid functions
- Error messages include line numbers and context
Java Language Support
Supported File Extensions
.java- Java source files
Supported Java Versions
Hotspots supports Java 8 through Java 21, including:
Java 8 Features:
- Lambda expressions
- Method references
- Stream API
- Default and static interface methods
- Try-with-resources
Java 11+ Features:
- Local variable type inference (
var) - Private interface methods
Java 14+ Features:
- Switch expressions
- Pattern matching for instanceof (preview)
Java 17+ Features:
- Sealed classes
- Pattern matching for switch (preview)
Java 21 Features:
- Record patterns
- Pattern matching for switch (full support)
Function Discovery
Java functions are discovered and analyzed as follows:
Methods:
- Instance methods
- Static methods
- Abstract methods (interface definitions)
- Default methods (interface implementations)
- Private interface methods
Constructors:
- Default constructors
- Parameterized constructors
- Constructor chaining (
this(),super())
Inner Classes:
- Non-static inner class methods
- Static nested class methods
- Anonymous inner class methods (each method analyzed separately)
- Local class methods
Not Analyzed:
- Interface method declarations (no body)
- Abstract method declarations (no body)
Control Flow Structures
Conditionals
If/Else:
- Standard if/else chains
- Each
ifadds +1 to CC - Nested conditions tracked for ND
Switch Statements (Traditional):
- Base +1 CC for switch
- Each
caselabel adds +1 CC defaultcase adds +1 CC- Fall-through behavior supported
Switch Expressions (Java 14+):
- Treated identically to traditional switch
- Base +1 CC
- Each case adds +1 CC
- Arrow (
->) and colon (:) syntax both supported
Ternary Operator:
condition ? true : falseadds +1 to CC- Nested ternary operators counted separately
Boolean Operators:
&&(logical AND) adds +1 to CC||(logical OR) adds +1 to CC- Short-circuit evaluation recognized
Loops
While Loop:
- Condition adds +1 to CC
- Loop body tracked for ND
breakandcontinuecounted as NS
Do-While Loop:
- Condition adds +1 to CC
- At least one iteration guaranteed
Traditional For Loop:
for (init; condition; update)adds +1 to CC- Complex conditions with
&&/||add additional CC
Enhanced For Loop:
for (Type item : collection)adds +1 to CC- Same complexity impact as while loop
Exception Handling
Try-Catch:
- Base +1 CC for try/catch construct
- Each
catchclause adds +1 to CC - Multiple catch clauses handled correctly
finallyblocks don't add to CC (always execute)
Try-With-Resources:
- Resource declarations add 0 CC
- Only catch clauses contribute to CC
- Example:
try (Scanner sc = ...) { } catch (IOException e) { }- CC +1 from catch only, resource doesn't count
Throw:
throwstatements counted as NS (non-structured exit)
Synchronization
Synchronized Blocks:
synchronized (obj) { }adds +1 to CC- Represents decision point for acquiring lock
- Critical section boundary
Synchronized Methods:
- Method-level
synchronizedkeyword adds +1 to CC
Java-Specific Constructs
Lambda Expressions
Lambdas are analyzed inline with their enclosing method:
list.forEach(item -> {
if (item > 5) { // This if adds +1 to enclosing method's CC
process(item);
}
});Design Decision: Lambdas don't create separate function entries. Control flow inside lambdas contributes to the parent method's metrics. This aligns with cognitive complexity principles - the complexity is experienced by the developer reading the method.
Stream API
Stream operations (filter, map, forEach, etc.) do not inflate CC:
items.stream()
.filter(x -> x > 5) // filter predicate counts
.map(x -> x * 2) // map function counts
.collect(Collectors.toList());Control flow inside lambda arguments to stream operations is counted, but the stream chain itself adds minimal complexity.
Anonymous Inner Classes
Methods inside anonymous inner classes are analyzed as separate functions:
Runnable r = new Runnable() {
@Override
public void run() { // Separate function entry
if (condition) {
doWork();
}
}
};Method Invocations
All method calls contribute to Fan-Out (FO):
- Instance method calls:
obj.method() - Static method calls:
ClassName.method() - Constructor calls:
new ClassName() - Super calls:
super.method()
Metric Calculation
Cyclomatic Complexity (CC)
Base formula: CC = E - N + 2 (from Control Flow Graph)
Additional contributions:
- Each
ifstatement: +1 - Each
while/do-while/forloop: +1 - Each
switchstatement: +1 (base) - Each
caselabel (includingdefault): +1 - Each
catchclause: +1 - Each
&&or||operator: +1 - Each ternary
? :expression: +1 - Each
synchronizedblock/method: +1
Example:
public String process(int x) {
if (x > 0 && x < 100) { // CC: +1 (if) +1 (&&) = +2
switch (x) {
case 1: // CC: +1 (switch base) +1 (case) = +2
return "one";
case 2: // CC: +1
return "two";
default: // CC: +1
return "other";
}
}
return "invalid";
}
// Total CC: 1 (base) + 2 (if + &&) + 4 (switch + cases) = 7Nesting Depth (ND)
Maximum depth of nested control structures:
if/elsewhile/do-while/forswitchtry/catchsynchronized
Example:
if (a) { // Depth 1
while (b) { // Depth 2
if (c) { // Depth 3
synchronized (obj) { // Depth 4
doWork();
}
}
}
}
// ND = 4Fan-Out (FO)
Count of unique method calls:
- Method invocations
- Constructor calls
- Static method calls
- Super calls
Example:
public void process() {
doA(); // FO +1
doB(); // FO +1
doA(); // Already counted, FO stays at 2
new Obj(); // FO +1
}
// Total FO = 3Non-Structured Exits (NS)
Count of exits that bypass normal control flow:
returnstatementsthrowstatementsbreakstatementscontinuestatements
Example:
public int exits(int x) {
if (x < 0) {
return 0; // NS +1
}
for (int i = 0; i < x; i++) {
if (i == 5) {
break; // NS +1
}
if (i == 3) {
continue; // NS +1
}
}
return x; // NS +1
}
// Total NS = 4Java-Specific Behavior
Try-With-Resources Does Not Inflate CC
Resource declarations in try-with-resources are not counted toward CC:
// CC = 3 (try/catch + 1 catch clause + final return)
try (BufferedReader br = new BufferedReader(...);
Scanner sc = new Scanner(...)) { // Resources don't add CC
return br.readLine();
} catch (IOException e) { // CC +1
return "";
}Rationale: Resource declarations don't represent decision points. They're deterministic initialization.
Switch Expressions vs Statements
Both traditional switch statements and modern switch expressions (Java 14+) are treated identically:
Traditional:
String result = switch (value) {
case 0:
result = "zero"; // CC +1
break;
case 1:
result = "one"; // CC +1
break;
default:
result = "other"; // CC +1
}
// Total CC: 1 (base) + 3 (cases) = 4Expression:
String result = switch (value) {
case 0 -> "zero"; // CC +1
case 1 -> "one"; // CC +1
default -> "other"; // CC +1
};
// Total CC: 1 (base) + 3 (cases) = 4Synchronized Adds to CC
Synchronized blocks represent a decision point (acquiring/waiting for lock):
synchronized (lock) { // CC +1
doWork();
}This aligns with the cognitive overhead of reasoning about concurrency and lock acquisition.
Limitations
Lambda Complexity:
- Lambdas with complex control flow contribute to parent method's metrics
- Very complex lambdas (>10 CC) may make parent appear more complex than it feels
- Consider refactoring complex lambdas into named methods
Stream Chains:
- Long stream chains with multiple lambdas don't inflate CC significantly
- FO counts individual method calls, not stream operations
- Deeply nested stream pipelines may have lower CC than equivalent imperative code
Anonymous Classes:
- Each anonymous class method is a separate function entry
- Can result in many small function entries in output
- Consider filtering output by minimum LRS to focus on complex methods
Generics:
- Type parameters don't affect metrics
- Generic method signatures treated like non-generic equivalents
Python Language Support
Supported File Extensions
Python:
.py- Python source files.pyw- Python GUI scripts (Windows)
Parser
Hotspots uses tree-sitter-python version 0.23 for parsing Python source files. Tree-sitter provides:
- Error-tolerant parsing
- Precise syntax tree representation
- Fast incremental parsing
Supported Features
Function Forms
All Python function forms are analyzed:
- Function declarations (
def name():) - Async functions (
async def name():) - Methods (instance, class, static)
- Nested functions
Note: Lambda expressions and closures are not yet supported.
Control Flow
All Python control flow constructs are fully supported:
Conditionals:
ifstatementsif/elif/elsechains- Ternary expressions (
x if condition else y)
Loops:
forloops (including async for)whileloops- List/dict/set comprehensions
- Generator expressions
breakandcontinuestatements
Exception Handling:
try/except/finallyblocks- Multiple except clauses
try/except/else/finally
Match Statements (Python 3.10+):
- Match expressions (
match value: case ...) - Pattern matching
- Guard clauses
Python-Specific Constructs
Context Managers (with statements):
withstatements are tracked for nesting depth (ND) only- Context managers do NOT contribute to cyclomatic complexity (CC)
- Rationale: Resource management is not branching logic
- Example:
with open(file) as f:increases ND but not CC
Comprehensions:
- List/dict/set comprehensions with if filters contribute to CC
- Comprehensions without filters do NOT contribute to CC
- Example:
[x for x in items if x > 0]increases CC by 1 - Example:
[x * 2 for x in items]does NOT increase CC
Async/Await:
- Async functions are analyzed like regular functions
async forandasync withare supported- Awaits contribute to fan-out (FO)
Boolean Operators:
andandoroperators each contribute +1 to CC- Example:
if a and b or c:increases CC by 2
Decorators:
- Decorators are parsed but do NOT affect complexity metrics
- Example:
@property,@staticmethod,@classmethod
Metric Calculation
Python metrics are calculated using the same principles as other languages, with Python-specific adaptations:
Cyclomatic Complexity (CC)
Base formula: CC = E - N + 2 (from Control Flow Graph)
Additional contributions:
- Each
elifclause: +1 - Each
exceptclause: +1 - Each match
case: +1 - Each boolean operator (
and,or): +1 - Each ternary expression: +1
- Each comprehension with
iffilter: +1
Important: Context managers (with statements) do NOT contribute to CC.
Example:
def process(x, y):
if x > 0 and y > 0: # CC: +1 (if) +1 (and) = +2
with open(file) as f: # CC: +0 (context manager)
data = f.read()
try:
result = parse(data)
except ValueError: # CC: +1
return None
except KeyError: # CC: +1
return {}
return result
# Total CC = 1 (base) + 1 (if) + 1 (and) + 2 (except clauses) = 5Nesting Depth (ND)
Maximum depth of nested control structures:
ifstatementsfor/whileloopstry/exceptblockswithstatementsmatchstatements
Example:
def nested():
if x > 0: # Depth 1
for item in items: # Depth 2
with open(f): # Depth 3
if cond: # Depth 4
match v: # Depth 5
case 0:
pass
# ND = 5Fan-Out (FO)
Count of unique function calls:
- Regular function calls
- Method calls
- Built-in function calls
Example:
def fan_out():
do_work() # FO: +1
do_work() # FO: +0 (duplicate)
do_other() # FO: +1
obj.method() # FO: +1
# Total FO = 3Non-Structured Exits (NS)
Count of exits that don't follow normal control flow:
returnstatements (excluding final tail return)raisestatementsbreakstatementscontinuestatements
Example:
def exits(items):
for item in items:
if item < 0:
continue # NS: +1
if item == 0:
raise ValueError # NS: +1
if item > 100:
return None # NS: +1
process(item)
return True # NS: +0 (final tail return)
# Total NS = 3Python-Specific Examples
Comprehensions with Filters
def filter_data(items):
# With filter - adds to CC
positive = [x for x in items if x > 0] # CC: +1
# Without filter - does NOT add to CC
doubled = [x * 2 for x in items] # CC: +0
# Total CC = 1 (base) + 1 (filtered comprehension) = 2Context Managers
def read_files(file1, file2):
with open(file1) as f1: # ND: +1, CC: +0
with open(file2) as f2: # ND: +2, CC: +0
data = f1.read() + f2.read()
return data
# CC=1 (base), ND=2, FO=2 (open calls), NS=0Exception Handling
def parse_data(text):
try:
value = int(text)
except ValueError: # CC: +1
return 0
except TypeError: # CC: +1
return -1
else:
return value
finally:
cleanup() # FO: +1 (cleanup call)
# CC=3 (base + 2 except), ND=1, FO=2 (int, cleanup), NS=2 (returns)Match Statements (Python 3.10+)
def handle_code(status):
match status:
case 200: # CC: +1
return "OK"
case 404: # CC: +1
return "Not Found"
case _: # CC: +1
return "Error"
# CC=4 (base + 3 cases), ND=1, FO=0, NS=3 (all returns)Boolean Operators
def check_conditions(a, b, c):
if a and b or c: # CC: +1 (if) +1 (and) +1 (or) = +3
return True
return False
# CC=4, ND=1, FO=0, NS=1 (early return)Implementation Details
The Python parser is implemented in hotspots-core/src/language/python/:
parser.rs- Tree-sitter-based parsercfg_builder.rs- Control Flow Graph builder- Metrics extracted in
hotspots-core/src/metrics.rs(extract_python_metrics())
Design Decisions
Why context managers don't add to CC: Context managers (with statements) are resource management constructs, not branching logic. They don't represent decision points or alternate execution paths. Including them in CC would artificially inflate complexity scores for resource-safe code.
Why comprehensions with filters add to CC: A comprehension with an if filter represents a conditional decision for each element. This is functionally equivalent to:
result = []
for x in items:
if condition: # This is a decision point
result.append(x)Why each except clause adds to CC: Each except clause represents a separate execution path based on the exception type. This is analogous to switch/match cases in other languages.
Unsupported Features
The following Python features are not yet supported:
- Lambda expressions - Anonymous functions
- Nested function definitions - Functions defined inside functions
- Walrus operator in complex contexts - Assignment expressions
These features will be added in future releases.
Error Handling
Parse errors in Python files are handled gracefully:
- Tree-sitter provides error-tolerant parsing
- Functions with parse errors are skipped
- Analysis continues with remaining valid functions
- Error messages indicate the failure point
Testing
Language support is validated with comprehensive test fixtures:
ECMAScript:
tests/fixtures/*.ts- TypeScript test casestests/fixtures/js/*.js- JavaScript equivalentstests/fixtures/tsx/*.tsx- TypeScript React componentstests/fixtures/jsx/*.jsx- JavaScript React components
Go:
tests/fixtures/go/simple.go- Basic functions and early returnstests/fixtures/go/loops.go- Loop variants and nestingtests/fixtures/go/switch.go- Switch statements and type switchestests/fixtures/go/go_specific.go- Defer, goroutines, select, panic/recovertests/fixtures/go/methods.go- Methods, interfaces, genericstests/fixtures/go/boolean_ops.go- Boolean operators and deep nesting
Rust:
tests/fixtures/rust/simple.rs- Basic functions, if/else, early returnstests/fixtures/rust/loops.rs- Loop variants (loop, while, for) and nestingtests/fixtures/rust/match.rs- Match expressions and pattern matchingtests/fixtures/rust/rust_specific.rs- ? operator, unwrap/expect, panictests/fixtures/rust/methods.rs- Methods, impl blocks, trait implementationstests/fixtures/rust/boolean_ops.rs- Boolean operators and complex conditions
Python:
tests/fixtures/python/simple.py- Basic functions and early returnstests/fixtures/python/loops.py- Loop variants (for, while, async for) and nestingtests/fixtures/python/exceptions.py- Exception handling with multiple except clausestests/fixtures/python/python_specific.py- Context managers, comprehensions, async, matchtests/fixtures/python/classes.py- Methods, decorators, async methodstests/fixtures/python/comprehensions.py- List/dict/set comprehensions with filterstests/fixtures/python/boolean_ops.py- Boolean operators and ternary expressions
Golden File Tests: All languages have golden file tests that verify deterministic output:
- ECMAScript: 6 golden tests
- Go: 5 golden tests (go-simple, go-loops, go-switch, go-specific, determinism)
- Python: 7 golden tests (python-simple, python-loops, python-exceptions, python-python_specific, python-classes, python-comprehensions, python-boolean_ops)
- Rust: 5 golden tests (rust-simple, rust-loops, rust-match, rust-specific, determinism)
Test Coverage:
- 221 total tests across all languages
- 100% pass rate
- Determinism verified across multiple runs
Future Support
Planned Languages
Priority: P1 (Popular languages)
- Java - Using tree-sitter-java
- C/C++ - Using tree-sitter-c/cpp
- Ruby - Using tree-sitter-ruby
- C# - Using tree-sitter-c-sharp
Planned Features
ECMAScript:
- Generator function analysis (
function*) - Vue Single File Components (
.vue) - Svelte components (
.svelte) - Angular component templates
Go:
- Anonymous function/closure support
- Function literals in expressions
Rust:
- Closure support
- If let / while let expressions
- Async blocks
All languages:
- Incremental parsing for performance
- Parallel analysis across files
Known Limitations
Control Flow
Break/Continue (partially supported)
- Counted toward NS — metric is accurate
- CFG routing to correct loop exit/header is simplified (does not affect LRS)
Labeled Break/Continue (supported, simplified)
- Counted and label-resolved statically
- Routing to the correct labeled target is not fully implemented
TypeScript/JavaScript Features
Generator Functions (function*) — not supported
- Functions with generators are skipped and an error is emitted
- Projects using generators will have incomplete analysis
Experimental Decorators — not supported
- Standard ES2022 decorators may work but are untested
Closures / anonymous functions (Go, Rust, Python) — not yet supported
- Named functions and methods are fully analyzed
- Future release planned
Analysis Scope
Async control flow (simplified)
- Async/await is parsed correctly but treated as sequential
- Promise chains are not analyzed as control flow
- LRS for async functions may be slightly underestimated
Type complexity — intentionally excluded
- Type annotations are not used in analysis
- Only structural control flow is analyzed
Output
Floating point precision
- Internal calculations use full
f64 - Results are deterministic within platform; may vary slightly across platforms
File path normalization
- Paths normalized to absolute paths; symlinks are not resolved
- Results may vary slightly on case-insensitive filesystems
Performance
Large codebases
- Analysis is sequential; no parallel file processing yet
- No incremental analysis — full re-analysis on every run
- Very large codebases (>10,000 files) may be slow