Skip to content

Commit 7af80f9

Browse files
committed
Added some more tests that verify analyzer behavior.
1 parent 4de2bc7 commit 7af80f9

10 files changed

Lines changed: 227 additions & 120 deletions

File tree

GenerateReadOnly Tests/BinarySchemaTestUtil.cs

Lines changed: 0 additions & 21 deletions
This file was deleted.

GenerateReadOnly Tests/GenerateReadOnly Tests.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
1212
</PropertyGroup>
1313

14+
<ItemGroup>
15+
<Compile Remove="NewFolder\**" />
16+
<EmbeddedResource Remove="NewFolder\**" />
17+
<None Remove="NewFolder\**" />
18+
</ItemGroup>
19+
1420
<ItemGroup>
1521
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
1622
<PrivateAssets>all</PrivateAssets>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using NUnit.Framework;
2+
3+
using readOnly.generator;
4+
5+
namespace readOnly.analyzer;
6+
7+
internal class PartialTests {
8+
[Test]
9+
public void TestReportsDiagnosticForTypeWithoutPartial() {
10+
ReadOnlyGeneratorTestUtil.AssertDiagnostics(
11+
"""
12+
using readOnly;
13+
14+
namespace foo.bar;
15+
16+
[GenerateReadOnly]
17+
public class Foo;
18+
""",
19+
Rules.TypeMustBePartial);
20+
}
21+
22+
[Test]
23+
public void TestReportsDiagnosticForParentWithoutPartial() {
24+
ReadOnlyGeneratorTestUtil.AssertDiagnostics(
25+
"""
26+
using readOnly;
27+
28+
namespace foo.bar;
29+
30+
public class Parent {
31+
[GenerateReadOnly]
32+
public partial class Child;
33+
}
34+
""",
35+
Rules.ParentTypeMustBePartial);
36+
}
37+
}
Lines changed: 70 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.IO;
34
using System.Linq;
5+
using System.Threading;
46

57
using Microsoft.CodeAnalysis;
68
using Microsoft.CodeAnalysis.CSharp;
79
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.CodeAnalysis.Diagnostics;
811

912
using NUnit.Framework;
1013

14+
using readOnly.analyzer;
1115
using readOnly.util.asserts;
16+
using readOnly.util.diagnostics;
1217

1318
#pragma warning disable CS8604
1419

@@ -25,44 +30,75 @@ internal static class ReadOnlyGeneratorTestUtil {
2530
.Select(path => MetadataReference.CreateFromFile(path)));
2631

2732
public static void AssertGenerated(string src, params string[] expected) {
33+
var actual = ParseReadOnlyTypeCandidates(src, out var semanticModel)
34+
.Select(symbolAndSyntax
35+
=> ReadOnlyTypeGenerator
36+
.GenerateSourceForNamedType(
37+
symbolAndSyntax.namedTypeSymbol,
38+
semanticModel,
39+
symbolAndSyntax.declarationSyntax)
40+
.ReplaceLineEndings());
41+
42+
CollectionAssert.AreEqual(expected, actual);
43+
}
44+
45+
public static void AssertDiagnostics(
46+
string src,
47+
params DiagnosticDescriptor[] expected) {
48+
var actual = new List<DiagnosticDescriptor>();
49+
50+
foreach (var (namedTypeSymbol, declarationSyntax) in
51+
ParseReadOnlyTypeCandidates(src, out var semanticModel)) {
52+
var context = new SyntaxNodeAnalysisContext(
53+
declarationSyntax,
54+
semanticModel,
55+
new AnalyzerOptions([]),
56+
diagnostic => actual.Add(diagnostic.Descriptor),
57+
_ => true,
58+
CancellationToken.None);
59+
60+
GenerateReadOnlyAnalyzer.CheckType(
61+
context,
62+
declarationSyntax,
63+
namedTypeSymbol);
64+
}
65+
66+
CollectionAssert.AreEqual(expected, actual);
67+
}
68+
69+
private static IEnumerable<(INamedTypeSymbol namedTypeSymbol,
70+
TypeDeclarationSyntax declarationSyntax)>
71+
ParseReadOnlyTypeCandidates(string src, out SemanticModel semanticModel) {
2872
var syntaxTree = CSharpSyntaxTree.ParseText(src);
2973
var compilation = Compilation.Clone()
3074
.AddSyntaxTrees(syntaxTree);
3175

32-
var semanticModel = compilation.GetSemanticModel(syntaxTree);
33-
34-
var actual = syntaxTree
35-
.GetRoot()
36-
.DescendantTokens()
37-
.Where(t => t is {
38-
Text: "GenerateReadOnly",
39-
Parent.Parent: AttributeSyntax
40-
})
41-
.Select(t => t.Parent?.Parent as AttributeSyntax)
42-
.Select(attributeSyntax => {
43-
var attributeListSyntax
44-
= Asserts.AsA<AttributeListSyntax>(
45-
attributeSyntax.Parent);
46-
var declarationSyntax
47-
= Asserts.AsA<TypeDeclarationSyntax>(
48-
attributeListSyntax.Parent);
49-
50-
var symbol
51-
= semanticModel
52-
.GetDeclaredSymbol(declarationSyntax);
53-
var namedTypeSymbol
54-
= symbol as INamedTypeSymbol;
55-
56-
return (namedTypeSymbol, declarationSyntax);
57-
})
58-
.Select(symbolAndSyntax
59-
=> new ReadOnlyTypeGenerator()
60-
.GenerateSourceForNamedType(
61-
symbolAndSyntax.namedTypeSymbol,
62-
semanticModel,
63-
symbolAndSyntax.declarationSyntax)
64-
.ReplaceLineEndings());
76+
semanticModel = compilation.GetSemanticModel(syntaxTree);
77+
var localSemanticModel = semanticModel;
6578

66-
CollectionAssert.AreEqual(expected, actual);
79+
return syntaxTree
80+
.GetRoot()
81+
.DescendantTokens()
82+
.Where(t => t is {
83+
Text: "GenerateReadOnly",
84+
Parent.Parent: AttributeSyntax
85+
})
86+
.Select(t => t.Parent?.Parent as AttributeSyntax)
87+
.Select(attributeSyntax => {
88+
var attributeListSyntax
89+
= Asserts.AsA<AttributeListSyntax>(
90+
attributeSyntax.Parent);
91+
var declarationSyntax
92+
= Asserts.AsA<TypeDeclarationSyntax>(
93+
attributeListSyntax.Parent);
94+
95+
var symbol
96+
= localSemanticModel.GetDeclaredSymbol(
97+
declarationSyntax);
98+
var namedTypeSymbol
99+
= symbol as INamedTypeSymbol;
100+
101+
return (namedTypeSymbol, declarationSyntax);
102+
});
67103
}
68104
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
using System;
2+
using System.Collections.Immutable;
3+
using System.Diagnostics;
4+
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using Microsoft.CodeAnalysis.Diagnostics;
9+
10+
using readOnly.generator;
11+
using readOnly.util.symbols;
12+
using readOnly.util.syntax;
13+
14+
namespace readOnly.analyzer;
15+
16+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
17+
public class GenerateReadOnlyAnalyzer : DiagnosticAnalyzer {
18+
public override ImmutableArray<DiagnosticDescriptor>
19+
SupportedDiagnostics { get; } =
20+
ImmutableArray.Create(
21+
Rules.TypeMustBePartial,
22+
Rules.ParentTypeMustBePartial,
23+
Rules.Exception
24+
);
25+
26+
public override void Initialize(AnalysisContext context) {
27+
context.RegisterSyntaxNodeAction(
28+
syntaxNodeContext => {
29+
var syntax = syntaxNodeContext.Node as ClassDeclarationSyntax;
30+
31+
var symbol =
32+
syntaxNodeContext.SemanticModel.GetDeclaredSymbol(syntax!);
33+
if (symbol is not INamedTypeSymbol namedTypeSymbol) {
34+
return;
35+
}
36+
37+
CheckType(syntaxNodeContext, syntax!, namedTypeSymbol);
38+
},
39+
SyntaxKind.ClassDeclaration);
40+
41+
context.RegisterSyntaxNodeAction(
42+
syntaxNodeContext => {
43+
var syntax = syntaxNodeContext.Node as StructDeclarationSyntax;
44+
45+
var symbol =
46+
syntaxNodeContext.SemanticModel.GetDeclaredSymbol(syntax!);
47+
if (symbol is not INamedTypeSymbol namedTypeSymbol) {
48+
return;
49+
}
50+
51+
CheckType(syntaxNodeContext, syntax!, namedTypeSymbol);
52+
},
53+
SyntaxKind.StructDeclaration);
54+
}
55+
56+
public static void CheckType(
57+
SyntaxNodeAnalysisContext context,
58+
TypeDeclarationSyntax syntax,
59+
INamedTypeSymbol symbol) {
60+
try {
61+
if (!symbol.HasAttribute<GenerateReadOnlyAttribute>()) {
62+
return;
63+
}
64+
65+
if (!syntax.IsPartial()) {
66+
Rules.ReportDiagnostic(
67+
context,
68+
symbol,
69+
Rules.TypeMustBePartial);
70+
return;
71+
}
72+
73+
var containingType = symbol.ContainingType;
74+
while (containingType != null) {
75+
var typeDeclarationSyntax =
76+
containingType.DeclaringSyntaxReferences[0].GetSyntax() as
77+
TypeDeclarationSyntax;
78+
79+
if (!typeDeclarationSyntax!.IsPartial()) {
80+
Rules.ReportDiagnostic(
81+
context,
82+
symbol,
83+
Rules.ParentTypeMustBePartial);
84+
}
85+
86+
containingType = containingType.ContainingType;
87+
}
88+
89+
ReadOnlyTypeGenerator.GenerateSourceForNamedType(
90+
symbol,
91+
context.SemanticModel,
92+
syntax);
93+
} catch (Exception exception) {
94+
if (Debugger.IsAttached) {
95+
throw;
96+
}
97+
98+
Rules.ReportExceptionDiagnostic(context, symbol, exception);
99+
}
100+
}
101+
}

GenerateReadOnly/src/generator/rules/Rules.cs renamed to GenerateReadOnly/src/analyzer/Rules.cs

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
using Microsoft.CodeAnalysis;
55
using Microsoft.CodeAnalysis.Diagnostics;
66

7-
namespace readOnly.generator.rules;
7+
namespace readOnly.analyzer;
88

99
public static partial class Rules {
1010
private static int diagnosticId_ = 0;
@@ -20,7 +20,7 @@ private static DiagnosticDescriptor CreateDiagnosticDescriptor_(
2020
=> new(Rules.GetNextDiagnosticId_(),
2121
title,
2222
messageFormat,
23-
"BinarySchemaAnalyzer",
23+
"GenerateReadOnlyAnalyzer",
2424
DiagnosticSeverity.Error,
2525
true);
2626

@@ -30,7 +30,7 @@ private static DiagnosticDescriptor CreateDiagnosticDescriptor_(
3030
"GenerateReadOnly type must be partial",
3131
$"Type '{{0}}' was annotated with {nameof(GenerateReadOnlyAttribute)}, so it must be partial to accept automatically generated read/write code.");
3232

33-
public static DiagnosticDescriptor ContainerTypeMustBePartial {
33+
public static DiagnosticDescriptor ParentTypeMustBePartial {
3434
get;
3535
} = Rules.CreateDiagnosticDescriptor_(
3636
"Container of GenerateReadOnly type must be partial",
@@ -39,12 +39,7 @@ public static DiagnosticDescriptor ContainerTypeMustBePartial {
3939
public static DiagnosticDescriptor Exception { get; }
4040
= Rules.CreateDiagnosticDescriptor_(
4141
"Exception",
42-
"Ran into an exception while generating source ({0}),{1}");
43-
44-
public static DiagnosticDescriptor SymbolException { get; }
45-
= Rules.CreateDiagnosticDescriptor_(
46-
"Exception",
47-
"Ran into an exception while parsing ({0}),{1}");
42+
"Ran into an exception while processing symbol '{0}',{1}");
4843

4944

5045
public static Diagnostic CreateDiagnostic(
@@ -65,21 +60,12 @@ public static void ReportDiagnostic(
6560
public static Diagnostic CreateExceptionDiagnostic(
6661
ISymbol symbol,
6762
Exception exception)
68-
=> Diagnostic.Create(
69-
Rules.SymbolException,
70-
symbol.Locations.First(),
71-
exception.Message,
72-
exception.StackTrace.Replace("\r\n", "").Replace("\n", ""));
73-
74-
public static Diagnostic CreateExceptionDiagnostic(
75-
Exception exception)
7663
=> Diagnostic.Create(
7764
Rules.Exception,
78-
null,
65+
symbol.Locations.First(),
7966
exception.Message,
8067
exception.StackTrace.Replace("\r\n", "").Replace("\n", ""));
8168

82-
8369
public static void ReportExceptionDiagnostic(
8470
SyntaxNodeAnalysisContext? context,
8571
ISymbol symbol,

GenerateReadOnly/src/generator/ReadOnlyTypeGenerator.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,16 @@ internal override bool FilterNamedTypesBeforeGenerating(
2727
SemanticModel semanticModel,
2828
TypeDeclarationSyntax syntax) {
2929
yield return ($"{symbol.GetUniqueNameForGenerator()}_readOnly.g",
30-
this.GenerateSourceForNamedType(
30+
GenerateSourceForNamedType(
3131
symbol,
3232
semanticModel,
3333
syntax));
3434
}
3535

36-
public string GenerateSourceForNamedType(INamedTypeSymbol typeSymbol,
37-
SemanticModel semanticModel,
38-
TypeDeclarationSyntax syntax) {
36+
public static string GenerateSourceForNamedType(
37+
INamedTypeSymbol typeSymbol,
38+
SemanticModel semanticModel,
39+
TypeDeclarationSyntax syntax) {
3940
var sb = new StringBuilder();
4041
using var sw = new SourceWriter(new StringWriter(sb));
4142

0 commit comments

Comments
 (0)