Skip to content

Commit 53af6d4

Browse files
committed
C#: Convert FileContent to XML parsing (One Shot).
1 parent fd2bfc8 commit 53af6d4

2 files changed

Lines changed: 242 additions & 122 deletions

File tree

‎csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FileContent.cs‎

Lines changed: 70 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4-
using System.Text.RegularExpressions;
4+
using System.Xml;
55
using Semmle.Util;
66
using Semmle.Util.Logging;
77

88
namespace Semmle.Extraction.CSharp.DependencyFetching
99
{
1010

11-
// <summary>
12-
// This class is used to read a set of files and decide different properties about the
13-
// content (by reading the content of the files only once).
14-
// The implementation is lazy, so the properties are only calculated when
15-
// the first property is accessed.
16-
// </summary>
17-
internal partial class FileContent
11+
/// <summary>
12+
/// Lazily reads XML files once to detect dependency and project settings without evaluating MSBuild.
13+
/// </summary>
14+
internal class FileContent
1815
{
1916
private readonly ILogger logger;
2017
private readonly IUnsafeFileReader unsafeFileReader;
@@ -135,46 +132,26 @@ internal FileContent(ILogger logger,
135132
public FileContent(ILogger logger, IEnumerable<string> files) : this(logger, files, new UnsafeFileReader())
136133
{ }
137134

138-
private static string GetGroup(ReadOnlySpan<char> input, ValueMatch valueMatch, string groupPrefix)
135+
private static string? GetAttribute(XmlReader reader, string name)
139136
{
140-
var match = input.Slice(valueMatch.Index, valueMatch.Length);
141-
var includeIndex = match.IndexOf(groupPrefix, StringComparison.OrdinalIgnoreCase);
142-
if (includeIndex == -1)
137+
while (reader.MoveToNextAttribute())
143138
{
144-
return string.Empty;
145-
}
146-
147-
match = match.Slice(includeIndex + groupPrefix.Length + 1);
148-
149-
var quoteIndex1 = match.IndexOf("\"");
150-
var quoteIndex2 = match.Slice(quoteIndex1 + 1).IndexOf("\"");
151-
152-
return match.Slice(quoteIndex1 + 1, quoteIndex2).ToString();
153-
}
154-
155-
private static bool IsGroupMatch(ReadOnlySpan<char> line, Regex regex, string groupPrefix, string value)
156-
{
157-
foreach (var valueMatch in regex.EnumerateMatches(line))
158-
{
159-
// We can't get the group from the ValueMatch, so doing it manually:
160-
if (string.Equals(GetGroup(line, valueMatch, groupPrefix), value, StringComparison.OrdinalIgnoreCase))
139+
if (string.Equals(reader.LocalName, name, StringComparison.OrdinalIgnoreCase))
161140
{
162-
return true;
141+
var value = reader.Value;
142+
reader.MoveToElement();
143+
return value;
163144
}
164145
}
165-
return false;
146+
reader.MoveToElement();
147+
return null;
166148
}
167149

168-
private void AddPackageReference(ReadOnlySpan<char> line, string groupName, Func<Regex> regex, PackageReferenceSource source)
150+
private void AddPackageReference(string? packageName, PackageReferenceSource source)
169151
{
170-
foreach (var valueMatch in regex().EnumerateMatches(line))
152+
if (!string.IsNullOrEmpty(packageName))
171153
{
172-
// We can't get the group from the ValueMatch, so doing it manually:
173-
var packageName = GetGroup(line, valueMatch, groupName).ToLowerInvariant();
174-
if (!string.IsNullOrEmpty(packageName))
175-
{
176-
allPackages.Add(new PackageReference(packageName, source));
177-
}
154+
allPackages.Add(new PackageReference(packageName.ToLowerInvariant(), source));
178155
}
179156
}
180157

@@ -185,98 +162,85 @@ private void DoInitialize()
185162
try
186163
{
187164
var isPackagesConfig = string.Equals(FileUtils.SafeGetFileName(file, logger), "packages.config", StringComparison.OrdinalIgnoreCase);
165+
using var stream = unsafeFileReader.OpenRead(file);
166+
using var reader = XmlReader.Create(stream, new XmlReaderSettings
167+
{
168+
DtdProcessing = DtdProcessing.Prohibit,
169+
XmlResolver = null,
170+
IgnoreComments = true,
171+
IgnoreProcessingInstructions = true
172+
});
188173

189-
foreach (ReadOnlySpan<char> line in unsafeFileReader.ReadLines(file))
174+
while (!reader.EOF)
190175
{
191-
// Find all the packages.
192-
if (isPackagesConfig)
193-
{
194-
AddPackageReference(line, "id", LegacyPackageReference, PackageReferenceSource.PackagesConfig);
195-
}
196-
else
176+
if (reader.NodeType != XmlNodeType.Element)
197177
{
198-
AddPackageReference(line, "Include", PackageReference, PackageReferenceSource.SdkCsProj);
178+
reader.Read();
179+
continue;
199180
}
200181

201-
// Determine if ASP.NET is used.
202-
useAspNetCoreDlls = useAspNetCoreDlls
203-
|| IsGroupMatch(line, ProjectSdk(), "Sdk", "Microsoft.NET.Sdk.Web")
204-
|| IsGroupMatch(line, FrameworkReference(), "Include", "Microsoft.AspNetCore.App");
205-
206-
// Determine if implicit usings are used.
207-
useImplicitUsings = useImplicitUsings
208-
|| line.Contains("<ImplicitUsings>enable</ImplicitUsings>".AsSpan(), StringComparison.OrdinalIgnoreCase)
209-
|| line.Contains("<ImplicitUsings>true</ImplicitUsings>".AsSpan(), StringComparison.OrdinalIgnoreCase);
210-
211-
// Determine if WPF is used.
212-
useWpf = useWpf
213-
|| line.Contains("<UseWPF>true</UseWPF>".AsSpan(), StringComparison.OrdinalIgnoreCase);
214-
215-
// Determine if Windows Forms is used.
216-
useWindowsForms = useWindowsForms
217-
|| line.Contains("<UseWindowsForms>true</UseWindowsForms>".AsSpan(), StringComparison.OrdinalIgnoreCase);
218-
219-
// Find all custom implicit usings.
220-
foreach (var valueMatch in CustomImplicitUsingDeclarations().EnumerateMatches(line))
182+
switch (reader.LocalName.ToLowerInvariant())
221183
{
222-
var ns = GetGroup(line, valueMatch, "Include");
223-
if (!string.IsNullOrEmpty(ns))
224-
{
225-
implicitUsingNamespaces.Add(ns);
226-
}
184+
case "package" when isPackagesConfig:
185+
AddPackageReference(GetAttribute(reader, "id"), PackageReferenceSource.PackagesConfig);
186+
break;
187+
case "packagereference" when !isPackagesConfig:
188+
AddPackageReference(GetAttribute(reader, "Include"), PackageReferenceSource.SdkCsProj);
189+
break;
190+
case "project":
191+
var sdk = GetAttribute(reader, "Sdk");
192+
isNewProjectStructureUsed |= sdk != null;
193+
useAspNetCoreDlls |= string.Equals(sdk, "Microsoft.NET.Sdk.Web", StringComparison.OrdinalIgnoreCase);
194+
break;
195+
case "frameworkreference":
196+
var framework = GetAttribute(reader, "Include");
197+
isNewProjectStructureUsed |= framework != null;
198+
useAspNetCoreDlls |= string.Equals(framework, "Microsoft.AspNetCore.App", StringComparison.OrdinalIgnoreCase);
199+
break;
200+
case "implicitusings":
201+
var implicitUsings = reader.ReadElementContentAsString().Trim();
202+
useImplicitUsings |= string.Equals(implicitUsings, "enable", StringComparison.OrdinalIgnoreCase)
203+
|| string.Equals(implicitUsings, "true", StringComparison.OrdinalIgnoreCase);
204+
continue;
205+
case "usewpf":
206+
useWpf |= string.Equals(reader.ReadElementContentAsString().Trim(), "true", StringComparison.OrdinalIgnoreCase);
207+
continue;
208+
case "usewindowsforms":
209+
useWindowsForms |= string.Equals(reader.ReadElementContentAsString().Trim(), "true", StringComparison.OrdinalIgnoreCase);
210+
continue;
211+
case "using":
212+
var ns = GetAttribute(reader, "Include");
213+
if (!string.IsNullOrEmpty(ns))
214+
{
215+
implicitUsingNamespaces.Add(ns);
216+
}
217+
break;
218+
case "import":
219+
isLegacyProjectStructureUsed |= GetAttribute(reader, "Project")?.EndsWith("Microsoft.CSharp.targets", StringComparison.OrdinalIgnoreCase) == true;
220+
break;
227221
}
228222

229-
// Determine project structure:
230-
isLegacyProjectStructureUsed = isLegacyProjectStructureUsed || MicrosoftCSharpTargets().IsMatch(line);
231-
isNewProjectStructureUsed = isNewProjectStructureUsed
232-
|| ProjectSdk().IsMatch(line)
233-
|| FrameworkReference().IsMatch(line);
234-
// TODO: we could also check `<Sdk Name="Microsoft.NET.Sdk" />`
223+
reader.Read();
235224
}
236225
}
237-
catch (Exception ex)
226+
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or XmlException)
238227
{
239228
logger.LogInfo($"Failed to read file {file}");
240229
logger.LogDebug($"Failed to read file {file}, exception: {ex}");
241230
}
242231
}
243232
}
244233

245-
[GeneratedRegex("(?<!<!--.*)<PackageReference.*\\sInclude=\"(.*?)\".*/?>", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
246-
private static partial Regex PackageReference();
247-
248-
[GeneratedRegex("(?<!<!--.*)<package.*\\sid=\"(.*?)\".*/?>", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
249-
private static partial Regex LegacyPackageReference();
250-
251-
[GeneratedRegex("(?<!<!--.*)<FrameworkReference.*\\sInclude=\"(.*?)\".*/?>", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
252-
private static partial Regex FrameworkReference();
253-
254-
[GeneratedRegex("(?<!<!--.*)<(.*\\s)?Project.*\\sSdk=\"(.*?)\".*/?>", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
255-
private static partial Regex ProjectSdk();
256-
257-
[GeneratedRegex("(?<!<!--.*)<Using.*\\sInclude=\"(.*?)\".*/?>", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
258-
private static partial Regex CustomImplicitUsingDeclarations();
259-
260-
[GeneratedRegex("(?<!<!--.*)<Import.*\\sProject=\".*Microsoft\\.CSharp\\.targets\".*/?>", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
261-
private static partial Regex MicrosoftCSharpTargets();
262234
}
263235

264236
internal interface IUnsafeFileReader
265237
{
266-
IEnumerable<string> ReadLines(string file);
238+
Stream OpenRead(string file);
267239
}
268240

269241
internal class UnsafeFileReader : IUnsafeFileReader
270242
{
271-
public IEnumerable<string> ReadLines(string file)
272-
{
273-
using var sr = new StreamReader(file);
274-
string? line;
275-
while ((line = sr.ReadLine()) != null)
276-
{
277-
yield return line;
278-
}
279-
}
243+
public Stream OpenRead(string file) => File.OpenRead(file);
280244
}
281245

282246
public enum PackageReferenceSource

0 commit comments

Comments
 (0)