Skip to content

Commit

Permalink
Merge pull request #15 from ByronMayne/feature/GeneratorDiagnosticAna…
Browse files Browse the repository at this point in the history
…lyzer

Added Code Analyzer for common mistakes
  • Loading branch information
ByronMayne authored Mar 10, 2024
2 parents 51774e1 + ab55c99 commit 34f4553
Show file tree
Hide file tree
Showing 7 changed files with 346 additions and 22 deletions.
90 changes: 84 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
{

}

Expand Down Expand Up @@ -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 `<SGF_EmbeddedAssembly Include="Your Assembly Path"/>`

## 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using Microsoft.CodeAnalysis.Text;
using Newtonsoft.Json;
using SGF;
using SGF.Diagnostics;
using System;
using System.Text;

Expand Down
98 changes: 98 additions & 0 deletions src/SourceGenerator.Foundations/Analyzer/Rules/AnalyzerRule.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Gets the descritor that this rule creates
/// </summary>
public DiagnosticDescriptor Descriptor { get; }

/// <summary>
/// Gets the current context
/// </summary>
protected SyntaxNodeAnalysisContext Context { get; private set; }

public AnalyzerRule(DiagnosticDescriptor descriptor)
{
Descriptor = descriptor;
}

/// <summary>
/// Invokes the rule
/// </summary>
public void Invoke(SyntaxNodeAnalysisContext context, ClassDeclarationSyntax classDeclaration)
{
Context = context;
try
{
Analyze(classDeclaration);
}
finally
{
Context = default;
}
}

/// <summary>
/// Tells the rule to analyze and report and errors that it sees
/// </summary>
protected abstract void Analyze(ClassDeclarationSyntax classDeclaration);

/// <summary>
/// Creates new <see cref="Diagnostic"/> using the <see cref="Descriptor"/> and reports
/// it to the current context
/// </summary>
/// <param name="location">The location to put the diagnostic</param>
/// <param name="messageArgs">Arguments that are used for it</param>
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;
}

/// <summary>
/// Returns back if the attribute with the given name is applied to the type
/// </summary>
protected bool HasAttribute(ClassDeclarationSyntax classDeclarationSyntax, string name, StringComparison stringComparison = StringComparison.Ordinal)
=> GetAttribute(classDeclarationSyntax, name, stringComparison) != null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace SGF.Analyzer.Rules
{
/// <summary>
/// Ensures tha the <see cref="GeneratorAttribute"/> is not applied to
/// <see cref="IncrementalGenerator"/> as these types are not really <see cref="IIncrementalGenerator"/>
/// and won't be pickedup by Roslyn.
/// </summary>
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");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Linq;

namespace SGF.Analyzer.Rules
{
/// <summary>
/// Ensures that <see cref="IncrementalGenerator"/> types have a default
/// constructor defined.
/// </summary>
internal class RequireDefaultConstructorRule : AnalyzerRule
{
public RequireDefaultConstructorRule() : base(CreateDescriptor())
{
}

protected override void Analyze(ClassDeclarationSyntax classDeclaration)
{
ConstructorDeclarationSyntax[] constructors = classDeclaration.Members
.OfType<ConstructorDeclarationSyntax>()
.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");
}
}
}
Original file line number Diff line number Diff line change
@@ -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");
}
}
Loading

0 comments on commit 34f4553

Please sign in to comment.