Releases: pfranccino/android-gradle-analyzer
Releases · pfranccino/android-gradle-analyzer
v1.7.0
Added
- Salida JSON estructurada en las 4 herramientas (
gradle-analyzer,gradle-externals,gradle-impact,gradle-sanity): nuevo--format json—y opción JSON en el selector de formato y en la exportación del menú— que escribe un.jsonconschema_versionestable, pensado para que una skill o script lo consuma y lo formatee. El JSON de internas respeta el foco y--depth(el árbol enraizado); externas incluye el cono de llamadores por nivel (caller_cone)
Fixed
- El resumen en pantalla (y los exports) ignoraba el foco.
run_internalgeneraba los archivos con el foco pero armaba el resumen sin él, así que en el menú "mostraba todo" el grafo. Ahora el resumen respeta el foco igual que los archivos - Timer "congelado" con el motor dinámico/auto. El spinner y la barra seguían
sys.stdout, que el análisis redirige a un buffer para capturar logs; sus frames se iban al buffer y el timer no avanzaba en pantalla. Ahora fijan suConsoleal stdout real - Prompt de profundidad simplificado: quedan "Todas (hasta las hojas)" y "1 (solo directas)"; se quitan las opciones fijas "2" y "3" (para una profundidad puntual está
--depth Npor CLI)
v1.6.0
Changed
- Dependencias internas ahora es un árbol enraizado en el módulo elegido. Al enfocar un módulo (ej.
customer:customer-account-recovery) la salida muestra solo lo que ese módulo usa, recursivamente (clausura hacia abajo), en vez de incluir a sus llamadores y volcar toda la lista de dependencias deapp. "Quién me llama" pasa a ser exclusivamente la función de Llamadas externas. Como el conjunto es cerrado bajo "depende de", la vista nunca arrastra módulos ajenos al foco - El reporte ASCII de internas se renderiza como árbol anidado real (antes era plano de un nivel), con dedup de subárboles repetidos (
↩) y corte de ciclos - Llamadas externas ahora es simétrico a internas: el cono transitivo de llamadores. Antes mostraba solo los llamadores directos (1 salto); ahora responde "de qué parte del proyecto te llaman" recursivamente. Una sola resolución del grafo deriva los llamadores directos (con scope, como antes) y el cono transitivo por niveles; el reporte agrega la sección "Cadena de llamadores"
- Sanidad enfocada muestra ambas direcciones. Al enfocar un módulo (o un directorio), el reporte agrega la sección "Dependencias del foco — ambas direcciones": para cada módulo del foco, quién lo llama (Ca) y a quién llama (Ce), medido sobre el grafo completo. Las métricas Ca/Ce/I siguen calculándose en el contexto del proyecto entero
Added
- Flag
--depth N|allengradle-analyzerygradle-externals(y selector de profundidad en el menú): limita cuántos niveles se recorren — las "internas de sus internas" hacia abajo, y el cono de llamadores hacia arriba (default:all)
Fixed
- Motor dinámico: explosión de dependencias por flavor/buildType. El init script recorría todas las configuraciones, incluidas las resolvables que AGP deriva (
*CompileClasspath/*RuntimeClasspath), que heredan las deps de los buckets declarables y reportaban la misma arista una vez por variante (en un proyecto con 4 flavors, ×5 por dependencia). Ahora se inspeccionan solo configuraciones declarables (cfg.canBeResolveddescarta las resolvables), con red de seguridad en_normalize_raw - Barra de progreso clavada en 0% con motor dinámico/auto. El motor dinámico hace todo el trabajo en una sola llamada bloqueante a Gradle antes de poder reportar progreso, así que la barra determinada se quedaba en
0/Ndurante los minutos de configuración y saltaba a 100% al final. Para dinámico/auto ahora se usa un spinner indeterminado y un aviso de que Gradle puede tardar varios minutos
v1.5.0
Added
- Foco + contexto completo en las 4 funciones: ahora se puede apuntar
gradle-analyzer,gradle-sanity,gradle-externalsygradle-impacta un módulo, un subárbol o el proyecto entero, y el análisis se hace siempre en relación al proyecto completo. El grafo de dependencias se construye desde la raíz (nombres canónicos) y la salida se centra en lo que se elige. Antes, apuntar a una subcarpeta analizaba ese subárbol de forma aislada y distorsionaba las métricas.- Sanidad acepta
--focus MODULE[,MODULE](y un selector de módulo en el menú): el reporte y el score se centran en el módulo elegido, pero Ca/Ce/I se miden en el contexto del proyecto completo. El acoplamiento entrante (Ca) ahora cuenta a los llamadores externos al subárbol (ej.app); antes los perdía e inflaba la inestabilidadI(y con ella SDP, fan-out y la detección de lógica mal ubicada) - Dependencias internas enfocadas muestran el módulo "en contexto": lo que usa (downstream transitivo) y quién lo llama (llamadores directos)
- Llamadas externas e impacto ahora funcionan también si se apunta a una subcarpeta: detectan la raíz y encuentran los llamadores/dependientes externos al subárbol
- Nuevo helper
compute_scope(base_path) -> (raíz, known_modules, focus_modules)que unifica la detección de raíz y foco para las 4 funciones. Nuevos tests:TestComputeScope,TestSanityFocusInContext, vista enfocada con llamadores y subcarpeta en externas/impacto
- Sanidad acepta
Fixed
- "No detecta nada" en proyectos con
includemultilínea: el parser desettings.gradle(.kts)leía línea por línea y descartaba toda línea sin la palabrainclude, por lo que el formato Kotlin DSL moderno —include(…":app",…":core",…)— devolvía 0 módulos. Y como elsettings.gradlesí existía pero quedaba vacío, el análisis no caía al escaneo por carpetas y reportaba cero dependencias. Ahora_extract_includesune statements lógicos (paréntesis abiertos + coma de continuación Groovy) y elimina comentarios antes de matchear. Cubre:include()multilínea con y sin coma final, listas Groovy multilínea,listOf(...).forEach { include(it) }, varios módulos porinclude(...), semicolons, CRLF/tabs einclude (...)con espacio - Falsos positivos en la detección de módulos: ahora la detección exige una llamada real a
include(la palabra seguida de(o comilla), no un substring. Con esto se evita contar como módulos: dependencias dentro de bloques comentados/* ... */, identificadores comoval includedFeatures = ...,includeBuild, la palabraincludeembebida en un string (ej.rootProject.name = "include-this"), y strings de sentencias vecinas pegadas con;en la misma línea (ej.include(":a"); rootProject.name = "x"o unproject(...).projectDir = file(...)a continuación) - Nombres canónicos al analizar un subárbol: al apuntar
gradle-analyzera una subcarpeta del proyecto (ej.customer/) los módulos se nombraban relativos a esa carpeta (customer-account-recovery) en vez de su nombre completo (customer:customer-account-recovery). Como las dependencias declaradas usan el nombre completo (project(":customer:...")), los edges internos del subárbol se descartaban y el grafo salía vacío ("no detecta nada"). Ahora los módulos se nombran siempre relativos a la raíz del proyecto (donde estásettings.gradle) y las dependencias se resuelven desde la raíz (ver "foco + contexto completo" en Added) - Fallback robusto cuando
settings.gradleno declara módulos parseables:parse_settings_modulesdevuelveNone(en vez de lista vacía) para que los tres analizadores (gradle-analyzer,gradle-impact,gradle-externals) caigan al escaneo por carpetas en lugar de reportar "0 módulos" silenciosamente - Nueva clase de test
TestExtractIncludesFormats(24 casos) sobre los formatos Kotlin DSL y Groovy desettings.gradle: include multilínea, listas,forEach, comentarios (línea/bloque/inline),;, condicionalesif, interpolación no resoluble, BOM, CRLF, y un settings a escala de monorepo coninclude("...")con paréntesis, sin:inicial y anidamiento profundo
Changed
- Documentación: README reescrito y reducido (~270 líneas) con salida real de los CLIs y un diagrama Mermaid que GitHub renderiza en vivo. El detalle (referencia de comandos, métricas de sanidad, motores, configuración, CI, internals y troubleshooting) se movió al Wiki como única fuente de verdad.
EXAMPLES.mdactualizado a los CLIs actuales con salida real y reproducible contratests/fixtures/
Removed
- Imágenes
docs/preview.svgydocs/preview-menu.svg(maquetas dibujadas a mano y desactualizadas; reemplazadas por salida real y Mermaid en vivo en el README)
v1.4.1
Fixed
- "No detecta nada" en proyectos con
includemultilínea: el parser desettings.gradle(.kts)leía línea por línea y descartaba toda línea sin la palabrainclude, por lo que el formato Kotlin DSL moderno —include(…":app",…":core",…)— devolvía 0 módulos. Y como elsettings.gradlesí existía pero quedaba vacío, el análisis no caía al escaneo por carpetas y reportaba cero dependencias. Ahora_extract_includesune statements lógicos (paréntesis abiertos + coma de continuación Groovy) y elimina comentarios antes de matchear. Cubre:include()multilínea con y sin coma final, listas Groovy multilínea,listOf(...).forEach { include(it) }, varios módulos porinclude(...), semicolons, CRLF/tabs einclude (...)con espacio - Falsos positivos en la detección de módulos: dependencias dentro de bloques comentados
/* ... */se contaban como módulos, e identificadores comoval includedFeatures = ...también (matcheo por substringinclude). Ahora se usa\binclude\b, que excluye ademásincludeBuildde forma natural - Fallback robusto cuando
settings.gradleno declara módulos parseables:parse_settings_modulesdevuelveNone(en vez de lista vacía) para que los tres analizadores (gradle-analyzer,gradle-impact,gradle-externals) caigan al escaneo por carpetas en lugar de reportar "0 módulos" silenciosamente - Nueva clase de test
TestExtractIncludesFormats(13 casos) sobre los formatos Kotlin DSL y Groovy desettings.gradle, incluyendo los antifalsos-positivos y un settings a escala de monorepo coninclude("...")con paréntesis, sin:inicial y anidamiento profundo
Changed
- Documentación: README reescrito y reducido (~270 líneas) con salida real de los CLIs y un diagrama Mermaid que GitHub renderiza en vivo. El detalle (referencia de comandos, métricas de sanidad, motores, configuración, CI, internals y troubleshooting) se movió al Wiki como única fuente de verdad.
EXAMPLES.mdactualizado a los CLIs actuales con salida real y reproducible contratests/fixtures/
Removed
- Imágenes
docs/preview.svgydocs/preview-menu.svg(maquetas dibujadas a mano y desactualizadas; reemplazadas por salida real y Mermaid en vivo en el README)
v1.4.0
Added
- Motor dinámico de extracción de dependencias (
--engine dynamic): ejecutagradlew -I <init script>y lee las dependenciasproject(...)que Gradle resuelve del modelo de proyecto, en vez de parsear texto. Precisión total: soporta Version Catalogs, variables, accessors type-safe y convention plugins sin actualizar el parser. Extrae dependencias declaradas directas por configuración (no resuelve el grafo transitivo), por lo que no necesita red ni compilar - Modo
--engine auto: usa el motor dinámico si hay wrapper de Gradle (gradlew) y la corrida tiene éxito; si no está disponible o falla, cae al motor estático con una advertencia. Mismo patrón de fallback que tree-sitter/regex - Nuevo módulo
dependency_engine.pyconStaticEngine/DynamicEngine/AutoEnginedetrás de una interfaz comúnresolve(project_root, modules, known_modules). Los cuatro análisis (gradle-analyzer,gradle-impact,gradle-externals,gradle-sanity) aceptan--engine static|dynamic|auto(defaultstatic), configurable también poranalyzer.yml(engine:por sección) - El menú interactivo incluye un paso de selección de motor en cada análisis
- Nueva clase de test
TestDependencyEngine*(18 tests): paridad del estático, normalización/filtrado del JSON dinámico, detección del wrapper y fallback de auto
Changed
- Los tres analizadores (
gradle_analyzer,gradle_impact,external_callers) ygradle_sanitydelegan ahora la extracción de dependencias endependency_engineen lugar de llamar aparse_gradle_file_scopeddirectamente. El motor estático reproduce el comportamiento previo byte a byte (sin regresión: 89 tests existentes en verde)
Security
- El motor dinámico ejecuta el build del proyecto analizado (settings, plugins, convention plugins). Es opt-in (
staticsigue siendo el default) y solo debe usarse sobre repos de confianza. El motor estático solo lee texto y es seguro para repos no confiables
v1.3.2
Fixed
- Falsos positivos en
.gradle.ktssin tree-sitter: el fallback de regex para Kotlin DSL no eliminaba comentarios, por lo que dependencias comentadas (// implementation(project(":x"))o bloques/* ... */) se reportaban como llamadas reales en el análisis de dependencias y en la búsqueda de llamadas externas. Ahora el fallback aplica_strip_comments, consistente con el path Groovy (_preprocess_groovy) y el de accessors. Afecta solo a la instalación por defecto (sin el extrakts); contree-sitter-kotlininstalado el comportamiento ya era correcto - Nueva clase de test
TestKtsRegexFallback: fuerza el camino sin tree-sitter (parchando_parse_kts_project_calls) para cubrir esta regresión siempre. Los tests previos de comentarios en KTS se saltaban cuando tree-sitter no estaba instalado — justo el escenario donde vivía el bug
v1.3.1
Changed
- Distribución por PyPI: la herramienta ahora se instala con
pipx install android-gradle-analyzer(antes sologit+https://...). El install desdegit+se mantiene documentado como vía de desarrollo - README y CONTRIBUTING actualizados: el flujo de desarrollo usa
pip install -e ".[kts,yaml]"y el de release ya no crea el tag a mano (lo hace el CI)
Added
- Publicación automática a PyPI vía Trusted Publishers (OIDC) en
release.yml: el jobpublish-pypisube el paquete sin tokens ni contraseñas, gateado a que el jobreleasehaya creado un tag nuevo (no publica en pushes sin bump de versión)
Removed
setup.shyrequirements.txt:pyproject.tomlqueda como fuente única de dependencias
v1.3.0
Added
- Detector de "lógica compartida mal ubicada" en
gradle-sanity: identifica un módulo hoja (feature o app —Ialto, en la punta del grafo de dependencias) del que sin embargo otros módulos dependen (Capor encima del límite). Suele indicar código común atrapado en un feature en vez de estar encore/shared. Se basa solo en las métricas que el tool ya calcula (IyCa) — no depende de nombres de módulo ni de plugins, ycore/commonquedan excluidos automáticamente por tenerIbajo (suCaalto es esperado) - Advisory por default: con
leaf_penalty/app_penaltyen0el detector solo aparece en el reporte y en el JSON (coupling_issues) sin afectar el score → cero regresión para proyectos existentes. Subir las penalizaciones activa el gate en CI - Nueva config
coupling_limits(leaf_instability,leaf_max_ca,leaf_penalty,app_max_ca,app_penalty) — funciona sinanalyzer_config.jsonusando los defaults internos - El punto de entrada (app) se detecta por el plugin
com.android.application(única señal de plugin confiable;com.android.libraryno distingue rol).coupling_overridespermite forzar ("app"/"leaf") o excluir ("ignore") módulos puntuales cuando la métrica se equivoca - Nueva clase de test
TestLeafCouplingy fixtureleaf_coupling
Known limitations
- La detección del plugin de app no resuelve
alias(libs.plugins.android.application)(requiere leer el version catalog); solo elid("com.android.application")literal. El detector funciona igual sin esa señal — solo pierde el umbral más estricto del punto de entrada
v1.2.2
Added
- Parser tree-sitter para
.gradle.kts: cuandotree-sitter-kotlinestá instalado (pip install "android-gradle-analyzer[kts]"), los archivos KTS se parsean con un AST real en lugar de regex. Maneja correctamente: dependencias multilínea, comentarios de línea//y de bloque/* */, y parámetros nombradosproject(path = ":module"). El fallback a regex es automático si la librería no está disponible - Preprocesador Groovy para archivos
.gradle: antes de aplicar regex, se eliminan comentarios//y/* */y se colapsan declaraciones multilínea (e.g.implementation(\n project(":core")\n)). Elimina falsos negativos por comentarios y falsos positivos desde dependencias comentadas - Nueva función
_strip_comments(interna): limpia comentarios sin colapso de paréntesis — usada en el path KTS para los accessorsprojects.* - Nuevos fixtures de test:
kts_multiline,kts_commented,groovy_multiline - Nuevas clases de test:
TestPreprocessGroovyyTestKtsTreeSitter(se saltan automáticamente sitree-sitter-kotlinno está instalado) - Nueva dependencia opcional
kts:pip install "android-gradle-analyzer[kts]"instalatree-sitter>=0.22ytree-sitter-kotlin>=0.3
v1.2.1
Changed
pyproject.toml:requires-pythonactualizado de>=3.8a>=3.10. El código usa sintaxis PEP 604 (str | None,list[str]) que requiere Python 3.10 o superior. Antes el paquete se instalaba en 3.8/3.9 pero crashaba al primer import — ahora pip se niega antes con un mensaje claro- README: badge de Python actualizado a
3.10+(antes mentía con3.7+)
Added
- README: badges de Tests (estado del último workflow) y Release (última versión publicada) vía shields.io