diff --git a/test/Microsoft.ML.CodeAnalyzer.Tests/Code/BestFriendOnPublicDeclarationTest.cs b/test/Microsoft.ML.CodeAnalyzer.Tests/Code/BestFriendOnPublicDeclarationTest.cs new file mode 100644 index 0000000000..ddd8fb5d72 --- /dev/null +++ b/test/Microsoft.ML.CodeAnalyzer.Tests/Code/BestFriendOnPublicDeclarationTest.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.ML.CodeAnalyzer.Tests.Helpers; +using Xunit; + +namespace Microsoft.ML.InternalCodeAnalyzer.Tests +{ + public sealed class BestFriendOnPublicDeclarationTest : DiagnosticVerifier + { + private readonly Lazy SourceAttribute = TestUtils.LazySource("BestFriendAttribute.cs"); + private readonly Lazy SourceDeclaration = TestUtils.LazySource("BestFriendOnPublicDeclaration.cs"); + + [Fact] + public void BestFriendOnPublicDeclaration() + { + Solution solution = null; + var projA = CreateProject("ProjectA", ref solution, SourceDeclaration.Value, SourceAttribute.Value); + + var analyzer = new BestFriendOnPublicDeclarationsAnalyzer(); + + var refs = new List { + RefFromType(), RefFromType(), + MetadataReference.CreateFromFile(Assembly.Load("netstandard, Version=2.0.0.0").Location), + MetadataReference.CreateFromFile(Assembly.Load("System.Runtime, Version=0.0.0.0").Location) + }; + + var comp = projA.GetCompilationAsync().Result.WithReferences(refs.ToArray()); + var compilationWithAnalyzers = comp.WithAnalyzers(ImmutableArray.Create((DiagnosticAnalyzer)analyzer)); + var allDiags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result; + + var projectTrees = new HashSet(projA.Documents.Select(r => r.GetSyntaxTreeAsync().Result)); + var diags = allDiags + .Where(d => d.Location == Location.None || d.Location.IsInMetadata || projectTrees.Contains(d.Location.SourceTree)) + .OrderBy(d => d.Location.SourceSpan.Start).ToArray(); + + var diag = analyzer.SupportedDiagnostics[0]; + var expected = new DiagnosticResult[] { + diag.CreateDiagnosticResult(8, 6, "PublicClass"), + diag.CreateDiagnosticResult(11, 10, "PublicField"), + diag.CreateDiagnosticResult(14, 10, "PublicProperty"), + diag.CreateDiagnosticResult(20, 10, "PublicMethod"), + diag.CreateDiagnosticResult(26, 10, "PublicDelegate"), + diag.CreateDiagnosticResult(29, 10, "PublicClass"), + diag.CreateDiagnosticResult(35, 6, "PublicStruct"), + diag.CreateDiagnosticResult(40, 6, "PublicEnum"), + diag.CreateDiagnosticResult(47, 6, "PublicInterface"), + diag.CreateDiagnosticResult(102, 10, "PublicMethod") + }; + + VerifyDiagnosticResults(diags, analyzer, expected); + } + } +} + diff --git a/test/Microsoft.ML.CodeAnalyzer.Tests/Resources/BestFriendOnPublicDeclaration.cs b/test/Microsoft.ML.CodeAnalyzer.Tests/Resources/BestFriendOnPublicDeclaration.cs new file mode 100644 index 0000000000..1515f21e1f --- /dev/null +++ b/test/Microsoft.ML.CodeAnalyzer.Tests/Resources/BestFriendOnPublicDeclaration.cs @@ -0,0 +1,107 @@ +using System; +using Microsoft.ML; + +namespace TestNamespace +{ + // all of the best friend declaration should fail the diagnostic + + [BestFriend] + public class PublicClass + { + [BestFriend] + public int PublicField; + + [BestFriend] + public string PublicProperty + { + get { return string.Empty; } + } + + [BestFriend] + public bool PublicMethod() + { + return true; + } + + [BestFriend] + public delegate string PublicDelegate(); + + [BestFriend] + public PublicClass() + { + } + } + + [BestFriend] + public struct PublicStruct + { + } + + [BestFriend] + public enum PublicEnum + { + EnumValue1, + EnumValue2 + } + + [BestFriend] + public interface PublicInterface + { + } + + // these should work + + [BestFriend] + internal class InternalClass + { + [BestFriend] + internal int InternalField; + + [BestFriend] + internal string InternalProperty + { + get { return string.Empty; } + } + + [BestFriend] + internal bool InternalMethod() + { + return true; + } + + [BestFriend] + internal delegate string InternalDelegate(); + + [BestFriend] + internal InternalClass() + { + } + } + + [BestFriend] + internal struct InternalStruct + { + } + + [BestFriend] + internal enum InternalEnum + { + EnumValue1, + EnumValue2 + } + + [BestFriend] + internal interface InternalInterface + { + } + + // this should fail the diagnostic + // a repro for https://github.com/dotnet/machinelearning/pull/2434#discussion_r254770946 + internal class InternalClassWithPublicMember + { + [BestFriend] + public void PublicMethod() + { + } + } +} diff --git a/tools-local/Microsoft.ML.InternalCodeAnalyzer/BestFriendOnPublicDeclarationsAnalyzer.cs b/tools-local/Microsoft.ML.InternalCodeAnalyzer/BestFriendOnPublicDeclarationsAnalyzer.cs new file mode 100644 index 0000000000..658e957c16 --- /dev/null +++ b/tools-local/Microsoft.ML.InternalCodeAnalyzer/BestFriendOnPublicDeclarationsAnalyzer.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.ML.InternalCodeAnalyzer +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class BestFriendOnPublicDeclarationsAnalyzer : DiagnosticAnalyzer + { + private const string Category = "Access"; + internal const string DiagnosticId = "MSML_BestFriendOnPublicDeclaration"; + + private const string Title = "Public declarations should not have " + AttributeName + " attribute."; + private const string Format = "The " + AttributeName + " should not be applied to publicly visible members."; + + private const string Description = + "The " + AttributeName + " attribute is not valid on public identifiers."; + + private static DiagnosticDescriptor Rule = + new DiagnosticDescriptor(DiagnosticId, Title, Format, Category, + DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); + + private const string AttributeName = "Microsoft.ML.BestFriendAttribute"; + + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterCompilationStartAction(CompilationStart); + } + + private void CompilationStart(CompilationStartAnalysisContext context) + { + var list = new List { AttributeName, "Microsoft.ML.Internal.CpuMath.Core.BestFriendAttribute" }; + + foreach (var attributeName in list) + { + var attribute = context.Compilation.GetTypeByMetadataName(attributeName); + + if (attribute == null) + continue; + + context.RegisterSymbolAction(c => AnalyzeCore(c, attribute), SymbolKind.NamedType, SymbolKind.Method, SymbolKind.Field, SymbolKind.Property); + } + } + + private void AnalyzeCore(SymbolAnalysisContext context, INamedTypeSymbol attributeType) + { + if (context.Symbol.DeclaredAccessibility != Accessibility.Public) + return; + + var attribute = context.Symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass == attributeType); + if (attribute == null) + return; + + var diagnostic = Diagnostic.Create(Rule, attribute.ApplicationSyntaxReference.GetSyntax().GetLocation(), context.Symbol.Name); + context.ReportDiagnostic(diagnostic); + } + } +} \ No newline at end of file