diff --git a/src/EditorFeatures/CSharpTest/Diagnostics/SimplifyTypeNames/SimplifyTypeNamesTests.cs b/src/EditorFeatures/CSharpTest/Diagnostics/SimplifyTypeNames/SimplifyTypeNamesTests.cs index 6f8ca53dc237606c665ed663f2bf7df886e3cf59..5e6268921993a114d45eadd05a85d6608d2bb371 100644 --- a/src/EditorFeatures/CSharpTest/Diagnostics/SimplifyTypeNames/SimplifyTypeNamesTests.cs +++ b/src/EditorFeatures/CSharpTest/Diagnostics/SimplifyTypeNames/SimplifyTypeNamesTests.cs @@ -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); } diff --git a/src/EditorFeatures/CSharpTest/Diagnostics/UseImplicitOrExplicitType/UseExplicitTypeTests.cs b/src/EditorFeatures/CSharpTest/Diagnostics/UseImplicitOrExplicitType/UseExplicitTypeTests.cs index 1317bb055fb3d3a887cb6ff61f48372cb2627e58..29450ad329ae6dbc9197f40da864933cb34432ea 100644 --- a/src/EditorFeatures/CSharpTest/Diagnostics/UseImplicitOrExplicitType/UseExplicitTypeTests.cs +++ b/src/EditorFeatures/CSharpTest/Diagnostics/UseImplicitOrExplicitType/UseExplicitTypeTests.cs @@ -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); } diff --git a/src/EditorFeatures/CSharpTest/Diagnostics/UseImplicitOrExplicitType/UseImplicitTypeTests.cs b/src/EditorFeatures/CSharpTest/Diagnostics/UseImplicitOrExplicitType/UseImplicitTypeTests.cs index 70798dee143147c3f395f58dc274fa022d0ab1f0..c351000f451ebeb74ae723808e2619e180ab4ea9 100644 --- a/src/EditorFeatures/CSharpTest/Diagnostics/UseImplicitOrExplicitType/UseImplicitTypeTests.cs +++ b/src/EditorFeatures/CSharpTest/Diagnostics/UseImplicitOrExplicitType/UseImplicitTypeTests.cs @@ -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); } diff --git a/src/EditorFeatures/CSharpTest/ValidateFormatString/ValidateFormatStringTests.cs b/src/EditorFeatures/CSharpTest/ValidateFormatString/ValidateFormatStringTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..37831a2b52641053b2552c19ea48e189fcf43ef6 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/ValidateFormatString/ValidateFormatStringTests.cs @@ -0,0 +1,968 @@ +// 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.Generic; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.ValidateFormatString; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.ValidateFormatString; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.ValidateFormatString +{ + public class ValidateFormatStringTests : AbstractCSharpDiagnosticProviderBasedUserDiagnosticTest + { + internal override (DiagnosticAnalyzer, CodeFixProvider) CreateDiagnosticProviderAndFixer(Workspace workspace) + => (new CSharpValidateFormatStringDiagnosticAnalyzer(), null); + + private IDictionary CSharpOptionOffVBOptionOn() + { + var optionsSet = new Dictionary(); + optionsSet.Add(new OptionKey(ValidateFormatStringOption.ReportInvalidPlaceholdersInStringDotFormatCalls, LanguageNames.CSharp) , false); + optionsSet.Add(new OptionKey(ValidateFormatStringOption.ReportInvalidPlaceholdersInStringDotFormatCalls, LanguageNames.VisualBasic), true); + return optionsSet; + } + + private IDictionary CSharpOptionOnVBOptionOff() + { + var optionsSet = new Dictionary(); + optionsSet.Add(new OptionKey(ValidateFormatStringOption.ReportInvalidPlaceholdersInStringDotFormatCalls, LanguageNames.CSharp), true); + optionsSet.Add(new OptionKey(ValidateFormatStringOption.ReportInvalidPlaceholdersInStringDotFormatCalls, LanguageNames.VisualBasic), false); + return optionsSet; + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task OnePlaceholder() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(""This {0[||]} works"", ""test""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task TwoPlaceholders() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(""This {0[||]} {1} works"", ""test"", ""also""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task ThreePlaceholders() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(""This {0} {1[||]} works {2} "", ""test"", ""also"", ""well""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task FourPlaceholders() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(""This {1} is {2} my {6[||]} test "", ""teststring1"", ""teststring2"", + ""teststring3"", ""teststring4"", ""teststring5"", ""teststring6"", ""teststring7""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task ObjectArray() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + object[] objectArray = { 1.25, ""2"", ""teststring""}; + string.Format(""This {0} {1} {2[||]} works"", objectArray); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task MultipleObjectArrays() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + object[] objectArray = { 1.25, ""2"", ""teststring""}; + string.Format(""This {0} {1} {2[||]} works"", objectArray, objectArray, objectArray, objectArray); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task IntArray() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + int[] intArray = {1, 2, 3}; + string.Format(""This {0[||]} works"", intArray); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task StringArray() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string[] stringArray = {""test1"", ""test2"", ""test3""}; + string.Format(""This {0} {1} {2[||]} works"", stringArray); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task StringArrayOutOfBounds_NoDiagnostic() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string[] stringArray = {""test1"", ""test2""}; + string.Format(""This {0} {1} {2[||]} works"", stringArray); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task IFormatProviderAndOnePlaceholder() + { + await TestDiagnosticMissingAsync(@" using System.Globalization; +class Program +{ + static void Main(string[] args) + { + testStr = string.Format(new CultureInfo(""pt-BR""), ""The current price is {0[||]:C2} per ounce"", 2.45); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task IFormatProviderAndTwoPlaceholders() + { + await TestDiagnosticMissingAsync(@" using System.Globalization; +class Program +{ + static void Main(string[] args) + { + testStr = string.Format(new CultureInfo(""pt-BR""), ""The current price is {0[||]:C2} per {1} "", 2.45, ""ounce""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task IFormatProviderAndThreePlaceholders() + { + await TestDiagnosticMissingAsync(@" using System.Globalization; +class Program +{ + static void Main(string[] args) + { + testStr = string.Format(new CultureInfo(""pt-BR""), ""The current price is {0} {[||]1} {2} "", + 2.45, ""per"", ""ounce""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task IFormatProviderAndFourPlaceholders() + { + await TestDiagnosticMissingAsync(@" using System.Globalization; +class Program +{ + static void Main(string[] args) + { + testStr = string.Format(new CultureInfo(""pt-BR""), ""The current price is {0} {1[||]} {2} {3} "", + 2.45, ""per"", ""ounce"", ""today only""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task IFormatProviderAndObjectArray() + { + await TestDiagnosticMissingAsync(@" using System.Globalization; +class Program +{ + static void Main(string[] args) + { + object[] objectArray = { 1.25, ""2"", ""teststring""}; + string.Format(new CultureInfo(""pt-BR""), ""This {0} {1} {[||]2} works"", objectArray); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task WithComma() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(""{0[||],6}"", 34); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task WithColon() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(""{[||]0:N0}"", 34); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task WithCommaAndColon() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(""Test {0,[||]15:N0} output"", 34); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task WithPlaceholderAtBeginning() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(""{0[||]} is my test case"", ""This""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task WithPlaceholderAtEnd() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(""This is my {0[||]}"", ""test""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task WithDoubleBraces() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format("" {{ 2}} This {1[||]} is {2} {{ my {0} test }} "", ""teststring1"", ""teststring2"", ""teststring3""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task WithDoubleBracesAtBeginning() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(""{{ 2}} This {1[||]} is {2} {{ my {0} test }} "", ""teststring1"", ""teststring2"", ""teststring3""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task WithDoubleBracesAtEnd() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format("" {{ 2}} This {1[||]} is {2} {{ my {0} test }}"", ""teststring1"", ""teststring2"", ""teststring3""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task WithTripleBraces() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format("" {{{2}} This {1[||]} is {2} {{ my {0} test }}"", ""teststring1"", ""teststring2"", ""teststring3""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task NamedParameters() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(arg0: ""test"", arg1: ""also"", format: ""This {0} {[||]1} works""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task NamedParametersWithIFormatProvider() + { + await TestDiagnosticMissingAsync(@" using System.Globalization; +class Program +{ + static void Main(string[] args) + { + string.Format(arg0: ""test"", provider: new CultureInfo(""pt-BR""), format: ""This {0[||]} works""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task NamespaceAliasForStringClass() + { + await TestDiagnosticMissingAsync(@" using stringAlias = System.String; +class Program +{ + static void Main(string[] args) + { + stringAlias.Format(""This {0[||]} works"", ""test""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task MethodCallAsAnArgumentToAnotherMethod() + { + await TestDiagnosticMissingAsync(@" using System.IO; +class Program +{ + static void Main(string[] args) + { + Console.WriteLine(string.Format(format: ""This {0[||]} works"", arg0:""test"")); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task VerbatimMultipleLines() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(@""This {0} +{1} {2[||]} works"", ""multiple"", ""line"", ""test"")); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task Interpolated() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + var Name = ""Peter""; + var Age = 30; + + string.Format($""{Name,[||] 20} is {Age:D3} ""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task Empty() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(""[||]""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task LeftParenOnly() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format([||]; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task ParenthesesOnly() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format([||]); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task EmptyString() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(""[||]""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task FormatOnly_NoStringDot() + { + await TestDiagnosticMissingAsync(@" using static System.String +class Program +{ + static void Main(string[] args) + { + Format(""[||]""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task NamedParameters_BlankName() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format( : ""value""[||])); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task DuplicateNamedArgs() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(format:""This [||] "", format:"" test ""); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task GenericIdentifier() + { + await TestDiagnosticMissingAsync(@"using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace Generics_CSharp +{ + public class String + { + public void Format(string teststr) + { + Console.WriteLine(teststr); + } + } + + class Generics + { + static void Main(string[] args) + { + String testList = new String(); + testList.Format(""Test[||]String""); + } + } +} +"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task ClassNamedString() + { + await TestDiagnosticMissingAsync(@"using System; + +namespace System +{ + public class String + { + public static String Format(string format, object arg0) { return new String(); } + } +} + +class C +{ + static void Main(string[] args) + { + Console.WriteLine(String.Format(""test {[||]5} "", 1)); + } +} +"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task WarningTurnedOff() + { + await TestDiagnosticMissingAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(""This {1[||]} works"", ""test""); + } +} +", new TestParameters(options: CSharpOptionOffVBOptionOn())); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task WarningTurnedOn() + { + await TestDiagnosticInfoAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(""This [|{1}|] is my test"", ""teststring1""); + } +}", + options: CSharpOptionOnVBOptionOff(), + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task OnePlaceholderOutOfBounds() + { + await TestDiagnosticInfoAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(""This [|{1}|] is my test"", ""teststring1""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task TwoPlaceholdersWithOnePlaceholderOutOfBounds() + { + await TestDiagnosticInfoAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(""This [|{2}|] is my test"", ""teststring1"", ""teststring2""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task ThreePlaceholdersWithOnePlaceholderOutOfBounds() + { + await TestDiagnosticInfoAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(""This{0}{1}{2}[|{3}|] is my test"", ""teststring1"", ""teststring2"", ""teststring3""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task FourPlaceholdersWithOnePlaceholderOutOfBounds() + { + await TestDiagnosticInfoAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(""This{0}{1}{2}{3}[|{4}|] is my test"", ""teststring1"", ""teststring2"", + ""teststring3"", ""teststring4""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task iFormatProviderAndOnePlaceholderOutOfBounds() + { + await TestDiagnosticInfoAsync(@" using System.Globalization; +class Program +{ + static void Main(string[] args) + { + string.Format(new CultureInfo(""pt-BR""), ""This [|{1}|] is my test"", ""teststring1""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task iFormatProviderAndTwoPlaceholdersWithOnePlaceholderOutOfBounds() + { + await TestDiagnosticInfoAsync(@" using System.Globalization; +class Program +{ + static void Main(string[] args) + { + string.Format(new CultureInfo(""pt-BR""), ""This [|{2}|] is my test"", ""teststring1"", ""teststring2""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task IFormatProviderAndThreePlaceholdersWithOnePlaceholderOutOfBounds() + { + await TestDiagnosticInfoAsync(@" using System.Globalization; +class Program +{ + static void Main(string[] args) + { + string.Format(new CultureInfo(""pt-BR""), ""This{0}{1}{2}[|{3}|] is my test"", ""teststring1"", + ""teststring2"", ""teststring3""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task IFormatProviderAndFourPlaceholdersWithOnePlaceholderOutOfBounds() + { + await TestDiagnosticInfoAsync(@" using System.Globalization; +class Program +{ + static void Main(string[] args) + { + string.Format(new CultureInfo(""pt-BR""), ""This{0}{1}{2}{3}[|{4}|] is my test"", ""teststring1"", + ""teststring2"", ""teststring3"", ""teststring4""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task PlaceholderAtBeginningWithOnePlaceholderOutOfBounds() + { + await TestDiagnosticInfoAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format( ""[|{1}|]is my test"", ""teststring1""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task PlaceholderAtEndWithOnePlaceholderOutOfBounds() + { + await TestDiagnosticInfoAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format( ""is my test [|{2}|]"", ""teststring1"", ""teststring2""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task DoubleBracesAtBeginningWithOnePlaceholderOutOfBounds() + { + await TestDiagnosticInfoAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format( ""}}is my test [|{2}|]"", ""teststring1"", ""teststring2""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task DoubleBracesAtEndWithOnePlaceholderOutOfBounds() + { + await TestDiagnosticInfoAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format( ""is my test [|{2}|]{{"", ""teststring1"", ""teststring2""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task NamedParametersOneOutOfBounds() + { + await TestDiagnosticInfoAsync(@" using System.Globalization; +class Program +{ + static void Main(string[] args) + { + string.Format(arg0: ""test"", arg1: ""also"", format: ""This {0} [|{2}|] works""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task NamedParametersWithIFormatProviderOneOutOfBounds() + { + await TestDiagnosticInfoAsync(@" using System.Globalization; +class Program +{ + static void Main(string[] args) + { + string.Format(arg0: ""test"", arg1: ""also"", format: ""This {0} [|{2}|] works"", provider: new CultureInfo(""pt-BR"")) + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task FormatOnly_NoStringDot_OneOutOfBounds() + { + await TestDiagnosticInfoAsync(@" using static System.String +class Program +{ + static void Main(string[] args) + { + Format(""This {0} [|{2}|] squiggles"", ""test"", ""gets""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task Net45TestOutOfBounds() + { + var input = @" + < Workspace > + < Project Language = ""C#"" AssemblyName=""Assembly1"" CommonReferencesNet45=""true""> + + + + "; + await TestDiagnosticInfoAsync(input, + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task VerbatimMultipleLinesPlaceholderOutOfBounds() + { + await TestDiagnosticInfoAsync(@" class Program +{ + static void Main(string[] args) + { + string.Format(@""This {0} +{1} [|{3}|] works"", ""multiple"", ""line"", ""test"")); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task IntArrayOutOfBounds() + { + await TestDiagnosticInfoAsync(@" class Program +{ + static void Main(string[] args) + { + int[] intArray = {1, 2}; + string.Format(""This {0} [|{1}|] {2} works"", intArray); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task FirstPlaceholderOutOfBounds() + { + await TestDiagnosticInfoAsync(@" class Program +{ + static void Main(string[] args) + { + int[] intArray = {1, 2}; + string.Format(""This {0} [|{1}|] {2} works"", ""TestString""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task SecondPlaceholderOutOfBounds() + { + await TestDiagnosticInfoAsync(@" class Program +{ + static void Main(string[] args) + { + int[] intArray = {1, 2}; + string.Format(""This {0} {1} [|{2}|] works"", ""TestString""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task FirstOfMultipleSameNamedPlaceholderOutOfBounds() + { + await TestDiagnosticInfoAsync(@" class Program +{ + static void Main(string[] args) + { + int[] intArray = {1, 2}; + string.Format(""This {0} [|{2}|] {2} works"", ""TestString""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task SecondOfMultipleSameNamedPlaceholderOutOfBounds() + { + await TestDiagnosticInfoAsync(@" class Program +{ + static void Main(string[] args) + { + int[] intArray = {1, 2}; + string.Format(""This {0} {2} [|{2}|] works"", ""TestString""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + + [Fact, Trait(Traits.Feature, Traits.Features.ValidateFormatString)] + public async Task EmptyPlaceholder() + { + await TestDiagnosticInfoAsync(@" class Program +{ + static void Main(string[] args) + { + int[] intArray = {1, 2}; + string.Format(""This [|{}|] "", ""TestString""); + } +}", + options: null, + diagnosticId: IDEDiagnosticIds.ValidateFormatStringDiagnosticID, + diagnosticSeverity: DiagnosticSeverity.Warning, + diagnosticMessage: FeaturesResources.Format_string_contains_invalid_placeholder); + } + } +} diff --git a/src/EditorFeatures/TestUtilities/CodeActions/AbstractCodeActionOrUserDiagnosticTest.cs b/src/EditorFeatures/TestUtilities/CodeActions/AbstractCodeActionOrUserDiagnosticTest.cs index b888e63329a7fe40bfd202dcc98769e8fd043c6c..09ccfc5b291a216c0edcc9394ed820b6cce0a0a4 100644 --- a/src/EditorFeatures/TestUtilities/CodeActions/AbstractCodeActionOrUserDiagnosticTest.cs +++ b/src/EditorFeatures/TestUtilities/CodeActions/AbstractCodeActionOrUserDiagnosticTest.cs @@ -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> GetCodeActionsAsync( TestWorkspace workspace, TestParameters parameters) { @@ -100,6 +111,10 @@ public TestParameters WithFixProviderData(object fixProviderData) protected abstract Task> GetCodeActionsWorkerAsync( TestWorkspace workspace, TestParameters parameters); + + protected abstract Task> GetDiagnosticsWorkerAsync( + TestWorkspace workspace, TestParameters parameters); + protected async Task TestSmartTagTextAsync( string initialMarkup, string displayText, diff --git a/src/EditorFeatures/TestUtilities/CodeActions/AbstractCodeActionTest.cs b/src/EditorFeatures/TestUtilities/CodeActions/AbstractCodeActionTest.cs index 260144908e96719b7a66bbc34366c3a9097dbc9e..07a298f4116957516052838f0c6c7b473e433c36 100644 --- a/src/EditorFeatures/TestUtilities/CodeActions/AbstractCodeActionTest.cs +++ b/src/EditorFeatures/TestUtilities/CodeActions/AbstractCodeActionTest.cs @@ -37,6 +37,11 @@ public abstract class AbstractCodeActionTest : AbstractCodeActionOrUserDiagnosti : refactoring.Actions; } + protected override Task> GetDiagnosticsWorkerAsync(TestWorkspace workspace, TestParameters parameters) + { + return SpecializedTasks.EmptyImmutableArray(); + } + internal async Task GetCodeRefactoringAsync( TestWorkspace workspace, TestParameters parameters) { diff --git a/src/EditorFeatures/TestUtilities/Diagnostics/AbstractDiagnosticProviderBasedUserDiagnosticTest.cs b/src/EditorFeatures/TestUtilities/Diagnostics/AbstractDiagnosticProviderBasedUserDiagnosticTest.cs index 6a50d1951a693d0f1d61c7a86bcaf559236ee465..4efe9c6f577fb5b15a79e43308f173b70330f041 100644 --- a/src/EditorFeatures/TestUtilities/Diagnostics/AbstractDiagnosticProviderBasedUserDiagnosticTest.cs +++ b/src/EditorFeatures/TestUtilities/Diagnostics/AbstractDiagnosticProviderBasedUserDiagnosticTest.cs @@ -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(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 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 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()); + } } } diff --git a/src/EditorFeatures/TestUtilities/Diagnostics/AbstractUserDiagnosticTest.cs b/src/EditorFeatures/TestUtilities/Diagnostics/AbstractUserDiagnosticTest.cs index 381168432b71d6130172f947f370b8c5f4fe0afc..ed24a72079101f5fb615a9f910cf25d2492f6314 100644 --- a/src/EditorFeatures/TestUtilities/Diagnostics/AbstractUserDiagnosticTest.cs +++ b/src/EditorFeatures/TestUtilities/Diagnostics/AbstractUserDiagnosticTest.cs @@ -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> GetDiagnosticsWorkerAsync(TestWorkspace workspace, TestParameters parameters) + { + var diagnosticsAndCodeFixes = await GetDiagnosticAndFixAsync(workspace, parameters); + if (diagnosticsAndCodeFixes == null) + { + return ImmutableArray.Empty; + } + + return ImmutableArray.Create(diagnosticsAndCodeFixes.Item1); + } + internal async Task> 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(); + 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()) diff --git a/src/EditorFeatures/TestUtilities/Traits.cs b/src/EditorFeatures/TestUtilities/Traits.cs index 85f58a4c6f6aad41906dfbe8ff432bb05f8787f6..991e379200b296192ef8536f44fad6755c3451c9 100644 --- a/src/EditorFeatures/TestUtilities/Traits.cs +++ b/src/EditorFeatures/TestUtilities/Traits.cs @@ -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); diff --git a/src/EditorFeatures/VisualBasicTest/BasicEditorServicesTest.vbproj b/src/EditorFeatures/VisualBasicTest/BasicEditorServicesTest.vbproj index e5e1e5bb9e83dc8d8b0d1bd20f94c1b9c1eda3b1..e81cdb362888fa350c8ea4ae0e44d82e6c4e02a0 100644 --- a/src/EditorFeatures/VisualBasicTest/BasicEditorServicesTest.vbproj +++ b/src/EditorFeatures/VisualBasicTest/BasicEditorServicesTest.vbproj @@ -72,6 +72,7 @@ + PreserveNewest diff --git a/src/EditorFeatures/VisualBasicTest/Diagnostics/SimplifyTypeNames/SimplifyTypeNamesTests.vb b/src/EditorFeatures/VisualBasicTest/Diagnostics/SimplifyTypeNames/SimplifyTypeNamesTests.vb index cf9e357f2a1c8eb2167b1066c488d438384db305..5f4f603e5161b25215aaef7d756546fcb281b237 100644 --- a/src/EditorFeatures/VisualBasicTest/Diagnostics/SimplifyTypeNames/SimplifyTypeNamesTests.vb +++ b/src/EditorFeatures/VisualBasicTest/Diagnostics/SimplifyTypeNames/SimplifyTypeNamesTests.vb @@ -2423,10 +2423,9 @@ End Class") 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 diff --git a/src/EditorFeatures/VisualBasicTest/ValidateFormatString/ValidateFormatStringTests.vb b/src/EditorFeatures/VisualBasicTest/ValidateFormatString/ValidateFormatStringTests.vb new file mode 100644 index 0000000000000000000000000000000000000000..9b9a0be16c3494d83a0dea2038c00ecef05eaa9d --- /dev/null +++ b/src/EditorFeatures/VisualBasicTest/ValidateFormatString/ValidateFormatStringTests.vb @@ -0,0 +1,316 @@ +' 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 + + + 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 + + + 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 + + + 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 + + + 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 + + + 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 + + + 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 + + + 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 + + + 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 + + + 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 + + + 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 + + + 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 + + + Public Async Function DuplicateNamedParametersInNet45() 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 + + + 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 + + + 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 + + + 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 + + + Public Async Function Empty() As Task + Await TestDiagnosticMissingAsync(" +Class C + Sub Main + string.Format(""[||]"") + End Sub +End Class") + End Function + + + Public Async Function LeftParenOnly() As Task + Await TestDiagnosticMissingAsync(" +Class C + Sub Main + string.Format([||] + End Sub +End Class") + End Function + + + Public Async Function ParenthesesOnly() As Task + Await TestDiagnosticMissingAsync(" +Class C + Sub Main + string.Format [||] + End Sub +End Class") + End Function + + + Public Async Function DifferentFunction() As Task + Await TestDiagnosticMissingAsync(" +Class C + Sub Main + string.Compare [||] + End Sub +End Class") + End Function + + + 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 + + + Public Async Function OmittedArgument() As Task + Await TestDiagnosticMissingAsync("Module M + Sub Main() + String.Format([||],) + End Sub +End Module") + End Function + + + 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 + + + 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 diff --git a/src/Features/CSharp/Portable/ValidateFormatString/CSharpValidateFormatStringDiagnosticAnalyzer.cs b/src/Features/CSharp/Portable/ValidateFormatString/CSharpValidateFormatStringDiagnosticAnalyzer.cs new file mode 100644 index 0000000000000000000000000000000000000000..5d914da97fc2ffdf6ef1f117aa6cba4acbaa478b --- /dev/null +++ b/src/Features/CSharp/Portable/ValidateFormatString/CSharpValidateFormatStringDiagnosticAnalyzer.cs @@ -0,0 +1,40 @@ +// 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 + { + protected override ISyntaxFactsService GetSyntaxFactsService() + => CSharpSyntaxFactsService.Instance; + + protected override SyntaxKind GetInvocationExpressionSyntaxKind() + => SyntaxKind.InvocationExpression; + + protected override SyntaxNode TryGetMatchingNamedArgument( + SeparatedSyntaxList arguments, + string searchArgumentName) + { + foreach (var argument in arguments.Cast()) + { + 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 diff --git a/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs b/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs index 4340daab26a0dd7ae15027d1a993c17e7bb64736..785d34a97e4a611580287a2d15916dfa5f7b32e3 100644 --- a/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs +++ b/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs @@ -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"; diff --git a/src/Features/Core/Portable/FeaturesResources.Designer.cs b/src/Features/Core/Portable/FeaturesResources.Designer.cs index 5dcaa83dcec86ed27aa696e9c7082db2d591a821..112b43a6125620d36fe548214c5278f1373c81d2 100644 --- a/src/Features/Core/Portable/FeaturesResources.Designer.cs +++ b/src/Features/Core/Portable/FeaturesResources.Designer.cs @@ -1188,6 +1188,15 @@ internal class FeaturesResources { } } + /// + /// Looks up a localized string similar to Format string contains invalid placeholder. + /// + internal static string Format_string_contains_invalid_placeholder { + get { + return ResourceManager.GetString("Format_string_contains_invalid_placeholder", resourceCulture); + } + } + /// /// Looks up a localized string similar to from {0}. /// @@ -1791,6 +1800,15 @@ internal class FeaturesResources { } } + /// + /// Looks up a localized string similar to Invalid format string. + /// + internal static string Invalid_format_string { + get { + return ResourceManager.GetString("Invalid_format_string", resourceCulture); + } + } + /// /// Looks up a localized string similar to is. /// diff --git a/src/Features/Core/Portable/FeaturesResources.resx b/src/Features/Core/Portable/FeaturesResources.resx index aa3b9329e724b469971422b3885544242282337f..44e2b5d364d12a58e2a0da8125a29a9356778016 100644 --- a/src/Features/Core/Portable/FeaturesResources.resx +++ b/src/Features/Core/Portable/FeaturesResources.resx @@ -1,4 +1,4 @@ - +