@antoniocorraliza/foundry-arch-scanner 0.0.0-stage → 1.1.1
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 +191 -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,192 @@
|
|
|
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
|
+
> **[🚀 Explore the live application](https://foundry-sandbox.vercel.app/)**
|
|
11
|
+
> _Interactive execution, real-time component evaluation, and live technical specifications for the entire Foundry suite._
|
|
12
|
+
|
|
13
|
+
## 🌐 Overview
|
|
14
|
+
|
|
15
|
+
**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.
|
|
16
|
+
|
|
17
|
+
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).
|
|
18
|
+
|
|
19
|
+
## 🏗️ Key features & architecture design
|
|
20
|
+
|
|
21
|
+
- **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.
|
|
22
|
+
|
|
23
|
+
- **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.
|
|
24
|
+
|
|
25
|
+
- **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.
|
|
26
|
+
|
|
27
|
+
- **Multi-format reporting:** Natively generates structured corporate reports in **SARIF 2.1.0**, **JSON**, and **CSV** for comprehensive auditing and analytics.
|
|
28
|
+
|
|
29
|
+
## 🔬 Core analysis domains & rules
|
|
30
|
+
|
|
31
|
+
The scanner evaluates codebases across four specialized analytical blocks:
|
|
32
|
+
|
|
33
|
+
### 1. RxJS & Memory Management
|
|
34
|
+
|
|
35
|
+
- **Infinite stream leak detection:** Audits safe completion operators (e.g., `takeUntilDestroyed`) to prevent memory leaks in infinite observables.
|
|
36
|
+
|
|
37
|
+
- **Subject lifecycle governance:** Detects uncompleted Subjects in root services that can persist eternally.
|
|
38
|
+
|
|
39
|
+
- **Eternal context exclusion:** Intelligently bypasses long-lived contexts (like `AppComponent`) where subscriptions do not require teardown.
|
|
40
|
+
|
|
41
|
+
### 2. Clean architecture & coupling
|
|
42
|
+
|
|
43
|
+
- **Standalone governance:** Enforces structural limits (`maxDependencies`, `maxComponentLines`, `maxStandaloneImports`) to prevent monolithic "God Components".
|
|
44
|
+
|
|
45
|
+
- **Domain isolation:** Detects domain leaks and unauthorized logic crossing boundaries.
|
|
46
|
+
|
|
47
|
+
- **Strict singleton encapsulation:** Enforces the `ServiceSignalEncapsulationRule`, requiring public mutable state in global root services to be exposed safely via `.asReadonly()`.
|
|
48
|
+
- **SSR Safety:** Validates Server-Side Rendering compliance (`HydrationSafetyRule`) by blocking unauthorized access to native browser globals.
|
|
49
|
+
|
|
50
|
+
### 3. Angular signals & modern reactivity
|
|
51
|
+
|
|
52
|
+
- **Computed purity strictness:** Validates absolute purity within `computed()`, flagging any side effects or mutations.
|
|
53
|
+
|
|
54
|
+
- **Effect infinite loop prevention:** Statically detects circular dependency loops inside `effect()` blocks.
|
|
55
|
+
|
|
56
|
+
- **Zone mixing detection:** Flags legacy manual change detection triggers (`detectChanges`, `markForCheck`) in modernized reactive zones.
|
|
57
|
+
|
|
58
|
+
- **Migration suggestions:** Identifies legacy patterns and suggests modern primitive upgrades (`toSignal`, `resource`, `linkedSignal`).
|
|
59
|
+
|
|
60
|
+
### 4. Optimization & performance
|
|
61
|
+
|
|
62
|
+
- **Change detection strategy audit:** Enforces `MissingOnPushRule` to ensure UI components are not eagerly re-rendered.
|
|
63
|
+
|
|
64
|
+
- **Event listener frequency:** Audits high-frequency DOM events bound via `@HostListener` that trigger excessive change detection cycles.
|
|
65
|
+
|
|
66
|
+
- **Heavy initialization:** Flags expensive synchronous operations within class property initializers.
|
|
67
|
+
|
|
68
|
+
## 🚀 Installation & usage
|
|
69
|
+
|
|
70
|
+
The `foundry-arch-scanner` is distributed via NPM and can be executed directly or installed globally/locally within your Angular workspace.
|
|
71
|
+
|
|
72
|
+
### Direct execution (npx)
|
|
73
|
+
|
|
74
|
+
Execute the scanner directly against your Angular project path without prior installation:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx @antoniocorraliza/foundry-arch-scanner ./src/app
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Global installation
|
|
81
|
+
|
|
82
|
+
Install the scanner globally for system-wide availability:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm install -g @antoniocorraliza/foundry-arch-scanner
|
|
86
|
+
foundry-arch-scanner ./src/app
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Local workspace installation (Dev dependency)
|
|
90
|
+
|
|
91
|
+
Install the scanner as a development dependency for CI/CD pipeline integration:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm install -D @antoniocorraliza/foundry-arch-scanner
|
|
95
|
+
npx foundry-arch-scanner ./src/app
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### CI/CD pipeline flags
|
|
99
|
+
|
|
100
|
+
Use the `--warn-only` flag to generate reports and calculate scores without intentionally blocking the pipeline (bypasses `process.exit(1)` on failure).
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx foundry-arch-scanner ./src/app --warn-only
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## ⚙️ Configuration
|
|
107
|
+
|
|
108
|
+
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.
|
|
109
|
+
|
|
110
|
+
If the file is not present, the engine will fallback to its internal default enterprise thresholds.
|
|
111
|
+
|
|
112
|
+
### Configuration File (`foundry-arch-scanner.json`)
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"architecture": {
|
|
117
|
+
"uiComponentPaths": ["src/app/shared/components/"],
|
|
118
|
+
"maxDependencies": 20,
|
|
119
|
+
"maxComponentLines": 500,
|
|
120
|
+
"allowedUiServices": [
|
|
121
|
+
"FoundryRouterService",
|
|
122
|
+
"FoundryDialogWindowService",
|
|
123
|
+
"NetworkStatusService",
|
|
124
|
+
"ThemeService",
|
|
125
|
+
"TranslationService",
|
|
126
|
+
"IconRegistryService",
|
|
127
|
+
"I18nService"
|
|
128
|
+
],
|
|
129
|
+
"domainRoots": [],
|
|
130
|
+
"forbiddenGlobals": [
|
|
131
|
+
"window",
|
|
132
|
+
"document",
|
|
133
|
+
"localStorage",
|
|
134
|
+
"sessionStorage"
|
|
135
|
+
],
|
|
136
|
+
"dataLayerSuffixes": ["Repository", "DataService", "ApiClient"],
|
|
137
|
+
"serviceSuffixes": ["Service", "Store", "Facade", "Repository"]
|
|
138
|
+
},
|
|
139
|
+
"qualityGate": {
|
|
140
|
+
"maxCriticalIssues": 0,
|
|
141
|
+
"minOverallScore": 90
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Governance parameters breakdown
|
|
147
|
+
|
|
148
|
+
The configuration schema is divided into two primary domains: **Architecture** and the **Quality gate**.
|
|
149
|
+
|
|
150
|
+
- **`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.
|
|
151
|
+
|
|
152
|
+
- **`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.
|
|
153
|
+
|
|
154
|
+
- **`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.
|
|
155
|
+
|
|
156
|
+
- **`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.
|
|
157
|
+
|
|
158
|
+
- **`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).
|
|
159
|
+
|
|
160
|
+
- **`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)`.
|
|
161
|
+
|
|
162
|
+
## 🤫 Developer suppression layer
|
|
163
|
+
|
|
164
|
+
In scenarios where an architectural exception is necessary, developers can selectively suppress rules using inline control comments.
|
|
165
|
+
|
|
166
|
+
Suppress a rule on the same line:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
// arch-scanner-disable architecture-direct-dom-manipulation
|
|
170
|
+
const element = this.el.nativeElement;
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Suppress a rule on the next line:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
// arch-scanner-disable-next-line rxjs-memory-leak
|
|
177
|
+
this.infiniteStream$.subscribe();
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## 📦 Peer dependencies
|
|
181
|
+
|
|
182
|
+
This suite is built on top of the latest Angular features and Node environments. Make sure your environment satisfies the following:
|
|
183
|
+
|
|
184
|
+
| PACKAGE | REQ. VERSION | PURPOSE IN THE SUITE |
|
|
185
|
+
| :------------- | :--------------- | :------------------------------------------------------- |
|
|
186
|
+
| **Node.js** | `v18.13+ / v20+` | Compatible with the LTS versions required by Angular 19. |
|
|
187
|
+
| **TypeScript** | `>=5.4.0` | Required for AST compilation and parsing. |
|
|
188
|
+
| **Angular** | `>=19.0.0` | Required for modern reactivity and signals architecture. |
|
|
189
|
+
|
|
190
|
+
## ⚖️ License
|
|
191
|
+
|
|
192
|
+
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
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { SyntaxKind, Node } from "ts-morph";
|
|
2
|
+
import { SSR_SAFE_HOOKS } from "../../../shared/constants/architecture.constants.js";
|
|
3
|
+
export class HydrationSafetyRule {
|
|
4
|
+
ssrEnabled;
|
|
5
|
+
forbiddenGlobals;
|
|
6
|
+
constructor(ssrEnabled, forbiddenGlobals) {
|
|
7
|
+
this.ssrEnabled = ssrEnabled;
|
|
8
|
+
this.forbiddenGlobals = forbiddenGlobals;
|
|
9
|
+
}
|
|
10
|
+
check(classDeclaration, filePath, className, scanContext) {
|
|
11
|
+
if (!this.ssrEnabled || !classDeclaration)
|
|
12
|
+
return [];
|
|
13
|
+
const discoveredIssues = [];
|
|
14
|
+
const identifiers = classDeclaration.getDescendantsOfKind(SyntaxKind.Identifier);
|
|
15
|
+
identifiers.forEach((identifier) => {
|
|
16
|
+
const text = identifier.getText();
|
|
17
|
+
if (this.forbiddenGlobals.includes(text)) {
|
|
18
|
+
const parent = identifier.getParent();
|
|
19
|
+
if (parent) {
|
|
20
|
+
const kind = parent.getKind();
|
|
21
|
+
if (kind === SyntaxKind.PropertyDeclaration ||
|
|
22
|
+
kind === SyntaxKind.MethodDeclaration ||
|
|
23
|
+
kind === SyntaxKind.Parameter ||
|
|
24
|
+
(kind === SyntaxKind.VariableDeclaration && parent.getNameNode() === identifier)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (kind === SyntaxKind.PropertyAccessExpression) {
|
|
28
|
+
const propertyAccess = parent;
|
|
29
|
+
const expressionText = propertyAccess.getExpression().getText();
|
|
30
|
+
if (propertyAccess.getNameNode() === identifier && expressionText !== 'globalThis' && expressionText !== 'window')
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (this.isProtectedBySsrGuardOrHook(identifier, classDeclaration))
|
|
35
|
+
return;
|
|
36
|
+
discoveredIssues.push({
|
|
37
|
+
ruleId: 'hydration-safety-global-access',
|
|
38
|
+
severity: 'CRITICAL',
|
|
39
|
+
message: `Direct access to the global browser API ('${text}') detected without validation. This will cause memory leaks or fatal errors in SSR (Server-Side Rendering). Use Angular's 'DOCUMENT' token, 'afterNextRender', or wrap the code with 'isPlatformBrowser'.`,
|
|
40
|
+
filePath,
|
|
41
|
+
className,
|
|
42
|
+
line: identifier.getStartLineNumber()
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return discoveredIssues.filter((issue, index, self) => index === self.findIndex((token) => token.line === issue.line && token.message === issue.message));
|
|
47
|
+
}
|
|
48
|
+
isProtectedBySsrGuardOrHook(identifier, classDeclaration) {
|
|
49
|
+
let current = identifier.getParent();
|
|
50
|
+
while (current && current !== classDeclaration) {
|
|
51
|
+
if (Node.isCallExpression(current)) {
|
|
52
|
+
const callerName = current.getExpression().getText();
|
|
53
|
+
if (SSR_SAFE_HOOKS.includes(callerName))
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if (Node.isIfStatement(current)) {
|
|
57
|
+
const conditionText = current.getExpression().getText();
|
|
58
|
+
if (conditionText.includes('isPlatformBrowser') && !conditionText.trim().startsWith('!')) {
|
|
59
|
+
if (current.getThenStatement().containsRange(identifier.getPos(), identifier.getEnd()))
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (Node.isMethodDeclaration(current) || Node.isConstructorDeclaration(current) || Node.isFunctionExpression(current) || Node.isArrowFunction(current)) {
|
|
64
|
+
const ifStatements = current.getDescendantsOfKind(SyntaxKind.IfStatement);
|
|
65
|
+
const hasEarlyReturnGuard = ifStatements.some((ifStatement) => {
|
|
66
|
+
const condition = ifStatement.getExpression().getText().trim();
|
|
67
|
+
const thenText = ifStatement.getThenStatement().getText();
|
|
68
|
+
const isNegatedBrowserCheck = condition.startsWith('!') && condition.includes('isPlatformBrowser');
|
|
69
|
+
const hasReturn = thenText.includes('return');
|
|
70
|
+
return isNegatedBrowserCheck && hasReturn && ifStatement.getEnd() < identifier.getPos();
|
|
71
|
+
});
|
|
72
|
+
if (hasEarlyReturnGuard)
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
current = current.getParent();
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { SyntaxKind } from "ts-morph";
|
|
2
|
+
export class PresentationLeakRule {
|
|
3
|
+
check(classDeclaration, filePath, className, scanContext) {
|
|
4
|
+
if (!classDeclaration)
|
|
5
|
+
return [];
|
|
6
|
+
const isComponent = classDeclaration.getDecorator('Component');
|
|
7
|
+
if (!isComponent)
|
|
8
|
+
return [];
|
|
9
|
+
let hasHttpClient = false;
|
|
10
|
+
let violationLine = classDeclaration.getStartLineNumber();
|
|
11
|
+
const constructors = classDeclaration.getConstructors();
|
|
12
|
+
constructors.forEach((ctor) => {
|
|
13
|
+
ctor.getParameters().forEach((parameter) => {
|
|
14
|
+
const sourcePath = scanContext.getSymbolSourceSafe(parameter, className);
|
|
15
|
+
if (sourcePath.includes('@angular/common/http')) {
|
|
16
|
+
hasHttpClient = true;
|
|
17
|
+
violationLine = parameter.getStartLineNumber();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
const callExpressions = classDeclaration.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
22
|
+
callExpressions.forEach((call) => {
|
|
23
|
+
if (call.getExpression().getText() === 'inject') {
|
|
24
|
+
const sourcePath = scanContext.getSymbolSourceSafe(call, className);
|
|
25
|
+
if (sourcePath.includes('@angular/common/http')) {
|
|
26
|
+
hasHttpClient = true;
|
|
27
|
+
violationLine = call.getStartLineNumber();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
if (hasHttpClient) {
|
|
32
|
+
return [{
|
|
33
|
+
ruleId: 'architecture-presentation-leak',
|
|
34
|
+
severity: 'CRITICAL',
|
|
35
|
+
message: `Direct '@angular/common/http' injection detected in UI Component. Network requests must be strictly delegated to Services or Repositories.`,
|
|
36
|
+
filePath,
|
|
37
|
+
className,
|
|
38
|
+
line: violationLine,
|
|
39
|
+
}];
|
|
40
|
+
}
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|