提交 20e867f7 编写于 作者: C Cheryl Borley 提交者: GitHub

Merge pull request #18718 from chborl/formatstring

Diagnostic analyzer to identify out of range placeholders in a format string.
......@@ -3744,7 +3744,7 @@ static void Main(string[] args)
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsSimplifyTypeNames)]
public async Task TestAppropriateDiagnosticOnMissingQualifier()
{
await TestDiagnosticSeverityAndCountAsync(
await TestDiagnosticInfoAsync(
@"class C
{
int SomeProperty { get; set; }
......@@ -3755,7 +3755,6 @@ void M()
}
}",
options: Option(CodeStyleOptions.QualifyPropertyAccess, false, NotificationOption.Warning),
diagnosticCount: 1,
diagnosticId: IDEDiagnosticIds.RemoveQualificationDiagnosticId,
diagnosticSeverity: DiagnosticSeverity.Warning);
}
......
......@@ -1079,9 +1079,8 @@ static void M()
[|var|] s = 5;
}
}";
await TestDiagnosticSeverityAndCountAsync(source,
await TestDiagnosticInfoAsync(source,
options: ExplicitTypeEnforcements(),
diagnosticCount: 1,
diagnosticId: IDEDiagnosticIds.UseExplicitTypeDiagnosticId,
diagnosticSeverity: DiagnosticSeverity.Info);
}
......@@ -1098,9 +1097,8 @@ static void M()
[|var|] n1 = new[] {2, 4, 6, 8};
}
}";
await TestDiagnosticSeverityAndCountAsync(source,
await TestDiagnosticInfoAsync(source,
options: ExplicitTypeEnforcements(),
diagnosticCount: 1,
diagnosticId: IDEDiagnosticIds.UseExplicitTypeDiagnosticId,
diagnosticSeverity: DiagnosticSeverity.Warning);
}
......@@ -1117,9 +1115,8 @@ static void M()
[|var|] n1 = new C();
}
}";
await TestDiagnosticSeverityAndCountAsync(source,
await TestDiagnosticInfoAsync(source,
options: ExplicitTypeEnforcements(),
diagnosticCount: 1,
diagnosticId: IDEDiagnosticIds.UseExplicitTypeDiagnosticId,
diagnosticSeverity: DiagnosticSeverity.Error);
}
......
......@@ -1487,9 +1487,8 @@ static void M()
[|int|] s = 5;
}
}";
await TestDiagnosticSeverityAndCountAsync(source,
await TestDiagnosticInfoAsync(source,
options: ImplicitTypeEnforcements(),
diagnosticCount: 1,
diagnosticId: IDEDiagnosticIds.UseImplicitTypeDiagnosticId,
diagnosticSeverity: DiagnosticSeverity.Info);
}
......@@ -1506,9 +1505,8 @@ static void M()
[|int[]|] n1 = new[] {2, 4, 6, 8};
}
}";
await TestDiagnosticSeverityAndCountAsync(source,
await TestDiagnosticInfoAsync(source,
options: ImplicitTypeEnforcements(),
diagnosticCount: 1,
diagnosticId: IDEDiagnosticIds.UseImplicitTypeDiagnosticId,
diagnosticSeverity: DiagnosticSeverity.Warning);
}
......@@ -1525,9 +1523,8 @@ static void M()
[|C|] n1 = new C();
}
}";
await TestDiagnosticSeverityAndCountAsync(source,
await TestDiagnosticInfoAsync(source,
options: ImplicitTypeEnforcements(),
diagnosticCount: 1,
diagnosticId: IDEDiagnosticIds.UseImplicitTypeDiagnosticId,
diagnosticSeverity: DiagnosticSeverity.Error);
}
......
......@@ -91,6 +91,17 @@ public TestParameters WithFixProviderData(object fixProviderData)
}
}
protected async Task TestDiagnosticMissingAsync(
string initialMarkup,
TestParameters parameters = default(TestParameters))
{
using (var workspace = CreateWorkspaceFromOptions(initialMarkup, parameters))
{
var diagnostics = await GetDiagnosticsWorkerAsync(workspace, parameters);
Assert.Equal(0, diagnostics.Length);
}
}
protected async Task<ImmutableArray<CodeAction>> GetCodeActionsAsync(
TestWorkspace workspace, TestParameters parameters)
{
......@@ -100,6 +111,10 @@ public TestParameters WithFixProviderData(object fixProviderData)
protected abstract Task<ImmutableArray<CodeAction>> GetCodeActionsWorkerAsync(
TestWorkspace workspace, TestParameters parameters);
protected abstract Task<ImmutableArray<Diagnostic>> GetDiagnosticsWorkerAsync(
TestWorkspace workspace, TestParameters parameters);
protected async Task TestSmartTagTextAsync(
string initialMarkup,
string displayText,
......
......@@ -37,6 +37,11 @@ public abstract class AbstractCodeActionTest : AbstractCodeActionOrUserDiagnosti
: refactoring.Actions;
}
protected override Task<ImmutableArray<Diagnostic>> GetDiagnosticsWorkerAsync(TestWorkspace workspace, TestParameters parameters)
{
return SpecializedTasks.EmptyImmutableArray<Diagnostic>();
}
internal async Task<CodeRefactoring> GetCodeRefactoringAsync(
TestWorkspace workspace, TestParameters parameters)
{
......
......@@ -10,6 +10,7 @@
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.UnitTests.Diagnostics;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
......@@ -64,38 +65,54 @@ internal virtual (DiagnosticAnalyzer, CodeFixProvider) CreateDiagnosticProviderA
AssertNoAnalyzerExceptionDiagnostics(diagnostics);
var fixer = providerAndFixer.Item2;
if (fixer == null)
{
return diagnostics.Select(d => Tuple.Create(d, (CodeFixCollection) null));
}
var ids = new HashSet<string>(fixer.FixableDiagnosticIds);
var dxs = diagnostics.Where(d => ids.Contains(d.Id)).ToList();
return await GetDiagnosticAndFixesAsync(
dxs, provider, fixer, testDriver, document, span, annotation, parameters.fixAllActionEquivalenceKey);
}
protected async Task TestDiagnosticSeverityAndCountAsync(
protected async Task TestDiagnosticInfoAsync(
string initialMarkup,
IDictionary<OptionKey, object> options,
int diagnosticCount,
string diagnosticId,
DiagnosticSeverity diagnosticSeverity)
DiagnosticSeverity diagnosticSeverity,
LocalizableString diagnosticMessage = null)
{
await TestDiagnosticSeverityAndCountAsync(initialMarkup, null, null, options, diagnosticCount, diagnosticId, diagnosticSeverity);
await TestDiagnosticSeverityAndCountAsync(initialMarkup, GetScriptOptions(), null, options, diagnosticCount, diagnosticId, diagnosticSeverity);
await TestDiagnosticInfoAsync(initialMarkup, null, null, options, diagnosticId, diagnosticSeverity, diagnosticMessage);
await TestDiagnosticInfoAsync(initialMarkup, GetScriptOptions(), null, options, diagnosticId, diagnosticSeverity, diagnosticMessage);
}
protected async Task TestDiagnosticSeverityAndCountAsync(
protected async Task TestDiagnosticInfoAsync(
string initialMarkup,
ParseOptions parseOptions,
CompilationOptions compilationOptions,
IDictionary<OptionKey, object> options,
int diagnosticCount,
string diagnosticId,
DiagnosticSeverity diagnosticSeverity)
DiagnosticSeverity diagnosticSeverity,
LocalizableString diagnosticMessage = null)
{
var testOptions = new TestParameters(parseOptions, compilationOptions, options);
using (var workspace = CreateWorkspaceFromOptions(initialMarkup, testOptions))
{
var diagnostics = (await GetDiagnosticsAsync(workspace, testOptions)).Where(d => d.Id == diagnosticId);
Assert.Equal(diagnosticCount, diagnostics.Count());
Assert.Equal(1, diagnostics.Count());
var hostDocument = workspace.Documents.Single(d => d.SelectedSpans.Any());
var expected = hostDocument.SelectedSpans.Single();
var actual = diagnostics.Single().Location.SourceSpan;
Assert.Equal(expected, actual);
Assert.Equal(diagnosticSeverity, diagnostics.Single().Severity);
if (diagnosticMessage != null)
{
Assert.Equal(diagnosticMessage, diagnostics.Single().GetMessage());
}
}
}
......
......@@ -40,6 +40,17 @@ public abstract partial class AbstractUserDiagnosticTest : AbstractCodeActionOrU
return (diagnostics?.Item2?.Fixes.Select(f => f.Action).ToImmutableArray()).GetValueOrDefault().NullToEmpty();
}
protected override async Task<ImmutableArray<Diagnostic>> GetDiagnosticsWorkerAsync(TestWorkspace workspace, TestParameters parameters)
{
var diagnosticsAndCodeFixes = await GetDiagnosticAndFixAsync(workspace, parameters);
if (diagnosticsAndCodeFixes == null)
{
return ImmutableArray<Diagnostic>.Empty;
}
return ImmutableArray.Create(diagnosticsAndCodeFixes.Item1);
}
internal async Task<Tuple<Diagnostic, CodeFixCollection>> GetDiagnosticAndFixAsync(
TestWorkspace workspace, TestParameters parameters)
{
......@@ -140,8 +151,17 @@ protected Document GetDocumentAndAnnotatedSpan(TestWorkspace workspace, out stri
// Simple code fix.
foreach (var diagnostic in diagnostics)
{
// to support diagnostics without fixers
if (fixer == null)
{
result.Add(Tuple.Create(diagnostic, (CodeFixCollection)null));
continue;
}
var fixes = new List<CodeFix>();
var context = new CodeFixContext(document, diagnostic, (a, d) => fixes.Add(new CodeFix(document.Project, a, d)), CancellationToken.None);
await fixer.RegisterCodeFixesAsync(context);
if (fixes.Any())
......
......@@ -176,6 +176,7 @@ public static class Features
public const string TodoComments = nameof(TodoComments);
public const string TypeInferenceService = nameof(TypeInferenceService);
public const string Venus = nameof(Venus);
public const string ValidateFormatString = nameof(ValidateFormatString);
public const string VsLanguageBlock = nameof(VsLanguageBlock);
public const string VsNavInfo = nameof(VsNavInfo);
public const string XmlTagCompletion = nameof(XmlTagCompletion);
......
......@@ -72,6 +72,7 @@
<PackageReference Include="Microsoft.VisualStudio.Text.UI.Wpf" Version="$(MicrosoftVisualStudioTextUIWpfVersion)" />
</ItemGroup>
<ItemGroup>
<None Include="PerfTests\BasicPerfFindRefs.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
......
......@@ -2423,10 +2423,9 @@ End Class")
<Fact, Trait(Traits.Feature, Traits.Features.CodeActionsSimplifyTypeNames)>
Public Async Function TestAppropriateDiagnosticOnMissingQualifier() As Task
Await TestDiagnosticSeverityAndCountAsync(
Await TestDiagnosticInfoAsync(
"Class C : Property SomeProperty As Integer : Sub M() : [|Me|].SomeProperty = 1 : End Sub : End Class",
options:=OptionsSet(SingleOption(CodeStyleOptions.QualifyPropertyAccess, False, NotificationOption.Error)),
diagnosticCount:=1,
diagnosticId:=IDEDiagnosticIds.RemoveQualificationDiagnosticId,
diagnosticSeverity:=DiagnosticSeverity.Error)
End Function
......
' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
Imports Microsoft.CodeAnalysis.CodeFixes
Imports Microsoft.CodeAnalysis.Diagnostics
Imports Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.Diagnostics
Imports Microsoft.CodeAnalysis.Options
Imports Microsoft.CodeAnalysis.ValidateFormatString
Imports Microsoft.CodeAnalysis.VisualBasic.ValidateFormatString
Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.ValidateFormatString
Public Class ValidateFormatStringTests
Inherits AbstractVisualBasicDiagnosticProviderBasedUserDiagnosticTest
Friend Overrides Function CreateDiagnosticProviderAndFixer(
workspace As Workspace) As (DiagnosticAnalyzer, CodeFixProvider)
Return (New VisualBasicValidateFormatStringDiagnosticAnalyzer, Nothing)
End Function
Private Function VBOptionOnCSharpOptionOff() As IDictionary(Of OptionKey, Object)
Dim optionsSet = New Dictionary(Of OptionKey, Object) From
{
{New OptionKey(ValidateFormatStringOption.ReportInvalidPlaceholdersInStringDotFormatCalls, LanguageNames.CSharp), False},
{New OptionKey(ValidateFormatStringOption.ReportInvalidPlaceholdersInStringDotFormatCalls, LanguageNames.VisualBasic), True}
}
Return optionsSet
End Function
Private Function VBOptionOffCSharpOptionOn() As IDictionary(Of OptionKey, Object)
Dim optionsSet = New Dictionary(Of OptionKey, Object) From
{
{New OptionKey(ValidateFormatStringOption.ReportInvalidPlaceholdersInStringDotFormatCalls, LanguageNames.CSharp), True},
{New OptionKey(ValidateFormatStringOption.ReportInvalidPlaceholdersInStringDotFormatCalls, LanguageNames.VisualBasic), False}
}
Return optionsSet
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function ParamsObjectArray() As Task
Await TestDiagnosticMissingAsync("
Class C
Sub Main
string.Format(""This {0} {1} {[||]2} works"", New Object { ""test"", ""test2"", ""test3"" })
End Sub
End Class")
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function TwoPlaceholders() As Task
Await TestDiagnosticMissingAsync("
Class C
Sub Main
string.Format(""This {0} {1[||]} works"", ""test"", ""also"")
End Sub
End Class")
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function IFormatProviderAndThreePlaceholders() As Task
Await TestDiagnosticMissingAsync("
Imports System.Globalization
Class C
Sub Main
Dim culture as CultureInfo = ""da - da""
string.Format(culture, ""The current price is {0[||]:C2} per ounce"", 2.45)
End Sub
End Class")
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function OnePlaceholderOutOfBounds() As Task
Await TestDiagnosticInfoAsync("
Class C
Sub Main
string.Format(""This [|{1}|] is my test"", ""teststring1"")
End Sub
End Class",
options:=Nothing,
diagnosticId:=IDEDiagnosticIds.ValidateFormatStringDiagnosticID,
diagnosticSeverity:=DiagnosticSeverity.Warning,
diagnosticMessage:=FeaturesResources.Format_string_contains_invalid_placeholder)
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function FourPlaceholdersWithOnePlaceholderOutOfBounds() As Task
Await TestDiagnosticInfoAsync("
Class C
Sub Main
string.Format(""This{0}{1}{2}{3}[|{4}|] is my test"", ""teststring1"", ""teststring2"", ""teststring3"", ""teststring4"")
End Sub
End Class",
options:=Nothing,
diagnosticId:=IDEDiagnosticIds.ValidateFormatStringDiagnosticID,
diagnosticSeverity:=DiagnosticSeverity.Warning,
diagnosticMessage:=FeaturesResources.Format_string_contains_invalid_placeholder)
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function IFormatProviderAndTwoPlaceholdersWithOnePlaceholderOutOfBounds() As Task
Await TestDiagnosticInfoAsync("
Imports System.Globalization
Class C
Sub Main
Dim culture As CultureInfo = ""da - da""
string.Format(culture, ""This [|{2}|] is my test"", ""teststring1"", ""teststring2"")
End Sub
End Class",
options:=Nothing,
diagnosticId:=IDEDiagnosticIds.ValidateFormatStringDiagnosticID,
diagnosticSeverity:=DiagnosticSeverity.Warning,
diagnosticMessage:=FeaturesResources.Format_string_contains_invalid_placeholder)
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function NamedParameters() As Task
Await TestDiagnosticMissingAsync("
Class C
Sub Main
string.Format(arg0:= ""test"", arg1:= ""also"", format:= ""This {0[||]} {1} works"")
End Sub
End Class")
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function NamedParametersOneOutOfBounds() As Task
Await TestDiagnosticInfoAsync("
Class C
Sub Main
string.Format(arg0:= ""test"", arg1:= ""also"", format:= ""This {0} [|{2}|] works"")
End Sub
End Class",
options:=Nothing,
diagnosticId:=IDEDiagnosticIds.ValidateFormatStringDiagnosticID,
diagnosticSeverity:=DiagnosticSeverity.Warning,
diagnosticMessage:=FeaturesResources.Format_string_contains_invalid_placeholder)
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function NamedParametersWithIFormatProvider() As Task
Await TestDiagnosticMissingAsync("
Imports System.Globalization
Class C
Sub Main
Dim culture As CultureInfo = ""da - da""
string.Format(arg0:= 2.45, provider:=culture, format :=""The current price is {0[||]:C2} per ounce -no squiggles"")
End Sub
End Class")
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function NamedParametersWithIFormatProviderAndParamsObject() As Task
Await TestDiagnosticMissingAsync("
Imports System.Globalization
Class C
Sub Main
Dim culture As CultureInfo = ""da - da""
string.Format(format:= ""This {0} {1[||]} {2} works"", args:=New Object { ""test"", ""it"", ""really"" }, provider:=culture)
End Sub
End Class")
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function DuplicateNamedParameters() As Task
Await TestDiagnosticMissingAsync("
Imports System.Globalization
Class C
Sub Main
Dim culture As CultureInfo = ""da - da""
string.Format(format:= ""This {0} {1[||]} {2} works"", format:=New Object { ""test"", ""it"", ""really"" }, provider:=culture)
End Sub
End Class")
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function DuplicateNamedParametersInNet45() As Task
Await TestDiagnosticMissingAsync("
<Workspace>
<Project Language=""Visual Basic"" CommonReferencesNet45=""true"">
<Document FilePath=""SourceDocument"">
Imports System.Globalization
Class C
Sub Main
Dim culture As CultureInfo = ""da - da""
string.Format(format:= ""This {0} {1[||]} {2} works"", format:=New Object { ""test"", ""it"", ""really"" }, provider:=culture)
End Sub
End Class
</Document>
</Project>
</Workspace>")
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function NamespaceAliasForStringClass() As Task
Await TestDiagnosticMissingAsync("
Imports stringalias = System.String
Class C
Sub Main
stringAlias.Format(""This {[||]0} works"", ""test"")
End Sub
End Class")
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function VerbatimMultipleLines() As Task
Await TestDiagnosticMissingAsync("
Class C
Sub Main
string.Format(@""This {[||]0}
{1} {2} works"", ""multiple"", ""line"", ""test""))
End Sub
End Class")
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function Interpolated() As Task
Await TestDiagnosticMissingAsync("
Class C
Sub Main
string.Format($""This {0}
{1[||]} {2} works"", ""multiple"", ""line"", ""test""))
End Sub
End Class")
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function Empty() As Task
Await TestDiagnosticMissingAsync("
Class C
Sub Main
string.Format(""[||]"")
End Sub
End Class")
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function LeftParenOnly() As Task
Await TestDiagnosticMissingAsync("
Class C
Sub Main
string.Format([||]
End Sub
End Class")
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function ParenthesesOnly() As Task
Await TestDiagnosticMissingAsync("
Class C
Sub Main
string.Format [||]
End Sub
End Class")
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function DifferentFunction() As Task
Await TestDiagnosticMissingAsync("
Class C
Sub Main
string.Compare [||]
End Sub
End Class")
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function FormatMethodOnGenericIdentifier() As Task
Await TestDiagnosticMissingAsync("
Class G(Of T)
Function Format(Of T)(foo as String)
Return True
End Function
End Class
Class C
Sub Foo()
Dim q As G(Of Integer)
q.Format(Of Integer)(""TestStr[||]ing"")
End Sub
End Class")
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function OmittedArgument() As Task
Await TestDiagnosticMissingAsync("Module M
Sub Main()
String.Format([||],)
End Sub
End Module")
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function WarningTurnedOff() As Task
Await TestDiagnosticMissingAsync("
Class C
Sub Main
string.Format(""This {0} {1} {[||]2} works"", New Object { ""test"", ""test2"", ""test3"" })
End Sub
End Class", New TestParameters(options:=VBOptionOffCSharpOptionOn))
End Function
<Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)>
Public Async Function WarningTurnedOn() As Task
Await TestDiagnosticInfoAsync("
Class C
Sub Main
string.Format(""This {0} [|{2}|] works"", ""test"", ""also"")
End Sub
End Class",
options:=VBOptionOnCSharpOptionOff,
diagnosticId:=IDEDiagnosticIds.ValidateFormatStringDiagnosticID,
diagnosticSeverity:=DiagnosticSeverity.Warning,
diagnosticMessage:=FeaturesResources.Format_string_contains_invalid_placeholder)
End Function
End Class
End Namespace
\ No newline at end of file
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.LanguageServices;
using Microsoft.CodeAnalysis.ValidateFormatString;
namespace Microsoft.CodeAnalysis.CSharp.ValidateFormatString
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal class CSharpValidateFormatStringDiagnosticAnalyzer :
AbstractValidateFormatStringDiagnosticAnalyzer<SyntaxKind>
{
protected override ISyntaxFactsService GetSyntaxFactsService()
=> CSharpSyntaxFactsService.Instance;
protected override SyntaxKind GetInvocationExpressionSyntaxKind()
=> SyntaxKind.InvocationExpression;
protected override SyntaxNode TryGetMatchingNamedArgument(
SeparatedSyntaxList<SyntaxNode> arguments,
string searchArgumentName)
{
foreach (var argument in arguments.Cast<ArgumentSyntax>())
{
if (argument.NameColon != null && argument.NameColon.Name.Identifier.ValueText.Equals(searchArgumentName))
{
return argument;
}
}
return null;
}
protected override SyntaxNode GetArgumentExpression(SyntaxNode syntaxNode)
=> ((ArgumentSyntax)syntaxNode).Expression;
}
}
\ No newline at end of file
......@@ -45,6 +45,7 @@ internal static class IDEDiagnosticIds
public const string UseExplicitTupleNameDiagnosticId = "IDE0033";
public const string UseDefaultLiteralDiagnosticId = "IDE0034";
public const string ValidateFormatStringDiagnosticID = "IDE0035";
public const string RemoveUnreachableCodeDiagnosticId = "IDE0035";
......
......@@ -1188,6 +1188,15 @@ internal class FeaturesResources {
}
}
/// <summary>
/// Looks up a localized string similar to Format string contains invalid placeholder.
/// </summary>
internal static string Format_string_contains_invalid_placeholder {
get {
return ResourceManager.GetString("Format_string_contains_invalid_placeholder", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to from {0}.
/// </summary>
......@@ -1791,6 +1800,15 @@ internal class FeaturesResources {
}
}
/// <summary>
/// Looks up a localized string similar to Invalid format string.
/// </summary>
internal static string Invalid_format_string {
get {
return ResourceManager.GetString("Invalid_format_string", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to is.
/// </summary>
......
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
......@@ -1244,6 +1244,12 @@ This version used in: {2}</value>
<data name="default_expression_can_be_simplified" xml:space="preserve">
<value>'default' expression can be simplified</value>
</data>
<data name="Format_string_contains_invalid_placeholder" xml:space="preserve">
<value>Format string contains invalid placeholder</value>
</data>
<data name="Invalid_format_string" xml:space="preserve">
<value>Invalid format string</value>
</data>
<data name="Use_inferred_member_name" xml:space="preserve">
<value>Use inferred member name</value>
</data>
......
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.LanguageServices;
namespace Microsoft.CodeAnalysis.ValidateFormatString
{
internal abstract class AbstractValidateFormatStringDiagnosticAnalyzer<TSyntaxKind>
: DiagnosticAnalyzer
where TSyntaxKind : struct
{
private const string DiagnosticID = IDEDiagnosticIds.ValidateFormatStringDiagnosticID;
private static readonly LocalizableString Title = new LocalizableResourceString(
nameof(FeaturesResources.Invalid_format_string),
FeaturesResources.ResourceManager,
typeof(FeaturesResources));
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(
nameof(FeaturesResources.Format_string_contains_invalid_placeholder),
FeaturesResources.ResourceManager,
typeof(FeaturesResources));
private static readonly LocalizableString Description = new LocalizableResourceString(
nameof(FeaturesResources.Invalid_format_string),
FeaturesResources.ResourceManager,
typeof(FeaturesResources));
private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
DiagnosticID,
Title,
MessageFormat,
DiagnosticCategory.Compiler,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: Description);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
=> ImmutableArray.Create(Rule);
/// <summary>
/// this regex is used to remove escaped brackets from
/// the format string before looking for valid {} pairs
/// </summary>
private static Regex s_removeEscapedBracketsRegex = new Regex("{{");
/// <summary>
/// this regex is used to extract the text between the
/// brackets and save the contents in a MatchCollection
/// </summary>
private static Regex s_extractPlaceholdersRegex = new Regex("{(.*?)}");
private const string NameOfArgsParameter = "args";
private const string NameOfFormatStringParameter = "format";
protected abstract ISyntaxFactsService GetSyntaxFactsService();
protected abstract TSyntaxKind GetInvocationExpressionSyntaxKind();
protected abstract SyntaxNode GetArgumentExpression(SyntaxNode syntaxNode);
protected abstract SyntaxNode TryGetMatchingNamedArgument(SeparatedSyntaxList<SyntaxNode> arguments, string searchArgumentName);
public override void Initialize(AnalysisContext context)
{
context.RegisterCompilationStartAction(startContext =>
{
var formatProviderType = startContext.Compilation.GetTypeByMetadataName(typeof(System.IFormatProvider).FullName);
if (formatProviderType == null)
{
return;
}
startContext.RegisterSyntaxNodeAction(
c => AnalyzeNode(c, formatProviderType),
GetInvocationExpressionSyntaxKind());
});
}
private void AnalyzeNode(SyntaxNodeAnalysisContext context, INamedTypeSymbol formatProviderType)
{
var optionSet = context.Options.GetDocumentOptionSetAsync(
context.Node.SyntaxTree, context.CancellationToken).GetAwaiter().GetResult();
if (optionSet.GetOption(
ValidateFormatStringOption.ReportInvalidPlaceholdersInStringDotFormatCalls,
context.SemanticModel.Language) == false)
{
return;
}
var syntaxFacts = GetSyntaxFactsService();
var expression = syntaxFacts.GetExpressionOfInvocationExpression(context.Node);
if (!IsValidFormatMethod(syntaxFacts, expression))
{
return;
}
var arguments = syntaxFacts.GetArgumentsOfInvocationExpression(context.Node);
var symbolInfo = context.SemanticModel.GetSymbolInfo(expression, context.CancellationToken);
var method = TryGetValidFormatMethodSymbol(context, symbolInfo);
if (method == null)
{
return;
}
var parameters = method.Parameters;
// If the chosen overload has a params object[] that is being passed an array of
// reference types then an actual array must be being passed instead of passing one
// argument at a time because we know the set of overloads and how overload resolution
// will work. This array may have been created somewhere we can't analyze, so never
// squiggle in this case.
if (ArgsIsArrayOfReferenceTypes(context.SemanticModel, arguments, parameters, syntaxFacts))
{
return;
}
var formatStringLiteralExpressionSyntax = TryGetFormatStringLiteralExpressionSyntax(
context.SemanticModel,
arguments,
parameters,
syntaxFacts);
if (formatStringLiteralExpressionSyntax == null)
{
return;
}
if (parameters.Length == 0)
{
return;
}
var numberOfArguments = arguments.Count;
var hasIFormatProvider = parameters[0].Type.Equals(formatProviderType);
// We know the format string parameter exists so numberOfArguments is at least one,
// and at least 2 if there is also an IFormatProvider. The result can be zero if
// calling string.Format("String only with no placeholders"), where there is an
// empty params array.
var numberOfPlaceholderArguments = numberOfArguments - (hasIFormatProvider ? 2 : 1);
var formatString = formatStringLiteralExpressionSyntax.ToString();
if (FormatCallWorksAtRuntime(formatString, numberOfPlaceholderArguments))
{
return;
}
ValidateAndReportDiagnostic(context, numberOfPlaceholderArguments,
formatString, formatStringLiteralExpressionSyntax.SpanStart);
}
private bool IsValidFormatMethod(ISyntaxFactsService syntaxFacts, SyntaxNode expression)
{
// When calling string.Format(...), the expression will be MemberAccessExpressionSyntax
if (syntaxFacts.IsSimpleMemberAccessExpression(expression))
{
var nameOfMemberAccessExpression = syntaxFacts.GetNameOfMemberAccessExpression(expression);
return !syntaxFacts.IsGenericName(nameOfMemberAccessExpression)
&& syntaxFacts.GetIdentifierOfSimpleName(nameOfMemberAccessExpression).ValueText
== (nameof(string.Format));
}
// When using static System.String and calling Format(...), the expression will be
// IdentifierNameSyntax
if (syntaxFacts.IsIdentifierName(expression))
{
return syntaxFacts.GetIdentifierOfSimpleName(expression).ValueText
== (nameof(string.Format));
}
return false;
}
private bool ArgsIsArrayOfReferenceTypes(
SemanticModel semanticModel,
SeparatedSyntaxList<SyntaxNode> arguments,
ImmutableArray<IParameterSymbol> parameters,
ISyntaxFactsService syntaxFacts)
{
var argsArgumentType = TryGetArgsArgumentType(semanticModel, arguments, parameters, syntaxFacts);
return argsArgumentType is IArrayTypeSymbol arrayType && arrayType.ElementType.IsReferenceType;
}
private ITypeSymbol TryGetArgsArgumentType(
SemanticModel semanticModel,
SeparatedSyntaxList<SyntaxNode> arguments,
ImmutableArray<IParameterSymbol> parameters,
ISyntaxFactsService syntaxFacts)
{
var argsArgument = TryGetArgument(semanticModel, NameOfArgsParameter, arguments, parameters);
if (argsArgument == null)
{
return null;
}
var expression = syntaxFacts.GetExpressionOfArgument(argsArgument);
return semanticModel.GetTypeInfo(expression).Type;
}
protected SyntaxNode TryGetArgument(
SemanticModel semanticModel,
string searchArgumentName,
SeparatedSyntaxList<SyntaxNode> arguments,
ImmutableArray<IParameterSymbol> parameters)
{
// First, look for a named argument that matches
var matchingNamedArgument = TryGetMatchingNamedArgument(arguments, searchArgumentName);
if (matchingNamedArgument != null)
{
return matchingNamedArgument;
}
// If no named argument exists, look for the named parameter
// and return the corresponding argument
var parameterWithMatchingName = GetParameterWithMatchingName(parameters, searchArgumentName);
if (parameterWithMatchingName == null)
{
return null;
}
// For the case string.Format("Test string"), there is only one argument
// but the compiler created an empty parameter array to bind to an overload
if (parameterWithMatchingName.Ordinal >= arguments.Count)
{
return null;
}
// Multiple arguments could have been converted to a single params array,
// so there wouldn't be a corresponding argument
if (parameterWithMatchingName.IsParams && parameters.Length != arguments.Count)
{
return null;
}
return arguments[parameterWithMatchingName.Ordinal];
}
private IParameterSymbol GetParameterWithMatchingName(ImmutableArray<IParameterSymbol> parameters, string searchArgumentName)
{
foreach (var p in parameters)
{
if (p.Name == searchArgumentName)
{
return p;
}
}
return null;
}
protected SyntaxNode TryGetFormatStringLiteralExpressionSyntax(
SemanticModel semanticModel,
SeparatedSyntaxList<SyntaxNode> arguments,
ImmutableArray<IParameterSymbol> parameters,
ISyntaxFactsService syntaxFacts)
{
var formatArgumentSyntax = TryGetArgument(
semanticModel,
NameOfFormatStringParameter,
arguments,
parameters);
if (formatArgumentSyntax == null)
{
return null;
}
if (!syntaxFacts.IsStringLiteralExpression(syntaxFacts.GetExpressionOfArgument(formatArgumentSyntax)))
{
return null;
}
return GetArgumentExpression(formatArgumentSyntax);
}
protected static IMethodSymbol TryGetValidFormatMethodSymbol(
SyntaxNodeAnalysisContext context,
SymbolInfo symbolInfo)
{
if (symbolInfo.Symbol == null)
{
return null;
}
if (symbolInfo.Symbol.Kind != SymbolKind.Method)
{
return null;
}
var containingType = (INamedTypeSymbol)symbolInfo.Symbol.ContainingSymbol;
if (containingType.SpecialType != SpecialType.System_String)
{
return null;
}
return (IMethodSymbol)symbolInfo.Symbol;
}
private bool FormatCallWorksAtRuntime(string formatString, int numberOfPlaceholderArguments)
{
var testArray = new object[numberOfPlaceholderArguments];
for (var i = 0; i < numberOfPlaceholderArguments; i++)
{
testArray[i] = "test";
}
try
{
string.Format(formatString, testArray);
}
catch
{
return false;
}
return true;
}
protected void ValidateAndReportDiagnostic(
SyntaxNodeAnalysisContext context,
int numberOfPlaceholderArguments,
string formatString,
int formatStringPosition)
{
// removing escaped left brackets and replacing with space characters so they won't
// impede the extraction of placeholders, yet the locations of the placeholders are
// the same as in the original string.
var formatStringWithEscapedBracketsChangedToSpaces = RemoveEscapedBrackets(formatString);
var matches = s_extractPlaceholdersRegex.Matches(formatStringWithEscapedBracketsChangedToSpaces);
foreach (Match match in matches)
{
var textInsideBrackets = match.Groups[1].Value;
if (!PlaceholderIndexIsValid(textInsideBrackets, numberOfPlaceholderArguments))
{
var invalidPlaceholderText = "{" + textInsideBrackets + "}";
var invalidPlaceholderLocation = Location.Create(
context.Node.SyntaxTree,
new Text.TextSpan(
formatStringPosition + match.Index,
invalidPlaceholderText.Length));
var diagnostic = Diagnostic.Create(
Rule,
invalidPlaceholderLocation,
invalidPlaceholderText);
context.ReportDiagnostic(diagnostic);
}
}
}
/// <summary>
/// removing escaped left brackets and replacing with space characters so they won't
/// impede the extraction of placeholders, yet the locations of the placeholders are
/// the same as in the original string.
/// </summary>
/// <param name="formatString"></param>
/// <returns>string with left brackets removed and replaced by spaces</returns>
private static string RemoveEscapedBrackets(string formatString)
=> s_removeEscapedBracketsRegex.Replace(formatString, " ");
private static bool PlaceholderIndexIsValid(
string textInsideBrackets,
int numberOfPlaceholderArguments)
{
var placeholderIndexText = textInsideBrackets.IndexOf(",") > 0
? textInsideBrackets.Split(',')[0]
: textInsideBrackets.Split(':')[0];
// placeholders cannot begin with whitespace
if (placeholderIndexText.Length > 0 && char.IsWhiteSpace(placeholderIndexText, 0))
{
return false;
}
if (!int.TryParse(placeholderIndexText, out var placeholderIndex))
{
return false;
}
if (placeholderIndex >= numberOfPlaceholderArguments)
{
return false;
}
return true;
}
}
}
\ No newline at end of file
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Immutable;
using System.Composition;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Options.Providers;
namespace Microsoft.CodeAnalysis.ValidateFormatString
{
internal class ValidateFormatStringOption
{
public static PerLanguageOption<bool> ReportInvalidPlaceholdersInStringDotFormatCalls =
new PerLanguageOption<bool>(
nameof(ValidateFormatStringOption),
nameof(ReportInvalidPlaceholdersInStringDotFormatCalls),
defaultValue: true,
storageLocations: new RoamingProfileStorageLocation("TextEditor.%LANGUAGE%.Specific.WarnOnInvalidStringDotFormatCalls"));
}
[ExportOptionProvider, Shared]
internal class ValidateFormatStringOptionProvider : IOptionProvider
{
public ImmutableArray<IOption> Options { get; } = ImmutableArray.Create<IOption>(
ValidateFormatStringOption.ReportInvalidPlaceholdersInStringDotFormatCalls);
}
}
'Copyright(c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt In the project root For license information.
Imports Microsoft.CodeAnalysis.Diagnostics
Imports Microsoft.CodeAnalysis.LanguageServices
Imports Microsoft.CodeAnalysis.ValidateFormatString
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
Namespace Microsoft.CodeAnalysis.VisualBasic.ValidateFormatString
<DiagnosticAnalyzer(LanguageNames.VisualBasic)>
Friend Class VisualBasicValidateFormatStringDiagnosticAnalyzer
Inherits AbstractValidateFormatStringDiagnosticAnalyzer(Of SyntaxKind)
Protected Overrides Function GetSyntaxFactsService() As ISyntaxFactsService
Return VisualBasicSyntaxFactsService.Instance
End Function
Protected Overrides Function GetInvocationExpressionSyntaxKind() As SyntaxKind
Return SyntaxKind.InvocationExpression
End Function
Protected Overrides Function TryGetMatchingNamedArgument(
arguments As SeparatedSyntaxList(Of SyntaxNode),
searchArgumentName As String) As SyntaxNode
For Each argument In arguments
Dim simpleArgumentSyntax = TryCast(argument, SimpleArgumentSyntax)
If Not simpleArgumentSyntax Is Nothing AndAlso simpleArgumentSyntax.NameColonEquals?.Name.Identifier.ValueText.Equals(searchArgumentName) Then
Return argument
End If
Next
Return Nothing
End Function
Protected Overrides Function GetArgumentExpression(syntaxNode As SyntaxNode) As SyntaxNode
Return DirectCast(syntaxNode, ArgumentSyntax).GetArgumentExpression
End Function
End Class
End Namespace
\ No newline at end of file
......@@ -1032,6 +1032,15 @@ internal class CSharpVSResources {
}
}
/// <summary>
/// Looks up a localized string similar to Report invalid placeholders in &apos;string.Format&apos; calls.
/// </summary>
internal static string Report_invalid_placeholders_in_string_dot_format_calls {
get {
return ResourceManager.GetString("Report_invalid_placeholders_in_string_dot_format_calls", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Selection In Completion List.
/// </summary>
......
......@@ -519,4 +519,7 @@
<data name="Fade_out_unused_usings" xml:space="preserve">
<value>Fade out unused usings</value>
</data>
<data name="Report_invalid_placeholders_in_string_dot_format_calls" xml:space="preserve">
<value>Report invalid placeholders in 'string.Format' calls</value>
</data>
</root>
\ No newline at end of file
......@@ -90,6 +90,8 @@
Content="{x:Static local:AdvancedOptionPageStrings.Option_RenameTrackingPreview}" />
<CheckBox x:Name="Split_string_literals_on_enter"
Content="{x:Static local:AdvancedOptionPageStrings.Option_Split_string_literals_on_enter}" />
<CheckBox x:Name="Report_invalid_placeholders_in_string_dot_format_calls"
Content="{x:Static local:AdvancedOptionPageStrings.Option_Report_invalid_placeholders_in_string_dot_format_calls}" />
</StackPanel>
</GroupBox>
<GroupBox x:Uid="ExtractMethodGroupBox"
......
......@@ -11,6 +11,7 @@
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Structure;
using Microsoft.CodeAnalysis.SymbolSearch;
using Microsoft.CodeAnalysis.ValidateFormatString;
using Microsoft.VisualStudio.LanguageServices.Implementation.Options;
namespace Microsoft.VisualStudio.LanguageServices.CSharp.Options
......@@ -56,6 +57,8 @@ public AdvancedOptionPageControl(IServiceProvider serviceProvider) : base(servic
BindToOption(prefer_throwing_properties, ImplementTypeOptions.PropertyGenerationBehavior, ImplementTypePropertyGenerationBehavior.PreferThrowingProperties, LanguageNames.CSharp);
BindToOption(prefer_auto_properties, ImplementTypeOptions.PropertyGenerationBehavior, ImplementTypePropertyGenerationBehavior.PreferAutoProperties, LanguageNames.CSharp);
BindToOption(Report_invalid_placeholders_in_string_dot_format_calls, ValidateFormatStringOption.ReportInvalidPlaceholdersInStringDotFormatCalls, LanguageNames.CSharp);
}
}
}
......@@ -161,5 +161,8 @@ public static string Option_PlaceSystemNamespaceFirst
public static string Option_Suggest_usings_for_types_in_NuGet_packages =>
CSharpVSResources.Suggest_usings_for_types_in_NuGet_packages;
public static string Option_Report_invalid_placeholders_in_string_dot_format_calls =>
CSharpVSResources.Report_invalid_placeholders_in_string_dot_format_calls;
}
}
......@@ -442,6 +442,15 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic
End Get
End Property
'''<summary>
''' Looks up a localized string similar to Report invalid placeholders in &apos;String.Format&apos; calls.
'''</summary>
Friend Shared ReadOnly Property Report_invalid_placeholders_in_string_dot_format_calls() As String
Get
Return ResourceManager.GetString("Report_invalid_placeholders_in_string_dot_format_calls", resourceCulture)
End Get
End Property
'''<summary>
''' Looks up a localized string similar to Show completion item _filters.
'''</summary>
......
......@@ -13,7 +13,7 @@
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="resmimetype">text/microsoft-resx</resheader
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
......@@ -270,4 +270,7 @@
<data name="Fade_out_unused_imports" xml:space="preserve">
<value>Fade out unused imports</value>
</data>
<data name="Report_invalid_placeholders_in_string_dot_format_calls" xml:space="preserve">
<value>Report invalid placeholders in 'String.Format' calls</value>
</data>
</root>
\ No newline at end of file
......@@ -91,6 +91,8 @@
Content="{x:Static local:AdvancedOptionPageStrings.Option_GenerateXmlDocCommentsForTripleApostrophes}" />
<CheckBox x:Name="RenameTrackingPreview"
Content="{x:Static local:AdvancedOptionPageStrings.Option_RenameTrackingPreview}" />
<CheckBox x:Name="Report_invalid_placeholders_in_string_dot_format_calls"
Content="{x:Static local:AdvancedOptionPageStrings.Option_Report_invalid_placeholders_in_string_dot_format_calls}" />
</StackPanel>
</GroupBox>
<GroupBox x:Uid="GoToDefinitionGroupBox"
......
......@@ -9,6 +9,7 @@ Imports Microsoft.CodeAnalysis.ImplementType
Imports Microsoft.CodeAnalysis.Remote
Imports Microsoft.CodeAnalysis.Structure
Imports Microsoft.CodeAnalysis.SymbolSearch
Imports Microsoft.CodeAnalysis.ValidateFormatString
Imports Microsoft.VisualStudio.LanguageServices.Implementation
Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic.Options
......@@ -54,6 +55,8 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic.Options
BindToOption(prefer_throwing_properties, ImplementTypeOptions.PropertyGenerationBehavior, ImplementTypePropertyGenerationBehavior.PreferThrowingProperties, LanguageNames.VisualBasic)
BindToOption(prefer_auto_properties, ImplementTypeOptions.PropertyGenerationBehavior, ImplementTypePropertyGenerationBehavior.PreferAutoProperties, LanguageNames.VisualBasic)
BindToOption(Report_invalid_placeholders_in_string_dot_format_calls, ValidateFormatStringOption.ReportInvalidPlaceholdersInStringDotFormatCalls, LanguageNames.VisualBasic)
End Sub
End Class
End Namespace
......@@ -179,6 +179,12 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic.Options
End Get
End Property
Public ReadOnly Property Option_Report_invalid_placeholders_in_string_dot_format_calls As String
Get
Return BasicVSResources.Report_invalid_placeholders_in_string_dot_format_calls
End Get
End Property
Public ReadOnly Property Option_RenameTrackingPreview As String
Get
Return BasicVSResources.Show_preview_for_rename_tracking
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册