Reglas de Refactorización¶
complexipy incluye un sistema de refactorización inspirado en clippy que proporciona sugerencias accionables para reducir la complejidad cognitiva. Cada regla tiene un ID único, categoría y nivel de aplicabilidad.
Categorías de Reglas¶
| Categoría | Icono | Descripción |
|---|---|---|
| Complejidad | ▲ | Reglas que reducen directamente la complejidad cognitiva |
| Legibilidad | ◆ | Reglas que mejoran la legibilidad del código |
Niveles de Aplicabilidad¶
| Nivel | Icono | Descripción |
|---|---|---|
| Seguro de aplicar | * | Alta confianza en que el código generado es correcto tal cual -- todavía no se aplica automáticamente, es una señal de confianza, no una promesa de automatización |
| Necesita revisión | ! | Puede ser incorrecto en algunos casos, necesita revisión humana |
| Informativo | i | Solo orientación, no es directamente accionable |
Reglas de Complejidad¶
C001: Aplanar Condiciones Anidadas¶
- Categoría: ▲ Complejidad
- Aplicabilidad: i Informativo
- Prioridad: Alta (4/5)
Aplana bloques de condiciones anidadas usando cláusulas de guarda con retornos anticipados.
¿Cuándo se activa?¶
Esta regla se activa cuando una función tiene sentencias if profundamente anidadas (2+ niveles de anidamiento) que añaden complejidad significativa.
Ejemplo¶
Antes:
def process_data(data):
if data:
if data.is_valid():
if data.is_ready():
return process(data)
return None
Después:
def process_data(data):
if not data:
return None
if not data.is_valid():
return None
if not data.is_ready():
return None
return process(data)
Por qué esto ayuda¶
Las condiciones profundamente anidadas son difíciles de seguir. Usar cláusulas de guarda con retornos anticipados reduce la carga cognitiva al mantener la ruta principal en un nivel de indentación menor.
C002: Guardas de Bucles¶
- Categoría: ▲ Complejidad
- Aplicabilidad: * Seguro de aplicar
- Prioridad: Media (3/5)
Usa guardas continue al inicio de los bucles para reducir el anidamiento.
¿Cuándo se activa?¶
Esta regla se activa cuando un bucle contiene sentencias if anidadas que podrían convertirse en guardas continue anticipadas.
Ejemplo¶
Antes:
def process_items(items):
total = 0
for item in items:
if item.active:
if item.ready:
total += item.value
return total
Después:
def process_items(items):
total = 0
for item in items:
if not item.active:
continue
if not item.ready:
continue
total += item.value
return total
Por qué esto ayuda¶
Las condiciones anidadas dentro de los bucles añaden indentación innecesaria. Usar guardas continue mantiene la lógica principal en un nivel de anidamiento menor y hace que el bucle sea más fácil de seguir.
C003: Extraer Función Auxiliar¶
- Categoría: ▲ Complejidad
- Aplicabilidad: i Informativo
- Prioridad: Baja (2/5)
Extrae bloques de código complejos en funciones auxiliares separadas.
¿Cuándo se activa?¶
Esta regla se activa cuando un bloque de código tiene alta complejidad (6+) y abarca múltiples líneas (5+).
Ejemplo¶
Antes:
def process_order(order):
# Complex validation and processing logic
if order.items:
for item in order.items:
if item.quantity > 0:
if item.price > 0:
total = item.quantity * item.price
if total > 100:
apply_discount(total)
process_item(item)
Después:
def process_order(order):
if not order.items:
return
for item in order.items:
process_order_item(item)
def process_order_item(item):
if item.quantity <= 0 or item.price <= 0:
return
total = item.quantity * item.price
if total > 100:
apply_discount(total)
process_item(item)
Por qué esto ayuda¶
Los bloques de código complejos deben extraerse en funciones con nombre para mejorar la legibilidad y la capacidad de prueba. La función extraída puede recibir un nombre descriptivo que explique su propósito.
C004: Dividir Despachador¶
- Categoría: ▲ Complejidad
- Aplicabilidad: i Informativo
- Prioridad: Baja (2/5)
Divide cadenas largas de elif en manejadores separados.
¿Cuándo se activa?¶
Esta regla se activa cuando una cadena if/elif tiene 3+ ramas. Las sentencias match
se excluyen intencionalmente: el modelo de complejidad cognitiva de complexipy asigna a
un match un costo fijo sin importar cuántas cláusulas case tenga (a diferencia de
una cadena elif, donde cada rama adicional suma a la puntuación), así que dividir un
match en manejadores separados no reduciría realmente la complejidad medida.
El refactor sugerido depende de la forma de la cadena:
- Si todas las ramas comparan la misma variable contra un literal con
==, la cadena puede convertirse en una sentenciamatch-- esto se recomienda por encima de un diccionario de despacho, ya que no necesita indirección adicional y el propio modelo de complexipy la puntúa sin el costo por rama que tiene la cadenaelif. - En caso contrario (rangos, varias variables, comparaciones que no son de igualdad --
cualquier cosa que un simple
match case <valor>:no pueda expresar), se sugiere en su lugar un diccionario de despacho que asigna casos a funciones manejadoras.
Ejemplo: igualdad de una sola variable → match¶
Antes:
def handle_action(action):
if action == "create":
return create_resource()
elif action == "read":
return read_resource()
elif action == "update":
return update_resource()
elif action == "delete":
return delete_resource()
return None
Después:
def handle_action(action):
match action:
case "create":
return create_resource()
case "read":
return read_resource()
case "update":
return update_resource()
case "delete":
return delete_resource()
return None
Ejemplo: rangos → diccionario de despacho¶
Antes:
def classify(score):
if score < 60:
return "fail"
elif score < 70:
return "d"
elif score < 85:
return "b"
elif score < 95:
return "a"
return "a+"
Después:
def classify(score):
thresholds = [(60, "fail"), (70, "d"), (85, "b"), (95, "a")]
for limit, grade in thresholds:
if score < limit:
return grade
return "a+"
Por qué esto ayuda¶
Las cadenas condicionales largas son difíciles de mantener y extender. Dividirlas en manejadores separados -- una sentencia match cuando la cadena es un despacho simple por igualdad, un diccionario de despacho en caso contrario -- hace que cada caso sea independientemente testeable y la lógica de despacho más clara.
C011: Aplanar Try/Except¶
- Categoría: ▲ Complejidad
- Aplicabilidad: i Informativo
- Prioridad: Baja (2/5)
Aplana bloques try/except anidados combinándolos o reestructurándolos.
¿Cuándo se activa?¶
Esta regla se activa cuando los bloques try/except están anidados unos dentro de otros.
Ejemplo¶
Antes:
def read_config(path):
try:
with open(path) as f:
try:
return json.load(f)
except json.JSONDecodeError:
return default_config()
except FileNotFoundError:
return default_config()
Después:
def read_config(path):
try:
with open(path) as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return default_config()
Por qué esto ayuda¶
Los bloques try/except anidados son confusos y difíciles de mantener. Considera fusionarlos o extraer el bloque interno en una función separada con su propio manejo de errores.
Reglas de Legibilidad¶
C005: Extraer Predicado¶
- Categoría: ◆ Legibilidad
- Aplicabilidad: * Seguro de aplicar
- Prioridad: Baja (2/5)
Extrae condiciones booleanas complejas en funciones predicado con nombre.
¿Cuándo se activa?¶
Esta regla se activa cuando una condición booleana contiene 2+ operadores lógicos (and, or, not).
Ejemplo¶
Antes:
def is_eligible(user, order):
if (user.is_active and user.has_subscription) or (order.total > 100 and not order.is_gift):
return True
return False
Después:
def is_eligible(user, order):
return has_active_subscription(user) or is_qualifying_order(order)
def has_active_subscription(user):
return user.is_active and user.has_subscription
def is_qualifying_order(order):
return order.total > 100 and not order.is_gift
Por qué esto ayuda¶
Las expresiones booleanas complejas son difíciles de entender a simple vista. Extraerlas en predicados con nombre hace que el código sea autoexplicativo y más fácil de probar.
C007: Aplanar If Anidados¶
- Categoría: ◆ Legibilidad
- Aplicabilidad: * Seguro de aplicar
- Prioridad: Máxima (5/5)
Combina sentencias if anidadas en un único if con las condiciones combinadas.
¿Cuándo se activa?¶
Esta regla se activa cuando el cuerpo completo de una sentencia if es un único if anidado sin rama else, para una cadena de dos o más niveles de este tipo.
Ejemplo¶
Antes:
Después:
Por qué esto ayuda¶
Las sentencias if anidadas con un único cuerpo pueden combinarse en un único if con las condiciones unidas mediante and. Esto reduce el anidamiento y mejora la legibilidad.
Uso de las Reglas de Refactorización¶
Línea de Comandos¶
# Muestra sugerencias de refactorización
complexipy . --suggest-refactors
# Muestra sugerencias solo para funciones fallidas
complexipy . --failed --suggest-refactors
# Exporta sugerencias a JSON
complexipy . --output-format json --suggest-refactors
API de Python¶
from complexipy import code_complexity
code = """
def process(data):
if data:
if data.is_valid():
return process(data)
return None
"""
result = code_complexity(code)
for func in result.functions:
for plan in func.refactor_plans:
print(f"[{plan.rule_id}] {plan.title}")
print(f" Category: {plan.category}")
print(f" Applicability: {plan.applicability}")
print(f" Reduction: -{plan.estimated_reduction} complexity")
if plan.suggestion:
print(f" Suggested replacement:\n{plan.suggestion.replacement}")
elif plan.help:
print(f" Help: {plan.help}")
print(f" Docs: {plan.doc_url}")
Salida JSON¶
La salida JSON incluye todos los metadatos de las reglas para consumo programático:
{
"rule_id": "C007",
"kind": "collapsible_if",
"title": "Merge nested if statements",
"category": "Readability",
"applicability": "MachineApplicable",
"description": "Merge nested if statements into a single if with combined conditions",
"line_start": 3,
"line_end": 5,
"column_start": 5,
"current_complexity": 4,
"estimated_reduction": 1,
"estimated_complexity_after": 3,
"reduction_is_measured": true,
"suggestion": {
"replacement": " if data and data.is_valid():\n return process(data)",
"applicability": "MachineApplicable",
"spliceable": true,
"description": "Merge nested conditions into `if data and data.is_valid():`"
},
"help": null,
"explanation": "Nested if statements with a single body can be merged into a single if with combined conditions using 'and'. This reduces nesting and improves readability.",
"references": [],
"doc_url": "https://rohaquinlop.github.io/complexipy/refactoring-rules/#c007-collapsible-if"
}
Reducciones medidas vs estimadas¶
Cada plan informa cuánto reduce la complejidad al aplicarlo
(estimated_reduction / estimated_complexity_after) junto con un
indicador de confianza, reduction_is_measured:
- Medida (
true): la regla insertó su sugerencia en el código real, lo re-analizó y volvió a ejecutar el puntuador. El número es la respuesta literal a "aplica esto y vuelve a puntuar" — exactamente lo que reportan C002 y C007, que llevan reemplazos aplicables por máquina. La CLI los muestra sin calificador:Reduction: -2 complexity (7 -> 5). - Estimada (
false): el número proviene de la fórmula manual de la regla (reglas solo con ayuda C001, C003, C004, C011, y el fragmento C005, además de cualquier respaldo cuando una inserción no se puede re-analizar). La CLI los muestra con una tilde:Estimated reduction: ~-2 complexity (7 -> 5).
Una reducción medida de 0 significa que la sugerencia no reduce la complejidad realmente; el plan se descarta en lugar de mostrarse.
Referencia de IDs de Reglas¶
| ID | Nombre | Categoría | Aplicabilidad | Prioridad |
|---|---|---|---|---|
| C001 | Aplanar Condiciones Anidadas | ▲ Complejidad | i Informativo | Alta |
| C002 | Guardas de Bucles | ▲ Complejidad | * Seguro de aplicar | Media |
| C003 | Extraer Función Auxiliar | ▲ Complejidad | i Informativo | Baja |
| C004 | Dividir Despachador | ▲ Complejidad | i Informativo | Baja |
| C005 | Extraer Predicado | ◆ Legibilidad | * Seguro de aplicar | Baja |
| C007 | Aplanar If Anidados | ◆ Legibilidad | * Seguro de aplicar | Máxima |
| C011 | Aplanar Try/Except | ▲ Complejidad | i Informativo | Baja |