diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FileContent.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FileContent.cs
index 7a1a4502ce5e..0c6723f72214 100644
--- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FileContent.cs
+++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FileContent.cs
@@ -1,20 +1,17 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Text.RegularExpressions;
+using System.Xml;
using Semmle.Util;
using Semmle.Util.Logging;
namespace Semmle.Extraction.CSharp.DependencyFetching
{
- //
- // This class is used to read a set of files and decide different properties about the
- // content (by reading the content of the files only once).
- // The implementation is lazy, so the properties are only calculated when
- // the first property is accessed.
- //
- internal partial class FileContent
+ ///
+ /// Lazily reads XML files to detect dependency and project settings without evaluating MSBuild.
+ ///
+ internal class FileContent
{
private readonly ILogger logger;
private readonly IUnsafeFileReader unsafeFileReader;
@@ -23,6 +20,9 @@ internal partial class FileContent
private readonly HashSet implicitUsingNamespaces = new HashSet();
private readonly Initializer initialize;
+ ///
+ /// All detected package references in the project.
+ ///
public HashSet AllPackages
{
get
@@ -37,7 +37,7 @@ public HashSet AllPackages
///
/// True if any file in the source directory indicates that ASP.NET Core is used.
/// The following heuristic is used to decide, if ASP.NET Core is used:
- /// If any file in the source directory contains something like (this will most like be a .csproj file)
+ /// If any XML file in the source directory contains something like (this will most like be a .csproj file)
/// <Project Sdk="Microsoft.NET.Sdk.Web">
/// <FrameworkReference Include="Microsoft.AspNetCore.App"/>
///
@@ -50,6 +50,9 @@ public bool UseAspNetCoreDlls
}
}
+ ///
+ /// True if there is an indication that ASP.NET Core is used.
+ ///
public bool IsAspNetCoreDetected
{
get
@@ -60,6 +63,9 @@ public bool IsAspNetCoreDetected
private bool useImplicitUsings = false;
+ ///
+ /// True if there is an indication that implicit usings are used.
+ ///
public bool UseImplicitUsings
{
get
@@ -71,6 +77,9 @@ public bool UseImplicitUsings
private bool useWpf = false;
+ ///
+ /// True if there is an indication that WPF is used.
+ ///
public bool UseWpf
{
get
@@ -82,6 +91,9 @@ public bool UseWpf
private bool useWindowsForms = false;
+ ///
+ /// True if there is an indication that Windows Forms is used.
+ ///
public bool UseWindowsForms
{
get
@@ -93,6 +105,9 @@ public bool UseWindowsForms
private bool isLegacyProjectStructureUsed = false;
+ ///
+ /// True if there is an indication that the legacy project structure is used (e.g., non-SDK-style .csproj files).
+ ///
public bool IsLegacyProjectStructureUsed
{
get
@@ -103,6 +118,10 @@ public bool IsLegacyProjectStructureUsed
}
private bool isSdkProjectStructureUsed = false;
+
+ ///
+ /// True if there is an indication that the new project structure is used (e.g., SDK-style .csproj files).
+ ///
public bool IsSdkProjectStructureUsed
{
get
@@ -135,47 +154,134 @@ internal FileContent(ILogger logger,
public FileContent(ILogger logger, IEnumerable files) : this(logger, files, new UnsafeFileReader())
{ }
- private static string GetGroup(ReadOnlySpan input, ValueMatch valueMatch, string groupPrefix)
+ private void AddPackageReference(string? packageName, PackageReferenceSource source)
{
- var match = input.Slice(valueMatch.Index, valueMatch.Length);
- var includeIndex = match.IndexOf(groupPrefix, StringComparison.OrdinalIgnoreCase);
- if (includeIndex == -1)
+ if (!string.IsNullOrEmpty(packageName))
{
- return string.Empty;
+ allPackages.Add(new PackageReference(packageName.ToLowerInvariant(), source));
}
+ }
- match = match.Slice(includeIndex + groupPrefix.Length + 1);
+ private void ProcessFile(string file, Action processElement)
+ {
+ try
+ {
+ using var stream = unsafeFileReader.OpenRead(file);
+ using var reader = XmlReader.Create(stream, new XmlReaderSettings
+ {
+ DtdProcessing = DtdProcessing.Prohibit,
+ XmlResolver = null,
+ IgnoreComments = true,
+ IgnoreProcessingInstructions = true
+ });
- var quoteIndex1 = match.IndexOf("\"");
- var quoteIndex2 = match.Slice(quoteIndex1 + 1).IndexOf("\"");
+ while (reader.Read())
+ {
+ if (reader.NodeType != XmlNodeType.Element)
+ {
+ continue;
+ }
- return match.Slice(quoteIndex1 + 1, quoteIndex2).ToString();
+ processElement(reader);
+ }
+ }
+ catch (XmlException)
+ {
+ logger.LogDebug($"Failed to parse {file} as an XML file.");
+ }
}
- private static bool IsGroupMatch(ReadOnlySpan line, Regex regex, string groupPrefix, string value)
+ ///
+ /// Parses a packages.config XML file to extract package references.
+ ///
+ ///
+ private void ParsePackagesConfigFile(string file)
{
- foreach (var valueMatch in regex.EnumerateMatches(line))
+ ProcessFile(file, r =>
{
- // We can't get the group from the ValueMatch, so doing it manually:
- if (string.Equals(GetGroup(line, valueMatch, groupPrefix), value, StringComparison.OrdinalIgnoreCase))
+ if (r.LocalName == "package")
{
- return true;
+ AddPackageReference(r.GetAttribute("id"), PackageReferenceSource.PackagesConfig);
+ }
+ });
+ }
+
+ ///
+ /// Checks if the content of the current XML element equals any of the specified values, ignoring case.
+ /// A new reader is created to avoid modifying the state of the original reader.
+ ///
+ /// The XML reader positioned at the element to read.
+ /// True if the element content equals any of the specified values, ignoring case; otherwise, false.
+ private bool ElementContentEquals(XmlReader reader, params string[] values)
+ {
+ using (XmlReader subReader = reader.ReadSubtree())
+ {
+ if (subReader.Read())
+ {
+ var content = subReader.ReadElementContentAsString().Trim();
+ foreach (var value in values)
+ {
+ if (string.Equals(content, value, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
}
}
return false;
}
- private void AddPackageReference(ReadOnlySpan line, string groupName, Func regex, PackageReferenceSource source)
+ ///
+ /// Attempt to parse the specified file as an XML file for project dependencies and settings.
+ ///
+ ///
+ private void Parse(string file)
{
- foreach (var valueMatch in regex().EnumerateMatches(line))
+ ProcessFile(file, reader =>
{
- // We can't get the group from the ValueMatch, so doing it manually:
- var packageName = GetGroup(line, valueMatch, groupName).ToLowerInvariant();
- if (!string.IsNullOrEmpty(packageName))
+ switch (reader.LocalName.ToLowerInvariant())
{
- allPackages.Add(new PackageReference(packageName, source));
+ case "packagereference":
+ AddPackageReference(reader.GetAttribute("Include"), PackageReferenceSource.SdkCsProj);
+ break;
+ case "project":
+ var sdk = reader.GetAttribute("Sdk");
+ isSdkProjectStructureUsed |= sdk != null;
+ useAspNetCoreDlls |= string.Equals(sdk, "Microsoft.NET.Sdk.Web", StringComparison.OrdinalIgnoreCase);
+ break;
+ case "frameworkreference":
+ var framework = reader.GetAttribute("Include");
+ isSdkProjectStructureUsed |= framework != null;
+ useAspNetCoreDlls |= string.Equals(framework, "Microsoft.AspNetCore.App", StringComparison.OrdinalIgnoreCase);
+ break;
+ case "implicitusings":
+ useImplicitUsings = useImplicitUsings || ElementContentEquals(reader, "enable", "true");
+ break;
+ case "usewpf":
+ useWpf = useWpf || ElementContentEquals(reader, "true");
+ break;
+ case "usewindowsforms":
+ useWindowsForms = useWindowsForms || ElementContentEquals(reader, "true");
+ break;
+ case "using":
+ var ns = reader.GetAttribute("Include");
+ if (!string.IsNullOrEmpty(ns))
+ {
+ implicitUsingNamespaces.Add(ns);
+ }
+ break;
+ case "import":
+ var project = reader.GetAttribute("Project");
+ isSdkProjectStructureUsed = isSdkProjectStructureUsed || project is not null && reader.GetAttribute("Sdk") is not null;
+ isLegacyProjectStructureUsed = isLegacyProjectStructureUsed || project?.EndsWith("Microsoft.CSharp.targets", StringComparison.OrdinalIgnoreCase) == true;
+ break;
+ case "sdk":
+ var sdkName = reader.GetAttribute("Name");
+ isSdkProjectStructureUsed |= sdkName != null;
+ useAspNetCoreDlls |= string.Equals(sdkName, "Microsoft.NET.Sdk.Web", StringComparison.OrdinalIgnoreCase);
+ break;
}
- }
+ });
}
private void DoInitialize()
@@ -185,53 +291,13 @@ private void DoInitialize()
try
{
var isPackagesConfig = string.Equals(FileUtils.SafeGetFileName(file, logger), "packages.config", StringComparison.OrdinalIgnoreCase);
-
- foreach (ReadOnlySpan line in unsafeFileReader.ReadLines(file))
+ if (isPackagesConfig)
{
- // Find all the packages.
- if (isPackagesConfig)
- {
- AddPackageReference(line, "id", LegacyPackageReference, PackageReferenceSource.PackagesConfig);
- }
- else
- {
- AddPackageReference(line, "Include", PackageReference, PackageReferenceSource.SdkCsProj);
- }
-
- // Determine if ASP.NET is used.
- useAspNetCoreDlls = useAspNetCoreDlls
- || IsGroupMatch(line, ProjectSdk(), "Sdk", "Microsoft.NET.Sdk.Web")
- || IsGroupMatch(line, FrameworkReference(), "Include", "Microsoft.AspNetCore.App");
-
- // Determine if implicit usings are used.
- useImplicitUsings = useImplicitUsings
- || line.Contains("enable".AsSpan(), StringComparison.OrdinalIgnoreCase)
- || line.Contains("true".AsSpan(), StringComparison.OrdinalIgnoreCase);
-
- // Determine if WPF is used.
- useWpf = useWpf
- || line.Contains("true".AsSpan(), StringComparison.OrdinalIgnoreCase);
-
- // Determine if Windows Forms is used.
- useWindowsForms = useWindowsForms
- || line.Contains("true".AsSpan(), StringComparison.OrdinalIgnoreCase);
-
- // Find all custom implicit usings.
- foreach (var valueMatch in CustomImplicitUsingDeclarations().EnumerateMatches(line))
- {
- var ns = GetGroup(line, valueMatch, "Include");
- if (!string.IsNullOrEmpty(ns))
- {
- implicitUsingNamespaces.Add(ns);
- }
- }
-
- // Determine project structure:
- isLegacyProjectStructureUsed = isLegacyProjectStructureUsed || MicrosoftCSharpTargets().IsMatch(line);
- isSdkProjectStructureUsed = isSdkProjectStructureUsed
- || ProjectSdk().IsMatch(line)
- || FrameworkReference().IsMatch(line);
- // TODO: we could also check ``
+ ParsePackagesConfigFile(file);
+ }
+ else
+ {
+ Parse(file);
}
}
catch (Exception ex)
@@ -250,41 +316,16 @@ private void DoInitialize()
logger.LogInfo($"Implicit usings enabled: {useImplicitUsings}");
}
- [GeneratedRegex("(?", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
- private static partial Regex PackageReference();
-
- [GeneratedRegex("(?", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
- private static partial Regex LegacyPackageReference();
-
- [GeneratedRegex("(?", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
- private static partial Regex FrameworkReference();
-
- [GeneratedRegex("(?", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
- private static partial Regex ProjectSdk();
-
- [GeneratedRegex("(?", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
- private static partial Regex CustomImplicitUsingDeclarations();
-
- [GeneratedRegex("(?", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
- private static partial Regex MicrosoftCSharpTargets();
}
internal interface IUnsafeFileReader
{
- IEnumerable ReadLines(string file);
+ Stream OpenRead(string file);
}
internal class UnsafeFileReader : IUnsafeFileReader
{
- public IEnumerable ReadLines(string file)
- {
- using var sr = new StreamReader(file);
- string? line;
- while ((line = sr.ReadLine()) != null)
- {
- yield return line;
- }
- }
+ public Stream OpenRead(string file) => File.OpenRead(file);
}
public enum PackageReferenceSource
diff --git a/csharp/extractor/Semmle.Extraction.Tests/FileContent.cs b/csharp/extractor/Semmle.Extraction.Tests/FileContent.cs
index 8d042fcc33b2..68b092f31fb3 100644
--- a/csharp/extractor/Semmle.Extraction.Tests/FileContent.cs
+++ b/csharp/extractor/Semmle.Extraction.Tests/FileContent.cs
@@ -1,76 +1,90 @@
using Xunit;
using System;
using System.Collections.Generic;
+using System.IO;
+using System.Text;
using Semmle.Extraction.CSharp.DependencyFetching;
+using Semmle.Util.Logging;
namespace Semmle.Extraction.Tests
{
internal class UnsafeFileReaderStub : IUnsafeFileReader
{
- private readonly IEnumerable lines;
+ private readonly IReadOnlyDictionary contents;
+ public int OpenCount { get; private set; }
+ public MemoryStream? LastStream { get; private set; }
- public UnsafeFileReaderStub(IEnumerable lines)
+ public UnsafeFileReaderStub(IReadOnlyDictionary contents)
{
- this.lines = lines;
+ this.contents = contents;
}
- public IEnumerable ReadLines(string file)
+ public Stream OpenRead(string file)
{
- foreach (var line in lines)
- {
- yield return line;
- }
+ OpenCount++;
+ LastStream = new MemoryStream(Encoding.UTF8.GetBytes(contents[file]));
+ return LastStream;
}
}
internal class TestFileContent : FileContent
{
- public TestFileContent(IEnumerable lines) : base(new LoggerStub(),
- new List() { "test1.cs" },
- new UnsafeFileReaderStub(lines))
+ public TestFileContent(string content, string file = "test1.csproj") : base(new LoggerStub(),
+ new[] { file },
+ new UnsafeFileReaderStub(new Dictionary { [file] = content }))
{ }
}
public class FileContentTests
{
+ ///
+ /// Verifies that `FileContent` correctly identifies SDK project structure, package references, and no ASP.NET Core usage.
+ ///
[Fact]
- public void TestFileContent1()
+ public void TestFileContent_ProjectSdkAndNoAspNet()
{
// Setup
- var lines = new List()
- {
- "",
- "",
- "",
- "",
- "",
- ""
- };
+ var lines =
+ """
+
+
+
+
+
+
+
+ """;
var fileContent = new TestFileContent(lines);
// Execute
var allPackages = fileContent.AllPackages;
+ var isSdkProject = fileContent.IsSdkProjectStructureUsed;
var useAspNetDlls = fileContent.UseAspNetCoreDlls;
// Verify
Assert.False(useAspNetDlls);
+ Assert.True(isSdkProject);
Assert.Equal(3, allPackages.Count);
Assert.Contains(new PackageReference("DotNetAnalyzers.DocumentationAnalyzers".ToLowerInvariant(), PackageReferenceSource.SdkCsProj), allPackages);
Assert.Contains(new PackageReference("Microsoft.CodeAnalysis.NetAnalyzers".ToLowerInvariant(), PackageReferenceSource.SdkCsProj), allPackages);
Assert.Contains(new PackageReference("StyleCop.Analyzers".ToLowerInvariant(), PackageReferenceSource.SdkCsProj), allPackages);
}
+ ///
+ /// Verifies that `FileContent` correctly identifies package references and ASP.NET Core usage.
+ ///
[Fact]
public void TestFileContent2()
{
// Setup
- var lines = new List()
- {
- "",
- "",
- "",
- ""
- };
+ var lines =
+ """
+
+
+
+
+
+ """;
var fileContent = new TestFileContent(lines);
// Execute
@@ -84,131 +98,353 @@ public void TestFileContent2()
Assert.Contains(new PackageReference("StyleCop.Analyzers".ToLowerInvariant(), PackageReferenceSource.SdkCsProj), allPackages);
}
- private static void CsProjSettingsTest(string line, bool expected, Func func)
+ ///
+ /// Verifies that `FileContent` correctly identifies the use of implicit usings.
+ ///
+ [Theory]
+ [InlineData("false", false)]
+ [InlineData("true", true)]
+ [InlineData("enable", true)]
+ public void TestFileContent_ImplicitUsings(string content, bool expectedImplicitUsings)
{
// Setup
- var lines = new List()
- {
- line
- };
- var fileContent = new TestFileContent(lines);
+ var fileContent = new TestFileContent(content);
// Execute
- var actual = func(fileContent);
+ var actual = fileContent.UseImplicitUsings;
// Verify
- Assert.Equal(expected, actual);
+ Assert.Equal(expectedImplicitUsings, actual);
}
- [Fact]
- public void TestFileContent_ImplicitUsings0()
+ ///
+ /// Verifies that `FileContent` correctly identifies the use of WPF.
+ ///
+ [Theory]
+ [InlineData("false", false)]
+ [InlineData("true", true)]
+ public void TestFileContent_UseWpf(string content, bool expectedUseWpf)
+ {
+ // Setup
+ var fileContent = new TestFileContent(content);
+
+ // Execute
+ var actual = fileContent.UseWpf;
+
+ // Verify
+ Assert.Equal(expectedUseWpf, actual);
+ }
+
+ ///
+ /// Verifies that `FileContent` correctly identifies the use of Windows Forms.
+ ///
+ [Theory]
+ [InlineData("false", false)]
+ [InlineData("true", true)]
+ public void TestFileContent_UseWindowsForms(string content, bool expectedUseWindowsForms)
{
- CsProjSettingsTest("false", false, fc => fc.UseImplicitUsings);
+ // Setup
+ var fileContent = new TestFileContent(content);
+
+ // Execute
+ var actual = fileContent.UseWindowsForms;
+
+ // Verify
+ Assert.Equal(expectedUseWindowsForms, actual);
}
+ ///
+ /// Verifies that `FileContent` correctly identifies custom implicit usings.
+ ///
[Fact]
- public void TestFileContent_ImplicitUsings1()
+ public void TestFileContent_ImplicitUsingsAdditional()
{
- CsProjSettingsTest("true", true, fc => fc.UseImplicitUsings);
+ // Setup
+ var content =
+ """
+
+
+
+
+
+ """;
+ var fileContent = new TestFileContent(content);
+
+ // Execute
+ var customImplicitUsings = fileContent.CustomImplicitUsings;
+
+ // Verify
+ Assert.Equal(["Ns0.Ns1", "Ns2"], customImplicitUsings);
}
+ ///
+ /// Verifies that `FileContent` correctly identifies legacy project structure.
+ ///
[Fact]
- public void TestFileContent_ImplicitUsings2()
+ public void TestFileContent_LegacyProjectStructure()
{
- CsProjSettingsTest("enable", true, fc => fc.UseImplicitUsings);
+ // Setup
+ var input =
+ """
+
+
+
+
+
+ """;
+ var fileContent = new TestFileContent(input);
+
+ // Execute
+ var isLegacy = fileContent.IsLegacyProjectStructureUsed;
+ var isSdk = fileContent.IsSdkProjectStructureUsed;
+
+ // Verify
+ Assert.True(isLegacy);
+ Assert.False(isSdk);
}
+ ///
+ /// Verifies that `FileContent` correctly identifies SDK project structure.
+ ///
[Fact]
- public void TestFileContent_UseWpf0()
+ public void TestFileContent_SdkProjectStructure()
{
- CsProjSettingsTest("false", false, fc => fc.UseWpf);
+ // Setup
+ var input =
+ """
+
+
+ net461;net70
+
+
+ """;
+ var fileContent = new TestFileContent(input);
+
+ // Execute
+ var isLegacy = fileContent.IsLegacyProjectStructureUsed;
+ var isSdk = fileContent.IsSdkProjectStructureUsed;
+
+ // Verify
+ Assert.True(isSdk);
+ Assert.False(isLegacy);
}
+ ///
+ /// Verifies that an SDK attribute on an import indicates SDK project structure.
+ ///
[Fact]
- public void TestFileContent_UseWpf1()
+ public void TestFileContent_SdkProjectImport()
{
- CsProjSettingsTest("true", true, fc => fc.UseWpf);
+ // Setup
+ var fileContent = new TestFileContent("");
+
+ // Execute
+ var isLegacy = fileContent.IsLegacyProjectStructureUsed;
+ var isSdk = fileContent.IsSdkProjectStructureUsed;
+
+ // Verify
+ Assert.True(isSdk);
+ Assert.False(isLegacy);
}
+ ///
+ /// Verifies that an SDK element indicates SDK project structure.
+ ///
[Fact]
- public void TestFileContent_UseWindowsForms0()
+ public void TestFileContent_SdkElement()
{
- CsProjSettingsTest("false", false, fc => fc.UseWindowsForms);
+ // Setup
+ var fileContent = new TestFileContent("");
+
+ // Execute
+ var isSdk = fileContent.IsSdkProjectStructureUsed;
+ var isLegacy = fileContent.IsLegacyProjectStructureUsed;
+
+ // Verify
+ Assert.True(isSdk);
+ Assert.False(isLegacy);
}
+ ///
+ /// Verifies that an ASP.NET SDK element indicates ASP.NET Core usage.
+ ///
[Fact]
- public void TestFileContent_UseWindowsForms1()
+ public void TestFileContent_AspNetCoreSdkElement()
{
- CsProjSettingsTest("true", true, fc => fc.UseWindowsForms);
+ // Setup
+ var fileContent = new TestFileContent("");
+
+ // Execute
+ var isSdk = fileContent.IsSdkProjectStructureUsed;
+ var useAspNetCoreDlls = fileContent.UseAspNetCoreDlls;
+ var isAspNetCoreDetected = fileContent.IsAspNetCoreDetected;
+
+ // Verify
+ Assert.True(isSdk);
+ Assert.True(useAspNetCoreDlls);
+ Assert.True(isAspNetCoreDetected);
}
+ ///
+ /// Verifies that `FileContent` correctly handles XML syntax with namespaces and various elements.
+ ///
[Fact]
- public void TestFileContent_ImplicitUsingsAdditional()
+ public void TestFileContent_XmlSyntax()
{
// Setup
- var lines = new List()
- {
- "",
- "",
- "",
- };
- var fileContent = new TestFileContent(lines);
+ var input =
+ """
+
+
+
+ 2.0
+
+
+
+
+
+
+
+ ENABLE
+ true
+
+
+
+ """;
+ var fileContent = new TestFileContent(input);
// Execute
+ var allPackages = fileContent.AllPackages;
+ var isAspNetCoreDetected = fileContent.IsAspNetCoreDetected;
+ var useImplicitUsings = fileContent.UseImplicitUsings;
+ var useWpf = fileContent.UseWpf;
+ var useWindowsForms = fileContent.UseWindowsForms;
var customImplicitUsings = fileContent.CustomImplicitUsings;
// Verify
- Assert.Equal(2, customImplicitUsings.Count);
- Assert.Contains("Ns0.Ns1", customImplicitUsings);
- Assert.Contains("Ns2", customImplicitUsings);
+ Assert.Equal(2, allPackages.Count);
+ Assert.Contains(new PackageReference("my.package", PackageReferenceSource.SdkCsProj), allPackages);
+ Assert.Contains(new PackageReference("other.package", PackageReferenceSource.SdkCsProj), allPackages);
+ Assert.True(isAspNetCoreDetected);
+ Assert.True(useImplicitUsings);
+ Assert.True(useWpf);
+ Assert.True(useWindowsForms);
+ Assert.Equal(["My.Namespace"], customImplicitUsings);
}
+ ///
+ /// Verify that `FileContent` correctly ignores commented-out XML elements.
+ ///
[Fact]
- public void TestFileContent_LegacyProjectStructure()
+ public void TestFileContent_CommentsAreIgnored()
{
// Setup
var input =
"""
-
-
-
-
+
+
+
""";
- var lines = input.Split(Environment.NewLine);
- var fileContent = new TestFileContent(lines);
+ var fileContent = new TestFileContent(input);
// Execute
- var isLegacy = fileContent.IsLegacyProjectStructureUsed;
- var isNew = fileContent.IsSdkProjectStructureUsed;
+ var allPackages = fileContent.AllPackages;
+ var isAspNetCoreDetected = fileContent.IsAspNetCoreDetected;
+ var useImplicitUsings = fileContent.UseImplicitUsings;
+ var useWpf = fileContent.UseWpf;
+ var useWindowsForms = fileContent.UseWindowsForms;
+ var customImplicitUsings = fileContent.CustomImplicitUsings;
+ var isLegacyProjectStructureUsed = fileContent.IsLegacyProjectStructureUsed;
+ var isSdkProjectStructureUsed = fileContent.IsSdkProjectStructureUsed;
// Verify
- Assert.True(isLegacy);
- Assert.False(isNew);
+ Assert.Empty(allPackages);
+ Assert.Empty(customImplicitUsings);
+ Assert.False(isAspNetCoreDetected);
+ Assert.False(useImplicitUsings);
+ Assert.False(useWpf);
+ Assert.False(useWindowsForms);
+ Assert.False(isLegacyProjectStructureUsed);
+ Assert.False(isSdkProjectStructureUsed);
}
+ ///
+ /// Verify that `FileContent` correctly reads package references from a `packages.config` file.
+ ///
[Fact]
- public void TestFileContent_NewProjectStructure()
+ public void TestFileContent_PackagesConfig()
{
// Setup
var input =
"""
-
-
- net461;net70
-
-
+
+
+
+
+
+
+
+
+
""";
- var lines = input.Split(Environment.NewLine);
+ var fileContent = new TestFileContent(input, "PACKAGES.CONFIG");
- var fileContent = new TestFileContent(lines);
+ // Execute
+ var allPackages = fileContent.AllPackages;
+
+ // Verify
+ Assert.Equal(2, allPackages.Count);
+ Assert.Contains(new PackageReference("my.package", PackageReferenceSource.PackagesConfig), allPackages);
+ Assert.Contains(new PackageReference("other.package", PackageReferenceSource.PackagesConfig), allPackages);
+ }
+
+ private sealed class RecordingLogger : ILogger
+ {
+ public List Messages { get; } = new List();
+ public void Log(Severity severity, string message, int? threadId = null) => Messages.Add(message);
+ public void Dispose() { }
+ }
+
+ ///
+ /// Verify that `FileContent` correctly handles invalid XML files without preventing reading other valid files.
+ ///
+ [Theory]
+ [InlineData("This is not XML.")]
+ [InlineData("]>")]
+ public void TestFileContent_InvalidXmlDoesNotPreventReadingOtherFiles(string invalidXml)
+ {
+ // Setup
+ var logger = new RecordingLogger();
+ var stub = new UnsafeFileReaderStub(new Dictionary
+ {
+ ["invalid.csproj"] = invalidXml,
+ ["valid.csproj"] = ""
+ });
+ var fileContent = new FileContent(logger, ["invalid.csproj", "valid.csproj"], stub);
// Execute
- var isLegacy = fileContent.IsLegacyProjectStructureUsed;
- var isNew = fileContent.IsSdkProjectStructureUsed;
+ var allPackages = fileContent.AllPackages;
+ var isAspNetCoreDetected = fileContent.IsAspNetCoreDetected;
// Verify
- Assert.True(isNew);
- Assert.False(isLegacy);
+ Assert.True(isAspNetCoreDetected);
+ Assert.Contains(new PackageReference("my.package", PackageReferenceSource.SdkCsProj), allPackages);
+ Assert.Contains("Failed to parse invalid.csproj as an XML file.", logger.Messages);
+ Assert.Equal(2, stub.OpenCount);
}
}
}