diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/ReadabilityRules/SA1135CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/ReadabilityRules/SA1135CodeFixProvider.cs index 26d123983..b9dc63276 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/ReadabilityRules/SA1135CodeFixProvider.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/ReadabilityRules/SA1135CodeFixProvider.cs @@ -65,7 +65,7 @@ private static async Task GetTransformedDocumentAsync(Document documen private static SyntaxNode GetReplacementNode(SemanticModel semanticModel, UsingDirectiveSyntax node, CancellationToken cancellationToken) { SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(node.Name, cancellationToken); - var symbolNameSyntax = SyntaxFactory.ParseName(symbolInfo.Symbol.ToQualifiedString()); + var symbolNameSyntax = SyntaxFactory.ParseName(symbolInfo.Symbol.ToQualifiedString(node.Name)); var newName = GetReplacementName(symbolNameSyntax, node.Name); return node.WithName((NameSyntax)newName); diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp7/ReadabilityRules/SA1135CSharp7UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp7/ReadabilityRules/SA1135CSharp7UnitTests.cs index 82beb5e4d..639ce7a5a 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp7/ReadabilityRules/SA1135CSharp7UnitTests.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp7/ReadabilityRules/SA1135CSharp7UnitTests.cs @@ -3,9 +3,41 @@ namespace StyleCop.Analyzers.Test.CSharp7.ReadabilityRules { + using System.Threading; + using System.Threading.Tasks; + using Microsoft.CodeAnalysis.Testing; using StyleCop.Analyzers.Test.ReadabilityRules; + using Xunit; + using static StyleCop.Analyzers.Test.Verifiers.StyleCopCodeFixVerifier< + StyleCop.Analyzers.ReadabilityRules.SA1135UsingDirectivesMustBeQualified, + StyleCop.Analyzers.ReadabilityRules.SA1135CodeFixProvider>; public class SA1135CSharp7UnitTests : SA1135UnitTests { + [Fact] + [WorkItem(2879, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/2879")] + public async Task TestTupleTypeInUsingAliasAsync() + { + var testCode = @" +namespace TestNamespace +{ + using Example = System.Collections.Generic.List<(int, int)>; +} +"; + await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + [WorkItem(2879, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/2879")] + public async Task TestTupleTypeWithNamedElementsInUsingAliasAsync() + { + var testCode = @" +namespace TestNamespace +{ + using Example = System.Collections.Generic.List<(int x, int y)>; +} +"; + await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } } } diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/ReadabilityRules/SA1135UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/ReadabilityRules/SA1135UnitTests.cs index 59bfdbea0..99bfc9f53 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/ReadabilityRules/SA1135UnitTests.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/ReadabilityRules/SA1135UnitTests.cs @@ -5,6 +5,7 @@ namespace StyleCop.Analyzers.Test.ReadabilityRules { using System.Threading; using System.Threading.Tasks; + using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Testing; using StyleCop.Analyzers.ReadabilityRules; using Xunit; @@ -319,5 +320,100 @@ namespace MyNamespace { await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); } + + [Fact] + [WorkItem(2879, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/2879")] + public async Task TestOmittedTypeInGenericAsync() + { + var testCode = @" +namespace TestNamespace +{ + using Example = System.Collections.Generic.List<>; +} +"; + + var expected = new DiagnosticResult("CS7003", DiagnosticSeverity.Error).WithLocation(4, 48).WithMessage("Unexpected use of an unbound generic name"); + await VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + [WorkItem(2879, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/2879")] + public async Task TestNullableTypeInGenericAsync() + { + var testCode = @" +namespace TestNamespace +{ + using Example = System.Collections.Generic.List; +} +"; + + await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + [WorkItem(2879, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/2879")] + public async Task TestGlobalQualifiedTypeInGenericAsync() + { + var testCode = @" +namespace TestNamespace +{ + using Example = System.Collections.Generic.List; +} +"; + + await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + [WorkItem(2879, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/2879")] + public async Task TestTypeInGlobalNamespaceAsync() + { + var testCode = @" +namespace TestNamespace +{ + using Example = MyClass; +} + +class MyClass +{ +} +"; + + await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + [WorkItem(2879, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/2879")] + public async Task TestAliasTypeNestedInGenericAsync() + { + var testCode = @" +namespace TestNamespace +{ + using Example = System.Collections.Immutable.ImmutableDictionary.Builder; +} +"; + await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + [WorkItem(2879, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/2879")] + public async Task TestValueTupleInUsingAliasAsync() + { + var testCode = @" +namespace System +{ + using Example = System.Collections.Generic.List>; +} +"; + var fixedCode = @" +namespace System +{ + using Example = System.Collections.Generic.List>; +} +"; + + var expected = Diagnostic(SA1135UsingDirectivesMustBeQualified.DescriptorType).WithLocation(4, 5).WithArguments("System.Collections.Generic.List>"); + await VerifyCSharpFixAsync(testCode, expected, fixedCode, CancellationToken.None).ConfigureAwait(false); + } } } diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/Helpers/SymbolNameHelpers.cs b/StyleCop.Analyzers/StyleCop.Analyzers/Helpers/SymbolNameHelpers.cs index 29ecf347e..0f4c3c488 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/Helpers/SymbolNameHelpers.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/Helpers/SymbolNameHelpers.cs @@ -5,6 +5,7 @@ namespace StyleCop.Analyzers.Helpers { using System.Text; using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using StyleCop.Analyzers.Lightup; @@ -17,83 +18,138 @@ internal static class SymbolNameHelpers private const string GenericTypeParametersClose = ">"; private const string GenericSeparator = ", "; + private const string TupleTypeOpen = "("; + private const string TupleTypeClose = ")"; + private const string TupleElementSeparator = ", "; + /// /// Generates the qualified name for the given symbol. /// /// The symbol to use. + /// The syntax node which resolves to the symbol. /// The generated qualified name. - public static string ToQualifiedString(this ISymbol symbol) + public static string ToQualifiedString(this ISymbol symbol, NameSyntax name) { var builder = ObjectPools.StringBuilderPool.Allocate(); + AppendQualifiedSymbolName(builder, symbol, name); + return ObjectPools.StringBuilderPool.ReturnAndFree(builder); + } - if (symbol is INamedTypeSymbol namedTypeSymbol) + private static bool AppendQualifiedSymbolName(StringBuilder builder, ISymbol symbol, TypeSyntax type) + { + switch (symbol.Kind) { - if (SpecialTypeHelper.TryGetPredefinedType(namedTypeSymbol.SpecialType, out PredefinedTypeSyntax specialTypeSyntax)) + case SymbolKind.ArrayType: + var arraySymbol = (IArrayTypeSymbol)symbol; + AppendQualifiedSymbolName(builder, arraySymbol.ElementType, (type as ArrayTypeSyntax)?.ElementType); + builder + .Append("[") + .Append(',', arraySymbol.Rank - 1) + .Append("]"); + return true; + + case SymbolKind.Namespace: + var namespaceSymbol = (INamespaceSymbol)symbol; + if (namespaceSymbol.IsGlobalNamespace) { - return specialTypeSyntax.ToFullString(); + return false; } - if (namedTypeSymbol.IsTupleType()) + builder.Append(namespaceSymbol.ToDisplayString()); + return true; + + case SymbolKind.NamedType: + var namedTypeSymbol = (INamedTypeSymbol)symbol; + if (SpecialTypeHelper.TryGetPredefinedType(namedTypeSymbol.SpecialType, out var specialTypeSyntax)) { - namedTypeSymbol = namedTypeSymbol.TupleUnderlyingType(); + builder.Append(specialTypeSyntax.ToFullString()); + return true; } + else if (namedTypeSymbol.IsTupleType()) + { + if (TupleTypeSyntaxWrapper.IsInstance(type)) + { + var tupleType = (TupleTypeSyntaxWrapper)type; + + builder.Append(TupleTypeOpen); + var elements = namedTypeSymbol.TupleElements(); + for (int i = 0; i < elements.Length; i++) + { + var field = elements[i]; + var fieldType = tupleType.Elements.Count > i ? tupleType.Elements[i] : default; - AppendQualifiedSymbolName(builder, namedTypeSymbol); + if (i > 0) + { + builder.Append(TupleElementSeparator); + } + + AppendQualifiedSymbolName(builder, field.Type, fieldType.Type); + if (field != field.CorrespondingTupleField()) + { + builder.Append(" ").Append(field.Name); + } + } - if (namedTypeSymbol.IsGenericType) + builder.Append(TupleTypeClose); + return true; + } + else + { + return AppendQualifiedSymbolName(builder, namedTypeSymbol.TupleUnderlyingType(), type); + } + } + else if (namedTypeSymbol.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + AppendQualifiedSymbolName(builder, namedTypeSymbol.TypeArguments[0], (type as NullableTypeSyntax)?.ElementType); + builder.Append("?"); + return true; + } + else { - builder.Append(GenericTypeParametersOpen); + if (AppendQualifiedSymbolName(builder, symbol.ContainingSymbol, (type as QualifiedNameSyntax)?.Left)) + { + builder.Append("."); + } - foreach (var typeArgument in namedTypeSymbol.TypeArguments) + builder.Append(symbol.Name); + if (namedTypeSymbol.IsGenericType && !namedTypeSymbol.TypeArguments.IsEmpty) { - if (typeArgument is INamedTypeSymbol namedTypeArgument && typeArgument.IsTupleType()) - { - builder.Append(namedTypeArgument.TupleUnderlyingType().ToQualifiedString()); - } - else + builder.Append(GenericTypeParametersOpen); + var arguments = namedTypeSymbol.TypeArguments; + var argumentTypes = type is QualifiedNameSyntax qualifiedName + ? (qualifiedName.Right as GenericNameSyntax)?.TypeArgumentList + : (type as GenericNameSyntax)?.TypeArgumentList; + + for (int i = 0; i < arguments.Length; i++) { - builder.Append(typeArgument.ToQualifiedString()); + var argument = arguments[i]; + var argumentType = argumentTypes != null && argumentTypes.Arguments.Count > i ? argumentTypes.Arguments[i] : null; + + if (i > 0) + { + builder.Append(GenericSeparator); + } + + if (!argumentType.IsKind(SyntaxKind.OmittedTypeArgument)) + { + AppendQualifiedSymbolName(builder, argument, argumentType); + } } - builder.Append(GenericSeparator); + builder.Append(GenericTypeParametersClose); } - builder.Remove(builder.Length - GenericSeparator.Length, GenericSeparator.Length); - builder.Append(GenericTypeParametersClose); + return true; } - } - else - { - AppendQualifiedSymbolName(builder, symbol); - } - - return ObjectPools.StringBuilderPool.ReturnAndFree(builder); - } - - private static void AppendQualifiedSymbolName(StringBuilder builder, ISymbol symbol) - { - switch (symbol) - { - case IArrayTypeSymbol arraySymbol: - builder - .Append(arraySymbol.ElementType.ContainingNamespace.ToDisplayString()) - .Append(".") - .Append(arraySymbol.ElementType.Name) - .Append("[") - .Append(',', arraySymbol.Rank - 1) - .Append("]"); - break; default: - if (!symbol.ContainingNamespace.IsGlobalNamespace) + if (symbol != null) { - builder - .Append(symbol.ContainingNamespace.ToDisplayString()) - .Append("."); + builder.Append(symbol.Name); + return true; } - builder.Append(symbol.Name); - break; + return false; } } } diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/Lightup/CSharp7.md b/StyleCop.Analyzers/StyleCop.Analyzers/Lightup/CSharp7.md index 3ef479ad4..fc6245b21 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/Lightup/CSharp7.md +++ b/StyleCop.Analyzers/StyleCop.Analyzers/Lightup/CSharp7.md @@ -61,7 +61,7 @@ See [dotnet/roslyn@c2af711](https://github.com/dotnet/roslyn/commit/c2af71127234 * [ ] `Microsoft.CodeAnalysis.IArrayTypeSymbol.Sizes.get -> System.Collections.Immutable.ImmutableArray` * [ ] `Microsoft.CodeAnalysis.IDiscardSymbol` * [ ] `Microsoft.CodeAnalysis.IDiscardSymbol.Type.get -> Microsoft.CodeAnalysis.ITypeSymbol` -* [ ] `Microsoft.CodeAnalysis.IFieldSymbol.CorrespondingTupleField.get -> Microsoft.CodeAnalysis.IFieldSymbol` +* [x] `Microsoft.CodeAnalysis.IFieldSymbol.CorrespondingTupleField.get -> Microsoft.CodeAnalysis.IFieldSymbol` * [ ] `Microsoft.CodeAnalysis.ILocalSymbol.IsRef.get -> bool` * [ ] `Microsoft.CodeAnalysis.IMethodSymbol.RefCustomModifiers.get -> System.Collections.Immutable.ImmutableArray` * [ ] `Microsoft.CodeAnalysis.IMethodSymbol.ReturnsByRef.get -> bool` diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/Lightup/IFieldSymbolExtensions.cs b/StyleCop.Analyzers/StyleCop.Analyzers/Lightup/IFieldSymbolExtensions.cs new file mode 100644 index 000000000..56dbb4df8 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers/Lightup/IFieldSymbolExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +namespace StyleCop.Analyzers.Lightup +{ + using System; + using Microsoft.CodeAnalysis; + + internal static class IFieldSymbolExtensions + { + private static readonly Func CorrespondingTupleFieldAccessor; + + static IFieldSymbolExtensions() + { + CorrespondingTupleFieldAccessor = LightupHelpers.CreateSyntaxPropertyAccessor(typeof(IFieldSymbol), nameof(CorrespondingTupleField)); + } + + public static IFieldSymbol CorrespondingTupleField(this IFieldSymbol symbol) + { + return CorrespondingTupleFieldAccessor(symbol); + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/ReadabilityRules/SA1135UsingDirectivesMustBeQualified.cs b/StyleCop.Analyzers/StyleCop.Analyzers/ReadabilityRules/SA1135UsingDirectivesMustBeQualified.cs index 1285ade4e..cddfef1ed 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/ReadabilityRules/SA1135UsingDirectivesMustBeQualified.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/ReadabilityRules/SA1135UsingDirectivesMustBeQualified.cs @@ -3,8 +3,8 @@ namespace StyleCop.Analyzers.ReadabilityRules { - using System; using System.Collections.Immutable; + using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -98,7 +98,7 @@ private static void CheckUsingDeclaration(SyntaxNodeAnalysisContext context, Usi symbol = typeSymbol.TupleUnderlyingType(); } - string symbolString = symbol.ToQualifiedString(); + string symbolString = symbol.ToQualifiedString(usingDirective.Name); string usingString = UsingDirectiveSyntaxToCanonicalString(usingDirective); if ((symbolString != usingString) && !usingDirective.StartsWithAlias(context.SemanticModel, context.CancellationToken)) @@ -124,37 +124,101 @@ private static void CheckUsingDeclaration(SyntaxNodeAnalysisContext context, Usi private static string UsingDirectiveSyntaxToCanonicalString(UsingDirectiveSyntax usingDirective) { var builder = StringBuilderPool.Allocate(); - var insideArrayDeclaration = false; + AppendCanonicalString(builder, usingDirective.Name); + return StringBuilderPool.ReturnAndFree(builder); + } - // NOTE: this does not cover embedded comments. It is very unlikely that comments are present - // within a multiline using statement and handling them requires a lot more effort (and keeping of state). - foreach (var c in usingDirective.Name.ToString()) + private static bool AppendCanonicalString(StringBuilder builder, TypeSyntax type) + { + switch (type) { - switch (c) + case AliasQualifiedNameSyntax aliasQualifiedName: + AppendCanonicalString(builder, aliasQualifiedName.Alias); + builder.Append("::"); + AppendCanonicalString(builder, aliasQualifiedName.Name); + return true; + + case IdentifierNameSyntax identifierName: + builder.Append(identifierName.Identifier.Text); + return true; + + case GenericNameSyntax genericName: + builder.Append(genericName.Identifier.Text); + builder.Append("<"); + + var typeArgumentList = genericName.TypeArgumentList; + for (int i = 0; i < typeArgumentList.Arguments.Count; i++) { - case ' ': - case '\t': - case '\r': - case '\n': - break; - case '[': - insideArrayDeclaration = true; - builder.Append(c); - break; - case ']': - insideArrayDeclaration = false; - builder.Append(c); - break; - case ',': - builder.Append(insideArrayDeclaration ? "," : ", "); - break; - default: - builder.Append(c); - break; + if (i > 0) + { + builder.Append(", "); + } + + AppendCanonicalString(builder, typeArgumentList.Arguments[i]); } - } - return StringBuilderPool.ReturnAndFree(builder); + builder.Append(">"); + return true; + + case QualifiedNameSyntax qualifiedName: + AppendCanonicalString(builder, qualifiedName.Left); + builder.Append("."); + AppendCanonicalString(builder, qualifiedName.Right); + return true; + + case PredefinedTypeSyntax predefinedType: + builder.Append(predefinedType.Keyword.Text); + return true; + + case ArrayTypeSyntax arrayType: + AppendCanonicalString(builder, arrayType.ElementType); + foreach (var rankSpecifier in arrayType.RankSpecifiers) + { + builder.Append("["); + builder.Append(',', rankSpecifier.Rank - 1); + builder.Append("]"); + } + + return true; + + case NullableTypeSyntax nullableType: + AppendCanonicalString(builder, nullableType.ElementType); + builder.Append("?"); + return true; + + case OmittedTypeArgumentSyntax _: + return false; + + default: + if (TupleTypeSyntaxWrapper.IsInstance(type)) + { + var tupleType = (TupleTypeSyntaxWrapper)type; + + builder.Append("("); + + var elements = tupleType.Elements; + for (int i = 0; i < elements.Count; i++) + { + if (i > 0) + { + builder.Append(", "); + } + + AppendCanonicalString(builder, elements[i].Type); + if (!elements[i].Identifier.IsKind(SyntaxKind.None)) + { + builder.Append(" ").Append(elements[i].Identifier.Text); + } + } + + builder.Append(")"); + return true; + } + else + { + return false; + } + } } } }