Skip to content
This repository has been archived by the owner on Jun 30, 2023. It is now read-only.

Commit

Permalink
Externalize default inline not implemented behavior
Browse files Browse the repository at this point in the history
Instead of generating different invocation patterns depending on
whether there is an abstract/interface member implementation or
a virtual one, and throwing (inline) a NotImplementedException,
we instead enable avatars to have a target implementation (decorator-
style) that is invoked instead, which by default just happens to
throw NotImplementedException unless an alternative (now decorated)
implementation is provided.

This simplifies the generator as well as the target call stacks
which will always be consistent across abstract and interface-based
members, and also adds support for default interface implementations
since the default implementation would in that case not be generated
in the default target implementation.

Fixes #113.
  • Loading branch information
kzu committed Apr 6, 2021
1 parent 10ffe9e commit 545391a
Show file tree
Hide file tree
Showing 16 changed files with 1,201 additions and 64 deletions.
17 changes: 17 additions & 0 deletions src/Avatar.CodeAnalysis/IsExternalInit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.ComponentModel;

namespace System.Runtime.CompilerServices
{
/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
sealed class IsExternalInit
{
}
}
6 changes: 3 additions & 3 deletions src/Avatar.CodeAnalysis/NamingConvention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ namespace Avatars.CodeAnalysis
/// <summary>
/// Naming conventions used for analyzers, code fixes and code generation.
/// </summary>
public class NamingConvention
public record NamingConvention
{
/// <summary>
/// The root or base namespace of the generated code.
/// </summary>
public virtual string RootNamespace => AvatarNaming.DefaultRootNamespace;
public string RootNamespace { get; init; } = AvatarNaming.DefaultRootNamespace;

/// <summary>
/// Suffix appended to the type name, i.e. <c>IFooAvatar</c>.
/// </summary>
public virtual string NameSuffix => AvatarNaming.DefaultSuffix;
public string NameSuffix { get; init; } = AvatarNaming.DefaultSuffix;

/// <summary>
/// The type name to generate for the given (optional) base type and implemented interfaces.
Expand Down
192 changes: 167 additions & 25 deletions src/Avatar.StaticProxy/AvatarGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using Avatars.CodeActions;
using Avatars.CodeAnalysis;
using Avatars.Processors;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using static Avatars.SyntaxFactoryGenerator;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace Avatars
{
Expand Down Expand Up @@ -178,6 +181,9 @@ void OnExecute(ProcessorContext context, NamingConvention naming)

var driver = new SyntaxProcessorDriver(processors);
var factory = AvatarSyntaxFactory.CreateFactory(context.Language);
// NOTE: Default scaffolding we provide is based on Roslyn code actions
var defaultScaffold = new AvatarScaffold(context);
var notImplNaming = naming with { NameSuffix = "NotImplemented" };
var avatars = new HashSet<string>();

foreach (var (source, candidate) in context.SyntaxReceivers
Expand All @@ -193,6 +199,17 @@ void OnExecute(ProcessorContext context, NamingConvention naming)
if (syntax.IsEquivalentTo(updated))
continue;

// Scaffold the default (not implemented) implementation
var notImplName = notImplNaming.GetName(candidate);
var notImplDoc = defaultScaffold.ScaffoldAsync(
factory.CreateSyntax(notImplNaming, candidate),
notImplNaming,
new[] { CodeFixes.CSharp.ImplementAbstractClass, CodeFixes.CSharp.ImplementInterface }).Result;

var notImplSyntax = notImplDoc.GetSyntaxRootAsync(context.CancellationToken).Result!;
if (context.Language == LanguageNames.CSharp)
notImplSyntax = new CSharpSingletonRewriter().Visit(notImplSyntax);

// At this point, we should have a type that has at least one public constructor
if (!updated.DescendantNodes().OfType<ConstructorDeclarationSyntax>().Any())
{
Expand All @@ -204,24 +221,32 @@ void OnExecute(ProcessorContext context, NamingConvention naming)
continue;
}

var code = updated.NormalizeWhitespace().ToFullString();
var avatarCode = updated.NormalizeWhitespace().ToFullString();
// AvatarScaffold already formats precisely what it generates, no need to
// drop all formatting again by applying the NormalizeWhitespace.
var notImplCode = notImplSyntax.ToFullString();
#if DEBUG
var shouldEmit = Debugger.IsAttached;
#else
var shouldEmit = false;
#endif

// Additional pretty-printing when emitting generated files, improves whitespace handling for C#
if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.EmitCompilerGeneratedFiles", out var emitSources) &&
shouldEmit = shouldEmit ||
(context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.EmitCompilerGeneratedFiles", out var emitSources) &&
bool.TryParse(emitSources, out shouldEmit) &&
shouldEmit &&
// NOTE: checking for C# last, since the Debugger.Attached section below would depend on
// the proper initialization of shouldEmit too, regardless of language
context.Language == LanguageNames.CSharp)
shouldEmit);

// Additional pretty-printing when emitting generated files, improves whitespace handling for C#
if (shouldEmit && context.Language == LanguageNames.CSharp)
{
updated = CSharpSyntaxTree.ParseText(code, (CSharpParseOptions)context.ParseOptions).GetRoot();
updated = new CSharpFormatter().Visit(updated);
code = updated.GetText().ToString();
var pretty = new CSharpPrettyRewriter().Visit(
CSharpSyntaxTree.ParseText(avatarCode, (CSharpParseOptions)context.ParseOptions).GetRoot());
avatarCode = pretty.GetText().ToString();
}

avatars.Add(name);
context.AddSource(name, SourceText.From(code, Encoding.UTF8));
context.AddSource(name, SourceText.From(avatarCode, Encoding.UTF8));
context.AddSource(notImplName, SourceText.From(notImplCode, Encoding.UTF8));

#if DEBUG
if (Debugger.IsAttached)
Expand All @@ -234,28 +259,39 @@ void OnExecute(ProcessorContext context, NamingConvention naming)
Directory.CreateDirectory(targetDir);

var filePath = Path.Combine(targetDir, name + (context.Language == LanguageNames.CSharp ? ".cs" : ".vb"));
File.WriteAllText(filePath, code);
File.WriteAllText(filePath, avatarCode);
Debugger.Log(0, "", "Avatar Generated: " + filePath + Environment.NewLine);

filePath = Path.Combine(targetDir, notImplName + (context.Language == LanguageNames.CSharp ? ".cs" : ".vb"));
File.WriteAllText(filePath, notImplCode);
Debugger.Log(0, "", "Avatar NotImplemented Generated: " + filePath + Environment.NewLine);
}

Debugger.Log(0, "", string.Join(
Environment.NewLine,
code.Split(new[] { Environment.NewLine }, StringSplitOptions.None)
avatarCode.Split(new[] { Environment.NewLine }, StringSplitOptions.None)
.Select((line, index) => index.ToString().PadLeft(3) + " " + line)) + Environment.NewLine);

Debugger.Log(0, "", string.Join(
Environment.NewLine,
notImplCode.Split(new[] { Environment.NewLine }, StringSplitOptions.None)
.Select((line, index) => index.ToString().PadLeft(3) + " " + line)) + Environment.NewLine);
}
#endif
}
}

class CSharpFormatter : CSharpSyntaxRewriter
// Pretty-printer that adds missing newlines/whitespaces here and there.
class CSharpPrettyRewriter : CSharpSyntaxRewriter
{
static SyntaxTrivia NewLine => SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, "\n");
static SyntaxTrivia Tab => SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, " ");
static SyntaxTrivia NewLine => SyntaxTrivia(SyntaxKind.WhitespaceTrivia, "\n");
static SyntaxTrivia Tab => Whitespace(" ");

public override SyntaxNode? VisitConstructorDeclaration(ConstructorDeclarationSyntax node)
// Ctor is always replaced, so it needs whitespace
=> base.VisitConstructorDeclaration(node)!
.WithTrailingTrivia(node.GetTrailingTrivia().Add(NewLine));
.WithTrailingTrivia(node.GetTrailingTrivia()
.Add(CarriageReturnLineFeed));

public override SyntaxNode? VisitPropertyDeclaration(PropertyDeclarationSyntax node)
{
Expand All @@ -267,29 +303,31 @@ class CSharpFormatter : CSharpSyntaxRewriter
return base.VisitPropertyDeclaration(node);

return base.VisitPropertyDeclaration(node)!
.WithTrailingTrivia(node.GetTrailingTrivia().Add(NewLine));
.WithTrailingTrivia(node.GetTrailingTrivia()
.Add(CarriageReturnLineFeed));
}

public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node)
{
// ref/out already get proper whitespace from scaffold
if (!node.ParameterList.Parameters.Any(x => x.IsRefOut()))
return base.VisitMethodDeclaration(node)!
.WithTrailingTrivia(node.GetTrailingTrivia().Add(NewLine));
if (node.ExpressionBody != null)
return base.VisitMethodDeclaration(node
.WithTrailingTrivia(node.GetTrailingTrivia()
.Add(CarriageReturnLineFeed)));

return base.VisitMethodDeclaration(node);
}

public override SyntaxNode? VisitFieldDeclaration(FieldDeclarationSyntax node)
=> base.VisitFieldDeclaration(node)!
.WithTrailingTrivia(node.GetTrailingTrivia().Add(NewLine));
.WithTrailingTrivia(node.GetTrailingTrivia()
.Add(CarriageReturnLineFeed));

SyntaxTriviaList? indent;

public override SyntaxNode? VisitObjectCreationExpression(ObjectCreationExpressionSyntax node)
{
// Save current indentation for subsequent lines in the object initializer syntax.
indent = node.Initializer?.GetLeadingTrivia();

return base.VisitObjectCreationExpression(node);
}

Expand All @@ -302,14 +340,118 @@ class CSharpFormatter : CSharpSyntaxRewriter
{
var last = node.Expressions.Count - 1;
return base.VisitInitializerExpression(node
.WithExpressions(SyntaxFactory.SeparatedList(
.WithExpressions(SeparatedList(
node.Expressions.Select((e, i) => i != last ? e : e.WithTrailingTrivia(indent?.Insert(0, NewLine))))));
}

return base.VisitInitializerExpression(node);
}
}

/// <summary>
/// Adds a private constructor (to prevent instance creation from outside), plus a static Instance
/// property and backing field.
/// </summary>
class CSharpSingletonRewriter : CSharpSyntaxRewriter
{
public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node)
{
var firstChild = node.ChildNodes().OfType<MemberDeclarationSyntax>().FirstOrDefault();
var ctors = node.ChildNodes().OfType<ConstructorDeclarationSyntax>().ToArray();

foreach (var ctor in ctors)
{
MemberDeclarationSyntax? singleton;
if (ctor.ParameterList.Parameters.Count == 0)
{
singleton = PropertyDeclaration(
IdentifierName(node.Identifier),
Identifier("Instance"))
.WithModifiers(
TokenList(
Token(
TriviaList(Whitespace(" ")),
SyntaxKind.PublicKeyword,
TriviaList(Space)),
Token(
TriviaList(),
SyntaxKind.StaticKeyword,
TriviaList(Space))))
.WithAccessorList(
AccessorList(
SingletonList(
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
.WithKeyword(Token(SyntaxKind.GetKeyword))
.WithSemicolon()))
.WithOpenBraceToken(
Token(
TriviaList(Space),
SyntaxKind.OpenBraceToken,
TriviaList(Space)))
.WithCloseBraceToken(
Token(
TriviaList(Space),
SyntaxKind.CloseBraceToken,
TriviaList(Space))))
.WithInitializer(
EqualsValueClause(
ObjectCreationExpression(
IdentifierName(node.Identifier))
.WithNewKeyword(
Token(
TriviaList(Space),
SyntaxKind.NewKeyword,
TriviaList(Space)))
.WithArgumentList(ArgumentList())))
.WithSemicolonToken(Token(
TriviaList(),
SyntaxKind.SemicolonToken,
TriviaList(CarriageReturnLineFeed, CarriageReturnLineFeed)));
}
else
{
singleton = MethodDeclaration(
IdentifierName(node.Identifier),
Identifier("Instance"))
.WithParameterList(ctor.ParameterList)
.WithModifiers(
TokenList(
Token(
TriviaList(Whitespace(" ")),
SyntaxKind.PublicKeyword,
TriviaList(Space)),
Token(
TriviaList(),
SyntaxKind.StaticKeyword,
TriviaList(Space))))
.WithExpressionBody(
ArrowExpressionClause(
ObjectCreationExpression(
IdentifierName(node.Identifier))
.WithNewKeyword(
Token(
TriviaList(Space),
SyntaxKind.NewKeyword,
TriviaList(Space)))
.WithArgumentList(ArgumentList(SeparatedList(
ctor.ParameterList.Parameters.Select(p => Argument(p.Identifier))))))
)
.WithSemicolonToken(Token(
TriviaList(),
SyntaxKind.SemicolonToken,
TriviaList(CarriageReturnLineFeed, CarriageReturnLineFeed)));
}

if (firstChild == null)
node = node.AddMembers(singleton);
else
node = node.InsertNodesBefore(firstChild, new[] { singleton });
}

return base.VisitClassDeclaration(node);
}
}

static bool CanGenerateFor(INamedTypeSymbol? symbol)
{
if (symbol == null)
Expand Down
Loading

0 comments on commit 545391a

Please sign in to comment.