From d6448487e8c3c9a8079945373ad20b61661c8d14 Mon Sep 17 00:00:00 2001 From: Rannes Date: Wed, 16 Oct 2024 10:30:27 +0700 Subject: [PATCH 01/11] feat: Add analyzer for QuerySelector --- .../AgodaCustom/AG0042UnitTests.cs | 63 +++++++++++ .../AG0042QuerySelectorShouldNotBeUsed.cs | 102 ++++++++++++++++++ .../CustomRulesResources.Designer.cs | 6 ++ .../AgodaCustom/CustomRulesResources.resx | 3 + 4 files changed, 174 insertions(+) create mode 100644 src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs create mode 100644 src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs diff --git a/src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs b/src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs new file mode 100644 index 0000000..03e5d9b --- /dev/null +++ b/src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs @@ -0,0 +1,63 @@ +using System.Threading.Tasks; +using Agoda.Analyzers.AgodaCustom; +using Agoda.Analyzers.Test.Helpers; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Playwright; +using NUnit.Framework; + +namespace Agoda.Analyzers.Test.AgodaCustom; + +class AG0042UnitTests : DiagnosticVerifier +{ + protected override DiagnosticAnalyzer DiagnosticAnalyzer => new AG0042QuerySelectorShouldNotBeUsed(); + + protected override string DiagnosticId => AG0042QuerySelectorShouldNotBeUsed.DIAGNOSTIC_ID; + + [Test] + public async Task AG0042_WhenUsingQuerySelectorAsyncWithPlaywrightPage_ShowWarning() + { + var code = new CodeDescriptor + { + References = new[] {typeof(IPage).Assembly}, + Code = @" + using System.Threading.Tasks; + using Microsoft.Playwright; + + class TestClass + { + public async Task TestMethod(IPage page) + { + await page.QuerySelectorAsync(""#element""); + } + }" + }; + + await VerifyDiagnosticsAsync(code, new DiagnosticLocation(9, 35)); + } + + [Test] + public async Task AG0042_RandomPageClass_DoNotShowWarning() + { + var code = new CodeDescriptor + { + References = new[] {typeof(IPage).Assembly}, + Code = @" + using System.Threading.Tasks; + + class TestClass + { + public async Task TestMethod(CustomPage page) + { + await page.QuerySelectorAsync(""#element""); + } + } + + class CustomPage + { + public Task QuerySelectorAsync(string selector) => Task.CompletedTask; + }" + }; + + await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults); + } +} diff --git a/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs b/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs new file mode 100644 index 0000000..8a292ad --- /dev/null +++ b/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs @@ -0,0 +1,102 @@ +using System.Collections.Immutable; +using Agoda.Analyzers.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Agoda.Analyzers.AgodaCustom +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class AG0042QuerySelectorShouldNotBeUsed : DiagnosticAnalyzer + { + public const string DIAGNOSTIC_ID = "AG0042"; + + private static readonly LocalizableString Title = new LocalizableResourceString( + nameof(CustomRulesResources.AG0042Title), CustomRulesResources.ResourceManager, + typeof(CustomRulesResources)); + + private static readonly LocalizableString MessageFormat = new LocalizableResourceString( + nameof(CustomRulesResources.AG0042Title), CustomRulesResources.ResourceManager, + typeof(CustomRulesResources)); + + private static readonly LocalizableString Description + = DescriptionContentLoader.GetAnalyzerDescription(nameof(AG0042QuerySelectorShouldNotBeUsed)); + + public static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + DIAGNOSTIC_ID, + Title, + MessageFormat, + AnalyzerCategory.CustomQualityRules, + DiagnosticSeverity.Warning, + AnalyzerConstants.EnabledByDefault, + Description, + "https://playwright.dev/dotnet/docs/api/class-elementhandle", + WellKnownDiagnosticTags.EditAndContinue); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.InvocationExpression); + } + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + + private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) + { + var invocationExpression = (InvocationExpressionSyntax)context.Node; + + var memberAccess = invocationExpression.Expression as MemberAccessExpressionSyntax; + if (memberAccess == null) + return; + + var methodName = memberAccess.Name.Identifier.Text; + + // Check if the method is QuerySelectorAsync + if (methodName != "QuerySelectorAsync") + return; + + // Get the semantic model to resolve types + var semanticModel = context.SemanticModel; + + // Get the type of the object calling the method (e.g., 'page' in 'page.QuerySelectorAsync') + var symbolInfo = semanticModel.GetSymbolInfo(memberAccess.Expression, context.CancellationToken); + var symbol = symbolInfo.Symbol; + + if (symbol == null) + return; + + // Check if it's a local variable, parameter, field, or property + INamedTypeSymbol typeSymbol = null; + + switch (symbol) + { + case ILocalSymbol localSymbol: + typeSymbol = localSymbol.Type as INamedTypeSymbol; + break; + case IParameterSymbol parameterSymbol: + typeSymbol = parameterSymbol.Type as INamedTypeSymbol; + break; + case IFieldSymbol fieldSymbol: + typeSymbol = fieldSymbol.Type as INamedTypeSymbol; + break; + case IPropertySymbol propertySymbol: + typeSymbol = propertySymbol.Type as INamedTypeSymbol; + break; + } + + if (typeSymbol == null) + return; + + // Check if the type is Playwright's Page or IPage + if (typeSymbol.ToString() != "Microsoft.Playwright.IPage" && + typeSymbol.ToString() != "Microsoft.Playwright.Page") + return; + + // Report a diagnostic at the location of this method call + var diagnostic = Diagnostic.Create(Descriptor, invocationExpression.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } +} \ No newline at end of file diff --git a/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.Designer.cs b/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.Designer.cs index 905e367..1b07b1b 100644 --- a/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.Designer.cs +++ b/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.Designer.cs @@ -323,5 +323,11 @@ public static string AG0041Title { return ResourceManager.GetString("AG0041Title", resourceCulture); } } + + public static string AG0042Title { + get { + return ResourceManager.GetString("AG0042Title", resourceCulture); + } + } } } diff --git a/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx b/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx index 1f99dcc..53ab91d 100644 --- a/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx +++ b/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx @@ -260,4 +260,7 @@ One exception is logging, where it can be useful to see the exact DC / cluster / You are using either an interpolated string or string concatenation in your logs, change these to the message template format to preserve structure in your logs + + QuerySelectorAsync makes things more flaky. Use locator-based "IPage.Locator" instead, as Playwright supports that better + \ No newline at end of file From 68a04e9384da9e9145de9312dc47c168c57811cf Mon Sep 17 00:00:00 2001 From: Rannes Date: Thu, 17 Oct 2024 11:35:21 +0700 Subject: [PATCH 02/11] docs: add MD doc for rule --- docs/AG0042.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/AG0042.md diff --git a/docs/AG0042.md b/docs/AG0042.md new file mode 100644 index 0000000..eb9d42d --- /dev/null +++ b/docs/AG0042.md @@ -0,0 +1,37 @@ +# AG0042: QuerySelectorAsync should not be used + +This rule detect if the method QuerySelectorAsync is used in the code. +As the method is not recommended by the library, then we shouldn't use it. + +## Description + +Reading the [official document](https://playwright.dev/dotnet/docs/api/class-elementhandle), it says this: +``` +The use of ElementHandle is discouraged, use Locator objects and web-first assertions instead. +``` + +## How to fix +Use [Page.Locator](https://playwright.dev/docs/locators) instead, which is the recommended way to interact with the page. +We cannot do the codefix for it, as it's not directly 1-to-1 replacement, but usually the replacement is easy. + +## Examples + +``` +DO: +_page.Locator("[data-testid='toast-example']") + +DO NOT: +_page.QuerySelectorAsync($"[data-testid='toast-example']") +``` + +``` +DO: +_page.Locator($"[data-element-name='dropdown-example']") + +DO NOT: +var dropdown = await _page.QuerySelectorAsync($"[data-element-name='dropdown-example']"); +if (dropdown == null) +{ + throw new Exception($"Dropdown with data-element-name 'dropdown-example' not found."); +} +``` \ No newline at end of file From f87e9afc9995c6403bf3452c6d8bea97d0e395e7 Mon Sep 17 00:00:00 2001 From: Rannes Date: Thu, 17 Oct 2024 11:36:11 +0700 Subject: [PATCH 03/11] feat: fix code and add more tests --- .../AgodaCustom/AG0042UnitTests.cs | 237 +++++++++++++++--- .../AG0042QuerySelectorShouldNotBeUsed.cs | 19 +- 2 files changed, 216 insertions(+), 40 deletions(-) diff --git a/src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs b/src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs index 03e5d9b..83a776a 100644 --- a/src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs +++ b/src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs @@ -18,46 +18,227 @@ public async Task AG0042_WhenUsingQuerySelectorAsyncWithPlaywrightPage_ShowWarni { var code = new CodeDescriptor { - References = new[] {typeof(IPage).Assembly}, + References = new[] { typeof(IPage).Assembly }, Code = @" - using System.Threading.Tasks; - using Microsoft.Playwright; + using System.Threading.Tasks; + using Microsoft.Playwright; - class TestClass + class TestClass + { + public async Task TestMethod(IPage page) { - public async Task TestMethod(IPage page) - { - await page.QuerySelectorAsync(""#element""); - } - }" + await page.QuerySelectorAsync(""#element""); + } + }" }; - - await VerifyDiagnosticsAsync(code, new DiagnosticLocation(9, 35)); + + await VerifyDiagnosticsAsync(code, new DiagnosticLocation(9, 31)); + } + + [Test] + public async Task AG0042_WhenUsingQuerySelectorAsyncWithIPageInstanceVariable_ShowWarning() + { + var code = new CodeDescriptor + { + References = new[] { typeof(IPage).Assembly }, + Code = @" + using System.Threading.Tasks; + using Microsoft.Playwright; + + class TestClass + { + private IPage _page; + + public async Task TestMethod() + { + await _page.QuerySelectorAsync(""#element""); + } + }" + }; + + await VerifyDiagnosticsAsync(code, new DiagnosticLocation(11, 31)); } - + [Test] - public async Task AG0042_RandomPageClass_DoNotShowWarning() + public async Task AG0042_WhenUsingQuerySelectorAsyncWithLocalIPageVariable_ShowWarning() { var code = new CodeDescriptor { - References = new[] {typeof(IPage).Assembly}, + References = new[] { typeof(IPage).Assembly }, Code = @" - using System.Threading.Tasks; + using System.Threading.Tasks; + using Microsoft.Playwright; - class TestClass - { - public async Task TestMethod(CustomPage page) - { - await page.QuerySelectorAsync(""#element""); - } - } + class TestClass + { + public async Task TestMethod() + { + IPage page = null; + await page.QuerySelectorAsync(""#element""); + } + }" + }; - class CustomPage - { - public Task QuerySelectorAsync(string selector) => Task.CompletedTask; - }" + await VerifyDiagnosticsAsync(code, new DiagnosticLocation(10, 31)); + } + + [Test] + public async Task AG0042_WhenUsingQuerySelectorAsyncWithIPageProperty_ShowWarning() + { + var code = new CodeDescriptor + { + References = new[] { typeof(IPage).Assembly }, + Code = @" + using System.Threading.Tasks; + using Microsoft.Playwright; + + class TestClass + { + public IPage Page { get; set; } + + public async Task TestMethod() + { + await Page.QuerySelectorAsync(""#element""); + } + }" }; - + + await VerifyDiagnosticsAsync(code, new DiagnosticLocation(11, 31)); + } + + [Test] + public async Task AG0042_WhenUsingQuerySelectorAsyncWithNonIPageType_NoWarning() + { + var code = new CodeDescriptor + { + // No need to reference Microsoft.Playwright + Code = @" + using System.Threading.Tasks; + + class CustomPage + { + public async Task QuerySelectorAsync(string selector) { } + } + + class TestClass + { + public async Task TestMethod() + { + CustomPage page = new CustomPage(); + await page.QuerySelectorAsync(""#element""); + } + }" + }; + + await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults); + } + + [Test] + public async Task AG0042_WhenUsingLocatorMethodName_NoWarning() + { + var code = new CodeDescriptor + { + References = new[] { typeof(IPage).Assembly }, + Code = @" + using System.Threading.Tasks; + using Microsoft.Playwright; + + class TestClass + { + public void TestMethod(IPage page) + { + page.Locator(""#selector""); + } + }" + }; + + await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults); + } + + [Test] + public async Task AG0042_WhenSymbolIsNull_NoWarning() + { + var code = new CodeDescriptor + { + // Intentionally use an unknown variable to cause symbol to be null + Code = @" + using System.Threading.Tasks; + + class TestClass + { + public async Task TestMethod() + { + dynamic unknownVariable = null; + await unknownVariable.QuerySelectorAsync(""#element""); + } + }" + }; + + await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults); + } + + [Test] + public async Task AG0042_WhenTypeSymbolIsNull_NoWarning() + { + var code = new CodeDescriptor + { + Code = @" + using System.Threading.Tasks; + + class TestClass + { + public async Task TestMethod(dynamic page) + { + await page.QuerySelectorAsync(""#element""); + } + }" + }; + + await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults); + } + + [Test] + public async Task AG0042_WhenInvocationExpressionIsNotMemberAccess_NoWarning() + { + var code = new CodeDescriptor + { + Code = @" + using System.Threading.Tasks; + + class TestClass + { + public async Task TestMethod() + { + await QuerySelectorAsync(""#element""); + } + + public async Task QuerySelectorAsync(string selector) { } + }" + }; + + await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults); + } + + [Test] + public async Task AG0042_WhenMemberAccessExpressionHasNoIdentifier_NoWarning() + { + var code = new CodeDescriptor + { + Code = @" + using System.Threading.Tasks; + + class TestClass + { + public async Task TestMethod() + { + var func = GetPage(); + await func().QuerySelectorAsync(""#element""); + } + + public System.Func GetPage() => null; + }" + }; + await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults); } -} +} \ No newline at end of file diff --git a/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs b/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs index 8a292ad..131b439 100644 --- a/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs +++ b/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs @@ -31,7 +31,7 @@ private static readonly LocalizableString Description DiagnosticSeverity.Warning, AnalyzerConstants.EnabledByDefault, Description, - "https://playwright.dev/dotnet/docs/api/class-elementhandle", + "https://github.com/agoda-com/AgodaAnalyzers/blob/master/docs/AG0042.md", WellKnownDiagnosticTags.EditAndContinue); public override void Initialize(AnalysisContext context) @@ -47,29 +47,23 @@ private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) { var invocationExpression = (InvocationExpressionSyntax)context.Node; - var memberAccess = invocationExpression.Expression as MemberAccessExpressionSyntax; - if (memberAccess == null) + if (!(invocationExpression.Expression is MemberAccessExpressionSyntax memberAccess)) return; var methodName = memberAccess.Name.Identifier.Text; - // Check if the method is QuerySelectorAsync if (methodName != "QuerySelectorAsync") return; - // Get the semantic model to resolve types var semanticModel = context.SemanticModel; - // Get the type of the object calling the method (e.g., 'page' in 'page.QuerySelectorAsync') var symbolInfo = semanticModel.GetSymbolInfo(memberAccess.Expression, context.CancellationToken); var symbol = symbolInfo.Symbol; if (symbol == null) return; - // Check if it's a local variable, parameter, field, or property - INamedTypeSymbol typeSymbol = null; - + INamedTypeSymbol typeSymbol; switch (symbol) { case ILocalSymbol localSymbol: @@ -84,17 +78,18 @@ private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) case IPropertySymbol propertySymbol: typeSymbol = propertySymbol.Type as INamedTypeSymbol; break; + default: + typeSymbol = null; + break; } - + if (typeSymbol == null) return; - // Check if the type is Playwright's Page or IPage if (typeSymbol.ToString() != "Microsoft.Playwright.IPage" && typeSymbol.ToString() != "Microsoft.Playwright.Page") return; - // Report a diagnostic at the location of this method call var diagnostic = Diagnostic.Create(Descriptor, invocationExpression.GetLocation()); context.ReportDiagnostic(diagnostic); } From 47751a50ed6478d42bbf560e91481c1eb7b634aa Mon Sep 17 00:00:00 2001 From: Rannes Date: Wed, 16 Oct 2024 10:30:27 +0700 Subject: [PATCH 04/11] feat: Add analyzer for QuerySelector --- .../AgodaCustom/AG0042UnitTests.cs | 63 +++++++++++ .../AG0042QuerySelectorShouldNotBeUsed.cs | 102 ++++++++++++++++++ .../CustomRulesResources.Designer.cs | 6 ++ .../AgodaCustom/CustomRulesResources.resx | 3 + 4 files changed, 174 insertions(+) create mode 100644 src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs create mode 100644 src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs diff --git a/src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs b/src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs new file mode 100644 index 0000000..03e5d9b --- /dev/null +++ b/src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs @@ -0,0 +1,63 @@ +using System.Threading.Tasks; +using Agoda.Analyzers.AgodaCustom; +using Agoda.Analyzers.Test.Helpers; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Playwright; +using NUnit.Framework; + +namespace Agoda.Analyzers.Test.AgodaCustom; + +class AG0042UnitTests : DiagnosticVerifier +{ + protected override DiagnosticAnalyzer DiagnosticAnalyzer => new AG0042QuerySelectorShouldNotBeUsed(); + + protected override string DiagnosticId => AG0042QuerySelectorShouldNotBeUsed.DIAGNOSTIC_ID; + + [Test] + public async Task AG0042_WhenUsingQuerySelectorAsyncWithPlaywrightPage_ShowWarning() + { + var code = new CodeDescriptor + { + References = new[] {typeof(IPage).Assembly}, + Code = @" + using System.Threading.Tasks; + using Microsoft.Playwright; + + class TestClass + { + public async Task TestMethod(IPage page) + { + await page.QuerySelectorAsync(""#element""); + } + }" + }; + + await VerifyDiagnosticsAsync(code, new DiagnosticLocation(9, 35)); + } + + [Test] + public async Task AG0042_RandomPageClass_DoNotShowWarning() + { + var code = new CodeDescriptor + { + References = new[] {typeof(IPage).Assembly}, + Code = @" + using System.Threading.Tasks; + + class TestClass + { + public async Task TestMethod(CustomPage page) + { + await page.QuerySelectorAsync(""#element""); + } + } + + class CustomPage + { + public Task QuerySelectorAsync(string selector) => Task.CompletedTask; + }" + }; + + await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults); + } +} diff --git a/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs b/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs new file mode 100644 index 0000000..8a292ad --- /dev/null +++ b/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs @@ -0,0 +1,102 @@ +using System.Collections.Immutable; +using Agoda.Analyzers.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Agoda.Analyzers.AgodaCustom +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class AG0042QuerySelectorShouldNotBeUsed : DiagnosticAnalyzer + { + public const string DIAGNOSTIC_ID = "AG0042"; + + private static readonly LocalizableString Title = new LocalizableResourceString( + nameof(CustomRulesResources.AG0042Title), CustomRulesResources.ResourceManager, + typeof(CustomRulesResources)); + + private static readonly LocalizableString MessageFormat = new LocalizableResourceString( + nameof(CustomRulesResources.AG0042Title), CustomRulesResources.ResourceManager, + typeof(CustomRulesResources)); + + private static readonly LocalizableString Description + = DescriptionContentLoader.GetAnalyzerDescription(nameof(AG0042QuerySelectorShouldNotBeUsed)); + + public static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + DIAGNOSTIC_ID, + Title, + MessageFormat, + AnalyzerCategory.CustomQualityRules, + DiagnosticSeverity.Warning, + AnalyzerConstants.EnabledByDefault, + Description, + "https://playwright.dev/dotnet/docs/api/class-elementhandle", + WellKnownDiagnosticTags.EditAndContinue); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.InvocationExpression); + } + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + + private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) + { + var invocationExpression = (InvocationExpressionSyntax)context.Node; + + var memberAccess = invocationExpression.Expression as MemberAccessExpressionSyntax; + if (memberAccess == null) + return; + + var methodName = memberAccess.Name.Identifier.Text; + + // Check if the method is QuerySelectorAsync + if (methodName != "QuerySelectorAsync") + return; + + // Get the semantic model to resolve types + var semanticModel = context.SemanticModel; + + // Get the type of the object calling the method (e.g., 'page' in 'page.QuerySelectorAsync') + var symbolInfo = semanticModel.GetSymbolInfo(memberAccess.Expression, context.CancellationToken); + var symbol = symbolInfo.Symbol; + + if (symbol == null) + return; + + // Check if it's a local variable, parameter, field, or property + INamedTypeSymbol typeSymbol = null; + + switch (symbol) + { + case ILocalSymbol localSymbol: + typeSymbol = localSymbol.Type as INamedTypeSymbol; + break; + case IParameterSymbol parameterSymbol: + typeSymbol = parameterSymbol.Type as INamedTypeSymbol; + break; + case IFieldSymbol fieldSymbol: + typeSymbol = fieldSymbol.Type as INamedTypeSymbol; + break; + case IPropertySymbol propertySymbol: + typeSymbol = propertySymbol.Type as INamedTypeSymbol; + break; + } + + if (typeSymbol == null) + return; + + // Check if the type is Playwright's Page or IPage + if (typeSymbol.ToString() != "Microsoft.Playwright.IPage" && + typeSymbol.ToString() != "Microsoft.Playwright.Page") + return; + + // Report a diagnostic at the location of this method call + var diagnostic = Diagnostic.Create(Descriptor, invocationExpression.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } +} \ No newline at end of file diff --git a/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.Designer.cs b/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.Designer.cs index 905e367..1b07b1b 100644 --- a/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.Designer.cs +++ b/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.Designer.cs @@ -323,5 +323,11 @@ public static string AG0041Title { return ResourceManager.GetString("AG0041Title", resourceCulture); } } + + public static string AG0042Title { + get { + return ResourceManager.GetString("AG0042Title", resourceCulture); + } + } } } diff --git a/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx b/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx index 1f99dcc..53ab91d 100644 --- a/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx +++ b/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx @@ -260,4 +260,7 @@ One exception is logging, where it can be useful to see the exact DC / cluster / You are using either an interpolated string or string concatenation in your logs, change these to the message template format to preserve structure in your logs + + QuerySelectorAsync makes things more flaky. Use locator-based "IPage.Locator" instead, as Playwright supports that better + \ No newline at end of file From 7e2d1ac7b0414a6d368bcba556a6a77886ca71c0 Mon Sep 17 00:00:00 2001 From: Rannes Date: Thu, 17 Oct 2024 11:35:21 +0700 Subject: [PATCH 05/11] docs: add MD doc for rule --- docs/AG0042.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/AG0042.md diff --git a/docs/AG0042.md b/docs/AG0042.md new file mode 100644 index 0000000..eb9d42d --- /dev/null +++ b/docs/AG0042.md @@ -0,0 +1,37 @@ +# AG0042: QuerySelectorAsync should not be used + +This rule detect if the method QuerySelectorAsync is used in the code. +As the method is not recommended by the library, then we shouldn't use it. + +## Description + +Reading the [official document](https://playwright.dev/dotnet/docs/api/class-elementhandle), it says this: +``` +The use of ElementHandle is discouraged, use Locator objects and web-first assertions instead. +``` + +## How to fix +Use [Page.Locator](https://playwright.dev/docs/locators) instead, which is the recommended way to interact with the page. +We cannot do the codefix for it, as it's not directly 1-to-1 replacement, but usually the replacement is easy. + +## Examples + +``` +DO: +_page.Locator("[data-testid='toast-example']") + +DO NOT: +_page.QuerySelectorAsync($"[data-testid='toast-example']") +``` + +``` +DO: +_page.Locator($"[data-element-name='dropdown-example']") + +DO NOT: +var dropdown = await _page.QuerySelectorAsync($"[data-element-name='dropdown-example']"); +if (dropdown == null) +{ + throw new Exception($"Dropdown with data-element-name 'dropdown-example' not found."); +} +``` \ No newline at end of file From 83dd8660caec2fd1391b00676770039b389ef46d Mon Sep 17 00:00:00 2001 From: Rannes Date: Thu, 17 Oct 2024 11:36:11 +0700 Subject: [PATCH 06/11] feat: fix code and add more tests --- .../AgodaCustom/AG0042UnitTests.cs | 237 +++++++++++++++--- .../AG0042QuerySelectorShouldNotBeUsed.cs | 19 +- 2 files changed, 216 insertions(+), 40 deletions(-) diff --git a/src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs b/src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs index 03e5d9b..83a776a 100644 --- a/src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs +++ b/src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs @@ -18,46 +18,227 @@ public async Task AG0042_WhenUsingQuerySelectorAsyncWithPlaywrightPage_ShowWarni { var code = new CodeDescriptor { - References = new[] {typeof(IPage).Assembly}, + References = new[] { typeof(IPage).Assembly }, Code = @" - using System.Threading.Tasks; - using Microsoft.Playwright; + using System.Threading.Tasks; + using Microsoft.Playwright; - class TestClass + class TestClass + { + public async Task TestMethod(IPage page) { - public async Task TestMethod(IPage page) - { - await page.QuerySelectorAsync(""#element""); - } - }" + await page.QuerySelectorAsync(""#element""); + } + }" }; - - await VerifyDiagnosticsAsync(code, new DiagnosticLocation(9, 35)); + + await VerifyDiagnosticsAsync(code, new DiagnosticLocation(9, 31)); + } + + [Test] + public async Task AG0042_WhenUsingQuerySelectorAsyncWithIPageInstanceVariable_ShowWarning() + { + var code = new CodeDescriptor + { + References = new[] { typeof(IPage).Assembly }, + Code = @" + using System.Threading.Tasks; + using Microsoft.Playwright; + + class TestClass + { + private IPage _page; + + public async Task TestMethod() + { + await _page.QuerySelectorAsync(""#element""); + } + }" + }; + + await VerifyDiagnosticsAsync(code, new DiagnosticLocation(11, 31)); } - + [Test] - public async Task AG0042_RandomPageClass_DoNotShowWarning() + public async Task AG0042_WhenUsingQuerySelectorAsyncWithLocalIPageVariable_ShowWarning() { var code = new CodeDescriptor { - References = new[] {typeof(IPage).Assembly}, + References = new[] { typeof(IPage).Assembly }, Code = @" - using System.Threading.Tasks; + using System.Threading.Tasks; + using Microsoft.Playwright; - class TestClass - { - public async Task TestMethod(CustomPage page) - { - await page.QuerySelectorAsync(""#element""); - } - } + class TestClass + { + public async Task TestMethod() + { + IPage page = null; + await page.QuerySelectorAsync(""#element""); + } + }" + }; - class CustomPage - { - public Task QuerySelectorAsync(string selector) => Task.CompletedTask; - }" + await VerifyDiagnosticsAsync(code, new DiagnosticLocation(10, 31)); + } + + [Test] + public async Task AG0042_WhenUsingQuerySelectorAsyncWithIPageProperty_ShowWarning() + { + var code = new CodeDescriptor + { + References = new[] { typeof(IPage).Assembly }, + Code = @" + using System.Threading.Tasks; + using Microsoft.Playwright; + + class TestClass + { + public IPage Page { get; set; } + + public async Task TestMethod() + { + await Page.QuerySelectorAsync(""#element""); + } + }" }; - + + await VerifyDiagnosticsAsync(code, new DiagnosticLocation(11, 31)); + } + + [Test] + public async Task AG0042_WhenUsingQuerySelectorAsyncWithNonIPageType_NoWarning() + { + var code = new CodeDescriptor + { + // No need to reference Microsoft.Playwright + Code = @" + using System.Threading.Tasks; + + class CustomPage + { + public async Task QuerySelectorAsync(string selector) { } + } + + class TestClass + { + public async Task TestMethod() + { + CustomPage page = new CustomPage(); + await page.QuerySelectorAsync(""#element""); + } + }" + }; + + await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults); + } + + [Test] + public async Task AG0042_WhenUsingLocatorMethodName_NoWarning() + { + var code = new CodeDescriptor + { + References = new[] { typeof(IPage).Assembly }, + Code = @" + using System.Threading.Tasks; + using Microsoft.Playwright; + + class TestClass + { + public void TestMethod(IPage page) + { + page.Locator(""#selector""); + } + }" + }; + + await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults); + } + + [Test] + public async Task AG0042_WhenSymbolIsNull_NoWarning() + { + var code = new CodeDescriptor + { + // Intentionally use an unknown variable to cause symbol to be null + Code = @" + using System.Threading.Tasks; + + class TestClass + { + public async Task TestMethod() + { + dynamic unknownVariable = null; + await unknownVariable.QuerySelectorAsync(""#element""); + } + }" + }; + + await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults); + } + + [Test] + public async Task AG0042_WhenTypeSymbolIsNull_NoWarning() + { + var code = new CodeDescriptor + { + Code = @" + using System.Threading.Tasks; + + class TestClass + { + public async Task TestMethod(dynamic page) + { + await page.QuerySelectorAsync(""#element""); + } + }" + }; + + await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults); + } + + [Test] + public async Task AG0042_WhenInvocationExpressionIsNotMemberAccess_NoWarning() + { + var code = new CodeDescriptor + { + Code = @" + using System.Threading.Tasks; + + class TestClass + { + public async Task TestMethod() + { + await QuerySelectorAsync(""#element""); + } + + public async Task QuerySelectorAsync(string selector) { } + }" + }; + + await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults); + } + + [Test] + public async Task AG0042_WhenMemberAccessExpressionHasNoIdentifier_NoWarning() + { + var code = new CodeDescriptor + { + Code = @" + using System.Threading.Tasks; + + class TestClass + { + public async Task TestMethod() + { + var func = GetPage(); + await func().QuerySelectorAsync(""#element""); + } + + public System.Func GetPage() => null; + }" + }; + await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults); } -} +} \ No newline at end of file diff --git a/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs b/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs index 8a292ad..131b439 100644 --- a/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs +++ b/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs @@ -31,7 +31,7 @@ private static readonly LocalizableString Description DiagnosticSeverity.Warning, AnalyzerConstants.EnabledByDefault, Description, - "https://playwright.dev/dotnet/docs/api/class-elementhandle", + "https://github.com/agoda-com/AgodaAnalyzers/blob/master/docs/AG0042.md", WellKnownDiagnosticTags.EditAndContinue); public override void Initialize(AnalysisContext context) @@ -47,29 +47,23 @@ private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) { var invocationExpression = (InvocationExpressionSyntax)context.Node; - var memberAccess = invocationExpression.Expression as MemberAccessExpressionSyntax; - if (memberAccess == null) + if (!(invocationExpression.Expression is MemberAccessExpressionSyntax memberAccess)) return; var methodName = memberAccess.Name.Identifier.Text; - // Check if the method is QuerySelectorAsync if (methodName != "QuerySelectorAsync") return; - // Get the semantic model to resolve types var semanticModel = context.SemanticModel; - // Get the type of the object calling the method (e.g., 'page' in 'page.QuerySelectorAsync') var symbolInfo = semanticModel.GetSymbolInfo(memberAccess.Expression, context.CancellationToken); var symbol = symbolInfo.Symbol; if (symbol == null) return; - // Check if it's a local variable, parameter, field, or property - INamedTypeSymbol typeSymbol = null; - + INamedTypeSymbol typeSymbol; switch (symbol) { case ILocalSymbol localSymbol: @@ -84,17 +78,18 @@ private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) case IPropertySymbol propertySymbol: typeSymbol = propertySymbol.Type as INamedTypeSymbol; break; + default: + typeSymbol = null; + break; } - + if (typeSymbol == null) return; - // Check if the type is Playwright's Page or IPage if (typeSymbol.ToString() != "Microsoft.Playwright.IPage" && typeSymbol.ToString() != "Microsoft.Playwright.Page") return; - // Report a diagnostic at the location of this method call var diagnostic = Diagnostic.Create(Descriptor, invocationExpression.GetLocation()); context.ReportDiagnostic(diagnostic); } From a2d2eadc31c4bf73a1d6f675605bb6e1022f88f4 Mon Sep 17 00:00:00 2001 From: Rannes Date: Thu, 17 Oct 2024 13:33:36 +0700 Subject: [PATCH 07/11] docs: move the MD to correct folder --- {docs => doc}/AG0042.md | 0 .../AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {docs => doc}/AG0042.md (100%) diff --git a/docs/AG0042.md b/doc/AG0042.md similarity index 100% rename from docs/AG0042.md rename to doc/AG0042.md diff --git a/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs b/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs index 131b439..176f565 100644 --- a/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs +++ b/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs @@ -31,7 +31,7 @@ private static readonly LocalizableString Description DiagnosticSeverity.Warning, AnalyzerConstants.EnabledByDefault, Description, - "https://github.com/agoda-com/AgodaAnalyzers/blob/master/docs/AG0042.md", + "https://github.com/agoda-com/AgodaAnalyzers/blob/master/doc/AG0042.md", WellKnownDiagnosticTags.EditAndContinue); public override void Initialize(AnalysisContext context) From c5fb5c5c88817056b92d84f238fc468998bd9263 Mon Sep 17 00:00:00 2001 From: Rannes Date: Thu, 17 Oct 2024 13:36:23 +0700 Subject: [PATCH 08/11] remove docs folder and files --- docs/AG0042.md | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 docs/AG0042.md diff --git a/docs/AG0042.md b/docs/AG0042.md deleted file mode 100644 index eb9d42d..0000000 --- a/docs/AG0042.md +++ /dev/null @@ -1,37 +0,0 @@ -# AG0042: QuerySelectorAsync should not be used - -This rule detect if the method QuerySelectorAsync is used in the code. -As the method is not recommended by the library, then we shouldn't use it. - -## Description - -Reading the [official document](https://playwright.dev/dotnet/docs/api/class-elementhandle), it says this: -``` -The use of ElementHandle is discouraged, use Locator objects and web-first assertions instead. -``` - -## How to fix -Use [Page.Locator](https://playwright.dev/docs/locators) instead, which is the recommended way to interact with the page. -We cannot do the codefix for it, as it's not directly 1-to-1 replacement, but usually the replacement is easy. - -## Examples - -``` -DO: -_page.Locator("[data-testid='toast-example']") - -DO NOT: -_page.QuerySelectorAsync($"[data-testid='toast-example']") -``` - -``` -DO: -_page.Locator($"[data-element-name='dropdown-example']") - -DO NOT: -var dropdown = await _page.QuerySelectorAsync($"[data-element-name='dropdown-example']"); -if (dropdown == null) -{ - throw new Exception($"Dropdown with data-element-name 'dropdown-example' not found."); -} -``` \ No newline at end of file From c6c7061298d5e96d382235a17332d0039972b284 Mon Sep 17 00:00:00 2001 From: Rannes Date: Thu, 17 Oct 2024 14:57:11 +0700 Subject: [PATCH 09/11] docs: let Claude generate docs --- doc/AG0042.md | 138 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 112 insertions(+), 26 deletions(-) diff --git a/doc/AG0042.md b/doc/AG0042.md index eb9d42d..a2151d2 100644 --- a/doc/AG0042.md +++ b/doc/AG0042.md @@ -1,37 +1,123 @@ -# AG0042: QuerySelectorAsync should not be used +# AG0042: QuerySelector should not be used with Playwright -This rule detect if the method QuerySelectorAsync is used in the code. -As the method is not recommended by the library, then we shouldn't use it. +## Problem Description -## Description +Using `QuerySelectorAsync()` in Playwright tests can lead to brittle and unreliable tests. This method uses CSS selectors which can be fragile and may break when the UI structure changes. Instead, more reliable locator strategies like data-testid or role-based selectors should be used. -Reading the [official document](https://playwright.dev/dotnet/docs/api/class-elementhandle), it says this: -``` -The use of ElementHandle is discouraged, use Locator objects and web-first assertions instead. -``` +## Rule Details -## How to fix -Use [Page.Locator](https://playwright.dev/docs/locators) instead, which is the recommended way to interact with the page. -We cannot do the codefix for it, as it's not directly 1-to-1 replacement, but usually the replacement is easy. +This rule raises an issue when `QuerySelectorAsync()` is called on Playwright `IPage` or `Page` objects. -## Examples - -``` -DO: -_page.Locator("[data-testid='toast-example']") +### Noncompliant Code Example -DO NOT: -_page.QuerySelectorAsync($"[data-testid='toast-example']") +```csharp +public async Task ClickLoginButton(IPage page) +{ + // Noncompliant: Using QuerySelectorAsync with CSS selector + var loginButton = await page.QuerySelectorAsync(".login-button"); + await loginButton.ClickAsync(); +} ``` -``` -DO: -_page.Locator($"[data-element-name='dropdown-example']") +### Compliant Solution -DO NOT: -var dropdown = await _page.QuerySelectorAsync($"[data-element-name='dropdown-example']"); -if (dropdown == null) +```csharp +public async Task ClickLoginButton(IPage page) { - throw new Exception($"Dropdown with data-element-name 'dropdown-example' not found."); + // Compliant: Using Locator with data-testid + await page.Locator("[data-testid='login-button']").ClickAsync(); + + // Compliant: Using role-based selector + await page.GetByRole(AriaRole.Button, new() { Name = "Login" }).ClickAsync(); + + // Compliant: Using text content + await page.GetByText("Login").ClickAsync(); } -``` \ No newline at end of file +``` + +## Why is this an Issue? + +1. **Fragile Selectors**: CSS selectors are tightly coupled to the DOM structure and styling classes, making tests brittle when: + - CSS classes are renamed or removed + - DOM hierarchy changes + - Styling frameworks are updated + +2. **Maintainability**: CSS selectors can be complex and hard to maintain, especially when dealing with nested elements or specific combinations of classes. + +3. **Best Practices**: Playwright provides better alternatives that are: + - More resilient to changes + - More readable and maintainable + - Better aligned with testing best practices + +## Better Alternatives + +Playwright provides several better methods for selecting elements: + +1. **Data Test IDs**: + ```csharp + await page.Locator("[data-testid='submit-button']").ClickAsync(); + ``` + +2. **Role-based Selectors**: + ```csharp + await page.GetByRole(AriaRole.Button).ClickAsync(); + await page.GetByRole(AriaRole.Textbox, new() { Name = "Username" }).FillAsync("user"); + ``` + +3. **Text Content**: + ```csharp + await page.GetByText("Sign up").ClickAsync(); + await page.GetByLabel("Password").FillAsync("secret"); + ``` + +4. **Placeholder Text**: + ```csharp + await page.GetByPlaceholder("Enter email").FillAsync("test@example.com"); + ``` + +## How to Fix It + +1. Replace `QuerySelectorAsync()` calls with more specific Playwright locators: + + ```csharp + // Before + var element = await page.QuerySelectorAsync(".submit-btn"); + + // After + var element = page.GetByRole(AriaRole.Button, new() { Name = "Submit" }); + ``` + +2. Add data-testid attributes to your application's elements: + ```html + + ``` + + ```csharp + await page.Locator("[data-testid='submit-button']").ClickAsync(); + ``` + +3. Use semantic HTML with ARIA roles and labels: + ```html + + ``` + + ```csharp + await page.GetByRole(AriaRole.Button, new() { Name = "Submit form" }).ClickAsync(); + ``` + +## Exceptions + +This rule might be relaxed in the following scenarios: +- Legacy test code that's pending migration +- Complex third-party components where other selectors aren't available +- Testing CSS-specific functionality + +## Benefits +- More reliable tests +- Better test maintenance +- Clearer test intentions +- Improved accessibility testing + +## References +- [ElementHandle is Discouraged by official Documents](https://playwright.dev/dotnet/docs/api/class-elementhandle) +- [Playwright Locators Documentation](https://playwright.dev/docs/locators) From 90576432106a99acffdfb16ab0572ee90ab1c103 Mon Sep 17 00:00:00 2001 From: Joel Dickson <9032274+joeldickson@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:05:19 +0700 Subject: [PATCH 10/11] Update CustomRulesResources.resx --- src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx b/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx index 53ab91d..91ecd67 100644 --- a/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx +++ b/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx @@ -261,6 +261,6 @@ One exception is logging, where it can be useful to see the exact DC / cluster / You are using either an interpolated string or string concatenation in your logs, change these to the message template format to preserve structure in your logs - QuerySelectorAsync makes things more flaky. Use locator-based "IPage.Locator" instead, as Playwright supports that better + QuerySelectorAsync usage found Replace with Locator or GetByRole for more reliable tests - \ No newline at end of file + From 5b6b80b0360fc1ff48066a4c476435b47dcaacee Mon Sep 17 00:00:00 2001 From: Rannes Date: Thu, 17 Oct 2024 16:17:23 +0700 Subject: [PATCH 11/11] fix: AnalyzerRelease warning --- src/Agoda.Analyzers/AnalyzerReleases.Unshipped.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Agoda.Analyzers/AnalyzerReleases.Unshipped.md b/src/Agoda.Analyzers/AnalyzerReleases.Unshipped.md index 81f6295..c59537d 100644 --- a/src/Agoda.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Agoda.Analyzers/AnalyzerReleases.Unshipped.md @@ -22,6 +22,9 @@ AG0030 | Agoda.CSharp.CustomQualityRules | Warning | AG0030PreventUseOfDynamics, AG0037 | Agoda.CSharp.CustomQualityRules | Error | AG0037EnsureSeleniumTestHasOwnedByAttribute AG0038 | Agoda.CSharp.CustomQualityRules | Warning | AG0038PreventUseOfRegionPreprocessorDirective, [Documentation](https://agoda-com.github.io/standards-c-sharp/code-style/regions.html) AG0039 | Documentation | Hidden | AG0039MethodLineLengthAnalyzer, [Documentation](https://github.com/agoda-com/AgodaAnalyzers/blob/master/src/Agoda.Analyzers/RuleContent/AG0039MethodLineLengthAnalyzer.html) +AG0041 | Best Practices | Warning | AG0041LogTemplateAnalyzer, [Documentation](https://github.com/agoda-com/AgodaAnalyzers/blob/master/doc/AG0041.md) +AG0042 | Agoda.CSharp.CustomQualityRules | Warning | AG0042QuerySelectorShouldNotBeUsed, [Documentation](https://github.com/agoda-com/AgodaAnalyzers/blob/master/doc/AG0042.md) +AG0043 | Agoda.CSharp.CustomQualityRules | Error | AG0043NoBuildServiceProvider, [Documentation](https://github.com/agoda-com/AgodaAnalyzers/blob/master/doc/AG0043.md) SA1106 | StyleCop.CSharp.ReadabilityRules | Warning | SA1106CodeMustNotContainEmptyStatements SA1107 | StyleCop.CSharp.ReadabilityRules | Warning | SA1107CodeMustNotContainMultipleStatementsOnOneLine SA1123 | StyleCop.CSharp.ReadabilityRules | Warning | SA1123DoNotPlaceRegionsWithinElements \ No newline at end of file