diff --git a/.gitignore b/.gitignore
index 828acaf..0a801bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -88,4 +88,20 @@ packages
Thumbs.db
# Rider
-.idea/
\ No newline at end of file
+.idea/
+
+# Verify framework test artifacts
+*.received.txt
+*.received.*
+
+# .NET test artifacts
+*.trx
+*.coverage
+*.coveragexml
+
+# Temporary files
+test-*
+temp-*
+
+# Test results directory
+TestResults/
diff --git a/src/Snitch.Tests.Fixtures/CentralPackages/CentralPackages.csproj b/src/Snitch.Tests.Fixtures/CentralPackages/CentralPackages.csproj
new file mode 100644
index 0000000..28de610
--- /dev/null
+++ b/src/Snitch.Tests.Fixtures/CentralPackages/CentralPackages.csproj
@@ -0,0 +1,13 @@
+
+
+
+ netstandard2.0
+ latest
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Snitch.Tests.Fixtures/CentralPackages/Class1.cs b/src/Snitch.Tests.Fixtures/CentralPackages/Class1.cs
new file mode 100644
index 0000000..68fc679
--- /dev/null
+++ b/src/Snitch.Tests.Fixtures/CentralPackages/Class1.cs
@@ -0,0 +1,6 @@
+namespace CentralPackages
+{
+ public class Class1
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Snitch.Tests.Fixtures/CentralPackages/Directory.Packages.props b/src/Snitch.Tests.Fixtures/CentralPackages/Directory.Packages.props
new file mode 100644
index 0000000..6bced27
--- /dev/null
+++ b/src/Snitch.Tests.Fixtures/CentralPackages/Directory.Packages.props
@@ -0,0 +1,9 @@
+
+
+ true
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Snitch.Tests.Fixtures/CentralPackagesBuildProps/CentralPackagesBuildProps.csproj b/src/Snitch.Tests.Fixtures/CentralPackagesBuildProps/CentralPackagesBuildProps.csproj
new file mode 100644
index 0000000..28de610
--- /dev/null
+++ b/src/Snitch.Tests.Fixtures/CentralPackagesBuildProps/CentralPackagesBuildProps.csproj
@@ -0,0 +1,13 @@
+
+
+
+ netstandard2.0
+ latest
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Snitch.Tests.Fixtures/CentralPackagesBuildProps/Class1.cs b/src/Snitch.Tests.Fixtures/CentralPackagesBuildProps/Class1.cs
new file mode 100644
index 0000000..0d0a20a
--- /dev/null
+++ b/src/Snitch.Tests.Fixtures/CentralPackagesBuildProps/Class1.cs
@@ -0,0 +1,6 @@
+namespace CentralPackagesBuildProps
+{
+ public class Class1
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Snitch.Tests.Fixtures/CentralPackagesBuildProps/Directory.Build.props b/src/Snitch.Tests.Fixtures/CentralPackagesBuildProps/Directory.Build.props
new file mode 100644
index 0000000..9a7757c
--- /dev/null
+++ b/src/Snitch.Tests.Fixtures/CentralPackagesBuildProps/Directory.Build.props
@@ -0,0 +1,5 @@
+
+
+ true
+
+
\ No newline at end of file
diff --git a/src/Snitch.Tests.Fixtures/CentralPackagesBuildProps/Directory.Packages.props b/src/Snitch.Tests.Fixtures/CentralPackagesBuildProps/Directory.Packages.props
new file mode 100644
index 0000000..47fbd67
--- /dev/null
+++ b/src/Snitch.Tests.Fixtures/CentralPackagesBuildProps/Directory.Packages.props
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Snitch.Tests.Fixtures/CentralPackagesDisabled/CentralPackagesDisabled.csproj b/src/Snitch.Tests.Fixtures/CentralPackagesDisabled/CentralPackagesDisabled.csproj
new file mode 100644
index 0000000..4ea2c21
--- /dev/null
+++ b/src/Snitch.Tests.Fixtures/CentralPackagesDisabled/CentralPackagesDisabled.csproj
@@ -0,0 +1,13 @@
+
+
+
+ netstandard2.0
+ latest
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Snitch.Tests.Fixtures/CentralPackagesDisabled/Class1.cs b/src/Snitch.Tests.Fixtures/CentralPackagesDisabled/Class1.cs
new file mode 100644
index 0000000..357e719
--- /dev/null
+++ b/src/Snitch.Tests.Fixtures/CentralPackagesDisabled/Class1.cs
@@ -0,0 +1,6 @@
+namespace CentralPackagesDisabled
+{
+ public class Class1
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Snitch.Tests.Fixtures/CentralPackagesDisabled/Directory.Packages.props b/src/Snitch.Tests.Fixtures/CentralPackagesDisabled/Directory.Packages.props
new file mode 100644
index 0000000..3a414a5
--- /dev/null
+++ b/src/Snitch.Tests.Fixtures/CentralPackagesDisabled/Directory.Packages.props
@@ -0,0 +1,9 @@
+
+
+ false
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Snitch.Tests.Fixtures/CentralPackagesGlobal/CentralPackagesGlobal.csproj b/src/Snitch.Tests.Fixtures/CentralPackagesGlobal/CentralPackagesGlobal.csproj
new file mode 100644
index 0000000..267679a
--- /dev/null
+++ b/src/Snitch.Tests.Fixtures/CentralPackagesGlobal/CentralPackagesGlobal.csproj
@@ -0,0 +1,13 @@
+
+
+
+ netstandard2.0
+ latest
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Snitch.Tests.Fixtures/CentralPackagesGlobal/Class1.cs b/src/Snitch.Tests.Fixtures/CentralPackagesGlobal/Class1.cs
new file mode 100644
index 0000000..192d358
--- /dev/null
+++ b/src/Snitch.Tests.Fixtures/CentralPackagesGlobal/Class1.cs
@@ -0,0 +1,6 @@
+namespace CentralPackagesGlobal
+{
+ public class Class1
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Snitch.Tests.Fixtures/CentralPackagesGlobal/Directory.Packages.props b/src/Snitch.Tests.Fixtures/CentralPackagesGlobal/Directory.Packages.props
new file mode 100644
index 0000000..474dc8c
--- /dev/null
+++ b/src/Snitch.Tests.Fixtures/CentralPackagesGlobal/Directory.Packages.props
@@ -0,0 +1,9 @@
+
+
+ true
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Snitch.Tests/Expectations/CentralPackages.Default.verified.txt b/src/Snitch.Tests/Expectations/CentralPackages.Default.verified.txt
new file mode 100644
index 0000000..d6245ed
--- /dev/null
+++ b/src/Snitch.Tests/Expectations/CentralPackages.Default.verified.txt
@@ -0,0 +1,5 @@
+Analyzing...
+Analyzing CentralPackages.csproj
+Analyzing CentralPackages...
+
+Everything looks good!
\ No newline at end of file
diff --git a/src/Snitch.Tests/Expectations/CentralPackagesBuildProps.Default.verified.txt b/src/Snitch.Tests/Expectations/CentralPackagesBuildProps.Default.verified.txt
new file mode 100644
index 0000000..ba55078
--- /dev/null
+++ b/src/Snitch.Tests/Expectations/CentralPackagesBuildProps.Default.verified.txt
@@ -0,0 +1,5 @@
+Analyzing...
+Analyzing CentralPackagesBuildProps.csproj
+Analyzing CentralPackagesBuildProps...
+
+Everything looks good!
\ No newline at end of file
diff --git a/src/Snitch.Tests/Expectations/CentralPackagesDisabled.Default.verified.txt b/src/Snitch.Tests/Expectations/CentralPackagesDisabled.Default.verified.txt
new file mode 100644
index 0000000..a1ea245
--- /dev/null
+++ b/src/Snitch.Tests/Expectations/CentralPackagesDisabled.Default.verified.txt
@@ -0,0 +1,5 @@
+Analyzing...
+Analyzing CentralPackagesDisabled.csproj
+Analyzing CentralPackagesDisabled...
+
+Everything looks good!
\ No newline at end of file
diff --git a/src/Snitch.Tests/Expectations/CentralPackagesGlobal.Default.verified.txt b/src/Snitch.Tests/Expectations/CentralPackagesGlobal.Default.verified.txt
new file mode 100644
index 0000000..1fa2014
--- /dev/null
+++ b/src/Snitch.Tests/Expectations/CentralPackagesGlobal.Default.verified.txt
@@ -0,0 +1,5 @@
+Analyzing...
+Analyzing CentralPackagesGlobal.csproj
+Analyzing CentralPackagesGlobal...
+
+Everything looks good!
\ No newline at end of file
diff --git a/src/Snitch.Tests/ProgramTests.cs b/src/Snitch.Tests/ProgramTests.cs
index f264bcd..2344ffe 100644
--- a/src/Snitch.Tests/ProgramTests.cs
+++ b/src/Snitch.Tests/ProgramTests.cs
@@ -222,5 +222,69 @@ public async Task Should_Return_Expected_Result_For_FSharp_Not_Specifying_Framew
exitCode.ShouldBe(0);
await Verifier.Verify(output);
}
+
+ [Fact]
+ [Expectation("CentralPackages", "Default")]
+ public async Task Should_Return_Expected_Result_For_CentralPackages()
+ {
+ // Given
+ var fixture = new Fixture();
+ var project = Fixture.GetPath("CentralPackages/CentralPackages.csproj");
+
+ // When
+ var (exitCode, output) = await Fixture.Run(project);
+
+ // Then
+ exitCode.ShouldBe(0);
+ await Verifier.Verify(output);
+ }
+
+ [Fact]
+ [Expectation("CentralPackagesGlobal", "Default")]
+ public async Task Should_Return_Expected_Result_For_CentralPackages_With_GlobalPackageReference()
+ {
+ // Given
+ var fixture = new Fixture();
+ var project = Fixture.GetPath("CentralPackagesGlobal/CentralPackagesGlobal.csproj");
+
+ // When
+ var (exitCode, output) = await Fixture.Run(project);
+
+ // Then
+ exitCode.ShouldBe(0);
+ await Verifier.Verify(output);
+ }
+
+ [Fact]
+ [Expectation("CentralPackagesDisabled", "Default")]
+ public async Task Should_Return_Expected_Result_For_CentralPackages_When_Disabled()
+ {
+ // Given
+ var fixture = new Fixture();
+ var project = Fixture.GetPath("CentralPackagesDisabled/CentralPackagesDisabled.csproj");
+
+ // When
+ var (exitCode, output) = await Fixture.Run(project);
+
+ // Then
+ exitCode.ShouldBe(0);
+ await Verifier.Verify(output);
+ }
+
+ [Fact]
+ [Expectation("CentralPackagesBuildProps", "Default")]
+ public async Task Should_Return_Expected_Result_For_CentralPackages_With_BuildProps()
+ {
+ // Given
+ var fixture = new Fixture();
+ var project = Fixture.GetPath("CentralPackagesBuildProps/CentralPackagesBuildProps.csproj");
+
+ // When
+ var (exitCode, output) = await Fixture.Run(project);
+
+ // Then
+ exitCode.ShouldBe(0);
+ await Verifier.Verify(output);
+ }
}
}
diff --git a/src/Snitch/Analysis/CentralPackageManager.cs b/src/Snitch/Analysis/CentralPackageManager.cs
new file mode 100644
index 0000000..cab2533
--- /dev/null
+++ b/src/Snitch/Analysis/CentralPackageManager.cs
@@ -0,0 +1,198 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Xml.Linq;
+
+namespace Snitch.Analysis
+{
+ internal sealed class CentralPackageManager
+ {
+ private readonly Dictionary _packageVersions;
+ private readonly Dictionary _globalPackageReferences;
+ private readonly bool _isCentralManagementEnabled;
+
+ public bool IsCentralManagementEnabled => _isCentralManagementEnabled;
+
+ public CentralPackageManager(string projectPath)
+ {
+ if (projectPath == null)
+ {
+ throw new ArgumentNullException(nameof(projectPath));
+ }
+
+ _packageVersions = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ _globalPackageReferences = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ var directoryPackagesPath = FindDirectoryPackagesProps(projectPath);
+ if (!string.IsNullOrEmpty(directoryPackagesPath))
+ {
+ _isCentralManagementEnabled = LoadDirectoryPackages(directoryPackagesPath);
+ }
+ }
+
+ public string? GetPackageVersion(string packageName)
+ {
+ if (_packageVersions.TryGetValue(packageName, out var version))
+ {
+ return version;
+ }
+
+ return null;
+ }
+
+ public string? GetGlobalPackageVersion(string packageName)
+ {
+ if (_globalPackageReferences.TryGetValue(packageName, out var version))
+ {
+ return version;
+ }
+
+ return null;
+ }
+
+ public IEnumerable GetGlobalPackages()
+ {
+ return _globalPackageReferences
+ .Where(kvp => !string.IsNullOrEmpty(kvp.Value))
+ .Select(kvp => new Package(kvp.Key, kvp.Value, null));
+ }
+
+ public bool HasPackageVersion(string packageName)
+ {
+ return _packageVersions.ContainsKey(packageName);
+ }
+
+ public bool HasGlobalPackageReference(string packageName)
+ {
+ return _globalPackageReferences.ContainsKey(packageName);
+ }
+
+ private string? FindDirectoryPackagesProps(string projectPath)
+ {
+ var currentDirectory = Path.GetDirectoryName(projectPath);
+
+ while (currentDirectory != null)
+ {
+ var directoryPackagesPath = Path.Combine(currentDirectory, "Directory.Packages.props");
+ if (File.Exists(directoryPackagesPath))
+ {
+ return directoryPackagesPath;
+ }
+
+ var parentDirectory = Directory.GetParent(currentDirectory);
+ if (parentDirectory == null)
+ {
+ break;
+ }
+
+ currentDirectory = parentDirectory.FullName;
+ }
+
+ return null;
+ }
+
+ private bool LoadDirectoryPackages(string directoryPackagesPath)
+ {
+ try
+ {
+ var document = XDocument.Load(directoryPackagesPath);
+ var root = document.Root;
+
+ if (root == null)
+ {
+ return false;
+ }
+
+ var isCentralManagementEnabled = IsCentralManagementEnabledInFile(root);
+
+ if (!isCentralManagementEnabled)
+ {
+ var directoryBuildPropsPath = Path.Combine(Path.GetDirectoryName(directoryPackagesPath)!, "Directory.Build.props");
+ if (File.Exists(directoryBuildPropsPath))
+ {
+ isCentralManagementEnabled = IsCentralManagementEnabledInPropsFile(directoryBuildPropsPath);
+ }
+ }
+
+ if (!isCentralManagementEnabled)
+ {
+ return false;
+ }
+
+ var itemGroups = root.Elements("ItemGroup");
+ foreach (var itemGroup in itemGroups)
+ {
+ var packageVersions = itemGroup.Elements("PackageVersion");
+ foreach (var packageVersion in packageVersions)
+ {
+ var include = packageVersion.Attribute("Include")?.Value;
+ var version = packageVersion.Attribute("Version")?.Value;
+
+ if (!string.IsNullOrEmpty(include) && !string.IsNullOrEmpty(version))
+ {
+ _packageVersions[include] = version;
+ }
+ }
+
+ var globalPackageReferences = itemGroup.Elements("GlobalPackageReference");
+ foreach (var globalPackageReference in globalPackageReferences)
+ {
+ var include = globalPackageReference.Attribute("Include")?.Value;
+ var version = globalPackageReference.Attribute("Version")?.Value;
+
+ if (!string.IsNullOrEmpty(include) && !string.IsNullOrEmpty(version))
+ {
+ _globalPackageReferences[include] = version;
+ }
+ }
+ }
+
+ return isCentralManagementEnabled;
+ }
+ catch (Exception)
+ {
+ // If we can't parse the file, just continue without central management
+ return false;
+ }
+ }
+
+ private static bool IsCentralManagementEnabledInFile(XElement root)
+ {
+ var propertyGroups = root.Elements("PropertyGroup");
+ foreach (var propertyGroup in propertyGroups)
+ {
+ var centralManagementElement = propertyGroup.Element("ManagePackageVersionsCentrally");
+ if (centralManagementElement != null &&
+ bool.TryParse(centralManagementElement.Value, out var isCentralManagement) &&
+ isCentralManagement)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool IsCentralManagementEnabledInPropsFile(string propsFilePath)
+ {
+ try
+ {
+ var document = XDocument.Load(propsFilePath);
+ var root = document.Root;
+
+ if (root == null)
+ {
+ return false;
+ }
+
+ return IsCentralManagementEnabledInFile(root);
+ }
+ catch (Exception)
+ {
+ // If we can't parse the file, just continue without central management
+ return false;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Snitch/Analysis/ProjectBuilder.cs b/src/Snitch/Analysis/ProjectBuilder.cs
index dbd30c3..8e15f74 100644
--- a/src/Snitch/Analysis/ProjectBuilder.cs
+++ b/src/Snitch/Analysis/ProjectBuilder.cs
@@ -98,13 +98,17 @@ private Project Build(
// Add the project to the built list.
built.Add(Path.GetFileName(path), project);
+ // Initialize Central Package Manager
+ var centralPackageManager = new CentralPackageManager(path);
+
// Get the package references.
foreach (var packageReference in result.PackageReferences)
{
- if (packageReference.Value.TryGetValue("Version", out var version))
- {
- var privateAssets = packageReference.Value.GetValueOrDefault("PrivateAssets");
+ var privateAssets = packageReference.Value.GetValueOrDefault("PrivateAssets");
+ var version = ResolvePackageVersion(packageReference, centralPackageManager);
+ if (!string.IsNullOrEmpty(version))
+ {
project.Packages.Add(new Package(packageReference.Key, version, privateAssets));
}
}
@@ -166,5 +170,35 @@ private Project Build(
return results.FirstOrDefault();
}
+
+ private static string? ResolvePackageVersion(
+ KeyValuePair> packageReference,
+ CentralPackageManager centralPackageManager)
+ {
+ // Try to get version from PackageReference first
+ if (packageReference.Value.TryGetValue("Version", out var version) && !string.IsNullOrEmpty(version))
+ {
+ return version;
+ }
+
+ // If no version and central management is enabled, try to get from Directory.Packages.props
+ if (centralPackageManager.IsCentralManagementEnabled)
+ {
+ var centralVersion = centralPackageManager.GetPackageVersion(packageReference.Key);
+ if (!string.IsNullOrEmpty(centralVersion))
+ {
+ return centralVersion;
+ }
+
+ // Check if this is a GlobalPackageReference (these might be processed as PackageReference without version)
+ var globalVersion = centralPackageManager.GetGlobalPackageVersion(packageReference.Key);
+ if (!string.IsNullOrEmpty(globalVersion))
+ {
+ return globalVersion;
+ }
+ }
+
+ return null;
+ }
}
}
\ No newline at end of file