|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Shared.Extensions;
namespace Microsoft.CodeAnalysis.CSharp.UseSimpleUsingStatement
{
/// <summary>
/// Looks for code like:
///
/// ```c#
/// using (var a = b)
/// using (var c = d)
/// using (var e = f)
/// {
/// }
/// ```
///
/// And offers to convert it to:
///
/// ```c#
/// using var a = b;
/// using var c = d;
/// using var e = f;
/// ```
///
/// (this of course works in the case where there is only one using).
///
/// A few design decisions:
///
/// 1. We only offer this if the entire group of usings in a nested stack can be
/// converted. We don't want to take a nice uniform group and break it into
/// a combination of using-statements and using-declarations. That may feel
/// less pleasant to the user than just staying uniform.
///
/// 2. We're conservative about converting. Because `using`s may be critical for
/// program correctness, we only convert when we're absolutely *certain* that
/// semantics will not change.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal class UseSimpleUsingStatementDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer
{
public UseSimpleUsingStatementDiagnosticAnalyzer()
: base(IDEDiagnosticIds.UseSimpleUsingStatementDiagnosticId,
EnforceOnBuildValues.UseSimpleUsingStatement,
CSharpCodeStyleOptions.PreferSimpleUsingStatement,
new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_simple_using_statement), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)),
new LocalizableResourceString(nameof(CSharpAnalyzersResources.using_statement_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
{
}
public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
=> DiagnosticAnalyzerCategory.SemanticSpanAnalysis;
protected override void InitializeWorker(AnalysisContext context)
{
context.RegisterCompilationStartAction(context =>
{
if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp8)
return;
context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.UsingStatement);
});
}
private void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
{
var cancellationToken = context.CancellationToken;
var outermostUsing = (UsingStatementSyntax)context.Node;
var semanticModel = context.SemanticModel;
if (outermostUsing.Parent is not BlockSyntax parentBlock)
{
// Don't offer on a using statement that is parented by another using statement. We'll just offer on the
// topmost using statement.
return;
}
var innermostUsing = outermostUsing;
// Check that all the immediately nested usings are convertible as well.
// We don't want take a sequence of nested-using and only convert some of them.
for (var current = outermostUsing; current != null; current = current.Statement as UsingStatementSyntax)
{
innermostUsing = current;
if (current.Declaration == null)
return;
}
// Verify that changing this using-statement into a using-declaration will not change semantics.
if (!PreservesSemantics(semanticModel, parentBlock, outermostUsing, innermostUsing, cancellationToken))
return;
// Converting a using-statement to a using-variable-declaration will cause the using's variables to now be
// pushed up to the parent block's scope. This is also true for any local variables in the innermost using's
// block. These may then collide with other variables in the block, causing an error. Check for that and
// bail if this happens.
if (CausesVariableCollision(
context.SemanticModel, parentBlock,
outermostUsing, innermostUsing, cancellationToken))
{
return;
}
var option = context.GetCSharpAnalyzerOptions().PreferSimpleUsingStatement;
if (!option.Value)
return;
// Good to go!
context.ReportDiagnostic(DiagnosticHelper.Create(
Descriptor,
outermostUsing.UsingKeyword.GetLocation(),
option.Notification.Severity,
additionalLocations: ImmutableArray.Create(outermostUsing.GetLocation()),
properties: null));
}
private static bool CausesVariableCollision(
SemanticModel semanticModel, BlockSyntax parentBlock,
UsingStatementSyntax outermostUsing, UsingStatementSyntax innermostUsing,
CancellationToken cancellationToken)
{
var symbolNameToExistingSymbol = semanticModel.GetExistingSymbols(parentBlock, cancellationToken).ToLookup(s => s.Name);
for (var current = outermostUsing; current != null; current = current.Statement as UsingStatementSyntax)
{
// Check if the using statement itself contains variables that will collide with other variables in the
// block.
var usingOperation = (IUsingOperation)semanticModel.GetRequiredOperation(current, cancellationToken);
if (DeclaredLocalCausesCollision(symbolNameToExistingSymbol, usingOperation.Locals))
return true;
}
var innerUsingOperation = (IUsingOperation)semanticModel.GetRequiredOperation(innermostUsing, cancellationToken);
if (innerUsingOperation.Body is IBlockOperation innerUsingBlock)
return DeclaredLocalCausesCollision(symbolNameToExistingSymbol, innerUsingBlock.Locals);
return false;
}
private static bool DeclaredLocalCausesCollision(ILookup<string, ISymbol> symbolNameToExistingSymbol, ImmutableArray<ILocalSymbol> locals)
=> locals.Any(static (local, symbolNameToExistingSymbol) => symbolNameToExistingSymbol[local.Name].Any(otherLocal => !local.Equals(otherLocal)), symbolNameToExistingSymbol);
private static bool PreservesSemantics(
SemanticModel semanticModel,
BlockSyntax parentBlock,
UsingStatementSyntax outermostUsing,
UsingStatementSyntax innermostUsing,
CancellationToken cancellationToken)
{
var statements = parentBlock.Statements;
var index = statements.IndexOf(outermostUsing);
return UsingValueDoesNotLeakToFollowingStatements(semanticModel, statements, index, cancellationToken) &&
UsingStatementDoesNotInvolveJumps(statements, index, innermostUsing);
}
private static bool UsingStatementDoesNotInvolveJumps(
SyntaxList<StatementSyntax> parentStatements, int index, UsingStatementSyntax innermostUsing)
{
// Jumps are not allowed to cross a using declaration in the forward direction, and can't go back unless
// there is a curly brace between the using and the label.
//
// We conservatively implement this by disallowing the change if there are gotos/labels
// in the containing block, or inside the using body.
// Note: we only have to check up to the `using`, since the checks below in
// UsingValueDoesNotLeakToFollowingStatements ensure that there would be no labels/gotos *after* the using
// statement.
for (var i = 0; i < index; i++)
{
var priorStatement = parentStatements[i];
if (IsGotoOrLabeledStatement(priorStatement))
return false;
}
var innerStatements = innermostUsing.Statement is BlockSyntax block
? block.Statements
: new SyntaxList<StatementSyntax>(innermostUsing.Statement);
foreach (var statement in innerStatements)
{
if (IsGotoOrLabeledStatement(statement))
return false;
}
return true;
}
private static bool IsGotoOrLabeledStatement(StatementSyntax priorStatement)
=> priorStatement.Kind() is SyntaxKind.GotoStatement or
SyntaxKind.LabeledStatement;
private static bool UsingValueDoesNotLeakToFollowingStatements(
SemanticModel semanticModel,
SyntaxList<StatementSyntax> statements,
int index,
CancellationToken cancellationToken)
{
// Has to be one of the following forms:
// 1. Using statement is the last statement in the parent.
// 2. Using statement is not the last statement in parent, but is followed by something that is unaffected
// by simplifying the using statement. i.e. `return`/`break`/`continue`. *Note*. `return expr` would
// *not* be ok. In that case, `expr` would now be evaluated *before* the using disposed the resource,
// instead of afterwards. Effectively, the statement following cannot actually execute any code that
// might depend on the .Dispose method being called or not.
// Note: we can skip local functions as moving their scope inside the using doesn't change anything.
while (index + 1 < statements.Count && statements[index + 1] is LocalFunctionStatementSyntax)
index++;
// if we got to the end of the the block then this can be converted.
if (index == statements.Count - 1)
return true;
// Not the last statement, get the next statement and examine that.
var nextStatement = statements[index + 1];
if (nextStatement is BreakStatementSyntax or ContinueStatementSyntax)
{
// using statement followed by break/continue. Can convert this as executing the break/continue will
// cause the code to exit the using scope, causing Dispose to be called at the same place as before.
return true;
}
if (nextStatement is ReturnStatementSyntax returnStatement)
{
// using statement followed by `return`. Can convert this as executing the `return` will cause the code
// to exit the using scope, causing Dispose to be called at the same place as before.
//
// Note: the expr has to be null. If it was non-null, then the expr would now execute before hte using
// called 'Dispose' instead of after, potentially changing semantics.
if (returnStatement.Expression is null)
return true;
// return constant;
//
// This is also safe to return as constants could not be affected by being inside or outside the using block.
var constantValue = semanticModel.GetConstantValue(returnStatement.Expression, cancellationToken);
return constantValue.HasValue;
}
// Add any additional cases here in the future.
return false;
}
}
}
|