Skip to content

Commit

Permalink
feat(analyzers): add rule for disallowing readonly members with Provi…
Browse files Browse the repository at this point in the history
…de or Query (UIC003)
  • Loading branch information
jonisavo committed Sep 26, 2023
1 parent 48fef70 commit c04dce6
Show file tree
Hide file tree
Showing 15 changed files with 318 additions and 14 deletions.
Binary file not shown.
Binary file not shown.
Binary file modified Assets/UIComponents/Roslyn/UIComponents.Roslyn.Analyzers.dll
Binary file not shown.
Binary file modified Assets/UIComponents/Roslyn/UIComponents.Roslyn.Analyzers.pdb
Binary file not shown.
Binary file modified Assets/UIComponents/Roslyn/UIComponents.Roslyn.Common.dll
Binary file not shown.
Binary file modified Assets/UIComponents/Roslyn/UIComponents.Roslyn.Common.pdb
Binary file not shown.
Binary file modified Assets/UIComponents/Roslyn/UIComponents.Roslyn.Generation.dll
Binary file not shown.
Binary file modified Assets/UIComponents/Roslyn/UIComponents.Roslyn.Generation.pdb
Binary file not shown.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,7 @@
<data name="PartialFix_Title" xml:space="preserve">
<value>Make class partial</value>
</data>
<data name="InvalidReadonlyFix_Title" xml:space="preserve">
<value>Remove readonly keyword</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp;

namespace UIComponents.Roslyn.Analyzers
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(InvalidReadonlyCodeFixProvider)), Shared]
public sealed class InvalidReadonlyCodeFixProvider : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds
{
get { return ImmutableArray.Create(InvalidReadonlyMemberAnalyzer.DiagnosticId); }
}

public sealed override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

var diagnostic = context.Diagnostics.First();
var diagnosticSpan = diagnostic.Location.SourceSpan;

var fieldDeclaration = root.FindToken(diagnosticSpan.Start).Parent
.AncestorsAndSelf()
.OfType<FieldDeclarationSyntax>()
.First();

context.RegisterCodeFix(
CodeAction.Create(
title: CodeFixResources.InvalidReadonlyFix_Title,
createChangedDocument: cancellationToken => RemoveReadonly(context.Document, fieldDeclaration, cancellationToken),
equivalenceKey: nameof(CodeFixResources.InvalidReadonlyFix_Title)),
diagnostic);
}

private async Task<Document> RemoveReadonly(Document document, FieldDeclarationSyntax fieldDeclaration, CancellationToken cancellationToken)
{
var typeDeclaration = fieldDeclaration
.Ancestors()
.OfType<BaseTypeDeclarationSyntax>()
.First();

var editor = new SyntaxEditor(typeDeclaration, document.Project.Solution.Workspace);

var newModifiers = fieldDeclaration.Modifiers
.Where(modifier => modifier.Kind() != SyntaxKind.ReadOnlyKeyword);
var newFieldDeclaration = fieldDeclaration.WithModifiers(new SyntaxTokenList(newModifiers));
editor.ReplaceNode(fieldDeclaration, newFieldDeclaration);

var newDeclaration = editor.GetChangedRoot();

var root = await document.GetSyntaxRootAsync(cancellationToken);
var newRoot = root.ReplaceNode(typeDeclaration, newDeclaration);
var newDocument = document.WithSyntaxRoot(newRoot);

return newDocument;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using Microsoft.CodeAnalysis.Testing;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading.Tasks;
using VerifyCS = UIComponents.Roslyn.Analyzers.Test.CSharpCodeFixVerifier<
UIComponents.Roslyn.Analyzers.InvalidReadonlyMemberAnalyzer,
UIComponents.Roslyn.Analyzers.InvalidReadonlyCodeFixProvider>;

namespace UIComponents.Roslyn.Analyzers.Test
{
[TestClass]
public class InvalidReadonlyMemberAnalyzerTests
{
private const string UIComponentsDefinition = @"
namespace UIComponents
{
public class ProvideAttribute : Attribute
{
public ProvideAttribute() {}
}
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
public class QueryAttribute : Attribute
{
public QueryAttribute(string id) {}
}
}
";

[TestMethod]
public async Task It_Reports_Readonly_Query_And_Provide_Members()
{
var test = $@"
using System;
{UIComponentsDefinition}
namespace Application
{{
public class OtherAttribute : Attribute {{}}
public class TestComponent
{{
{{|#0:[UIComponents.Provide]
public readonly string first;|}}
{{|#1:[Other, UIComponents.Query(""test"")]
[UIComponents.Query(""test2"")]
public readonly string second;|}}
}}
}}";

var fixtest = $@"
using System;
{UIComponentsDefinition}
namespace Application
{{
public class OtherAttribute : Attribute {{}}
public class TestComponent
{{
[UIComponents.Provide]
public string first;
[Other, UIComponents.Query(""test"")]
[UIComponents.Query(""test2"")]
public string second;
}}
}}";
var firstResult = VerifyCS.Diagnostic("UIC003")
.WithLocation(0)
.WithArguments("ProvideAttribute");
var secondResult = VerifyCS.Diagnostic("UIC003")
.WithLocation(1)
.WithArguments("QueryAttribute");
await VerifyCS.VerifyCodeFixAsync(test,
new DiagnosticResult[] { firstResult, secondResult },
fixtest
);
}

[TestMethod]
public async Task It_Does_Not_Report_Non_Readonly_Fields()
{
var test = $@"
using System;
{UIComponentsDefinition}
namespace Application
{{
public class OtherAttribute : Attribute {{}}
public class TestComponent
{{
[UIComponents.Provide]
public string first;
[Other, UIComponents.Query(""test"")]
public string second;
}}
}}";
await VerifyCS.VerifyAnalyzerAsync(test);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
using System.Linq;

namespace UIComponents.Roslyn.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class InvalidReadonlyMemberAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "UIC003";

private static readonly LocalizableString Title =
new LocalizableResourceString(nameof(Resources.UIC003_Title), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableString MessageFormat =
new LocalizableResourceString(nameof(Resources.UIC003_MessageFormat), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableString Description =
new LocalizableResourceString(nameof(Resources.UIC003_Description), Resources.ResourceManager, typeof(Resources));
private static readonly string Category = "Core";

private static readonly DiagnosticDescriptor Rule =
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
get { return ImmutableArray.Create(Rule); }
}

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.FieldDeclaration);
}

private void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context)
{
var fieldDeclaration = (FieldDeclarationSyntax)context.Node;

if (!fieldDeclaration.Modifiers.Any(SyntaxKind.ReadOnlyKeyword))
return;

var provideAttributeSymbol =
context.Compilation.GetTypeByMetadataName("UIComponents.ProvideAttribute");
var queryAttributeSymbol =
context.Compilation.GetTypeByMetadataName("UIComponents.QueryAttribute");

var attributeLists = fieldDeclaration.AttributeLists;

foreach (var attributeList in attributeLists)
{
var found = false;

foreach (var attribute in attributeList.Attributes)
{
var symbol = context.SemanticModel.GetSymbolInfo(attribute).Symbol;

if (symbol == null)
continue;

var typeSymbol = symbol.ContainingType;

if (typeSymbol == null)
continue;

if (!SymbolEqualityComparer.Default.Equals(typeSymbol, provideAttributeSymbol)
&& !SymbolEqualityComparer.Default.Equals(typeSymbol, queryAttributeSymbol))
continue;

var diagnostic = Diagnostic.Create(Rule, fieldDeclaration.GetLocation(), typeSymbol.Name);
context.ReportDiagnostic(diagnostic);
found = true;

break;
}

if (found)
break;
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@
<data name="UIC002_Title" xml:space="preserve">
<value>Internal UIComponent methods should not be used</value>
</data>
<data name="UIC003_Description" xml:space="preserve">
<value>Attribute '{0}' generates code which assigns a new value to this member. Therefore, it can not be readonly.</value>
</data>
<data name="UIC003_MessageFormat" xml:space="preserve">
<value>Member can not be readonly, because '{0}' generates code to reassign it</value>
</data>
<data name="UIC003_Title" xml:space="preserve">
<value>Invalid readonly member</value>
</data>
<data name="UIC101_Description" xml:space="preserve">
<value>The stylesheet has already been declared in the UIComponent.</value>
</data>
Expand Down

0 comments on commit c04dce6

Please sign in to comment.