diff --git a/example/source/ApiOperationPolicy.cs b/example/source/ApiOperationPolicy.cs index 428d461..d4f0b54 100644 --- a/example/source/ApiOperationPolicy.cs +++ b/example/source/ApiOperationPolicy.cs @@ -19,7 +19,7 @@ public void Inbound(IInboundContext context) { context.AuthenticationManagedIdentity(new ManagedIdentityAuthenticationConfig() { - Resource = "https://management.azure.com/", + Resource = Constants.AzureManagementUrl, }); } } diff --git a/example/source/Constants.cs b/example/source/Constants.cs new file mode 100644 index 0000000..ab8a73d --- /dev/null +++ b/example/source/Constants.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Contoso.Apis; + +public static class Constants +{ + public const string AzureManagementUrl = "https://management.azure.com/"; +} \ No newline at end of file diff --git a/src/Core/Compiling/CompilerUtils.cs b/src/Core/Compiling/CompilerUtils.cs index 41108b9..3f09b5e 100644 --- a/src/Core/Compiling/CompilerUtils.cs +++ b/src/Core/Compiling/CompilerUtils.cs @@ -4,9 +4,9 @@ using System.Diagnostics.CodeAnalysis; using System.Xml.Linq; -using Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring; using Microsoft.Azure.ApiManagement.PolicyToolkit.Compiling.Diagnostics; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Microsoft.Azure.ApiManagement.PolicyToolkit.Compiling; @@ -21,6 +21,8 @@ public static string ProcessParameter(this ExpressionSyntax expression, IDocumen return syntax.Token.ValueText; case InvocationExpressionSyntax syntax: return FindCode(syntax, context); + case MemberAccessExpressionSyntax syntax: + return FindCode(syntax, context); // case InterpolatedStringExpressionSyntax syntax: // var interpolationParts = syntax.Contents.Select(c => c switch // { @@ -46,7 +48,12 @@ public static string ProcessParameter(this ExpressionSyntax expression, IDocumen public static string FindCode(this InvocationExpressionSyntax syntax, IDocumentCompilationContext context) { - if (syntax.Expression is not IdentifierNameSyntax identifierSyntax) + Compilation compilation = context.Compilation; + SemanticModel semanticModel = compilation.GetSemanticModel(syntax.SyntaxTree); + var symbolInfo = semanticModel.GetSymbolInfo(syntax.Expression); + var symbol = symbolInfo.Symbol ?? symbolInfo.CandidateSymbols.SingleOrDefault(s => s is IMethodSymbol); + + if (symbol is not IMethodSymbol methodSymbol) { context.Report(Diagnostic.Create( CompilationErrors.InvalidExpression, @@ -55,10 +62,20 @@ public static string FindCode(this InvocationExpressionSyntax syntax, IDocumentC return ""; } - var methodIdentifier = identifierSyntax.Identifier.ValueText; - var expressionMethod = context.SyntaxRoot.DescendantNodes() + var expressionMethod = methodSymbol.DeclaringSyntaxReferences + .Select(r => r.GetSyntax()) .OfType() - .First(m => m.Identifier.ValueText == methodIdentifier); + .FirstOrDefault(); + + if (expressionMethod is null) + { + context.Report(Diagnostic.Create( + CompilationErrors.CannotFindMethodCode, + syntax.GetLocation(), + methodSymbol.Name + )); + return ""; + } expressionMethod = Normalize(expressionMethod); @@ -76,6 +93,45 @@ public static string FindCode(this InvocationExpressionSyntax syntax, IDocumentC } } + public static string FindCode(this MemberAccessExpressionSyntax syntax, IDocumentCompilationContext context) + { + Compilation compilation = context.Compilation; + SemanticModel semanticModel = compilation.GetSemanticModel(syntax.SyntaxTree); + var symbolInfo = semanticModel.GetSymbolInfo(syntax); + var symbol = symbolInfo.Symbol ?? symbolInfo.CandidateSymbols.SingleOrDefault(s => s is IFieldSymbol); + + if (symbol is not IFieldSymbol fieldSymbol) + { + context.Report(Diagnostic.Create( + CompilationErrors.InvalidConstantReference, + syntax.GetLocation() + )); + return ""; + } + + if (!fieldSymbol.IsConst) + { + context.Report(Diagnostic.Create( + CompilationErrors.InvalidExpression, + syntax.GetLocation() + )); + return ""; + } + + var value = fieldSymbol.ConstantValue?.ToString(); + if (value is null) + { + context.Report(Diagnostic.Create( + CompilationErrors.IsNotAConstant, + syntax.GetLocation(), + fieldSymbol.Name + )); + value = ""; + } + + return value; + } + public static InitializerValue Process( this ObjectCreationExpressionSyntax creationSyntax, IDocumentCompilationContext context) diff --git a/src/Core/Compiling/Diagnostics/CompilationErrors.cs b/src/Core/Compiling/Diagnostics/CompilationErrors.cs index 92a66a4..4bdd809 100644 --- a/src/Core/Compiling/Diagnostics/CompilationErrors.cs +++ b/src/Core/Compiling/Diagnostics/CompilationErrors.cs @@ -215,4 +215,37 @@ public static class CompilationErrors "Description.", "TODO", ["APIM", "ApiManagement"]); + + public readonly static DiagnosticDescriptor CannotFindMethodCode = new( + "APIM2009", + "Cannot find method code", + "Cannot find method code. Method '{0}' is not declared in the projects source code.", + "PolicyDocumentCompilation", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Description.", + helpLinkUri: "TODO", + customTags: ["APIM", "ApiManagement"]); + + public readonly static DiagnosticDescriptor InvalidConstantReference = new( + "APIM2010", + "Invalid constant reference for policy parameter", + "Argument should be an constant field reference", + "PolicyDocumentCompilation", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Description.", + helpLinkUri: "TODO", + customTags: ["APIM", "ApiManagement"]); + + public readonly static DiagnosticDescriptor IsNotAConstant = new( + "APIM2011", + "External value is not a constant", + "Field '{0}' should be a const for it to be inlined in the policy document", + "PolicyDocumentCompilation", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Description.", + helpLinkUri: "TODO", + customTags: ["APIM", "ApiManagement"]); } \ No newline at end of file diff --git a/test/Test.Core/CompilerTestInitialize.cs b/test/Test.Core/CompilerTestInitialize.cs index 973741b..527b086 100644 --- a/test/Test.Core/CompilerTestInitialize.cs +++ b/test/Test.Core/CompilerTestInitialize.cs @@ -42,24 +42,29 @@ public static void CompilerCleanup() s_serviceProvider.Dispose(); } - public static IDocumentCompilationResult CompileDocument(this string document) + public static IDocumentCompilationResult CompileDocument(this string document) => document.CompileDocument([]); + + public static IDocumentCompilationResult CompileDocument(this string document, params string[] separateDocuments) { - var doc = $""" - using Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring; - using Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring.Expressions; + string[] docs = [document, ..separateDocuments]; + var syntaxTrees = docs.Select(d => + $""" + using Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring; + using Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring.Expressions; - namespace Test; + namespace Test; - {document} - """; + {d} + """) + .Select(d => CSharpSyntaxTree.ParseText(d)) + .ToArray(); - SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(doc); var compilation = CSharpCompilation.Create( Guid.NewGuid().ToString(), - syntaxTrees: [syntaxTree], + syntaxTrees: syntaxTrees, references: References); - var semantics = compilation.GetSemanticModel(syntaxTree); - ClassDeclarationSyntax policy = syntaxTree + var semantics = compilation.GetSemanticModel(syntaxTrees[0]); + ClassDeclarationSyntax policy = syntaxTrees[0] .GetRoot() .DescendantNodes() .OfType() diff --git a/test/Test.Core/ReferencingTests.cs b/test/Test.Core/ReferencingTests.cs new file mode 100644 index 0000000..2d412ab --- /dev/null +++ b/test/Test.Core/ReferencingTests.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Test.Core; + +[TestClass] +public class ReferencingTests +{ + [TestMethod] + [DataRow( + """ + [Document] + public class PolicyDocument : IDocument + { + public void Inbound(IInboundContext context) + { + context.AuthenticationBasic("{{username}}", Expressions.Password(context.ExpressionContext)); + } + } + """, + """ + public static class Expressions + { + public static string Password(IExpressionContext context) => context.Subscription.Key; + } + """, + """ + + + + + + """, + DisplayName = "Should reference external expression code in policy document" + )] + [DataRow( + """ + [Document] + public class PolicyDocument : IDocument + { + public void Inbound(IInboundContext context) + { + context.AuthenticationBasic(Constants.Username, "{{password}}"); + } + } + """, + """ + public static class Constants + { + public const string Username = "{{username}}"; + } + """, + """ + + + + + + """, + DisplayName = "Should reference external constant in policy document" + )] + public void ShouldReference(string document, string externalCode, string expectedXml) + { + document.CompileDocument(externalCode).Should().BeSuccessful().And.DocumentEquivalentTo(expectedXml); + } +} \ No newline at end of file