Skip to content

Commit 29cbcb8

Browse files
authored
Little fixes and features (#11)
1 parent 908f7b5 commit 29cbcb8

19 files changed

Lines changed: 2086 additions & 30 deletions

.github/workflows/ci.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
build-and-test:
11+
runs-on: ${{ matrix.os }}
12+
strategy:
13+
matrix:
14+
os: [ubuntu-latest, windows-latest, macos-latest]
15+
dotnet-version: ['10.0.x']
16+
17+
steps:
18+
- name: Checkout code
19+
uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 0 # Required for Nerdbank.GitVersioning
22+
23+
- name: Setup .NET
24+
uses: actions/setup-dotnet@v4
25+
with:
26+
dotnet-version: ${{ matrix.dotnet-version }}
27+
28+
- name: Restore dependencies
29+
run: dotnet restore src/CodeMedic.sln
30+
31+
- name: Build
32+
run: dotnet build src/CodeMedic.sln --configuration Release --no-restore
33+
34+
- name: Run tests
35+
run: dotnet test src/CodeMedic.sln --configuration Release --no-build --verbosity normal --logger trx --results-directory "TestResults-${{ matrix.os }}"
36+
37+
- name: Upload test results
38+
uses: actions/upload-artifact@v4
39+
if: always()
40+
with:
41+
name: test-results-${{ matrix.os }}
42+
path: TestResults-${{ matrix.os }}/*.trx

doc/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ Welcome to the CodeMedic documentation. This folder contains technical documenta
1212
- **[Repository Health Dashboard](feature_repository-health-dashboard.md)** - Design and implementation details for the unified health analysis system
1313
- **[Bill of Materials (BOM)](feature_bill-of-materials.md)** - Specification for the comprehensive dependency and vendor inventory feature
1414

15+
### Scanning & Analysis
16+
- **[NuGet Scanning Architecture](nuget_scanning_architecture.md)** - Design and implementation of NuGet package discovery, resolution, and analysis including central package management support and version mismatch detection
17+
1518
### Implementation Details
1619
- **[CLI Skeleton Implementation](cli_skeleton_implementation.md)** - Details of the initial CLI implementation including command handling and console rendering
1720
- **[CLI Skeleton Test Results](cli_skeleton_test_results.md)** - Test results and validation of the CLI skeleton functionality

doc/nuget_scanning_architecture.md

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
# NuGet Scanning Architecture
2+
3+
## Overview
4+
5+
The NuGet scanning subsystem is responsible for discovering, parsing, and analyzing NuGet package dependencies across .NET projects in a repository. It handles both direct package references and transitive (indirect) dependencies, with special support for central package management.
6+
7+
## Architecture
8+
9+
### Components
10+
11+
```
12+
RepositoryScanner
13+
14+
├── NuGetInspector
15+
│ │
16+
│ ├── RestorePackagesAsync() # Restore packages for lock/assets files
17+
│ ├── RefreshCentralPackageVersionFiles()
18+
│ ├── ReadPackageReferences() # Parse direct PackageReference entries
19+
│ │
20+
│ └── ExtractTransitiveDependencies() # Extract indirect dependencies
21+
│ │
22+
│ ├── ExtractFromLockFile() # From packages.lock.json
23+
│ └── ExtractFromAssetsFile() # From project.assets.json
24+
25+
└── PackageVersionMismatchDetection # Cross-project version alignment
26+
```
27+
28+
### Key Classes
29+
30+
#### **RepositoryScanner**
31+
The main scanning orchestrator that:
32+
- Discovers all `.csproj` files in the repository
33+
- Delegates NuGet operations to `NuGetInspector`
34+
- Aggregates results from all projects
35+
- Generates comprehensive health reports
36+
- Detects package version mismatches across projects
37+
38+
**Key Methods:**
39+
- `ScanAsync()` - Main scanning entry point
40+
- `GenerateReport()` - Creates structured report document
41+
- `FindPackageVersionMismatches()` - Identifies inconsistent package versions
42+
43+
#### **NuGetInspector**
44+
Specialized handler for all NuGet-related operations:
45+
- Package restore and lock file generation
46+
- Direct package reference resolution
47+
- Central package management (Directory.Packages.props) support
48+
- Transitive dependency extraction
49+
50+
**Key Methods:**
51+
- `RestorePackagesAsync()` - Executes `dotnet restore` to generate lock/assets files
52+
- `RefreshCentralPackageVersionFiles()` - Discovers Directory.Packages.props files
53+
- `ReadPackageReferences()` - Parses and resolves direct dependencies
54+
- `ExtractTransitiveDependencies()` - Extracts indirect dependencies
55+
56+
### Data Flow
57+
58+
```
59+
1. Repository Scan
60+
└─> Discover all .csproj files
61+
62+
2. Package Restore
63+
└─> NuGetInspector.RestorePackagesAsync()
64+
└─> Generates: packages.lock.json or obj/project.assets.json
65+
66+
3. Central Package Discovery
67+
└─> NuGetInspector.RefreshCentralPackageVersionFiles()
68+
└─> Finds all Directory.Packages.props files
69+
└─> Caches version definitions
70+
71+
4. Parse Project Files
72+
└─> For each .csproj:
73+
74+
a. Read PropertyGroup settings
75+
└─> Target framework, output type, nullable, etc.
76+
77+
b. Read PackageReference entries
78+
└─> NuGetInspector.ReadPackageReferences()
79+
└─> Resolves missing versions from Directory.Packages.props
80+
└─> Builds Package(Name, Version) list
81+
82+
c. Read ProjectReference entries
83+
└─> Internal project-to-project references
84+
85+
d. Extract transitive dependencies
86+
└─> NuGetInspector.ExtractTransitiveDependencies()
87+
└─> Reads packages.lock.json (preferred)
88+
OR obj/project.assets.json (fallback)
89+
└─> Filters out direct deps and project refs
90+
└─> Builds TransitiveDependency list
91+
92+
5. Aggregate and Detect Mismatches
93+
└─> FindPackageVersionMismatches()
94+
└─> Groups packages by name across projects
95+
└─> Identifies versions with conflicts
96+
97+
6. Generate Report
98+
└─> ReportDocument with sections:
99+
- Summary statistics
100+
- Package version mismatches (if any)
101+
- Project listing and details
102+
- Parse errors (if any)
103+
```
104+
105+
## Central Package Management Support
106+
107+
CodeMedic supports the MSBuild [central package management](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management) feature.
108+
109+
### How It Works
110+
111+
When `Directory.Packages.props` exists at the repository root or in project directories:
112+
113+
1. **Discovery Phase**
114+
- `RefreshCentralPackageVersionFiles()` recursively finds all `Directory.Packages.props` files
115+
- Paths are cached for fast lookups
116+
117+
2. **Resolution Phase**
118+
- When a project references a package without a version attribute:
119+
```xml
120+
<PackageReference Include="Spectre.Console" />
121+
```
122+
- `ReadPackageReferences()` walks up from the project directory to the repo root
123+
- Looks for `Directory.Packages.props` in each directory
124+
- Parses `<PackageVersion>` entries:
125+
```xml
126+
<PackageVersion Include="Spectre.Console" Version="0.49.0" />
127+
```
128+
129+
3. **Version Resolution**
130+
- Version resolved from central file replaces missing inline version
131+
- Supports `Update` attribute for version overrides
132+
- Falls back to "unknown" if version cannot be resolved
133+
134+
### Example Structure
135+
136+
```
137+
Repository Root/
138+
├── Directory.Packages.props # Central definitions
139+
│ └── <PackageVersion Include="Serilog" Version="3.0.0" />
140+
141+
└── src/
142+
├── Project1/
143+
│ └── Project1.csproj
144+
│ └── <PackageReference Include="Serilog" /> ← Resolved to 3.0.0
145+
146+
└── Project2/
147+
└── Project2.csproj
148+
└── <PackageReference Include="Serilog" /> ← Resolved to 3.0.0
149+
```
150+
151+
## Package Version Mismatch Detection
152+
153+
CodeMedic identifies when different projects use different versions of the same package and recommends alignment.
154+
155+
### Algorithm
156+
157+
1. Aggregate all packages from all projects
158+
2. Group by package name (case-insensitive)
159+
3. For each package:
160+
- Collect all distinct versions used
161+
- If more than one distinct version exists:
162+
- Add to mismatches list
163+
- Record which projects use which versions
164+
165+
### Report Output
166+
167+
When mismatches are detected, the report includes a dedicated section:
168+
169+
```
170+
Package Version Mismatches
171+
──────────────────────────
172+
Align package versions across projects to avoid restore/runtime drift.
173+
174+
Packages with differing versions:
175+
• Newtonsoft.Json: Project1=12.0.3, Project2=13.0.1
176+
• Serilog: Project1=2.12.0, Project2=3.0.0
177+
```
178+
179+
## Transitive Dependencies
180+
181+
CodeMedic distinguishes between direct and transitive dependencies:
182+
183+
- **Direct dependencies** come from `<PackageReference>` in project files
184+
- **Transitive dependencies** are pulled in by direct dependencies (from lock/assets files)
185+
186+
### Source Tracking
187+
188+
For transitive dependencies, the system attempts to track which direct dependency introduced them:
189+
- Walks the NuGet graph from lock/assets file
190+
- Links transitive package back to originating direct dependency
191+
- Stored in `TransitiveDependency.SourcePackage` property
192+
193+
### Private Assets Handling
194+
195+
When a dependency is marked as private (`PrivateAssets="All"`):
196+
- Not exposed to projects that reference the current project
197+
- Still tracked and reported
198+
- Marked with `IsPrivate` flag
199+
200+
## File System Abstraction
201+
202+
To improve testability, `NuGetInspector` uses an abstraction layer for file operations:
203+
204+
### INuGetFileSystem Interface
205+
206+
```csharp
207+
public interface INuGetFileSystem
208+
{
209+
IEnumerable<string> EnumerateFiles(string path, string searchPattern, SearchOption searchOption);
210+
bool FileExists(string path);
211+
Stream OpenRead(string path);
212+
}
213+
```
214+
215+
### Implementation Options
216+
217+
- **PhysicalNuGetFileSystem** - Default implementation using actual file system
218+
- **Mock implementations** - For unit testing (inject custom implementations)
219+
220+
### Usage Example
221+
222+
```csharp
223+
// Production usage (physical file system)
224+
var inspector = new NuGetInspector(rootPath);
225+
226+
// Testing usage (mock file system)
227+
var mockFs = new MockNuGetFileSystem();
228+
var inspector = new NuGetInspector(rootPath, mockFs);
229+
```
230+
231+
## Error Handling
232+
233+
The NuGet scanning subsystem is designed to be resilient:
234+
235+
- **Package restore failures** are logged but don't stop scanning
236+
- **Missing lock/assets files** result in no transitive dependencies (acceptable)
237+
- **Central package file parse errors** are caught and logged
238+
- **Individual project failures** are recorded in `ProjectInfo.ParseErrors`
239+
- **Partial results** are preferred over complete failure
240+
241+
## Performance Considerations
242+
243+
### Optimizations
244+
245+
1. **Parallel Line Counting**
246+
- C# file counting uses `Parallel.ForEach` for multi-core utilization
247+
- Respects system processor count for optimal performance
248+
249+
2. **Caching**
250+
- Central package versions cached after first read
251+
- Prevents redundant XML parsing
252+
253+
3. **Early Returns**
254+
- Transitive extraction checks `packages.lock.json` first (faster)
255+
- Falls back to `project.assets.json` only if needed
256+
257+
4. **Streaming**
258+
- Uses streams for XML and JSON parsing (lower memory overhead)
259+
- Replaces string-based parsing
260+
261+
### Time Complexity
262+
263+
- **Per-project scanning** - O(n) where n = number of C# files
264+
- **Lock file parsing** - O(m) where m = number of dependencies
265+
- **Central package lookup** - O(log d) where d = directory depth
266+
- **Overall** - Linear with repository size
267+
268+
## Testing Strategy
269+
270+
The file system abstraction enables comprehensive unit testing:
271+
272+
```csharp
273+
[Fact]
274+
public void ReadPackageReferences_ResolvesFromCentralPackageFile()
275+
{
276+
var mockFs = new MockNuGetFileSystem();
277+
mockFs.AddFile("Directory.Packages.props",
278+
@"<PackageVersion Include=""TestPkg"" Version=""1.0.0"" />");
279+
280+
var inspector = new NuGetInspector(rootPath, mockFs);
281+
var packages = inspector.ReadPackageReferences(projectRoot, ns, projectDir);
282+
283+
Assert.Contains(packages, p => p.Name == "TestPkg" && p.Version == "1.0.0");
284+
}
285+
```
286+
287+
## Future Enhancements
288+
289+
1. **Transitive Version Conflict Detection**
290+
- Warn when transitive dependencies have conflicting versions
291+
- Suggest explicit pinning via PackageVersion entries
292+
293+
2. **Dependency Graph Visualization**
294+
- Generate mermaid diagrams showing dependency chains
295+
- Export as JSON for tooling integration
296+
297+
3. **Vulnerability Scanning**
298+
- Integrate with NuGet security advisories
299+
- Flag known vulnerabilities in dependencies
300+
301+
4. **License Analysis**
302+
- Extract and report package licenses
303+
- Detect license compliance issues
304+
305+
5. **Package Age and Maintenance**
306+
- Check when packages were last updated
307+
- Flag abandoned or dormant packages
308+
309+
## Related Documentation
310+
311+
- [Repository Health Dashboard](feature_repository-health-dashboard.md) - High-level overview
312+
- [Bill of Materials](feature_bill-of-materials.md) - Comprehensive dependency inventory
313+
- [CLI Architecture](cli_architecture.md) - Command structure and extensibility

run-health.cmd

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
@echo off
2+
REM CodeMedic Health Analysis Script (Batch)
3+
REM Runs CodeMedic against the repository with the health parameter
4+
REM Works on: Windows Command Prompt (cmd.exe)
5+
6+
setlocal enabledelayedexpansion
7+
8+
REM Get the directory where this script is located
9+
set SCRIPT_DIR=%~dp0
10+
11+
REM Change to the repository root directory
12+
cd /d "%SCRIPT_DIR%"
13+
14+
echo Running CodeMedic health analysis...
15+
echo Repository: %SCRIPT_DIR%
16+
echo.
17+
18+
dotnet run --project .\src\CodeMedic\CodeMedic.csproj -- health
19+
20+
endlocal
21+

0 commit comments

Comments
 (0)