@antoniocorraliza/foundry-arch-scanner 0.0.0-stage → 1.1.2
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/LICENSE +21 -0
- package/README.md +264 -2
- package/dist/analyzers/architecture/clean-architecture.analyzer.js +68 -0
- package/dist/analyzers/architecture/rules/boundary-leak.rule.js +49 -0
- package/dist/analyzers/architecture/rules/circular-dependency.rule.js +63 -0
- package/dist/analyzers/architecture/rules/data-layer-governance.rule.js +33 -0
- package/dist/analyzers/architecture/rules/direct-dom-manipulation.rule.js +29 -0
- package/dist/analyzers/architecture/rules/god-component.rule.js +72 -0
- package/dist/analyzers/architecture/rules/hydration-safety.rule.js +79 -0
- package/dist/analyzers/architecture/rules/presentation-leak.rule.js +43 -0
- package/dist/analyzers/architecture/rules/service-signal-encapsulation.rule.js +51 -0
- package/dist/analyzers/architecture/rules/ui-component-leak.rule.js +52 -0
- package/dist/analyzers/performance/performance.analyzer.js +54 -0
- package/dist/analyzers/performance/rules/heavy-constructor.rule.js +40 -0
- package/dist/analyzers/performance/rules/heavy-host-listener.rule.js +29 -0
- package/dist/analyzers/performance/rules/missing-on-push.rule.js +34 -0
- package/dist/analyzers/rxjs/rules/memory-leak.rule.js +82 -0
- package/dist/analyzers/rxjs/rules/nested-subscribe.rule.js +41 -0
- package/dist/analyzers/rxjs/rules/share-replay-leak.rule.js +64 -0
- package/dist/analyzers/rxjs/rules/uncompleted-subject.rule.js +55 -0
- package/dist/analyzers/rxjs/rxjs.analyzer.js +59 -0
- package/dist/analyzers/signals/rules/signal-computed-mutation.rule.js +50 -0
- package/dist/analyzers/signals/rules/signal-effect-cleanup.rule.js +32 -0
- package/dist/analyzers/signals/rules/signal-effect-infinite-loop.rule.js +72 -0
- package/dist/analyzers/signals/rules/signal-effect-state-sync.rule.js +41 -0
- package/dist/analyzers/signals/rules/signal-linked-state-anti-pattern.rule.js +46 -0
- package/dist/analyzers/signals/rules/signal-pure-computed.rule.js +103 -0
- package/dist/analyzers/signals/rules/signal-resource-candidate.rule.js +32 -0
- package/dist/analyzers/signals/rules/signal-rxjs-bridge.rule.js +58 -0
- package/dist/analyzers/signals/rules/signal-untracked-abuse.rule.js +35 -0
- package/dist/analyzers/signals/rules/signal-zone-mixing.rule.js +28 -0
- package/dist/analyzers/signals/signals.analyzer.js +62 -0
- package/dist/core/arch-scanner.engine.js +163 -0
- package/dist/core/scan.context.js +74 -0
- package/dist/core/scanner.config.js +20 -0
- package/dist/core/suppression.handler.js +34 -0
- package/dist/index.js +47 -0
- package/dist/scoring/scoring.engine.js +46 -0
- package/dist/shared/constants/architecture.constants.js +7 -0
- package/dist/shared/constants/brand.constants.js +32 -0
- package/dist/shared/constants/config.constants.js +18 -0
- package/dist/shared/constants/performance.constants.js +1 -0
- package/dist/shared/constants/rxjs.constants.js +5 -0
- package/dist/shared/constants/scoring.constants.js +5 -0
- package/dist/shared/constants/signals.constants.js +18 -0
- package/dist/shared/interfaces/issues/architecture-issue.interface.js +1 -0
- package/dist/shared/interfaces/issues/base-issue.interface.js +1 -0
- package/dist/shared/interfaces/issues/performance-issue.interface.js +1 -0
- package/dist/shared/interfaces/issues/rxjs-issue.interface.js +1 -0
- package/dist/shared/interfaces/issues/signal-issue.interface.js +1 -0
- package/dist/shared/interfaces/rules/architecture-rule.interface.js +1 -0
- package/dist/shared/interfaces/rules/performance-rule.interface.js +1 -0
- package/dist/shared/interfaces/rules/rxjs-rule.interface.js +1 -0
- package/dist/shared/interfaces/rules/signal-rule.interface.js +1 -0
- package/dist/shared/interfaces/scanner-config.interface.js +1 -0
- package/dist/shared/interfaces/scoring-result.interface.js +1 -0
- package/dist/shared/types/scoring.types.js +1 -0
- package/dist/shared/types/severity.types.js +1 -0
- package/package.json +51 -3
- package/src/analyzers/architecture/clean-architecture.analyzer.ts +76 -0
- package/src/analyzers/architecture/rules/boundary-leak.rule.ts +50 -0
- package/src/analyzers/architecture/rules/circular-dependency.rule.ts +72 -0
- package/src/analyzers/architecture/rules/data-layer-governance.rule.ts +40 -0
- package/src/analyzers/architecture/rules/direct-dom-manipulation.rule.ts +34 -0
- package/src/analyzers/architecture/rules/god-component.rule.ts +81 -0
- package/src/analyzers/architecture/rules/hydration-safety.rule.ts +89 -0
- package/src/analyzers/architecture/rules/presentation-leak.rule.ts +49 -0
- package/src/analyzers/architecture/rules/service-signal-encapsulation.rule.ts +61 -0
- package/src/analyzers/architecture/rules/ui-component-leak.rule.ts +57 -0
- package/src/analyzers/performance/performance.analyzer.ts +68 -0
- package/src/analyzers/performance/rules/heavy-constructor.rule.ts +43 -0
- package/src/analyzers/performance/rules/heavy-host-listener.rule.ts +34 -0
- package/src/analyzers/performance/rules/missing-on-push.rule.ts +40 -0
- package/src/analyzers/rxjs/rules/memory-leak.rule.ts +78 -0
- package/src/analyzers/rxjs/rules/nested-subscribe.rule.ts +43 -0
- package/src/analyzers/rxjs/rules/share-replay-leak.rule.ts +60 -0
- package/src/analyzers/rxjs/rules/uncompleted-subject.rule.ts +56 -0
- package/src/analyzers/rxjs/rxjs.analyzer.ts +80 -0
- package/src/analyzers/signals/rules/signal-computed-mutation.rule.ts +55 -0
- package/src/analyzers/signals/rules/signal-effect-cleanup.rule.ts +36 -0
- package/src/analyzers/signals/rules/signal-effect-infinite-loop.rule.ts +81 -0
- package/src/analyzers/signals/rules/signal-effect-state-sync.rule.ts +46 -0
- package/src/analyzers/signals/rules/signal-linked-state-anti-pattern.rule.ts +52 -0
- package/src/analyzers/signals/rules/signal-pure-computed.rule.ts +115 -0
- package/src/analyzers/signals/rules/signal-resource-candidate.rule.ts +36 -0
- package/src/analyzers/signals/rules/signal-rxjs-bridge.rule.ts +62 -0
- package/src/analyzers/signals/rules/signal-untracked-abuse.rule.ts +39 -0
- package/src/analyzers/signals/rules/signal-zone-mixing.rule.ts +32 -0
- package/src/analyzers/signals/signals.analyzer.ts +69 -0
- package/src/core/arch-scanner.engine.ts +173 -0
- package/src/core/scan.context.ts +66 -0
- package/src/core/scanner.config.ts +20 -0
- package/src/core/suppression.handler.ts +39 -0
- package/src/index.ts +56 -0
- package/src/scoring/scoring.engine.ts +43 -0
- package/src/shared/constants/architecture.constants.ts +7 -0
- package/src/shared/constants/brand.constants.ts +36 -0
- package/src/shared/constants/config.constants.ts +21 -0
- package/src/shared/constants/performance.constants.ts +1 -0
- package/src/shared/constants/rxjs.constants.ts +5 -0
- package/src/shared/constants/scoring.constants.ts +5 -0
- package/src/shared/constants/signals.constants.ts +20 -0
- package/src/shared/interfaces/issues/architecture-issue.interface.ts +3 -0
- package/src/shared/interfaces/issues/base-issue.interface.ts +11 -0
- package/src/shared/interfaces/issues/performance-issue.interface.ts +3 -0
- package/src/shared/interfaces/issues/rxjs-issue.interface.ts +3 -0
- package/src/shared/interfaces/issues/signal-issue.interface.ts +3 -0
- package/src/shared/interfaces/rules/architecture-rule.interface.ts +13 -0
- package/src/shared/interfaces/rules/performance-rule.interface.ts +7 -0
- package/src/shared/interfaces/rules/rxjs-rule.interface.ts +10 -0
- package/src/shared/interfaces/rules/signal-rule.interface.ts +7 -0
- package/src/shared/interfaces/scanner-config.interface.ts +20 -0
- package/src/shared/interfaces/scoring-result.interface.ts +15 -0
- package/src/shared/types/scoring.types.ts +1 -0
- package/src/shared/types/severity.types.ts +1 -0
- package/tsconfig.json +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Antonio Corraliza León
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,265 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🔎 Foundry arch scanner
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An ultra-fast, static enterprise architecture analysis and governance engine for modern Angular applications.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@antoniocorraliza/foundry-arch-scanner)
|
|
6
|
+
[](#)
|
|
7
|
+
[](#)
|
|
8
|
+
[](#license)
|
|
9
|
+
|
|
10
|
+
## 🌐 Overview
|
|
11
|
+
|
|
12
|
+
**Foundry arch scanner** is an enterprise-grade static code analysis engine designed to enforce clean architecture, modern reactivity, strict memory management in RxJS, and prevent performance bottlenecks in Angular applications.
|
|
13
|
+
|
|
14
|
+
Built on top of `ts-morph`, it is highly optimized for CI/CD pipelines, providing ultra-fast, deep AST navigation and real-time generation of corporate audit reports in multiple formats (JSON, CSV, SARIF 2.1.0).
|
|
15
|
+
|
|
16
|
+
## 🏗️ Key features & architecture design
|
|
17
|
+
|
|
18
|
+
- **AST bnavigation & performance:** Leverages intelligent `WeakMap`-based caching and `Proxy` pattern acceleration to ensure amortized O(1) performance without V8 heap leaks, enabling lightning-fast scans even in massive enterprise monorepos.
|
|
19
|
+
|
|
20
|
+
- **Zero-CFG Pragmatism:** Employs a hybrid approach combining local AST navigation and 1-hop type resolution, bypassing heavy Control Flow Graphs (CFG). This strikes the perfect balance between high-speed CI/CD execution and deep diagnostic capabilities.
|
|
21
|
+
|
|
22
|
+
- **Enterprise quality gates:** Features a sophisticated statistical scoring engine (`ScoringEngine`) with configurable criticality thresholds capable of automatically blocking pipelines (`process.exit(1)`) upon architectural non-compliance.
|
|
23
|
+
|
|
24
|
+
- **Multi-format reporting:** Natively generates structured corporate reports in **SARIF 2.1.0**, **JSON**, and **CSV** for comprehensive auditing and analytics.
|
|
25
|
+
|
|
26
|
+
## 🔬 Core analysis domains & rules
|
|
27
|
+
|
|
28
|
+
The scanner evaluates codebases across four specialized analytical blocks:
|
|
29
|
+
|
|
30
|
+
### 1. RxJS & Memory Management
|
|
31
|
+
|
|
32
|
+
- **Infinite stream leak detection:** Audits safe completion operators (e.g., `takeUntilDestroyed`) to prevent memory leaks in infinite observables.
|
|
33
|
+
|
|
34
|
+
- **Subject lifecycle governance:** Detects uncompleted Subjects in root services that can persist eternally.
|
|
35
|
+
|
|
36
|
+
- **Eternal context exclusion:** Intelligently bypasses long-lived contexts (like `AppComponent`) where subscriptions do not require teardown.
|
|
37
|
+
|
|
38
|
+
### 2. Clean architecture & coupling
|
|
39
|
+
|
|
40
|
+
- **Standalone governance:** Enforces structural limits (`maxDependencies`, `maxComponentLines`, `maxStandaloneImports`) to prevent monolithic "God Components".
|
|
41
|
+
|
|
42
|
+
- **Domain isolation:** Detects domain leaks and unauthorized logic crossing boundaries.
|
|
43
|
+
|
|
44
|
+
- **Strict singleton encapsulation:** Enforces the `ServiceSignalEncapsulationRule`, requiring public mutable state in global root services to be exposed safely via `.asReadonly()`.
|
|
45
|
+
- **SSR Safety:** Validates Server-Side Rendering compliance (`HydrationSafetyRule`) by blocking unauthorized access to native browser globals.
|
|
46
|
+
|
|
47
|
+
### 3. Angular signals & modern reactivity
|
|
48
|
+
|
|
49
|
+
- **Computed purity strictness:** Validates absolute purity within `computed()`, flagging any side effects or mutations.
|
|
50
|
+
|
|
51
|
+
- **Effect infinite loop prevention:** Statically detects circular dependency loops inside `effect()` blocks.
|
|
52
|
+
|
|
53
|
+
- **Zone mixing detection:** Flags legacy manual change detection triggers (`detectChanges`, `markForCheck`) in modernized reactive zones.
|
|
54
|
+
|
|
55
|
+
- **Migration suggestions:** Identifies legacy patterns and suggests modern primitive upgrades (`toSignal`, `resource`, `linkedSignal`).
|
|
56
|
+
|
|
57
|
+
### 4. Optimization & performance
|
|
58
|
+
|
|
59
|
+
- **Change detection strategy audit:** Enforces `MissingOnPushRule` to ensure UI components are not eagerly re-rendered.
|
|
60
|
+
|
|
61
|
+
- **Event listener frequency:** Audits high-frequency DOM events bound via `@HostListener` that trigger excessive change detection cycles.
|
|
62
|
+
|
|
63
|
+
- **Heavy initialization:** Flags expensive synchronous operations within class property initializers.
|
|
64
|
+
|
|
65
|
+
## 📊 Architecture scoring engine & quality gates
|
|
66
|
+
|
|
67
|
+
The **Foundry arch scanner** engine goes beyond simple vulnerability detection; it implements a statistical and deterministic model designed to quantify technical debt and evaluate the structural health of Angular applications. This approach allows Enterprise teams to establish strict, automated, and measurable **quality gates** in their continuous integration (CI/CD) cycles.
|
|
68
|
+
|
|
69
|
+
The internal mechanics of the _scoring engine_ for calculating architectural metrics are detailed below.
|
|
70
|
+
|
|
71
|
+
### 1. Violation severity weighting
|
|
72
|
+
|
|
73
|
+
Each violation detected in the source code is classified and penalized based on its direct impact on system stability, performance, or maintainability.
|
|
74
|
+
|
|
75
|
+
| Severity | Weight (Points) | Architectural Criterion |
|
|
76
|
+
| ------------ | --------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
77
|
+
| **CRITICAL** | $10$ | Severe violations (e.g., memory leaks, uncontrolled state mutations, circular dependencies). Potential system blockage. |
|
|
78
|
+
| **WARNING** | $3$ | Anti-patterns and excessive coupling. They don't break the app immediately but generate significant short-term technical debt. |
|
|
79
|
+
| **INFO** | $1$ | Refactoring opportunities and minor deviations from code conventions (e.g., missing use of modern primitives). |
|
|
80
|
+
|
|
81
|
+
### 2. Defect density calculation
|
|
82
|
+
|
|
83
|
+
To ensure fair evaluations regardless of repository size, the engine calculates a **defect density** for each analytical block.
|
|
84
|
+
|
|
85
|
+
To prevent a single error from disproportionately penalizing isolated components or very small projects, the engine implements a smoothing threshold (`SMALL_PROJECT_SMOOTHING_THRESHOLD = 15`). This ensures that the minimum divisor of evaluated files is always $15$.
|
|
86
|
+
|
|
87
|
+
$$
|
|
88
|
+
\text{Defect density} = \frac{\sum \text{Weights}}{\max(\text{Scanned files}, 15)}
|
|
89
|
+
$$
|
|
90
|
+
|
|
91
|
+
### 3. Block score formula
|
|
92
|
+
|
|
93
|
+
Each analytical block starts from a base score of 100 perfect points. A penalty proportional to the defect density multiplied by a strict severity factor (`DENSITY_PENALTY_MULTIPLIER = 15`) is applied. The final value is rounded and floored to avoid negative scores.
|
|
94
|
+
|
|
95
|
+
$$
|
|
96
|
+
\text{Block score} = \max\left(0, \text{round}\left(100 - (\text{Defect density} \times 15)\right)\right)
|
|
97
|
+
$$
|
|
98
|
+
|
|
99
|
+
### 4. Grade assignment
|
|
100
|
+
|
|
101
|
+
The obtained numeric score is translated into a standardized letter grade, providing an immediate reading of the project's risk level:
|
|
102
|
+
|
|
103
|
+
- **A (**$\ge 90$ **points):** Excellent architectural compliance. Clean, reactive, and highly optimized code.
|
|
104
|
+
|
|
105
|
+
- **B (**$\ge 80$ **points):** Good structural condition with minor observations.
|
|
106
|
+
|
|
107
|
+
- **C (**$\ge 70$ **points):** Moderate warnings. Technical debt detected that should be scheduled for refactoring.
|
|
108
|
+
|
|
109
|
+
- **D (**$\ge 60$ **points):** Significant architectural risk. Strongly discouraged practices present in the code.
|
|
110
|
+
|
|
111
|
+
- **F (**$< 60$ **points):** Critical non-compliance. Project integrity is compromised.
|
|
112
|
+
|
|
113
|
+
### 5. Overall project score
|
|
114
|
+
|
|
115
|
+
The overall state of the application is determined by the strict arithmetic mean of the scores obtained in the four analysis domains:
|
|
116
|
+
|
|
117
|
+
1. RxJS & Memory management
|
|
118
|
+
|
|
119
|
+
2. Clean architecture & Coupling
|
|
120
|
+
|
|
121
|
+
3. Angular signals & Modern reactivity
|
|
122
|
+
|
|
123
|
+
4. Optimization & Performance
|
|
124
|
+
|
|
125
|
+
$$
|
|
126
|
+
\text{Overall score} = \frac{\text{Score}_{\text{RxJS}} + \text{Score}_{\text{Arch}} + \text{Score}_{\text{Signals}} + \text{Score}_{\text{Perf}}}{4}
|
|
127
|
+
$$
|
|
128
|
+
|
|
129
|
+
### 🛑 CI/CD Pipeline & Quality Gate Impact
|
|
130
|
+
|
|
131
|
+
The _scorign engine_ is designed to act as an automated guardian in DevSecOps environments. The engine will automatically block the pipeline (via `process.exit(1)`) if any of the following _quality gate_ failure conditions are met:
|
|
132
|
+
|
|
133
|
+
- **Critical grade:** The overall project score drops below 60 points (Grade **F**).
|
|
134
|
+
|
|
135
|
+
- **Critical overflow:** The total number of issues categorized as `CRITICAL` exceeds the maximum limit configured in `foundry-arch-scanner.json` (`maxCriticalIssues`).
|
|
136
|
+
|
|
137
|
+
- **Warning overflow:** (Optional) If a limit for `maxWarnings` is defined and exceeded.
|
|
138
|
+
|
|
139
|
+
> **💡 CI/CD tip:** During early adoption phases or legacy code migrations, you can run the scanner with the `--warn-only` flag to generate full analytical reports without interrupting the deployment.
|
|
140
|
+
|
|
141
|
+
## 🚀 Installation & usage
|
|
142
|
+
|
|
143
|
+
The `foundry-arch-scanner` is distributed via NPM and can be executed directly or installed globally/locally within your Angular workspace.
|
|
144
|
+
|
|
145
|
+
### Direct execution (npx)
|
|
146
|
+
|
|
147
|
+
Execute the scanner directly against your Angular project path without prior installation:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npx @antoniocorraliza/foundry-arch-scanner ./src/app
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Global installation
|
|
154
|
+
|
|
155
|
+
Install the scanner globally for system-wide availability:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npm install -g @antoniocorraliza/foundry-arch-scanner
|
|
159
|
+
foundry-arch-scanner ./src/app
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Local workspace installation (Dev dependency)
|
|
163
|
+
|
|
164
|
+
Install the scanner as a development dependency for CI/CD pipeline integration:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npm install -D @antoniocorraliza/foundry-arch-scanner
|
|
168
|
+
npx foundry-arch-scanner ./src/app
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### CI/CD pipeline flags
|
|
172
|
+
|
|
173
|
+
Use the `--warn-only` flag to generate reports and calculate scores without intentionally blocking the pipeline (bypasses `process.exit(1)` on failure).
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
npx foundry-arch-scanner ./src/app --warn-only
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## ⚙️ Configuration
|
|
180
|
+
|
|
181
|
+
The engine relies on a centralized governance file (`foundry-arch-scanner.json`) located at the root of your Angular workspace. This file dictates the strict architectural thresholds, boundary rules, and CI/CD quality gates under which the application is audited.
|
|
182
|
+
|
|
183
|
+
If the file is not present, the engine will fallback to its internal default enterprise thresholds.
|
|
184
|
+
|
|
185
|
+
### Configuration File (`foundry-arch-scanner.json`)
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"architecture": {
|
|
190
|
+
"uiComponentPaths": ["src/app/shared/components/"],
|
|
191
|
+
"maxDependencies": 20,
|
|
192
|
+
"maxComponentLines": 500,
|
|
193
|
+
"allowedUiServices": [
|
|
194
|
+
"FoundryRouterService",
|
|
195
|
+
"FoundryDialogWindowService",
|
|
196
|
+
"NetworkStatusService",
|
|
197
|
+
"ThemeService",
|
|
198
|
+
"TranslationService",
|
|
199
|
+
"IconRegistryService",
|
|
200
|
+
"I18nService"
|
|
201
|
+
],
|
|
202
|
+
"domainRoots": [],
|
|
203
|
+
"forbiddenGlobals": [
|
|
204
|
+
"window",
|
|
205
|
+
"document",
|
|
206
|
+
"localStorage",
|
|
207
|
+
"sessionStorage"
|
|
208
|
+
],
|
|
209
|
+
"dataLayerSuffixes": ["Repository", "DataService", "ApiClient"],
|
|
210
|
+
"serviceSuffixes": ["Service", "Store", "Facade", "Repository"]
|
|
211
|
+
},
|
|
212
|
+
"qualityGate": {
|
|
213
|
+
"maxCriticalIssues": 0,
|
|
214
|
+
"minOverallScore": 90
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Governance parameters breakdown
|
|
220
|
+
|
|
221
|
+
The configuration schema is divided into two primary domains: **Architecture** and the **Quality gate**.
|
|
222
|
+
|
|
223
|
+
- **`architecture.uiComponentPaths`**: Target directories monitored for strict UI component governance. Components inside these paths are heavily audited to ensure they implement `ChangeDetectionStrategy.OnPush` and do not contain leaked domain logic.
|
|
224
|
+
|
|
225
|
+
- **`architecture.maxDependencies` & `architecture.maxComponentLines`**: Coupling thresholds and maximum size constraints designed to prevent the emergence of monolithic "God Components". Exceeding these limits triggers coupling warnings.
|
|
226
|
+
|
|
227
|
+
- **`architecture.allowedUiServices`**: An explicit whitelist of acceptable services that can be injected into presentation layers or UI components. Any attempt to inject unauthorized services (e.g., business logic or HTTP repositories) will be flagged as a boundary leak.
|
|
228
|
+
|
|
229
|
+
- **`architecture.forbiddenGlobals`**: Strict prohibition of native browser global objects. Accessing these globals directly without Angular's `DOCUMENT` token or `isPlatformBrowser` guards will result in a critical violation to guarantee strict Server-Side Rendering (SSR) safety and prevent memory leaks.
|
|
230
|
+
|
|
231
|
+
- **`architecture.dataLayerSuffixes` & `architecture.serviceSuffixes`**: Naming conventions utilized by the AST engine to identify, isolate, and audit data layers, state stores, and singleton services (e.g., verifying that a `*Repository` does not manually subscribe to an observable).
|
|
232
|
+
|
|
233
|
+
- **`qualityGate.maxCriticalIssues` & `qualityGate.minOverallScore`**: Execution thresholds that dictate the CI/CD pipeline blocking behavior. If the global audit grade drops below the defined minimum score (`minOverallScore`), or if the total number of critical issues exceeds `maxCriticalIssues`, the scanner will intentionally halt the pipeline with `process.exit(1)`.
|
|
234
|
+
|
|
235
|
+
## 🤫 Developer suppression layer
|
|
236
|
+
|
|
237
|
+
In scenarios where an architectural exception is necessary, developers can selectively suppress rules using inline control comments.
|
|
238
|
+
|
|
239
|
+
Suppress a rule on the same line:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
// arch-scanner-disable architecture-direct-dom-manipulation
|
|
243
|
+
const element = this.el.nativeElement;
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Suppress a rule on the next line:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
// arch-scanner-disable-next-line rxjs-memory-leak
|
|
250
|
+
this.infiniteStream$.subscribe();
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## 📦 Peer dependencies
|
|
254
|
+
|
|
255
|
+
This suite is built on top of the latest Angular features and Node environments. Make sure your environment satisfies the following:
|
|
256
|
+
|
|
257
|
+
| PACKAGE | REQ. VERSION | PURPOSE IN THE SUITE |
|
|
258
|
+
| :------------- | :--------------- | :------------------------------------------------------- |
|
|
259
|
+
| **Node.js** | `v18.13+ / v20+` | Compatible with the LTS versions required by Angular 19. |
|
|
260
|
+
| **TypeScript** | `>=5.4.0` | Required for AST compilation and parsing. |
|
|
261
|
+
| **Angular** | `>=19.0.0` | Required for modern reactivity and signals architecture. |
|
|
262
|
+
|
|
263
|
+
## ⚖️ License
|
|
264
|
+
|
|
265
|
+
Property of **Antonio Corraliza León** as part of the Foundry development suite. All rights reserved.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { PresentationLeakRule } from "./rules/presentation-leak.rule.js";
|
|
2
|
+
import { GodComponentRule } from "./rules/god-component.rule.js";
|
|
3
|
+
import { DirectDomManipulationRule } from "./rules/direct-dom-manipulation.rule.js";
|
|
4
|
+
import { UiComponentLeakRule } from "./rules/ui-component-leak.rule.js";
|
|
5
|
+
import { BoundaryLeakRule } from "./rules/boundary-leak.rule.js";
|
|
6
|
+
import { HydrationSafetyRule } from "./rules/hydration-safety.rule.js";
|
|
7
|
+
import { DataLayerGovernanceRule } from "./rules/data-layer-governance.rule.js";
|
|
8
|
+
import { ScoringEngine } from "../../scoring/scoring.engine.js";
|
|
9
|
+
import { dimText, whiteText, successText, warningText, errorText, cyanText, formatStatus } from "../../shared/constants/brand.constants.js";
|
|
10
|
+
import { CircularDependencyRule } from "./rules/circular-dependency.rule.js";
|
|
11
|
+
import { DATA_LAYER_SUFFIXES, FORBIDDEN_GLOBALS_LIST, SERVICE_SUFFIXES } from "../../shared/constants/architecture.constants.js";
|
|
12
|
+
import { SuppressionHandler } from "../../core/suppression.handler.js";
|
|
13
|
+
import { ServiceSignalEncapsulationRule } from "./rules/service-signal-encapsulation.rule.js";
|
|
14
|
+
export class CleanArchitectureAnalyzer {
|
|
15
|
+
scoringEngine = new ScoringEngine();
|
|
16
|
+
projectRules = [new CircularDependencyRule()];
|
|
17
|
+
fileRules;
|
|
18
|
+
classRules = [];
|
|
19
|
+
constructor(scannerConfig) {
|
|
20
|
+
const ssrEnabled = scannerConfig.architecture.ssrEnabled ?? false;
|
|
21
|
+
this.fileRules = [new BoundaryLeakRule(scannerConfig.architecture.domainRoots)];
|
|
22
|
+
this.classRules = [
|
|
23
|
+
new PresentationLeakRule(),
|
|
24
|
+
new GodComponentRule(scannerConfig.architecture.maxDependencies, scannerConfig.architecture.maxComponentLines, scannerConfig.architecture.maxStandaloneImports ?? 10),
|
|
25
|
+
new DirectDomManipulationRule(),
|
|
26
|
+
new UiComponentLeakRule(scannerConfig.architecture.uiComponentPaths, scannerConfig.architecture.allowedUiServices),
|
|
27
|
+
new HydrationSafetyRule(ssrEnabled, scannerConfig.architecture.forbiddenGlobals ?? FORBIDDEN_GLOBALS_LIST),
|
|
28
|
+
new DataLayerGovernanceRule(scannerConfig.architecture.dataLayerSuffixes ?? DATA_LAYER_SUFFIXES),
|
|
29
|
+
new ServiceSignalEncapsulationRule(scannerConfig.architecture.serviceSuffixes ?? SERVICE_SUFFIXES)
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
analyzeSourceFiles(sourceFiles, scanContext) {
|
|
33
|
+
console.log(` ▸ ${cyanText('clean architecture & coupling')}`);
|
|
34
|
+
let discoveredIssues = [];
|
|
35
|
+
this.projectRules.forEach((rule) => discoveredIssues.push(...rule.check(sourceFiles, scanContext)));
|
|
36
|
+
sourceFiles.forEach((sourceFile) => {
|
|
37
|
+
const filePath = sourceFile.getFilePath().replace(/\\/g, '/');
|
|
38
|
+
this.fileRules.forEach((rule) => discoveredIssues.push(...rule.check(sourceFile, scanContext)));
|
|
39
|
+
const classesInFile = sourceFile.getClasses();
|
|
40
|
+
classesInFile.forEach((classDeclaration) => {
|
|
41
|
+
const cachedClass = scanContext.applyAstCacheProxy(classDeclaration);
|
|
42
|
+
const className = cachedClass.getName() || 'AnonymousClass';
|
|
43
|
+
this.classRules.forEach((rule) => discoveredIssues.push(...rule.check(cachedClass, filePath, className, scanContext)));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
discoveredIssues = SuppressionHandler.filterSuppressedIssues(discoveredIssues, sourceFiles);
|
|
47
|
+
const blockScore = this.scoringEngine.evaluateBlock('Clean architecture & coupling', discoveredIssues, sourceFiles.length);
|
|
48
|
+
this.printExecutionReport(discoveredIssues);
|
|
49
|
+
return { issues: discoveredIssues, blockScore };
|
|
50
|
+
}
|
|
51
|
+
printExecutionReport(issues) {
|
|
52
|
+
if (issues.length === 0) {
|
|
53
|
+
console.log(` ${successText(formatStatus('[SUCCESS]'))}${whiteText('No architectural boundary violations detected.')}`);
|
|
54
|
+
console.log('');
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
issues.forEach(issue => {
|
|
58
|
+
const severityHeader = issue.severity === 'CRITICAL' ? errorText(formatStatus(`[${issue.severity}]`)) : issue.severity === 'WARNING' ? warningText(formatStatus(`[${issue.severity}]`)) : cyanText(formatStatus(`[${issue.severity}]`));
|
|
59
|
+
console.log(` ${severityHeader}${whiteText.bold(issue.ruleId)}`);
|
|
60
|
+
console.log(` ${dimText('File: ')} ${whiteText(issue.filePath)}`);
|
|
61
|
+
console.log(` ${dimText('Line: ')} ${whiteText(issue.line.toString())}`);
|
|
62
|
+
console.log(` ${dimText('Context: ')} ${whiteText(issue.className)}`);
|
|
63
|
+
console.log(` ${dimText('Reason: ')} ${whiteText(issue.message)}`);
|
|
64
|
+
console.log('');
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { SyntaxKind } from "ts-morph";
|
|
2
|
+
export class BoundaryLeakRule {
|
|
3
|
+
domainRoots;
|
|
4
|
+
constructor(domainRoots) {
|
|
5
|
+
this.domainRoots = domainRoots;
|
|
6
|
+
}
|
|
7
|
+
check(sourceFile, scanContext) {
|
|
8
|
+
const discoveredIssues = [];
|
|
9
|
+
const filePath = sourceFile.getFilePath().replace(/\\/g, '/');
|
|
10
|
+
const className = sourceFile.getClasses()[0]?.getName() || 'Global file';
|
|
11
|
+
const pathParts = filePath.split('/');
|
|
12
|
+
const currentDomain = this.extractDomain(pathParts);
|
|
13
|
+
if (!currentDomain)
|
|
14
|
+
return [];
|
|
15
|
+
const imports = sourceFile.getDescendantsOfKind(SyntaxKind.ImportDeclaration);
|
|
16
|
+
imports.forEach((importDeclaration) => {
|
|
17
|
+
const moduleSpecifier = importDeclaration.getModuleSpecifierValue();
|
|
18
|
+
if (moduleSpecifier.includes('index') || moduleSpecifier.includes('public-api'))
|
|
19
|
+
return;
|
|
20
|
+
const importedFile = importDeclaration.getModuleSpecifierSourceFile();
|
|
21
|
+
if (importedFile) {
|
|
22
|
+
const importedPath = importedFile.getFilePath().replace(/\\/g, '/');
|
|
23
|
+
if (importedPath.includes('node_modules') || importedPath.includes('shared') || importedPath.endsWith('/index.ts') || importedPath.endsWith('/public-api.ts'))
|
|
24
|
+
return;
|
|
25
|
+
const importedParts = importedPath.split('/');
|
|
26
|
+
const importedDomain = this.extractDomain(importedParts);
|
|
27
|
+
if (!importedDomain || currentDomain === importedDomain)
|
|
28
|
+
return;
|
|
29
|
+
discoveredIssues.push({
|
|
30
|
+
ruleId: 'architecture-boundary-leak',
|
|
31
|
+
severity: 'WARNING',
|
|
32
|
+
message: `Deep domain boundary leak detected: '${currentDomain}' is illegally importing internal files from '${importedDomain}'. Modules should communicate through public APIs (index.ts) rather than deep relative imports.`,
|
|
33
|
+
filePath,
|
|
34
|
+
className,
|
|
35
|
+
line: importDeclaration.getStartLineNumber()
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return discoveredIssues;
|
|
40
|
+
}
|
|
41
|
+
extractDomain(pathParts) {
|
|
42
|
+
for (const root of this.domainRoots) {
|
|
43
|
+
const index = pathParts.indexOf(root);
|
|
44
|
+
if (index !== -1 && pathParts.length > index + 2)
|
|
45
|
+
return pathParts[index + 1];
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export class CircularDependencyRule {
|
|
2
|
+
check(sourceFiles, scanContext) {
|
|
3
|
+
const graph = new Map();
|
|
4
|
+
const fileMap = new Map();
|
|
5
|
+
sourceFiles.forEach((sourceFile) => {
|
|
6
|
+
const filePath = sourceFile.getFilePath().replace(/\\/g, '/');
|
|
7
|
+
fileMap.set(filePath, sourceFile);
|
|
8
|
+
if (!graph.has(filePath))
|
|
9
|
+
graph.set(filePath, []);
|
|
10
|
+
const imports = sourceFile.getImportDeclarations();
|
|
11
|
+
imports.forEach((imp) => {
|
|
12
|
+
const importedFile = imp.getModuleSpecifierSourceFile();
|
|
13
|
+
if (importedFile) {
|
|
14
|
+
const importedPath = importedFile.getFilePath().replace(/\\/g, '/');
|
|
15
|
+
if (!importedPath.includes('node_modules') && !importedPath.includes('/dist/') && !importedPath.includes('/.angular/'))
|
|
16
|
+
graph.get(filePath).push(importedPath);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
const discoveredIssues = [];
|
|
21
|
+
const visited = new Map();
|
|
22
|
+
const pathTracker = [];
|
|
23
|
+
const reportedCycles = [];
|
|
24
|
+
const depthFirstSearch = (node) => {
|
|
25
|
+
visited.set(node, 1);
|
|
26
|
+
pathTracker.push(node);
|
|
27
|
+
const neighbors = graph.get(node) || [];
|
|
28
|
+
for (const neighbor of neighbors) {
|
|
29
|
+
const state = visited.get(neighbor) || 0;
|
|
30
|
+
if (state === 1) {
|
|
31
|
+
const cycleStartIndex = pathTracker.indexOf(neighbor);
|
|
32
|
+
const cyclePath = pathTracker.slice(cycleStartIndex);
|
|
33
|
+
cyclePath.push(neighbor);
|
|
34
|
+
const cycleKey = [...cyclePath].sort().join('->');
|
|
35
|
+
if (!reportedCycles.includes(cycleKey)) {
|
|
36
|
+
reportedCycles.push(cycleKey);
|
|
37
|
+
const sourceFile = fileMap.get(node);
|
|
38
|
+
const className = sourceFile?.getClasses()[0]?.getName() || 'ModuleGraphContext';
|
|
39
|
+
const shortNames = cyclePath.map((path) => path.split('/').pop()).join(' -> ');
|
|
40
|
+
discoveredIssues.push({
|
|
41
|
+
ruleId: 'architecture-circular-dependency',
|
|
42
|
+
severity: 'CRITICAL',
|
|
43
|
+
message: `Circular dependency chain detected: [${shortNames}]. This breaks Angular's initialization order and causes silent DI initialization bugs.`,
|
|
44
|
+
filePath: node,
|
|
45
|
+
className,
|
|
46
|
+
line: 1
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (state === 0) {
|
|
51
|
+
depthFirstSearch(neighbor);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
pathTracker.pop();
|
|
55
|
+
visited.set(node, 2);
|
|
56
|
+
};
|
|
57
|
+
for (const node of graph.keys()) {
|
|
58
|
+
if ((visited.get(node) || 0) === 0)
|
|
59
|
+
depthFirstSearch(node);
|
|
60
|
+
}
|
|
61
|
+
return discoveredIssues;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SyntaxKind } from "ts-morph";
|
|
2
|
+
export class DataLayerGovernanceRule {
|
|
3
|
+
dataLayerSuffixes;
|
|
4
|
+
constructor(dataLayerSuffixes) {
|
|
5
|
+
this.dataLayerSuffixes = dataLayerSuffixes;
|
|
6
|
+
}
|
|
7
|
+
check(classDeclaration, filePath, className, scanContext) {
|
|
8
|
+
if (!classDeclaration)
|
|
9
|
+
return [];
|
|
10
|
+
const isDataLayerClass = this.dataLayerSuffixes.some(suffix => className.endsWith(suffix));
|
|
11
|
+
if (!isDataLayerClass)
|
|
12
|
+
return [];
|
|
13
|
+
const discoveredIssues = [];
|
|
14
|
+
const callExpressions = classDeclaration.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
15
|
+
callExpressions.forEach((call) => {
|
|
16
|
+
const expression = call.getExpression();
|
|
17
|
+
if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
18
|
+
const propertyAccess = expression;
|
|
19
|
+
if (propertyAccess.getName() === 'subscribe') {
|
|
20
|
+
discoveredIssues.push({
|
|
21
|
+
ruleId: 'architecture-data-layer-governance',
|
|
22
|
+
severity: 'CRITICAL',
|
|
23
|
+
message: `An isolated side effect has been detected. Data classes ('${className}') must not call '.subscribe()'. Return pure streams (Observables/Signals) and delegate subscription and side effects to UI components or state stores.`,
|
|
24
|
+
filePath,
|
|
25
|
+
className,
|
|
26
|
+
line: call.getStartLineNumber()
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return discoveredIssues;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SyntaxKind } from "ts-morph";
|
|
2
|
+
export class DirectDomManipulationRule {
|
|
3
|
+
check(classDeclaration, filePath, className, scanContext) {
|
|
4
|
+
if (!classDeclaration)
|
|
5
|
+
return [];
|
|
6
|
+
const discoveredIssues = [];
|
|
7
|
+
const propertyAccesses = classDeclaration.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression);
|
|
8
|
+
propertyAccesses.forEach((access) => {
|
|
9
|
+
if (access.getName() === 'nativeElement') {
|
|
10
|
+
let isElementRef = false;
|
|
11
|
+
const typeText = scanContext.getTypeTextSafe(access.getExpression(), className);
|
|
12
|
+
if (typeText.includes('ElementRef'))
|
|
13
|
+
isElementRef = true;
|
|
14
|
+
const parent = access.getParent();
|
|
15
|
+
if (isElementRef && parent && parent.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
16
|
+
discoveredIssues.push({
|
|
17
|
+
ruleId: 'architecture-direct-dom-manipulation',
|
|
18
|
+
severity: 'CRITICAL',
|
|
19
|
+
message: `Direct DOM manipulation detected via 'nativeElement.${parent.asKind(SyntaxKind.PropertyAccessExpression)?.getName()}'. Use Angular's 'Renderer2' or HostBindings.`,
|
|
20
|
+
filePath,
|
|
21
|
+
className,
|
|
22
|
+
line: access.getStartLineNumber()
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return discoveredIssues;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { SyntaxKind } from "ts-morph";
|
|
2
|
+
export class GodComponentRule {
|
|
3
|
+
maxDependencies;
|
|
4
|
+
maxLines;
|
|
5
|
+
maxStandaloneImports;
|
|
6
|
+
constructor(maxDependencies, maxLines, maxStandaloneImports) {
|
|
7
|
+
this.maxDependencies = maxDependencies;
|
|
8
|
+
this.maxLines = maxLines;
|
|
9
|
+
this.maxStandaloneImports = maxStandaloneImports;
|
|
10
|
+
}
|
|
11
|
+
check(classDeclaration, filePath, className, scanContext) {
|
|
12
|
+
if (!classDeclaration)
|
|
13
|
+
return [];
|
|
14
|
+
const componentDecorator = classDeclaration.getDecorator('Component');
|
|
15
|
+
if (!componentDecorator)
|
|
16
|
+
return [];
|
|
17
|
+
const discoveredIssues = [];
|
|
18
|
+
let dependencyCount = 0;
|
|
19
|
+
let maxCtorParams = 0;
|
|
20
|
+
classDeclaration.getConstructors().forEach((ctor) => {
|
|
21
|
+
if (ctor.getParameters().length > maxCtorParams)
|
|
22
|
+
maxCtorParams = ctor.getParameters().length;
|
|
23
|
+
});
|
|
24
|
+
dependencyCount += maxCtorParams;
|
|
25
|
+
const injectCalls = classDeclaration.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => call.getExpression().getText() === 'inject');
|
|
26
|
+
dependencyCount += injectCalls.length;
|
|
27
|
+
if (dependencyCount > this.maxDependencies) {
|
|
28
|
+
discoveredIssues.push({
|
|
29
|
+
ruleId: 'architecture-god-component-dependencies',
|
|
30
|
+
severity: 'WARNING',
|
|
31
|
+
message: `Excessive coupling: Component injects ${dependencyCount} dependencies (Max allowed: ${this.maxDependencies}).`,
|
|
32
|
+
filePath,
|
|
33
|
+
className,
|
|
34
|
+
line: classDeclaration.getStartLineNumber()
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const totalLines = classDeclaration.getEndLineNumber() - classDeclaration.getStartLineNumber();
|
|
38
|
+
if (totalLines > this.maxLines) {
|
|
39
|
+
discoveredIssues.push({
|
|
40
|
+
ruleId: 'architecture-god-component-size',
|
|
41
|
+
severity: 'WARNING',
|
|
42
|
+
message: `Oversized component: Class spans ${totalLines} lines (Max allowed: ${this.maxLines}).`,
|
|
43
|
+
filePath,
|
|
44
|
+
className,
|
|
45
|
+
line: classDeclaration.getStartLineNumber()
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const decoratorArgs = componentDecorator.getArguments();
|
|
49
|
+
if (decoratorArgs.length > 0 && decoratorArgs[0].getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
50
|
+
const config = decoratorArgs[0];
|
|
51
|
+
const importsProperty = config.getProperty('imports');
|
|
52
|
+
if (importsProperty && importsProperty.getKind() === SyntaxKind.PropertyAssignment) {
|
|
53
|
+
const initializer = importsProperty.getInitializer();
|
|
54
|
+
if (initializer && initializer.getKind() === SyntaxKind.ArrayLiteralExpression) {
|
|
55
|
+
const importsArray = initializer;
|
|
56
|
+
const elementsCount = importsArray.getElements().length;
|
|
57
|
+
if (elementsCount > this.maxStandaloneImports) {
|
|
58
|
+
discoveredIssues.push({
|
|
59
|
+
ruleId: 'architecture-god-component-standalone-imports',
|
|
60
|
+
severity: 'WARNING',
|
|
61
|
+
message: `Coupling god component: The Standalone component directly imports ${elementsCount} modules/components in its metadata (Limit: ${this.maxStandaloneImports}). Consider refactoring by grouping imports into shared modules or splitting the component responsibilities.`,
|
|
62
|
+
filePath,
|
|
63
|
+
className,
|
|
64
|
+
line: importsProperty.getStartLineNumber()
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return discoveredIssues;
|
|
71
|
+
}
|
|
72
|
+
}
|