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); } } }