Skip to content

Commit 0d855cd

Browse files
Update SA1121 and SA1404 to detect global using aliases
1 parent b209e4e commit 0d855cd

File tree

8 files changed

+333
-8
lines changed

8 files changed

+333
-8
lines changed

StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp10/MaintainabilityRules/SA1404CSharp10UnitTests.cs

+45
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,54 @@
33

44
namespace StyleCop.Analyzers.Test.CSharp10.MaintainabilityRules
55
{
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using StyleCop.Analyzers.MaintainabilityRules;
69
using StyleCop.Analyzers.Test.CSharp9.MaintainabilityRules;
10+
using Xunit;
11+
using static StyleCop.Analyzers.Test.Verifiers.StyleCopCodeFixVerifier<
12+
StyleCop.Analyzers.MaintainabilityRules.SA1404CodeAnalysisSuppressionMustHaveJustification,
13+
StyleCop.Analyzers.MaintainabilityRules.SA1404CodeFixProvider>;
714

815
public class SA1404CSharp10UnitTests : SA1404CSharp9UnitTests
916
{
17+
[Fact]
18+
public async Task TestUsingNameChangeInGlobalUsingInAnotherFileAsync()
19+
{
20+
var testCode1 = @"
21+
global using MySuppressionAttribute = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute;";
22+
23+
var testCode2 = @"
24+
public class Foo
25+
{
26+
[[|MySuppression(null, null)|]]
27+
public void Bar()
28+
{
29+
30+
}
31+
}";
32+
33+
var fixedCode2 = @"
34+
public class Foo
35+
{
36+
[MySuppression(null, null, Justification = """ + SA1404CodeAnalysisSuppressionMustHaveJustification.JustificationPlaceholder + @""")]
37+
public void Bar()
38+
{
39+
40+
}
41+
}";
42+
43+
await new CSharpTest()
44+
{
45+
TestSources = { testCode1, testCode2 },
46+
FixedSources = { testCode1, fixedCode2 },
47+
RemainingDiagnostics =
48+
{
49+
Diagnostic().WithLocation("/0/Test1.cs", 4, 32),
50+
},
51+
NumberOfIncrementalIterations = 2,
52+
NumberOfFixAllIterations = 2,
53+
}.RunAsync(CancellationToken.None).ConfigureAwait(false);
54+
}
1055
}
1156
}

StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp10/ReadabilityRules/SA1121CSharp10UnitTests.cs

+25
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,30 @@ class Bar
4242
FixedCode = newSource,
4343
}.RunAsync(CancellationToken.None).ConfigureAwait(false);
4444
}
45+
46+
[Fact]
47+
public async Task TestUsingNameChangeInGlobalUsingInAnotherFileAsync()
48+
{
49+
var source1 = @"
50+
global using MyDouble = System.Double;";
51+
52+
var oldSource2 = @"
53+
class TestClass
54+
{
55+
private [|MyDouble|] x;
56+
}";
57+
58+
var newSource2 = @"
59+
class TestClass
60+
{
61+
private double x;
62+
}";
63+
64+
await new CSharpTest()
65+
{
66+
TestSources = { source1, oldSource2 },
67+
FixedSources = { source1, newSource2 },
68+
}.RunAsync(CancellationToken.None).ConfigureAwait(false);
69+
}
4570
}
4671
}

StyleCop.Analyzers/StyleCop.Analyzers/Helpers/SyntaxTreeHelpers.cs

+13-6
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public static bool IsWhitespaceOnly(this SyntaxTree tree, CancellationToken canc
7272
&& TriviaHelper.IndexOfFirstNonWhitespaceTrivia(firstToken.LeadingTrivia) == -1;
7373
}
7474

75-
internal static bool ContainsUsingAlias(this SyntaxTree tree, ConcurrentDictionary<SyntaxTree, bool> cache)
75+
internal static bool ContainsUsingAlias(this SyntaxTree tree, ConcurrentDictionary<SyntaxTree, bool> cache, SemanticModel semanticModel, CancellationToken cancellationToken)
7676
{
7777
if (tree == null)
7878
{
@@ -85,16 +85,23 @@ internal static bool ContainsUsingAlias(this SyntaxTree tree, ConcurrentDictiona
8585
return result;
8686
}
8787

88-
bool generated = ContainsUsingAliasNoCache(tree);
88+
bool generated = ContainsUsingAliasNoCache(tree, semanticModel, cancellationToken);
8989
cache.TryAdd(tree, generated);
9090
return generated;
9191
}
9292

93-
private static bool ContainsUsingAliasNoCache(SyntaxTree tree)
93+
private static bool ContainsUsingAliasNoCache(SyntaxTree tree, SemanticModel semanticModel, CancellationToken cancellationToken)
9494
{
95-
var nodes = tree.GetRoot().DescendantNodes(node => node.IsKind(SyntaxKind.CompilationUnit) || node.IsKind(SyntaxKind.NamespaceDeclaration) || node.IsKind(SyntaxKindEx.FileScopedNamespaceDeclaration));
96-
97-
return nodes.OfType<UsingDirectiveSyntax>().Any(x => x.Alias != null);
95+
if (SemanticModelExtensions.SupportsGetImportScopes)
96+
{
97+
var scopes = semanticModel.GetImportScopes(0, cancellationToken);
98+
return scopes.Any(x => x.Aliases.Length > 0);
99+
}
100+
else
101+
{
102+
var nodes = tree.GetRoot().DescendantNodes(node => node.IsKind(SyntaxKind.CompilationUnit) || node.IsKind(SyntaxKind.NamespaceDeclaration) || node.IsKind(SyntaxKindEx.FileScopedNamespaceDeclaration));
103+
return nodes.OfType<UsingDirectiveSyntax>().Any(x => x.Alias != null);
104+
}
98105
}
99106
}
100107
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
namespace StyleCop.Analyzers.Lightup
5+
{
6+
using System;
7+
using System.Collections.Immutable;
8+
using Microsoft.CodeAnalysis;
9+
10+
internal readonly struct IImportScopeWrapper
11+
{
12+
internal const string WrappedTypeName = "Microsoft.CodeAnalysis.IImportScope";
13+
private static readonly Type WrappedType;
14+
15+
private static readonly Func<object?, ImmutableArray<IAliasSymbol>> AliasesAccessor;
16+
17+
private readonly object node;
18+
19+
static IImportScopeWrapper()
20+
{
21+
WrappedType = WrapperHelper.GetWrappedType(typeof(IImportScopeWrapper));
22+
23+
AliasesAccessor = LightupHelpers.CreateSyntaxPropertyAccessor<object?, ImmutableArray<IAliasSymbol>>(WrappedType, nameof(Aliases));
24+
}
25+
26+
private IImportScopeWrapper(object node)
27+
{
28+
this.node = node;
29+
}
30+
31+
public ImmutableArray<IAliasSymbol> Aliases
32+
{
33+
get
34+
{
35+
if (this.node == null && WrappedType == null)
36+
{
37+
// Gracefully fall back to a collection with no values
38+
return ImmutableArray<IAliasSymbol>.Empty;
39+
}
40+
41+
return AliasesAccessor(this.node);
42+
}
43+
}
44+
45+
// NOTE: Referenced via reflection
46+
public static IImportScopeWrapper FromObject(object node)
47+
{
48+
if (node == null)
49+
{
50+
return default;
51+
}
52+
53+
if (!IsInstance(node))
54+
{
55+
throw new InvalidCastException($"Cannot cast '{node.GetType().FullName}' to '{WrappedTypeName}'");
56+
}
57+
58+
return new IImportScopeWrapper(node);
59+
}
60+
61+
public static bool IsInstance(object obj)
62+
{
63+
return obj != null && LightupHelpers.CanWrapObject(obj, WrappedType);
64+
}
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
namespace StyleCop.Analyzers.Lightup
5+
{
6+
using System;
7+
using System.Collections.Immutable;
8+
using System.Linq;
9+
using System.Linq.Expressions;
10+
using System.Reflection;
11+
using System.Threading;
12+
using Microsoft.CodeAnalysis;
13+
14+
internal static class SemanticModelExtensions
15+
{
16+
private static readonly Func<SemanticModel, int, CancellationToken, ImmutableArray<IImportScopeWrapper>> GetImportScopesAccessor;
17+
18+
static SemanticModelExtensions()
19+
{
20+
CreateGetImportScopesAccessor(out var accessor, out var isSupported);
21+
GetImportScopesAccessor = accessor;
22+
SupportsGetImportScopes = isSupported;
23+
}
24+
25+
public static bool SupportsGetImportScopes { get; }
26+
27+
public static ImmutableArray<IImportScopeWrapper> GetImportScopes(this SemanticModel semanticModel, int position, CancellationToken cancellationToken = default)
28+
{
29+
return GetImportScopesAccessor(semanticModel, position, cancellationToken);
30+
}
31+
32+
private static void CreateGetImportScopesAccessor(
33+
out Func<SemanticModel, int, CancellationToken, ImmutableArray<IImportScopeWrapper>> accessor,
34+
out bool isSupported)
35+
{
36+
// Very error-prone code and potentially brittle regarding future API changes.
37+
// Wrapping everything in a try statement just in case something breaks later on.
38+
try
39+
{
40+
var semanticModelType = typeof(SemanticModel);
41+
42+
var codeAnalysisWorkspacesAssembly = semanticModelType.GetTypeInfo().Assembly;
43+
var nativeImportScopeType = codeAnalysisWorkspacesAssembly.GetType("Microsoft.CodeAnalysis.IImportScope");
44+
if (nativeImportScopeType is null)
45+
{
46+
accessor = FallbackAccessor;
47+
isSupported = false;
48+
}
49+
50+
var methods = semanticModelType.GetTypeInfo().GetDeclaredMethods("GetImportScopes").Where(IsCorrectGetImportScopesMethod);
51+
var method = methods.FirstOrDefault();
52+
if (method == null)
53+
{
54+
accessor = FallbackAccessor;
55+
isSupported = false;
56+
}
57+
58+
var importScopeWrapperFromObjectMethod = typeof(IImportScopeWrapper).GetTypeInfo().GetDeclaredMethod("FromObject");
59+
var nativeImportScopeArrayType = typeof(ImmutableArray<>).MakeGenericType(nativeImportScopeType);
60+
var nativeImportScopeArrayGetItemMethod = nativeImportScopeArrayType.GetTypeInfo().GetDeclaredMethod("get_Item");
61+
var wrapperImportScopeArrayType = typeof(ImmutableArray<IImportScopeWrapper>);
62+
var wrapperImportScopeArrayBuilderType = typeof(ImmutableArray<IImportScopeWrapper>.Builder);
63+
var wrapperImportScopeArrayBuilderAddMethod = wrapperImportScopeArrayBuilderType.GetTypeInfo().GetDeclaredMethod("Add");
64+
var wrapperImportScopeArrayBuilderToImmutableMethod = wrapperImportScopeArrayBuilderType.GetTypeInfo().GetDeclaredMethod("ToImmutable");
65+
var arrayCreateImportScopeBuilderMethod = typeof(ImmutableArray).GetTypeInfo().GetDeclaredMethods("CreateBuilder").Single(IsCorrectCreateBuilderMethod).MakeGenericMethod(typeof(IImportScopeWrapper));
66+
67+
var semanticModelParameter = Expression.Parameter(typeof(SemanticModel), "semanticModel");
68+
var positionParameter = Expression.Parameter(typeof(int), "position");
69+
var cancellationTokenParameter = Expression.Parameter(typeof(CancellationToken), "cancellationToken");
70+
71+
var nativeImportScopesVariable = Expression.Variable(nativeImportScopeArrayType, "nativeImportScopes");
72+
var nativeImportScopeVariable = Expression.Variable(nativeImportScopeType, "nativeImportScope");
73+
var countVariable = Expression.Variable(typeof(int), "count");
74+
var indexVariable = Expression.Variable(typeof(int), "index");
75+
var wrapperImportScopesBuilderVariable = Expression.Variable(wrapperImportScopeArrayBuilderType, "wrapperImportScopesBuilder");
76+
var wrapperImportScopesVariable = Expression.Variable(wrapperImportScopeArrayType, "wrapperImoprtScopes");
77+
var wrapperImportScopeVariable = Expression.Variable(typeof(IImportScopeWrapper), "wrapperImportScope");
78+
79+
var breakLabel = Expression.Label("break");
80+
var block = Expression.Block(
81+
new[] { nativeImportScopesVariable, countVariable, indexVariable, wrapperImportScopesBuilderVariable, wrapperImportScopesVariable },
82+
//// nativeImportScopes = semanticModel.GetImportScopes(position, cancellationToken);
83+
Expression.Assign(
84+
nativeImportScopesVariable,
85+
Expression.Call(
86+
semanticModelParameter,
87+
method,
88+
new[] { positionParameter, cancellationTokenParameter })),
89+
//// index = 0;
90+
Expression.Assign(indexVariable, Expression.Constant(0)),
91+
//// count = nativeImportScopes.Length;
92+
Expression.Assign(
93+
countVariable,
94+
Expression.Property(nativeImportScopesVariable, "Length")),
95+
//// wrapperImportScopesBuilder = ImmutableArray.CreateBuilder<IImportScopesWrapper>();
96+
Expression.Assign(
97+
wrapperImportScopesBuilderVariable,
98+
Expression.Call(null, arrayCreateImportScopeBuilderMethod)),
99+
Expression.Loop(
100+
Expression.Block(
101+
new[] { nativeImportScopeVariable, wrapperImportScopeVariable },
102+
//// if (index >= count) break;
103+
Expression.IfThen(
104+
Expression.GreaterThanOrEqual(indexVariable, countVariable),
105+
Expression.Break(breakLabel)),
106+
//// nativeImportScope = nativeImportScopes[index];
107+
Expression.Assign(
108+
nativeImportScopeVariable,
109+
Expression.Call(
110+
nativeImportScopesVariable,
111+
nativeImportScopeArrayGetItemMethod,
112+
indexVariable)),
113+
//// wrapperImportScope = IImportScopeWrapper.FromObject(nativeImportScope);
114+
Expression.Assign(
115+
wrapperImportScopeVariable,
116+
Expression.Call(
117+
null,
118+
importScopeWrapperFromObjectMethod,
119+
nativeImportScopeVariable)),
120+
//// wrapperImportScopesBuilder.Add(wrapperImportScope);
121+
Expression.Call(
122+
wrapperImportScopesBuilderVariable,
123+
wrapperImportScopeArrayBuilderAddMethod,
124+
new[] { wrapperImportScopeVariable }),
125+
//// index++;
126+
Expression.PostIncrementAssign(indexVariable)),
127+
breakLabel),
128+
//// wrapperImportScopes = wrapperImportScopesBuilder.ToImmutable();
129+
Expression.Assign(
130+
wrapperImportScopesVariable,
131+
Expression.Call(
132+
wrapperImportScopesBuilderVariable,
133+
wrapperImportScopeArrayBuilderToImmutableMethod)));
134+
135+
var lambda = Expression.Lambda<Func<SemanticModel, int, CancellationToken, ImmutableArray<IImportScopeWrapper>>>(
136+
block,
137+
new[] { semanticModelParameter, positionParameter, cancellationTokenParameter });
138+
139+
accessor = lambda.Compile();
140+
isSupported = true;
141+
}
142+
catch (Exception)
143+
{
144+
accessor = FallbackAccessor;
145+
isSupported = false;
146+
}
147+
148+
// NOTE: At time of implementation, there is no other overload of GetImportScopes, but check in case more are added later on
149+
static bool IsCorrectGetImportScopesMethod(MethodInfo method)
150+
{
151+
var parameters = method.GetParameters();
152+
if (parameters.Length != 2)
153+
{
154+
return false;
155+
}
156+
157+
return
158+
parameters[0].ParameterType == typeof(int) &&
159+
parameters[1].ParameterType == typeof(CancellationToken);
160+
}
161+
162+
static bool IsCorrectCreateBuilderMethod(MethodInfo method)
163+
{
164+
var parameters = method.GetParameters();
165+
return parameters.Length == 0;
166+
}
167+
168+
static ImmutableArray<IImportScopeWrapper> FallbackAccessor(SemanticModel semanticModel, int position, CancellationToken cancellationToken)
169+
{
170+
if (semanticModel == null)
171+
{
172+
// Unlike an extension method which would throw ArgumentNullException here, the light-up
173+
// behavior needs to match the behavior of the underlying method.
174+
throw new NullReferenceException();
175+
}
176+
177+
return ImmutableArray<IImportScopeWrapper>.Empty;
178+
}
179+
}
180+
}
181+
}

StyleCop.Analyzers/StyleCop.Analyzers/Lightup/WrapperHelper.cs

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ static WrapperHelper()
2222

2323
builder.Add(typeof(AnalyzerConfigOptionsProviderWrapper), codeAnalysisAssembly.GetType(AnalyzerConfigOptionsProviderWrapper.WrappedTypeName));
2424
builder.Add(typeof(AnalyzerConfigOptionsWrapper), codeAnalysisAssembly.GetType(AnalyzerConfigOptionsWrapper.WrappedTypeName));
25+
builder.Add(typeof(IImportScopeWrapper), codeAnalysisAssembly.GetType(IImportScopeWrapper.WrappedTypeName));
2526

2627
WrappedTypes = builder.ToImmutable();
2728
}

StyleCop.Analyzers/StyleCop.Analyzers/MaintainabilityRules/SA1404CodeAnalysisSuppressionMustHaveJustification.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public void HandleAttributeNode(SyntaxNodeAnalysisContext context)
9696
var attribute = (AttributeSyntax)context.Node;
9797

9898
// Return fast if the name doesn't match and the file doesn't contain any using alias directives
99-
if (!attribute.SyntaxTree.ContainsUsingAlias(this.usingAliasCache))
99+
if (!attribute.SyntaxTree.ContainsUsingAlias(this.usingAliasCache, context.SemanticModel, context.CancellationToken))
100100
{
101101
if (!(attribute.Name is SimpleNameSyntax simpleNameSyntax))
102102
{

StyleCop.Analyzers/StyleCop.Analyzers/ReadabilityRules/SA1121UseBuiltInTypeAlias.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ public void HandleIdentifierNameSyntax(SyntaxNodeAnalysisContext context, StyleC
211211
// Most source files will not have any using alias directives. Then we don't have to use semantics
212212
// if the identifier name doesn't match the name of a special type
213213
if (settings.ReadabilityRules.AllowBuiltInTypeAliases
214-
|| !identifierNameSyntax.SyntaxTree.ContainsUsingAlias(this.usingAliasCache))
214+
|| !identifierNameSyntax.SyntaxTree.ContainsUsingAlias(this.usingAliasCache, context.SemanticModel, context.CancellationToken))
215215
{
216216
switch (identifierNameSyntax.Identifier.ValueText)
217217
{

0 commit comments

Comments
 (0)