diff --git a/src/Compilers/CSharp/Test/Semantic/Diagnostics/DiagnosticAnalyzerTests.cs b/src/Compilers/CSharp/Test/Semantic/Diagnostics/DiagnosticAnalyzerTests.cs index bcec4f86fe0e139e552bc621cbf3f65badd4b0ad..b8dc4e4df5ace57a17e6df22463e8f39703adc6d 100644 --- a/src/Compilers/CSharp/Test/Semantic/Diagnostics/DiagnosticAnalyzerTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Diagnostics/DiagnosticAnalyzerTests.cs @@ -1491,5 +1491,19 @@ private static void AddExpectedDiagnostic(ArrayBuilder bu var diagnostic = Diagnostic(diagnosticId, squiggledText).WithArguments(arguments).WithLocation(line, column); builder.Add(diagnostic); } + + [Fact] + public void TestEnsureNoMergedNamespaceSymbolAnalyzer() + { + var source = @"namespace N1.N2 { }"; + + var metadataReference = CreateCompilationWithMscorlib(source).ToMetadataReference(); + var compilation = CreateCompilationWithMscorlib(source, new[] { metadataReference }); + compilation.VerifyDiagnostics(); + + // Analyzer reports a diagnostic if it receives a merged namespace symbol across assemblies in compilation. + var analyzers = new DiagnosticAnalyzer[] { new EnsureNoMergedNamespaceSymbolAnalyzer() }; + compilation.VerifyAnalyzerDiagnostics(analyzers); + } } } diff --git a/src/Compilers/Core/AnalyzerDriver/DeclarationComputer.cs b/src/Compilers/Core/AnalyzerDriver/DeclarationComputer.cs index c0944b2f97e283df90b9481ffda2098bf8a60615..554f60d62c3c4e70e780dbe94bd7de4d4dc1fe78 100644 --- a/src/Compilers/Core/AnalyzerDriver/DeclarationComputer.cs +++ b/src/Compilers/Core/AnalyzerDriver/DeclarationComputer.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Linq; using System.Threading; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis { @@ -12,7 +13,7 @@ internal class DeclarationComputer { internal static DeclarationInfo GetDeclarationInfo(SemanticModel model, SyntaxNode node, bool getSymbol, IEnumerable executableCodeBlocks, CancellationToken cancellationToken) { - var declaredSymbol = getSymbol ? model.GetDeclaredSymbol(node, cancellationToken) : null; + var declaredSymbol = GetDeclaredSymbol(model, node, getSymbol, cancellationToken); var codeBlocks = executableCodeBlocks?.Where(c => c != null).AsImmutableOrEmpty() ?? ImmutableArray.Empty; return new DeclarationInfo(node, codeBlocks, declaredSymbol); } @@ -24,14 +25,39 @@ internal static DeclarationInfo GetDeclarationInfo(SemanticModel model, SyntaxNo internal static DeclarationInfo GetDeclarationInfo(SemanticModel model, SyntaxNode node, bool getSymbol, SyntaxNode executableCodeBlock, CancellationToken cancellationToken) { - var declaredSymbol = getSymbol ? model.GetDeclaredSymbol(node, cancellationToken) : null; - var codeBlock = executableCodeBlock == null ? ImmutableArray.Empty : ImmutableArray.Create(executableCodeBlock); - return new DeclarationInfo(node, codeBlock, declaredSymbol); + return GetDeclarationInfo(model, node, getSymbol, SpecializedCollections.SingletonEnumerable(executableCodeBlock), cancellationToken); } internal static DeclarationInfo GetDeclarationInfo(SemanticModel model, SyntaxNode node, bool getSymbol, CancellationToken cancellationToken, params SyntaxNode[] executableCodeBlocks) { return GetDeclarationInfo(model, node, getSymbol, executableCodeBlocks.AsEnumerable(), cancellationToken); } + + private static ISymbol GetDeclaredSymbol(SemanticModel model, SyntaxNode node, bool getSymbol, CancellationToken cancellationToken) + { + if (!getSymbol) + { + return null; + } + + var declaredSymbol = model.GetDeclaredSymbol(node, cancellationToken); + + // For namespace declarations, GetDeclaredSymbol returns a compilation scoped namespace symbol, + // which includes declarations across the compilation, including those in referenced assemblies. + // However, we are only interested in the namespace symbol scoped to the compilation's source assembly. + var namespaceSymbol = declaredSymbol as INamespaceSymbol; + if (namespaceSymbol != null && namespaceSymbol.ConstituentNamespaces.Length > 1) + { + var assemblyToScope = model.Compilation.Assembly; + var assemblyScopedNamespaceSymbol = namespaceSymbol.ConstituentNamespaces.FirstOrDefault(ns => ns.ContainingAssembly == assemblyToScope); + if (assemblyScopedNamespaceSymbol != null) + { + Debug.Assert(assemblyScopedNamespaceSymbol.ConstituentNamespaces.Length == 1); + declaredSymbol = assemblyScopedNamespaceSymbol; + } + } + + return declaredSymbol; + } } } diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalysisState.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalysisState.cs index 1af2f9da590f486265bf38b462110eb6c3e7a7db..a486eded020876324b0ca1a04ebc940701c38e1c 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalysisState.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalysisState.cs @@ -182,7 +182,8 @@ private static ImmutableArray CreateCompilationEventsForTree(I { var builder = ImmutableArray.CreateBuilder(); foreach (var symbol in declaredSymbols) - { + { + Debug.Assert(symbol.ContainingAssembly == compilation.Assembly); builder.Add(new SymbolDeclaredCompilationEvent(compilation, symbol)); } diff --git a/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb b/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb index 15272d85a20c7a8cc78a50dc1e4bee2a2232b488..11e385c573d333fc100b68bb59ce8a3d4ce94570 100644 --- a/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb +++ b/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb @@ -1850,5 +1850,40 @@ class MyClass Assert.Equal(HiddenDiagnosticsCompilationAnalyzer.Descriptor.Id, diagnostics.Single().Id) End Using End Sub + + + Public Sub TestEnsureNoMergedNamespaceSymbolAnalyzer() + Dim test = + + + namespace N1.N2 { class C1 { } } + + + + BaseAssembly + + namespace N1.N2 { class C2 { } } + + + + + Using workspace = TestWorkspaceFactory.CreateWorkspace(test) + Dim project = workspace.CurrentSolution.Projects.Single(Function(p As Project) p.Name = "MainAssembly") + + ' Analyzer reports a diagnostic if it receives a merged namespace symbol across assemblies in compilation. + Dim analyzer = New EnsureNoMergedNamespaceSymbolAnalyzer() + Dim analyzerReference = New AnalyzerImageReference(ImmutableArray.Create(Of DiagnosticAnalyzer)(analyzer)) + project = project.AddAnalyzerReference(analyzerReference) + + Dim diagnosticService = New TestDiagnosticAnalyzerService() + + Dim descriptorsMap = diagnosticService.GetDiagnosticDescriptors(project) + Assert.Equal(1, descriptorsMap.Count) + + Dim incrementalAnalyzer = diagnosticService.CreateIncrementalAnalyzer(workspace) + Dim diagnostics = diagnosticService.GetDiagnosticsAsync(project.Solution, project.Id).WaitAndGetResult(CancellationToken.None) + Assert.Equal(0, diagnostics.Count()) + End Using + End Sub End Class End Namespace diff --git a/src/Test/Utilities/Desktop/CommonDiagnosticAnalyzers.cs b/src/Test/Utilities/Desktop/CommonDiagnosticAnalyzers.cs index e834ec1c05e877338c1235a31d498c8c29f8d19f..0b13373e9d2b4bb7264989c74701031ce79211e8 100644 --- a/src/Test/Utilities/Desktop/CommonDiagnosticAnalyzers.cs +++ b/src/Test/Utilities/Desktop/CommonDiagnosticAnalyzers.cs @@ -365,6 +365,38 @@ public sealed class AnalyzerWithNoActions : DiagnosticAnalyzer public override void Initialize(AnalysisContext context) { } } + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public sealed class EnsureNoMergedNamespaceSymbolAnalyzer : DiagnosticAnalyzer + { + public const string DiagnosticId = "DiagnosticId"; + public const string Title = "Title"; + public const string Message = "Message"; + public const string Category = "Category"; + public const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; + + internal static DiagnosticDescriptor Rule = + new DiagnosticDescriptor(DiagnosticId, Title, Message, + Category, Severity, isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Namespace); + } + + private void AnalyzeSymbol(SymbolAnalysisContext context) + { + // Ensure we are not invoked for merged namespace symbol, but instead for constituent namespace scoped to the source assembly. + var ns = (INamespaceSymbol)context.Symbol; + if (ns.ContainingAssembly != context.Compilation.Assembly || ns.ConstituentNamespaces.Length > 1) + { + context.ReportDiagnostic(Diagnostic.Create(Rule, ns.Locations[0])); + } + } + } + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] public sealed class AnalyzerWithNoSupportedDiagnostics : DiagnosticAnalyzer {