Skip to content

Commit

Permalink
add pinned analyzer
Browse files Browse the repository at this point in the history
  • Loading branch information
alamarre committed Nov 6, 2023
1 parent c33720c commit 084fdd2
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 1 deletion.
12 changes: 11 additions & 1 deletion src/D2L.CodeStyle.Analyzers/Diagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ public static class Diagnostics {
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Unnecessarily listed in an analyzer allowed list."
);
);

public static readonly DiagnosticDescriptor NamedArgumentsRequired = new DiagnosticDescriptor(
id: "D2L0058",
Expand Down Expand Up @@ -766,5 +766,15 @@ public static class Diagnostics {
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true
);

public static readonly DiagnosticDescriptor PinnedTypesMustNotMove = new DiagnosticDescriptor(
id: "D2L0103",
title: "Pinned types must not move",
messageFormat: "Changing the fully qualified assembly name risks breaking operations on Pinned objects",
category: "Correctness",
helpLinkUri: "https://github.com/Brightspace/architecture/blob/main/proposals/lms-modern-dot-net/pinning.md",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true
);
}
}
20 changes: 20 additions & 0 deletions src/D2L.CodeStyle.Analyzers/Pinning/PinnedAnalyzerHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;

namespace D2L.CodeStyle.Analyzers.Pinning {
public static class PinnedAnalyzerHelper {

public const string PinnedAttributeName = "D2L.CodeStyle.Annotations.Pinning.PinnedAttribute";

public static AttributeData? GetPinnedAttribute( ISymbol classSymbol, INamedTypeSymbol pinnedAttributeSymbol ) {
foreach( AttributeData? attributeData in classSymbol.GetAttributes() ) {
INamedTypeSymbol? attributeSymbol = attributeData.AttributeClass;
if( pinnedAttributeSymbol.Equals( attributeSymbol, SymbolEqualityComparer.Default ) ) {
return attributeData;
}
}

return null;
}
}
}
61 changes: 61 additions & 0 deletions src/D2L.CodeStyle.Analyzers/Pinning/PinnedAttributeAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace D2L.CodeStyle.Analyzers.Pinning {
[DiagnosticAnalyzer( LanguageNames.CSharp )]
internal sealed class PinnedAttributeAnalyzer : DiagnosticAnalyzer {

private static SymbolDisplayFormat FullyQualifiedNameFormat = new SymbolDisplayFormat(
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters
);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
Diagnostics.PinnedTypesMustNotMove
);

public override void Initialize( AnalysisContext context ) {
context.ConfigureGeneratedCodeAnalysis( GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics );
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction( OnCompilationStart );
}

private void OnCompilationStart(
CompilationStartAnalysisContext context
) {
INamedTypeSymbol? pinnedAttributeSymbol = context.Compilation.GetTypeByMetadataName( PinnedAnalyzerHelper.PinnedAttributeName );
if( pinnedAttributeSymbol != null ) {
context.RegisterSymbolAction( ( ctx ) => AnalyzeSymbol(ctx, pinnedAttributeSymbol),
SymbolKind.NamedType );
}
}

private static void AnalyzeSymbol( SymbolAnalysisContext context, INamedTypeSymbol pinnedAttributeSymbol ) {
INamedTypeSymbol classSymbol = (INamedTypeSymbol)context.Symbol;

Location? location = classSymbol.Locations.FirstOrDefault();
var attribute = PinnedAnalyzerHelper.GetPinnedAttribute( classSymbol, pinnedAttributeSymbol );
if( attribute == null ) {
return;
}

string? fqName = attribute.ConstructorArguments[0].Value?.ToString();
string? assembly = attribute.ConstructorArguments[1].Value?.ToString();
if( fqName == null || assembly == null ) {
context.ReportDiagnostic( Diagnostic.Create( Diagnostics.PinnedTypesMustNotMove, location, classSymbol.Name ) );
return;
}

string classFqName = classSymbol.ToDisplayString( FullyQualifiedNameFormat );

if( fqName != classFqName
|| assembly != classSymbol.ContainingAssembly.Name
) {
context.ReportDiagnostic( Diagnostic.Create( Diagnostics.PinnedTypesMustNotMove, location, classSymbol.Name ) );
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// analyzer: D2L.CodeStyle.Analyzers.Pinning.PinnedAttributeAnalyzer


using D2L.CodeStyle.Annotations.Pinning;

namespace D2L.Pinning.Test {
[Pinned(fullyQualifiedName: "D2L.Pinning.Test.PinnedProperly", assembly: "PinnedAttributeAnalyzer", pinnedRecursively: false)]
public class PinnedProperly {}

[Pinned(fullyQualifiedName: "D2L.Pinning.Test.PinnedProperly", assembly: "PinnedAttributeAnalyzer", pinnedRecursively: false)]
public class /* PinnedTypesMustNotMove() */ PinnedWithIncorrectName /**/ {}

[Pinned(fullyQualifiedName: "D2L.Pinning.Test.PinnedProperly", assembly: "PinnedAttributeAnalyzer", pinnedRecursively: false)]
public interface /* PinnedTypesMustNotMove() */ IPinnedWithIncorrectName /**/ {}

[Pinned(fullyQualifiedName: "D2L.Pinning.Test.PinnedProperly", assembly: "PinnedAttributeAnalyzer", pinnedRecursively: false)]
public record /* PinnedTypesMustNotMove() */ RecordPinnedWithIncorrectName /**/ {}

[Pinned(fullyQualifiedName: "D2L.Pinning.Test.PinnedProperly", assembly: "PinnedAttributeAnalyzer", pinnedRecursively: false)]
public struct /* PinnedTypesMustNotMove() */ StructPinnedWithIncorrectName /**/ {}

[Pinned(fullyQualifiedName: "D2L.Pinning.Test.PinnedWithIncorrectAssembly", assembly: "WrongPinnedAttributeAnalyzer", pinnedRecursively: false)]
public class /* PinnedTypesMustNotMove() */ PinnedWithIncorrectAssembly /**/ {}

[Pinned(fullyQualifiedName: "D2L.Pinning.Test.GenericPinnedProperly<T>", assembly: "PinnedAttributeAnalyzer", pinnedRecursively: false)]
public class GenericPinnedProperly<T> {}

[Pinned(fullyQualifiedName: "D2L.Pinning.Test.GenericPinnedWithIncorrectName<TWrong>", assembly: "PinnedAttributeAnalyzer", pinnedRecursively: false)]
public class /* PinnedTypesMustNotMove() */ GenericPinnedWithIncorrectName/**/<T> {}

// test that classes pinned inside an unpinned class are handled reasonably
public class Outer {
[Pinned(fullyQualifiedName: "D2L.Pinning.Test.Outer.PinnedProperly", assembly: "PinnedAttributeAnalyzer", pinnedRecursively: false)]
private class PinnedProperly {}

[Pinned(fullyQualifiedName: "D2L.Pinning.Outer.PinnedProperly", assembly: "PinnedAttributeAnalyzer", pinnedRecursively: false)]
private class /* PinnedTypesMustNotMove() */ PinnedWithIncorrectName /**/ {}
}
}

0 comments on commit 084fdd2

Please sign in to comment.