Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 107 additions & 9 deletions src/Analyzer/ReferenceTrimmerAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);

/// <summary>
/// The supported diagnostics.
/// </summary>
Expand All @@ -52,7 +61,8 @@ public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
RT0000Descriptor,
RT0001Descriptor,
RT0002Descriptor,
RT0003Descriptor);
RT0003Descriptor,
RT9999Descriptor);

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
Expand All @@ -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)
{
Expand Down Expand Up @@ -105,11 +136,11 @@ private static void DumpUsedReferences(CompilationAnalysisContext context)
}
}

DumpReferencesInfo(usedReferences, unusedReferences, declaredReferencesPath);
DumpReferencesInfo(usedReferences, unusedReferences, declaredReferencesFile.Path);
}

Dictionary<string, List<string>> packageAssembliesDict = new(StringComparer.OrdinalIgnoreCase);
foreach (DeclaredReference declaredReference in declaredReferences.References)
foreach (DeclaredReference declaredReference in ReadDeclaredReferences(sourceText))
{
switch (declaredReference.Kind)
{
Expand Down Expand Up @@ -150,7 +181,7 @@ private static void DumpUsedReferences(CompilationAnalysisContext context)
{
string packageName = kvp.Key;
List<string> packageAssemblies = kvp.Value;
if (!packageAssemblies.Any(usedReferences.Contains))
if (!usedReferences.Overlaps(packageAssemblies))
{
context.ReportDiagnostic(Diagnostic.Create(RT0003Descriptor, Location.None, packageName));
}
Expand Down Expand Up @@ -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<DeclaredReference> 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);
}
}
}
77 changes: 1 addition & 76 deletions src/Shared/DeclaredReferences.cs
Original file line number Diff line number Diff line change
@@ -1,80 +1,5 @@
using System.Text;

namespace ReferenceTrimmer.Shared;

internal record DeclaredReferences(IReadOnlyList<DeclaredReference> References)
{
private const char FieldDelimiter = '\t';

private static readonly char[] FieldDelimiters = new[] { FieldDelimiter };

private static readonly Dictionary<DeclaredReferenceKind, string> KindEnumToString = new()
{
{ DeclaredReferenceKind.Reference, nameof(DeclaredReferenceKind.Reference) },
{ DeclaredReferenceKind.ProjectReference, nameof(DeclaredReferenceKind.ProjectReference) },
{ DeclaredReferenceKind.PackageReference, nameof(DeclaredReferenceKind.PackageReference) },
};

private static readonly Dictionary<string, DeclaredReferenceKind> 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<DeclaredReference> 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 }
40 changes: 39 additions & 1 deletion src/Tasks/CollectDeclaredReferencesTask.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Reflection;
using System.Text;
using System.Xml.Linq;
using Microsoft.Build.Framework;
using NuGet.Common;
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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<DeclaredReference> 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<string>? _compileTimeAssemblies;
Expand Down