diff --git a/README.md b/README.md index 66da656..e8599e2 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,8 @@ namespace Example [SgfGenerator] public class ExampleSourceGenerator : IncrementalGenerator { - // Constructor can only take two arguments in this order - public ExampleSourceGenerator( - IGeneratorEnvironment generatorEnvironment, - ILogger logger) : base("ExampleSourceGenerator", - generatorPlatform, logger) - {$$ + public ExampleSourceGenerator() : base("ExampleSourceGenerator") + { } @@ -129,6 +125,88 @@ When your source generator runs it needs to find it's dependencies and this is o You can embed any assemblies you want by adding them to `` +## Diagnostic Analyzer + +Included with this package is a code analyzer that will be used to catch common mistakes when working with this library. + + +### `SGF1001` +**Has SgfGenerator Attribute** + +Any class that inherits from `IncrementalGenerator` is required to have the `SgfGenerator` attribute applied to it. + +```cs +// Error +public class MyGenerator : IncrementalGenerator +{ + public MyGenerator() : base("MyGenerator") + {} +} +``` + +To fix the error just apply the attribute. + +```cs +// Fixed +[SgfGeneratorAttribute] +public class MyGenerator : IncrementalGenerator +{ + public MyGenerator() : base("MyGenerator") + {} +} +``` + +### `SGF1002` + +**Prohibit Generator Attribute** + +If an `IncrementalGenerator` has the `Generator` attribute applied it will cause a compiler error. The reason being that the `Generator` attribute is used on classes that implement `IIncrementalGenerator` which `IncrementalGenerator` does not. SGF has it's own attribute to not confuse roslyn. SGFs `IncrementalGenerator` is run from within a wrapper to help capture exceptions and handle runtime type resolving. + +```cs +// Error +[Generator] +public class MyGenerator : IncrementalGenerator +{ + public MyGenerator() : base("MyGenerator") + {} +} +``` + +To fix the error just remove the attribute. + +```cs +// Fixed +public class MyGenerator : IncrementalGenerator +{ + public MyGenerator() : base("MyGenerator") + {} +} +``` + +### `SGF1003` +**Has Default Constructor** + +`IncrementalGenerator` require a default constructor so they can be instantiated at runtime. If no constructor is defined the generator will never be run. + +```cs +// Error +public class MyGenerator : IncrementalGenerator +{ + public MyGenerator(string name) : base(name) + {} +} +``` + +Add a default constructor that takes no arguments. + +```cs +// Fixed +public class MyGenerator : IncrementalGenerator +{ + public MyGenerator() : base("MyGenerator") + {} +} +``` ## Project Layout diff --git a/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleAppSourceGenerator.cs b/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleAppSourceGenerator.cs index 7e573ec..89e56b2 100644 --- a/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleAppSourceGenerator.cs +++ b/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleAppSourceGenerator.cs @@ -2,7 +2,6 @@ using Microsoft.CodeAnalysis.Text; using Newtonsoft.Json; using SGF; -using SGF.Diagnostics; using System; using System.Text; diff --git a/src/SourceGenerator.Foundations/Analyzer/Rules/AnalyzerRule.cs b/src/SourceGenerator.Foundations/Analyzer/Rules/AnalyzerRule.cs new file mode 100644 index 0000000..e6197f7 --- /dev/null +++ b/src/SourceGenerator.Foundations/Analyzer/Rules/AnalyzerRule.cs @@ -0,0 +1,98 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System; + +namespace SGF.Analyzer.Rules +{ + internal abstract class AnalyzerRule + { + /// + /// Gets the descritor that this rule creates + /// + public DiagnosticDescriptor Descriptor { get; } + + /// + /// Gets the current context + /// + protected SyntaxNodeAnalysisContext Context { get; private set; } + + public AnalyzerRule(DiagnosticDescriptor descriptor) + { + Descriptor = descriptor; + } + + /// + /// Invokes the rule + /// + public void Invoke(SyntaxNodeAnalysisContext context, ClassDeclarationSyntax classDeclaration) + { + Context = context; + try + { + Analyze(classDeclaration); + } + finally + { + Context = default; + } + } + + /// + /// Tells the rule to analyze and report and errors that it sees + /// + protected abstract void Analyze(ClassDeclarationSyntax classDeclaration); + + /// + /// Creates new using the and reports + /// it to the current context + /// + /// The location to put the diagnostic + /// Arguments that are used for it + protected void ReportDiagnostic(Location location, params object[] messageArgs) + { + Diagnostic diagnostic = Diagnostic.Create(Descriptor, location, messageArgs); ; + Context.ReportDiagnostic(diagnostic); + } + + protected bool TryGetAttribute(ClassDeclarationSyntax classDeclaration, string name, out AttributeSyntax? attribute) + => TryGetAttribute(classDeclaration, name, StringComparison.Ordinal, out attribute); + + protected bool TryGetAttribute(ClassDeclarationSyntax classDeclaration, string name, StringComparison stringComparison, out AttributeSyntax? attribute) + { + attribute = GetAttribute(classDeclaration, name); + return attribute != null; + } + + protected AttributeSyntax? GetAttribute(ClassDeclarationSyntax classDeclarationSyntax, string name, StringComparison stringComparison = StringComparison.Ordinal) + { + const string POSTFIX = "Attribute"; + + string alterntiveName = name.EndsWith(POSTFIX, StringComparison.Ordinal) + ? name.Substring(0, name.Length - POSTFIX.Length) + : $"{name}{POSTFIX}"; + + foreach (AttributeListSyntax attributeList in classDeclarationSyntax.AttributeLists) + { + foreach (AttributeSyntax attribute in attributeList.Attributes) + { + string attributeName = attribute.Name.ToString(); + + if (string.Equals(attributeName, name, stringComparison) || + string.Equals(attributeName, alterntiveName, stringComparison)) + { + return attribute; + } + } + } + + return null; + } + + /// + /// Returns back if the attribute with the given name is applied to the type + /// + protected bool HasAttribute(ClassDeclarationSyntax classDeclarationSyntax, string name, StringComparison stringComparison = StringComparison.Ordinal) + => GetAttribute(classDeclarationSyntax, name, stringComparison) != null; + } +} diff --git a/src/SourceGenerator.Foundations/Analyzer/Rules/ProhibitGeneratorAttributeRule.cs b/src/SourceGenerator.Foundations/Analyzer/Rules/ProhibitGeneratorAttributeRule.cs new file mode 100644 index 0000000..349a841 --- /dev/null +++ b/src/SourceGenerator.Foundations/Analyzer/Rules/ProhibitGeneratorAttributeRule.cs @@ -0,0 +1,38 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace SGF.Analyzer.Rules +{ + /// + /// Ensures tha the is not applied to + /// as these types are not really + /// and won't be pickedup by Roslyn. + /// + internal class ProhibitGeneratorAttributeRule : AnalyzerRule + { + public ProhibitGeneratorAttributeRule() : base(CreateDescriptor()) + { + + } + + protected override void Analyze(ClassDeclarationSyntax classDeclaration) + { + + if (TryGetAttribute(classDeclaration, nameof(GeneratorAttribute), out AttributeSyntax? attributeSyntax)) + { + Location location = attributeSyntax!.GetLocation(); + ReportDiagnostic(location, classDeclaration.Identifier.Text); + } + } + + private static DiagnosticDescriptor CreateDescriptor() + => new DiagnosticDescriptor("SGF1002", + "Prohibit GeneratorAttribute", + $"{{0}} has the {nameof(GeneratorAttribute)} which can't be applied to classes which are inheirting from the Generator Foundations type {nameof(IncrementalGenerator)}.", + "SourceGeneration", + DiagnosticSeverity.Error, + true, + $"Incremental Generators should not have the {nameof(GeneratorAttribute)} applied to them.", + "https://github.com/ByronMayne/SourceGenerator.Foundations?tab=readme-ov-file#sgf1002"); + } +} diff --git a/src/SourceGenerator.Foundations/Analyzer/Rules/RequireDefaultConstructorRule.cs b/src/SourceGenerator.Foundations/Analyzer/Rules/RequireDefaultConstructorRule.cs new file mode 100644 index 0000000..3ac74c3 --- /dev/null +++ b/src/SourceGenerator.Foundations/Analyzer/Rules/RequireDefaultConstructorRule.cs @@ -0,0 +1,52 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Linq; + +namespace SGF.Analyzer.Rules +{ + /// + /// Ensures that types have a default + /// constructor defined. + /// + internal class RequireDefaultConstructorRule : AnalyzerRule + { + public RequireDefaultConstructorRule() : base(CreateDescriptor()) + { + } + + protected override void Analyze(ClassDeclarationSyntax classDeclaration) + { + ConstructorDeclarationSyntax[] constructors = classDeclaration.Members + .OfType() + .ToArray(); + + if(constructors.Length == 0) + { + // Already a compiler error since you need to call the base class constructor + return; + } + + if(constructors.Any(c => c.ParameterList.Parameters.Count == 0)) + { + // We have a default constructor + return; + } + + + Location location = classDeclaration.Identifier.GetLocation(); + ReportDiagnostic(location, classDeclaration.Identifier.Text); + } + + private static DiagnosticDescriptor CreateDescriptor() + { + return new DiagnosticDescriptor("SGF1003", + "HasDefaultConstructor", + $"{{0}} is missing a default constructor", + "SourceGeneration", + DiagnosticSeverity.Error, + true, + "SGF Incremental Generators must have a default constructor otherwise they will not be run", + "https://github.com/ByronMayne/SourceGenerator.Foundations?tab=readme-ov-file#sgf1003"); + } + } +} diff --git a/src/SourceGenerator.Foundations/Analyzer/Rules/RequireSfgGeneratorAttributeRule.cs b/src/SourceGenerator.Foundations/Analyzer/Rules/RequireSfgGeneratorAttributeRule.cs new file mode 100644 index 0000000..b73daa7 --- /dev/null +++ b/src/SourceGenerator.Foundations/Analyzer/Rules/RequireSfgGeneratorAttributeRule.cs @@ -0,0 +1,32 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace SGF.Analyzer.Rules +{ + internal class RequireSfgGeneratorAttributeRule : AnalyzerRule + { + + public RequireSfgGeneratorAttributeRule() : base(CreateDescriptor()) + { + } + + protected override void Analyze(ClassDeclarationSyntax classDeclaration) + { + if (!HasAttribute(classDeclaration, nameof(SgfGeneratorAttribute))) + { + Location location = classDeclaration.Identifier.GetLocation(); + ReportDiagnostic(location, classDeclaration.Identifier.Text); + } + } + + private static DiagnosticDescriptor CreateDescriptor() + => new DiagnosticDescriptor("SGF1001", + "SGFGeneratorAttributeApplied", + $"{{0}} is missing the {nameof(SgfGeneratorAttribute)}", + "SourceGeneration", + DiagnosticSeverity.Error, + true, + $"Source generators are required to have the attribute {nameof(SgfGeneratorAttribute)} applied to them otherwise the compiler won't invoke them", + "https://github.com/ByronMayne/SourceGenerator.Foundations?tab=readme-ov-file#sgf1001"); + } +} diff --git a/src/SourceGenerator.Foundations/Analyzer/SourceGeneratorAnalyzer.cs b/src/SourceGenerator.Foundations/Analyzer/SourceGeneratorAnalyzer.cs index 9118f4b..21d4c04 100644 --- a/src/SourceGenerator.Foundations/Analyzer/SourceGeneratorAnalyzer.cs +++ b/src/SourceGenerator.Foundations/Analyzer/SourceGeneratorAnalyzer.cs @@ -2,31 +2,29 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -using System; +using SGF.Analyzer.Rules; using System.Collections.Immutable; +using System.Linq; namespace SGF.Analyzer { [DiagnosticAnalyzer(LanguageNames.CSharp)] public class SourceGeneratorAnalyzer : DiagnosticAnalyzer { - public DiagnosticDescriptor GeneratorAttributeDescriptor { get; } - /// public override ImmutableArray SupportedDiagnostics { get; } + internal ImmutableArray Rules { get; } + public SourceGeneratorAnalyzer() { - GeneratorAttributeDescriptor = new DiagnosticDescriptor("sgf-generator-attribute-is-applied", - "SourceGeneratorAttributeApplied", - $"The class is missing the {nameof(GeneratorAttribute)} which is required for them to work.", - "SourceGeneration", - DiagnosticSeverity.Error, - true, - $"Source generators are required to have the attribute {nameof(GeneratorAttribute)} applied to them otherwise the compiler won't invoke them", - "https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.generatorattribute?view=roslyn-dotnet-4.3.0"); - - SupportedDiagnostics = new[] { GeneratorAttributeDescriptor }.ToImmutableArray(); + Rules = new AnalyzerRule[] + { + new RequireSfgGeneratorAttributeRule(), + new ProhibitGeneratorAttributeRule(), + new RequireDefaultConstructorRule() + }.ToImmutableArray(); + SupportedDiagnostics = Rules.Select(r => r.Descriptor).ToImmutableArray(); } public override void Initialize(AnalysisContext context) @@ -38,8 +36,37 @@ public override void Initialize(AnalysisContext context) private void CheckForAttribute(SyntaxNodeAnalysisContext context) { - - + SemanticModel semanticModel = context.SemanticModel; + ClassDeclarationSyntax classDeclaration = (ClassDeclarationSyntax)context.Node; + INamedTypeSymbol? symbolInfo = semanticModel.GetDeclaredSymbol(classDeclaration); + + if (classDeclaration.BaseList == null || symbolInfo == null) return; + if (!IsIncrementalGenerator(symbolInfo)) return; + + foreach(AnalyzerRule rule in Rules) + { + rule.Invoke(context, classDeclaration); + } + } + + /// + /// Returns back if the type inheirts from or not + /// + /// The type to check + /// True if it does and false if it does not + private static bool IsIncrementalGenerator(INamedTypeSymbol? typeSymbol) + { + while (typeSymbol != null) + { + if (string.Equals(typeSymbol.ToDisplayString(), "SGF.IncrementalGenerator")) + { + return true; + } + + typeSymbol = typeSymbol.BaseType; + } + + return false; } } }