public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SingleSyntaxTreeAnalysisContext analysisContext) { return(syntaxTree.GetRoot() .DescendantNodes() .OfType <AccessorDeclarationSyntax>() .Where (accessor => accessor.Keyword.IsKind(SyntaxKind.GetKeyword) && ((AccessorListSyntax)accessor.Parent).Accessors.Count == 1 // We must have only the get-accessor. && accessor.FirstAncestorOrSelf <TBasePropertyDeclarationSyntax>() != null // We must be either in a property or in an indexer. && ( // We have a get accessor with a body. ( accessor.Body != null && accessor.Body.Statements.Count == 1 && accessor.Body.Statements[0].IsKind(SyntaxKind.ReturnStatement) ) || // We have an expression bodied get accessor. accessor.ExpressionBody != null ) ) .Select(accessor => new AnalysisResult ( this, analysisContext, syntaxTree.FilePath, accessor.Keyword, accessor.FirstAncestorOrSelf <TBasePropertyDeclarationSyntax>() ))); }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SingleSyntaxTreeAnalysisContext analysisContext) { return(syntaxTree.GetRoot() .DescendantNodes() .OfType <DestructorDeclarationSyntax>() .Where (destructor => destructor.Body != null && destructor.Body.Statements.Count == 1 && destructor.Body.Statements[0].IsKind(SyntaxKind.ExpressionStatement) ) .Select(destructor => new AnalysisResult ( this, analysisContext, syntaxTree.FilePath, destructor.TildeToken, destructor ))); }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SingleSyntaxTreeAnalysisContext analysisContext) { return(syntaxTree.GetRoot() .DescendantNodes() .OfType <AccessorDeclarationSyntax>() .Where (accessor => accessor.Keyword.IsKind(SyntaxKind.SetKeyword) && accessor.Body != null && accessor.Body.Statements.Count == 1 && accessor.Body.Statements[0].IsKind(SyntaxKind.ExpressionStatement) && accessor.FirstAncestorOrSelf <TBasePropertyDeclarationSyntax>() != null // We must be either in a property or in an indexer. ) .Select(accessor => new AnalysisResult ( this, analysisContext, syntaxTree.FilePath, accessor.Keyword, accessor.FirstAncestorOrSelf <TBasePropertyDeclarationSyntax>() ))); }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { return(Enumerable.Empty <AnalysisResult>()); }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { return(syntaxTree.GetRoot() .DescendantNodes() .OfType <ThrowStatementSyntax>() .Select(@throw => new { ThrowStatement = @throw, ParameterNameArgument = GetParameterNameArgumentIfThrowThrowsArgumentExceptionWithProperParameterName(@throw) }) .Where(throwWithParameterName => throwWithParameterName.ParameterNameArgument != null) .Select(@throw => new AnalysisResult ( this, analysisContext, syntaxTree.FilePath, @throw.ParameterNameArgument.GetFirstToken(), @throw.ThrowStatement.Expression ))); ArgumentSyntax GetParameterNameArgumentIfThrowThrowsArgumentExceptionWithProperParameterName(ThrowStatementSyntax throwStatementSyntax) { // Let's first do fast checks that quickly and cheaply eliminate obvious non-candidates. // We want to use the semantic model only if we are sure that we have candidates that are // very likely throw statements that we are looking for. if (!(throwStatementSyntax.Expression is ObjectCreationExpressionSyntax objectCreationExpression)) { return(null); } if (!KnownArgumentExceptions.Select(exception => exception.TypeName).Any(name => objectCreationExpression.Type.ToString().EndsWith(name, StringComparison.Ordinal))) { return(null); } var argumentsThatCouldBeParameterNames = objectCreationExpression.ArgumentList.Arguments .Where(argument => argument.Expression.IsKind(SyntaxKind.StringLiteralExpression) && ((LiteralExpressionSyntax)argument.Expression).Token.ValueText.CouldBePotentialParameterName() ) .ToList(); if (!argumentsThatCouldBeParameterNames.Any()) { return(null); } var parameterNamesAvailableInScopeOfThrow = throwStatementSyntax.GetParameterNamesVisibleInScope(); var argumentsThatAreParameterNames = argumentsThatCouldBeParameterNames .Where(argument => parameterNamesAvailableInScopeOfThrow.Contains(((LiteralExpressionSyntax)argument.Expression).Token.ValueText)) .ToList(); if (!argumentsThatAreParameterNames.Any()) { return(null); } // If we reach this point it means that we have a throw statement that has a constructor call // that creates an exception whose name pretty much surely fits the name of the argument exceptions and whose // constructor argument, any or more of them, are strings that appear in the surrounding parameters names. // Which means we are in a real case most likely 99% percent sure that we have a fit. Cool :-) // Still, we have to check for those 1% since we could have a something like this: // throw new AlmostArgumentNullException("A message, although the first parameter should be parameter name ;-)", "parameterName"); // For that check, we need semantic analysis. var constructorSymbol = (IMethodSymbol)semanticModel.GetSymbolInfo(objectCreationExpression).Symbol; if (constructorSymbol == null) { return(null); } // 1. Check that the created object is 100% one of the argument exceptions. var argumentException = KnownArgumentExceptions .FirstOrDefault(exception => exception.RepresentsType(constructorSymbol.ContainingType)); if (argumentException == null) { return(null); } // 2. Check that the constructor arguments that are strings with enclosing parameter names // are passed as proper constructor parameters. return(argumentsThatAreParameterNames.FirstOrDefault(argument => GetParameterNameBehindTheArgument(argument) == argumentException.ParameterName)); string GetParameterNameBehindTheArgument(ArgumentSyntax argument) { // If we are lucky and have a named argument ;-) if (argument.NameColon != null) { var parameterNameText = argument.NameColon.Name?.Identifier.ValueText; return(parameterNameText ?? string.Empty); } // We were nor lucky. We have a normal positional argument in the list of arguments. var index = ((ArgumentListSyntax)argument.Parent).Arguments.IndexOf(argument); return(index < 0 || index >= constructorSymbol.Parameters.Length ? string.Empty : constructorSymbol.Parameters[index].Name); } } }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { return(syntaxTree.GetRoot() .DescendantNodes() .OfType <LocalFunctionStatementSyntax>() .Where (localFunction => localFunction.Body != null && localFunction.Body.Statements.Count == 1 && localFunction.Body.Statements[0].IsKind(SyntaxKind.ExpressionStatement) ) .Select(localFunction => new AnalysisResult ( this, analysisContext, syntaxTree.FilePath, localFunction.Identifier, localFunction ))); }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { return(syntaxTree.GetRoot() .DescendantNodes() .OfKind(SyntaxKind.CoalesceExpression) .Where(node => node.Parent?.IsKind(SyntaxKind.SimpleAssignmentExpression) == true && node.Parent is AssignmentExpressionSyntax assignment && assignment.Right == node && node is BinaryExpressionSyntax coalesceExpressionNode && assignment.Left != null && coalesceExpressionNode.Left != null && AssignmentTargetIsSameAsCoalesceOperatorLeftSide(assignment.Left, coalesceExpressionNode.Left) && // Ignore situations like: X = X ?? throw new Exception(). coalesceExpressionNode.Right?.IsKind(SyntaxKind.ThrowExpression) == false && // Don't offer suggestion if this is `X = X ?? ...` inside an object initializer like e.g. `new SomeClass { X = X ?? ... }`. !assignment.Left.IsObjectInitializerNamedAssignmentIdentifier() ) .Select(coalesceExpressionNode => new AnalysisResult ( IsSideEffectFree(((AssignmentExpressionSyntax)coalesceExpressionNode.Parent).Left, true) ? (ISharpenSuggestion)UseNullCoalescingAssignmentOperatorInsteadOfAssigningResultOfTheNullCoalescingOperator.Instance : ConsiderUsingNullCoalescingAssignmentOperatorInsteadOfAssigningResultOfTheNullCoalescingOperator.Instance, analysisContext, syntaxTree.FilePath, coalesceExpressionNode.Parent.GetFirstToken(), coalesceExpressionNode.Parent ))); // We are changing the number of times the expression is executed from // twice to ones. This is potentially not side-effect-free in certain cases. // E.g. when expression looks like `SomeProperty.SomeOtherProperty.AgainSomeOtherProperty` // and there is an arbitrary logic running behind each of the properties. // Even in a simple case like `SomeProperty = SomeProperty ?? ...` // we can have side effects. In the above case, the setter is always called, // while `SomeProperty ??= ...` calls the setter only if SomeProperty is null. bool IsSideEffectFree(SyntaxNode expression, bool isTopLevel) { // This algorithm works "backwards". E.g for "this.a.b.c" the check goes // from "c" towards "this". if (expression == null) { return(false); } // Expression basically has to be of the form "a.b.c", where all parts are locals, // parameters or fields. Basically, nothing that can cause arbitrary user code // execution when being evaluated by the compiler. // Referencing this or base like e.g. this.a.b.c causes no side effects itself. if (expression.IsThisExpression() || expression.IsBaseExpression()) { return(true); } if (expression.IsIdentifierName()) { return(IsSideEffectFreeSymbol(expression, isTopLevel)); } if (expression.IsParenthesizedExpression()) { expression.GetPartsOfParenthesizedExpression(out _, out var parenthesizedExpression, out _); return(IsSideEffectFree(parenthesizedExpression, isTopLevel)); } if (expression.IsSimpleMemberAccessExpression()) { expression.GetPartsOfMemberAccessExpression(out var subExpression, out _, out _); return(IsSideEffectFree(subExpression, isTopLevel: false) && IsSideEffectFreeSymbol(expression, isTopLevel)); } if (expression.IsConditionalAccessExpression()) { expression.GetPartsOfConditionalAccessExpression(out var accessExpression, out _, out var whenNotNull); return(IsSideEffectFree(accessExpression, isTopLevel: false) && IsSideEffectFree(whenNotNull, isTopLevel: false)); } // Something we don't explicitly handle. Assume this may have side effects. return(false); bool IsSideEffectFreeSymbol(SyntaxNode node, bool isTopLevelNode) { var symbolInfo = semanticModel.GetSymbolInfo(node); if (symbolInfo.CandidateSymbols.Length > 0 || symbolInfo.Symbol == null) { // Couldn't bind successfully, assume that this might have side-effects. return(false); } var symbol = symbolInfo.Symbol; switch (symbol.Kind) { case SymbolKind.Namespace: case SymbolKind.NamedType: case SymbolKind.Field: case SymbolKind.Parameter: case SymbolKind.Local: return(true); } if (symbol.Kind == SymbolKind.Property && isTopLevelNode) { // TODO: Check this. It looks like a bug for ??. // Will the setter be called? // What when it is ref-property? // Note, this doesn't apply if the property is a ref-property. In that case, we'd // go from a read and a write to just a read (and a write to it's returned ref // value). var property = (IPropertySymbol)symbol; // TODO: There is no RefKind in this version of Roslyn that we use. // Investigate how to implement the below line in this version. // I guess that should be possible with the available RefCustomModifiers and ReturnsByRef. // So far we wil just ignore it. //if (property.RefKind == RefKind.None) //{ // return true; //} } return(false); } } bool AssignmentTargetIsSameAsCoalesceOperatorLeftSide(SyntaxNode assignmentTarget, SyntaxNode coalesceOperatorLeftSide) { // TODO: Implement symbol and not only text equality. E.g. this.x = x ?? ... return(SyntaxNodeFacts.AreEquivalent(assignmentTarget, coalesceOperatorLeftSide)); } }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { // TODO: At the moment, if an identifier is used in different syntaxTrees // (very very very likely :-)) it will be reported several times in // the result, once per syntax tree. So far we can live with that. // Implement proper removal of duplicates. // TODO: What to do when a variable is declared by using the var keyword? // At the moment we will simply pretend that the feature is there. // It is planned and will be implemented one day. return(syntaxTree.GetRoot() .DescendantNodes() // TODO: Parameter declaration with null as default value: string parameter = null. // TODO: Variable declaration with initialization to null: string variable = null. // TODO: Property declaration with initialization to null: public string Property { get; } = null. .OfAnyOfKinds ( SyntaxKind.SimpleAssignmentExpression, // identifier = null; SyntaxKind.EqualsExpression, // identifier == null; SyntaxKind.NotEqualsExpression, // identifier != null SyntaxKind.VariableDeclarator, // object _fieldIdentifier = null; object localVariableIdentifier = null; SyntaxKind.ConditionalAccessExpression, // identifier?.Something; SyntaxKind.CoalesceExpression // identifier ?? something; ) .Select(GetNullableIdentifierSymbol) .Where(symbol => symbol?.IsImplicitlyDeclared == false) .Distinct() .Select(symbol => (symbol, declaringNode: GetSymbolDeclaringNode(symbol))) .Where(NullableContextCanBeEnabledForIdentifier) .Select(GetAnalysisResultInfo) .Where(analysisResultInfo => analysisResultInfo.suggestion != null) .Select(analysisResultInfo => new AnalysisResult ( analysisResultInfo.suggestion, // TODO-IG: This is actually wrong, a bug to be completely honest. // In a usual case, the project in which the filePath // really is, is different then the project in which // the currently analyzed syntax tree resides. // Which means that the information in the analysisContext // does not fit to filePath. // Luckily the consequences are not so tragical, the // whole path to the file will be wrong. // The file will be misplaced, essentially the file name // of the filePath will be placed with the LogicalFolderPath // of the analysisContext. // Know bug, let's leave it so far, we have more urgent things // to do in order to have a reasonable release before the // talk in Bangalore. analysisContext, analysisResultInfo.filePath, analysisResultInfo.startingToken, analysisResultInfo.displayTextNode ))); bool NullableContextCanBeEnabledForIdentifier((ISymbol symbol, SyntaxNode declaringNode) symbolAndDeclaringNode) { var(symbol, declaringNode) = symbolAndDeclaringNode; var filePath = declaringNode?.SyntaxTree?.FilePath; if (string.IsNullOrEmpty(filePath)) { return(false); } // We skip the check for IsGeneratedAssemblyInfo() so far. // To get the Document object at this point requires // a bit of refactoring. Plus, the chance that the declaring // node be in the generated AssemblyInfo is zero. // TODO-IG: Think of adding the check later. if (GeneratedCodeDetection.IsGeneratedFile(filePath) || declaringNode.SyntaxTree.BeginsWithAutoGeneratedComment()) { return(false); } // If the nullable context is already enabled we do not // want to show the suggestion. In that case the compiler // will provide the appropriate warnings. // The main idea behind this suggestion is to motive // programmers to enable nullable context and start using // nullable reference types! if (NullableContextIsAlreadyEnabled()) { return(false); } var typeSymbol = GetTypeSymbol(); if (typeSymbol == null) { return(false); } // For this, we want to have a special suggestion // that suggest to mark the type parameter as nullable. // So far, just ignore this case. // TODO-IG: Implement suggestion for marking unconstrained type parameters as nullable. if (typeSymbol.IsUnconstrainedTypeParameter()) { return(false); } if (IdentifierIsAlreadyNullable()) { return(false); } return(true); bool IdentifierIsAlreadyNullable() { // This is VS2017 implementation. // If the identifier is value type and is // surely nullable it can only by Nullable<T> (T?). // So, it is already nullable if it is a // value type. // TODO-IG: Solve how to approach to support for VS2017 and VS2019 // at the same time and implement properly the VS2019 // version of this logic. // Also, add the negative-case smoke tests to the // CSharp80.VS2019.csproj. // In VS2019 (means C# 8.0) we have nullable reference types ;-) return(typeSymbol.IsValueType); } ITypeSymbol GetTypeSymbol() { switch (symbol) { case IFieldSymbol fieldSymbol: return(fieldSymbol.Type); case IPropertySymbol propertySymbol: return(propertySymbol.Type); case IParameterSymbol parameterSymbol: return(parameterSymbol.Type); case ILocalSymbol localSymbol: return(localSymbol.Type); default: return(null); } } bool NullableContextIsAlreadyEnabled() { // TODO-IG: This is the implementation for VS2017. // Solve how to approach to support for VS2017 and VS2019 // at the same time and implement properly the VS2019 // version of this method. // Also, add the negative-case smoke tests to the // CSharp80.VS2019.csproj. return(false); } } (ISharpenSuggestion suggestion, string filePath, SyntaxToken startingToken, SyntaxNode displayTextNode) GetAnalysisResultInfo((ISymbol symbol, SyntaxNode declaringNode) symbolAndDeclaringNode) { return ( GetSuggestion(symbolAndDeclaringNode.symbol), symbolAndDeclaringNode.declaringNode.SyntaxTree.FilePath, GetStartingToken(symbolAndDeclaringNode.declaringNode), symbolAndDeclaringNode.declaringNode ); } SyntaxToken GetStartingToken(SyntaxNode declaringNode) { // If we have more then one variable declared in the // declaration, we want to position the cursor to that variable. if (declaringNode.IsKind(SyntaxKind.VariableDeclarator) && // To avoid expensive type based checks. declaringNode is VariableDeclaratorSyntax variableDeclarator && variableDeclarator.Parent is VariableDeclarationSyntax variableDeclaration && variableDeclaration.Variables.Count != 1) { return(variableDeclarator.Identifier); } // In general, we would like to position the cursor // to the type part of the declaration, because that's // what should be changed in code (by adding ?). // But to identify it in general case is a bit tricky, // and case by case implementation is time consuming. // Therefore at the moment we will just position to the // beginning of the declaration. // TODO-IG: Position to type part of the declaration. return(declaringNode.GetFirstToken()); } ISharpenSuggestion GetSuggestion(ISymbol symbol) { switch (symbol) { case IFieldSymbol _: return(EnableNullableContextAndDeclareReferenceFieldAsNullable.Instance); case IPropertySymbol _: return(EnableNullableContextAndDeclareReferencePropertyAsNullable.Instance); case IParameterSymbol _: return(EnableNullableContextAndDeclareReferenceParameterAsNullable.Instance); case ILocalSymbol _: return(EnableNullableContextAndDeclareReferenceVariableAsNullable.Instance); default: return(null); } } SyntaxNode GetSymbolDeclaringNode(ISymbol symbol) { return(symbol.DeclaringSyntaxReferences.Length != 1 ? null : symbol.DeclaringSyntaxReferences[0].GetSyntax()); } ISymbol GetNullableIdentifierSymbol(SyntaxNode node) { // TODO: Ignore the case where the comparison with null is actually a null guard. // E.g.: if (identifier == null) throw ArgumentNullException(...); // E.g.: identifier ?? throw ArgumentNullException(...); // TODO: If the "value" in the property setter is checked for null // we could assume that the property is nullable. Think about it. switch (node) { case AssignmentExpressionSyntax assignment: return(IsSurelyNullable(assignment.Right) && IsIdentifierOrPropertyAccess(assignment.Left) ? semanticModel.GetSymbolInfo(assignment.Left).Symbol : null); case BinaryExpressionSyntax binaryExpression: switch (binaryExpression.Kind()) { case SyntaxKind.EqualsExpression: case SyntaxKind.NotEqualsExpression: if (IsSurelyNullable(binaryExpression.Right) && IsIdentifierOrPropertyAccess(binaryExpression.Left)) { return(semanticModel.GetSymbolInfo(binaryExpression.Left).Symbol); } if (IsSurelyNullable(binaryExpression.Left) && IsIdentifierOrPropertyAccess(binaryExpression.Right)) { return(semanticModel.GetSymbolInfo(binaryExpression.Right).Symbol); } return(null); case SyntaxKind.CoalesceExpression: return(IsIdentifierOrPropertyAccess(binaryExpression.Left) ? semanticModel.GetSymbolInfo(binaryExpression.Left).Symbol : null); default: return(null); } case VariableDeclaratorSyntax variableDeclarator: return(IsSurelyNullable(variableDeclarator.Initializer?.Value) && variableDeclarator.Identifier.IsKind(SyntaxKind.IdentifierToken) ? semanticModel.GetDeclaredSymbol(variableDeclarator) : null); case ConditionalAccessExpressionSyntax conditionalAccess: return(IsIdentifierOrPropertyAccess(conditionalAccess.Expression) ? semanticModel.GetSymbolInfo(conditionalAccess.Expression).Symbol : null); default: return(null); } bool IsSurelyNullable(SyntaxNode potentiallyNullableNode) { // We do not do any data flow analysis here, of course :-) // Just identifying the obvious cases. return(potentiallyNullableNode?.IsKind(SyntaxKind.NullLiteralExpression) == true || potentiallyNullableNode?.IsKind(SyntaxKind.AsExpression) == true); } bool IsIdentifierOrPropertyAccess(SyntaxNode syntaxNode) { return(syntaxNode?.IsAnyOfKinds(SyntaxKind.IdentifierName, SyntaxKind.SimpleMemberAccessExpression) == true); } } }
public FilePathTreeViewItem(BaseTreeViewItem parent, IAnalysisResultTreeViewBuilder treeViewBuilder, int numberOfItems, SingleSyntaxTreeAnalysisContext analysisContext, string filePath) : base(parent, treeViewBuilder, numberOfItems, null) { this.analysisContext = analysisContext; FilePath = filePath; }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { var descendantNodes = syntaxTree.GetRoot().DescendantNodes(); // In unit test we of course expect people to test synchronous // versions of their methods. Moreover, we expect them to write // all kind of crazy acrobatics in tests in general. // Every suggestion to replace any synchronous method with their // async counterparts would very likely be totally // misplaced and misleading in unit tests. So we will simply // ignore these suggestions in unit tests. // It's fine to re-enumerate the nodes tree. It is a materialized structure. // ReSharper disable once PossibleMultipleEnumeration if (descendantNodes.ContainUnitTestingCode()) { return(Enumerable.Empty <AnalysisResult>()); } // ReSharper disable once PossibleMultipleEnumeration return(descendantNodes .OfType <YieldStatementSyntax>() .Select(yieldStatement => yieldStatement.FirstAncestorOrSelf <MethodDeclarationSyntax>()) // TODO: Yield must be in the method itself, not nested within a local function, or lambda, or delegate. // TODO: Make it work also for local functions and not only for methods. .Where(method => method != null) .Distinct() // Because we can have more than one yield statement in a method. .SelectMany(method => method.DescendantNodes().OfType <InvocationExpressionSyntax>()) .Where(InvokedMethodHasAsynchronousEquivalent) .Select(invocation => new AnalysisResult( ConsiderAwaitingEquivalentAsynchronousMethodAndYieldingIAsyncEnumerable.Instance, analysisContext, syntaxTree.FilePath, GetStartingSyntaxNode(invocation).GetFirstToken(), invocation ))); bool InvokedMethodHasAsynchronousEquivalent(InvocationExpressionSyntax invocation) { return(asynchronousMethodFinder.EquivalentAsynchronousCandidateExistsFor(invocation, semanticModel, EquivalentAsynchronousMethodFinder.CallerAsyncStatus.CallerMustNotBeAsync)); } SyntaxNode GetStartingSyntaxNode(InvocationExpressionSyntax invocation) { if (!(invocation.Expression is MemberAccessExpressionSyntax memberAccess)) { return(invocation.Expression); } return(memberAccess.Name ?? (SyntaxNode)memberAccess); } }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { return(syntaxTree.GetRoot() .DescendantNodes() .OfAnyOfKinds(SyntaxKind.MethodDeclaration, SyntaxKind.LocalFunctionStatement) .Where(methodOrLocalFunction => methodOrLocalFunction.IsAsync()) .SelectMany(methodOrLocalFunction => methodOrLocalFunction .DescendantNodes() // We can have both known methods like e.g. Wait() (invocation) // and known properties like e.g. Result (simple member access). .OfAnyOfKinds(SyntaxKind.InvocationExpression, SyntaxKind.SimpleMemberAccessExpression) .Select(node => ( caller: methodOrLocalFunction, node ))) .Where(callerAndNode => !callerAndNode.node.IsWithinLambdaOrAnonymousMethod() && InvocationIsDirectlyWithinTheCaller(callerAndNode.node, callerAndNode.caller) && ( callerAndNode.node.IsKind(SyntaxKind.InvocationExpression) && ((InvocationExpressionSyntax)callerAndNode.node).GetInvokedMemberName() == replacementInfo.SynchronousMemberName || callerAndNode.node.IsKind(SyntaxKind.SimpleMemberAccessExpression) && callerAndNode.node.Parent?.IsKind(SyntaxKind.InvocationExpression) == false && ((MemberAccessExpressionSyntax)callerAndNode.node).Name.Identifier.ValueText == replacementInfo.SynchronousMemberName ) && InvocationHasAsynchronousEquivalentThatCanBeAwaited(callerAndNode.node) ) .Select(callerAndNode => new AnalysisResult( this, analysisContext, syntaxTree.FilePath, GetStartingSyntaxNode(callerAndNode.node).GetFirstToken(), callerAndNode.node.FirstAncestorOrSelf <StatementSyntax>() ?? callerAndNode.node ))); bool InvocationIsDirectlyWithinTheCaller(SyntaxNode invocation, SyntaxNode caller) { // We have to check that the invocation node is directly within // the caller, means it is not within some local function within the // caller. // In other words, there must not be any LocalFunctionStatementSyntax node // between the invocation and the caller. return(invocation.FirstAncestorOrSelfWithinEnclosingNode <LocalFunctionStatementSyntax>(caller, false) == null); } bool InvocationHasAsynchronousEquivalentThatCanBeAwaited(SyntaxNode invocation) { var invokedMember = semanticModel.GetSymbolInfo(invocation).Symbol; if (invokedMember?.ContainingType == null) { return(false); } return(invokedMember.Name == replacementInfo.SynchronousMemberName && invokedMember.ContainingType.FullNameIsEqualTo(replacementInfo.SynchronousMemberTypeNamespace, replacementInfo.SynchronousMemberTypeName)); } SyntaxNode GetStartingSyntaxNode(SyntaxNode node) { MemberAccessExpressionSyntax memberAccess; if (node.IsKind(SyntaxKind.InvocationExpression)) { var invocation = (InvocationExpressionSyntax)node; memberAccess = invocation.Expression as MemberAccessExpressionSyntax; if (memberAccess == null) { return(invocation.Expression); } } else { memberAccess = (MemberAccessExpressionSyntax)node; } return(memberAccess.Name ?? (SyntaxNode)memberAccess); } }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { var descendantNodes = syntaxTree.GetRoot().DescendantNodes(); // In unit test we of course expect people to test synchronous // versions of their methods. Moreover, we expect them to write // all kind of crazy acrobatics in tests in general. // Every suggestion to replace any synchronous method with their // async counterparts would very likely be totally // misplaced and misleading in unit tests. So we will simply // ignore these suggestions in unit tests. // It's fine to re-enumerate the nodes tree. It is a materialized structure. // ReSharper disable once PossibleMultipleEnumeration if (descendantNodes.ContainUnitTestingCode()) { return(Enumerable.Empty <AnalysisResult>()); } // ReSharper disable once PossibleMultipleEnumeration return(descendantNodes .OfType <InvocationExpressionSyntax>() .Where(InvokedMethodHasAsynchronousEquivalent) .Select(invocation => new AnalysisResult( this, analysisContext, syntaxTree.FilePath, GetStartingSyntaxNode(invocation).GetFirstToken(), invocation ))); bool InvokedMethodHasAsynchronousEquivalent(InvocationExpressionSyntax invocation) { return(asynchronousMethodFinder.EquivalentAsynchronousCandidateExistsFor(invocation, semanticModel, callerAsyncStatus, callerYieldingStatus)); } SyntaxNode GetStartingSyntaxNode(InvocationExpressionSyntax invocation) { if (!(invocation.Expression is MemberAccessExpressionSyntax memberAccess)) { return(invocation.Expression); } return(memberAccess.Name ?? (SyntaxNode)memberAccess); } }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { return(syntaxTree.GetRoot() .DescendantNodes() .OfType <VariableDeclarationSyntax>() .Where(declaration => declaration.Parent?.IsAnyOfKinds(SyntaxKind.LocalDeclarationStatement, SyntaxKind.UsingStatement) == true && DeclarationDoesNotUseVarKeyword(declaration) && DeclarationDeclaresExactlyOneVariable(declaration) && InitializationContainsOnlyObjectCreation(declaration) && LeftSideTypeIsExactlyTheSameAsRightSideType(declaration) // TODO-IG: Check that there is no type named var declared in the scope. // TODO-IG: Check that there is now generic parameter named var in the scope. ) .Select(declaration => new AnalysisResult ( this, analysisContext, syntaxTree.FilePath, declaration.GetFirstToken(), declaration ))); bool DeclarationDoesNotUseVarKeyword(VariableDeclarationSyntax declaration) { return(declaration.Type?.IsVar == false); } bool DeclarationDeclaresExactlyOneVariable(VariableDeclarationSyntax declaration) { return(declaration.Variables.Count == 1); } bool InitializationContainsOnlyObjectCreation(VariableDeclarationSyntax declaration) { return(declaration.Variables[0] .Initializer?.Value?.IsKind(SyntaxKind.ObjectCreationExpression) == true); } bool LeftSideTypeIsExactlyTheSameAsRightSideType(VariableDeclarationSyntax declaration) { var leftSideType = semanticModel.GetTypeInfo(declaration.Type).Type; if (leftSideType == null || leftSideType is IErrorTypeSymbol) { return(false); } var objectCreationExpression = (ObjectCreationExpressionSyntax)declaration.Variables[0].Initializer.Value; var rightSideType = semanticModel.GetTypeInfo(objectCreationExpression).Type; if (rightSideType == null || rightSideType is IErrorTypeSymbol) { return(false); } return(leftSideType.Equals(rightSideType)); } }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { // TODO-IG: We can have more than one case section pointing to the same statements ;-) // E.g. case 1: case 2: case 3: ... // Implement also this case properly. // One possibility could be, depending e.g. on the number of the cases pointing to // the same statements to offer a "Consider" suggestion. return(syntaxTree.GetRoot() .DescendantNodes() .OfType <SwitchStatementSyntax>() .Select(GetSwitchStatementPotentialReplaceabilityInfo) .Where(replaceabilityInfo => replaceabilityInfo.switchStatement != null) .Select(replaceabilityInfo => new AnalysisResult ( GetSuggestion(replaceabilityInfo.category, replaceabilityInfo.isSurelyExhaustive), analysisContext, syntaxTree.FilePath, replaceabilityInfo.switchStatement.SwitchKeyword, replaceabilityInfo.switchStatement ))); ISharpenSuggestion GetSuggestion(SwitchStatementSectionsCategory category, bool isSurelyExhaustive) { // TODO-IG: It will be so nice to switch to switch statement one day we move Sharpen to C# 8.0 :-) if (category == SwitchStatementSectionsCategory.AllSwitchSectionsAreAssignmentsToTheSameIdentifier) { return(isSurelyExhaustive ? (ISharpenSuggestion)ReplaceSwitchStatementContainingOnlyAssignmentsWithSwitchExpression.Instance : ConsiderReplacingSwitchStatementContainingOnlyAssignmentsWithSwitchExpression.Instance); } else { return(isSurelyExhaustive ? (ISharpenSuggestion)ReplaceSwitchStatementContainingOnlyReturnsWithSwitchExpression.Instance : ConsiderReplacingSwitchStatementContainingOnlyReturnsWithSwitchExpression.Instance); } } (SwitchStatementSyntax switchStatement, SwitchStatementSectionsCategory category, bool isSurelyExhaustive) GetSwitchStatementPotentialReplaceabilityInfo(SwitchStatementSyntax switchStatement) { // We have to have at least one switch section (case or default). if (switchStatement.Sections.Count <= 0) { return(null, SwitchStatementSectionsCategory.None, false); } // If we have the default section it is surely exhaustive. // Otherwise we cannot be sure. We will of course not do any // proper check that the compiler does. bool isSurelyExhaustive = switchStatement.Sections.Any(switchSection => switchSection.Labels.Any(label => label.IsKind(SyntaxKind.DefaultSwitchLabel))); if (AllSwitchSectionsAreAssignmentsToTheSameIdentifier(switchStatement.Sections)) { return(switchStatement, SwitchStatementSectionsCategory.AllSwitchSectionsAreAssignmentsToTheSameIdentifier, isSurelyExhaustive); } if (AllSwitchSectionsAreReturnStatements(switchStatement.Sections)) { return(switchStatement, SwitchStatementSectionsCategory.AllSwitchSectionsAreReturnStatements, isSurelyExhaustive); } return(null, SwitchStatementSectionsCategory.None, false); bool AllSwitchSectionsAreAssignmentsToTheSameIdentifier(SyntaxList <SwitchSectionSyntax> switchSections) { string previousIdentifier = null; foreach (var switchSection in switchSections) { // TODO-IG: Valid case is also throwing an exception instead of assigning to an identifier. // We expect exactly two statements, the assignment and the break; if (switchSection.Statements.Count != 2) { return(false); } if (!switchSection.Statements[1].IsKind(SyntaxKind.BreakStatement)) { return(false); } if (!(switchSection.Statements[0] is ExpressionStatementSyntax expression)) { return(false); } if (!(expression.Expression is AssignmentExpressionSyntax assignment)) { return(false); } // TODO-IG: Compare by symbol and not by name/text because we can have this._field, or something.Something, or a static access or similar. var currentIdentifier = assignment.Left?.ToString(); if (previousIdentifier != null && previousIdentifier != currentIdentifier) { return(false); } previousIdentifier = currentIdentifier; } return(true); } bool AllSwitchSectionsAreReturnStatements(SyntaxList <SwitchSectionSyntax> switchSections) { foreach (var switchSection in switchSections) { // TODO-IG: Valid case is also throwing an exception instead of assigning to an identifier. // We expect exactly one statement, the return statement; if (switchSection.Statements.Count != 1) { return(false); } if (!(switchSection.Statements[0] is ReturnStatementSyntax @return)) { return(false); } if (@return.Expression == null) { return(false); } } return(true); } } }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { return(syntaxTree.GetRoot() .DescendantNodes() .OfType <FieldDeclarationSyntax>() .Select(GetPropertyNameArgumentsIfFieldDeclarationIsDependencyPropertyDeclaration) .Where(propertyNames => propertyNames.Length > 0) .SelectMany(propertyNames => propertyNames) .Select(propertyName => new AnalysisResult ( this, analysisContext, syntaxTree.FilePath, propertyName.GetFirstToken(), propertyName, GetRegisterMethodDisplayText ))); ArgumentSyntax[] GetPropertyNameArgumentsIfFieldDeclarationIsDependencyPropertyDeclaration(FieldDeclarationSyntax fieldDeclaration) { // Let's first do fast checks that quickly and cheaply eliminate obvious non-candidates. // We want to use the semantic model only if we are sure that we have candidates that are // very likely field declarations that we are looking for. if (!(RequiredDependencyPropertyFieldModifiers.All(modifier => fieldDeclaration.Modifiers.Select(token => token.Kind()).Contains(modifier)) && fieldDeclaration.Declaration.Type.GetLastToken().ValueText == "DependencyProperty")) { return(Array.Empty <ArgumentSyntax>()); } return(fieldDeclaration .Declaration .Variables .Where(variableDeclaration => variableDeclaration.Identifier.ValueText.EndsWith("Property", StringComparison.Ordinal) && variableDeclaration.Initializer != null && variableDeclaration.Initializer.Value.IsKind(SyntaxKind.InvocationExpression)) .Select(variableDeclaration => new { FieldName = variableDeclaration.Identifier.ValueText, Invocation = (InvocationExpressionSyntax)variableDeclaration.Initializer.Value }) .Where(fieldNameAndInvocation => fieldNameAndInvocation.Invocation.Expression.GetLastToken().ValueText == "Register" && fieldNameAndInvocation.Invocation.ArgumentList.Arguments.Count > 0 && fieldNameAndInvocation.Invocation.ArgumentList.Arguments[0].Expression.IsKind(SyntaxKind.StringLiteralExpression) && fieldNameAndInvocation.Invocation.ArgumentList.Arguments[0].Expression.GetLastToken().ValueText is string propertyName && fieldNameAndInvocation.FieldName.StartsWith(propertyName, StringComparison.Ordinal) && fieldNameAndInvocation.FieldName.Length == propertyName.Length + "Property".Length && // Check that they are equal without creating temporary strings. InstancePropertyWithPropertyNameExists(propertyName, fieldNameAndInvocation.Invocation) && // If we reach this point it means that we have a field declaration that in a real case most likely // 99.9999 % percent sure represents a dependency property declaration. Cool :-) // Still, we have to check for those 0.00001% since we could have a situation sketched // in the smoke tests, where the Register method or the DependencyProperty class are not the "real" one. // I am asking myself now if this level of paranoia is really appropriate considering the cost of // this additional analysis. Hmmm. // TODO-THINK: We can provide an analysis option like "Use optimistic analysis" that would skip the below test. semanticModel.GetSymbolInfo(fieldNameAndInvocation.Invocation).Symbol is IMethodSymbol method && method.ContainingType?.Name == "DependencyProperty" && (method.ContainingType.ContainingType == null || method.ContainingType.ContainingType.IsNamespace) && // It must not be nested in another type. method.ContainingType.ContainingNamespace.FullNameIsEqualTo("System.Windows") // To be really sure, we should check here that the DependencyProperty type is // really the System.Windows.DependencyProperty. And this check would add a whole // bunch of super crazy paranoia on already paranoid check ;-) // Basically, the only possibility for this check to fail would be if someone // introduces it's own type named DependencyProperty that implicitly converts // the System.Windows.DependencyProperty to itself. This is demonstrated in // smoke tests but it is completely crazy. Who would ever do that? // And since this check would be quite complex to implement, we will simply skip it. ) .Select(fieldNameAndInvocation => fieldNameAndInvocation.Invocation.ArgumentList.Arguments[0]) .ToArray()); bool InstancePropertyWithPropertyNameExists(string propertyName, SyntaxNode anyChildNodeWithinTypeDeclaration) { var typeDeclaration = anyChildNodeWithinTypeDeclaration.FirstAncestorOrSelf <TypeDeclarationSyntax>(); if (typeDeclaration == null) { return(false); } // We could cache the information about non static properties per type declaration // to avoid traversing the tree every time. // But in the real world the number of dependency properties per type is very moderate. // This would most likely be a premature optimization. // So let's leave it this way. return(typeDeclaration .DescendantNodes() .OfType <PropertyDeclarationSyntax>( ) .Any(property => property.Modifiers.All(token => !token.IsKind(SyntaxKind.StaticKeyword)) && property.Identifier.ValueText == propertyName )); } } }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { return(syntaxTree.GetRoot() .DescendantNodes() .OfType <DefaultExpressionSyntax>() .Where(expression => expression.FirstAncestorOrSelf <ParameterSyntax>() != null && expression.FirstAncestorOrSelf <TBaseMethodDeclarationSyntax>() != null ) .Select(expression => new { Parameter = expression.FirstAncestorOrSelf <ParameterSyntax>(), DefaultExpressionType = semanticModel.GetTypeInfo(expression).Type, DefaultKeywordToken = expression.Keyword } ) .Where(types => types.DefaultExpressionType.Equals(semanticModel.GetTypeInfo(types.Parameter.Type).Type)) .Select(types => new AnalysisResult ( this, analysisContext, syntaxTree.FilePath, types.DefaultKeywordToken, types.Parameter ))); }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { return(syntaxTree.GetRoot() .DescendantNodes() .OfType <ArgumentSyntax>() .Where(argument => argument.RefOrOutKeyword.IsKind(SyntaxKind.OutKeyword) && argument.Expression.IsKind(SyntaxKind.IdentifierName) && GetLocalVariableDeclaratorForIdentifier(argument, ((IdentifierNameSyntax)argument.Expression).Identifier) is VariableDeclaratorSyntax declarator && VariableIsNotUsedBeforePassedAsOutArgument(semanticModel, declarator, argument) ) .Select(argument => new AnalysisResult ( this, analysisContext, syntaxTree.FilePath, argument.RefOrOutKeyword, argument.FirstAncestorOrSelf <InvocationExpressionSyntax>() ))); }
public IEnumerable<AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { return syntaxTree.GetRoot() .DescendantNodes() .OfKind(SyntaxKind.CoalesceExpression) ); }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { bool shouldVarBeUsed(VariableDeclarationSyntax declaration) { var LHSType = semanticModel.GetTypeInfo(declaration.ChildNodes()? .FirstOrDefault(syntax => syntax is PredefinedTypeSyntax || syntax is GenericNameSyntax || syntax is QualifiedNameSyntax || syntax is IdentifierNameSyntax)).Type; int totalDeclarationsInLine = declaration.DescendantNodes().Count(x => x is VariableDeclaratorSyntax); var RHSType = totalDeclarationsInLine > 1 ? null : semanticModel.GetTypeInfo( declaration.DescendantNodes() .FirstOrDefault(node => node is ObjectCreationExpressionSyntax).ChildNodes()? .FirstOrDefault(syntax => syntax is QualifiedNameSyntax || syntax is GenericNameSyntax || syntax is PredefinedTypeSyntax || syntax is IdentifierNameSyntax ).Parent ).Type; return((LHSType != null && RHSType != null) && (LHSType.Equals(RHSType))); } return(syntaxTree.GetRoot().DescendantNodes().OfType <VariableDeclarationSyntax>() .Where(shouldVarBeUsed) .Select(declaration => new AnalysisResult( this, analysisContext, syntaxTree.FilePath, declaration.GetFirstToken(), declaration))); }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { return(syntaxTree.GetRoot() .DescendantNodes() .OfType <ConstructorDeclarationSyntax>() .Where (constructor => constructor.Body != null && constructor.Body.Statements.Count == 1 && constructor.Body.Statements[0].IsKind(SyntaxKind.ExpressionStatement) ) .Select(constructor => new AnalysisResult ( this, analysisContext, syntaxTree.FilePath, constructor.Identifier, constructor ))); }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { // TODO: What to do when a variable is declared by using the var keyword? return(syntaxTree.GetRoot() .DescendantNodes() .Where(node => node.IsAnyOfKinds ( // TODO: Parameter declaration with null as default value: string parameter = null. // TODO: Variable declaration with initialization to null: string variable = null. // TODO: Field declaration with initialization to null: private string _field = null. // TODO: Property declaration with initialization to null: public string Property { get; } = null. // TODO: Usage of ??: x = identifier ?? something. // TODO: Usage of ?.: identifier?.Something. // TODO: Usage of as: identifier = something as Something. SyntaxKind.SimpleAssignmentExpression, SyntaxKind.EqualsExpression, SyntaxKind.NotEqualsExpression )) .Select(GetNullableIdentifierNode) .Where(identifier => identifier != null) .Select(identifier => semanticModel.GetSymbolInfo(identifier).Symbol) .Where(symbol => symbol?.IsImplicitlyDeclared == false) .Distinct() .Select(symbol => (symbol, declaringNode: GetSymbolDeclaringNode(symbol))) .Where(symbolAndDeclaringNode => !string.IsNullOrEmpty(symbolAndDeclaringNode.declaringNode?.SyntaxTree?.FilePath)) .Select(symbolAndDeclaringNode => { var(startingToken, displayTextNode) = GetStartingTokenAndDisplayTextNode(symbolAndDeclaringNode.declaringNode); return new AnalysisResult ( GetSuggestion(symbolAndDeclaringNode.symbol), analysisContext, symbolAndDeclaringNode.declaringNode.SyntaxTree.FilePath, startingToken, displayTextNode ); })); (SyntaxToken startingToken, SyntaxNode displayTextNode) GetStartingTokenAndDisplayTextNode(SyntaxNode symbolDeclaringNode) { return(symbolDeclaringNode.GetFirstToken(), symbolDeclaringNode); } ISharpenSuggestion GetSuggestion(ISymbol symbol) { switch (symbol) { case IFieldSymbol _: return(EnableNullableContextAndDeclareReferenceFieldAsNullable.Instance); case IPropertySymbol _: return(EnableNullableContextAndDeclareReferencePropertyAsNullable.Instance); case IParameterSymbol _: return(EnableNullableContextAndDeclareReferenceParameterAsNullable.Instance); case ILocalSymbol _: return(EnableNullableContextAndDeclareReferenceVariableAsNullable.Instance); // TODO: The default case should never happen. See what to return. default: return(EnableNullableContextAndDeclareReferenceFieldAsNullable.Instance); } } SyntaxNode GetSymbolDeclaringNode(ISymbol symbol) { return(symbol.DeclaringSyntaxReferences.Length != 1 ? null : symbol.DeclaringSyntaxReferences[0].GetSyntax()); } SyntaxNode GetNullableIdentifierNode(SyntaxNode node) { // TODO: Ignore the case where the comparison with null is actually a null guard. // E.g.: if (identifier == null) throw ArgumentNullException(...); // E.g.: identifier ?? throw ArgumentNullException(...); // TODO: If the "value" in the property setter is checked for null // we could assume that the property is nullable. Think about it. switch (node) { case AssignmentExpressionSyntax assignment: return(assignment.Right?.IsKind(SyntaxKind.NullLiteralExpression) == true && assignment.Left?.IsAnyOfKinds(SyntaxKind.IdentifierName, SyntaxKind.SimpleMemberAccessExpression) == true ? assignment.Left : null); case BinaryExpressionSyntax comparison: return(comparison.Right?.IsKind(SyntaxKind.NullLiteralExpression) == true && comparison.Left?.IsAnyOfKinds(SyntaxKind.IdentifierName, SyntaxKind.SimpleMemberAccessExpression) == true ? comparison.Left : null); default: return(null); } } }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { return(syntaxTree.GetRoot() .DescendantNodes() .OfType <DefaultExpressionSyntax>() .Where(expression => expression.Parent.IsKind(SyntaxKind.ReturnStatement)) .Select(expression => new { ReturnStatement = (ReturnStatementSyntax)expression.Parent, DefaultExpressionType = semanticModel.GetTypeInfo(expression).Type, EnclosingDeclarationReturnType = GetEnclosingDeclarationReturnType(expression, semanticModel) } ) // DefaultExpressionType is never null and the EnclosingDeclarationReturnType can so far be null. // That's why it is important to call Equals exactly on the DefaultExpressionType. .Where(types => types.DefaultExpressionType.Equals(types.EnclosingDeclarationReturnType)) .Select(types => new AnalysisResult ( this, analysisContext, syntaxTree.FilePath, types.ReturnStatement.ReturnKeyword, types.ReturnStatement ))); }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { return(syntaxTree.GetRoot() .DescendantNodes() .OfType <SwitchStatementSyntax>() .Select(GetSwitchStatementPotentialReplaceabilityInfo) .Where(replaceabilityInfo => replaceabilityInfo.suggestion != null) .Select(replaceabilityInfo => new AnalysisResult ( replaceabilityInfo.suggestion, analysisContext, syntaxTree.FilePath, replaceabilityInfo.switchStatement.SwitchKeyword, replaceabilityInfo.switchStatement ))); (ISharpenSuggestion suggestion, SwitchStatementSyntax switchStatement) GetSwitchStatementPotentialReplaceabilityInfo(SwitchStatementSyntax switchStatement) { // We have to have at least one switch section (case or default). if (switchStatement.Sections.Count <= 0) { return(null, null); } // TODO-IG: We can have more than one case label within a single section. // E.g. case 1: case 2: case 3: ... // At the moment we will simply not support this case. // We insist so far on a single label. // Implement this case in the future by offering a "Consider" suggestion. if (switchStatement.Sections.Any(switchSection => switchSection.Labels.Count != 1)) { return(null, null); } // If we have the default section it is surely exhaustive. // Otherwise we cannot be sure. We will, of course, not do any // check for exhaustiveness that the compiler does. bool isSurelyExhaustive = switchStatement.Sections.Any(switchSection => switchSection.Labels.Any(label => label.IsKind(SyntaxKind.DefaultSwitchLabel))); if (AllSwitchSectionsAreAssignmentsToTheSameIdentifier(switchStatement.Sections)) { return ( isSurelyExhaustive ? (ISharpenSuggestion)ReplaceSwitchStatementContainingOnlyAssignmentsWithSwitchExpression.Instance : ConsiderReplacingSwitchStatementContainingOnlyAssignmentsWithSwitchExpression.Instance, switchStatement ); } if (AllSwitchSectionsAreReturnStatements(switchStatement.Sections)) { return ( isSurelyExhaustive ? (ISharpenSuggestion)ReplaceSwitchStatementContainingOnlyReturnsWithSwitchExpression.Instance : ConsiderReplacingSwitchStatementContainingOnlyReturnsWithSwitchExpression.Instance, switchStatement ); } return(null, null); bool AllSwitchSectionsAreAssignmentsToTheSameIdentifier(SyntaxList <SwitchSectionSyntax> switchSections) { ISymbol previousIdentifierSymbol = null; foreach (var switchSection in switchSections) { // We can have two cases that are valid. switch (switchSection.Statements.Count) { // We have only one statement which then must be exception throwing. case 1: if (!switchSection.Statements[0].IsKind(SyntaxKind.ThrowStatement)) { return(false); } break; // We have two statements, which then must be an assignment immediately followed by break. case 2: if (!switchSection.Statements[1].IsKind(SyntaxKind.BreakStatement)) { return(false); } if (!(switchSection.Statements[0] is ExpressionStatementSyntax expression)) { return(false); } if (!(expression.Expression is AssignmentExpressionSyntax assignment)) { return(false); } // TODO-IG: At the moment we do not support compound assignments (+=, *=, -=, /=, ...). // We insist so far on the simple assignment operator (=). // Implement this case in the future by offering a "Consider" suggestion. if (!assignment.IsKind(SyntaxKind.SimpleAssignmentExpression)) { return(false); } if (assignment.Left == null) { return(false); } var currentIdentifierSymbol = semanticModel.GetSymbolInfo(assignment.Left).Symbol; if (currentIdentifierSymbol == null) { return(false); } if (previousIdentifierSymbol != null && !previousIdentifierSymbol.Equals(currentIdentifierSymbol)) { return(false); } previousIdentifierSymbol = currentIdentifierSymbol; break; default: return(false); } } return(true); } bool AllSwitchSectionsAreReturnStatements(SyntaxList <SwitchSectionSyntax> switchSections) { foreach (var switchSection in switchSections) { // Valid cases are either throwing an exception or having return. // In both cases we expect exactly one statement. if (switchSection.Statements.Count != 1) { return(false); } switch (switchSection.Statements[0].Kind()) { case SyntaxKind.ReturnStatement: var returnStatement = (ReturnStatementSyntax)switchSection.Statements[0]; // The statement must return something. // We are not interested in checking the type // of the returned expression, checking that // they are always the same. We assume that the switch // statement is already valid. if (returnStatement.Expression == null) { return(false); } break; case SyntaxKind.ThrowStatement: // This is just fine. break; default: return(false); } } return(true); } } }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { return(syntaxTree.GetRoot() .DescendantNodes() .OfType <UsingStatementSyntax>() .Select(GetUsingStatementPotentialReplaceabilityInfo) .Where(replaceabilityInfo => replaceabilityInfo.suggestion != null) .Select(replaceabilityInfo => new AnalysisResult ( replaceabilityInfo.suggestion, analysisContext, syntaxTree.FilePath, replaceabilityInfo.usingStatement.UsingKeyword, replaceabilityInfo.usingStatement ))); (ISharpenSuggestion suggestion, UsingStatementSyntax usingStatement) GetUsingStatementPotentialReplaceabilityInfo(UsingStatementSyntax usingStatement) { var outermostUsing = usingStatement; // Don't offer on a using statement that is parented by another using statement. // We'll just offer on the topmost using statement. if (!(outermostUsing.Parent is BlockSyntax parentBlock)) { return(null, null); } 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(null, null); } } // Verify that changing this using-statement into a using-declaration will not // change semantics. var parentStatements = parentBlock.Statements; var index = parentStatements.IndexOf(outermostUsing); // TODO: At the moment, if we have jumps (OMG!) we will simply not deal with it at all. if (!UsingStatementDoesNotInvolveJumps()) { return(null, null); } // Everything is fine, the using statement *could* be replaced // with the using declaration, the potential leakage of the using value // determines if it is 100% save to do it or not. return ( UsingValueDoesNotLeakToFollowingStatements() ? (ISharpenSuggestion)ReplaceUsingStatementWithUsingDeclaration.Instance : ConsiderReplacingUsingStatementWithUsingDeclaration.Instance, usingStatement ); bool UsingValueDoesNotLeakToFollowingStatements() { // 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. // Very last statement in the block. Can be converted. if (index == parentStatements.Count - 1) { return(true); } // Not the last statement, get the next statement and examine that. var nextStatement = parentStatements[index + 1]; // 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. if (nextStatement is BreakStatementSyntax || nextStatement is ContinueStatementSyntax) { return(true); } // 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 the using called Dispose() instead of after, potentially // changing semantics. if (nextStatement is ReturnStatementSyntax returnStatement && returnStatement.Expression == null) { return(true); } // TODO-IG: I've just discovered a bug in the Roslyn implementation :-) // Local function declarations should also be allowed after the // using since they are not executed. Open issue on Roslyn on // GitHub and also provide a fix. return(false); } bool UsingStatementDoesNotInvolveJumps() { // 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. // TODO-IG: Check for after as well in the case of "Consider" suggestion where we will have statements after the using. // 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 (int i = 0; i < index; i++) { var priorStatement = parentStatements[i]; if (IsGotoOrLabeledStatement(priorStatement)) { return(false); } } var innerStatements = innermostUsing.Statement is BlockSyntax block ? block.Statements // TODO-IG: See how to create a SyntaxList. This version of Roslyn does not support the constructor with parameters. : new SyntaxList <StatementSyntax>().Add(innermostUsing.Statement); foreach (var statement in innerStatements) { if (IsGotoOrLabeledStatement(statement)) { return(false); } } return(true); bool IsGotoOrLabeledStatement(StatementSyntax priorStatement) => priorStatement.Kind() == SyntaxKind.GotoStatement || priorStatement.Kind() == SyntaxKind.LabeledStatement; } } }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { return(syntaxTree.GetRoot() .DescendantNodes() .OfType <ArgumentSyntax>() .Where(argument => argument.RefOrOutKeyword.IsKind(SyntaxKind.OutKeyword) && argument.Expression.IsKind(SyntaxKind.IdentifierName) && OutArgumentCanBecomeOutVariableOrCanBeDiscarded(semanticModel, argument, outArgumentCanBeDiscarded) ) .Select(argument => new AnalysisResult ( this, analysisContext, syntaxTree.FilePath, argument.RefOrOutKeyword, argument.FirstAncestorOrSelf <TEnclosingExpressionSyntax>() ))); }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { return(syntaxTree.GetRoot() .DescendantNodes() .OfType <AccessorDeclarationSyntax>() .Where (accessor => accessor.Keyword.IsKind(SyntaxKind.GetKeyword) && accessor.Body != null && accessor.Body.Statements.Count == 1 && accessor.Body.Statements[0].IsKind(SyntaxKind.ReturnStatement) && ((AccessorListSyntax)accessor.Parent).Accessors.Count > 1 && // We must have the set-accessor as well (see [1]). accessor.FirstAncestorOrSelf <TBasePropertyDeclarationSyntax>() != null // We must be either in a property or in an indexer. ) .Select(accessor => new AnalysisResult ( this, analysisContext, syntaxTree.FilePath, accessor.Keyword, accessor.FirstAncestorOrSelf <TBasePropertyDeclarationSyntax>() ))); }
public IEnumerable <AnalysisResult> Analyze(SyntaxTree syntaxTree, SemanticModel semanticModel, SingleSyntaxTreeAnalysisContext analysisContext) { return(syntaxTree.GetRoot() .DescendantNodes() .OfType <MethodDeclarationSyntax>() .Where(method => method.IsAsync()) .SelectMany(method => method.DescendantNodes()) // We can have both methods (invocation) and properties (simple member access). // Unfortunately, it is not possible to combine them under a single umbrella. .Where(node => ( node.IsKind(SyntaxKind.InvocationExpression) && !node.IsWithinLambdaOrAnonymousMethod() && // See [1]. ((InvocationExpressionSyntax)node).GetInvokedMemberName() == replacementInfo.SynchronousMemberName || node.IsKind(SyntaxKind.SimpleMemberAccessExpression) && !node.IsWithinLambdaOrAnonymousMethod() && // See [1]. node.Parent?.IsKind(SyntaxKind.InvocationExpression) == false && ((MemberAccessExpressionSyntax)node).Name.Identifier.ValueText == replacementInfo.SynchronousMemberName ) && InvokedMemberHasAsynchronousEquivalentThatCanBeAwaited(node) ) .Select(node => new AnalysisResult( this, analysisContext, syntaxTree.FilePath, GetStartingSyntaxNode(node).GetFirstToken(), node.FirstAncestorOrSelf <StatementSyntax>() ?? node ))); bool InvokedMemberHasAsynchronousEquivalentThatCanBeAwaited(SyntaxNode invocation) { var invokedMember = semanticModel.GetSymbolInfo(invocation).Symbol; if (invokedMember?.ContainingType == null) { return(false); } return(invokedMember.Name == replacementInfo.SynchronousMemberName && invokedMember.ContainingType.FullNameIsEqualTo(replacementInfo.SynchronousMemberTypeNamespace, replacementInfo.SynchronousMemberTypeName)); } SyntaxNode GetStartingSyntaxNode(SyntaxNode node) { MemberAccessExpressionSyntax memberAccess; if (node.IsKind(SyntaxKind.InvocationExpression)) { var invocation = (InvocationExpressionSyntax)node; memberAccess = invocation.Expression as MemberAccessExpressionSyntax; if (memberAccess == null) { return(invocation.Expression); } } else { memberAccess = (MemberAccessExpressionSyntax)node; } return(memberAccess.Name ?? (SyntaxNode)memberAccess); } }