pumuki 6.3.109 → 6.3.110
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 +4 -51
- package/README.md +2 -4
- package/VERSION +1 -1
- package/core/facts/detectors/typescript/index.test.ts +229 -0
- package/core/facts/detectors/typescript/index.ts +278 -0
- package/core/facts/extractHeuristicFacts.ts +4 -0
- package/core/rules/presets/heuristics/typescript.test.ts +21 -1
- package/core/rules/presets/heuristics/typescript.ts +72 -0
- package/docs/README.md +9 -13
- package/docs/codex-skills/backend-enterprise-rules.md +3 -3
- package/docs/operations/RELEASE_NOTES.md +4 -40
- package/docs/product/HOW_IT_WORKS.md +0 -6
- package/docs/product/INSTALLATION.md +1 -1
- package/docs/product/USAGE.md +4 -41
- package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +44 -100
- package/docs/validation/README.md +3 -6
- package/integrations/config/skillsDetectorRegistry.ts +24 -0
- package/integrations/config/skillsMarkdownRules.ts +57 -0
- package/integrations/evidence/buildEvidence.ts +24 -0
- package/integrations/evidence/repoState.ts +3 -0
- package/integrations/evidence/schema.ts +18 -0
- package/integrations/evidence/trackingContract.ts +17 -0
- package/integrations/evidence/writeEvidence.ts +24 -0
- package/integrations/gate/evaluateAiGate.ts +251 -8
- package/integrations/gate/governanceActionCatalog.ts +275 -0
- package/integrations/gate/remediationCatalog.ts +8 -0
- package/integrations/git/GitService.ts +5 -44
- package/integrations/git/aiGateRepoPolicyFindings.ts +17 -86
- package/integrations/git/runPlatformGate.ts +9 -1
- package/integrations/git/runPlatformGateFacts.ts +1 -19
- package/integrations/git/runPlatformGateOutput.ts +42 -41
- package/integrations/lifecycle/adapter.templates.json +0 -1
- package/integrations/lifecycle/adapter.ts +24 -0
- package/integrations/lifecycle/bootstrapManifest.ts +248 -0
- package/integrations/lifecycle/cli.ts +99 -120
- package/integrations/lifecycle/cliGovernanceConsole.ts +69 -0
- package/integrations/lifecycle/cliSdd.ts +26 -4
- package/integrations/lifecycle/doctor.ts +111 -17
- package/integrations/lifecycle/governanceNextAction.ts +171 -0
- package/integrations/lifecycle/governanceObservationSnapshot.ts +379 -0
- package/integrations/lifecycle/index.ts +0 -2
- package/integrations/lifecycle/install.ts +21 -0
- package/integrations/lifecycle/state.ts +8 -1
- package/integrations/lifecycle/status.ts +57 -2
- package/integrations/lifecycle/trackingState.ts +392 -0
- package/integrations/mcp/aiGateCheck.ts +194 -10
- package/integrations/mcp/alignedPlatformGate.ts +232 -0
- package/integrations/mcp/autoExecuteAiStart.ts +92 -116
- package/integrations/mcp/enterpriseServer.ts +23 -7
- package/integrations/mcp/enterpriseStdioServer.cli.ts +31 -4
- package/integrations/mcp/preFlightCheck.ts +67 -5
- package/integrations/mcp/readMcpPrePushStdin.ts +7 -0
- package/integrations/platform/detectPlatforms.ts +0 -37
- package/integrations/sdd/policy.ts +20 -28
- package/package.json +1 -1
- package/scripts/build-ruralgo-s1-evidence-pack.ts +85 -0
- package/scripts/check-tracking-single-active.sh +1 -1
- package/scripts/consumer-menu-matrix-baseline-report-lib.ts +38 -13
- package/scripts/consumer-postinstall.cjs +21 -76
- package/scripts/framework-menu-advanced-view-lib.ts +49 -0
- package/scripts/framework-menu-consumer-actions-lib.ts +4 -28
- package/scripts/framework-menu-consumer-preflight-hints.ts +2 -5
- package/scripts/framework-menu-consumer-preflight-render.ts +10 -0
- package/scripts/framework-menu-consumer-preflight-run.ts +23 -0
- package/scripts/framework-menu-consumer-preflight-types.ts +12 -0
- package/scripts/framework-menu-consumer-runtime-actions.ts +17 -87
- package/scripts/framework-menu-consumer-runtime-audit.ts +2 -36
- package/scripts/framework-menu-consumer-runtime-lib.ts +38 -2
- package/scripts/framework-menu-consumer-runtime-menu.ts +31 -4
- package/scripts/framework-menu-consumer-runtime-types.ts +5 -3
- package/scripts/framework-menu-evidence-summary-lib.ts +0 -1
- package/scripts/framework-menu-evidence-summary-read.ts +5 -57
- package/scripts/framework-menu-evidence-summary-severity.ts +1 -3
- package/scripts/framework-menu-evidence-summary-types.ts +0 -7
- package/scripts/framework-menu-gate-lib.ts +0 -9
- package/scripts/framework-menu-layout-data.ts +0 -5
- package/scripts/framework-menu-matrix-baseline-lib.ts +14 -15
- package/scripts/framework-menu-matrix-canary-lib.ts +1 -22
- package/scripts/framework-menu-matrix-evidence-lib.ts +0 -1
- package/scripts/framework-menu-matrix-evidence-types.ts +1 -13
- package/scripts/framework-menu-matrix-runner-lib.ts +0 -35
- package/scripts/framework-menu-system-notifications-cause.ts +3 -0
- package/scripts/framework-menu-system-notifications-macos-swift-source.ts +204 -24
- package/scripts/framework-menu-system-notifications-macos.ts +0 -4
- package/scripts/framework-menu-system-notifications-payloads-blocked.ts +1 -1
- package/scripts/framework-menu-system-notifications-text.ts +7 -1
- package/scripts/framework-menu.ts +24 -3
- package/scripts/package-install-smoke-consumer-git-repo-lib.ts +10 -1
- package/scripts/package-install-smoke-consumer-npm-lib.ts +46 -9
- package/scripts/ruralgo-s1-evidence-pack-lib.ts +200 -0
- package/integrations/lifecycle/audit.ts +0 -101
- package/scripts/consumer-postinstall-resolve-args.cjs +0 -44
- package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +0 -140
- package/scripts/pumuki-full-surface-smoke-lib.ts +0 -37
- package/scripts/pumuki-full-surface-smoke.ts +0 -346
- package/scripts/pumuki-smoke-installed-wrapper.cjs +0 -31
package/CHANGELOG.md
CHANGED
|
@@ -6,59 +6,12 @@ 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.110] - 2026-04-23
|
|
10
10
|
|
|
11
11
|
### Fixed
|
|
12
12
|
|
|
13
|
-
- **`
|
|
14
|
-
- **
|
|
15
|
-
- **Cobertura de regresión del ciclo de install:** nuevas pruebas fijan que `runLifecycleInstall` materializa el contrato firmado cuando `AGENTS.md` y `skills.lock.json` exponen los insumos mínimos.
|
|
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.
|
|
13
|
+
- **`status` expone `issues` canónicos cuando el runtime está bloqueado:** la salida JSON ya no obliga a combinar `governanceEffective`, `attention_codes` y `human_summary_preview` para entender un bloqueo real.
|
|
14
|
+
- **Contrato alineado en `main` y `develop`:** la lista de issues consumible queda fijada en ambas líneas para los consumers que leen `status` como fuente principal de diagnóstico.
|
|
62
15
|
|
|
63
16
|
## [6.3.102] - 2026-04-22
|
|
64
17
|
|
|
@@ -87,7 +40,7 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
87
40
|
### Fixed
|
|
88
41
|
|
|
89
42
|
- **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.
|
|
90
|
-
- **Arranque agentic sin éxito falso:** `auto_execute_ai_start` devuelve
|
|
43
|
+
- **Arranque agentic sin éxito falso:** `auto_execute_ai_start` devuelve semántica de fallo real cuando el gate bloquea y fuerza remediación explícita antes de continuar.
|
|
91
44
|
- **Contrato MCP actualizado:** la superficie HTTP del enterprise server hereda ese mismo contrato de bloqueo para `auto_execute_ai_start`.
|
|
92
45
|
- **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`.
|
|
93
46
|
|
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` puede ejecutar `pumuki install` para hooks y lifecycle). | 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,8 +50,6 @@ 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
|
-
|
|
55
53
|
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).
|
|
56
54
|
|
|
57
55
|
## 5-Minute Quick Start (Consumer)
|
|
@@ -71,7 +69,7 @@ npx --yes pumuki status
|
|
|
71
69
|
npx --yes pumuki doctor --json
|
|
72
70
|
```
|
|
73
71
|
|
|
74
|
-
Desde **6.3.63**, `npm install` en la raíz de un repo **Git** dispara un `postinstall` que ejecuta
|
|
72
|
+
Desde **6.3.63**, `npm install` en la raíz de un repo **Git** dispara un `postinstall` que ejecuta **`pumuki install` solo** (hooks `pre-commit` / `pre-push`, lifecycle, evidencia cuando aplica). **Pumuki no depende de ningún IDE** para el baseline: no toca `.cursor/` ni otros ficheros de editor por defecto. 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). Tras install, si no existía, aparece **`.pumuki/adapter.json`** con los comandos de hooks y **MCP stdio** para referencia o para clientes que los registren manualmente. Para generar también config de IDE: **`pumuki install --with-mcp --agent=cursor`** (u otro) o **`pumuki bootstrap --enterprise --agent=…`**. 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.
|
|
75
73
|
|
|
76
74
|
Fallback (equivalent in pasos separados):
|
|
77
75
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
1
|
+
v6.3.102
|
|
@@ -13,6 +13,12 @@ import {
|
|
|
13
13
|
findUndefinedInBaseTypeUnionLines,
|
|
14
14
|
findUnknownWithoutGuardLines,
|
|
15
15
|
findUnknownTypeAssertionLines,
|
|
16
|
+
findAnemicDomainModelLines,
|
|
17
|
+
findControllerBusinessLogicLines,
|
|
18
|
+
findMagicNumberLiteralLines,
|
|
19
|
+
findProductionMockArtifactUsageLines,
|
|
20
|
+
hasAnemicDomainModel,
|
|
21
|
+
hasControllerBusinessLogic,
|
|
16
22
|
hasAsyncPromiseExecutor,
|
|
17
23
|
hasConcreteDependencyInstantiation,
|
|
18
24
|
hasConsoleErrorCall,
|
|
@@ -24,6 +30,8 @@ import {
|
|
|
24
30
|
hasExplicitAnyType,
|
|
25
31
|
hasFrameworkDependencyImport,
|
|
26
32
|
hasFunctionConstructorUsage,
|
|
33
|
+
hasMagicNumberLiteral,
|
|
34
|
+
hasProductionMockArtifactUsage,
|
|
27
35
|
hasNetworkCallWithoutErrorHandling,
|
|
28
36
|
hasMixedCommandQueryClass,
|
|
29
37
|
hasMixedCommandQueryInterface,
|
|
@@ -884,6 +892,227 @@ test('hasLargeClassDeclaration detecta clases con 300 lineas o mas', () => {
|
|
|
884
892
|
assert.equal(hasLargeClassDeclaration(compactClassAst), false);
|
|
885
893
|
});
|
|
886
894
|
|
|
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
|
+
|
|
887
1116
|
test('hasRecordStringUnknownType detecta Record<string, unknown>', () => {
|
|
888
1117
|
const recordUnknownAst = {
|
|
889
1118
|
type: 'TSTypeReference',
|
|
@@ -21,6 +21,13 @@ 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)$/;
|
|
24
31
|
type AstNode = Record<string, string | number | boolean | bigint | symbol | null | Date | object>;
|
|
25
32
|
type TypeScriptSemanticNode = {
|
|
26
33
|
kind: 'class' | 'property' | 'call' | 'member';
|
|
@@ -407,6 +414,101 @@ const collectClassMethodDescriptors = (classNode: AstNode): readonly ClassMethod
|
|
|
407
414
|
return descriptors;
|
|
408
415
|
};
|
|
409
416
|
|
|
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
|
+
|
|
410
512
|
const interfaceNameFromNode = (node: AstNode): string => {
|
|
411
513
|
const idNode = node.id;
|
|
412
514
|
if (isObject(idNode) && idNode.type === 'Identifier' && typeof idNode.name === 'string') {
|
|
@@ -1546,6 +1648,96 @@ export const hasAsyncPromiseExecutor = (node: unknown): boolean => {
|
|
|
1546
1648
|
});
|
|
1547
1649
|
};
|
|
1548
1650
|
|
|
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
|
+
|
|
1549
1741
|
export const hasWithStatement = (node: unknown): boolean => {
|
|
1550
1742
|
return hasNode(node, (value) => value.type === 'WithStatement');
|
|
1551
1743
|
};
|
|
@@ -1759,6 +1951,92 @@ const nodeLineSpan = (node: unknown): number => {
|
|
|
1759
1951
|
return Math.max(0, end - start + 1);
|
|
1760
1952
|
};
|
|
1761
1953
|
|
|
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
|
+
|
|
1762
2040
|
export const hasLargeClassDeclaration = (node: unknown): boolean => {
|
|
1763
2041
|
return hasNode(node, (value) => {
|
|
1764
2042
|
if (value.type !== 'ClassDeclaration' && value.type !== 'ClassExpression') {
|