diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessarySuppressions/CSharpRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessarySuppressions/CSharpRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs index 515e79509839d3db6b430ccc98952f0ebc9e84b9..76ff42f3659b2e540adfd009d233a313e94d03ef 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessarySuppressions/CSharpRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessarySuppressions/CSharpRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs @@ -13,12 +13,13 @@ namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessarySuppressions { [DiagnosticAnalyzer(LanguageNames.CSharp)] - internal sealed class CSharpRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer - : AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer + internal sealed class CSharpRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer + : AbstractRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer { protected override string CompilerErrorCodePrefix => "CS"; protected override int CompilerErrorCodeDigitCount => 4; protected override ISyntaxFacts SyntaxFacts => CSharpSyntaxFacts.Instance; + protected override ISemanticFacts SemanticFacts => CSharpSemanticFacts.Instance; protected override (Assembly assembly, string typeName) GetCompilerDiagnosticAnalyzerInfo() => (typeof(SyntaxKind).Assembly, CompilerDiagnosticAnalyzerNames.CSharpCompilerAnalyzerTypeName); } diff --git a/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs index 1c20823629064a94782aee95b0312d2ca1fcdbca..f5c9a6e2895a8d561279495a0c9819a7323206b0 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs @@ -18,15 +18,13 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.LanguageServices; using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; - -#if !NETCOREAPP using Roslyn.Utilities; -#endif namespace Microsoft.CodeAnalysis.RemoveUnnecessarySuppressions { - internal abstract class AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer + internal abstract class AbstractRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer : AbstractCodeQualityDiagnosticAnalyzer, IPragmaSuppressionsAnalyzer { private static readonly LocalizableResourceString s_localizableRemoveUnnecessarySuppression = new LocalizableResourceString( @@ -36,7 +34,7 @@ internal abstract class AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAna private readonly Lazy> _lazySupportedCompilerErrorCodes; - protected AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer() + protected AbstractRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer() : base(ImmutableArray.Create(s_removeUnnecessarySuppressionDescriptor), GeneratedCodeAnalysisFlags.None) { _lazySupportedCompilerErrorCodes = new Lazy>(() => GetSupportedCompilerErrorCodes()); @@ -45,6 +43,7 @@ protected AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer() protected abstract string CompilerErrorCodePrefix { get; } protected abstract int CompilerErrorCodeDigitCount { get; } protected abstract ISyntaxFacts SyntaxFacts { get; } + protected abstract ISemanticFacts SemanticFacts { get; } protected abstract (Assembly assembly, string typeName) GetCompilerDiagnosticAnalyzerInfo(); private ImmutableHashSet GetSupportedCompilerErrorCodes() @@ -120,34 +119,115 @@ protected sealed override void InitializeWorker(AnalysisContext context) var root = tree.GetRoot(cancellationToken); - // Bail out if there are no pragma directives or tree has syntax errors. - if (!root.ContainsDirectives || root.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) + // Bail out if tree has syntax errors. + if (root.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) { return; } - // Process pragma directives in the tree. + // Process pragma directives and inline SuppressMessageAttributes in the tree. // The core algorithm is as follows: - // 1. Iterate through all the active pragmas in the source file and identify the pragmas - // with diagnostics IDs for which we support unnecesary pragma analysis. + // 1. Iterate through all the active pragmas and local SuppressMessageAttributes in the source file and + // identify the pragmas and local SuppressMessageAttributes + // with diagnostics IDs for which we support unnecesary suppression analysis. // 2. Build the following data structures during this loop: // a. A map from diagnostic ID to list of pragmas for the ID. This map tracks supported diagnostic IDs for this tree's pragmas. // b. A array of tuples of candidate pragmas sorted by span, along with associated IDs and enable/disable flag. // This sorted array allows mapping an unnecessary pragma to the corresponding toggling pragma pair for removal. // c. A map from pragmas to a boolean indicating if the pragma was used or not. - // d. A set of supported compiler diagnostic IDs that are used in pragmas in this file. + // d. A map from diagnostic ID to list of SuppressMessageAttribute nodes for the ID. + // This map tracks supported diagnostic IDs for this tree's SuppressMessageAttribute nodes. + // e. A map from SuppressMessageAttribute nodes to a boolean indicating if the attribute was used or not. + // f. A set of supported compiler diagnostic IDs that are used in pragmas or SuppressMessageAttributes in this file. // 3. Map the set of candidate diagnostic IDs to the analyzers that can report diagnostics with these IDs. // 4. Execute these analyzers to compute the diagnostics reported by these analyzers in this file. - // 5. Iterate through the suppressed diagnostics from this list, and mark the closest preceeeding disable pragma - // which suppresses this ID as used/necessary. Also mark the matching restore pragma as used. - // 6. Finally, report a diagostic all the pragmas which have not been marked as used. + // 5. Iterate through the suppressed diagnostics from this list and do the following: + // a. If the diagnostic was suppressed with a prama, mark the closest preceeeding disable pragma + // which suppresses this ID as used/necessary. Also mark the matching restore pragma as used. + // b. Otherwise, if the diagnostic was suppressed with SuppressMessageAttribute, mark the attribute as used. + // 6. Finally, report a diagostic all the pragmas and SuppressMessageAttributes which have not been marked as used. - var hasPragmaInAnalysisSpan = false; using var _1 = PooledDictionary>.GetInstance(out var idToPragmasMap); using var _2 = ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray ids, bool isDisable)>.GetInstance(out var sortedPragmasWithIds); using var _3 = PooledDictionary.GetInstance(out var pragmasToIsUsedMap); - using var _4 = ArrayBuilder.GetInstance(out var idsBuilder); - using var _5 = PooledHashSet.GetInstance(out var compilerDiagnosticIds); + using var _4 = PooledHashSet.GetInstance(out var compilerDiagnosticIds); + var hasPragmaInAnalysisSpan = ProcessPragmaDirectives(root, span, idToPragmasMap, + pragmasToIsUsedMap, sortedPragmasWithIds, compilerDiagnosticIds, userExclusions); + + cancellationToken.ThrowIfCancellationRequested(); + + using var _5 = PooledDictionary>.GetInstance(out var idToSuppressMessageAttributesMap); + using var _6 = PooledDictionary.GetInstance(out var suppressMessageAttributesToIsUsedMap); + var hasAttributeInAnalysisSpan = await ProcessSuppressMessageAttributesAsync(root, semanticModel, span, + idToSuppressMessageAttributesMap, suppressMessageAttributesToIsUsedMap, userExclusions, cancellationToken).ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + + // Bail out if we have no pragma directives or SuppressMessageAttributes to analyze. + if (!hasPragmaInAnalysisSpan && !hasAttributeInAnalysisSpan) + { + return; + } + + using var _8 = PooledHashSet.GetInstance(out var idsToAnalyzeBuilder); + idsToAnalyzeBuilder.AddAll(idToPragmasMap.Keys); + idsToAnalyzeBuilder.AddAll(idToSuppressMessageAttributesMap.Keys); + var idsToAnalyze = idsToAnalyzeBuilder.ToImmutableHashSet(); + + // Compute all the reported compiler and analyzer diagnostics for diagnostic IDs corresponding to pragmas in the tree. + var (diagnostics, unhandledIds) = await GetReportedDiagnosticsForIdsAsync( + idsToAnalyze, root, semanticModel, compilationWithAnalyzers, + getSupportedDiagnostics, getIsCompilationEndAnalyzer, compilerDiagnosticIds, cancellationToken).ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + + // Iterate through reported diagnostics which are suppressed in source through pragmas and mark the corresponding pragmas as used. + await ProcessReportedDiagnosticsAsync(diagnostics, tree, compilationWithAnalyzers, idToPragmasMap, + pragmasToIsUsedMap, idToSuppressMessageAttributesMap, suppressMessageAttributesToIsUsedMap, cancellationToken).ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + + // Remove entries for unhandled diagnostic ids. + foreach (var id in unhandledIds) + { + foreach (var (pragma, _) in idToPragmasMap[id]) + { + pragmasToIsUsedMap.Remove(pragma); + } + + if (idToSuppressMessageAttributesMap.TryGetValue(id, out var attributeNodes)) + { + foreach (var attributeNode in attributeNodes) + { + suppressMessageAttributesToIsUsedMap.Remove(attributeNode); + } + + idToSuppressMessageAttributesMap.Remove(id); + } + } + + // Finally, report the unnecessary suppressions. + var effectiveSeverity = severity.ToDiagnosticSeverity() ?? s_removeUnnecessarySuppressionDescriptor.DefaultSeverity; + ReportUnnecessarySuppressions(pragmasToIsUsedMap, sortedPragmasWithIds, + suppressMessageAttributesToIsUsedMap, reportDiagnostic, effectiveSeverity, compilationWithAnalyzers.Compilation); + } + + private bool ProcessPragmaDirectives( + SyntaxNode root, + TextSpan? span, + PooledDictionary> idToPragmasMap, + PooledDictionary pragmasToIsUsedMap, + ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray ids, bool isDisable)> sortedPragmasWithIds, + PooledHashSet compilerDiagnosticIds, + ImmutableArray userExclusions) + { + if (!root.ContainsDirectives) + { + return false; + } + + using var _ = ArrayBuilder.GetInstance(out var idsBuilder); + var hasPragmaInAnalysisSpan = false; foreach (var trivia in root.DescendantTrivia()) { // Check if this is an active pragma with at least one applicable diagnostic ID/error code. @@ -200,34 +280,7 @@ protected sealed override void InitializeWorker(AnalysisContext context) } } - // Bail out if we have no pragma directives to analyze. - if (!hasPragmaInAnalysisSpan) - { - return; - } - - // Compute all the reported compiler and analyzer diagnostics for diagnostic IDs corresponding to pragmas in the tree. - var (diagnostics, unhandledIds) = await GetReportedDiagnosticsForIdsAsync( - idToPragmasMap.Keys.ToImmutableHashSet(), root, semanticModel, compilationWithAnalyzers, - getSupportedDiagnostics, getIsCompilationEndAnalyzer, compilerDiagnosticIds, cancellationToken).ConfigureAwait(false); - - cancellationToken.ThrowIfCancellationRequested(); - - // Iterate through reported diagnostics which are suppressed in source through pragmas and mark the corresponding pragmas as used. - ProcessReportedDiagnostics(diagnostics, tree, compilationWithAnalyzers, idToPragmasMap, pragmasToIsUsedMap); - - // Remove pragma entries for unhandled diagnostic ids. - foreach (var id in unhandledIds) - { - foreach (var (pragma, _) in idToPragmasMap[id]) - { - pragmasToIsUsedMap.Remove(pragma); - } - } - - // Finally, report the unnecessary pragmas. - ReportUnnecessaryPragmaDiagnostics(pragmasToIsUsedMap, sortedPragmasWithIds, - reportDiagnostic, severity, compilationWithAnalyzers.Compilation); + return hasPragmaInAnalysisSpan; } private bool IsSupportedId( @@ -258,6 +311,12 @@ protected sealed override void InitializeWorker(AnalysisContext context) } isCompilerDiagnosticId = false; + return IsSupportedAnalyzerDiagnosticId(id) && + idWithoutPrefix == id; + } + + private static bool IsSupportedAnalyzerDiagnosticId(string id) + { switch (id) { case IDEDiagnosticIds.RemoveUnnecessarySuppressionDiagnosticId: @@ -270,7 +329,7 @@ protected sealed override void InitializeWorker(AnalysisContext context) return false; default: - return idWithoutPrefix == id; + return true; } } @@ -393,28 +452,53 @@ private static (ImmutableArray userExclusions, bool analyzerDisabled) Pa return (reportedDiagnostics.ToImmutable(), unhandledIds.ToImmutable()); } - private static void ProcessReportedDiagnostics( + private static async Task ProcessReportedDiagnosticsAsync( ImmutableArray diagnostics, SyntaxTree tree, CompilationWithAnalyzers compilationWithAnalyzers, PooledDictionary> idToPragmasMap, - PooledDictionary pragmasToIsUsedMap) + PooledDictionary pragmasToIsUsedMap, + PooledDictionary> idToSuppressMessageAttributesMap, + PooledDictionary suppressMessageAttributesToIsUsedMap, + CancellationToken cancellationToken) { foreach (var diagnostic in diagnostics) { - if (!diagnostic.IsSuppressed || - !idToPragmasMap.TryGetValue(diagnostic.Id, out var pragmasForIdInReverseOrder)) + if (!diagnostic.IsSuppressed) { continue; } var suppressionInfo = diagnostic.GetSuppressionInfo(compilationWithAnalyzers.Compilation); - if (suppressionInfo?.Attribute != null) + if (suppressionInfo == null) { - // Ignore diagnostics suppressed by SuppressMessageAttributes. continue; } + if (suppressionInfo.Attribute is { } attribute) + { + await ProcessAttributeSuppressionsAsync(diagnostic, attribute, + idToSuppressMessageAttributesMap, suppressMessageAttributesToIsUsedMap, cancellationToken).ConfigureAwait(false); + } + else + { + ProcessPragmaSuppressions(diagnostic, tree, idToPragmasMap, pragmasToIsUsedMap); + } + } + + return; + + static void ProcessPragmaSuppressions( + Diagnostic diagnostic, + SyntaxTree tree, + PooledDictionary> idToPragmasMap, + PooledDictionary pragmasToIsUsedMap) + { + if (!idToPragmasMap.TryGetValue(diagnostic.Id, out var pragmasForIdInReverseOrder)) + { + return; + } + Debug.Assert(diagnostic.Location.IsInSource); Debug.Assert(diagnostic.Location.SourceTree == tree); @@ -443,47 +527,98 @@ private static (ImmutableArray userExclusions, bool analyzerDisabled) Pa } } } + + static async Task ProcessAttributeSuppressionsAsync( + Diagnostic diagnostic, + AttributeData attribute, + PooledDictionary> idToSuppressMessageAttributesMap, + PooledDictionary suppressMessageAttributesToIsUsedMap, + CancellationToken cancellationToken) + { + if (attribute.ApplicationSyntaxReference == null || + !idToSuppressMessageAttributesMap.TryGetValue(diagnostic.Id, out var suppressMessageAttributesForId)) + { + return; + } + + var attributeNode = await attribute.ApplicationSyntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); + foreach (var node in suppressMessageAttributesForId) + { + if (attributeNode == node) + { + suppressMessageAttributesToIsUsedMap[attributeNode] = true; + return; + } + } + } } - private static void ReportUnnecessaryPragmaDiagnostics( + private static void ReportUnnecessarySuppressions( PooledDictionary pragmasToIsUsedMap, ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray ids, bool isDisable)> sortedPragmasWithIds, + PooledDictionary suppressMessageAttributesToIsUsedMap, Action reportDiagnostic, - ReportDiagnostic severity, + DiagnosticSeverity severity, Compilation compilation) { using var _ = ArrayBuilder.GetInstance(out var diagnosticsBuilder); - foreach (var (pragma, isUsed) in pragmasToIsUsedMap) + AddUnnecessaryPragmaDiagnostics(diagnosticsBuilder, pragmasToIsUsedMap, sortedPragmasWithIds, severity); + AddUnnecessarySuppressMessageAttributeDiagnostics(diagnosticsBuilder, suppressMessageAttributesToIsUsedMap, severity); + + // Apply the diagnostic filtering + var effectiveDiagnostics = CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnosticsBuilder, compilation); + foreach (var diagnostic in effectiveDiagnostics) { - if (!isUsed) + reportDiagnostic(diagnostic); + } + + return; + + static void AddUnnecessaryPragmaDiagnostics( + ArrayBuilder diagnosticsBuilder, + PooledDictionary pragmasToIsUsedMap, + ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray ids, bool isDisable)> sortedPragmasWithIds, + DiagnosticSeverity severity) + { + foreach (var (pragma, isUsed) in pragmasToIsUsedMap) { - // We found an unnecessary pragma directive. - // Try to find a matching disable/restore counterpart that toggles the pragma state. - // This enables the code fix to simultaneously remove both the disable and restore directives. - // If we don't find a matching pragma, report just the current pragma. - ImmutableArray additionalLocations; - if (TryGetTogglingPragmaDirective(pragma, sortedPragmasWithIds, out var togglePragma) && - pragmasToIsUsedMap.TryGetValue(togglePragma, out var isToggleUsed) && - !isToggleUsed) + if (!isUsed) { - additionalLocations = ImmutableArray.Create(togglePragma.GetLocation()); - } - else - { - additionalLocations = ImmutableArray.Empty; - } + // We found an unnecessary pragma directive. + // Try to find a matching disable/restore counterpart that toggles the pragma state. + // This enables the code fix to simultaneously remove both the disable and restore directives. + // If we don't find a matching pragma, report just the current pragma. + ImmutableArray additionalLocations; + if (TryGetTogglingPragmaDirective(pragma, sortedPragmasWithIds, out var togglePragma) && + pragmasToIsUsedMap.TryGetValue(togglePragma, out var isToggleUsed) && + !isToggleUsed) + { + additionalLocations = ImmutableArray.Create(togglePragma.GetLocation()); + } + else + { + additionalLocations = ImmutableArray.Empty; + } - var effectiveSeverity = severity.ToDiagnosticSeverity() ?? s_removeUnnecessarySuppressionDescriptor.DefaultSeverity; - var diagnostic = Diagnostic.Create(s_removeUnnecessarySuppressionDescriptor, pragma.GetLocation(), effectiveSeverity, additionalLocations, properties: null); - diagnosticsBuilder.Add(diagnostic); + var diagnostic = Diagnostic.Create(s_removeUnnecessarySuppressionDescriptor, pragma.GetLocation(), severity, additionalLocations, properties: null); + diagnosticsBuilder.Add(diagnostic); + } } } - // Apply the diagnostic filtering - var effectiveDiagnostics = CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnosticsBuilder, compilation); - foreach (var diagnostic in effectiveDiagnostics) + static void AddUnnecessarySuppressMessageAttributeDiagnostics( + ArrayBuilder diagnosticsBuilder, + PooledDictionary suppressMessageAttributesToIsUsedMap, + DiagnosticSeverity severity) { - reportDiagnostic(diagnostic); + foreach (var (attribute, isUsed) in suppressMessageAttributesToIsUsedMap) + { + if (!isUsed) + { + var diagnostic = Diagnostic.Create(s_removeUnnecessarySuppressionDescriptor, attribute.GetLocation(), severity, additionalLocations: null, properties: null); + diagnosticsBuilder.Add(diagnostic); + } + } } } @@ -541,5 +676,117 @@ private static (ImmutableArray userExclusions, bool analyzerDisabled) Pa togglePragma = default; return false; } + + private async Task ProcessSuppressMessageAttributesAsync( + SyntaxNode root, + SemanticModel semanticModel, + TextSpan? span, + PooledDictionary> idToSuppressMessageAttributesMap, + PooledDictionary suppressMessageAttributesToIsUsedMap, + ImmutableArray userExclusions, + CancellationToken cancellationToken) + { + var suppressMessageAttributeType = semanticModel.Compilation.SuppressMessageAttributeType(); + if (suppressMessageAttributeType == null) + { + return false; + } + + var declarationNodes = SyntaxFacts.GetTopLevelAndMethodLevelMembers(root); + using var _ = PooledHashSet.GetInstance(out var processedPartialSymbols); + if (declarationNodes.Count > 0) + { + foreach (var node in declarationNodes) + { + if (span.HasValue && !node.FullSpan.Contains(span.Value)) + { + continue; + } + + var symbols = SemanticFacts.GetDeclaredSymbols(semanticModel, node, cancellationToken); + foreach (var symbol in symbols) + { + switch (symbol?.Kind) + { + // Local SuppressMessageAttributes are only applicable for types and members. + case SymbolKind.NamedType: + case SymbolKind.Method: + case SymbolKind.Field: + case SymbolKind.Property: + case SymbolKind.Event: + break; + + default: + continue; + } + + // Skip already processed symbols from partial declarations + var isPartial = symbol.Locations.Length > 1; + if (isPartial && !processedPartialSymbols.Add(symbol)) + { + continue; + } + + foreach (var attribute in symbol.GetAttributes()) + { + if (attribute.ApplicationSyntaxReference != null && + TryGetSuppressedDiagnosticId(attribute, suppressMessageAttributeType, out var id)) + { + // Ignore unsupported IDs and those excluded through user option. + if (!IsSupportedAnalyzerDiagnosticId(id) || + userExclusions.Contains(id, StringComparer.OrdinalIgnoreCase)) + { + continue; + } + + if (!idToSuppressMessageAttributesMap.TryGetValue(id, out var nodesForId)) + { + nodesForId = new List(); + idToSuppressMessageAttributesMap.Add(id, nodesForId); + } + + var attributeNode = await attribute.ApplicationSyntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); + nodesForId.Add(attributeNode); + + // Initialize the attribute node as unnecessary at the start of the algorithm. + // Later processing will identify attributes which are indeed responsible for suppressing diagnostics + // and mark them as used. + // NOTE: For attributes on partial symbols with multiple declarations, we conservatively + // consider them as used and avoid unnecessary attribute analysis because that would potentially + // require analysis across multiple files, which can be expensive from a performance standpoint. + suppressMessageAttributesToIsUsedMap.Add(attributeNode, isPartial); + } + } + } + } + } + + return idToSuppressMessageAttributesMap.Count > 0; + } + + private static bool TryGetSuppressedDiagnosticId( + AttributeData attribute, + INamedTypeSymbol suppressMessageAttributeType, + [NotNullWhen(returnValue: true)] out string? id) + { + if (suppressMessageAttributeType.Equals(attribute.AttributeClass) && + attribute.AttributeConstructor?.Parameters.Length >= 2 && + attribute.AttributeConstructor.Parameters[1].Name == "checkId" && + attribute.AttributeConstructor.Parameters[1].Type.SpecialType == SpecialType.System_String && + attribute.ConstructorArguments.Length >= 2 && + attribute.ConstructorArguments[1] is { } typedConstant && + typedConstant.Kind == TypedConstantKind.Primitive && + typedConstant.Value is string checkId) + { + // CheckId represents diagnostic ID, followed by an option ':' and name. + // For example, "CA1801:ReviewUnusedParameters" + var index = checkId.IndexOf(':'); + id = index > 0 ? checkId.Substring(0, index) : checkId; + return id.Length > 0; + } + + id = null; + return false; + } } } diff --git a/src/Analyzers/Core/CodeFixes/RemoveUnnecessarySuppressions/RemoveUnnecessaryPragmaSuppressionsCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/RemoveUnnecessarySuppressions/RemoveUnnecessaryPragmaSuppressionsCodeFixProvider.cs index 37eae3cfefbcee15d2343c00e992647242b03ac3..39965a5419dfc3953acc5c8845dd47b27604c05d 100644 --- a/src/Analyzers/Core/CodeFixes/RemoveUnnecessarySuppressions/RemoveUnnecessaryPragmaSuppressionsCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/RemoveUnnecessarySuppressions/RemoveUnnecessaryPragmaSuppressionsCodeFixProvider.cs @@ -15,17 +15,18 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.LanguageServices; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; namespace Microsoft.CodeAnalysis.RemoveUnnecessarySuppressions { [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, Name = PredefinedCodeFixProviderNames.RemoveUnnecessaryPragmaSuppressions), Shared] - internal sealed class RemoveUnnecessaryPragmaSuppressionsCodeFixProvider : SyntaxEditorBasedCodeFixProvider + internal sealed class RemoveUnnecessaryInlineSuppressionsCodeFixProvider : SyntaxEditorBasedCodeFixProvider { [ImportingConstructor] [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public RemoveUnnecessaryPragmaSuppressionsCodeFixProvider() + public RemoveUnnecessaryInlineSuppressionsCodeFixProvider() { } @@ -38,10 +39,12 @@ internal override CodeFixCategory CodeFixCategory public override async Task RegisterCodeFixesAsync(CodeFixContext context) { var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var syntaxFacts = context.Document.GetRequiredLanguageService(); foreach (var diagnostic in context.Diagnostics) { // Defensive check that we are operating on the diagnostic on a pragma. - if (root.FindTrivia(diagnostic.Location.SourceSpan.Start).HasStructure) + if (root.FindNode(diagnostic.Location.SourceSpan) is { } node && syntaxFacts.IsAttribute(node) || + root.FindTrivia(diagnostic.Location.SourceSpan.Start).HasStructure) { context.RegisterCodeFix( new MyCodeAction(c => FixAsync(context.Document, diagnostic, c)), @@ -58,22 +61,36 @@ protected override Task FixAllAsync(Document document, ImmutableArray.GetInstance(out var processedNodes); - + var syntaxFacts = document.GetRequiredLanguageService(); foreach (var diagnostic in diagnostics) { - RemoveNode(diagnostic.Location, editor, processedNodes); + RemoveNode(diagnostic.Location, editor, processedNodes, syntaxFacts); foreach (var location in diagnostic.AdditionalLocations) { - RemoveNode(location, editor, processedNodes); + RemoveNode(location, editor, processedNodes, syntaxFacts); } } return Task.CompletedTask; - static void RemoveNode(Location location, SyntaxEditor editor, HashSet processedNodes) + static void RemoveNode( + Location location, + SyntaxEditor editor, + HashSet processedNodes, + ISyntaxFacts syntaxFacts) { - var node = editor.OriginalRoot.FindTrivia(location.SourceSpan.Start).GetStructure()!; + SyntaxNode node; + if (editor.OriginalRoot.FindNode(location.SourceSpan) is { } attribute && + syntaxFacts.IsAttribute(attribute)) + { + node = attribute; + } + else + { + node = editor.OriginalRoot.FindTrivia(location.SourceSpan.Start).GetStructure()!; + } + if (processedNodes.Add(node)) { editor.RemoveNode(node); @@ -84,7 +101,7 @@ static void RemoveNode(Location location, SyntaxEditor editor, HashSet> createChangedDocument) - : base(AnalyzersResources.Remove_unnecessary_suppression, createChangedDocument, nameof(RemoveUnnecessaryPragmaSuppressionsCodeFixProvider)) + : base(AnalyzersResources.Remove_unnecessary_suppression, createChangedDocument, nameof(RemoveUnnecessaryInlineSuppressionsCodeFixProvider)) { } } diff --git a/src/Analyzers/VisualBasic/Analyzers/RemoveUnnecessarySuppressions/VisualBasicRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.vb b/src/Analyzers/VisualBasic/Analyzers/RemoveUnnecessarySuppressions/VisualBasicRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.vb index f34cc1f5952e1de965450b89c76860336f2ce3ab..63617f6dd9b5ec6ccac5ed8fd1b068c59df8d1f4 100644 --- a/src/Analyzers/VisualBasic/Analyzers/RemoveUnnecessarySuppressions/VisualBasicRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.vb +++ b/src/Analyzers/VisualBasic/Analyzers/RemoveUnnecessarySuppressions/VisualBasicRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.vb @@ -11,8 +11,8 @@ Imports Microsoft.CodeAnalysis.VisualBasic.LanguageServices Namespace Microsoft.CodeAnalysis.VisualBasic.RemoveUnnecessarySuppressions - Friend NotInheritable Class VisualBasicRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer - Inherits AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer + Friend NotInheritable Class VisualBasicRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer + Inherits AbstractRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer Protected Overrides ReadOnly Property CompilerErrorCodePrefix As String = "BC" @@ -20,6 +20,8 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.RemoveUnnecessarySuppressions Protected Overrides ReadOnly Property SyntaxFacts As ISyntaxFacts = VisualBasicSyntaxFacts.Instance + Protected Overrides ReadOnly Property SemanticFacts As ISemanticFacts = VisualBasicSemanticFacts.Instance + Protected Overrides Function GetCompilerDiagnosticAnalyzerInfo() As (assembly As Assembly, typeName As String) Return (GetType(SyntaxKind).Assembly, CompilerDiagnosticAnalyzerNames.VisualBasicCompilerAnalyzerTypeName) End Function diff --git a/src/EditorFeatures/CSharpTest/Diagnostics/Suppression/RemoveUnnecessaryPragmaSuppressionsTests.cs b/src/EditorFeatures/CSharpTest/Diagnostics/Suppression/RemoveUnnecessaryPragmaSuppressionsTests.cs index 7923142c2de8ea7c7cc52c0edf34abf26bebb13a..9c81edcabf4a56f27b7bf67f013e521335bacdf7 100644 --- a/src/EditorFeatures/CSharpTest/Diagnostics/Suppression/RemoveUnnecessaryPragmaSuppressionsTests.cs +++ b/src/EditorFeatures/CSharpTest/Diagnostics/Suppression/RemoveUnnecessaryPragmaSuppressionsTests.cs @@ -32,14 +32,14 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.RemoveUnnecessarySuppre { [Trait(Traits.Feature, Traits.Features.CodeActionsRemoveUnnecessarySuppressions)] [WorkItem(44177, "https://github.com/dotnet/roslyn/issues/44177")] - public abstract class RemoveUnnecessaryPragmaSuppressionsTests : AbstractUnncessarySuppressionDiagnosticTest + public abstract class RemoveUnnecessaryInlineSuppressionsTests : AbstractUnncessarySuppressionDiagnosticTest { #region Helpers internal sealed override CodeFixProvider CodeFixProvider - => new RemoveUnnecessaryPragmaSuppressionsCodeFixProvider(); - internal sealed override AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer SuppressionAnalyzer - => new CSharpRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer(); + => new RemoveUnnecessaryInlineSuppressionsCodeFixProvider(); + internal sealed override AbstractRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer SuppressionAnalyzer + => new CSharpRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer(); protected sealed override ParseOptions GetScriptOptions() => Options.Script; protected internal sealed override string GetLanguage() => LanguageNames.CSharp; @@ -117,8 +117,9 @@ protected sealed class CompilationEndDiagnosticAnalyzer : DiagnosticAnalyzer #region Single analyzer tests (Compiler OR Analyzer) - public abstract class CompilerOrAnalyzerTests : RemoveUnnecessaryPragmaSuppressionsTests + public abstract class CompilerOrAnalyzerTests : RemoveUnnecessaryInlineSuppressionsTests { + protected abstract bool IsCompilerDiagnosticsTest { get; } protected abstract string VariableDeclaredButNotUsedDiagnosticId { get; } protected abstract string VariableAssignedButNotUsedDiagnosticId { get; } protected abstract ImmutableArray UnsupportedDiagnosticIds { get; } @@ -127,6 +128,7 @@ public sealed class CompilerTests : CompilerOrAnalyzerTests { internal override ImmutableArray OtherAnalyzers => ImmutableArray.Create(new CSharpCompilerDiagnosticAnalyzer()); + protected override bool IsCompilerDiagnosticsTest => true; protected override string VariableDeclaredButNotUsedDiagnosticId => "CS0168"; protected override string VariableAssignedButNotUsedDiagnosticId => "CS0219"; protected override ImmutableArray UnsupportedDiagnosticIds @@ -141,9 +143,12 @@ protected override ImmutableArray UnsupportedDiagnosticIds if (!supported.Contains(errorCode) && errorCode > 0) { // Add all 3 supported formats for suppressions: integer, integer with leading zeros, "CS" prefix - builder.Add(errorCode.ToString()); - builder.Add(errorCode.ToString("D4")); - builder.Add("CS" + errorCode.ToString("D4")); + var errorCodeString = errorCode.ToString(); + var errorCodeD4String = errorCode.ToString("D4"); + builder.Add(errorCodeString); + if (errorCodeD4String != errorCodeString) + builder.Add(errorCodeD4String); + builder.Add("CS" + errorCodeD4String); } } @@ -156,6 +161,7 @@ public sealed class AnalyzerTests : CompilerOrAnalyzerTests { internal override ImmutableArray OtherAnalyzers => ImmutableArray.Create(new UserDiagnosticAnalyzer(), new CompilationEndDiagnosticAnalyzer()); + protected override bool IsCompilerDiagnosticsTest => false; protected override string VariableDeclaredButNotUsedDiagnosticId => UserDiagnosticAnalyzer.Descriptor0168.Id; protected override string VariableAssignedButNotUsedDiagnosticId => UserDiagnosticAnalyzer.Descriptor0219.Id; protected override ImmutableArray UnsupportedDiagnosticIds @@ -167,7 +173,7 @@ protected override ImmutableArray UnsupportedDiagnosticIds } [Fact] - public async Task TestDoNotRemoveRequiredDiagnosticSuppression() + public async Task TestDoNotRemoveRequiredDiagnosticSuppression_Pragma() { await TestMissingInRegularAndScriptAsync( $@" @@ -183,7 +189,7 @@ void M() } [Fact] - public async Task TestDoNotRemoveRequiredDiagnosticSuppression_02() + public async Task TestDoNotRemoveRequiredDiagnosticSuppression_Pragma_02() { await TestMissingInRegularAndScriptAsync( $@" @@ -197,17 +203,93 @@ void M() }}"); } + [Fact] + public async Task TestDoNotRemoveRequiredDiagnosticSuppression_Attribute_Method() + { + var code = $@" +class Class +{{ + [|[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")]|] + void M() + {{ + int y; + }} +}}"; + // Compiler diagnostics cannot be suppressed with SuppressMessageAttribute. + // Hence, attribute suppressions for compiler diagnostics are always unnecessary. + if (!IsCompilerDiagnosticsTest) + { + await TestMissingInRegularAndScriptAsync(code); + } + else + { + await TestInRegularAndScript1Async(code, @" +class Class +{ + void M() + { + int y; + } +}"); + } + } + + [Fact] + public async Task TestDoNotRemoveRequiredDiagnosticSuppression_Attribute_02() + { + var code = $@" +[|[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")]|] +class Class +{{ + void M() + {{ + int y; + }} +}}"; + // Compiler diagnostics cannot be suppressed with SuppressMessageAttribute. + // Hence, attribute suppressions for compiler diagnostics are always unnecessary. + if (!IsCompilerDiagnosticsTest) + { + await TestMissingInRegularAndScriptAsync(code); + } + else + { + await TestInRegularAndScript1Async(code, @" +class Class +{ + void M() + { + int y; + } +}"); + } + } + [Theory, CombinatorialData] public async Task TestDoNotRemoveUnsupportedDiagnosticSuppression(bool disable) { var disableOrRestore = disable ? "disable" : "restore"; var pragmas = new StringBuilder(); + var suppressMessageAttribtes = new StringBuilder(); foreach (var id in UnsupportedDiagnosticIds) { pragmas.AppendLine($@"#pragma warning {disableOrRestore} {id}"); + suppressMessageAttribtes.AppendLine($@"[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{id}"")]"); } - await TestMissingInRegularAndScriptAsync($"[|{pragmas}|]"); + var source = $@"{{|FixAllInDocument:{pragmas}{suppressMessageAttribtes}|}}class Class {{ }}"; + + // Compiler diagnostics cannot be suppressed with SuppressMessageAttribute. + // Hence, attribute suppressions for compiler diagnostics are always unnecessary. + if (!IsCompilerDiagnosticsTest) + { + await TestMissingInRegularAndScriptAsync(source); + } + else + { + var fixedSource = $@"{pragmas}class Class {{ }}"; + await TestInRegularAndScriptAsync(source, fixedSource); + } } [Fact] @@ -216,18 +298,19 @@ public async Task TestDoNotRemoveInactiveDiagnosticSuppression() await TestMissingInRegularAndScriptAsync( $@" #if false - +[| class Class {{ + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")] void M() {{ -[|#pragma warning disable {VariableDeclaredButNotUsedDiagnosticId} // Variable is declared but never used - Inactive +#pragma warning disable {VariableDeclaredButNotUsedDiagnosticId} // Variable is declared but never used - Inactive int y; -#pragma warning restore {VariableDeclaredButNotUsedDiagnosticId} // Variable is declared but never used - Inactive|] +#pragma warning restore {VariableDeclaredButNotUsedDiagnosticId} // Variable is declared but never used - Inactive y = 1; }} }} - +|] #endif"); } @@ -236,18 +319,19 @@ public async Task TestDoNotRemoveDiagnosticSuppressionsInCodeWithSyntaxErrors() { await TestMissingInRegularAndScriptAsync( $@" +[| class Class {{ + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")] void M() {{ -[|#pragma warning disable {VariableDeclaredButNotUsedDiagnosticId} // Variable is declared but never used +#pragma warning disable {VariableDeclaredButNotUsedDiagnosticId} // Variable is declared but never used int y // CS1002: ; expected -#pragma warning restore {VariableDeclaredButNotUsedDiagnosticId} // Variable is declared but never used|] +#pragma warning restore {VariableDeclaredButNotUsedDiagnosticId} // Variable is declared but never used y = 1; }} }} - -#endif"); +|]"); } [Fact] @@ -256,17 +340,19 @@ public async Task TestDoNotRemoveDiagnosticSuppressionWhenAnalyzerSuppressed() await TestMissingInRegularAndScriptAsync( $@" #pragma warning disable {IDEDiagnosticIds.RemoveUnnecessarySuppressionDiagnosticId} - +[| class Class {{ + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")] void M() {{ -[|#pragma warning disable {VariableDeclaredButNotUsedDiagnosticId} // Variable is declared but never used - Unnecessary, but suppressed +#pragma warning disable {VariableDeclaredButNotUsedDiagnosticId} // Variable is declared but never used - Unnecessary, but suppressed int y; -#pragma warning restore {VariableDeclaredButNotUsedDiagnosticId} // Variable is declared but never used - Unnecessary, but suppressed|] +#pragma warning restore {VariableDeclaredButNotUsedDiagnosticId} // Variable is declared but never used - Unnecessary, but suppressed y = 1; }} -}}"); +}} +|]"); } [Theory, CombinatorialData] @@ -279,20 +365,46 @@ public async Task TestDoNotRemoveExcludedDiagnosticSuppression(bool excludeAll) await TestMissingInRegularAndScriptAsync( $@" +[| class Class {{ + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")] void M() {{ -[|#pragma warning disable {VariableDeclaredButNotUsedDiagnosticId} // Variable is declared but never used - Unnecessary, but suppressed +#pragma warning disable {VariableDeclaredButNotUsedDiagnosticId} // Variable is declared but never used - Unnecessary, but suppressed int y; -#pragma warning restore {VariableDeclaredButNotUsedDiagnosticId} // Variable is declared but never used - Unnecessary, but suppressed|] +#pragma warning restore {VariableDeclaredButNotUsedDiagnosticId} // Variable is declared but never used - Unnecessary, but suppressed y = 1; }} -}}", new TestParameters(options: options)); +}} +|]", new TestParameters(options: options)); + } + + [Fact] + public async Task TestDoNotRemoveDiagnosticSuppression_Attribute_OnPartialDeclarations() + { + await TestMissingInRegularAndScriptAsync( + $@" +[| +// Unnecessary, but we do not perform analysis for SuppressMessageAttributes on partial declarations. +[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")] +partial class Class +{{ +}} + +partial class Class +{{ + void M() + {{ + int y; + y = 1; + }} +}} +|]"); } [Theory, CombinatorialData] - public async Task TestRemoveDiagnosticSuppression(bool testFixFromDisable) + public async Task TestRemoveDiagnosticSuppression_Pragma(bool testFixFromDisable) { var (disablePrefix, disableSuffix, restorePrefix, restoreSuffix) = testFixFromDisable ? ("[|", "|]", "", "") @@ -321,6 +433,66 @@ void M() }"); } + [Fact] + public async Task TestRemoveDiagnosticSuppression_Attribute() + { + await TestInRegularAndScript1Async( + $@" +class Class +{{ + [|[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")]|] // Variable is declared but never used - Unnecessary + void M() + {{ + int y; + y = 1; + }} +}}", + @" +class Class +{ + void M() + { + int y; + y = 1; + } +}"); + } + + [Fact] + public async Task TestRemoveDiagnosticSuppression_Attribute_Trivia() + { + // This test should not remove Comment1 and DocComment. + // TODO: File a bug for SyntaxEditor.RemoveNode API removing doc comment and its preceeeding trivia. + + await TestInRegularAndScript1Async( + $@" +class Class +{{ + // Comment1 + /// + /// DocComment + /// + // Comment2 + [|[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")]|] // Comment3 + // Comment4 + void M() + {{ + int y; + y = 1; + }} +}}", + @" +class Class +{ + // Comment4 + void M() + { + int y; + y = 1; + } +}"); + } + [Fact] public async Task TestRemoveDiagnosticSuppression_OnlyDisableDirective() { @@ -372,7 +544,7 @@ void M() } [Theory, CombinatorialData] - public async Task TestRemoveDiagnosticSuppression_DuplicateSuppression(bool testFixFromDisable) + public async Task TestRemoveDiagnosticSuppression_DuplicatePragmaSuppression(bool testFixFromDisable) { var (disablePrefix, disableSuffix, restorePrefix, restoreSuffix) = testFixFromDisable ? ("[|", "|]", "", "") @@ -403,8 +575,119 @@ void M() }}"); } + [Fact] + public async Task TestRemoveDiagnosticSuppression_DuplicateAttributeSuppression() + { + // Compiler diagnostics cannot be suppressed with SuppressMessageAttribute. + // Hence, attribute suppressions for compiler diagnostics are always unnecessary. + var retainedAttributesInFixCode = IsCompilerDiagnosticsTest + ? string.Empty + : $@"[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")] // Variable is declared but never used - Necessary + "; + + await TestInRegularAndScript1Async( + $@" +class Class +{{ + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")] // Variable is declared but never used - Necessary + {{|FixAllInDocument:[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")] // Variable is declared but never used - Unnecessary|}} + void M() + {{ + int y; + }} +}}", + $@" +class Class +{{ + {retainedAttributesInFixCode}void M() + {{ + int y; + }} +}}"); + } + + [Fact] + public async Task TestRemoveDiagnosticSuppression_DuplicateAttributeSuppression_OnContainingSymbol() + { + // Compiler diagnostics cannot be suppressed with SuppressMessageAttribute. + // Hence, attribute suppressions for compiler diagnostics are always unnecessary. + var retainedAttributesInFixCode = IsCompilerDiagnosticsTest + ? string.Empty + : $@"[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")] // Variable is declared but never used - Necessary + "; + + await TestInRegularAndScript1Async( + $@" +{{|FixAllInDocument:[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")] // Variable is declared but never used - Unnecessary|}} +class Class +{{ + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")] // Variable is declared but never used - Necessary + void M() + {{ + int y; + }} +}}", + $@" +class Class +{{ + {retainedAttributesInFixCode}void M() + {{ + int y; + }} +}}"); + } + + [Fact] + public async Task TestRemoveDiagnosticSuppression_DuplicatePragmaAndAttributeSuppression() + { + var source = $@" +class Class +{{ + [|[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")] + void M() + {{ +#pragma warning disable {VariableDeclaredButNotUsedDiagnosticId}|] + int y; +#pragma warning restore {VariableDeclaredButNotUsedDiagnosticId} + }} +}}"; + string fixedSource; + if (IsCompilerDiagnosticsTest) + { + // Compiler diagnostics cannot be suppressed with SuppressMessageAttribute. + // Hence, attribute suppressions for compiler diagnostics are always unnecessary. + fixedSource = $@" +class Class +{{ + void M() + {{ +#pragma warning disable {VariableDeclaredButNotUsedDiagnosticId} + int y; +#pragma warning restore {VariableDeclaredButNotUsedDiagnosticId} + }} +}}"; + } + else + { + // Analyzer diagnostics can be suppressed with both SuppressMessageAttribute and pragmas. + // SuppressMessageAttribute takes precedence over pragmas for duplicate suppressions, + // hence duplicate pragmas are considered unnecessary. + fixedSource = $@" +class Class +{{ + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")] + void M() + {{ + int y; + }} +}}"; + } + + await TestInRegularAndScript1Async(source, fixedSource); + } + [Theory, CombinatorialData] - public async Task TestRemoveDiagnosticSuppression_InnerValidSuppression(bool testFixFromDisable) + public async Task TestRemoveDiagnosticSuppression_Pragma_InnerValidSuppression(bool testFixFromDisable) { var (disablePrefix, disableSuffix, restorePrefix, restoreSuffix) = testFixFromDisable ? ("[|", "|]", "", "") @@ -435,8 +718,33 @@ void M() }}"); } + [Fact] + public async Task TestRemoveDiagnosticSuppression_Attribute_InnerValidSuppression() + { + await TestInRegularAndScript1Async( + $@" +class Class +{{ + [|[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")]|] // Variable is declared but never used - Unnecessary + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableAssignedButNotUsedDiagnosticId}"")] // Variable is assigned but its value is never used - Necessary + void M() + {{ + int y = 0; + }} +}}", + $@" +class Class +{{ + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableAssignedButNotUsedDiagnosticId}"")] // Variable is assigned but its value is never used - Necessary + void M() + {{ + int y = 0; + }} +}}"); + } + [Theory, CombinatorialData] - public async Task TestRemoveDiagnosticSuppression_OuterValidSuppression(bool testFixFromDisable) + public async Task TestRemoveDiagnosticSuppression_Pragma_OuterValidSuppression(bool testFixFromDisable) { var (disablePrefix, disableSuffix, restorePrefix, restoreSuffix) = testFixFromDisable ? ("[|", "|]", "", "") @@ -467,6 +775,31 @@ void M() }}"); } + [Fact] + public async Task TestRemoveDiagnosticSuppression_Attribute_OuterValidSuppression() + { + await TestInRegularAndScript1Async( + $@" +class Class +{{ + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableAssignedButNotUsedDiagnosticId}"")] // Variable is assigned but its value is never used - Necessary + [|[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableDeclaredButNotUsedDiagnosticId}"")]|] // Variable is declared but never used - Unnecessary + void M() + {{ + int y = 0; + }} +}}", + $@" +class Class +{{ + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{VariableAssignedButNotUsedDiagnosticId}"")] // Variable is assigned but its value is never used - Necessary + void M() + {{ + int y = 0; + }} +}}"); + } + [Theory, CombinatorialData] public async Task TestRemoveDiagnosticSuppression_OverlappingDirectives(bool testFixFromDisable) { @@ -554,7 +887,7 @@ void M() } [Theory, CombinatorialData] - public async Task TestRemoveUnknownDiagnosticSuppression(bool testFixFromDisable) + public async Task TestRemoveUnknownDiagnosticSuppression_Pragma(bool testFixFromDisable) { var (disablePrefix, disableSuffix, restorePrefix, restoreSuffix) = testFixFromDisable ? ("[|", "|]", "", "") @@ -570,6 +903,21 @@ class Class @" class Class { +}"); + } + + [Fact] + public async Task TestRemoveUnknownDiagnosticSuppression_Attribute() + { + await TestInRegularAndScript1Async( + @" +[|[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""UnknownId"")]|] +class Class +{ +}", + @" +class Class +{ }"); } } @@ -578,7 +926,7 @@ class Class #region Multiple analyzer tests (Compiler AND Analyzer) - public sealed class CompilerAndAnalyzerTests : RemoveUnnecessaryPragmaSuppressionsTests + public sealed class CompilerAndAnalyzerTests : RemoveUnnecessaryInlineSuppressionsTests { internal override ImmutableArray OtherAnalyzers => ImmutableArray.Create(new CSharpCompilerDiagnosticAnalyzer(), new UserDiagnosticAnalyzer()); @@ -605,18 +953,20 @@ void M() public async Task TestDoNotRemoveDiagnosticSuppressionsForSuppressedAnalyzer() { var source = $@" -class Class +[|class Class {{ + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""CS0168"")] // Variable is declared but never used - Unnecessary, but suppressed + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{UserDiagnosticAnalyzer.Descriptor0168.Id}"")] // Variable is declared but never used - Unnecessary, but suppressed void M() {{ -[|#pragma warning disable CS0168 // Variable is declared but never used - Unnecessary, but suppressed +#pragma warning disable CS0168 // Variable is declared but never used - Unnecessary, but suppressed #pragma warning disable {UserDiagnosticAnalyzer.Descriptor0168.Id} // Variable is declared but never used - Unnecessary, but suppressed int y; #pragma warning restore {UserDiagnosticAnalyzer.Descriptor0168.Id} // Variable is declared but never used - Unnecessary, but suppressed -#pragma warning restore CS0168 // Variable is declared but never used - Unnecessary, but suppressed|] +#pragma warning restore CS0168 // Variable is declared but never used - Unnecessary, but suppressed y = 1; }} -}}"; +}}|]"; var parameters = new TestParameters(); using var workspace = CreateWorkspaceFromFile(source, parameters); @@ -685,18 +1035,20 @@ public async Task TestDoNotRemoveExcludedDiagnosticSuppression_Multiple(bool exc await TestMissingInRegularAndScriptAsync( $@" -class Class +[|class Class {{ + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""CS0168"")] // Variable is declared but never used - Unnecessary, but suppressed + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{UserDiagnosticAnalyzer.Descriptor0168.Id}"")] // Variable is declared but never used - Unnecessary, but suppressed void M() {{ -[|#pragma warning disable CS0168 // Variable is declared but never used - Unnecessary, but suppressed +#pragma warning disable CS0168 // Variable is declared but never used - Unnecessary, but suppressed #pragma warning disable {UserDiagnosticAnalyzer.Descriptor0168.Id} // Variable is declared but never used - Unnecessary, but suppressed int y; #pragma warning restore {UserDiagnosticAnalyzer.Descriptor0168.Id} // Variable is declared but never used - Unnecessary, but suppressed -#pragma warning restore CS0168 // Variable is declared but never used - Unnecessary, but suppressed|] +#pragma warning restore CS0168 // Variable is declared but never used - Unnecessary, but suppressed y = 1; }} -}}", new TestParameters(options: options)); +}}|]", new TestParameters(options: options)); } [Theory, CombinatorialData] @@ -769,8 +1121,12 @@ public async Task TestRemoveDiagnosticSuppression_FixAll(bool testFixFromDisable $@" #pragma warning disable CS0168 // Variable is declared but never used - Unnecessary #pragma warning disable {UserDiagnosticAnalyzer.Descriptor0168.Id} +[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""CS0168"")] +[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{UserDiagnosticAnalyzer.Descriptor0168.Id}"")] class Class {{ + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""CS0168"")] + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""{UserDiagnosticAnalyzer.Descriptor0168.Id}"")] void M() {{ {disablePrefix}#pragma warning disable {UserDiagnosticAnalyzer.Descriptor0168.Id} // Variable is declared but never used - Unnecessary{disableSuffix} @@ -793,6 +1149,54 @@ void M() } } + [Fact] + public async Task TestRemoveDiagnosticSuppression_Attribute_Field() + { + await TestInRegularAndScript1Async( + $@" +class Class +{{ + [|[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""UnknownId"")]|] + private int f; +}}", @" +class Class +{ + private int f; +}"); + } + + [Fact] + public async Task TestRemoveDiagnosticSuppression_Attribute_Property() + { + await TestInRegularAndScript1Async( + $@" +class Class +{{ + [|[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""UnknownId"")]|] + public int P {{ get; }} +}}", @" +class Class +{ + public int P { get; } +}"); + } + + [Fact] + public async Task TestRemoveDiagnosticSuppression_Attribute_Event() + { + await TestInRegularAndScript1Async( + $@" +class Class +{{ + [|[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category"", ""UnknownId"")]|] + private event System.EventHandler SampleEvent; +}}", @" +class Class +{ + private event System.EventHandler SampleEvent; +}"); + } + #endregion } } diff --git a/src/EditorFeatures/DiagnosticsTestUtilities/Diagnostics/AbstractUnncessarySuppressionDiagnosticTest.cs b/src/EditorFeatures/DiagnosticsTestUtilities/Diagnostics/AbstractUnncessarySuppressionDiagnosticTest.cs index 1492f25af71ca5926ef970e8fe3170918bba0561..143f98fec1f1ad15f8713e2dd4591d3a280a24ae 100644 --- a/src/EditorFeatures/DiagnosticsTestUtilities/Diagnostics/AbstractUnncessarySuppressionDiagnosticTest.cs +++ b/src/EditorFeatures/DiagnosticsTestUtilities/Diagnostics/AbstractUnncessarySuppressionDiagnosticTest.cs @@ -18,7 +18,7 @@ namespace Microsoft.CodeAnalysis.Editor.UnitTests.Diagnostics public abstract class AbstractUnncessarySuppressionDiagnosticTest : AbstractUserDiagnosticTest { internal abstract CodeFixProvider CodeFixProvider { get; } - internal abstract AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer SuppressionAnalyzer { get; } + internal abstract AbstractRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer SuppressionAnalyzer { get; } internal abstract ImmutableArray OtherAnalyzers { get; } private void AddAnalyzersToWorkspace(TestWorkspace workspace) diff --git a/src/EditorFeatures/Test/Diagnostics/DiagnosticAnalyzerServiceTests.cs b/src/EditorFeatures/Test/Diagnostics/DiagnosticAnalyzerServiceTests.cs index 03039e4308ca2fbf5b2562e2d7ed1c50241533ac..bb63687255d6be7622715aaac1ab77c9672e1d4b 100644 --- a/src/EditorFeatures/Test/Diagnostics/DiagnosticAnalyzerServiceTests.cs +++ b/src/EditorFeatures/Test/Diagnostics/DiagnosticAnalyzerServiceTests.cs @@ -642,16 +642,19 @@ internal async Task TestDiagnosticSuppressor(bool includeAnalyzer, bool includeS } [Theory, CombinatorialData] - internal async Task TestPragmaSuppressionsAnalyzer(BackgroundAnalysisScope analysisScope) + internal async Task TestRemoveUnnecessaryInlineSuppressionsAnalyzer(BackgroundAnalysisScope analysisScope, bool testPragma) { var analyzers = ImmutableArray.Create( new CSharpCompilerDiagnosticAnalyzer(), new NamedTypeAnalyzer(), - new CSharpRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer()); + new CSharpRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer()); var analyzerReference = new AnalyzerImageReference(analyzers); - var code = $@" + string code; + if (testPragma) + { + code = $@" #pragma warning disable {NamedTypeAnalyzer.DiagnosticId} // Unnecessary #pragma warning disable CS0168 // Variable is declared but never used - Unnecessary @@ -665,6 +668,23 @@ void M() }} }} "; + } + else + { + code = $@" +[System.Diagnostics.CodeAnalysis.SuppressMessage(""Category1"", ""{NamedTypeAnalyzer.DiagnosticId}"")] // Necessary +class A +{{ + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category2"", ""{NamedTypeAnalyzer.DiagnosticId}"")] // Unnecessary + [System.Diagnostics.CodeAnalysis.SuppressMessage(""Category3"", ""CS0168"")] // Unnecessary + void M() + {{ +#pragma warning disable CS0168 // Variable is declared but never used - Necessary + int x; + }} +}} +"; + } using var workspace = TestWorkspace.CreateCSharp(code, exportProvider: EditorServicesUtil.ExportProvider); var options = workspace.Options.WithChangedOption(SolutionCrawlerOptions.BackgroundAnalysisScopeOption, LanguageNames.CSharp, analysisScope); @@ -710,10 +730,20 @@ void M() Assert.Equal(2, diagnostics.Count); var root = await document.GetSyntaxRootAsync(); - var pragma1 = root.FindTrivia(diagnostics[0].GetTextSpan().Start).ToString(); - Assert.Equal($"#pragma warning disable {NamedTypeAnalyzer.DiagnosticId} // Unnecessary", pragma1); - var pragma2 = root.FindTrivia(diagnostics[1].GetTextSpan().Start).ToString(); - Assert.Equal($"#pragma warning disable CS0168 // Variable is declared but never used - Unnecessary", pragma2); + if (testPragma) + { + var pragma1 = root.FindTrivia(diagnostics[0].GetTextSpan().Start).ToString(); + Assert.Equal($"#pragma warning disable {NamedTypeAnalyzer.DiagnosticId} // Unnecessary", pragma1); + var pragma2 = root.FindTrivia(diagnostics[1].GetTextSpan().Start).ToString(); + Assert.Equal($"#pragma warning disable CS0168 // Variable is declared but never used - Unnecessary", pragma2); + } + else + { + var attribute1 = root.FindNode(diagnostics[0].GetTextSpan()).ToString(); + Assert.Equal($@"System.Diagnostics.CodeAnalysis.SuppressMessage(""Category2"", ""{NamedTypeAnalyzer.DiagnosticId}"")", attribute1); + var attribute2 = root.FindNode(diagnostics[1].GetTextSpan()).ToString(); + Assert.Equal($@"System.Diagnostics.CodeAnalysis.SuppressMessage(""Category3"", ""CS0168"")", attribute2); + } } private static Document GetDocumentFromIncompleteProject(AdhocWorkspace workspace) diff --git a/src/EditorFeatures/VisualBasicTest/Diagnostics/Suppression/RemoveUnnecessaryPragmaSuppressionsTests.vb b/src/EditorFeatures/VisualBasicTest/Diagnostics/Suppression/RemoveUnnecessaryPragmaSuppressionsTests.vb index 11d3f09c6a840e7f21e56c233663978fa2061045..d3af66328757dd1fba12cc2670e39baebb2eecde 100644 --- a/src/EditorFeatures/VisualBasicTest/Diagnostics/Suppression/RemoveUnnecessaryPragmaSuppressionsTests.vb +++ b/src/EditorFeatures/VisualBasicTest/Diagnostics/Suppression/RemoveUnnecessaryPragmaSuppressionsTests.vb @@ -17,7 +17,7 @@ Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.Diagnostics.Remove - Public NotInheritable Class RemoveUnnecessaryPragmaSuppressionsTests + Public NotInheritable Class RemoveUnnecessaryInlineSuppressionsTests Inherits AbstractUnncessarySuppressionDiagnosticTest Protected Overrides Function GetScriptOptions() As ParseOptions @@ -37,13 +37,13 @@ Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.Diagnostics.Remove Friend Overrides ReadOnly Property CodeFixProvider As CodeFixProvider Get - Return New RemoveUnnecessaryPragmaSuppressionsCodeFixProvider() + Return New RemoveUnnecessaryInlineSuppressionsCodeFixProvider() End Get End Property - Friend Overrides ReadOnly Property SuppressionAnalyzer As AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer + Friend Overrides ReadOnly Property SuppressionAnalyzer As AbstractRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer Get - Return New VisualBasicRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer() + Return New VisualBasicRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer() End Get End Property @@ -123,6 +123,68 @@ Class C Dim x As Integer x = 1 End Sub +End Class") + End Function + + + Public Async Function TestRemoveUnnecessaryAttributeSuppression_Method() As Task + Await TestInRegularAndScript1Async($" +Imports System +Class C + [||] + Sub Method() + Dim x As Integer + x = 1 + End Sub +End Class", $" +Imports System +Class C + Sub Method() + Dim x As Integer + x = 1 + End Sub +End Class") + End Function + + + Public Async Function TestRemoveUnnecessaryAttributeSuppression_Field() As Task + Await TestInRegularAndScript1Async($" +Imports System +Class C + [||] + Dim f As Integer +End Class", $" +Imports System +Class C + Dim f As Integer +End Class") + End Function + + + Public Async Function TestRemoveUnnecessaryAttributeSuppression_Property() As Task + Await TestInRegularAndScript1Async($" +Imports System +Class C + [||] + Public ReadOnly Property P As Integer +End Class", $" +Imports System +Class C + Public ReadOnly Property P As Integer +End Class") + End Function + + + Public Async Function TestRemoveUnnecessaryAttributeSuppression_Event() As Task + Await TestInRegularAndScript1Async($" +Imports System +Class C + [||] + Public Event SampleEvent As EventHandler +End Class", $" +Imports System +Class C + Public Event SampleEvent As EventHandler End Class") End Function End Class diff --git a/src/Features/Core/Portable/Diagnostics/DocumentAnalysisExecutor.cs b/src/Features/Core/Portable/Diagnostics/DocumentAnalysisExecutor.cs index f6f13a3ca8741bda13c063cd8c6abcec1cad7b85..1e8554a1def8d2fb34d159981ab5bb0c9aa07250 100644 --- a/src/Features/Core/Portable/Diagnostics/DocumentAnalysisExecutor.cs +++ b/src/Features/Core/Portable/Diagnostics/DocumentAnalysisExecutor.cs @@ -210,10 +210,10 @@ private async Task> GetSemanticDiagnosticsAsync(Seman return await _compilationWithAnalyzers.GetAnalyzerSemanticDiagnosticsAsync(model, adjustedSpan, ImmutableArray.Create(analyzer), cancellationToken).ConfigureAwait(false); } - // We specially handle IPragmaSuppressionsAnalyzer by passing in the 'CompilationWithAnalyzers' - // context to compute unnecessary pragma suppression diagnostics. + // We specially handle IInlineSourceSuppressionsAnalyzer by passing in the 'CompilationWithAnalyzers' + // context to compute unnecessary inline source suppression diagnostics. // This is required because this analyzer relies on reported compiler + analyzer diagnostics - // for unnecessary pragma analysis. + // for unnecessary inline source suppression analysis. if (analyzer is IPragmaSuppressionsAnalyzer suppressionsAnalyzer && !AnalysisScope.Span.HasValue) { diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CSharpCompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CSharpCompilerExtensions.projitems index c11693b00b67c670b2316a613ea7466593d2101c..9943de663a1bb903a03eccfc1ac631d1755b4a14 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CSharpCompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CSharpCompilerExtensions.projitems @@ -36,6 +36,7 @@ + @@ -87,6 +88,7 @@ + diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/SemanticModelExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SemanticModelExtensions.cs similarity index 100% rename from src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/SemanticModelExtensions.cs rename to src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SemanticModelExtensions.cs diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SemanticFacts/CSharpSemanticFacts.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SemanticFacts/CSharpSemanticFacts.cs new file mode 100644 index 0000000000000000000000000000000000000000..475dc0bf0482c9f3fd70994ca206f7b84bf381a0 --- /dev/null +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SemanticFacts/CSharpSemanticFacts.cs @@ -0,0 +1,328 @@ +// 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.Diagnostics; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.LanguageServices; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp +{ + internal sealed class CSharpSemanticFacts : ISemanticFacts + { + internal static readonly CSharpSemanticFacts Instance = new CSharpSemanticFacts(); + + private CSharpSemanticFacts() + { + } + + public bool SupportsImplicitInterfaceImplementation => true; + + public bool ExposesAnonymousFunctionParameterNames => false; + + public bool IsWrittenTo(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + => (node as ExpressionSyntax).IsWrittenTo(); + + public bool IsOnlyWrittenTo(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + => (node as ExpressionSyntax).IsOnlyWrittenTo(); + + public bool IsInOutContext(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + => (node as ExpressionSyntax).IsInOutContext(); + + public bool IsInRefContext(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + => (node as ExpressionSyntax).IsInRefContext(); + + public bool IsInInContext(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + => (node as ExpressionSyntax).IsInInContext(); + + public bool CanReplaceWithRValue(SemanticModel semanticModel, SyntaxNode expression, CancellationToken cancellationToken) + => (expression as ExpressionSyntax).CanReplaceWithRValue(semanticModel, cancellationToken); + + public string GenerateNameForExpression(SemanticModel semanticModel, SyntaxNode expression, bool capitalize, CancellationToken cancellationToken) + => semanticModel.GenerateNameForExpression((ExpressionSyntax)expression, capitalize, cancellationToken); + + public ISymbol GetDeclaredSymbol(SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken) + { + var location = token.GetLocation(); + + foreach (var ancestor in token.GetAncestors()) + { + var symbol = semanticModel.GetDeclaredSymbol(ancestor, cancellationToken); + + if (symbol != null) + { + if (symbol.Locations.Contains(location)) + { + return symbol; + } + + // We found some symbol, but it defined something else. We're not going to have a higher node defining _another_ symbol with this token, so we can stop now. + return null; + } + + // If we hit an executable statement syntax and didn't find anything yet, we can just stop now -- anything higher would be a member declaration which won't be defined by something inside a statement. + if (CSharpSyntaxFacts.Instance.IsExecutableStatement(ancestor)) + { + return null; + } + } + + return null; + } + + public bool LastEnumValueHasInitializer(INamedTypeSymbol namedTypeSymbol) + { + var enumDecl = namedTypeSymbol.DeclaringSyntaxReferences.Select(r => r.GetSyntax()).OfType().FirstOrDefault(); + if (enumDecl != null) + { + var lastMember = enumDecl.Members.LastOrDefault(); + if (lastMember != null) + { + return lastMember.EqualsValue != null; + } + } + + return false; + } + + public bool SupportsParameterizedProperties => false; + + public bool TryGetSpeculativeSemanticModel(SemanticModel oldSemanticModel, SyntaxNode oldNode, SyntaxNode newNode, out SemanticModel speculativeModel) + { + Debug.Assert(oldNode.Kind() == newNode.Kind()); + + var model = oldSemanticModel; + if (!(oldNode is BaseMethodDeclarationSyntax oldMethod) || !(newNode is BaseMethodDeclarationSyntax newMethod) || oldMethod.Body == null) + { + speculativeModel = null; + return false; + } + + var success = model.TryGetSpeculativeSemanticModelForMethodBody(oldMethod.Body.OpenBraceToken.Span.End, newMethod, out var csharpModel); + speculativeModel = csharpModel; + return success; + } + + public ImmutableHashSet GetAliasNameSet(SemanticModel model, CancellationToken cancellationToken) + { + var original = model.GetOriginalSemanticModel(); + if (!original.SyntaxTree.HasCompilationUnitRoot) + { + return ImmutableHashSet.Create(); + } + + var root = original.SyntaxTree.GetCompilationUnitRoot(cancellationToken); + var builder = ImmutableHashSet.CreateBuilder(StringComparer.Ordinal); + + AppendAliasNames(root.Usings, builder); + AppendAliasNames(root.Members.OfType(), builder, cancellationToken); + + return builder.ToImmutable(); + } + + private static void AppendAliasNames(SyntaxList usings, ImmutableHashSet.Builder builder) + { + foreach (var @using in usings) + { + if (@using.Alias == null || @using.Alias.Name == null) + { + continue; + } + + @using.Alias.Name.Identifier.ValueText.AppendToAliasNameSet(builder); + } + } + + private void AppendAliasNames(IEnumerable namespaces, ImmutableHashSet.Builder builder, CancellationToken cancellationToken) + { + foreach (var @namespace in namespaces) + { + cancellationToken.ThrowIfCancellationRequested(); + + AppendAliasNames(@namespace.Usings, builder); + AppendAliasNames(@namespace.Members.OfType(), builder, cancellationToken); + } + } + + public ForEachSymbols GetForEachSymbols(SemanticModel semanticModel, SyntaxNode forEachStatement) + { + if (forEachStatement is CommonForEachStatementSyntax csforEachStatement) + { + var info = semanticModel.GetForEachStatementInfo(csforEachStatement); + return new ForEachSymbols( + info.GetEnumeratorMethod, + info.MoveNextMethod, + info.CurrentProperty, + info.DisposeMethod, + info.ElementType); + } + else + { + return default; + } + } + + public IMethodSymbol GetGetAwaiterMethod(SemanticModel semanticModel, SyntaxNode node) + { + if (node is AwaitExpressionSyntax awaitExpression) + { + var info = semanticModel.GetAwaitExpressionInfo(awaitExpression); + return info.GetAwaiterMethod; + } + + return null; + } + + public ImmutableArray GetDeconstructionAssignmentMethods(SemanticModel semanticModel, SyntaxNode node) + { + if (node is AssignmentExpressionSyntax assignment && assignment.IsDeconstruction()) + { + var builder = ArrayBuilder.GetInstance(); + FlattenDeconstructionMethods(semanticModel.GetDeconstructionInfo(assignment), builder); + return builder.ToImmutableAndFree(); + } + + return ImmutableArray.Empty; + } + + public ImmutableArray GetDeconstructionForEachMethods(SemanticModel semanticModel, SyntaxNode node) + { + if (node is ForEachVariableStatementSyntax @foreach) + { + var builder = ArrayBuilder.GetInstance(); + FlattenDeconstructionMethods(semanticModel.GetDeconstructionInfo(@foreach), builder); + return builder.ToImmutableAndFree(); + } + + return ImmutableArray.Empty; + } + + private static void FlattenDeconstructionMethods(DeconstructionInfo deconstruction, ArrayBuilder builder) + { + var method = deconstruction.Method; + if (method != null) + { + builder.Add(method); + } + + foreach (var nested in deconstruction.Nested) + { + FlattenDeconstructionMethods(nested, builder); + } + } + + public bool IsPartial(ITypeSymbol typeSymbol, CancellationToken cancellationToken) + { + var syntaxRefs = typeSymbol.DeclaringSyntaxReferences; + return syntaxRefs.Any(n => ((BaseTypeDeclarationSyntax)n.GetSyntax(cancellationToken)).Modifiers.Any(SyntaxKind.PartialKeyword)); + } + + public IEnumerable GetDeclaredSymbols( + SemanticModel semanticModel, SyntaxNode memberDeclaration, CancellationToken cancellationToken) + { + switch (memberDeclaration) + { + case FieldDeclarationSyntax field: + return field.Declaration.Variables.Select( + v => semanticModel.GetDeclaredSymbol(v, cancellationToken)); + + case EventFieldDeclarationSyntax eventField: + return eventField.Declaration.Variables.Select( + v => semanticModel.GetDeclaredSymbol(v, cancellationToken)); + + default: + return SpecializedCollections.SingletonEnumerable( + semanticModel.GetDeclaredSymbol(memberDeclaration, cancellationToken)); + } + } + + public IParameterSymbol FindParameterForArgument(SemanticModel semanticModel, SyntaxNode argumentNode, CancellationToken cancellationToken) + => ((ArgumentSyntax)argumentNode).DetermineParameter(semanticModel, allowParams: false, cancellationToken); + + public ImmutableArray GetBestOrAllSymbols(SemanticModel semanticModel, SyntaxNode node, SyntaxToken token, CancellationToken cancellationToken) + { + if (node == null) + return ImmutableArray.Empty; + + return node switch + { + AssignmentExpressionSyntax _ when token.Kind() == SyntaxKind.EqualsToken => GetDeconstructionAssignmentMethods(semanticModel, node).As(), + ForEachVariableStatementSyntax _ when token.Kind() == SyntaxKind.InKeyword => GetDeconstructionForEachMethods(semanticModel, node).As(), + _ => GetSymbolInfo(semanticModel, node, token, cancellationToken).GetBestOrAllSymbols(), + }; + } + + private static SymbolInfo GetSymbolInfo(SemanticModel semanticModel, SyntaxNode node, SyntaxToken token, CancellationToken cancellationToken) + { + switch (node) + { + case OrderByClauseSyntax orderByClauseSyntax: + if (token.Kind() == SyntaxKind.CommaToken) + { + // Returning SymbolInfo for a comma token is the last resort + // in an order by clause if no other tokens to bind to a are present. + // See also the proposal at https://github.com/dotnet/roslyn/issues/23394 + var separators = orderByClauseSyntax.Orderings.GetSeparators().ToImmutableList(); + var index = separators.IndexOf(token); + if (index >= 0 && (index + 1) < orderByClauseSyntax.Orderings.Count) + { + var ordering = orderByClauseSyntax.Orderings[index + 1]; + if (ordering.AscendingOrDescendingKeyword.Kind() == SyntaxKind.None) + { + return semanticModel.GetSymbolInfo(ordering, cancellationToken); + } + } + } + else if (orderByClauseSyntax.Orderings[0].AscendingOrDescendingKeyword.Kind() == SyntaxKind.None) + { + // The first ordering is displayed on the "orderby" keyword itself if there isn't a + // ascending/descending keyword. + return semanticModel.GetSymbolInfo(orderByClauseSyntax.Orderings[0], cancellationToken); + } + + return default; + case QueryClauseSyntax queryClauseSyntax: + var queryInfo = semanticModel.GetQueryClauseInfo(queryClauseSyntax, cancellationToken); + var hasCastInfo = queryInfo.CastInfo.Symbol != null; + var hasOperationInfo = queryInfo.OperationInfo.Symbol != null; + + if (hasCastInfo && hasOperationInfo) + { + // In some cases a single clause binds to more than one method. In those cases + // the tokens in the clause determine which of the two SymbolInfos are returned. + // See also the proposal at https://github.com/dotnet/roslyn/issues/23394 + return token.IsKind(SyntaxKind.InKeyword) ? queryInfo.CastInfo : queryInfo.OperationInfo; + } + + if (hasCastInfo) + { + return queryInfo.CastInfo; + } + + return queryInfo.OperationInfo; + } + + //Only in the orderby clause a comma can bind to a symbol. + if (token.IsKind(SyntaxKind.CommaToken)) + { + return default; + } + + return semanticModel.GetSymbolInfo(node, cancellationToken); + } + + public bool IsInsideNameOfExpression(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + => (node as ExpressionSyntax).IsInsideNameOfExpression(semanticModel, cancellationToken); + } +} diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs index 8e0a9ac08ff24f8283924b9acd89f968acee7927..cefaec9fd5cd55a70d15adc2d82863268c4ee990 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs @@ -894,10 +894,17 @@ private static void AppendTypeParameterList(StringBuilder builder, TypeParameter } } + public List GetTopLevelAndMethodLevelMembers(SyntaxNode root) + { + var list = new List(); + AppendMembers(root, list, topLevel: true, methodLevel: true); + return list; + } + public List GetMethodLevelMembers(SyntaxNode root) { var list = new List(); - AppendMethodLevelMembers(root, list); + AppendMembers(root, list, topLevel: false, methodLevel: true); return list; } @@ -916,17 +923,24 @@ public SyntaxList GetMembersOfNamespaceDeclaration(SyntaxNode namesp public SyntaxList GetMembersOfCompilationUnit(SyntaxNode compilationUnit) => ((CompilationUnitSyntax)compilationUnit).Members; - private void AppendMethodLevelMembers(SyntaxNode node, List list) + private void AppendMembers(SyntaxNode node, List list, bool topLevel, bool methodLevel) { + Debug.Assert(topLevel || methodLevel); + foreach (var member in node.GetMembers()) { if (IsTopLevelNodeWithMembers(member)) { - AppendMethodLevelMembers(member, list); + if (topLevel) + { + list.Add(member); + } + + AppendMembers(member, list, topLevel, methodLevel); continue; } - if (IsMethodLevelMember(member)) + if (methodLevel && IsMethodLevelMember(member)) { list.Add(member); } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems index e3ecebd0960b112a64582273b5b00c20897ebf26..fc9aba6ae34e20838b0469af65ebd79699b57263 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems @@ -345,10 +345,12 @@ + + diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Diagnostics/IPragmaSuppressionsAnalyzer.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Diagnostics/IPragmaSuppressionsAnalyzer.cs index 26eef9504908621eb20fddcf01dbf4cacefb1dc4..cf8e2af523d9700a6b04a6b99e235d6950afe4ac 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Diagnostics/IPragmaSuppressionsAnalyzer.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Diagnostics/IPragmaSuppressionsAnalyzer.cs @@ -13,12 +13,13 @@ namespace Microsoft.CodeAnalysis.Diagnostics { /// - /// Special IDE analyzer to flag unnecessary pragma suppressions. + /// Special IDE analyzer to flag unnecessary inline source suppressions, + /// i.e. pragma and local SuppressMessageAttribute suppressions. /// internal interface IPragmaSuppressionsAnalyzer { /// - /// Analyzes the tree, with an optional span scope, and report unnecessary pragma suppressions. + /// Analyzes the tree, with an optional span scope, and report unnecessary inline suppressions. /// Task AnalyzeAsync( SemanticModel semanticModel, diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/LanguageServices/SemanticsFactsService/ForEachSymbols.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SemanticFacts/ForEachSymbols.cs similarity index 100% rename from src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/LanguageServices/SemanticsFactsService/ForEachSymbols.cs rename to src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SemanticFacts/ForEachSymbols.cs diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SemanticFacts/ISemanticFacts.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SemanticFacts/ISemanticFacts.cs new file mode 100644 index 0000000000000000000000000000000000000000..28e168227f524dcfbb9227d5e0cbed546f23e8c1 --- /dev/null +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SemanticFacts/ISemanticFacts.cs @@ -0,0 +1,98 @@ +// 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.Threading; + +namespace Microsoft.CodeAnalysis.LanguageServices +{ + internal interface ISemanticFacts + { + /// + /// True if this language supports implementing an interface by signature only. If false, + /// implementations must specific explicitly which symbol they're implementing. + /// + bool SupportsImplicitInterfaceImplementation { get; } + + bool SupportsParameterizedProperties { get; } + + /// + /// True if anonymous functions in this language have signatures that include named + /// parameters that can be referenced later on when the function is invoked. Or, if the + /// anonymous function is simply a signature that will be assigned to a delegate, and the + /// delegate's parameter names are used when invoking. + /// + /// For example, in VB one can do this: + /// + /// dim v = Sub(x as Integer) Blah() + /// v(x:=4) + /// + /// However, in C# that would need to be: + /// + /// Action<int> v = (int x) => Blah(); + /// v(obj:=4) + /// + /// Note that in VB one can access 'x' outside of the declaration of the anonymous type. + /// While in C# 'x' can only be accessed within the anonymous type. + /// + bool ExposesAnonymousFunctionParameterNames { get; } + + /// + /// True if a write is performed to the given expression. Note: reads may also be performed + /// to the expression as well. For example, "++a". In this expression 'a' is both read from + /// and written to. + /// + bool IsWrittenTo(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); + + /// + /// True if a write is performed to the given expression. Note: unlike IsWrittenTo, this + /// will not return true if reads are performed on the expression as well. For example, + /// "++a" will return 'false'. However, 'a' in "out a" or "a = 1" will return true. + /// + bool IsOnlyWrittenTo(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); + bool IsInOutContext(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); + bool IsInRefContext(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); + bool IsInInContext(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); + + bool CanReplaceWithRValue(SemanticModel semanticModel, SyntaxNode expression, CancellationToken cancellationToken); + + string GenerateNameForExpression(SemanticModel semanticModel, SyntaxNode expression, bool capitalize, CancellationToken cancellationToken); + + ISymbol GetDeclaredSymbol(SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken); + + bool LastEnumValueHasInitializer(INamedTypeSymbol namedTypeSymbol); + + /// + /// return speculative semantic model for supported node. otherwise, it will return null + /// + bool TryGetSpeculativeSemanticModel(SemanticModel oldSemanticModel, SyntaxNode oldNode, SyntaxNode newNode, out SemanticModel speculativeModel); + + /// + /// get all alias names defined in the semantic model + /// + ImmutableHashSet GetAliasNameSet(SemanticModel model, CancellationToken cancellationToken); + + ForEachSymbols GetForEachSymbols(SemanticModel semanticModel, SyntaxNode forEachStatement); + + IMethodSymbol GetGetAwaiterMethod(SemanticModel semanticModel, SyntaxNode node); + + ImmutableArray GetDeconstructionAssignmentMethods(SemanticModel semanticModel, SyntaxNode node); + + ImmutableArray GetDeconstructionForEachMethods(SemanticModel semanticModel, SyntaxNode node); + + bool IsPartial(ITypeSymbol typeSymbol, CancellationToken cancellationToken); + + IEnumerable GetDeclaredSymbols(SemanticModel semanticModel, SyntaxNode memberDeclaration, CancellationToken cancellationToken); + + IParameterSymbol FindParameterForArgument(SemanticModel semanticModel, SyntaxNode argumentNode, CancellationToken cancellationToken); + +#nullable enable + ImmutableArray GetBestOrAllSymbols(SemanticModel semanticModel, SyntaxNode? node, SyntaxToken token, CancellationToken cancellationToken); +#nullable disable + + bool IsInsideNameOfExpression(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); + } +} diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs index 5e919d99724237289dccb5a8ac672450a6afe5f3..788a9b0754e925a174ef6bbe9c8d8a7d1edf4e3e 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs @@ -369,6 +369,7 @@ internal partial interface ISyntaxFacts bool IsClassDeclaration(SyntaxNode node); bool IsNamespaceDeclaration(SyntaxNode node); + List GetTopLevelAndMethodLevelMembers(SyntaxNode root); List GetMethodLevelMembers(SyntaxNode root); SyntaxList GetMembersOfTypeDeclaration(SyntaxNode typeDeclaration); SyntaxList GetMembersOfNamespaceDeclaration(SyntaxNode namespaceDeclaration); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb index a9e0c16c71f7b7b4f4e4f8b46ffd6e927714e414..709b4213fe00509d64efac79d7134a79d5e6367c 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb @@ -345,5 +345,196 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions Return False End Function + + ''' + ''' Decompose a name or member access expression into its component parts. + ''' + ''' The name or member access expression. + ''' The qualifier (or left-hand-side) of the name expression. This may be null if there is no qualifier. + ''' The name of the expression. + ''' The number of generic type parameters. + + Public Sub DecomposeName(expression As ExpressionSyntax, ByRef qualifier As ExpressionSyntax, ByRef name As String, ByRef arity As Integer) + Select Case expression.Kind + Case SyntaxKind.SimpleMemberAccessExpression + Dim memberAccess = DirectCast(expression, MemberAccessExpressionSyntax) + qualifier = memberAccess.Expression + name = memberAccess.Name.Identifier.ValueText + arity = memberAccess.Name.Arity + Case SyntaxKind.QualifiedName + Dim qualifiedName = DirectCast(expression, QualifiedNameSyntax) + qualifier = qualifiedName.Left + name = qualifiedName.Right.Identifier.ValueText + arity = qualifiedName.Arity + Case SyntaxKind.GenericName + Dim genericName = DirectCast(expression, GenericNameSyntax) + qualifier = Nothing + name = genericName.Identifier.ValueText + arity = genericName.Arity + Case SyntaxKind.IdentifierName + Dim identifierName = DirectCast(expression, IdentifierNameSyntax) + qualifier = Nothing + name = identifierName.Identifier.ValueText + arity = 0 + Case Else + qualifier = Nothing + name = Nothing + arity = 0 + End Select + End Sub + + Private Function CanReplace(symbol As ISymbol) As Boolean + Select Case symbol.Kind + Case SymbolKind.Field, + SymbolKind.Local, + SymbolKind.Method, + SymbolKind.Parameter, + SymbolKind.Property, + SymbolKind.RangeVariable + Return True + End Select + + Return False + End Function + + + Public Function CanReplaceWithRValue(expression As ExpressionSyntax, semanticModel As SemanticModel, cancellationToken As CancellationToken) As Boolean + Return expression IsNot Nothing AndAlso + Not expression.IsWrittenTo(semanticModel, cancellationToken) AndAlso + expression.CanReplaceWithLValue(semanticModel, cancellationToken) + End Function + + + Public Function CanReplaceWithLValue(expression As ExpressionSyntax, semanticModel As SemanticModel, cancellationToken As CancellationToken) As Boolean +#If False Then + ' Things that are definitely illegal to replace + If ContainsImplicitMemberAccess(expression) Then + Return False + End If +#End If + + If expression.IsKind(SyntaxKind.MyBaseExpression) OrElse + expression.IsKind(SyntaxKind.MyClassExpression) Then + Return False + End If + + If Not (TypeOf expression Is ObjectCreationExpressionSyntax) AndAlso + Not (TypeOf expression Is AnonymousObjectCreationExpressionSyntax) Then + Dim symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken) + If Not symbolInfo.GetBestOrAllSymbols().All(AddressOf CanReplace) Then + ' If the expression is actually a reference to a type, then it can't be replaced + ' with an arbitrary expression. + Return False + End If + End If + + ' Technically, you could introduce an LValue for "Goo" in "Goo()" even if "Goo" binds + ' to a method. (i.e. by assigning to a Func<...> type). However, this is so contrived + ' and none of the features that use this extension consider this replaceable. + If TypeOf expression.Parent Is InvocationExpressionSyntax Then + + ' If something is being invoked, then it's either something like Goo(), Goo.Bar(), or + ' SomeExpr() (i.e. Blah[1]()). In the first and second case, we only allow + ' replacement if Goo and Goo.Bar didn't bind to a method. If we can't bind it, we'll + ' assume it's a method and we don't allow it to be replaced either. However, if it's + ' an arbitrary expression, we do allow replacement. + If expression.IsKind(SyntaxKind.IdentifierName) OrElse expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) Then + Dim symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken) + If Not symbolInfo.GetBestOrAllSymbols().Any() Then + Return False + End If + + ' don't allow it to be replaced if it is bound to an indexed property + Return Not symbolInfo.GetBestOrAllSymbols().OfType(Of IMethodSymbol)().Any() AndAlso + Not symbolInfo.GetBestOrAllSymbols().OfType(Of IPropertySymbol)().Any() + Else + Return True + End If + End If + + ' expression in next statement's control variables should match one in the head + Dim nextStatement = expression.FirstAncestorOrSelf(Of NextStatementSyntax)() + If nextStatement IsNot Nothing Then + Return False + End If + + ' Direct parent kind checks. + If expression.IsParentKind(SyntaxKind.EqualsValue) OrElse + expression.IsParentKind(SyntaxKind.ParenthesizedExpression) OrElse + expression.IsParentKind(SyntaxKind.SelectStatement) OrElse + expression.IsParentKind(SyntaxKind.SyncLockStatement) OrElse + expression.IsParentKind(SyntaxKind.CollectionInitializer) OrElse + expression.IsParentKind(SyntaxKind.InferredFieldInitializer) OrElse + expression.IsParentKind(SyntaxKind.BinaryConditionalExpression) OrElse + expression.IsParentKind(SyntaxKind.TernaryConditionalExpression) OrElse + expression.IsParentKind(SyntaxKind.ReturnStatement) OrElse + expression.IsParentKind(SyntaxKind.YieldStatement) OrElse + expression.IsParentKind(SyntaxKind.XmlEmbeddedExpression) OrElse + expression.IsParentKind(SyntaxKind.ThrowStatement) OrElse + expression.IsParentKind(SyntaxKind.IfStatement) OrElse + expression.IsParentKind(SyntaxKind.WhileStatement) OrElse + expression.IsParentKind(SyntaxKind.ElseIfStatement) OrElse + expression.IsParentKind(SyntaxKind.ForEachStatement) OrElse + expression.IsParentKind(SyntaxKind.ForStatement) OrElse + expression.IsParentKind(SyntaxKind.ConditionalAccessExpression) OrElse + expression.IsParentKind(SyntaxKind.TypeOfIsExpression) OrElse + expression.IsParentKind(SyntaxKind.TypeOfIsNotExpression) Then + + Return True + End If + + ' Parent type checks + If TypeOf expression.Parent Is BinaryExpressionSyntax OrElse + TypeOf expression.Parent Is AssignmentStatementSyntax OrElse + TypeOf expression.Parent Is WhileOrUntilClauseSyntax OrElse + TypeOf expression.Parent Is SingleLineLambdaExpressionSyntax OrElse + TypeOf expression.Parent Is AwaitExpressionSyntax Then + Return True + End If + + ' Specific child checks. + If expression.CheckParent(Of NamedFieldInitializerSyntax)(Function(n) n.Expression Is expression) OrElse + expression.CheckParent(Of MemberAccessExpressionSyntax)(Function(m) m.Expression Is expression) OrElse + expression.CheckParent(Of TryCastExpressionSyntax)(Function(t) t.Expression Is expression) OrElse + expression.CheckParent(Of CatchFilterClauseSyntax)(Function(c) c.Filter Is expression) OrElse + expression.CheckParent(Of SimpleArgumentSyntax)(Function(n) n.Expression Is expression) OrElse + expression.CheckParent(Of DirectCastExpressionSyntax)(Function(d) d.Expression Is expression) OrElse + expression.CheckParent(Of FunctionAggregationSyntax)(Function(f) f.Argument Is expression) OrElse + expression.CheckParent(Of RangeArgumentSyntax)(Function(r) r.UpperBound Is expression) Then + Return True + End If + + ' Misc checks + If TypeOf expression.Parent Is ExpressionRangeVariableSyntax AndAlso + TypeOf expression.Parent.Parent Is QueryClauseSyntax Then + Dim rangeVariable = DirectCast(expression.Parent, ExpressionRangeVariableSyntax) + Dim selectClause = TryCast(rangeVariable.Parent, SelectClauseSyntax) + + ' Can't replace the expression in a select unless its the last select clause *or* + ' it's a select of the form "select a = " + If selectClause IsNot Nothing Then + If rangeVariable.NameEquals IsNot Nothing Then + Return True + End If + + Dim queryExpression = TryCast(selectClause.Parent, QueryExpressionSyntax) + If queryExpression IsNot Nothing Then + Return queryExpression.Clauses.Last() Is selectClause + End If + + Dim aggregateClause = TryCast(selectClause.Parent, AggregateClauseSyntax) + If aggregateClause IsNot Nothing Then + Return aggregateClause.AdditionalQueryOperators().Last() Is selectClause + End If + + Return False + End If + + ' Any other query type is ok. Note(cyrusn): This may be too broad. + Return True + End If + + Return False + End Function End Module End Namespace diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/Extensions/SemanticModelExtensions.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/SemanticModelExtensions.vb similarity index 100% rename from src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/Extensions/SemanticModelExtensions.vb rename to src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/SemanticModelExtensions.vb diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SemanticFacts/VisualBasicSemanticFacts.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SemanticFacts/VisualBasicSemanticFacts.vb new file mode 100644 index 0000000000000000000000000000000000000000..fd78d50e7e2c5ad4c23f13d02105f504eae5b526 --- /dev/null +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SemanticFacts/VisualBasicSemanticFacts.vb @@ -0,0 +1,250 @@ +' 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. + +Imports System.Collections.Immutable +Imports System.Runtime.InteropServices +Imports System.Threading +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.LanguageServices +Imports Microsoft.CodeAnalysis.VisualBasic.LanguageServices +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax + +Namespace Microsoft.CodeAnalysis.VisualBasic + Friend NotInheritable Class VisualBasicSemanticFacts + Implements ISemanticFacts + + Public Shared ReadOnly Instance As New VisualBasicSemanticFacts() + + Private Sub New() + End Sub + + Public ReadOnly Property SupportsImplicitInterfaceImplementation As Boolean Implements ISemanticFacts.SupportsImplicitInterfaceImplementation + Get + Return False + End Get + End Property + + Public ReadOnly Property ExposesAnonymousFunctionParameterNames As Boolean Implements ISemanticFacts.ExposesAnonymousFunctionParameterNames + Get + Return True + End Get + End Property + + Public Function IsOnlyWrittenTo(semanticModel As SemanticModel, node As SyntaxNode, cancellationToken As CancellationToken) As Boolean Implements ISemanticFacts.IsOnlyWrittenTo + Return TryCast(node, ExpressionSyntax).IsOnlyWrittenTo() + End Function + + Public Function IsWrittenTo(semanticModel As SemanticModel, node As SyntaxNode, cancellationToken As CancellationToken) As Boolean Implements ISemanticFacts.IsWrittenTo + Return TryCast(node, ExpressionSyntax).IsWrittenTo(semanticModel, cancellationToken) + End Function + + Public Function IsInOutContext(semanticModel As SemanticModel, node As SyntaxNode, cancellationToken As CancellationToken) As Boolean Implements ISemanticFacts.IsInOutContext + Return TryCast(node, ExpressionSyntax).IsInOutContext() + End Function + + Public Function IsInRefContext(semanticModel As SemanticModel, node As SyntaxNode, cancellationToken As CancellationToken) As Boolean Implements ISemanticFacts.IsInRefContext + Return TryCast(node, ExpressionSyntax).IsInRefContext(semanticModel, cancellationToken) + End Function + + Public Function IsInInContext(semanticModel As SemanticModel, node As SyntaxNode, cancellationToken As CancellationToken) As Boolean Implements ISemanticFacts.IsInInContext + Return TryCast(node, ExpressionSyntax).IsInInContext() + End Function + + Public Function CanReplaceWithRValue(semanticModel As SemanticModel, expression As SyntaxNode, cancellationToken As CancellationToken) As Boolean Implements ISemanticFacts.CanReplaceWithRValue + Return TryCast(expression, ExpressionSyntax).CanReplaceWithRValue(semanticModel, cancellationToken) + End Function + + Public Function GenerateNameForExpression(semanticModel As SemanticModel, + expression As SyntaxNode, + capitalize As Boolean, + cancellationToken As CancellationToken) As String Implements ISemanticFacts.GenerateNameForExpression + Return semanticModel.GenerateNameForExpression( + DirectCast(expression, ExpressionSyntax), capitalize, cancellationToken) + End Function + + Public Function GetDeclaredSymbol(semanticModel As SemanticModel, token As SyntaxToken, cancellationToken As CancellationToken) As ISymbol Implements ISemanticFacts.GetDeclaredSymbol + Dim location = token.GetLocation() + + For Each ancestor In token.GetAncestors(Of SyntaxNode)() + If Not TypeOf ancestor Is AggregationRangeVariableSyntax AndAlso + Not TypeOf ancestor Is CollectionRangeVariableSyntax AndAlso + Not TypeOf ancestor Is ExpressionRangeVariableSyntax AndAlso + Not TypeOf ancestor Is InferredFieldInitializerSyntax Then + + Dim symbol = semanticModel.GetDeclaredSymbol(ancestor) + + If symbol IsNot Nothing Then + If symbol.Locations.Contains(location) Then + Return symbol + End If + + ' We found some symbol, but it defined something else. We're not going to have a higher node defining _another_ symbol with this token, so we can stop now. + Return Nothing + End If + + ' If we hit an executable statement syntax and didn't find anything yet, we can just stop now -- anything higher would be a member declaration which won't be defined by something inside a statement. + If VisualBasicSyntaxFacts.Instance.IsExecutableStatement(ancestor) Then + Return Nothing + End If + End If + Next + + Return Nothing + End Function + + Public Function LastEnumValueHasInitializer(namedTypeSymbol As INamedTypeSymbol) As Boolean Implements ISemanticFacts.LastEnumValueHasInitializer + Dim enumStatement = namedTypeSymbol.DeclaringSyntaxReferences.Select(Function(r) r.GetSyntax()).OfType(Of EnumStatementSyntax).FirstOrDefault() + If enumStatement IsNot Nothing Then + Dim enumBlock = DirectCast(enumStatement.Parent, EnumBlockSyntax) + + Dim lastMember = TryCast(enumBlock.Members.LastOrDefault(), EnumMemberDeclarationSyntax) + If lastMember IsNot Nothing Then + Return lastMember.Initializer IsNot Nothing + End If + End If + + Return False + End Function + + Public ReadOnly Property SupportsParameterizedProperties As Boolean Implements ISemanticFacts.SupportsParameterizedProperties + Get + Return True + End Get + End Property + + Public Function TryGetSpeculativeSemanticModel(oldSemanticModel As SemanticModel, oldNode As SyntaxNode, newNode As SyntaxNode, ByRef speculativeModel As SemanticModel) As Boolean Implements ISemanticFacts.TryGetSpeculativeSemanticModel + Debug.Assert(oldNode.Kind = newNode.Kind) + + Dim model = oldSemanticModel + + ' currently we only support method. field support will be added later. + Dim oldMethod = TryCast(oldNode, MethodBlockBaseSyntax) + Dim newMethod = TryCast(newNode, MethodBlockBaseSyntax) + If oldMethod Is Nothing OrElse newMethod Is Nothing Then + speculativeModel = Nothing + Return False + End If + + ' No method body? + If oldMethod.Statements.IsEmpty AndAlso oldMethod.EndBlockStatement.IsMissing Then + speculativeModel = Nothing + Return False + End If + + Dim position As Integer + If model.IsSpeculativeSemanticModel Then + ' Chaining speculative semantic model is not supported, use the original model. + position = model.OriginalPositionForSpeculation + model = model.ParentModel + Contract.ThrowIfNull(model) + Contract.ThrowIfTrue(model.IsSpeculativeSemanticModel) + Else + position = oldMethod.BlockStatement.FullSpan.End + End If + + Dim vbSpeculativeModel As SemanticModel = Nothing + Dim success = model.TryGetSpeculativeSemanticModelForMethodBody(position, newMethod, vbSpeculativeModel) + speculativeModel = vbSpeculativeModel + Return success + End Function + + Public Function GetAliasNameSet(model As SemanticModel, cancellationToken As CancellationToken) As ImmutableHashSet(Of String) Implements ISemanticFacts.GetAliasNameSet + Dim original = DirectCast(model.GetOriginalSemanticModel(), SemanticModel) + + If Not original.SyntaxTree.HasCompilationUnitRoot Then + Return ImmutableHashSet.Create(Of String)() + End If + + Dim root = original.SyntaxTree.GetCompilationUnitRoot() + + Dim builder = ImmutableHashSet.CreateBuilder(Of String)(StringComparer.OrdinalIgnoreCase) + For Each globalImport In original.Compilation.AliasImports + globalImport.Name.AppendToAliasNameSet(builder) + Next + + For Each importsClause In root.GetAliasImportsClauses() + importsClause.Alias.Identifier.ValueText.AppendToAliasNameSet(builder) + Next + + Return builder.ToImmutable() + End Function + + Public Function GetForEachSymbols(model As SemanticModel, forEachStatement As SyntaxNode) As ForEachSymbols Implements ISemanticFacts.GetForEachSymbols + + Dim vbForEachStatement = TryCast(forEachStatement, ForEachStatementSyntax) + If vbForEachStatement IsNot Nothing Then + Dim info = model.GetForEachStatementInfo(vbForEachStatement) + Return New ForEachSymbols( + info.GetEnumeratorMethod, + info.MoveNextMethod, + info.CurrentProperty, + info.DisposeMethod, + info.ElementType) + End If + + Dim vbForBlock = TryCast(forEachStatement, ForEachBlockSyntax) + If vbForBlock IsNot Nothing Then + Dim info = model.GetForEachStatementInfo(vbForBlock) + Return New ForEachSymbols( + info.GetEnumeratorMethod, + info.MoveNextMethod, + info.CurrentProperty, + info.DisposeMethod, + info.ElementType) + End If + + Return Nothing + End Function + + Public Function GetGetAwaiterMethod(model As SemanticModel, node As SyntaxNode) As IMethodSymbol Implements ISemanticFacts.GetGetAwaiterMethod + If node.IsKind(SyntaxKind.AwaitExpression) Then + Dim awaitExpression = DirectCast(node, AwaitExpressionSyntax) + Dim info = model.GetAwaitExpressionInfo(awaitExpression) + Return info.GetAwaiterMethod + End If + + Return Nothing + End Function + + Public Function GetDeconstructionAssignmentMethods(model As SemanticModel, deconstruction As SyntaxNode) As ImmutableArray(Of IMethodSymbol) Implements ISemanticFacts.GetDeconstructionAssignmentMethods + Return ImmutableArray(Of IMethodSymbol).Empty + End Function + + Public Function GetDeconstructionForEachMethods(model As SemanticModel, deconstruction As SyntaxNode) As ImmutableArray(Of IMethodSymbol) Implements ISemanticFacts.GetDeconstructionForEachMethods + Return ImmutableArray(Of IMethodSymbol).Empty + End Function + + Public Function IsPartial(typeSymbol As ITypeSymbol, cancellationToken As CancellationToken) As Boolean Implements ISemanticFacts.IsPartial + Dim syntaxRefs = typeSymbol.DeclaringSyntaxReferences + Return syntaxRefs.Any( + Function(n As SyntaxReference) + Return DirectCast(n.GetSyntax(cancellationToken), TypeStatementSyntax).Modifiers.Any(SyntaxKind.PartialKeyword) + End Function) + End Function + + Public Function GetDeclaredSymbols(semanticModel As SemanticModel, memberDeclaration As SyntaxNode, cancellationToken As CancellationToken) As IEnumerable(Of ISymbol) Implements ISemanticFacts.GetDeclaredSymbols + If TypeOf memberDeclaration Is FieldDeclarationSyntax Then + Return DirectCast(memberDeclaration, FieldDeclarationSyntax).Declarators. + SelectMany(Function(d) d.Names.AsEnumerable()). + Select(Function(n) semanticModel.GetDeclaredSymbol(n, cancellationToken)) + End If + + Return SpecializedCollections.SingletonEnumerable(semanticModel.GetDeclaredSymbol(memberDeclaration, cancellationToken)) + End Function + + Public Function FindParameterForArgument(semanticModel As SemanticModel, argumentNode As SyntaxNode, cancellationToken As CancellationToken) As IParameterSymbol Implements ISemanticFacts.FindParameterForArgument + Return DirectCast(argumentNode, ArgumentSyntax).DetermineParameter(semanticModel, allowParamArray:=False, cancellationToken) + End Function + + Public Function GetBestOrAllSymbols(semanticModel As SemanticModel, node As SyntaxNode, token As SyntaxToken, cancellationToken As CancellationToken) As ImmutableArray(Of ISymbol) Implements ISemanticFacts.GetBestOrAllSymbols + Return If(node Is Nothing, + ImmutableArray(Of ISymbol).Empty, + semanticModel.GetSymbolInfo(node, cancellationToken).GetBestOrAllSymbols()) + End Function + + Public Function IsInsideNameOfExpression(semanticModel As SemanticModel, node As SyntaxNode, cancellationToken As CancellationToken) As Boolean Implements ISemanticFacts.IsInsideNameOfExpression + Return node.FirstAncestorOrSelf(Of NameOfExpressionSyntax) IsNot Nothing + End Function + End Class +End Namespace diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb index b117fcd5d51943e85b21c142bad364930d0fb047..8b1e2e98343c7ae67d8cda60a2497923bb4a272a 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb @@ -893,9 +893,15 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.LanguageServices Return TextSpan.FromBounds(list.First.SpanStart, list.Last.Span.End) End Function + Public Function GetTopLevelAndMethodLevelMembers(root As SyntaxNode) As List(Of SyntaxNode) Implements ISyntaxFacts.GetTopLevelAndMethodLevelMembers + Dim list = New List(Of SyntaxNode)() + AppendMembers(root, list, topLevel:=True, methodLevel:=True) + Return list + End Function + Public Function GetMethodLevelMembers(root As SyntaxNode) As List(Of SyntaxNode) Implements ISyntaxFacts.GetMethodLevelMembers Dim list = New List(Of SyntaxNode)() - AppendMethodLevelMembers(root, list) + AppendMembers(root, list, topLevel:=False, methodLevel:=True) Return list End Function @@ -1056,14 +1062,20 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.LanguageServices End If End Sub - Private Sub AppendMethodLevelMembers(node As SyntaxNode, list As List(Of SyntaxNode)) + Private Sub AppendMembers(node As SyntaxNode, list As List(Of SyntaxNode), topLevel As Boolean, methodLevel As Boolean) + Debug.Assert(topLevel OrElse methodLevel) + For Each member In node.GetMembers() If IsTopLevelNodeWithMembers(member) Then - AppendMethodLevelMembers(member, list) + If topLevel Then + list.Add(member) + End If + + AppendMembers(member, list, topLevel, methodLevel) Continue For End If - If IsMethodLevelMember(member) Then + If methodLevel AndAlso IsMethodLevelMember(member) Then list.Add(member) End If Next diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/VisualBasicCompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/VisualBasicCompilerExtensions.projitems index b0f85b99f81587b5a72ff2e3cf1819144e3d597d..86c7289a2e1150d56ed4bc3a3124a1fb8a8a3e3e 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/VisualBasicCompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/VisualBasicCompilerExtensions.projitems @@ -18,6 +18,7 @@ + @@ -28,6 +29,7 @@ + diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/CSharpWorkspaceExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/CSharpWorkspaceExtensions.projitems index 7d57be13cda2cd36ac7a689a8e593b5365bcdbae..65f29aa8427a52ac29457a942f79c7800e22a81b 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/CSharpWorkspaceExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/CSharpWorkspaceExtensions.projitems @@ -28,7 +28,6 @@ - diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/LanguageServices/CSharpSemanticFactsService.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/LanguageServices/CSharpSemanticFactsService.cs index b5b0f3e936e9334f7d8ee46d081cdb5dfd9451f9..b93f294ea7f87bd15110b6d65830123b6d6955b7 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/LanguageServices/CSharpSemanticFactsService.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/LanguageServices/CSharpSemanticFactsService.cs @@ -2,29 +2,25 @@ // 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.Diagnostics; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.LanguageServices; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.LanguageServices; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp { - internal class CSharpSemanticFactsService : AbstractSemanticFactsService, ISemanticFactsService + internal sealed class CSharpSemanticFactsService : AbstractSemanticFactsService, ISemanticFactsService { internal static readonly CSharpSemanticFactsService Instance = new CSharpSemanticFactsService(); protected override ISyntaxFacts SyntaxFacts => CSharpSyntaxFacts.Instance; + protected override ISemanticFacts SemanticFacts => CSharpSemanticFacts.Instance; private CSharpSemanticFactsService() { @@ -57,10 +53,6 @@ bool ShouldDescendInto(SyntaxNode node) } } - public bool SupportsImplicitInterfaceImplementation => true; - - public bool ExposesAnonymousFunctionParameterNames => false; - public bool IsExpressionContext(SemanticModel semanticModel, int position, CancellationToken cancellationToken) { return semanticModel.SyntaxTree.IsExpressionContext( @@ -107,277 +99,5 @@ public bool IsLabelContext(SemanticModel semanticModel, int position, Cancellati public bool IsAttributeNameContext(SemanticModel semanticModel, int position, CancellationToken cancellationToken) => semanticModel.SyntaxTree.IsAttributeNameContext(position, cancellationToken); - - public bool IsWrittenTo(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) - => (node as ExpressionSyntax).IsWrittenTo(); - - public bool IsOnlyWrittenTo(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) - => (node as ExpressionSyntax).IsOnlyWrittenTo(); - - public bool IsInOutContext(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) - => (node as ExpressionSyntax).IsInOutContext(); - - public bool IsInRefContext(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) - => (node as ExpressionSyntax).IsInRefContext(); - - public bool IsInInContext(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) - => (node as ExpressionSyntax).IsInInContext(); - - public bool CanReplaceWithRValue(SemanticModel semanticModel, SyntaxNode expression, CancellationToken cancellationToken) - => (expression as ExpressionSyntax).CanReplaceWithRValue(semanticModel, cancellationToken); - - public string GenerateNameForExpression(SemanticModel semanticModel, SyntaxNode expression, bool capitalize, CancellationToken cancellationToken) - => semanticModel.GenerateNameForExpression((ExpressionSyntax)expression, capitalize, cancellationToken); - - public ISymbol GetDeclaredSymbol(SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken) - { - var location = token.GetLocation(); - - foreach (var ancestor in token.GetAncestors()) - { - var symbol = semanticModel.GetDeclaredSymbol(ancestor, cancellationToken); - - if (symbol != null) - { - if (symbol.Locations.Contains(location)) - { - return symbol; - } - - // We found some symbol, but it defined something else. We're not going to have a higher node defining _another_ symbol with this token, so we can stop now. - return null; - } - - // If we hit an executable statement syntax and didn't find anything yet, we can just stop now -- anything higher would be a member declaration which won't be defined by something inside a statement. - if (SyntaxFacts.IsExecutableStatement(ancestor)) - { - return null; - } - } - - return null; - } - - public bool LastEnumValueHasInitializer(INamedTypeSymbol namedTypeSymbol) - { - var enumDecl = namedTypeSymbol.DeclaringSyntaxReferences.Select(r => r.GetSyntax()).OfType().FirstOrDefault(); - if (enumDecl != null) - { - var lastMember = enumDecl.Members.LastOrDefault(); - if (lastMember != null) - { - return lastMember.EqualsValue != null; - } - } - - return false; - } - - public bool SupportsParameterizedProperties => false; - - public ImmutableHashSet GetAliasNameSet(SemanticModel model, CancellationToken cancellationToken) - { - var original = model.GetOriginalSemanticModel(); - if (!original.SyntaxTree.HasCompilationUnitRoot) - { - return ImmutableHashSet.Create(); - } - - var root = original.SyntaxTree.GetCompilationUnitRoot(cancellationToken); - var builder = ImmutableHashSet.CreateBuilder(StringComparer.Ordinal); - - AppendAliasNames(root.Usings, builder); - AppendAliasNames(root.Members.OfType(), builder, cancellationToken); - - return builder.ToImmutable(); - } - - private static void AppendAliasNames(SyntaxList usings, ImmutableHashSet.Builder builder) - { - foreach (var @using in usings) - { - if (@using.Alias == null || @using.Alias.Name == null) - { - continue; - } - - @using.Alias.Name.Identifier.ValueText.AppendToAliasNameSet(builder); - } - } - - private void AppendAliasNames(IEnumerable namespaces, ImmutableHashSet.Builder builder, CancellationToken cancellationToken) - { - foreach (var @namespace in namespaces) - { - cancellationToken.ThrowIfCancellationRequested(); - - AppendAliasNames(@namespace.Usings, builder); - AppendAliasNames(@namespace.Members.OfType(), builder, cancellationToken); - } - } - - public ForEachSymbols GetForEachSymbols(SemanticModel semanticModel, SyntaxNode forEachStatement) - { - if (forEachStatement is CommonForEachStatementSyntax csforEachStatement) - { - var info = semanticModel.GetForEachStatementInfo(csforEachStatement); - return new ForEachSymbols( - info.GetEnumeratorMethod, - info.MoveNextMethod, - info.CurrentProperty, - info.DisposeMethod, - info.ElementType); - } - else - { - return default; - } - } - - public IMethodSymbol GetGetAwaiterMethod(SemanticModel semanticModel, SyntaxNode node) - { - if (node is AwaitExpressionSyntax awaitExpression) - { - var info = semanticModel.GetAwaitExpressionInfo(awaitExpression); - return info.GetAwaiterMethod; - } - - return null; - } - - public ImmutableArray GetDeconstructionAssignmentMethods(SemanticModel semanticModel, SyntaxNode node) - { - if (node is AssignmentExpressionSyntax assignment && assignment.IsDeconstruction()) - { - var builder = ArrayBuilder.GetInstance(); - FlattenDeconstructionMethods(semanticModel.GetDeconstructionInfo(assignment), builder); - return builder.ToImmutableAndFree(); - } - - return ImmutableArray.Empty; - } - - public ImmutableArray GetDeconstructionForEachMethods(SemanticModel semanticModel, SyntaxNode node) - { - if (node is ForEachVariableStatementSyntax @foreach) - { - var builder = ArrayBuilder.GetInstance(); - FlattenDeconstructionMethods(semanticModel.GetDeconstructionInfo(@foreach), builder); - return builder.ToImmutableAndFree(); - } - - return ImmutableArray.Empty; - } - - private static void FlattenDeconstructionMethods(DeconstructionInfo deconstruction, ArrayBuilder builder) - { - var method = deconstruction.Method; - if (method != null) - { - builder.Add(method); - } - - foreach (var nested in deconstruction.Nested) - { - FlattenDeconstructionMethods(nested, builder); - } - } - - public bool IsPartial(ITypeSymbol typeSymbol, CancellationToken cancellationToken) - { - var syntaxRefs = typeSymbol.DeclaringSyntaxReferences; - return syntaxRefs.Any(n => ((BaseTypeDeclarationSyntax)n.GetSyntax(cancellationToken)).Modifiers.Any(SyntaxKind.PartialKeyword)); - } - - public IEnumerable GetDeclaredSymbols( - SemanticModel semanticModel, SyntaxNode memberDeclaration, CancellationToken cancellationToken) - { - if (memberDeclaration is FieldDeclarationSyntax field) - { - return field.Declaration.Variables.Select( - v => semanticModel.GetDeclaredSymbol(v, cancellationToken)); - } - - return SpecializedCollections.SingletonEnumerable( - semanticModel.GetDeclaredSymbol(memberDeclaration, cancellationToken)); - } - - public IParameterSymbol FindParameterForArgument(SemanticModel semanticModel, SyntaxNode argumentNode, CancellationToken cancellationToken) - => ((ArgumentSyntax)argumentNode).DetermineParameter(semanticModel, allowParams: false, cancellationToken); - - public ImmutableArray GetBestOrAllSymbols(SemanticModel semanticModel, SyntaxNode node, SyntaxToken token, CancellationToken cancellationToken) - { - if (node == null) - return ImmutableArray.Empty; - - return node switch - { - AssignmentExpressionSyntax _ when token.Kind() == SyntaxKind.EqualsToken => GetDeconstructionAssignmentMethods(semanticModel, node).As(), - ForEachVariableStatementSyntax _ when token.Kind() == SyntaxKind.InKeyword => GetDeconstructionForEachMethods(semanticModel, node).As(), - _ => GetSymbolInfo(semanticModel, node, token, cancellationToken).GetBestOrAllSymbols(), - }; - } - - private static SymbolInfo GetSymbolInfo(SemanticModel semanticModel, SyntaxNode node, SyntaxToken token, CancellationToken cancellationToken) - { - switch (node) - { - case OrderByClauseSyntax orderByClauseSyntax: - if (token.Kind() == SyntaxKind.CommaToken) - { - // Returning SymbolInfo for a comma token is the last resort - // in an order by clause if no other tokens to bind to a are present. - // See also the proposal at https://github.com/dotnet/roslyn/issues/23394 - var separators = orderByClauseSyntax.Orderings.GetSeparators().ToImmutableList(); - var index = separators.IndexOf(token); - if (index >= 0 && (index + 1) < orderByClauseSyntax.Orderings.Count) - { - var ordering = orderByClauseSyntax.Orderings[index + 1]; - if (ordering.AscendingOrDescendingKeyword.Kind() == SyntaxKind.None) - { - return semanticModel.GetSymbolInfo(ordering, cancellationToken); - } - } - } - else if (orderByClauseSyntax.Orderings[0].AscendingOrDescendingKeyword.Kind() == SyntaxKind.None) - { - // The first ordering is displayed on the "orderby" keyword itself if there isn't a - // ascending/descending keyword. - return semanticModel.GetSymbolInfo(orderByClauseSyntax.Orderings[0], cancellationToken); - } - - return default; - case QueryClauseSyntax queryClauseSyntax: - var queryInfo = semanticModel.GetQueryClauseInfo(queryClauseSyntax, cancellationToken); - var hasCastInfo = queryInfo.CastInfo.Symbol != null; - var hasOperationInfo = queryInfo.OperationInfo.Symbol != null; - - if (hasCastInfo && hasOperationInfo) - { - // In some cases a single clause binds to more than one method. In those cases - // the tokens in the clause determine which of the two SymbolInfos are returned. - // See also the proposal at https://github.com/dotnet/roslyn/issues/23394 - return token.IsKind(SyntaxKind.InKeyword) ? queryInfo.CastInfo : queryInfo.OperationInfo; - } - - if (hasCastInfo) - { - return queryInfo.CastInfo; - } - - return queryInfo.OperationInfo; - } - - //Only in the orderby clause a comma can bind to a symbol. - if (token.IsKind(SyntaxKind.CommaToken)) - { - return default; - } - - return semanticModel.GetSymbolInfo(node, cancellationToken); - } - - public bool IsInsideNameOfExpression(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) - => (node as ExpressionSyntax).IsInsideNameOfExpression(semanticModel, cancellationToken); } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/LanguageServices/SemanticsFactsService/AbstractSemanticFactsService.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/LanguageServices/SemanticsFactsService/AbstractSemanticFactsService.cs index 5984d3d52de1d17c8308a64da6a6ec623f2d9a1b..f9f414e3e3aac757e6e9f6bc1bfa6ae03e45b97c 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/LanguageServices/SemanticsFactsService/AbstractSemanticFactsService.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/LanguageServices/SemanticsFactsService/AbstractSemanticFactsService.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis.Shared.Extensions; @@ -11,9 +12,11 @@ namespace Microsoft.CodeAnalysis.LanguageServices { - internal abstract class AbstractSemanticFactsService + internal abstract class AbstractSemanticFactsService : ISemanticFacts { protected abstract ISyntaxFacts SyntaxFacts { get; } + protected abstract ISemanticFacts SemanticFacts { get; } + protected abstract SyntaxToken ToIdentifierToken(string identifier); public SyntaxToken GenerateUniqueName( @@ -80,5 +83,74 @@ public SyntaxToken GenerateUniqueName(string baseName, IEnumerable usedN NameGenerator.EnsureUniqueness( baseName, usedNames, this.SyntaxFacts.IsCaseSensitive)); } + + #region ISemanticFacts implementation + + public bool SupportsImplicitInterfaceImplementation => SemanticFacts.SupportsImplicitInterfaceImplementation; + + public bool SupportsParameterizedProperties => SemanticFacts.SupportsParameterizedProperties; + + public bool ExposesAnonymousFunctionParameterNames => SemanticFacts.ExposesAnonymousFunctionParameterNames; + + public bool IsWrittenTo(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + => SemanticFacts.IsWrittenTo(semanticModel, node, cancellationToken); + + public bool IsOnlyWrittenTo(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + => SemanticFacts.IsOnlyWrittenTo(semanticModel, node, cancellationToken); + + public bool IsInOutContext(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + => SemanticFacts.IsInOutContext(semanticModel, node, cancellationToken); + + public bool IsInRefContext(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + => SemanticFacts.IsInRefContext(semanticModel, node, cancellationToken); + + public bool IsInInContext(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + => SemanticFacts.IsInInContext(semanticModel, node, cancellationToken); + + public bool CanReplaceWithRValue(SemanticModel semanticModel, SyntaxNode expression, CancellationToken cancellationToken) + => SemanticFacts.CanReplaceWithRValue(semanticModel, expression, cancellationToken); + + public string GenerateNameForExpression(SemanticModel semanticModel, SyntaxNode expression, bool capitalize, CancellationToken cancellationToken) + => SemanticFacts.GenerateNameForExpression(semanticModel, expression, capitalize, cancellationToken); + + public ISymbol GetDeclaredSymbol(SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken) + => SemanticFacts.GetDeclaredSymbol(semanticModel, token, cancellationToken); + + public bool LastEnumValueHasInitializer(INamedTypeSymbol namedTypeSymbol) + => SemanticFacts.LastEnumValueHasInitializer(namedTypeSymbol); + + public bool TryGetSpeculativeSemanticModel(SemanticModel oldSemanticModel, SyntaxNode oldNode, SyntaxNode newNode, out SemanticModel speculativeModel) + => SemanticFacts.TryGetSpeculativeSemanticModel(oldSemanticModel, oldNode, newNode, out speculativeModel); + + public ImmutableHashSet GetAliasNameSet(SemanticModel model, CancellationToken cancellationToken) + => SemanticFacts.GetAliasNameSet(model, cancellationToken); + + public ForEachSymbols GetForEachSymbols(SemanticModel semanticModel, SyntaxNode forEachStatement) + => SemanticFacts.GetForEachSymbols(semanticModel, forEachStatement); + + public IMethodSymbol GetGetAwaiterMethod(SemanticModel semanticModel, SyntaxNode node) + => SemanticFacts.GetGetAwaiterMethod(semanticModel, node); + + public ImmutableArray GetDeconstructionAssignmentMethods(SemanticModel semanticModel, SyntaxNode node) + => SemanticFacts.GetDeconstructionAssignmentMethods(semanticModel, node); + + public ImmutableArray GetDeconstructionForEachMethods(SemanticModel semanticModel, SyntaxNode node) + => SemanticFacts.GetDeconstructionForEachMethods(semanticModel, node); + + public bool IsPartial(ITypeSymbol typeSymbol, CancellationToken cancellationToken) + => SemanticFacts.IsPartial(typeSymbol, cancellationToken); + + public IEnumerable GetDeclaredSymbols(SemanticModel semanticModel, SyntaxNode memberDeclaration, CancellationToken cancellationToken) + => SemanticFacts.GetDeclaredSymbols(semanticModel, memberDeclaration, cancellationToken); + + public IParameterSymbol FindParameterForArgument(SemanticModel semanticModel, SyntaxNode argumentNode, CancellationToken cancellationToken) + => SemanticFacts.FindParameterForArgument(semanticModel, argumentNode, cancellationToken); + + public ImmutableArray GetBestOrAllSymbols(SemanticModel semanticModel, SyntaxNode node, SyntaxToken token, CancellationToken cancellationToken) + => SemanticFacts.GetBestOrAllSymbols(semanticModel, node, token, cancellationToken); + + public bool IsInsideNameOfExpression(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + => SemanticFacts.IsInsideNameOfExpression(semanticModel, node, cancellationToken); + #endregion } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/LanguageServices/SemanticsFactsService/ISemanticFactsService.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/LanguageServices/SemanticsFactsService/ISemanticFactsService.cs index eb41ad759b34640c1262730bea325628c56b595a..bd0fd1d353538fe172dd186a0f17c620288f6a8b 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/LanguageServices/SemanticsFactsService/ISemanticFactsService.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/LanguageServices/SemanticsFactsService/ISemanticFactsService.cs @@ -4,43 +4,13 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Threading; using Microsoft.CodeAnalysis.Host; namespace Microsoft.CodeAnalysis.LanguageServices { - internal interface ISemanticFactsService : ILanguageService + internal interface ISemanticFactsService : ISemanticFacts, ILanguageService { - /// - /// True if this language supports implementing an interface by signature only. If false, - /// implementations must specific explicitly which symbol they're implementing. - /// - bool SupportsImplicitInterfaceImplementation { get; } - - bool SupportsParameterizedProperties { get; } - - /// - /// True if anonymous functions in this language have signatures that include named - /// parameters that can be referenced later on when the function is invoked. Or, if the - /// anonymous function is simply a signature that will be assigned to a delegate, and the - /// delegate's parameter names are used when invoking. - /// - /// For example, in VB one can do this: - /// - /// dim v = Sub(x as Integer) Blah() - /// v(x:=4) - /// - /// However, in C# that would need to be: - /// - /// Action<int> v = (int x) => Blah(); - /// v(obj:=4) - /// - /// Note that in VB one can access 'x' outside of the declaration of the anonymous type. - /// While in C# 'x' can only be accessed within the anonymous type. - /// - bool ExposesAnonymousFunctionParameterNames { get; } - bool IsExpressionContext(SemanticModel semanticModel, int position, CancellationToken cancellationToken); bool IsStatementContext(SemanticModel semanticModel, int position, CancellationToken cancellationToken); bool IsTypeContext(SemanticModel semanticModel, int position, CancellationToken cancellationToken); @@ -54,54 +24,6 @@ internal interface ISemanticFactsService : ILanguageService bool IsInExpressionTree(SemanticModel semanticModel, SyntaxNode node, INamedTypeSymbol expressionTypeOpt, CancellationToken cancellationToken); - /// - /// True if a write is performed to the given expression. Note: reads may also be performed - /// to the expression as well. For example, "++a". In this expression 'a' is both read from - /// and written to. - /// - bool IsWrittenTo(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); - - /// - /// True if a write is performed to the given expression. Note: unlike IsWrittenTo, this - /// will not return true if reads are performed on the expression as well. For example, - /// "++a" will return 'false'. However, 'a' in "out a" or "a = 1" will return true. - /// - bool IsOnlyWrittenTo(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); - bool IsInOutContext(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); - bool IsInRefContext(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); - bool IsInInContext(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); - - bool CanReplaceWithRValue(SemanticModel semanticModel, SyntaxNode expression, CancellationToken cancellationToken); - - string GenerateNameForExpression(SemanticModel semanticModel, SyntaxNode expression, bool capitalize, CancellationToken cancellationToken); - - ISymbol GetDeclaredSymbol(SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken); - - bool LastEnumValueHasInitializer(INamedTypeSymbol namedTypeSymbol); - - /// - /// get all alias names defined in the semantic model - /// - ImmutableHashSet GetAliasNameSet(SemanticModel model, CancellationToken cancellationToken); - - ForEachSymbols GetForEachSymbols(SemanticModel semanticModel, SyntaxNode forEachStatement); - - IMethodSymbol GetGetAwaiterMethod(SemanticModel semanticModel, SyntaxNode node); - - ImmutableArray GetDeconstructionAssignmentMethods(SemanticModel semanticModel, SyntaxNode node); - - ImmutableArray GetDeconstructionForEachMethods(SemanticModel semanticModel, SyntaxNode node); - - bool IsPartial(ITypeSymbol typeSymbol, CancellationToken cancellationToken); - - IEnumerable GetDeclaredSymbols(SemanticModel semanticModel, SyntaxNode memberDeclaration, CancellationToken cancellationToken); - - IParameterSymbol FindParameterForArgument(SemanticModel semanticModel, SyntaxNode argumentNode, CancellationToken cancellationToken); - -#nullable enable - ImmutableArray GetBestOrAllSymbols(SemanticModel semanticModel, SyntaxNode? node, SyntaxToken token, CancellationToken cancellationToken); -#nullable disable - SyntaxToken GenerateUniqueName( SemanticModel semanticModel, SyntaxNode location, SyntaxNode containerOpt, string baseName, CancellationToken cancellationToken); @@ -118,7 +40,5 @@ internal interface ISemanticFactsService : ILanguageService SyntaxNode containerOpt, string baseName, CancellationToken cancellationToken); SyntaxToken GenerateUniqueName(string baseName, IEnumerable usedNames); - - bool IsInsideNameOfExpression(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken); } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/WorkspaceExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/WorkspaceExtensions.projitems index 2b9e49214e7761cb759d60775c7a1c5713e522c0..fd630ae4ba52c3e1dfd3132efd732f52851742e8 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/WorkspaceExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/WorkspaceExtensions.projitems @@ -48,7 +48,6 @@ - diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb index 745a4116756e00291b6093d41e7b1fef435476e5..22a59e2d194b44909777af19c94c3322f5d7474d 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb @@ -39,43 +39,6 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions result) End Function - ''' - ''' Decompose a name or member access expression into its component parts. - ''' - ''' The name or member access expression. - ''' The qualifier (or left-hand-side) of the name expression. This may be null if there is no qualifier. - ''' The name of the expression. - ''' The number of generic type parameters. - - Public Sub DecomposeName(expression As ExpressionSyntax, ByRef qualifier As ExpressionSyntax, ByRef name As String, ByRef arity As Integer) - Select Case expression.Kind - Case SyntaxKind.SimpleMemberAccessExpression - Dim memberAccess = DirectCast(expression, MemberAccessExpressionSyntax) - qualifier = memberAccess.Expression - name = memberAccess.Name.Identifier.ValueText - arity = memberAccess.Name.Arity - Case SyntaxKind.QualifiedName - Dim qualifiedName = DirectCast(expression, QualifiedNameSyntax) - qualifier = qualifiedName.Left - name = qualifiedName.Right.Identifier.ValueText - arity = qualifiedName.Arity - Case SyntaxKind.GenericName - Dim genericName = DirectCast(expression, GenericNameSyntax) - qualifier = Nothing - name = genericName.Identifier.ValueText - arity = genericName.Arity - Case SyntaxKind.IdentifierName - Dim identifierName = DirectCast(expression, IdentifierNameSyntax) - qualifier = Nothing - name = identifierName.Identifier.ValueText - arity = 0 - Case Else - qualifier = Nothing - name = Nothing - arity = 0 - End Select - End Sub - Public Function TryGetNameParts(expression As ExpressionSyntax, ByRef parts As IList(Of String)) As Boolean Dim partsList = New List(Of String) @@ -244,160 +207,6 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions Return simpleArgument IsNot Nothing AndAlso simpleArgument.NameColonEquals.Name Is expression End Function - Private Function CanReplace(symbol As ISymbol) As Boolean - Select Case symbol.Kind - Case SymbolKind.Field, - SymbolKind.Local, - SymbolKind.Method, - SymbolKind.Parameter, - SymbolKind.Property, - SymbolKind.RangeVariable - Return True - End Select - - Return False - End Function - - - Public Function CanReplaceWithRValue(expression As ExpressionSyntax, semanticModel As SemanticModel, cancellationToken As CancellationToken) As Boolean - Return expression IsNot Nothing AndAlso - Not expression.IsWrittenTo(semanticModel, cancellationToken) AndAlso - expression.CanReplaceWithLValue(semanticModel, cancellationToken) - End Function - - - Public Function CanReplaceWithLValue(expression As ExpressionSyntax, semanticModel As SemanticModel, cancellationToken As CancellationToken) As Boolean -#If False Then - ' Things that are definitely illegal to replace - If ContainsImplicitMemberAccess(expression) Then - Return False - End If -#End If - - If expression.IsKind(SyntaxKind.MyBaseExpression) OrElse - expression.IsKind(SyntaxKind.MyClassExpression) Then - Return False - End If - - If Not (TypeOf expression Is ObjectCreationExpressionSyntax) AndAlso - Not (TypeOf expression Is AnonymousObjectCreationExpressionSyntax) Then - Dim symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken) - If Not symbolInfo.GetBestOrAllSymbols().All(AddressOf CanReplace) Then - ' If the expression is actually a reference to a type, then it can't be replaced - ' with an arbitrary expression. - Return False - End If - End If - - ' Technically, you could introduce an LValue for "Goo" in "Goo()" even if "Goo" binds - ' to a method. (i.e. by assigning to a Func<...> type). However, this is so contrived - ' and none of the features that use this extension consider this replaceable. - If TypeOf expression.Parent Is InvocationExpressionSyntax Then - - ' If something is being invoked, then it's either something like Goo(), Goo.Bar(), or - ' SomeExpr() (i.e. Blah[1]()). In the first and second case, we only allow - ' replacement if Goo and Goo.Bar didn't bind to a method. If we can't bind it, we'll - ' assume it's a method and we don't allow it to be replaced either. However, if it's - ' an arbitrary expression, we do allow replacement. - If expression.IsKind(SyntaxKind.IdentifierName) OrElse expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) Then - Dim symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken) - If Not symbolInfo.GetBestOrAllSymbols().Any() Then - Return False - End If - - ' don't allow it to be replaced if it is bound to an indexed property - Return Not symbolInfo.GetBestOrAllSymbols().OfType(Of IMethodSymbol)().Any() AndAlso - Not symbolInfo.GetBestOrAllSymbols().OfType(Of IPropertySymbol)().Any() - Else - Return True - End If - End If - - ' expression in next statement's control variables should match one in the head - Dim nextStatement = expression.FirstAncestorOrSelf(Of NextStatementSyntax)() - If nextStatement IsNot Nothing Then - Return False - End If - - ' Direct parent kind checks. - If expression.IsParentKind(SyntaxKind.EqualsValue) OrElse - expression.IsParentKind(SyntaxKind.ParenthesizedExpression) OrElse - expression.IsParentKind(SyntaxKind.SelectStatement) OrElse - expression.IsParentKind(SyntaxKind.SyncLockStatement) OrElse - expression.IsParentKind(SyntaxKind.CollectionInitializer) OrElse - expression.IsParentKind(SyntaxKind.InferredFieldInitializer) OrElse - expression.IsParentKind(SyntaxKind.BinaryConditionalExpression) OrElse - expression.IsParentKind(SyntaxKind.TernaryConditionalExpression) OrElse - expression.IsParentKind(SyntaxKind.ReturnStatement) OrElse - expression.IsParentKind(SyntaxKind.YieldStatement) OrElse - expression.IsParentKind(SyntaxKind.XmlEmbeddedExpression) OrElse - expression.IsParentKind(SyntaxKind.ThrowStatement) OrElse - expression.IsParentKind(SyntaxKind.IfStatement) OrElse - expression.IsParentKind(SyntaxKind.WhileStatement) OrElse - expression.IsParentKind(SyntaxKind.ElseIfStatement) OrElse - expression.IsParentKind(SyntaxKind.ForEachStatement) OrElse - expression.IsParentKind(SyntaxKind.ForStatement) OrElse - expression.IsParentKind(SyntaxKind.ConditionalAccessExpression) OrElse - expression.IsParentKind(SyntaxKind.TypeOfIsExpression) OrElse - expression.IsParentKind(SyntaxKind.TypeOfIsNotExpression) Then - - Return True - End If - - ' Parent type checks - If TypeOf expression.Parent Is BinaryExpressionSyntax OrElse - TypeOf expression.Parent Is AssignmentStatementSyntax OrElse - TypeOf expression.Parent Is WhileOrUntilClauseSyntax OrElse - TypeOf expression.Parent Is SingleLineLambdaExpressionSyntax OrElse - TypeOf expression.Parent Is AwaitExpressionSyntax Then - Return True - End If - - ' Specific child checks. - If expression.CheckParent(Of NamedFieldInitializerSyntax)(Function(n) n.Expression Is expression) OrElse - expression.CheckParent(Of MemberAccessExpressionSyntax)(Function(m) m.Expression Is expression) OrElse - expression.CheckParent(Of TryCastExpressionSyntax)(Function(t) t.Expression Is expression) OrElse - expression.CheckParent(Of CatchFilterClauseSyntax)(Function(c) c.Filter Is expression) OrElse - expression.CheckParent(Of SimpleArgumentSyntax)(Function(n) n.Expression Is expression) OrElse - expression.CheckParent(Of DirectCastExpressionSyntax)(Function(d) d.Expression Is expression) OrElse - expression.CheckParent(Of FunctionAggregationSyntax)(Function(f) f.Argument Is expression) OrElse - expression.CheckParent(Of RangeArgumentSyntax)(Function(r) r.UpperBound Is expression) Then - Return True - End If - - ' Misc checks - If TypeOf expression.Parent Is ExpressionRangeVariableSyntax AndAlso - TypeOf expression.Parent.Parent Is QueryClauseSyntax Then - Dim rangeVariable = DirectCast(expression.Parent, ExpressionRangeVariableSyntax) - Dim selectClause = TryCast(rangeVariable.Parent, SelectClauseSyntax) - - ' Can't replace the expression in a select unless its the last select clause *or* - ' it's a select of the form "select a = " - If selectClause IsNot Nothing Then - If rangeVariable.NameEquals IsNot Nothing Then - Return True - End If - - Dim queryExpression = TryCast(selectClause.Parent, QueryExpressionSyntax) - If queryExpression IsNot Nothing Then - Return queryExpression.Clauses.Last() Is selectClause - End If - - Dim aggregateClause = TryCast(selectClause.Parent, AggregateClauseSyntax) - If aggregateClause IsNot Nothing Then - Return aggregateClause.AdditionalQueryOperators().Last() Is selectClause - End If - - Return False - End If - - ' Any other query type is ok. Note(cyrusn): This may be too broad. - Return True - End If - - Return False - End Function - Public Function ContainsImplicitMemberAccess(expression As ExpressionSyntax) As Boolean Return ContainsImplicitMemberAccessWorker(expression) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/LanguageServices/VisualBasicSemanticFactsService.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/LanguageServices/VisualBasicSemanticFactsService.vb index 22a76b9b189345730b698e31ee6b4acddb7fd887..09239a4918b43be306227c4dee3d8ea7d2bb7098 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/LanguageServices/VisualBasicSemanticFactsService.vb +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/LanguageServices/VisualBasicSemanticFactsService.vb @@ -2,17 +2,13 @@ ' The .NET Foundation licenses this file to you under the MIT license. ' See the LICENSE file in the project root for more information. -Imports System.Collections.Immutable Imports System.Composition -Imports System.Runtime.InteropServices Imports System.Threading -Imports Microsoft.CodeAnalysis Imports Microsoft.CodeAnalysis.Host Imports Microsoft.CodeAnalysis.Host.Mef Imports Microsoft.CodeAnalysis.LanguageServices Imports Microsoft.CodeAnalysis.VisualBasic.Extensions.ContextQuery Imports Microsoft.CodeAnalysis.VisualBasic.LanguageServices -Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Namespace Microsoft.CodeAnalysis.VisualBasic @@ -29,29 +25,18 @@ Namespace Microsoft.CodeAnalysis.VisualBasic End Function End Class - Friend Class VisualBasicSemanticFactsService + Friend NotInheritable Class VisualBasicSemanticFactsService Inherits AbstractSemanticFactsService Implements ISemanticFactsService Public Shared ReadOnly Instance As New VisualBasicSemanticFactsService() Protected Overrides ReadOnly Property SyntaxFacts As ISyntaxFacts = VisualBasicSyntaxFacts.Instance + Protected Overrides ReadOnly Property SemanticFacts As ISemanticFacts = VisualBasicSemanticFacts.Instance Private Sub New() End Sub - Public ReadOnly Property SupportsImplicitInterfaceImplementation As Boolean Implements ISemanticFactsService.SupportsImplicitInterfaceImplementation - Get - Return False - End Get - End Property - - Public ReadOnly Property ExposesAnonymousFunctionParameterNames As Boolean Implements ISemanticFactsService.ExposesAnonymousFunctionParameterNames - Get - Return True - End Get - End Property - Protected Overrides Function ToIdentifierToken(identifier As String) As SyntaxToken Return identifier.ToIdentifierToken End Function @@ -114,186 +99,10 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Return tree.IsAttributeNameContext(position, token, cancellationToken) End Function - Public Function IsOnlyWrittenTo(semanticModel As SemanticModel, node As SyntaxNode, cancellationToken As CancellationToken) As Boolean Implements ISemanticFactsService.IsOnlyWrittenTo - Return TryCast(node, ExpressionSyntax).IsOnlyWrittenTo() - End Function - - Public Function IsWrittenTo(semanticModel As SemanticModel, node As SyntaxNode, cancellationToken As CancellationToken) As Boolean Implements ISemanticFactsService.IsWrittenTo - Return TryCast(node, ExpressionSyntax).IsWrittenTo(semanticModel, cancellationToken) - End Function - - Public Function IsInOutContext(semanticModel As SemanticModel, node As SyntaxNode, cancellationToken As CancellationToken) As Boolean Implements ISemanticFactsService.IsInOutContext - Return TryCast(node, ExpressionSyntax).IsInOutContext() - End Function - - Public Function IsInRefContext(semanticModel As SemanticModel, node As SyntaxNode, cancellationToken As CancellationToken) As Boolean Implements ISemanticFactsService.IsInRefContext - Return TryCast(node, ExpressionSyntax).IsInRefContext(semanticModel, cancellationToken) - End Function - - Public Function IsInInContext(semanticModel As SemanticModel, node As SyntaxNode, cancellationToken As CancellationToken) As Boolean Implements ISemanticFactsService.IsInInContext - Return TryCast(node, ExpressionSyntax).IsInInContext() - End Function - - Public Function CanReplaceWithRValue(semanticModel As SemanticModel, expression As SyntaxNode, cancellationToken As CancellationToken) As Boolean Implements ISemanticFactsService.CanReplaceWithRValue - Return TryCast(expression, ExpressionSyntax).CanReplaceWithRValue(semanticModel, cancellationToken) - End Function - - Public Function GenerateNameForExpression(semanticModel As SemanticModel, - expression As SyntaxNode, - capitalize As Boolean, - cancellationToken As CancellationToken) As String Implements ISemanticFactsService.GenerateNameForExpression - Return semanticModel.GenerateNameForExpression( - DirectCast(expression, ExpressionSyntax), capitalize, cancellationToken) - End Function - - Public Function GetDeclaredSymbol(semanticModel As SemanticModel, token As SyntaxToken, cancellationToken As CancellationToken) As ISymbol Implements ISemanticFactsService.GetDeclaredSymbol - Dim location = token.GetLocation() - - For Each ancestor In token.GetAncestors(Of SyntaxNode)() - If Not TypeOf ancestor Is AggregationRangeVariableSyntax AndAlso - Not TypeOf ancestor Is CollectionRangeVariableSyntax AndAlso - Not TypeOf ancestor Is ExpressionRangeVariableSyntax AndAlso - Not TypeOf ancestor Is InferredFieldInitializerSyntax Then - - Dim symbol = semanticModel.GetDeclaredSymbol(ancestor) - - If symbol IsNot Nothing Then - If symbol.Locations.Contains(location) Then - Return symbol - End If - - ' We found some symbol, but it defined something else. We're not going to have a higher node defining _another_ symbol with this token, so we can stop now. - Return Nothing - End If - - ' If we hit an executable statement syntax and didn't find anything yet, we can just stop now -- anything higher would be a member declaration which won't be defined by something inside a statement. - If SyntaxFacts.IsExecutableStatement(ancestor) Then - Return Nothing - End If - End If - Next - - Return Nothing - End Function - - Public Function LastEnumValueHasInitializer(namedTypeSymbol As INamedTypeSymbol) As Boolean Implements ISemanticFactsService.LastEnumValueHasInitializer - Dim enumStatement = namedTypeSymbol.DeclaringSyntaxReferences.Select(Function(r) r.GetSyntax()).OfType(Of EnumStatementSyntax).FirstOrDefault() - If enumStatement IsNot Nothing Then - Dim enumBlock = DirectCast(enumStatement.Parent, EnumBlockSyntax) - - Dim lastMember = TryCast(enumBlock.Members.LastOrDefault(), EnumMemberDeclarationSyntax) - If lastMember IsNot Nothing Then - Return lastMember.Initializer IsNot Nothing - End If - End If - - Return False - End Function - - Public ReadOnly Property SupportsParameterizedProperties As Boolean Implements ISemanticFactsService.SupportsParameterizedProperties - Get - Return True - End Get - End Property - - Public Function GetAliasNameSet(model As SemanticModel, cancellationToken As CancellationToken) As ImmutableHashSet(Of String) Implements ISemanticFactsService.GetAliasNameSet - Dim original = DirectCast(model.GetOriginalSemanticModel(), SemanticModel) - - If Not original.SyntaxTree.HasCompilationUnitRoot Then - Return ImmutableHashSet.Create(Of String)() - End If - - Dim root = original.SyntaxTree.GetCompilationUnitRoot() - - Dim builder = ImmutableHashSet.CreateBuilder(Of String)(StringComparer.OrdinalIgnoreCase) - For Each globalImport In original.Compilation.AliasImports - globalImport.Name.AppendToAliasNameSet(builder) - Next - - For Each importsClause In root.GetAliasImportsClauses() - importsClause.Alias.Identifier.ValueText.AppendToAliasNameSet(builder) - Next - - Return builder.ToImmutable() - End Function - - Public Function GetForEachSymbols(model As SemanticModel, forEachStatement As SyntaxNode) As ForEachSymbols Implements ISemanticFactsService.GetForEachSymbols - - Dim vbForEachStatement = TryCast(forEachStatement, ForEachStatementSyntax) - If vbForEachStatement IsNot Nothing Then - Dim info = model.GetForEachStatementInfo(vbForEachStatement) - Return New ForEachSymbols( - info.GetEnumeratorMethod, - info.MoveNextMethod, - info.CurrentProperty, - info.DisposeMethod, - info.ElementType) - End If - - Dim vbForBlock = TryCast(forEachStatement, ForEachBlockSyntax) - If vbForBlock IsNot Nothing Then - Dim info = model.GetForEachStatementInfo(vbForBlock) - Return New ForEachSymbols( - info.GetEnumeratorMethod, - info.MoveNextMethod, - info.CurrentProperty, - info.DisposeMethod, - info.ElementType) - End If - - Return Nothing - End Function - - Public Function GetGetAwaiterMethod(model As SemanticModel, node As SyntaxNode) As IMethodSymbol Implements ISemanticFactsService.GetGetAwaiterMethod - If node.IsKind(SyntaxKind.AwaitExpression) Then - Dim awaitExpression = DirectCast(node, AwaitExpressionSyntax) - Dim info = model.GetAwaitExpressionInfo(awaitExpression) - Return info.GetAwaiterMethod - End If - - Return Nothing - End Function - - Public Function GetDeconstructionAssignmentMethods(model As SemanticModel, deconstruction As SyntaxNode) As ImmutableArray(Of IMethodSymbol) Implements ISemanticFactsService.GetDeconstructionAssignmentMethods - Return ImmutableArray(Of IMethodSymbol).Empty - End Function - - Public Function GetDeconstructionForEachMethods(model As SemanticModel, deconstruction As SyntaxNode) As ImmutableArray(Of IMethodSymbol) Implements ISemanticFactsService.GetDeconstructionForEachMethods - Return ImmutableArray(Of IMethodSymbol).Empty - End Function - Public Function IsNamespaceDeclarationNameContext(semanticModel As SemanticModel, position As Integer, cancellationToken As CancellationToken) As Boolean Implements ISemanticFactsService.IsNamespaceDeclarationNameContext Return semanticModel.SyntaxTree.IsNamespaceDeclarationNameContext(position, cancellationToken) End Function - Public Function IsPartial(typeSymbol As ITypeSymbol, cancellationToken As CancellationToken) As Boolean Implements ISemanticFactsService.IsPartial - Dim syntaxRefs = typeSymbol.DeclaringSyntaxReferences - Return syntaxRefs.Any( - Function(n As SyntaxReference) - Return DirectCast(n.GetSyntax(cancellationToken), TypeStatementSyntax).Modifiers.Any(SyntaxKind.PartialKeyword) - End Function) - End Function - - Public Function GetDeclaredSymbols(semanticModel As SemanticModel, memberDeclaration As SyntaxNode, cancellationToken As CancellationToken) As IEnumerable(Of ISymbol) Implements ISemanticFactsService.GetDeclaredSymbols - If TypeOf memberDeclaration Is FieldDeclarationSyntax Then - Return DirectCast(memberDeclaration, FieldDeclarationSyntax).Declarators. - SelectMany(Function(d) d.Names.AsEnumerable()). - Select(Function(n) semanticModel.GetDeclaredSymbol(n, cancellationToken)) - End If - - Return SpecializedCollections.SingletonEnumerable(semanticModel.GetDeclaredSymbol(memberDeclaration, cancellationToken)) - End Function - - Public Function FindParameterForArgument(semanticModel As SemanticModel, argumentNode As SyntaxNode, cancellationToken As CancellationToken) As IParameterSymbol Implements ISemanticFactsService.FindParameterForArgument - Return DirectCast(argumentNode, ArgumentSyntax).DetermineParameter(semanticModel, allowParamArray:=False, cancellationToken) - End Function - - Public Function GetBestOrAllSymbols(semanticModel As SemanticModel, node As SyntaxNode, token As SyntaxToken, cancellationToken As CancellationToken) As ImmutableArray(Of ISymbol) Implements ISemanticFactsService.GetBestOrAllSymbols - Return If(node Is Nothing, - ImmutableArray(Of ISymbol).Empty, - semanticModel.GetSymbolInfo(node, cancellationToken).GetBestOrAllSymbols()) - End Function - Private Function ISemanticFactsService_GenerateUniqueName( semanticModel As SemanticModel, location As SyntaxNode, containerOpt As SyntaxNode, baseName As String, cancellationToken As CancellationToken) As SyntaxToken Implements ISemanticFactsService.GenerateUniqueName Return MyBase.GenerateUniqueName(semanticModel, location, containerOpt, baseName, cancellationToken) @@ -309,10 +118,6 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Return MyBase.GenerateUniqueLocalName(semanticModel, location, containerOpt, baseName, cancellationToken) End Function - Public Function IsInsideNameOfExpression(semanticModel As SemanticModel, node As SyntaxNode, cancellationToken As CancellationToken) As Boolean Implements ISemanticFactsService.IsInsideNameOfExpression - Return node.FirstAncestorOrSelf(Of NameOfExpressionSyntax) IsNot Nothing - End Function - Private Function ISemanticFactsService_GenerateUniqueName(semanticModel As SemanticModel, location As SyntaxNode, containerOpt As SyntaxNode, baseName As String, filter As Func(Of ISymbol, Boolean), usedNames As IEnumerable(Of String), cancellationToken As CancellationToken) As SyntaxToken Implements ISemanticFactsService.GenerateUniqueName Return MyBase.GenerateUniqueName(semanticModel, location, containerOpt, baseName, filter, usedNames, cancellationToken) End Function diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/VisualBasicWorkspaceExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/VisualBasicWorkspaceExtensions.projitems index 122fe2f7a6def1d233f153a6076277b84c80687d..1fd40dc687db514f0f0f8220866d85e9799ee645 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/VisualBasicWorkspaceExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/VisualBasicWorkspaceExtensions.projitems @@ -28,7 +28,6 @@ -