pumuki 6.3.112 → 6.3.114
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.
- package/CHANGELOG.md +51 -4
- package/README.md +4 -2
- package/VERSION +1 -1
- package/core/facts/detectors/typescript/index.test.ts +0 -229
- package/core/facts/detectors/typescript/index.ts +0 -278
- package/core/facts/extractHeuristicFacts.ts +0 -4
- package/core/rules/presets/heuristics/typescript.test.ts +1 -21
- package/core/rules/presets/heuristics/typescript.ts +0 -72
- package/docs/README.md +13 -9
- package/docs/codex-skills/backend-enterprise-rules.md +3 -3
- package/docs/operations/RELEASE_NOTES.md +40 -3
- package/docs/product/API_REFERENCE.md +1 -1
- package/docs/product/HOW_IT_WORKS.md +6 -0
- package/docs/product/INSTALLATION.md +1 -1
- package/docs/product/USAGE.md +42 -5
- package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +100 -44
- package/docs/validation/README.md +6 -3
- package/integrations/config/skillsDetectorRegistry.ts +0 -24
- package/integrations/config/skillsMarkdownRules.ts +0 -57
- package/integrations/evidence/buildEvidence.ts +0 -24
- package/integrations/evidence/repoState.ts +0 -3
- package/integrations/evidence/schema.ts +0 -18
- package/integrations/evidence/writeEvidence.ts +0 -24
- package/integrations/gate/evaluateAiGate.ts +8 -251
- package/integrations/gate/remediationCatalog.ts +0 -8
- package/integrations/git/GitService.ts +44 -5
- package/integrations/git/aiGateRepoPolicyFindings.ts +86 -17
- package/integrations/git/runPlatformGate.ts +1 -9
- package/integrations/git/runPlatformGateFacts.ts +19 -1
- package/integrations/git/runPlatformGateOutput.ts +41 -42
- package/integrations/lifecycle/adapter.templates.json +1 -0
- package/integrations/lifecycle/adapter.ts +0 -24
- package/integrations/lifecycle/audit.ts +101 -0
- package/integrations/lifecycle/cli.ts +120 -99
- package/integrations/lifecycle/cliSdd.ts +4 -26
- package/integrations/lifecycle/doctor.ts +40 -103
- package/integrations/lifecycle/index.ts +2 -0
- package/integrations/lifecycle/install.ts +0 -21
- package/integrations/lifecycle/packageInfo.ts +1 -118
- package/integrations/lifecycle/state.ts +1 -8
- package/integrations/lifecycle/status.ts +41 -46
- package/integrations/lifecycle/watch.ts +1 -1
- package/integrations/mcp/aiGateCheck.ts +10 -194
- package/integrations/mcp/autoExecuteAiStart.ts +116 -92
- package/integrations/mcp/enterpriseServer.ts +7 -23
- package/integrations/mcp/enterpriseStdioServer.cli.ts +4 -31
- package/integrations/mcp/preFlightCheck.ts +5 -67
- package/integrations/platform/detectPlatforms.ts +37 -0
- package/integrations/sdd/policy.ts +28 -20
- package/package.json +1 -1
- package/scripts/check-tracking-single-active.sh +1 -1
- package/scripts/consumer-menu-matrix-baseline-report-lib.ts +13 -38
- package/scripts/consumer-postinstall-resolve-args.cjs +44 -0
- package/scripts/consumer-postinstall.cjs +76 -21
- package/scripts/framework-menu-advanced-view-lib.ts +0 -49
- package/scripts/framework-menu-consumer-actions-lib.ts +28 -4
- package/scripts/framework-menu-consumer-preflight-hints.ts +5 -2
- package/scripts/framework-menu-consumer-preflight-render.ts +0 -10
- package/scripts/framework-menu-consumer-preflight-run.ts +0 -23
- package/scripts/framework-menu-consumer-preflight-types.ts +0 -12
- package/scripts/framework-menu-consumer-runtime-actions.ts +87 -17
- package/scripts/framework-menu-consumer-runtime-audit.ts +36 -2
- package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +140 -0
- package/scripts/framework-menu-consumer-runtime-lib.ts +2 -38
- package/scripts/framework-menu-consumer-runtime-menu.ts +4 -31
- package/scripts/framework-menu-consumer-runtime-types.ts +3 -5
- package/scripts/framework-menu-evidence-summary-lib.ts +1 -0
- package/scripts/framework-menu-evidence-summary-read.ts +57 -5
- package/scripts/framework-menu-evidence-summary-severity.ts +3 -1
- package/scripts/framework-menu-evidence-summary-types.ts +7 -0
- package/scripts/framework-menu-gate-lib.ts +9 -0
- package/scripts/framework-menu-layout-data.ts +5 -0
- package/scripts/framework-menu-matrix-baseline-lib.ts +15 -14
- package/scripts/framework-menu-matrix-canary-lib.ts +22 -1
- package/scripts/framework-menu-matrix-evidence-lib.ts +1 -0
- package/scripts/framework-menu-matrix-evidence-types.ts +13 -1
- package/scripts/framework-menu-matrix-runner-lib.ts +35 -0
- package/scripts/framework-menu-system-notifications-cause.ts +0 -3
- package/scripts/framework-menu-system-notifications-macos-swift-source.ts +24 -204
- package/scripts/framework-menu-system-notifications-macos.ts +4 -0
- package/scripts/framework-menu-system-notifications-payloads-blocked.ts +1 -1
- package/scripts/framework-menu-system-notifications-text.ts +1 -7
- package/scripts/framework-menu.ts +3 -24
- package/scripts/package-install-smoke-consumer-git-repo-lib.ts +1 -10
- package/scripts/package-install-smoke-consumer-npm-lib.ts +9 -46
- package/scripts/pumuki-full-surface-smoke-lib.ts +37 -0
- package/scripts/pumuki-full-surface-smoke.ts +346 -0
- package/scripts/pumuki-smoke-installed-wrapper.cjs +31 -0
- package/integrations/evidence/trackingContract.ts +0 -17
- package/integrations/gate/governanceActionCatalog.ts +0 -275
- package/integrations/lifecycle/bootstrapManifest.ts +0 -248
- package/integrations/lifecycle/cliGovernanceConsole.ts +0 -69
- package/integrations/lifecycle/governanceNextAction.ts +0 -171
- package/integrations/lifecycle/governanceObservationSnapshot.ts +0 -379
- package/integrations/lifecycle/trackingState.ts +0 -392
- package/integrations/mcp/alignedPlatformGate.ts +0 -232
- package/integrations/mcp/readMcpPrePushStdin.ts +0 -7
- package/scripts/build-ruralgo-s1-evidence-pack.ts +0 -85
- package/scripts/ruralgo-s1-evidence-pack-lib.ts +0 -200
package/CHANGELOG.md
CHANGED
|
@@ -6,12 +6,59 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
## [6.3.
|
|
9
|
+
## [6.3.114] - 2026-04-24
|
|
10
10
|
|
|
11
11
|
### Fixed
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
- **
|
|
13
|
+
- **`status` y `doctor` exponen `issues` canónicos también en evidencia `WARN`:** la línea publicada deja de reservar la lista de findings a estados bloqueados y pasa a emitir una advertencia consumible por automatización cuando governance está en atención operativa real.
|
|
14
|
+
- **Hotfix mínimo sobre la superficie estable de `main`:** el contrato bloqueado existente se conserva, pero ahora la evidencia `WARN` produce `Governance requires attention (...)` como issue canónico sin arrastrar snapshots adicionales de `develop`.
|
|
15
|
+
- **Cobertura de regresión de `INC-084` en la línea publicada:** nuevas pruebas fijan el caso `WARN` tanto en `status` como en `doctor`, manteniendo la semántica previa para estados `BLOCK`.
|
|
16
|
+
|
|
17
|
+
## [6.3.108] - 2026-04-22
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **MCP enterprise visible por defecto en la línea publicada:** `mcp_enterprise` deja de nacer en `off`, así que el editor/agente puede ver `ai_gate_check`, `pre_flight_check` y `auto_execute_ai_start` sin opt-in adicional.
|
|
22
|
+
- **Enforcement temprano más perceptible para `PRE_WRITE`:** el catálogo enterprise visible por defecto reduce el gap entre `status`/`doctor` bloqueados y la ruta real de trabajo del agente/editor.
|
|
23
|
+
- **Cobertura de regresión de la baseline MCP:** nuevas pruebas fijan el catálogo del enterprise server y la proyección de baseline consumer asociada a `mcp_enterprise`.
|
|
24
|
+
|
|
25
|
+
## [6.3.107] - 2026-04-22
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **Semántica inequívoca para sesión SDD expirada:** una sesión vencida deja de proyectarse como `active=true` y pasa a exponerse como inactiva (`active=false`) manteniendo `valid=false` y `remainingSeconds=0`.
|
|
30
|
+
- **Refresh de sesión expiradas todavía permitido:** `refreshSddSession` ya no exige `active=true`; basta con conservar el `changeId` para poder renovar una sesión caducada sin reabrirla manualmente.
|
|
31
|
+
- **Policy SDD alineada con esa semántica:** `evaluateSddPolicy` trata la sesión como `missing` solo cuando falta `changeId`, y conserva `SDD_SESSION_INVALID` para sesiones expiradas con contexto recuperable.
|
|
32
|
+
|
|
33
|
+
## [6.3.106] - 2026-04-22
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **Activación advisory de SDD/PRE_WRITE fijada al runtime diagnosticado:** `sdd validate` deja de devolver `activation_command` con `pumuki@latest` cuando el namespace experimental está desactivado por defecto.
|
|
38
|
+
- **Session guidance reproducible en SDD:** las instrucciones de `session --refresh` y `session --open` también quedan fijadas a la versión efectiva del runtime en lugar de depender de `latest`.
|
|
39
|
+
|
|
40
|
+
## [6.3.105] - 2026-04-22
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- **Remediaciones PRE_WRITE fijadas a la versión diagnosticada:** `sdd validate`, `auto_execute_ai_start` y la remediación por defecto dejan de recomendar `pumuki@latest` y pasan a devolver comandos con la versión efectiva del runtime (`pumuki@6.3.105` en esta línea).
|
|
45
|
+
- **Backport útil de `PUMUKI-INC-089` a la línea publicada:** `main` mantiene la ruta reproducible para `install`, `policy reconcile --strict --json` y la revalidación `PRE_WRITE` sin exigir al consumer adivinar la versión correcta.
|
|
46
|
+
|
|
47
|
+
## [6.3.104] - 2026-04-22
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **Tracking canónico de RuralGo reconocido por el parser de repo-policy:** `appendTrackingActionableContext` ya inspecciona `docs/RURALGO_SEGUIMIENTO.md`, que es la ruta canónica real del consumer.
|
|
52
|
+
- **Filas `| 🚧 | TASK |` tratadas como entradas activas válidas:** el diagnóstico accionable cubre el formato de tabla usado por el hub de seguimiento de RuralGo además del backlog tabular de incidencias.
|
|
53
|
+
- **Cobertura de regresión para el hub canónico:** nuevas pruebas fijan parsing y priorización de `docs/RURALGO_SEGUIMIENTO.md` antes de otros archivos de tracking del consumer.
|
|
54
|
+
|
|
55
|
+
## [6.3.103] - 2026-04-22
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- **Diagnóstico accionable del tracking canónico en consumers:** `status`, `doctor` y el gate repo-policy ya incluyen `TRACKING_CANONICAL_IN_PROGRESS_INVALID` junto con referencias a entradas activas y al board canónico del repo consumidor cuando existe.
|
|
60
|
+
- **Separación explícita entre blocker y warning secundario:** la salida de `PRE_WRITE` conserva un `block-summary` primario y añade `warning-summary` para warnings de higiene (`EVIDENCE_PREWRITE_WORKTREE_WARN`) cuando conviven con un bloqueo duro de tracking.
|
|
61
|
+
- **Cobertura de regresión del hotfix:** nuevas pruebas fijan el parsing de boards tabulares `🚧 reported activo` y la impresión del resumen jerárquico con warning secundario.
|
|
15
62
|
|
|
16
63
|
## [6.3.102] - 2026-04-22
|
|
17
64
|
|
|
@@ -40,7 +87,7 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
40
87
|
### Fixed
|
|
41
88
|
|
|
42
89
|
- **PRE_WRITE visible y coherente en la línea de producción:** `policyValidationSnapshot` refleja `PRE_WRITE` como estricto cuando el enforcement efectivo está activado en `strict`, evitando contradicción entre policy y runtime.
|
|
43
|
-
- **Arranque agentic sin éxito falso:** `auto_execute_ai_start` devuelve
|
|
90
|
+
- **Arranque agentic sin éxito falso:** `auto_execute_ai_start` devuelve `success=false` cuando el gate bloquea y fuerza remediación explícita antes de continuar.
|
|
44
91
|
- **Contrato MCP actualizado:** la superficie HTTP del enterprise server hereda ese mismo contrato de bloqueo para `auto_execute_ai_start`.
|
|
45
92
|
- **Cobertura de regresión del hotfix:** nuevas regresiones fijan la proyección de `PRE_WRITE` y el comportamiento bloqueante del arranque agentic sobre la línea `main`.
|
|
46
93
|
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ Elige un perfil y profundiza en los enlaces; **no** repite aquí reglas largas (
|
|
|
32
32
|
|
|
33
33
|
| Perfil | Qué instalar / arrancar | Stages habituales | Opcional típico |
|
|
34
34
|
|--------|-------------------------|-------------------|-----------------|
|
|
35
|
-
| **Mínimo** | `npm install --save-exact pumuki` (en repos Git el `postinstall`
|
|
35
|
+
| **Mínimo** | `npm install --save-exact pumuki` (en repos Git el `postinstall` ejecuta baseline `pumuki install`; `--with-mcp --agent=repo` es opt-in con `PUMUKI_POSTINSTALL_WITH_MCP=1` o `PUMUKI_POSTINSTALL_MCP_AGENT=repo`). | Hooks Git: **PRE_COMMIT**, **PRE_PUSH**; cadena **PRE_WRITE** cuando el hook lo encadena (según versión y config). | Evidencia [`.ai_evidence.json` v2.1](docs/mcp/ai-evidence-v2.1-contract.md); reglas core embebidas. |
|
|
36
36
|
| **Estándar** | Lo anterior + flujo **OpenSpec/SDD** bajo `openspec/` según tu política. | Lo anterior + validación SDD por stage (`pumuki sdd validate --stage=…`). | Sesiones SDD, cambios versionados bajo `openspec/changes/`. |
|
|
37
37
|
| **Enterprise completo** | `pumuki bootstrap --enterprise` (o equivalente documentado) + `skills.lock.json` / reglas custom / [policy-as-code](docs/product/CONFIGURATION.md) donde aplique. | Lo anterior + **CI** (`pumuki-ci`) y comprobaciones de alineación (`doctor`, parity). | [Skills / MCP](docs/mcp/mcp-servers-overview.md), `pumuki doctor --parity`, notificaciones, [hard mode](docs/product/CONFIGURATION.md). |
|
|
38
38
|
|
|
@@ -50,6 +50,8 @@ Cinco entradas que cubren el 80 % del día a día en un consumidor; el detalle e
|
|
|
50
50
|
4. **Gates locales** — `npx pumuki-pre-write`, `npx pumuki-pre-commit` (y `pumuki-pre-push` cuando toque push). Detalle: [USAGE](docs/product/USAGE.md), [Troubleshooting (USAGE)](docs/product/USAGE.md#troubleshooting).
|
|
51
51
|
5. **SDD por stage (enterprise)** — `npx pumuki sdd validate --stage=PRE_COMMIT` (u otro stage). Detalle: [USAGE](docs/product/USAGE.md), [INSTALLATION](docs/product/INSTALLATION.md#troubleshooting) si falla el bootstrap.
|
|
52
52
|
|
|
53
|
+
**Desarrollo en este repo (sin depender de GitHub Actions):** barra mínima antes de merge o publicar — `npm run -s validation:local-merge-bar` (`typecheck` + smoke de superficie CLI + `npm test`). Detalle del smoke: [docs/validation/README.md](docs/validation/README.md).
|
|
54
|
+
|
|
53
55
|
Si algo bloquea o el mensaje no es claro: [Troubleshooting](#troubleshooting) (más abajo en este README), [USAGE § Troubleshooting](docs/product/USAGE.md#troubleshooting) y [GitHub Issues](https://github.com/SwiftEnProfundidad/ast-intelligence-hooks/issues).
|
|
54
56
|
|
|
55
57
|
## 5-Minute Quick Start (Consumer)
|
|
@@ -69,7 +71,7 @@ npx --yes pumuki status
|
|
|
69
71
|
npx --yes pumuki doctor --json
|
|
70
72
|
```
|
|
71
73
|
|
|
72
|
-
Desde **6.3.63**, `npm install` en la raíz de un repo **Git** dispara un `postinstall` que ejecuta **`pumuki install
|
|
74
|
+
Desde **6.3.63**, `npm install` en la raíz de un repo **Git** dispara un `postinstall` que ejecuta baseline **`pumuki install`** (hooks `pre-commit` / `pre-push`, lifecycle, merge de **`.pumuki/adapter.json`** con comandos MCP stdio, sin rutas de IDE salvo opt-in). El wiring MCP no va activado por defecto; para activarlo en postinstall usa `PUMUKI_POSTINSTALL_WITH_MCP=1` o `PUMUKI_POSTINSTALL_MCP_AGENT=repo/cursor/claude/codex`. **Pumuki no depende de ningún IDE** para el baseline. Opt-out del postinstall: `PUMUKI_SKIP_POSTINSTALL=1`. Ficheros de IDE en postinstall: `PUMUKI_POSTINSTALL_MCP_AGENT=cursor|claude|codex` o comando manual / `bootstrap`. Desde **6.3.68**, cada hook gestionado ejecuta **`pumuki-pre-write` antes** de `pumuki-pre-commit` / `pumuki-pre-push` (stage **PRE_WRITE** vía Git). Saltar solo PRE_WRITE en hooks: `PUMUKI_SKIP_CHAINED_PRE_WRITE=1`. Desde **6.3.69**, esos mismos hooks aplican también **git-flow en ramas protegidas** (`GITFLOW_PROTECTED_BRANCH`) e **higiene de worktree** (`PUMUKI_PREWRITE_WORKTREE_*` / códigos `EVIDENCE_PREWRITE_WORKTREE_*`) cuando la evidencia es válida; el **modal macOS** de bloqueo (Desactivar / Silenciar 30 min / Mantener activas) queda **activo por defecto** si las notificaciones están habilitadas (`"blockedDialogEnabled": false` o `PUMUKI_MACOS_BLOCKED_DIALOG=0` para apagarlo). Desactivar el postinstall: `PUMUKI_SKIP_POSTINSTALL=1`. En CI suele saltarse solo (`CI=true`). En **6.3.64+**, las notificaciones del sistema en plataformas sin banner nativo se reflejan en **stderr** por defecto (`PUMUKI_DISABLE_STDERR_NOTIFICATIONS=1` para silenciarlas). En **6.3.69+**, un `gate.blocked` en macOS también deja una copia en **stderr** por defecto (`PUMUKI_DISABLE_GATE_BLOCKED_STDERR_MIRROR=1` para desactivar solo eso). Desde **6.3.70**, si **`.ai_evidence.json` está versionado** y **PRE_PUSH** no bloquea, ese archivo **no se reescribe** en el push (compatibilidad con **pre-commit** como hook de **pre-push**); para forzar escritura: `PUMUKI_PRE_PUSH_ALWAYS_WRITE_TRACKED_EVIDENCE=1`. Con modal de bloqueo activo, el panel interactivo prioriza foco/clics y se evita el banner duplicado.
|
|
73
75
|
|
|
74
76
|
Fallback (equivalent in pasos separados):
|
|
75
77
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
1
|
+
v6.3.114
|
|
@@ -13,12 +13,6 @@ import {
|
|
|
13
13
|
findUndefinedInBaseTypeUnionLines,
|
|
14
14
|
findUnknownWithoutGuardLines,
|
|
15
15
|
findUnknownTypeAssertionLines,
|
|
16
|
-
findAnemicDomainModelLines,
|
|
17
|
-
findControllerBusinessLogicLines,
|
|
18
|
-
findMagicNumberLiteralLines,
|
|
19
|
-
findProductionMockArtifactUsageLines,
|
|
20
|
-
hasAnemicDomainModel,
|
|
21
|
-
hasControllerBusinessLogic,
|
|
22
16
|
hasAsyncPromiseExecutor,
|
|
23
17
|
hasConcreteDependencyInstantiation,
|
|
24
18
|
hasConsoleErrorCall,
|
|
@@ -30,8 +24,6 @@ import {
|
|
|
30
24
|
hasExplicitAnyType,
|
|
31
25
|
hasFrameworkDependencyImport,
|
|
32
26
|
hasFunctionConstructorUsage,
|
|
33
|
-
hasMagicNumberLiteral,
|
|
34
|
-
hasProductionMockArtifactUsage,
|
|
35
27
|
hasNetworkCallWithoutErrorHandling,
|
|
36
28
|
hasMixedCommandQueryClass,
|
|
37
29
|
hasMixedCommandQueryInterface,
|
|
@@ -892,227 +884,6 @@ test('hasLargeClassDeclaration detecta clases con 300 lineas o mas', () => {
|
|
|
892
884
|
assert.equal(hasLargeClassDeclaration(compactClassAst), false);
|
|
893
885
|
});
|
|
894
886
|
|
|
895
|
-
test('hasMagicNumberLiteral detecta literales numericos repetidos en contexto ejecutable y descarta declarativos', () => {
|
|
896
|
-
const magicAst = {
|
|
897
|
-
type: 'Program',
|
|
898
|
-
body: [
|
|
899
|
-
{
|
|
900
|
-
type: 'ExpressionStatement',
|
|
901
|
-
expression: {
|
|
902
|
-
type: 'CallExpression',
|
|
903
|
-
callee: { type: 'Identifier', name: 'retry' },
|
|
904
|
-
arguments: [{ type: 'NumericLiteral', value: 42, loc: { start: { line: 3 }, end: { line: 3 } } }],
|
|
905
|
-
},
|
|
906
|
-
},
|
|
907
|
-
{
|
|
908
|
-
type: 'VariableDeclaration',
|
|
909
|
-
kind: 'const',
|
|
910
|
-
declarations: [
|
|
911
|
-
{
|
|
912
|
-
type: 'VariableDeclarator',
|
|
913
|
-
id: { type: 'Identifier', name: 'timeoutMs' },
|
|
914
|
-
init: { type: 'NumericLiteral', value: 42, loc: { start: { line: 5 }, end: { line: 5 } } },
|
|
915
|
-
},
|
|
916
|
-
],
|
|
917
|
-
},
|
|
918
|
-
{
|
|
919
|
-
type: 'ExpressionStatement',
|
|
920
|
-
expression: {
|
|
921
|
-
type: 'BinaryExpression',
|
|
922
|
-
operator: '>',
|
|
923
|
-
left: { type: 'Identifier', name: 'elapsedMs' },
|
|
924
|
-
right: { type: 'NumericLiteral', value: 42, loc: { start: { line: 7 }, end: { line: 7 } } },
|
|
925
|
-
},
|
|
926
|
-
},
|
|
927
|
-
{
|
|
928
|
-
type: 'ReturnStatement',
|
|
929
|
-
argument: { type: 'NumericLiteral', value: 1, loc: { start: { line: 9 }, end: { line: 9 } } },
|
|
930
|
-
},
|
|
931
|
-
],
|
|
932
|
-
};
|
|
933
|
-
|
|
934
|
-
const ignoredAst = {
|
|
935
|
-
type: 'Program',
|
|
936
|
-
body: [
|
|
937
|
-
{
|
|
938
|
-
type: 'VariableDeclaration',
|
|
939
|
-
kind: 'const',
|
|
940
|
-
declarations: [
|
|
941
|
-
{
|
|
942
|
-
type: 'VariableDeclarator',
|
|
943
|
-
id: { type: 'Identifier', name: 'port' },
|
|
944
|
-
init: { type: 'NumericLiteral', value: 3000, loc: { start: { line: 2 }, end: { line: 2 } } },
|
|
945
|
-
},
|
|
946
|
-
],
|
|
947
|
-
},
|
|
948
|
-
{
|
|
949
|
-
type: 'ReturnStatement',
|
|
950
|
-
argument: { type: 'NumericLiteral', value: 1, loc: { start: { line: 4 }, end: { line: 4 } } },
|
|
951
|
-
},
|
|
952
|
-
],
|
|
953
|
-
};
|
|
954
|
-
|
|
955
|
-
assert.equal(hasMagicNumberLiteral(magicAst), true);
|
|
956
|
-
assert.deepEqual(findMagicNumberLiteralLines(magicAst), [3, 7]);
|
|
957
|
-
assert.equal(hasMagicNumberLiteral(ignoredAst), false);
|
|
958
|
-
assert.deepEqual(findMagicNumberLiteralLines(ignoredAst), []);
|
|
959
|
-
});
|
|
960
|
-
|
|
961
|
-
test('hasProductionMockArtifactUsage detecta imports/requires de doubles en runtime productivo', () => {
|
|
962
|
-
const importAst = {
|
|
963
|
-
type: 'ImportDeclaration',
|
|
964
|
-
source: { type: 'StringLiteral', value: '../mocks/user-repository', loc: { start: { line: 3 }, end: { line: 3 } } },
|
|
965
|
-
specifiers: [],
|
|
966
|
-
loc: { start: { line: 3 }, end: { line: 3 } },
|
|
967
|
-
};
|
|
968
|
-
const requireAst = {
|
|
969
|
-
type: 'CallExpression',
|
|
970
|
-
callee: { type: 'Identifier', name: 'require' },
|
|
971
|
-
arguments: [{ type: 'StringLiteral', value: 'sinon', loc: { start: { line: 7 }, end: { line: 7 } } }],
|
|
972
|
-
loc: { start: { line: 7 }, end: { line: 7 } },
|
|
973
|
-
};
|
|
974
|
-
const cleanAst = {
|
|
975
|
-
type: 'Program',
|
|
976
|
-
body: [
|
|
977
|
-
{
|
|
978
|
-
type: 'ImportDeclaration',
|
|
979
|
-
source: { type: 'StringLiteral', value: '../adapters/user-repository', loc: { start: { line: 1 }, end: { line: 1 } } },
|
|
980
|
-
specifiers: [],
|
|
981
|
-
},
|
|
982
|
-
],
|
|
983
|
-
};
|
|
984
|
-
const mixedAst = {
|
|
985
|
-
type: 'Program',
|
|
986
|
-
body: [importAst, { type: 'ExpressionStatement', expression: requireAst }],
|
|
987
|
-
};
|
|
988
|
-
|
|
989
|
-
assert.equal(hasProductionMockArtifactUsage(importAst), true);
|
|
990
|
-
assert.equal(hasProductionMockArtifactUsage(requireAst), true);
|
|
991
|
-
assert.equal(hasProductionMockArtifactUsage(cleanAst), false);
|
|
992
|
-
assert.deepEqual(findProductionMockArtifactUsageLines(mixedAst), [3, 7]);
|
|
993
|
-
});
|
|
994
|
-
|
|
995
|
-
test('hasAnemicDomainModel detecta clases de dominio con solo accessors y sin comportamiento', () => {
|
|
996
|
-
const anemicAst = {
|
|
997
|
-
type: 'ClassDeclaration',
|
|
998
|
-
id: { type: 'Identifier', name: 'OrderEntity' },
|
|
999
|
-
body: {
|
|
1000
|
-
type: 'ClassBody',
|
|
1001
|
-
body: [
|
|
1002
|
-
{ type: 'ClassMethod', kind: 'constructor', key: { type: 'Identifier', name: 'constructor' }, loc: { start: { line: 3 }, end: { line: 3 } } },
|
|
1003
|
-
{ type: 'ClassMethod', key: { type: 'Identifier', name: 'getStatus' }, loc: { start: { line: 5 }, end: { line: 5 } } },
|
|
1004
|
-
{ type: 'ClassMethod', key: { type: 'Identifier', name: 'setStatus' }, loc: { start: { line: 7 }, end: { line: 7 } } },
|
|
1005
|
-
],
|
|
1006
|
-
},
|
|
1007
|
-
loc: { start: { line: 1 }, end: { line: 9 } },
|
|
1008
|
-
};
|
|
1009
|
-
const richAst = {
|
|
1010
|
-
type: 'ClassDeclaration',
|
|
1011
|
-
id: { type: 'Identifier', name: 'OrderEntity' },
|
|
1012
|
-
body: {
|
|
1013
|
-
type: 'ClassBody',
|
|
1014
|
-
body: [
|
|
1015
|
-
{ type: 'ClassMethod', kind: 'constructor', key: { type: 'Identifier', name: 'constructor' }, loc: { start: { line: 3 }, end: { line: 3 } } },
|
|
1016
|
-
{ type: 'ClassMethod', key: { type: 'Identifier', name: 'getStatus' }, loc: { start: { line: 5 }, end: { line: 5 } } },
|
|
1017
|
-
{ type: 'ClassMethod', key: { type: 'Identifier', name: 'confirm' }, loc: { start: { line: 7 }, end: { line: 7 } } },
|
|
1018
|
-
],
|
|
1019
|
-
},
|
|
1020
|
-
loc: { start: { line: 1 }, end: { line: 9 } },
|
|
1021
|
-
};
|
|
1022
|
-
const serviceAst = {
|
|
1023
|
-
type: 'ClassDeclaration',
|
|
1024
|
-
id: { type: 'Identifier', name: 'OrderService' },
|
|
1025
|
-
body: {
|
|
1026
|
-
type: 'ClassBody',
|
|
1027
|
-
body: [
|
|
1028
|
-
{ type: 'ClassMethod', kind: 'constructor', key: { type: 'Identifier', name: 'constructor' }, loc: { start: { line: 3 }, end: { line: 3 } } },
|
|
1029
|
-
{ type: 'ClassMethod', key: { type: 'Identifier', name: 'getStatus' }, loc: { start: { line: 5 }, end: { line: 5 } } },
|
|
1030
|
-
{ type: 'ClassMethod', key: { type: 'Identifier', name: 'setStatus' }, loc: { start: { line: 7 }, end: { line: 7 } } },
|
|
1031
|
-
],
|
|
1032
|
-
},
|
|
1033
|
-
loc: { start: { line: 1 }, end: { line: 9 } },
|
|
1034
|
-
};
|
|
1035
|
-
|
|
1036
|
-
assert.equal(hasAnemicDomainModel(anemicAst), true);
|
|
1037
|
-
assert.deepEqual(findAnemicDomainModelLines(anemicAst), [1]);
|
|
1038
|
-
assert.equal(hasAnemicDomainModel(richAst), false);
|
|
1039
|
-
assert.equal(hasAnemicDomainModel(serviceAst), false);
|
|
1040
|
-
});
|
|
1041
|
-
|
|
1042
|
-
test('hasControllerBusinessLogic detecta flow control dentro de handlers en clases Controller', () => {
|
|
1043
|
-
const controllerAst = {
|
|
1044
|
-
type: 'ClassDeclaration',
|
|
1045
|
-
id: { type: 'Identifier', name: 'OrdersController' },
|
|
1046
|
-
body: {
|
|
1047
|
-
type: 'ClassBody',
|
|
1048
|
-
body: [
|
|
1049
|
-
{
|
|
1050
|
-
type: 'ClassMethod',
|
|
1051
|
-
key: { type: 'Identifier', name: 'createOrder' },
|
|
1052
|
-
decorators: [{ expression: { type: 'Identifier', name: 'Post' } }],
|
|
1053
|
-
body: {
|
|
1054
|
-
type: 'BlockStatement',
|
|
1055
|
-
body: [
|
|
1056
|
-
{
|
|
1057
|
-
type: 'VariableDeclaration',
|
|
1058
|
-
declarations: [{ type: 'VariableDeclarator', id: { type: 'Identifier', name: 'status' } }],
|
|
1059
|
-
},
|
|
1060
|
-
{
|
|
1061
|
-
type: 'IfStatement',
|
|
1062
|
-
test: { type: 'Identifier', name: 'isPriority' },
|
|
1063
|
-
consequent: { type: 'BlockStatement', body: [] },
|
|
1064
|
-
},
|
|
1065
|
-
],
|
|
1066
|
-
},
|
|
1067
|
-
loc: { start: { line: 4 }, end: { line: 10 } },
|
|
1068
|
-
},
|
|
1069
|
-
],
|
|
1070
|
-
},
|
|
1071
|
-
loc: { start: { line: 1 }, end: { line: 12 } },
|
|
1072
|
-
};
|
|
1073
|
-
const delegatedControllerAst = {
|
|
1074
|
-
type: 'ClassDeclaration',
|
|
1075
|
-
id: { type: 'Identifier', name: 'OrdersController' },
|
|
1076
|
-
body: {
|
|
1077
|
-
type: 'ClassBody',
|
|
1078
|
-
body: [
|
|
1079
|
-
{
|
|
1080
|
-
type: 'ClassMethod',
|
|
1081
|
-
key: { type: 'Identifier', name: 'createOrder' },
|
|
1082
|
-
decorators: [{ expression: { type: 'Identifier', name: 'Post' } }],
|
|
1083
|
-
body: {
|
|
1084
|
-
type: 'BlockStatement',
|
|
1085
|
-
body: [
|
|
1086
|
-
{
|
|
1087
|
-
type: 'ReturnStatement',
|
|
1088
|
-
argument: {
|
|
1089
|
-
type: 'CallExpression',
|
|
1090
|
-
callee: {
|
|
1091
|
-
type: 'MemberExpression',
|
|
1092
|
-
object: {
|
|
1093
|
-
type: 'MemberExpression',
|
|
1094
|
-
object: { type: 'ThisExpression' },
|
|
1095
|
-
property: { type: 'Identifier', name: 'ordersService' },
|
|
1096
|
-
},
|
|
1097
|
-
property: { type: 'Identifier', name: 'create' },
|
|
1098
|
-
},
|
|
1099
|
-
arguments: [],
|
|
1100
|
-
},
|
|
1101
|
-
},
|
|
1102
|
-
],
|
|
1103
|
-
},
|
|
1104
|
-
loc: { start: { line: 4 }, end: { line: 8 } },
|
|
1105
|
-
},
|
|
1106
|
-
],
|
|
1107
|
-
},
|
|
1108
|
-
loc: { start: { line: 1 }, end: { line: 10 } },
|
|
1109
|
-
};
|
|
1110
|
-
|
|
1111
|
-
assert.equal(hasControllerBusinessLogic(controllerAst), true);
|
|
1112
|
-
assert.deepEqual(findControllerBusinessLogicLines(controllerAst), [1]);
|
|
1113
|
-
assert.equal(hasControllerBusinessLogic(delegatedControllerAst), false);
|
|
1114
|
-
});
|
|
1115
|
-
|
|
1116
887
|
test('hasRecordStringUnknownType detecta Record<string, unknown>', () => {
|
|
1117
888
|
const recordUnknownAst = {
|
|
1118
889
|
type: 'TSTypeReference',
|
|
@@ -21,13 +21,6 @@ const concreteDependencyNames = new Set<string>([
|
|
|
21
21
|
]);
|
|
22
22
|
const GOD_CLASS_MAX_LINES = 300;
|
|
23
23
|
const networkCallCalleePattern = /^(fetch|axios|get|post|put|patch|delete|request)$/i;
|
|
24
|
-
const ignoredMagicNumberValues = new Set<number>([0, 1]);
|
|
25
|
-
const runtimeTestDoubleLibraryPattern = /^(sinon|testdouble|ts-mockito|jest-mock|vitest)$/i;
|
|
26
|
-
const runtimeTestDoublePathPattern = /(^|\/)(__mocks__|mocks|fakes|spies|stubs)(\/|$)|\.(mock|fake|spy|stub)$/i;
|
|
27
|
-
const anemicDomainClassNamePattern = /(Entity|Aggregate|Model)$/i;
|
|
28
|
-
const controllerClassNamePattern = /Controller$/i;
|
|
29
|
-
const controllerRoutingDecoratorPattern =
|
|
30
|
-
/^(Get|Post|Put|Patch|Delete|All|Head|Options|MessagePattern|EventPattern)$/;
|
|
31
24
|
type AstNode = Record<string, string | number | boolean | bigint | symbol | null | Date | object>;
|
|
32
25
|
type TypeScriptSemanticNode = {
|
|
33
26
|
kind: 'class' | 'property' | 'call' | 'member';
|
|
@@ -414,101 +407,6 @@ const collectClassMethodDescriptors = (classNode: AstNode): readonly ClassMethod
|
|
|
414
407
|
return descriptors;
|
|
415
408
|
};
|
|
416
409
|
|
|
417
|
-
const isAccessorLikeMethodName = (name: string): boolean => {
|
|
418
|
-
return name === 'constructor' || /^(get|set)[A-Z_]/.test(name);
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
const isAnemicDomainClassNode = (value: AstNode): boolean => {
|
|
422
|
-
if (value.type !== 'ClassDeclaration' && value.type !== 'ClassExpression') {
|
|
423
|
-
return false;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const className = classNameFromNode(value);
|
|
427
|
-
if (!anemicDomainClassNamePattern.test(className)) {
|
|
428
|
-
return false;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const descriptors = collectClassMethodDescriptors(value);
|
|
432
|
-
if (descriptors.length < 2) {
|
|
433
|
-
return false;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return descriptors.every((descriptor) => isAccessorLikeMethodName(descriptor.name));
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
const hasDecoratorNamed = (node: unknown, decoratorName: RegExp): boolean => {
|
|
440
|
-
if (!isObject(node) || !Array.isArray(node.decorators)) {
|
|
441
|
-
return false;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
return node.decorators.some((decorator) => {
|
|
445
|
-
if (!isObject(decorator)) {
|
|
446
|
-
return false;
|
|
447
|
-
}
|
|
448
|
-
const expression = decorator.expression;
|
|
449
|
-
if (!isObject(expression)) {
|
|
450
|
-
return false;
|
|
451
|
-
}
|
|
452
|
-
const callee = expression.type === 'CallExpression' ? expression.callee : expression;
|
|
453
|
-
const name = methodNameFromNode(callee) ?? memberExpressionPropertyName(callee);
|
|
454
|
-
return typeof name === 'string' && decoratorName.test(name);
|
|
455
|
-
});
|
|
456
|
-
};
|
|
457
|
-
|
|
458
|
-
const isControllerHandlerMethodNode = (node: AstNode): boolean => {
|
|
459
|
-
return node.type === 'ClassMethod' && node.kind !== 'constructor';
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
const statementCountFromMethodNode = (node: AstNode): number => {
|
|
463
|
-
if (!isObject(node.body) || !Array.isArray(node.body.body)) {
|
|
464
|
-
return 0;
|
|
465
|
-
}
|
|
466
|
-
return node.body.body.filter((statement) => isObject(statement)).length;
|
|
467
|
-
};
|
|
468
|
-
|
|
469
|
-
const hasControllerBusinessFlow = (node: AstNode): boolean => {
|
|
470
|
-
return hasNode(node, (value) => {
|
|
471
|
-
return (
|
|
472
|
-
value.type === 'IfStatement' ||
|
|
473
|
-
value.type === 'SwitchStatement' ||
|
|
474
|
-
value.type === 'ForStatement' ||
|
|
475
|
-
value.type === 'ForInStatement' ||
|
|
476
|
-
value.type === 'ForOfStatement' ||
|
|
477
|
-
value.type === 'WhileStatement' ||
|
|
478
|
-
value.type === 'DoWhileStatement' ||
|
|
479
|
-
value.type === 'TryStatement' ||
|
|
480
|
-
value.type === 'ConditionalExpression'
|
|
481
|
-
);
|
|
482
|
-
});
|
|
483
|
-
};
|
|
484
|
-
|
|
485
|
-
const isControllerBusinessLogicMethodNode = (node: AstNode): boolean => {
|
|
486
|
-
if (!isControllerHandlerMethodNode(node)) {
|
|
487
|
-
return false;
|
|
488
|
-
}
|
|
489
|
-
if (statementCountFromMethodNode(node) < 2) {
|
|
490
|
-
return false;
|
|
491
|
-
}
|
|
492
|
-
return hasControllerBusinessFlow(node);
|
|
493
|
-
};
|
|
494
|
-
|
|
495
|
-
const isControllerClassNode = (value: AstNode): boolean => {
|
|
496
|
-
if (value.type !== 'ClassDeclaration' && value.type !== 'ClassExpression') {
|
|
497
|
-
return false;
|
|
498
|
-
}
|
|
499
|
-
return controllerClassNamePattern.test(classNameFromNode(value));
|
|
500
|
-
};
|
|
501
|
-
|
|
502
|
-
const findControllerBusinessLogicMethod = (node: AstNode): AstNode | undefined => {
|
|
503
|
-
const classBody = node.body;
|
|
504
|
-
if (!isObject(classBody) || !Array.isArray(classBody.body)) {
|
|
505
|
-
return undefined;
|
|
506
|
-
}
|
|
507
|
-
return classBody.body.find((member): member is AstNode => {
|
|
508
|
-
return isObject(member) && isControllerBusinessLogicMethodNode(member);
|
|
509
|
-
});
|
|
510
|
-
};
|
|
511
|
-
|
|
512
410
|
const interfaceNameFromNode = (node: AstNode): string => {
|
|
513
411
|
const idNode = node.id;
|
|
514
412
|
if (isObject(idNode) && idNode.type === 'Identifier' && typeof idNode.name === 'string') {
|
|
@@ -1648,96 +1546,6 @@ export const hasAsyncPromiseExecutor = (node: unknown): boolean => {
|
|
|
1648
1546
|
});
|
|
1649
1547
|
};
|
|
1650
1548
|
|
|
1651
|
-
export const hasMagicNumberLiteral = (node: unknown): boolean => {
|
|
1652
|
-
return [...collectMagicNumberOccurrences(node).values()].some(
|
|
1653
|
-
(occurrence) => occurrence.count >= 2
|
|
1654
|
-
);
|
|
1655
|
-
};
|
|
1656
|
-
|
|
1657
|
-
export const findMagicNumberLiteralLines = (node: unknown): readonly number[] => {
|
|
1658
|
-
return collectRepeatedMagicNumberLines(node);
|
|
1659
|
-
};
|
|
1660
|
-
|
|
1661
|
-
const runtimeTestDoubleSpecifier = (node: unknown): string | undefined => {
|
|
1662
|
-
if (!isObject(node) || node.type !== 'StringLiteral' || typeof node.value !== 'string') {
|
|
1663
|
-
return undefined;
|
|
1664
|
-
}
|
|
1665
|
-
return node.value;
|
|
1666
|
-
};
|
|
1667
|
-
|
|
1668
|
-
const matchesRuntimeTestDoubleImport = (specifier: string): boolean => {
|
|
1669
|
-
return (
|
|
1670
|
-
runtimeTestDoubleLibraryPattern.test(specifier) ||
|
|
1671
|
-
runtimeTestDoublePathPattern.test(specifier)
|
|
1672
|
-
);
|
|
1673
|
-
};
|
|
1674
|
-
|
|
1675
|
-
const isRuntimeTestDoubleImportNode = (value: AstNode): boolean => {
|
|
1676
|
-
if (value.type === 'ImportDeclaration') {
|
|
1677
|
-
const specifier = runtimeTestDoubleSpecifier(value.source);
|
|
1678
|
-
return typeof specifier === 'string' && matchesRuntimeTestDoubleImport(specifier);
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
if (
|
|
1682
|
-
value.type === 'CallExpression' &&
|
|
1683
|
-
isObject(value.callee) &&
|
|
1684
|
-
value.callee.type === 'Identifier' &&
|
|
1685
|
-
value.callee.name === 'require' &&
|
|
1686
|
-
Array.isArray(value.arguments)
|
|
1687
|
-
) {
|
|
1688
|
-
return value.arguments.some((argument) => {
|
|
1689
|
-
const specifier = runtimeTestDoubleSpecifier(argument);
|
|
1690
|
-
return typeof specifier === 'string' && matchesRuntimeTestDoubleImport(specifier);
|
|
1691
|
-
});
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
return false;
|
|
1695
|
-
};
|
|
1696
|
-
|
|
1697
|
-
export const hasProductionMockArtifactUsage = (node: unknown): boolean => {
|
|
1698
|
-
return hasNode(node, (value) => isRuntimeTestDoubleImportNode(value));
|
|
1699
|
-
};
|
|
1700
|
-
|
|
1701
|
-
export const findProductionMockArtifactUsageLines = (node: unknown): readonly number[] => {
|
|
1702
|
-
return collectLineMatchesWithAncestors(node, (value) => isRuntimeTestDoubleImportNode(value), {
|
|
1703
|
-
max: 8,
|
|
1704
|
-
});
|
|
1705
|
-
};
|
|
1706
|
-
|
|
1707
|
-
export const hasAnemicDomainModel = (node: unknown): boolean => {
|
|
1708
|
-
return hasNode(node, (value) => isAnemicDomainClassNode(value));
|
|
1709
|
-
};
|
|
1710
|
-
|
|
1711
|
-
export const findAnemicDomainModelLines = (node: unknown): readonly number[] => {
|
|
1712
|
-
return collectLineMatchesWithAncestors(node, (value) => isAnemicDomainClassNode(value), {
|
|
1713
|
-
max: 8,
|
|
1714
|
-
});
|
|
1715
|
-
};
|
|
1716
|
-
|
|
1717
|
-
export const hasControllerBusinessLogic = (node: unknown): boolean => {
|
|
1718
|
-
return hasNode(node, (value) => {
|
|
1719
|
-
if (!isControllerClassNode(value)) {
|
|
1720
|
-
return false;
|
|
1721
|
-
}
|
|
1722
|
-
return Boolean(findControllerBusinessLogicMethod(value));
|
|
1723
|
-
});
|
|
1724
|
-
};
|
|
1725
|
-
|
|
1726
|
-
export const findControllerBusinessLogicLines = (node: unknown): readonly number[] => {
|
|
1727
|
-
return collectLineMatchesWithAncestors(
|
|
1728
|
-
node,
|
|
1729
|
-
(value) => {
|
|
1730
|
-
if (!isControllerClassNode(value)) {
|
|
1731
|
-
return false;
|
|
1732
|
-
}
|
|
1733
|
-
return Boolean(findControllerBusinessLogicMethod(value));
|
|
1734
|
-
},
|
|
1735
|
-
{
|
|
1736
|
-
max: 8,
|
|
1737
|
-
}
|
|
1738
|
-
);
|
|
1739
|
-
};
|
|
1740
|
-
|
|
1741
1549
|
export const hasWithStatement = (node: unknown): boolean => {
|
|
1742
1550
|
return hasNode(node, (value) => value.type === 'WithStatement');
|
|
1743
1551
|
};
|
|
@@ -1951,92 +1759,6 @@ const nodeLineSpan = (node: unknown): number => {
|
|
|
1951
1759
|
return Math.max(0, end - start + 1);
|
|
1952
1760
|
};
|
|
1953
1761
|
|
|
1954
|
-
type MagicNumberOccurrence = {
|
|
1955
|
-
count: number;
|
|
1956
|
-
lines: number[];
|
|
1957
|
-
};
|
|
1958
|
-
|
|
1959
|
-
const magicNumberValueFromNode = (node: unknown): number | undefined => {
|
|
1960
|
-
if (!isObject(node) || node.type !== 'NumericLiteral' || typeof node.value !== 'number') {
|
|
1961
|
-
return undefined;
|
|
1962
|
-
}
|
|
1963
|
-
if (!Number.isFinite(node.value) || ignoredMagicNumberValues.has(node.value)) {
|
|
1964
|
-
return undefined;
|
|
1965
|
-
}
|
|
1966
|
-
return node.value;
|
|
1967
|
-
};
|
|
1968
|
-
|
|
1969
|
-
const isExecutableMagicNumberContext = (
|
|
1970
|
-
value: AstNode,
|
|
1971
|
-
ancestors: ReadonlyArray<AstNode>
|
|
1972
|
-
): boolean => {
|
|
1973
|
-
const parent = ancestors[ancestors.length - 1];
|
|
1974
|
-
if (!isObject(parent)) {
|
|
1975
|
-
return false;
|
|
1976
|
-
}
|
|
1977
|
-
if (parent.type === 'BinaryExpression') {
|
|
1978
|
-
return parent.left === value || parent.right === value;
|
|
1979
|
-
}
|
|
1980
|
-
if (parent.type === 'CallExpression' || parent.type === 'NewExpression') {
|
|
1981
|
-
return Array.isArray(parent.arguments) && parent.arguments.includes(value);
|
|
1982
|
-
}
|
|
1983
|
-
if (parent.type === 'AssignmentExpression') {
|
|
1984
|
-
return parent.right === value;
|
|
1985
|
-
}
|
|
1986
|
-
if (parent.type === 'ReturnStatement') {
|
|
1987
|
-
return parent.argument === value;
|
|
1988
|
-
}
|
|
1989
|
-
if (parent.type === 'SwitchCase') {
|
|
1990
|
-
return parent.test === value;
|
|
1991
|
-
}
|
|
1992
|
-
return false;
|
|
1993
|
-
};
|
|
1994
|
-
|
|
1995
|
-
const collectMagicNumberOccurrences = (
|
|
1996
|
-
node: unknown
|
|
1997
|
-
): ReadonlyMap<number, MagicNumberOccurrence> => {
|
|
1998
|
-
const occurrences = new Map<number, MagicNumberOccurrence>();
|
|
1999
|
-
|
|
2000
|
-
const walk = (value: unknown, ancestors: ReadonlyArray<AstNode>): void => {
|
|
2001
|
-
if (!isObject(value)) {
|
|
2002
|
-
return;
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
const numericValue = magicNumberValueFromNode(value);
|
|
2006
|
-
if (typeof numericValue === 'number' && isExecutableMagicNumberContext(value, ancestors)) {
|
|
2007
|
-
const current = occurrences.get(numericValue) ?? { count: 0, lines: [] };
|
|
2008
|
-
current.count += 1;
|
|
2009
|
-
const line = toPositiveLine(value);
|
|
2010
|
-
if (typeof line === 'number') {
|
|
2011
|
-
current.lines.push(line);
|
|
2012
|
-
}
|
|
2013
|
-
occurrences.set(numericValue, current);
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
const nextAncestors = [...ancestors, value];
|
|
2017
|
-
for (const child of Object.values(value)) {
|
|
2018
|
-
if (Array.isArray(child)) {
|
|
2019
|
-
for (const entry of child) {
|
|
2020
|
-
walk(entry, nextAncestors);
|
|
2021
|
-
}
|
|
2022
|
-
continue;
|
|
2023
|
-
}
|
|
2024
|
-
walk(child, nextAncestors);
|
|
2025
|
-
}
|
|
2026
|
-
};
|
|
2027
|
-
|
|
2028
|
-
walk(node, []);
|
|
2029
|
-
return occurrences;
|
|
2030
|
-
};
|
|
2031
|
-
|
|
2032
|
-
const collectRepeatedMagicNumberLines = (node: unknown): readonly number[] => {
|
|
2033
|
-
return sortedUniqueLines(
|
|
2034
|
-
[...collectMagicNumberOccurrences(node).values()]
|
|
2035
|
-
.filter((occurrence) => occurrence.count >= 2)
|
|
2036
|
-
.flatMap((occurrence) => occurrence.lines)
|
|
2037
|
-
);
|
|
2038
|
-
};
|
|
2039
|
-
|
|
2040
1762
|
export const hasLargeClassDeclaration = (node: unknown): boolean => {
|
|
2041
1763
|
return hasNode(node, (value) => {
|
|
2042
1764
|
if (value.type !== 'ClassDeclaration' && value.type !== 'ClassExpression') {
|