Skip to content

ProvideCorrectArgumentsToFormattingMethodsAnalyzer incorrectly treats IFormatProvider as an arg parameter #7800

Description

@daiplusplus

Problem Statement

The ProvideCorrectArgumentsToFormattingMethodsAnalyzer correctly detects incorrect format-strings and format-args in methods like Method( [StringSyntax("composite")] String format, Object? arg0[, Object? arg1[, Object? arg2 ]]) and params Object?[]? args) overloads, however in methods with additional parameters in-between String format and Object? arg0 the analyzer incorrectly considers those additional-parameter arguments as format-args even though they are not named arg, arg0 or args - nor typed as Object? (in this case, typed as IFormatProvider. the same as used by String.Format itself).

Background + Context

  • In my particular case, I have a utility-library project that originally dates back to .NET Framework 2.0 days and keeps-up with new .NET features as hey come. In particular, long before $"" interpolated-strings were added to C# it added extension-methods to String for a more ergonomic alternative to the cumbersome String.Format( IFormatProvider, String format, Object? arg... ) call-sites. More recently, the StringSyntax attribute was added so callers would benefit from the same analyzer checks as the normal String.Format.
    • String Fmt( [StrSyn( StrSyn.CompositeFormat )] this String format, Object? arg... ) called String.Format with CultureInfo.CurrentCulture.
    • String Fmt( [StrSyn( StrSyn.CompositeFormat )] this String format, IFormatProvider provider, Object? arg... ) called String.Format with provider.
    • String FmtInv( [StrSyn( StrSyn.CompositeFormat )] this String format, Object? arg... ) called String.Format with CultureInfo.InvariantCulture.
  • I recently changed my .editorconfig to promote CA2241 from a Warning to an Error, and I noticed that my unit-test-cases for Fmt( this String format, IFormatProvider provider, Object? arg, ... ) were being flagged by the analyzer despite having the correct number of arg parameter arguments, and this is because the analyzer is treating the IFormatProvider parameter as an "arg" parameter, so it thinks there's +1 extra parameters than were really are.
  • If these weren't extension-methods then a workaround would be simple: reorder the parameters so IFormatProvider provider appears before String format - but because these are extension-methods operating on this String format there really isn't anywhere else for provider to go.

Version Used

  • .NET SDK 10.0.102
  • Also repros in .NET SDK 9.0.305

My .csproj specifies:

	<TargetFrameworks>netcoreapp3.1;net48;net6.0;net8.0;</TargetFrameworks>
	<LangVersion>12.0</LangVersion>
	<Nullable>enable</Nullable>
	<EnableNETAnalyzers>True</EnableNETAnalyzers>
	<AnalysisLevel>latest-all</AnalysisLevel>

Steps to Reproduce

Image

Diagnostic Id

Severity Code Description File Line Project Suppression State Details
Error (active) CA2241 The format argument that is passed to System.String.Format does not contain a format item that corresponds to each object argument, or vice versa. C:\git\me\MyProject.Tests\Strings\PluralFormatProviderTests.cs 17 MyProject.Tests (net6.0), MyProject.Tests (net8.0)

If this is a report about a bug in an analyzer, please include the diagnostic ID and message if possible (e.g. "IDE0030: Use coalesce expression").

Expected Behavior

I expected the ProvideCorrectArgumentsToFormattingMethodsAnalyzer to ignore/exclude arguments typed as IFormatProvider and/or parameters not named arg, arg0, arg1, arg2, or args.

Actual Behavior

As described above.

Possible solutions to the problem

The StringSyntax attribute already supports additional arguments of its own (via its Arguments property) - which could be used with "CompositeFormat" to specify which parameter names should be ignored or excluded from being counted as format-args by the analyzer (or vice-versa: to explicitly name which parameters are format-args params.

So I'd change my Fmt() method signature to something like this:

	/// <summary>Calls <see cref="String.Format(IFormatProvider, String, Object)"/>.</summary>
	[MethodImpl( Consts.Inline )]
	public static String Fmt( [StrSyn( StrSyn.CompositeFormat, "ignore:" + nameof(formatProvider) )] this String format, IFormatProvider? formatProvider, Object? arg0 )
	{
		return String.Format( formatProvider, format, arg0 );
	}

...or the opposite whereby format-args params are explicitly named:

	/// <summary>Calls <see cref="String.Format(IFormatProvider, String, Object)"/>.</summary>
	[MethodImpl( Consts.Inline )]
	public static String Fmt( [StrSyn( StrSyn.CompositeFormat, nameof(arg0)) )] this String format, IFormatProvider? formatProvider, Object? arg0, Object? arg1 )
	{
		return String.Format( formatProvider, format, arg0 );
	}

	// ...repeated for all the other overloads too:

	public static String Fmt( [StrSyn( StrSyn.CompositeFormat, nameof(arg0), nameof(arg1) )] this String format, IFormatProvider? formatProvider, Object? arg0 )
	public static String Fmt( [StrSyn( StrSyn.CompositeFormat, nameof(arg0), nameof(arg1), nameof(arg2) )] this String format, IFormatProvider? formatProvider, Object? arg0 )
	public static String Fmt( [StrSyn( StrSyn.CompositeFormat, nameof(args) )] this String format, IFormatProvider? formatProvider, params Object?[]? args )

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions