diff --git a/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseSafeAccessRuleTests.cs b/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseSafeAccessRuleTests.cs new file mode 100644 index 00000000000..f78701da447 --- /dev/null +++ b/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseSafeAccessRuleTests.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Bicep.Core.Analyzers.Linter.Rules; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Bicep.Core.UnitTests.Diagnostics.LinterRuleTests; + +[TestClass] +public class UseSafeAccessRuleTests : LinterRuleTestsBase +{ + private void AssertCodeFix(string inputFile, string resultFile) + => AssertCodeFix(UseSafeAccessRule.Code, "Use the safe access (.?) operator", inputFile, resultFile); + + private void AssertNoDiagnostics(string inputFile) + => AssertLinterRuleDiagnostics(UseSafeAccessRule.Code, inputFile, [], new(OnCompileErrors.Ignore, IncludePosition.None)); + + [TestMethod] + public void Codefix_fixes_syntax_which_can_be_simplified() => AssertCodeFix(""" +param foo object +var test = contai|ns(foo, 'bar') ? foo.bar : 'baz' +""", """ +param foo object +var test = foo.?bar ?? 'baz' +"""); + + [TestMethod] + public void Rule_ignores_syntax_which_cannot_be_simplified() => AssertNoDiagnostics(""" +param foo object +var test = contains(foo, 'bar') ? foo.baz : 'baz' +"""); + + [TestMethod] + public void Rule_ignores_syntax_which_cannot_be_simplified_2() => AssertNoDiagnostics(""" +param foo object +var test = contains(foo, 'bar') ? bar.bar : 'baz' +"""); +} diff --git a/src/Bicep.Core/Analyzers/Linter/Rules/UseSafeAccessRule.cs b/src/Bicep.Core/Analyzers/Linter/Rules/UseSafeAccessRule.cs new file mode 100644 index 00000000000..7b6c1790dda --- /dev/null +++ b/src/Bicep.Core/Analyzers/Linter/Rules/UseSafeAccessRule.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Bicep.Core.CodeAction; +using Bicep.Core.Diagnostics; +using Bicep.Core.Parsing; +using Bicep.Core.Semantics; +using Bicep.Core.Semantics.Namespaces; +using Bicep.Core.Syntax; +using Bicep.Core.Syntax.Comparers; +using Bicep.Core.Syntax.Visitors; + +namespace Bicep.Core.Analyzers.Linter.Rules; + +public sealed class UseSafeAccessRule : LinterRuleBase +{ + public new const string Code = "use-safe-access"; + + public UseSafeAccessRule() : base( + code: Code, + description: CoreResources.UseSafeAccessRule_Description, + LinterRuleCategory.BestPractice, + docUri: new Uri($"https://aka.ms/bicep/linter/{Code}")) + { } + + public override IEnumerable AnalyzeInternal(SemanticModel model, DiagnosticLevel diagnosticLevel) + { + foreach (var ternary in SyntaxAggregator.AggregateByType(model.Root.Syntax)) + { + if (SemanticModelHelper.TryGetNamedFunction(model, SystemNamespaceType.BuiltInName, "contains", ternary.ConditionExpression) is not {} functionCall || + functionCall.Arguments.Length != 2 || + functionCall.Arguments[1].Expression is not StringSyntax containsString || + containsString.TryGetLiteralValue() is not {} propertyName) + { + continue; + } + + if (ternary.TrueExpression is not PropertyAccessSyntax truePropertyAccess || + !truePropertyAccess.PropertyName.NameEquals(propertyName)) + { + continue; + } + + if (!SyntaxIgnoringTriviaComparer.Instance.Equals(functionCall.Arguments[0].Expression, truePropertyAccess.BaseExpression)) + { + continue; + } + + var replacement = SyntaxFactory.CreateBinaryOperationSyntax( + SyntaxFactory.CreateSafePropertyAccess(truePropertyAccess.BaseExpression, propertyName), + TokenType.DoubleQuestion, + ternary.FalseExpression); + + yield return CreateFixableDiagnosticForSpan( + diagnosticLevel, + ternary.Span, + new CodeFix( + CoreResources.UseSafeAccessRule_CodeFix, + isPreferred: true, + CodeFixKind.QuickFix, + new CodeReplacement(ternary.Span, replacement.ToString())), + CoreResources.UseSafeAccessRule_MessageFormat); + } + } +} \ No newline at end of file diff --git a/src/Bicep.Core/CoreResources.Designer.cs b/src/Bicep.Core/CoreResources.Designer.cs index 3ae6d009ad2..be6814707b9 100644 --- a/src/Bicep.Core/CoreResources.Designer.cs +++ b/src/Bicep.Core/CoreResources.Designer.cs @@ -1058,5 +1058,23 @@ internal static string UseStableVMImageRuleFixMessageFormat { return ResourceManager.GetString("UseStableVMImageRuleFixMessageFormat", resourceCulture); } } + + internal static string UseSafeAccessRule_Description { + get { + return ResourceManager.GetString("UseSafeAccessRule_Description", resourceCulture); + } + } + + internal static string UseSafeAccessRule_MessageFormat { + get { + return ResourceManager.GetString("UseSafeAccessRule_MessageFormat", resourceCulture); + } + } + + internal static string UseSafeAccessRule_CodeFix { + get { + return ResourceManager.GetString("UseSafeAccessRule_CodeFix", resourceCulture); + } + } } } diff --git a/src/Bicep.Core/CoreResources.resx b/src/Bicep.Core/CoreResources.resx index 9b617ad6a97..3a19b6c9979 100644 --- a/src/Bicep.Core/CoreResources.resx +++ b/src/Bicep.Core/CoreResources.resx @@ -495,4 +495,13 @@ Resource-derived types + + Use the safe access (.?) operator instead of checking object contents with the 'contains' function. + + + Use the safe access (.?) operator + + + The syntax can be simplified by using the safe access (.?) operator. + \ No newline at end of file diff --git a/src/Bicep.Core/Semantics/SemanticModelHelper.cs b/src/Bicep.Core/Semantics/SemanticModelHelper.cs index edb3f530584..491850558bf 100644 --- a/src/Bicep.Core/Semantics/SemanticModelHelper.cs +++ b/src/Bicep.Core/Semantics/SemanticModelHelper.cs @@ -21,6 +21,18 @@ public static IEnumerable GetFunctionsByName(SemanticMod .Where(s => SemanticModelHelper.TryGetFunctionInNamespace(model, @namespace, s) is { }); } + public static FunctionCallSyntaxBase? TryGetNamedFunction(SemanticModel model, string @namespace, string functionName, SyntaxBase syntax) + { + if (syntax is FunctionCallSyntaxBase functionCall && + functionCall.NameEquals(functionName) && + SemanticModelHelper.TryGetFunctionInNamespace(model, @namespace, functionCall) is { }) + { + return functionCall; + } + + return null; + } + public static FunctionCallSyntaxBase? TryGetFunctionInNamespace(SemanticModel semanticModel, string @namespace, SyntaxBase syntax) { if (semanticModel.GetSymbolInfo(syntax) is FunctionSymbol function && diff --git a/src/Bicep.Core/Syntax/SyntaxFactory.cs b/src/Bicep.Core/Syntax/SyntaxFactory.cs index cc176a705fe..c90d3de71e9 100644 --- a/src/Bicep.Core/Syntax/SyntaxFactory.cs +++ b/src/Bicep.Core/Syntax/SyntaxFactory.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Collections.Immutable; +using System.ComponentModel; using Bicep.Core.Extensions; using Bicep.Core.Parsing; @@ -64,6 +65,7 @@ public static Token GetCommaToken(IEnumerable? leadingTrivia = nul => CreateToken(TokenType.Comma, leadingTrivia, trailingTrivia); public static Token DotToken => CreateToken(TokenType.Dot); public static Token QuestionToken => CreateToken(TokenType.Question); + public static Token DoubleQuestionToken => CreateToken(TokenType.DoubleQuestion); public static Token ColonToken => CreateToken(TokenType.Colon); public static Token SemicolonToken => CreateToken(TokenType.Semicolon); public static Token AssignmentToken => CreateToken(TokenType.Assignment, EmptyTrivia, SingleSpaceTrivia); @@ -358,11 +360,20 @@ public static LambdaSyntax CreateLambdaSyntax(IReadOnlyList parameterNam public static PropertyAccessSyntax CreatePropertyAccess(SyntaxBase @base, string propertyName) => new(@base, DotToken, null, CreateIdentifier(propertyName)); + public static PropertyAccessSyntax CreateSafePropertyAccess(SyntaxBase @base, string propertyName) + => new(@base, DotToken, QuestionToken, CreateIdentifier(propertyName)); + public static ParameterAssignmentSyntax CreateParameterAssignmentSyntax(string name, SyntaxBase value) => new( ParameterKeywordToken, CreateIdentifierWithTrailingSpace(name), AssignmentToken, value); + + public static BinaryOperationSyntax CreateBinaryOperationSyntax(SyntaxBase left, TokenType operatorType, SyntaxBase right) + => new( + left, + CreateToken(operatorType, SingleSpaceTrivia, SingleSpaceTrivia), + right); } } diff --git a/src/vscode-bicep/schemas/bicepconfig.schema.json b/src/vscode-bicep/schemas/bicepconfig.schema.json index 23786847cce..348036040e2 100644 --- a/src/vscode-bicep/schemas/bicepconfig.schema.json +++ b/src/vscode-bicep/schemas/bicepconfig.schema.json @@ -700,6 +700,16 @@ } ] }, + "use-safe-access": { + "allOf": [ + { + "description": "Use the safe access (.?) operator instead of checking object contents with the 'contains' function. Defaults to 'Warning'. See https://aka.ms/bicep/linter/use-parent-property" + }, + { + "$ref": "#/definitions/rule-def-level-warning" + } + ] + }, "use-recent-api-versions": { "allOf": [ {