diff --git a/src/Analyzer/ReferenceTrimmerAnalyzer.cs b/src/Analyzer/ReferenceTrimmerAnalyzer.cs
index 52ff60d..0fa619f 100644
--- a/src/Analyzer/ReferenceTrimmerAnalyzer.cs
+++ b/src/Analyzer/ReferenceTrimmerAnalyzer.cs
@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Text;
using ReferenceTrimmer.Shared;
namespace ReferenceTrimmer.Analyzer;
@@ -44,6 +45,14 @@ public class ReferenceTrimmerAnalyzer : DiagnosticAnalyzer
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
+ private static readonly DiagnosticDescriptor RT9999Descriptor = new(
+ "RT9999",
+ "ReferenceTrimmer internal error",
+ "ReferenceTrimmer encountered an unexpected error: {0}. Please file a bug at https://github.com/dfederm/ReferenceTrimmer/issues",
+ "ReferenceTrimmer",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
///
/// The supported diagnostics.
///
@@ -52,7 +61,8 @@ public override ImmutableArray SupportedDiagnostics
RT0000Descriptor,
RT0001Descriptor,
RT0002Descriptor,
- RT0003Descriptor);
+ RT0003Descriptor,
+ RT9999Descriptor);
///
public override void Initialize(AnalysisContext context)
@@ -64,14 +74,35 @@ public override void Initialize(AnalysisContext context)
private static void DumpUsedReferences(CompilationAnalysisContext context)
{
- string? declaredReferencesPath = GetDeclaredReferencesPath(context);
- if (declaredReferencesPath == null)
+ try
+ {
+ DumpUsedReferencesCore(context);
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(RT9999Descriptor, Location.None, ex.Message));
+ }
+ }
+
+ private static void DumpUsedReferencesCore(CompilationAnalysisContext context)
+ {
+ AdditionalText? declaredReferencesFile = GetDeclaredReferencesFile(context);
+ if (declaredReferencesFile == null)
{
// Reference Trimmer is disabled
return;
}
- DeclaredReferences declaredReferences = DeclaredReferences.ReadFromFile(declaredReferencesPath);
+ SourceText? sourceText = declaredReferencesFile.GetText(context.CancellationToken);
+ if (sourceText == null)
+ {
+ return;
+ }
+
Compilation compilation = context.Compilation;
if (compilation.SyntaxTrees.FirstOrDefault()?.Options.DocumentationMode == DocumentationMode.None)
{
@@ -105,11 +136,11 @@ private static void DumpUsedReferences(CompilationAnalysisContext context)
}
}
- DumpReferencesInfo(usedReferences, unusedReferences, declaredReferencesPath);
+ DumpReferencesInfo(usedReferences, unusedReferences, declaredReferencesFile.Path);
}
Dictionary> packageAssembliesDict = new(StringComparer.OrdinalIgnoreCase);
- foreach (DeclaredReference declaredReference in declaredReferences.References)
+ foreach (DeclaredReference declaredReference in ReadDeclaredReferences(sourceText))
{
switch (declaredReference.Kind)
{
@@ -150,7 +181,7 @@ private static void DumpUsedReferences(CompilationAnalysisContext context)
{
string packageName = kvp.Key;
List packageAssemblies = kvp.Value;
- if (!packageAssemblies.Any(usedReferences.Contains))
+ if (!usedReferences.Overlaps(packageAssemblies))
{
context.ReportDiagnostic(Diagnostic.Create(RT0003Descriptor, Location.None, packageName));
}
@@ -188,16 +219,83 @@ private static void WriteFile(string filePath, string text)
}
}
- private static string? GetDeclaredReferencesPath(CompilationAnalysisContext context)
+ private static AdditionalText? GetDeclaredReferencesFile(CompilationAnalysisContext context)
{
foreach (AdditionalText additionalText in context.Options.AdditionalFiles)
{
if (Path.GetFileName(additionalText.Path).Equals(DeclaredReferencesFileName, StringComparison.Ordinal))
{
- return additionalText.Path;
+ return additionalText;
}
}
return null;
}
+
+ // File format: tab-separated fields (AssemblyPath, Kind, Spec), one reference per line.
+ // Keep in sync with SaveDeclaredReferences in CollectDeclaredReferencesTask.cs.
+ private static IEnumerable ReadDeclaredReferences(SourceText sourceText)
+ {
+ foreach (TextLine textLine in sourceText.Lines)
+ {
+ TextSpan lineSpan = textLine.Span;
+ if (lineSpan.Length == 0)
+ {
+ continue;
+ }
+
+ // Find tab delimiters within the line span to avoid full-line ToString + Split.
+ int start = lineSpan.Start;
+ int end = lineSpan.End;
+
+ int firstTab = -1;
+ int secondTab = -1;
+ for (int i = start; i < end; i++)
+ {
+ if (sourceText[i] == '\t')
+ {
+ if (firstTab == -1)
+ {
+ firstTab = i;
+ }
+ else
+ {
+ secondTab = i;
+ break;
+ }
+ }
+ }
+
+ if (firstTab == -1 || secondTab == -1)
+ {
+ yield break;
+ }
+
+ string assemblyPath = sourceText.ToString(TextSpan.FromBounds(start, firstTab));
+ string spec = sourceText.ToString(TextSpan.FromBounds(secondTab + 1, end));
+
+ // Determine kind without allocating a string. The three possible values are
+ // "Reference" (len 9), "ProjectReference" (len 16), "PackageReference" (len 16).
+ int kindLength = secondTab - firstTab - 1;
+ DeclaredReferenceKind kind;
+ if (kindLength == 9)
+ {
+ kind = DeclaredReferenceKind.Reference;
+ }
+ else if (kindLength == 16 && sourceText[firstTab + 1] == 'P' && sourceText[firstTab + 2] == 'r')
+ {
+ kind = DeclaredReferenceKind.ProjectReference;
+ }
+ else if (kindLength == 16 && sourceText[firstTab + 1] == 'P' && sourceText[firstTab + 2] == 'a')
+ {
+ kind = DeclaredReferenceKind.PackageReference;
+ }
+ else
+ {
+ continue;
+ }
+
+ yield return new DeclaredReference(assemblyPath, kind, spec);
+ }
+ }
}
diff --git a/src/Shared/DeclaredReferences.cs b/src/Shared/DeclaredReferences.cs
index 12128b8..fa9aa05 100644
--- a/src/Shared/DeclaredReferences.cs
+++ b/src/Shared/DeclaredReferences.cs
@@ -1,80 +1,5 @@
-using System.Text;
-
namespace ReferenceTrimmer.Shared;
-internal record DeclaredReferences(IReadOnlyList References)
-{
- private const char FieldDelimiter = '\t';
-
- private static readonly char[] FieldDelimiters = new[] { FieldDelimiter };
-
- private static readonly Dictionary KindEnumToString = new()
- {
- { DeclaredReferenceKind.Reference, nameof(DeclaredReferenceKind.Reference) },
- { DeclaredReferenceKind.ProjectReference, nameof(DeclaredReferenceKind.ProjectReference) },
- { DeclaredReferenceKind.PackageReference, nameof(DeclaredReferenceKind.PackageReference) },
- };
-
- private static readonly Dictionary KindStringToEnum = new()
- {
- { nameof(DeclaredReferenceKind.Reference), DeclaredReferenceKind.Reference },
- { nameof(DeclaredReferenceKind.ProjectReference), DeclaredReferenceKind.ProjectReference },
- { nameof(DeclaredReferenceKind.PackageReference), DeclaredReferenceKind.PackageReference },
- };
-
- public void SaveToFile(string filePath)
- {
- StringBuilder writer = new();
- foreach (DeclaredReference reference in References)
- {
- writer.Append(reference.AssemblyPath);
- writer.Append(FieldDelimiter);
- writer.Append(KindEnumToString[reference.Kind]);
- writer.Append(FieldDelimiter);
- writer.Append(reference.Spec);
- writer.AppendLine();
- }
-
- string newContent = writer.ToString();
- if (File.Exists(filePath))
- {
- string existing = File.ReadAllText(filePath);
- if (string.Equals(existing, newContent, StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
- }
-
- File.WriteAllText(filePath, newContent);
- }
-
- public static DeclaredReferences ReadFromFile(string filePath)
- {
- List references = new();
-
- using FileStream stream = File.OpenRead(filePath);
- using StreamReader reader = new(stream);
-
- string? line;
- while ((line = reader.ReadLine()) != null)
- {
- string[] parts = line.Split(FieldDelimiters, 3);
- if (parts.Length != 3)
- {
- throw new InvalidDataException($"File '{filePath}' is invalid. Line: {references.Count + 1}");
- }
-
- string assemblyName = parts[0];
- DeclaredReferenceKind kind = KindStringToEnum[parts[1]];
- string spec = parts[2];
- DeclaredReference reference = new(assemblyName, kind, spec);
- references.Add(reference);
- }
-
- return new DeclaredReferences(references);
- }
-}
-
-internal record DeclaredReference(string AssemblyPath, DeclaredReferenceKind Kind, string Spec);
+internal readonly record struct DeclaredReference(string AssemblyPath, DeclaredReferenceKind Kind, string Spec);
internal enum DeclaredReferenceKind { Reference, ProjectReference, PackageReference }
\ No newline at end of file
diff --git a/src/Tasks/CollectDeclaredReferencesTask.cs b/src/Tasks/CollectDeclaredReferencesTask.cs
index 425d624..00e3c6a 100644
--- a/src/Tasks/CollectDeclaredReferencesTask.cs
+++ b/src/Tasks/CollectDeclaredReferencesTask.cs
@@ -1,4 +1,5 @@
using System.Reflection;
+using System.Text;
using System.Xml.Linq;
using Microsoft.Build.Framework;
using NuGet.Common;
@@ -211,7 +212,7 @@ public override bool Execute()
if (OutputFile is not null)
{
- new DeclaredReferences(declaredReferences).SaveToFile(OutputFile);
+ SaveDeclaredReferences(declaredReferences, OutputFile);
Log.LogMessage(MessageImportance.Low, "Saved {0} declared references to '{1}'", declaredReferences.Count, OutputFile);
}
}
@@ -475,6 +476,43 @@ private static bool IsSuppressed(ITaskItem item, string warningId)
return false;
}
+ // File format: tab-separated fields (AssemblyPath, Kind, Spec), one reference per line.
+ // Keep in sync with ReadDeclaredReferences in ReferenceTrimmerAnalyzer.cs.
+ private static void SaveDeclaredReferences(IReadOnlyList declaredReferences, string filePath)
+ {
+ const char fieldDelimiter = '\t';
+
+ StringBuilder writer = new();
+ foreach (DeclaredReference reference in declaredReferences)
+ {
+ writer.Append(reference.AssemblyPath);
+ writer.Append(fieldDelimiter);
+ string kindString = reference.Kind switch
+ {
+ DeclaredReferenceKind.Reference => nameof(DeclaredReferenceKind.Reference),
+ DeclaredReferenceKind.ProjectReference => nameof(DeclaredReferenceKind.ProjectReference),
+ DeclaredReferenceKind.PackageReference => nameof(DeclaredReferenceKind.PackageReference),
+ _ => throw new InvalidDataException($"Unknown reference kind '{reference.Kind}'."),
+ };
+ writer.Append(kindString);
+ writer.Append(fieldDelimiter);
+ writer.Append(reference.Spec);
+ writer.AppendLine();
+ }
+
+ string newContent = writer.ToString();
+ if (File.Exists(filePath))
+ {
+ string existing = File.ReadAllText(filePath);
+ if (string.Equals(existing, newContent, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+ }
+
+ File.WriteAllText(filePath, newContent);
+ }
+
private sealed class PackageInfoBuilder
{
private List? _compileTimeAssemblies;