Skip to content

[Performance] ReferenceEvaluationContextFactory.Create uses linear LINQ Contains on NoWarn codes #71

Description

@mfogliatto

Description

ReferenceEvaluationContextFactory.Create() checks for warning suppression using noWarnCodes.Contains(Violation.ViolationSeverityWarningCode), where noWarnCodes is IEnumerable<string>. This performs O(n) linear enumeration via LINQ each time.

In the Roslyn analyzer path (ReferenceCopAnalyzer.AnalyzeCompilation), noWarnCodes is the IEnumerable<string> from NoWarnAssembliesProvider.GetNoWarnByAssembly(), which returns arrays from .Split().Select().ToArray(). The Contains call re-enumerates this array each time.

In the MSBuild path (ReferenceCopTask.Execute), noWarnCodes comes from ProjectReferenceInfo.NoWarn, which is similarly an enumerable from .Split().Select().

Affected Files

  • src/ReferenceCop/Detectors/ReferenceEvaluationContextFactory.csCreate(), line 22 (.Contains() call)
  • src/ReferenceCop.Roslyn/Providers/NoWarnAssembliesProvider.cs — produces IEnumerable<string> values
  • src/ReferenceCop.MSBuild/Providers/MSBuildProjectMetadataProvider.cs — produces IEnumerable<string> from split

Impact

  • Per-reference cost: Contains on IEnumerable is O(n) per call. While individual NoWarn lists are typically small (1-5 codes), this runs for every reference in the compilation. With 100+ references, the overhead accumulates.
  • Unnecessary enumeration: The LINQ Contains extension method enumerates the sequence each time instead of using a set-based lookup.
  • Multiple scans: If additional suppression codes are added in the future (e.g., checking both RC0001 and RC0002), each check re-enumerates.

Suggested Optimization

Use a HashSet<string> for the NoWarn codes, either at the provider level or at the factory level:

Option 1 — At the provider level (preferred, change once):

// In NoWarnAssembliesProvider:
public Dictionary<string, HashSet<string>> GetNoWarnByAssembly(string noWarnAssembliesString)
{
    // ...
    var noWarnCodes = new HashSet<string>(
        noWarnCodesString.Split(NoWarnCodesSeparator, StringSplitOptions.RemoveEmptyEntries)
                         .Select(code => code.Trim()),
        StringComparer.OrdinalIgnoreCase);
    result[assemblyName] = noWarnCodes;
}

Option 2 — Pre-compute the boolean (simplest):

public static ReferenceEvaluationContext<T> Create<T>(T reference, IEnumerable<string> noWarnCodes = null)
{
    bool isSuppressed = noWarnCodes?.Any(c =>
        c == Violation.ViolationSeverityWarningCode) ?? false;
    return new ReferenceEvaluationContext<T>(reference, isSuppressed);
}

Since there are currently only two violation codes (RC0001, RC0002), and only RC0002 is checked, the boolean pre-compute is the simplest and most effective fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    performancePerformance optimization

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions