Created
February 4, 2024 05:48
-
-
Save Msirkovsky/86e08b441da7add13c50075113767c6e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Linq; | |
using Microsoft.CodeAnalysis; | |
using Microsoft.CodeAnalysis.CSharp.Syntax; | |
namespace SourceGeneratorsExample; | |
[Generator(LanguageNames.CSharp)] | |
public class SourceGeneratorWithInterceptors : IIncrementalGenerator | |
{ | |
public void Initialize(IncrementalGeneratorInitializationContext context) | |
{ | |
var providerInvocationExp = context.SyntaxProvider.CreateSyntaxProvider( | |
(n, _) => n is MemberAccessExpressionSyntax, // we are looking only for the member access expressions | |
(n, cp) => | |
{ | |
var node = (MemberAccessExpressionSyntax)n.Node; | |
// we need semantic model to get info about type of expression and attributes | |
var semanticModel = n.SemanticModel; | |
// get location of expression, we need to for InterceptsLocation attribute. | |
var location = node.Name.GetLocation(); | |
var lineSpan = location.GetLineSpan(); | |
//+1 since location uses the zero based line/character numbering | |
var startLine = lineSpan.StartLinePosition.Line + 1; | |
var startColumn = lineSpan.StartLinePosition.Character + 1; | |
// check the existence of attribute via symbols. | |
var symbol = semanticModel.GetSymbolInfo(node, cp).Symbol; | |
var hasAttribute = symbol?.GetAttributes().Any(attribute => | |
attribute.AttributeClass?.ToDisplayString() == "SourceGeneratorsExample.Sample.InterceptAttribute"); | |
if (hasAttribute != true) // no attribute, no interception | |
return null; | |
// get the class name and namespace via typeInfo | |
var typeInfo = semanticModel.GetTypeInfo(node.Expression); | |
var className = ""; | |
var namespaceName = ""; | |
if (typeInfo.Type is INamedTypeSymbol namedTypeSymbol) | |
{ | |
className = namedTypeSymbol.Name; | |
namespaceName = namedTypeSymbol.ContainingNamespace.ToDisplayString(); | |
} | |
var filePath = location.SourceTree?.FilePath; | |
// Info is arbitrary "DTO" with all information you need in the next step. | |
return new MethodInfoToIntercept(className, namespaceName, node.Name.ToFullString(), startLine, startColumn, filePath); | |
}); | |
var compilationWithProvider = context.CompilationProvider.Combine(providerInvocationExp.Collect()); | |
context.RegisterSourceOutput(compilationWithProvider, (ctx, t) => | |
{ | |
var foundedMethods = t.Right; | |
// filter out methods without an attribute | |
var extensions = foundedMethods | |
.Where(method => method != null) | |
.Select(method => | |
{ | |
// create InterceptsLocation & InterceptorMethod using information from MethodInfoToIntercept. | |
var str = $@" | |
[System.Runtime.CompilerServices.InterceptsLocation(@""{method.FilePath}"", line: {method.Line}, character: {method.Column})] | |
public static void InterceptorMethod(this {method.ClassNameWithNamespace} obj, string email, string password) | |
{{ | |
Console.WriteLine($""interceptor - BEFORE""); | |
obj.SignUp(email, password); | |
Console.WriteLine($""interceptor - AFTER""); | |
return; | |
}}"; | |
return str; | |
}); | |
var extensionCode = string.Join("\r\n", extensions); | |
// add InterceptsLocationAttribute and put all generated "interceptors" to the InterceptionExtensions class | |
var source = $@" | |
// <auto-generated/> | |
using System; | |
using System.Runtime.CompilerServices; | |
namespace System.Runtime.CompilerServices | |
{{ | |
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] | |
sealed class InterceptsLocationAttribute(string filePath, int line, int character) : Attribute | |
{{ | |
}} | |
static class InterceptionExtensions | |
{{ | |
{extensionCode} | |
}} | |
}}"; | |
ctx.AddSource("SampleIntercepting.g.cs", source); | |
}); | |
} | |
} | |
internal class MethodInfoToIntercept | |
{ | |
public string ClassName { get; } | |
public string ClassNameWithNamespace => $"{NamespaceName}.{ClassName}"; | |
public string NamespaceName { get; } | |
public string MethodName { get; } | |
public int Line { get; } | |
public int Column { get; } | |
public string? FilePath { get; } | |
public MethodInfoToIntercept(string className, string namespaceName, string methodName, int line, int column, string? filePath) | |
{ | |
ClassName = className; | |
NamespaceName = namespaceName; | |
MethodName = methodName; | |
Line = line; | |
Column = column; | |
FilePath = filePath; | |
} | |
} | |
//user.cs | |
public class User | |
{ | |
public static void Test() | |
{ | |
var u = new User(); | |
u.SignUp("email", "pass"); | |
} | |
[Intercept] | |
public void SignUp(string email, string password) | |
{ | |
var newUser = CreateUser(email, password); | |
} | |
public User CreateUser(string email, string password) | |
{ | |
Console.WriteLine($"email:{email} - password:{password}"); | |
return new User { }; | |
} | |
} | |
public class InterceptAttribute : Attribute | |
{ | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment