Skip to content

Commit

Permalink
Add use-safe-access linter rule
Browse files Browse the repository at this point in the history
  • Loading branch information
anthony-c-martin committed May 11, 2024
1 parent be0b3b7 commit 2431efa
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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'
""");
}
65 changes: 65 additions & 0 deletions src/Bicep.Core/Analyzers/Linter/Rules/UseSafeAccessRule.cs
Original file line number Diff line number Diff line change
@@ -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<IDiagnostic> AnalyzeInternal(SemanticModel model, DiagnosticLevel diagnosticLevel)
{
foreach (var ternary in SyntaxAggregator.AggregateByType<TernaryOperationSyntax>(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);
}
}
}
18 changes: 18 additions & 0 deletions src/Bicep.Core/CoreResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Bicep.Core/CoreResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -495,4 +495,13 @@
<data name="ExperimentalFeatureNames_ResourceDerivedTypes" xml:space="preserve">
<value>Resource-derived types</value>
</data>
<data name="UseSafeAccessRule_Description" xml:space="preserve">
<value>Use the safe access (.?) operator instead of checking object contents with the 'contains' function.</value>
</data>
<data name="UseSafeAccessRule_CodeFix" xml:space="preserve">
<value>Use the safe access (.?) operator</value>
</data>
<data name="UseSafeAccessRule_MessageFormat" xml:space="preserve">
<value>The syntax can be simplified by using the safe access (.?) operator.</value>
</data>
</root>
12 changes: 12 additions & 0 deletions src/Bicep.Core/Semantics/SemanticModelHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ public static IEnumerable<FunctionCallSyntaxBase> 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 &&
Expand Down
11 changes: 11 additions & 0 deletions src/Bicep.Core/Syntax/SyntaxFactory.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -64,6 +65,7 @@ public static Token GetCommaToken(IEnumerable<SyntaxTrivia>? 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);
Expand Down Expand Up @@ -358,11 +360,20 @@ public static LambdaSyntax CreateLambdaSyntax(IReadOnlyList<string> 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);
}
}
10 changes: 10 additions & 0 deletions src/vscode-bicep/schemas/bicepconfig.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down

0 comments on commit 2431efa

Please sign in to comment.