Skip to content

Instantly share code, notes, and snippets.

@msirkovsky-moodys
Created February 11, 2024 06:32
Show Gist options
  • Save msirkovsky-moodys/9aefcbab27ee2a947bc73be03d8e687b to your computer and use it in GitHub Desktop.
Save msirkovsky-moodys/9aefcbab27ee2a947bc73be03d8e687b to your computer and use it in GitHub Desktop.
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();
//add 1 since location uses the zero based line/character numbering
var startLine = lineSpan.StartLinePosition.Line + 1;
var startColumn = lineSpan.StartLinePosition.Character + 1;
// check 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 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($""User is being created"");
obj.SignUp(email, password);
Console.WriteLine($""User has been created"");
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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment