Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Code Analyzer for common mistakes #15

Merged
merged 4 commits into from
Mar 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading