inspectr 0.0.4__py3-none-any.whl → 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
inspectr/complexity.py ADDED
@@ -0,0 +1,738 @@
1
+ #!/usr/bin/env python3
2
+ import ast
3
+ import sys
4
+ from dataclasses import dataclass, field
5
+ from collections import defaultdict
6
+ from typing import List, Dict, Optional, Any
7
+ from pathlib import Path
8
+ from colorama import init, Fore, Style
9
+
10
+ # initialize colorama for cross-platform colored output
11
+ init()
12
+
13
+ @dataclass
14
+ class Complexity:
15
+ expression: str
16
+ is_approximate: bool = False
17
+ details: List[str] = field(default_factory=list)
18
+
19
+ @classmethod
20
+ def constant(cls):
21
+ return cls("O(1)", False, [])
22
+
23
+ @classmethod
24
+ def linear(cls, coefficient: int = 1):
25
+ expr = f"O({coefficient}n)" if coefficient != 1 else "O(n)"
26
+ return cls(expr, False, [])
27
+
28
+ @classmethod
29
+ def approximate(cls, expr: str):
30
+ return cls(expr, True, [])
31
+
32
+ def with_detail(self, detail: str):
33
+ self.details.append(detail)
34
+ return self
35
+
36
+ def combine_sequential(self, other: 'Complexity') -> 'Complexity':
37
+ """For sequential operations, we add complexities"""
38
+ is_approx = self.is_approximate or other.is_approximate
39
+ details = self.details + other.details
40
+
41
+ # don't add O(1) unless both are O(1)
42
+ if self.expression == "O(1)" and other.expression == "O(1)":
43
+ expr = "O(2)"
44
+ elif self.expression == "O(1)":
45
+ expr = other.expression
46
+ elif other.expression == "O(1)":
47
+ expr = self.expression
48
+ elif self.expression == other.expression:
49
+ # Same complexity - combine coefficients
50
+ if self.expression == "O(n)":
51
+ expr = "O(2n)"
52
+ elif "O(" in self.expression and "n)" in self.expression:
53
+ # Extract coefficient
54
+ import re
55
+ match = re.match(r'O\((\d*)n\)', self.expression)
56
+ if match:
57
+ coef = int(match.group(1)) if match.group(1) else 1
58
+ expr = f"O({coef*2}n)"
59
+ else:
60
+ expr = f"{self.expression}+{other.expression}"
61
+ else:
62
+ expr = f"{self.expression}+{other.expression}"
63
+ elif "O(" in self.expression and "O(" in other.expression:
64
+ # try to combine similar terms
65
+ expr = self._combine_similar_terms(self.expression, other.expression)
66
+ if not expr:
67
+ expr = f"{self.expression}+{other.expression}"
68
+ else:
69
+ expr = f"{self.expression}+{other.expression}"
70
+
71
+ return Complexity(expr, is_approx, details)
72
+
73
+ def combine_nested(self, other: 'Complexity') -> 'Complexity':
74
+ """For nested operations, we multiply complexities"""
75
+ is_approx = self.is_approximate or other.is_approximate
76
+ details = self.details + other.details
77
+
78
+ if self.expression == "O(1)":
79
+ expr = other.expression
80
+ elif other.expression == "O(1)":
81
+ expr = self.expression
82
+ else:
83
+ expr = f"{self.expression}*{other.expression}"
84
+
85
+ return Complexity(expr, is_approx, details)
86
+
87
+ def max(self, other: 'Complexity') -> 'Complexity':
88
+ """Return the maximum complexity (for if/else branches)"""
89
+ # for branches, we should take the worse case
90
+ # this is a simplified comparison
91
+ self_weight = self._get_weight()
92
+ other_weight = other._get_weight()
93
+
94
+ if self_weight >= other_weight:
95
+ return self
96
+ else:
97
+ return other
98
+
99
+ def _combine_similar_terms(self, expr1: str, expr2: str) -> Optional[str]:
100
+ """Combine similar complexity terms with coefficients"""
101
+ import re
102
+
103
+ # parse expressions into a dict of complexity -> coefficient
104
+ terms = defaultdict(int)
105
+
106
+ for expr in [expr1, expr2]:
107
+ # split by + if present
108
+ parts = expr.split('+') if '+' in expr else [expr]
109
+ for part in parts:
110
+ part = part.strip()
111
+ if part == "O(1)":
112
+ terms["1"] += 1
113
+ elif part == "O(n)":
114
+ terms["n"] += 1
115
+ elif match := re.match(r'O\((\d+)\)', part):
116
+ terms["1"] += int(match.group(1))
117
+ elif match := re.match(r'O\((\d*)n\)', part):
118
+ coef = int(match.group(1)) if match.group(1) else 1
119
+ terms["n"] += coef
120
+ elif "n*log(n)" in part:
121
+ terms["n*log(n)"] += 1
122
+ elif "n²" in part:
123
+ terms["n²"] += 1
124
+ elif "n³" in part:
125
+ terms["n³"] += 1
126
+ else:
127
+ return None # can't parse, return None
128
+
129
+ # build result
130
+ result_parts = []
131
+ for complexity in ["n³", "n²", "n*log(n)", "n", "1"]:
132
+ if complexity in terms and terms[complexity] > 0:
133
+ coef = terms[complexity]
134
+ result_parts.append(f"O({coef if coef > 1 else ''}{complexity})")
135
+
136
+ return "+".join(result_parts) if result_parts else None
137
+
138
+
139
+ def _get_weight(self) -> int:
140
+ """Get a rough weight for complexity comparison"""
141
+ expr = self.expression
142
+ if 'n⁴' in expr or 'n^4' in expr:
143
+ return 4
144
+ elif 'n³' in expr or 'n^3' in expr:
145
+ return 3
146
+ elif 'n²' in expr or 'n^2' in expr:
147
+ return 2
148
+ elif 'n*log' in expr:
149
+ return 1.5
150
+ elif 'n' in expr:
151
+ return 1
152
+ elif 'log' in expr:
153
+ return 0.5
154
+ else:
155
+ return 0
156
+
157
+ def simplify(self) -> 'Complexity':
158
+ """Simplify the complexity expression WITHOUT reducing to dominant term"""
159
+ expr = self.expression
160
+
161
+ # first handle nested multiplications (convert to exponents)
162
+ if '*' in expr:
163
+ expr = self._simplify_multiplications(expr)
164
+
165
+ # then combine similar terms in additions
166
+ if '+' in expr:
167
+ expr = self._combine_additions(expr)
168
+
169
+ # clean up constants
170
+ if expr == "O(0)" or not expr:
171
+ expr = "O(1)"
172
+
173
+ return Complexity(expr, self.is_approximate, self.details)
174
+
175
+ def _simplify_multiplications(self, expr: str) -> str:
176
+ """Simplify multiplication expressions"""
177
+ import re
178
+
179
+ # handle O(n²)*O(n²) -> O(n⁴) etc
180
+ if "O(n²)*O(n²)" in expr:
181
+ expr = expr.replace("O(n²)*O(n²)", "O(n⁴)")
182
+ if "O(n³)*O(n)" in expr or "O(n)*O(n³)" in expr:
183
+ expr = expr.replace("O(n³)*O(n)", "O(n⁴)")
184
+ expr = expr.replace("O(n)*O(n³)", "O(n⁴)")
185
+ if "O(n²)*O(n)" in expr or "O(n)*O(n²)" in expr:
186
+ expr = expr.replace("O(n²)*O(n)", "O(n³)")
187
+ expr = expr.replace("O(n)*O(n²)", "O(n³)")
188
+
189
+ # count O(n) multiplications
190
+ n_count = expr.count("O(n)*O(n)")
191
+ if n_count > 0:
192
+ power_notation = {1: "²", 2: "⁴"}
193
+ if n_count in power_notation:
194
+ expr = expr.replace("O(n)*O(n)", f"O(n{power_notation[n_count]})", 1)
195
+
196
+ # count individual O(n) terms being multiplied
197
+ parts = []
198
+ current = ""
199
+ depth = 0
200
+
201
+ for char in expr:
202
+ if char == '(':
203
+ depth += 1
204
+ current += char
205
+ elif char == ')':
206
+ depth -= 1
207
+ current += char
208
+ elif char == '*' and depth == 0:
209
+ parts.append(current)
210
+ current = ""
211
+ else:
212
+ current += char
213
+
214
+ if current:
215
+ parts.append(current)
216
+
217
+ # count O(n) occurrences
218
+ n_count = sum(1 for p in parts if p == "O(n)")
219
+ other_parts = [p for p in parts if p != "O(n)" and p != "O(1)"]
220
+
221
+ if n_count >= 2:
222
+ power_notation = {2: "²", 3: "³", 4: "⁴"}
223
+ if n_count in power_notation:
224
+ result = f"O(n{power_notation[n_count]})"
225
+ else:
226
+ result = f"O(n^{n_count})"
227
+ # add other multiplicative factors
228
+ for part in other_parts:
229
+ if "log" in part:
230
+ result = result.replace(")", "*log(n))")
231
+ elif part and part != "O(1)":
232
+ result = f"{result}*{part}"
233
+
234
+ expr = result
235
+ elif n_count == 1:
236
+ # single O(n) with other factors
237
+ result = "O(n)"
238
+ for part in other_parts:
239
+ if "log" in part:
240
+ result = "O(n*log(n))"
241
+ elif part and part != "O(1)":
242
+ result = f"{result}*{part}"
243
+ expr = result
244
+
245
+ return expr
246
+
247
+ def _combine_additions(self, expr: str) -> str:
248
+ """Combine similar terms in additions"""
249
+ import re
250
+
251
+ parts = expr.split('+')
252
+ terms = defaultdict(int)
253
+
254
+ for part in parts:
255
+ part = part.strip()
256
+ if part == "O(1)":
257
+ terms["1"] += 1
258
+ elif match := re.match(r'O\((\d+)\)', part):
259
+ terms["1"] += int(match.group(1))
260
+ elif part == "O(n)":
261
+ terms["n"] += 1
262
+ elif match := re.match(r'O\((\d+)n\)', part):
263
+ terms["n"] += int(match.group(1))
264
+ elif "n*log(n)" in part:
265
+ terms["n*log(n)"] += 1
266
+ elif "log(n)" in part and "n*" not in part:
267
+ terms["log(n)"] += 1
268
+ elif "n²" in part or "n^2" in part:
269
+ terms["n²"] += 1
270
+ elif "n³" in part or "n^3" in part:
271
+ terms["n³"] += 1
272
+ elif "n⁴" in part or "n^4" in part:
273
+ terms["n⁴"] += 1
274
+ else:
275
+ # unknown term, keep as-is
276
+ if part and part != "O(1)":
277
+ terms[part] = 1
278
+
279
+ # build result with proper ordering
280
+ result_parts = []
281
+ order = ["n⁴", "n³", "n²", "n*log(n)", "n", "log(n)", "1"]
282
+
283
+ for complexity in order:
284
+ if complexity in terms and terms[complexity] > 0:
285
+ count = terms[complexity]
286
+ if complexity == "1":
287
+ if count == 1:
288
+ result_parts.append("O(1)")
289
+ result_parts.append(f"O({count})")
290
+ elif complexity == "n":
291
+ if count == 1:
292
+ result_parts.append("O(n)")
293
+ else:
294
+ result_parts.append(f"O({count}n)")
295
+ else:
296
+ if count == 1:
297
+ result_parts.append(f"O({complexity})")
298
+ else:
299
+ # for higher order terms, we don't usually show coefficient
300
+ # but we could if needed
301
+ result_parts.append(f"O({complexity})")
302
+
303
+ # add any unknown terms
304
+ for term, count in terms.items():
305
+ if term not in order and count > 0:
306
+ result_parts.append(term)
307
+
308
+ # remove O(1) terms if there are higher order terms
309
+ if len(result_parts) > 1:
310
+ result_parts = [p for p in result_parts if not (p == "O(1)" or re.match(r'O\(\d+\)', p))]
311
+ # actually, keep constant terms as per requirement
312
+ # revert this filtering
313
+ result_parts = []
314
+ for complexity in order:
315
+ if complexity in terms and terms[complexity] > 0:
316
+ count = terms[complexity]
317
+ if complexity == "1":
318
+ if count == 1:
319
+ result_parts.append("1")
320
+ else:
321
+ result_parts.append(str(count))
322
+ elif complexity == "n":
323
+ if count == 1:
324
+ result_parts.append("n")
325
+ else:
326
+ result_parts.append(f"{count}n")
327
+ else:
328
+ if count == 1:
329
+ result_parts.append(complexity)
330
+ else:
331
+ # for polynomial terms, coefficient usually not shown
332
+ result_parts.append(complexity)
333
+
334
+ if result_parts:
335
+ return "O(" + " + ".join(result_parts) + ")"
336
+ else:
337
+ return "O(1)"
338
+
339
+ @dataclass
340
+ class AntiPattern:
341
+ line: int
342
+ pattern_type: str
343
+ description: str
344
+
345
+ @dataclass
346
+ class FunctionAnalysis:
347
+ name: str
348
+ complexity: Complexity
349
+ anti_patterns: List[AntiPattern]
350
+
351
+ class Analyzer(ast.NodeVisitor):
352
+ def __init__(self):
353
+ self.functions: Dict[str, Complexity] = {}
354
+ self.variable_types: Dict[str, str] = {}
355
+ self.call_stack: List[str] = [] # Track call stack for recursion
356
+ self.max_call_depth = 3
357
+ self.current_class = None
358
+ self.results: List[FunctionAnalysis] = []
359
+ self.anti_patterns: List[AntiPattern] = []
360
+
361
+ def analyze_file(self, content: str, filename: str = "<file>") -> List[FunctionAnalysis]:
362
+ """Analyze Python file content"""
363
+ try:
364
+ tree = ast.parse(content, filename)
365
+ except SyntaxError as e:
366
+ raise ValueError(f"Parse error: {e}")
367
+
368
+ self.visit(tree)
369
+ return self.results
370
+
371
+ def visit_ClassDef(self, node: ast.ClassDef):
372
+ """Visit class definition"""
373
+ old_class = self.current_class
374
+ self.current_class = node.name
375
+ self.generic_visit(node)
376
+ self.current_class = old_class
377
+
378
+ def visit_FunctionDef(self, node: ast.FunctionDef):
379
+ """Visit function definition"""
380
+ self.anti_patterns = []
381
+ self.variable_types.clear()
382
+ self.call_stack = [] # reset call stack for each function
383
+
384
+ # extract type annotations from parameters
385
+ for arg in node.args.args:
386
+ if arg.annotation:
387
+ if isinstance(arg.annotation, ast.Subscript):
388
+ # handle List[int], Dict[str, int], etc.
389
+ if isinstance(arg.annotation.value, ast.Name):
390
+ self.variable_types[arg.arg] = arg.annotation.value.id
391
+ elif isinstance(arg.annotation, ast.Name):
392
+ self.variable_types[arg.arg] = arg.annotation.id
393
+
394
+ # analyze function body
395
+ complexity = self.analyze_body(node.body)
396
+
397
+ # store function name with class prefix if in a class
398
+ if self.current_class:
399
+ func_name = f"{self.current_class}.{node.name}"
400
+ else:
401
+ func_name = node.name
402
+
403
+ self.functions[func_name] = complexity
404
+
405
+ analysis = FunctionAnalysis(
406
+ name=func_name,
407
+ complexity=complexity,
408
+ anti_patterns=self.anti_patterns.copy()
409
+ )
410
+ self.results.append(analysis)
411
+
412
+ # don't visit nested functions
413
+ return
414
+
415
+ visit_AsyncFunctionDef = visit_FunctionDef
416
+
417
+ def analyze_body(self, body: List[ast.stmt]) -> Complexity:
418
+ """Analyze a list of statements"""
419
+ total = Complexity.constant()
420
+
421
+ # Track variable assignments for type inference
422
+ for stmt in body:
423
+ if isinstance(stmt, ast.Assign):
424
+ if isinstance(stmt.value, ast.Call):
425
+ if isinstance(stmt.value.func, ast.Name):
426
+ if stmt.value.func.id == 'set':
427
+ # Variable is assigned a set
428
+ for target in stmt.targets:
429
+ if isinstance(target, ast.Name):
430
+ self.variable_types[target.id] = 'set'
431
+ elif stmt.value.func.id == 'list':
432
+ for target in stmt.targets:
433
+ if isinstance(target, ast.Name):
434
+ self.variable_types[target.id] = 'list'
435
+
436
+
437
+ for stmt in body:
438
+ stmt_complexity = self.analyze_statement(stmt)
439
+ total = total.combine_sequential(stmt_complexity)
440
+
441
+ return total.simplify()
442
+
443
+ def analyze_statement(self, stmt: ast.stmt) -> Complexity:
444
+ """Analyze a single statement"""
445
+ if isinstance(stmt, ast.For):
446
+ return self.analyze_for_loop(stmt)
447
+ elif isinstance(stmt, ast.While):
448
+ return self.analyze_while_loop(stmt)
449
+ elif isinstance(stmt, ast.If):
450
+ return self.analyze_if(stmt)
451
+ elif isinstance(stmt, (ast.Return, ast.Expr)):
452
+ if isinstance(stmt, ast.Return) and stmt.value:
453
+ return self.analyze_expr(stmt.value)
454
+ elif isinstance(stmt, ast.Expr):
455
+ return self.analyze_expr(stmt.value)
456
+ return Complexity.constant()
457
+ elif isinstance(stmt, (ast.Assign, ast.AugAssign, ast.AnnAssign)):
458
+ if isinstance(stmt, ast.Assign):
459
+ return self.analyze_expr(stmt.value)
460
+ elif isinstance(stmt, ast.AugAssign):
461
+ return self.analyze_expr(stmt.value)
462
+ elif isinstance(stmt, ast.AnnAssign) and stmt.value:
463
+ return self.analyze_expr(stmt.value)
464
+ return Complexity.constant()
465
+ else:
466
+ return Complexity.constant()
467
+
468
+ def analyze_for_loop(self, node: ast.For) -> Complexity:
469
+ """Analyze for loop complexity"""
470
+ iter_complexity = self.estimate_iteration_count(node.iter)
471
+ body_complexity = self.analyze_body(node.body)
472
+
473
+ combined = iter_complexity.combine_nested(body_complexity)
474
+ return combined.with_detail("for loop").simplify()
475
+
476
+ def analyze_while_loop(self, node: ast.While) -> Complexity:
477
+ """Analyze while loop complexity"""
478
+ body_complexity = self.analyze_body(node.body)
479
+
480
+ # while loops have unknown iterations in static analysis
481
+ if body_complexity.expression == "O(1)":
482
+ result = Complexity.linear(1)
483
+ else:
484
+ result = body_complexity.combine_nested(Complexity.linear(1))
485
+ result.is_approximate = True
486
+
487
+ return result.with_detail("while loop (unknown iterations)").simplify()
488
+
489
+ def analyze_if(self, node: ast.If) -> Complexity:
490
+ """Analyze if statement - take max of branches"""
491
+ if_body = self.analyze_body(node.body)
492
+ else_body = self.analyze_body(node.orelse)
493
+ return if_body.max(else_body)
494
+
495
+ def estimate_iteration_count(self, iter_node: ast.expr) -> Complexity:
496
+ """Estimate iteration count for loop iterator"""
497
+ if isinstance(iter_node, ast.Call):
498
+ if isinstance(iter_node.func, ast.Name):
499
+ if iter_node.func.id == 'range':
500
+ return Complexity.linear(1)
501
+ elif iter_node.func.id in ('enumerate', 'zip'):
502
+ # these iterate over their arguments
503
+ if iter_node.args:
504
+ return self.estimate_iteration_count(iter_node.args[0])
505
+ return Complexity.linear(1)
506
+ return Complexity.approximate("O(n)")
507
+ elif isinstance(iter_node, (ast.Name, ast.Attribute)):
508
+ return Complexity.linear(1)
509
+ elif isinstance(iter_node, (ast.List, ast.Tuple)):
510
+ # literal list/tuple - count elements
511
+ return Complexity.linear(1)
512
+ else:
513
+ return Complexity.approximate("O(n)")
514
+
515
+ def analyze_expr(self, expr: ast.expr) -> Complexity:
516
+ """Analyze expression complexity"""
517
+ if isinstance(expr, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
518
+ return self.analyze_comprehension(expr)
519
+ elif isinstance(expr, ast.Call):
520
+ return self.analyze_call(expr)
521
+ elif isinstance(expr, ast.Compare):
522
+ return self.analyze_compare(expr)
523
+ elif isinstance(expr, ast.Lambda):
524
+ # analyze lambda body as expression
525
+ return self.analyze_expr(expr.body)
526
+ elif isinstance(expr, ast.List):
527
+ total = Complexity.constant()
528
+ for el in expr.elts:
529
+ total = total.combine_sequential(self.analyze_expr(el))
530
+ return total
531
+ else:
532
+ return Complexity.constant()
533
+
534
+ def analyze_comprehension(self, node) -> Complexity:
535
+ """Analyze list/set/dict comprehension or generator expression"""
536
+ complexity = Complexity.constant()
537
+
538
+ for generator in node.generators:
539
+ iter_complexity = self.estimate_iteration_count(generator.iter)
540
+ complexity = complexity.combine_nested(iter_complexity)
541
+
542
+ # check for anti-pattern (using list comp where generator would work)
543
+ if isinstance(node, ast.ListComp):
544
+ self.anti_patterns.append(AntiPattern(
545
+ line=node.lineno,
546
+ pattern_type="list_comprehension",
547
+ description="List comprehension could potentially be replaced with generator expression for memory efficiency"
548
+ ))
549
+
550
+ return complexity.with_detail("comprehension").simplify()
551
+
552
+ def analyze_call(self, node: ast.Call) -> Complexity:
553
+ """Analyze function call complexity"""
554
+ if isinstance(node.func, ast.Name):
555
+ func_name = node.func.id
556
+
557
+ # built-in functions with known complexity
558
+ complexity_map = {
559
+ 'len': Complexity.constant(),
560
+ 'print': Complexity.constant(),
561
+ 'sum': Complexity.linear(1),
562
+ 'max': Complexity.linear(1),
563
+ 'min': Complexity.linear(1),
564
+ 'sorted': Complexity("O(n*log(n))", False),
565
+ 'reversed': Complexity.linear(1),
566
+ 'enumerate': Complexity.constant(),
567
+ 'zip': Complexity.constant(),
568
+ 'map': Complexity.constant(),
569
+ 'filter': Complexity.constant(),
570
+ 'list': Complexity.linear(1) if node.args else Complexity.constant(),
571
+ 'set': Complexity.linear(1) if node.args else Complexity.constant(),
572
+ 'dict': Complexity.constant(),
573
+ 'all': Complexity.linear(1),
574
+ 'any': Complexity.linear(1),
575
+ }
576
+
577
+ if func_name in complexity_map:
578
+ complexity = complexity_map[func_name]
579
+ elif func_name in self.functions:
580
+ # check recursion depth
581
+ if func_name in self.call_stack:
582
+ # already in call stack - check depth
583
+ depth = self.call_stack.count(func_name)
584
+ if depth >= self.max_call_depth:
585
+ known_complexity = self.functions.get(func_name, Complexity.approximate("O(?)"))
586
+ complexity = Complexity.approximate(f"at least {known_complexity.expression}")
587
+ complexity.with_detail(f"(maximum recursion depth {self.max_call_depth} reached)")
588
+ else:
589
+ # allow the recursive call but track it
590
+ self.call_stack.append(func_name)
591
+ complexity = self.functions[func_name]
592
+ self.call_stack.pop()
593
+ else:
594
+ # first call to this function
595
+ self.call_stack.append(func_name)
596
+ complexity = self.functions[func_name]
597
+ self.call_stack.pop()
598
+ else:
599
+ complexity = Complexity.approximate("O(?)")
600
+ complexity.with_detail(f"unknown function: {func_name}")
601
+
602
+ # analyze arguments
603
+ for arg in node.args:
604
+ arg_complexity = self.analyze_expr(arg)
605
+ complexity = complexity.combine_sequential(arg_complexity)
606
+
607
+ return complexity
608
+
609
+ elif isinstance(node.func, ast.Attribute):
610
+ return self.analyze_method_call(node.func)
611
+ else:
612
+ return Complexity.approximate("O(?)")
613
+
614
+ def analyze_method_call(self, attr: ast.Attribute) -> Complexity:
615
+ """Analyze method call complexity"""
616
+ method_complexity = {
617
+ 'append': Complexity.constant(),
618
+ 'pop': Complexity.constant(),
619
+ 'insert': Complexity.linear(1),
620
+ 'remove': Complexity.linear(1),
621
+ 'sort': Complexity("O(n*log(n))", False),
622
+ 'index': Complexity.linear(1),
623
+ 'count': Complexity.linear(1),
624
+ 'extend': Complexity.linear(1),
625
+ 'copy': Complexity.linear(1),
626
+ 'clear': Complexity.constant(),
627
+ 'get': Complexity.constant(),
628
+ 'items': Complexity.linear(1),
629
+ 'keys': Complexity.linear(1),
630
+ 'values': Complexity.linear(1),
631
+ 'update': Complexity.linear(1),
632
+ 'add': Complexity.constant(), # set.add
633
+ 'discard': Complexity.constant(), # set.discard
634
+ 'union': Complexity.linear(1),
635
+ 'intersection': Complexity.linear(1),
636
+ 'difference': Complexity.linear(1),
637
+ }
638
+
639
+ return method_complexity.get(attr.attr, Complexity.approximate("O(?)")).simplify()
640
+
641
+ def analyze_compare(self, node: ast.Compare) -> Complexity:
642
+ """Analyze comparison operations"""
643
+ for i, op in enumerate(node.ops):
644
+ if isinstance(op, (ast.In, ast.NotIn)):
645
+ if i < len(node.comparators):
646
+ comparator = node.comparators[i]
647
+
648
+ if isinstance(comparator, ast.List):
649
+ # list literal membership test - always O(n)
650
+ self.anti_patterns.append(AntiPattern(
651
+ line=node.lineno,
652
+ pattern_type="list_membership",
653
+ description="Membership test with list literal - use set for O(1) lookup instead of O(n)"
654
+ ))
655
+ return Complexity.linear(1).with_detail("list membership check")
656
+
657
+ elif isinstance(comparator, ast.Set):
658
+ # set literal membership test - O(1)
659
+ return Complexity.constant().with_detail("set membership check")
660
+
661
+ elif isinstance(comparator, ast.Name):
662
+ # check variable type if known
663
+ var_type = self.variable_types.get(comparator.id)
664
+ if var_type in ('List', 'list'):
665
+ self.anti_patterns.append(AntiPattern(
666
+ line=node.lineno,
667
+ pattern_type="membership_check",
668
+ description="Membership test with List type - consider using Set for O(1) lookup instead of O(n)"
669
+ ))
670
+ return Complexity.linear(1).with_detail("list membership check")
671
+ elif var_type in ('Set', 'set', 'Dict', 'dict'):
672
+ return Complexity.constant().with_detail("set/dict membership check")
673
+ else:
674
+ # unknown type - assume worst case (list)
675
+ self.anti_patterns.append(AntiPattern(
676
+ line=node.lineno,
677
+ pattern_type="membership_check",
678
+ description="Membership test with unknown type - if this is a list, consider using set for O(1) lookup"
679
+ ))
680
+ return Complexity.approximate("O(n)").with_detail("membership check (unknown type)")
681
+
682
+ elif isinstance(comparator, ast.Attribute):
683
+ # accessing an attribute - assume list for worst case
684
+ self.anti_patterns.append(AntiPattern(
685
+ line=node.lineno,
686
+ pattern_type="membership_check",
687
+ description="Membership test with unknown type - if this is a list, consider using set for O(1) lookup"
688
+ ))
689
+ return Complexity.approximate("O(n)").with_detail("membership check (unknown type)")
690
+
691
+ return Complexity.constant()
692
+
693
+ def main(files: List[Path], **kwargs) -> None:
694
+ for file_path in files:
695
+ if not file_path.exists():
696
+ print(f"{Fore.RED}{Style.BRIGHT}Error:{Style.RESET_ALL} File does not exist: {file_path}")
697
+ continue
698
+
699
+ if not file_path.is_file():
700
+ print(f"{Fore.RED}{Style.BRIGHT}Error:{Style.RESET_ALL} Not a file: {file_path}")
701
+ continue
702
+
703
+ try:
704
+ content = file_path.read_text()
705
+ except Exception as e:
706
+ print(f"{Fore.RED}{Style.BRIGHT}Error:{Style.RESET_ALL} Failed to read file: {e}")
707
+ continue
708
+
709
+ analyzer = Analyzer()
710
+
711
+ try:
712
+ results = analyzer.analyze_file(content, str(file_path))
713
+ except ValueError as e:
714
+ print(f"{Fore.RED}{Style.BRIGHT}Analysis failed:{Style.RESET_ALL} {e}")
715
+ continue
716
+
717
+ print(f"\n{Fore.CYAN}{'═' * 80}{Style.RESET_ALL}")
718
+ print(f"{Fore.CYAN}{Style.BRIGHT} Python Complexity Analysis: {file_path}{Style.RESET_ALL}")
719
+ print(f"{Fore.CYAN}{'═' * 80}{Style.RESET_ALL}\n")
720
+
721
+ has_approximate = any(r.complexity.is_approximate for r in results)
722
+
723
+ for analysis in results:
724
+ approx_marker = " *" if analysis.complexity.is_approximate else ""
725
+
726
+ print(f"{Fore.GREEN}{Style.BRIGHT}Function/Method:{Style.RESET_ALL} {Fore.YELLOW}{analysis.name}{Style.RESET_ALL}")
727
+ print(f" {Fore.BLUE}{Style.BRIGHT}Complexity:{Style.RESET_ALL} {Fore.MAGENTA}{analysis.complexity.expression}{Style.RESET_ALL}{Fore.RED}{Style.BRIGHT}{approx_marker}{Style.RESET_ALL}")
728
+
729
+ if analysis.anti_patterns:
730
+ print(f" {Fore.YELLOW}{Style.BRIGHT}⚠ Performance Issues:{Style.RESET_ALL}")
731
+ for ap in analysis.anti_patterns:
732
+ print(f" {Fore.RED}•{Style.RESET_ALL} [line {ap.line}]: {Fore.YELLOW}{ap.description}{Style.RESET_ALL}")
733
+
734
+ print()
735
+
736
+ if has_approximate:
737
+ print(f"{Fore.YELLOW}{Style.BRIGHT}Note:{Style.RESET_ALL} {Fore.RED}{Style.BRIGHT}*{Style.RESET_ALL} Complexity marked with * is approximate due to static analysis limitations")
738
+