1
using System.Collections.Immutable;
2
using System.Linq;
3
using Libplanet.Action;
4
using Microsoft.CodeAnalysis;
5
using Microsoft.CodeAnalysis.Diagnostics;
6
using Microsoft.CodeAnalysis.Operations;
7

8
namespace Libplanet.Analyzers
9
{
10
    [DiagnosticAnalyzer(
11
        LanguageNames.CSharp,
12
        LanguageNames.FSharp,
13
        LanguageNames.VisualBasic
14
    )]
15
    public class ActionAnalyzer : DiagnosticAnalyzer
16
    {
17
        public const string IdPrefix = "LAA";
18

19
#pragma warning disable S1075
20
        public const string HelpLinkUriPrefix =
21
            "https://github.com/planetarium/libplanet/blob/main/Libplanet.Analyzers/rules/LAA";
22
#pragma warning restore S1075
23

24
#pragma warning disable SA1118
25 1
        private static readonly DiagnosticDescriptor[] Diagnostics =
26 1
        {
27 1
            new DiagnosticDescriptor(
28 1
                id: $"{IdPrefix}9999",
29 1
                title: "DebugMessageWhichShouldNotAppearToUsers",
30 1
                messageFormat: "DEBUG: {0}",
31 1
                category: "Debug",
32 1
                defaultSeverity: DiagnosticSeverity.Hidden,
33 1
                isEnabledByDefault: true
34 1
            ),
35 1
            new DiagnosticDescriptor(
36 1
                id: $"{IdPrefix}1001",
37 1
                title: "SystemRandomBreaksActionDeterminism",
38 1
                messageFormat:
39 1
                    $"The {{0}} makes an {nameof(IAction)} indeterministic; use " +
40 1
                    $"{nameof(IActionContext)}.{nameof(IActionContext.Random)} property instead.",
41 1
                category: "Determinism",
42 1
                defaultSeverity: DiagnosticSeverity.Warning,
43 1
                isEnabledByDefault: true,
44 1
                helpLinkUri: $"{HelpLinkUriPrefix}1001.md"
45 1
            ),
46 1
            new DiagnosticDescriptor(
47 1
                id: $"{IdPrefix}1002",
48 1
                title: "DictionariesOrSetsShouldBeOrderedToEnumerate",
49 1
                messageFormat: "Enumerating an instance of {0} is indeterministic since " +
50 1
                    "the order of {0} is unspecified; explicitly sort them before {1}.",
51 1
                category: "Determinism",
52 1
                defaultSeverity: DiagnosticSeverity.Warning,
53 1
                isEnabledByDefault: true,
54 1
                helpLinkUri: $"{HelpLinkUriPrefix}1002.md"
55 1
            ),
56 1
        };
57
#pragma warning restore SA1118
58

59
        public static IImmutableDictionary<string, DiagnosticDescriptor> Rules =>
60 1
            Diagnostics.ToImmutableDictionary(d => d.Id, d => d);
61

62
        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
63 1
            Diagnostics.ToImmutableArray();
64

65
        public override void Initialize(AnalysisContext context)
66 1
        {
67 1
            context.EnableConcurrentExecution();
68 1
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
69 1
            context.RegisterOperationAction(LAA1001_SystemRandom, OperationKind.ObjectCreation);
70 1
            context.RegisterOperationAction(
71 1
                LAA1002_DictionarySetEnumeration,
72 1
                OperationKind.Conversion,
73 1
                OperationKind.Loop
74 1
            );
75 1
        }
76

77
        private static void LAA1001_SystemRandom(OperationAnalysisContext context)
78 1
        {
79 1
            if (context.Operation is IObjectCreationOperation op &&
80 1
                context.Compilation.GetTypeByMetadataName("System.Random") is ITypeSymbol ts &&
81 1
                op.Type is ITypeSymbol opType &&
82 1
                SymbolEqualityComparer.Default.Equals(opType, ts))
83 1
            {
84 1
                Diagnostic diag = Diagnostic.Create(
85 1
                    Rules[$"{IdPrefix}1001"],
86 1
                    op.Syntax.GetLocation(),
87 1
                    ts.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)
88 1
                );
89 1
                context.ReportDiagnostic(diag);
90 1
            }
91 1
        }
92

93
        private static void LAA1002_DictionarySetEnumeration(OperationAnalysisContext context)
94 1
        {
95
            const string ns = "System.Collections";
96 1
            SymbolDisplayFormat symbolFormat = SymbolDisplayFormat.CSharpErrorMessageFormat;
97 1
            Compilation comp = context.Compilation;
98 1
            SymbolEqualityComparer comparer = SymbolEqualityComparer.Default;
99

100 1
            bool TypeEquals(ITypeSymbol? valType, string metadataName) => valType is ITypeSymbol a
101 1
                && comp.GetTypeByMetadataName(metadataName) is INamedTypeSymbol b
102 1
                && comparer.Equals(a, b);
103

104 1
            string[] dictOrSets =
105 1
            {
106 1
                $"{ns}.Generic.IDictionary`2",
107 1
                $"{ns}.Generic.IReadOnlyDictionary`2",
108 1
                $"{ns}.Immutable.IImmutableDictionary`2",
109 1
                $"{ns}.IDictionary",
110 1
                $"{ns}.Generic.ISet`1",
111 1
                $"{ns}.Immutable.IImmutableSet`1",
112 1
            };
113

114 1
            bool IsDictOrSet(ITypeSymbol? valType) => valType is ITypeSymbol t &&
115 1
                t.OriginalDefinition.AllInterfaces
116 1
                    .OfType<ITypeSymbol>()
117 1
                    .Select(ifce => ifce.OriginalDefinition)
118 1
                    .OfType<ITypeSymbol>()
119 1
                    .Any(i => dictOrSets.Any(dst => TypeEquals(i, dst))
120 1
                    );
121

122 1
            switch (context.Operation)
123
            {
124
                case IConversionOperation conv:
125 1
                {
126 1
                    ITypeSymbol? endType = conv.Type;
127 1
                    if (!TypeEquals(endType?.OriginalDefinition, $"{ns}.Generic.IEnumerable`1") &&
128 1
                        !TypeEquals(endType, $"{ns}.IEnumerable"))
129 1
                    {
130 1
                        return;
131
                    }
132

133 1
                    if (conv.Parent is IArgumentOperation arg &&
134 1
                        arg.Parent is IInvocationOperation invoke &&
135 1
                        invoke.TargetMethod.IsGenericMethod &&
136 1
                        invoke.TargetMethod.OriginalDefinition is IMethodSymbol proto)
137 1
                    {
138 1
                        var method = invoke.TargetMethod.Name;
139 1
                        if (method.StartsWith("OrderBy") &&
140 1
                            TypeEquals(proto.ContainingType, "System.Linq.Enumerable") &&
141 1
                            TypeEquals(
142 1
                                proto.ReturnType.OriginalDefinition,
143 1
                                "System.Linq.IOrderedEnumerable`1"
144 1
                            ))
145 1
                        {
146
                            // Ignores Linq's .OrderBy()/OrderByDescending() methods.
147 1
                            return;
148
                        }
149 1
                        else if (method.StartsWith("To") &&
150 1
                                 (method.EndsWith("Dictionary") || method.EndsWith("Set")) &&
151 1
                                 IsDictOrSet(proto.ReturnType))
152 1
                        {
153
                            // Ignores .ToDictionary()/ToHashSet() etc.
154 1
                            return;
155
                        }
156 1
                    }
157

158 1
                    if (conv.Parent is IArgumentOperation arg1 &&
159 1
                        arg1.Parent is IObjectCreationOperation @new &&
160 1
                        IsDictOrSet(@new.Type))
161 1
                    {
162
                        // Ignores new Dictionary()/new HashSet() etc.
163 1
                        return;
164
                    }
165

166 1
                    ITypeSymbol? valType = conv.Operand?.Type;
167 1
                    if (IsDictOrSet(valType))
168 1
                    {
169 1
                        string func = "enumerating";
170 1
                        if (conv.Parent is IArgumentOperation argConv)
171 1
                        {
172 1
                            func = argConv.Parent switch
173 1
                            {
174 1
                                IObjectCreationOperation c =>
175 1
                                    $"passing to {c.Constructor.ToDisplayString(symbolFormat)} " +
176 1
                                    "constructor",
177 1
                                IInvocationOperation m =>
178 1
                                    $"passing to {m.TargetMethod.ToDisplayString(symbolFormat)} " +
179 1
                                    "method",
180 1
                                _ => func,
181 1
                            };
182 1
                        }
183

184 1
                        Diagnostic diag = Diagnostic.Create(
185 1
                            Rules[$"{IdPrefix}1002"],
186 1
                            conv.Syntax.GetLocation(),
187 1
                            valType!.ToDisplayString(symbolFormat),
188 1
                            func
189 1
                        );
190 1
                        context.ReportDiagnostic(diag);
191 1
                    }
192

193 1
                    break;
194
                }
195

196
                case IForEachLoopOperation loop:
197 1
                {
198 1
                    IOperation collection = loop.Collection;
199 1
                    ITypeSymbol? collType = collection.Type;
200 1
                    if (IsDictOrSet(collType))
201 1
                    {
202 1
                        Diagnostic diag = Diagnostic.Create(
203 1
                            Rules[$"{IdPrefix}1002"],
204 1
                            collection.Syntax.GetLocation(),
205 1
                            collType.ToDisplayString(symbolFormat),
206 1
                            "iterating via foreach"
207 1
                        );
208 1
                        context.ReportDiagnostic(diag);
209 1
                    }
210

211 1
                    break;
212
                }
213
            }
214 1
        }
215

216
        private static Diagnostic Debug(string message) =>
217 0
            Diagnostic.Create(Rules[$"{IdPrefix}9999"], null, message);
218
    }
219
}

Read our documentation on viewing source code .

Loading