Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Proof Of Concept) Order Preservation for Parameter Guard #27

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,4 @@ UpgradeLog*.htm

# Microsoft Fakes
FakesAssemblies/
/NullParameterCheckRefactoring_(CS+VB)
8 changes: 7 additions & 1 deletion NullParameterCheckRefactoring.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.22310.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NullParameterCheckRefactoring", "src\NullParameterCheckRefactoring\NullParameterCheckRefactoring.csproj", "{6FD2E415-46CD-4370-87BD-A5E2D03A5C0D}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NullParameterCheckRefactoring.CSharp", "src\NullParameterCheckRefactoring.CSharp\NullParameterCheckRefactoring.CSharp.csproj", "{6FD2E415-46CD-4370-87BD-A5E2D03A5C0D}"
EndProject
Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "NullParameterCheckRefactoring.VB", "src\NullParameterCheckRefactoring.VB\NullParameterCheckRefactoring.VB.vbproj", "{5C5C6CFE-22CB-4E7A-9916-B6F28785B5C0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NullParameterCheckRefactoring.Vsix", "src\NullParameterCheckRefactoring.Vsix\NullParameterCheckRefactoring.Vsix.csproj", "{E9BE117A-4BF4-41C3-843A-CB659B433963}"
EndProject
Expand All @@ -17,6 +19,10 @@ Global
{6FD2E415-46CD-4370-87BD-A5E2D03A5C0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6FD2E415-46CD-4370-87BD-A5E2D03A5C0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6FD2E415-46CD-4370-87BD-A5E2D03A5C0D}.Release|Any CPU.Build.0 = Release|Any CPU
{5C5C6CFE-22CB-4E7A-9916-B6F28785B5C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5C5C6CFE-22CB-4E7A-9916-B6F28785B5C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5C5C6CFE-22CB-4E7A-9916-B6F28785B5C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5C5C6CFE-22CB-4E7A-9916-B6F28785B5C0}.Release|Any CPU.Build.0 = Release|Any CPU
{E9BE117A-4BF4-41C3-843A-CB659B433963}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E9BE117A-4BF4-41C3-843A-CB659B433963}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E9BE117A-4BF4-41C3-843A-CB659B433963}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>NullParameterCheckRefactoring</RootNamespace>
<AssemblyName>NullParameterCheckRefactoring</AssemblyName>
<AssemblyName>NullParameterCheckRefactoring.CSharp</AssemblyName>
<TargetFrameworkProfile>Profile7</TargetFrameworkProfile>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
</PropertyGroup>
Expand Down Expand Up @@ -53,6 +53,12 @@
<HintPath>..\..\packages\Microsoft.CodeAnalysis.Workspaces.Common.1.0.0-beta1-20141031-01\lib\portable-net45+win8\Microsoft.CodeAnalysis.Workspaces.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="RoslynExts.CS">
<HintPath>..\..\packages\RoslynExts.1.0.7\lib\portable-net45+win8\RoslynExts.CS.dll</HintPath>
</Reference>
<Reference Include="RoslynExts.VB">
<HintPath>..\..\packages\RoslynExts.1.0.7\lib\portable-net45+win8\RoslynExts.VB.dll</HintPath>
</Reference>
<Reference Include="System.Collections.Immutable">
<HintPath>..\..\packages\System.Collections.Immutable.1.1.32-beta\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll</HintPath>
<Private>False</Private>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using System;
using System.Collections.Generic;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using RoslynExts.CS;


namespace NullParameterCheckRefactoring
{
[ExportCodeRefactoringProvider(RefactoringId, LanguageNames.CSharp), Shared]
public class NullParameterCheckRefactoringProvider : CodeRefactoringProvider
{
internal const string RefactoringId = "TR0001";

public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
SyntaxNode root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
SyntaxNode node = root.FindNode(context.Span);
ParameterSyntax parameterSyntax = node as ParameterSyntax;

if (parameterSyntax != null)
{
TypeSyntax paramTypeName = parameterSyntax.Type;
SemanticModel semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken);
ITypeSymbol type = semanticModel.GetTypeInfo(paramTypeName).ConvertedType;

BaseMethodDeclarationSyntax methodDeclaration = parameterSyntax.Parent.Parent as BaseMethodDeclarationSyntax;
IEnumerable<IfStatementSyntax> availableIfStatements = methodDeclaration.Body.ChildNodes().OfType<IfStatementSyntax>();

if (type.IsReferenceType)
{
// check if the null check already exists.
bool isNullCheckAlreadyPresent = availableIfStatements.Any(ifStatement =>
{
return ifStatement.ChildNodes()
.OfType<BinaryExpressionSyntax>()
.Where(x => x.IsKind(SyntaxKind.EqualsExpression))
.Any(expression =>
{
if (expression.Right.IsKind(SyntaxKind.NullLiteralExpression))
{
var identifierSyntaxt = expression.ChildNodes().OfType<IdentifierNameSyntax>().FirstOrDefault();
return (identifierSyntaxt != null) &&
(identifierSyntaxt.Identifier.Text.Equals(parameterSyntax.Identifier.Text, StringComparison.Ordinal));
}
return false;
});
});

if (isNullCheckAlreadyPresent == false)
{
CodeAction action = CodeAction.Create(
"Check parameter for null",
ct => AddParameterNullCheckAsync(context.Document, parameterSyntax, methodDeclaration, ct));

context.RegisterRefactoring(action);
}
}
}
}

private async Task<Document> AddParameterNullCheckAsync(Document document, ParameterSyntax parameter, BaseMethodDeclarationSyntax methodDeclaration, CancellationToken cancellationToken)
{

var nullCheckIfStatement =
"if( \{parameter.Identifier} == null ) { throw new ArgumentNullException( nameof( \{parameter.Identifier.Text} )); };".ToSExpr<IfStatementSyntax>();
SyntaxList<SyntaxNode> newStatements = methodDeclaration.Body.Statements.Insert(0, nullCheckIfStatement);
BlockSyntax newBlock = SyntaxFactory.Block(newStatements).WithAdditionalAnnotations(Formatter.Annotation);
SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken);
SyntaxNode newRoot = root.ReplaceNode(methodDeclaration.Body, newBlock);

return document.WithSyntaxRoot(newRoot);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("NullParameterCheckRefactoring")]
[assembly: AssemblyTitle("NullParameterCheckRefactoring.CSharp")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("NullParameterCheckRefactoring")]
[assembly: AssemblyProduct("NullParameterCheckRefactoring.CSharp")]
[assembly: AssemblyCopyright("Copyright © 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<package id="Microsoft.CodeAnalysis.CSharp.Workspaces" version="1.0.0-beta1-20141031-01" targetFramework="portable-net45+win" />
<package id="Microsoft.CodeAnalysis.Workspaces.Common" version="1.0.0-beta1-20141031-01" targetFramework="portable-net45+win" />
<package id="Microsoft.Composition" version="1.0.27" targetFramework="portable-net45+win" />
<package id="RoslynExts" version="1.0.7" targetFramework="portable-net45+win" />
<package id="System.Collections.Immutable" version="1.1.32-beta" targetFramework="portable-net45+win" />
<package id="System.Reflection.Metadata" version="1.0.17-beta" targetFramework="portable-net45+win" />
</packages>
187 changes: 187 additions & 0 deletions src/NullParameterCheckRefactoring.VB/CodeRefactoringProvider.vb
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
Imports Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory
'Imports Microsoft.CodeAnalysis.VisualBasic.SyntaxKind
Imports RoslynExts.VB


<ExportCodeRefactoringProvider(NullCheck_CodeRefactoringCodeRefactoringProvider.RefactoringId, LanguageNames.VisualBasic), [Shared]>
Friend Class NullCheck_CodeRefactoringCodeRefactoringProvider
Inherits CodeRefactoringProvider

Public Const RefactoringId As String = "TR0001"

Public NotOverridable Overrides Async Function ComputeRefactoringsAsync(context As CodeRefactoringContext) As Task
Dim root = Await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(False)
' Find the node at the selection.
Dim node = root.FindNode(context.Span)
' Only offer a refactoring if the selected node is a type statement node.
Dim _Identifier_ = node.Try(Of ModifiedIdentifierSyntax) : If _Identifier_ Is Nothing Then Return
Dim _parmeter_ = _Identifier_.Parent.Try(of ParameterSyntax) : If _parmeter_ Is Nothing Then Return
Dim _method_ = _parmeter_.Parent.Parent.Parent.Try(of MethodBlockSyntax) : If _method_ Is Nothing Then Return

Dim _Model_ = Await context.Document.GetSemanticModelAsync(context.CancellationToken)

Dim ifStatements = _method_.Statements.Where(Function(s) (TypeOf s Is MultiLineIfBlockSyntax) OrElse (TypeOf s Is SingleLineIfStatementSyntax))
Dim pinfo = _Model_.GetTypeInfo(_parmeter_.AsClause.Type, context.CancellationToken)
If pinfo.ConvertedType.IsReferenceType = False Then Return
Dim GuardStatements = ifStatements.Where(NullChecks(_parmeter_))

Dim IsNullCheckAlreadyPresent = GuardStatements.Any
If Not IsNullCheckAlreadyPresent Then context.RegisterRefactoring(CodeAction.Create("Check Parameter for null", Function(ct As CancellationToken) AddParameterNullCheckAsync(context.Document, _parmeter_, _method_, ct)))
End Function

Private Shared Function NullChecks(_parmeter_ As ParameterSyntax) As Func(Of StatementSyntax, Boolean)
Return Function(s)
If TypeOf s Is SingleLineIfStatementSyntax Then
Dim singleIF = s.As(of SingleLineIfStatementSyntax)
Dim isExpr = singleIF.Condition.Try(of BinaryExpressionSyntax)
If (isExpr Is Nothing) OrElse (Not isExpr.IsKind(SyntaxKind.IsExpression)) Then Return False
Dim res = CheckIfCondition(_parmeter_, isExpr)
If Not res Then Return res
Dim _sif_ = GetGuardStatement(_parmeter_).NormalizeWhitespace
Return Not singleIF.IsEquivalentTo(_sif_)
ElseIf TypeOf s Is MultiLineIfBlockSyntax Then
Dim multiIF = s.As(Of MultiLineIfBlockSyntax)
Dim isExpr = multiIF.IfStatement.Condition.Try(of BinaryExpressionSyntax)
If (isExpr Is Nothing) OrElse (Not isExpr.IsKind(SyntaxKind.IsExpression)) Then Return False
Dim res = CheckIfCondition(_parmeter_, isExpr)
If Not res Then Return res
Dim _mif_ = GetMultiLineGuardStatement(_parmeter_)
Return Not multiIF.WithoutAnnotations().IsEquivalentTo(_mif_)
End If
Return False
End Function
End Function

Private Shared Function CheckIfCondition(paramSyntax As ParameterSyntax, isExpr As BinaryExpressionSyntax) As Boolean
Dim l = isExpr.Left.Try(of IdentifierNameSyntax)
Dim r = isExpr.Right.Try(Of LiteralExpressionSyntax)
If l Is Nothing OrElse r Is Nothing Then Return False
If r.IsKind(SyntaxKind.NothingLiteralExpression) = False Then Return False
Return String.Compare(l.Identifier.Text, paramSyntax.Identifier.Identifier.Text, StringComparison.Ordinal) = 0
End Function

Private Shared Function CheckIfCondition(isExpr As BinaryExpressionSyntax) As Boolean
Dim l = isExpr.Left.Try(Of IdentifierNameSyntax)
Dim r = isExpr.Right.Try(Of LiteralExpressionSyntax)
If l Is Nothing OrElse r Is Nothing Then Return False
Return r.IsKind(SyntaxKind.NothingLiteralExpression)
End Function

Private Async Function AddParameterNullCheckAsync(document As Document,
_parmeter_ As ParameterSyntax,
method As MethodBlockSyntax,
cancellationToken As CancellationToken) As Task(Of Document)

Dim NewGuardStatement = GetGuardStatement(_parmeter_)

Dim ifStatements = method.Statements.Where(Function(s) (TypeOf s Is MultiLineIfBlockSyntax) OrElse (TypeOf s Is SingleLineIfStatementSyntax))
' Dim ifStatements = method.Statements.OfType(Of MultiLineIfBlockSyntax, SingleLineIfStatementSyntax)

Dim ExistingGuards = ifStatements.Where(
Function(s)
If TypeOf s Is SingleLineIfStatementSyntax Then Return CheckIfCondition(s.As(Of SingleLineIfStatementSyntax).Condition.Try(Of BinaryExpressionSyntax))
If TypeOf s Is MultiLineIfBlockSyntax Then Return CheckIfCondition(s.As(Of MultiLineIfBlockSyntax).IfStatement.Condition.Try(Of BinaryExpressionSyntax))
Return False
End Function)

Dim ExistingGuardParameters = ExistingGuards.Select(
Function(s)
If TypeOf s Is SingleLineIfStatementSyntax Then Return s.As(Of SingleLineIfStatementSyntax).Condition.Try(Of BinaryExpressionSyntax)?.Left.As(Of IdentifierNameSyntax)
If TypeOf s Is MultiLineIfBlockSyntax Then Return s.As(Of MultiLineIfBlockSyntax).IfStatement.Condition.Try(Of BinaryExpressionSyntax)?.Left.As(Of IdentifierNameSyntax)
Return Nothing
End Function)


Dim parameters = method.Begin.ParameterList.Parameters
Dim NumberOfParameters = parameters.Count


Dim Guards = Enumerable.Repeat(Of StatementSyntax)(Nothing, NumberOfParameters).ToArray
Dim parameterNames = parameters.Select(Function(p) p.Identifier).ToArray

For i = 0 To ExistingGuards.Count - 1
Dim eg = ExistingGuards(i)
Dim Id = ExistingGuardParameters(i)
Dim index = FindGuardIndex(parameterNames, Id)
If index.HasValue Then Guards(index.Value) = eg '.WithSameTriviaAs(eg)
Next

Dim NewGuardIndex = FindGuardIndex(parameterNames, _parmeter_.Identifier)
If NewGuardIndex.HasValue Then Guards(NewGuardIndex.Value) = NewGuardStatement.AddEOL

' Remove the existing guard statement
Dim newmethod = method.RemoveNodes(ExistingGuards, SyntaxRemoveOptions.KeepNoTrivia)
' Get the Guards that will exist
Dim NonNullGuards = Guards.WhereNonNull
' Insert them into the code
Dim newStatements = newmethod.Statements.InsertRange(0, NonNullGuards)
newmethod = newmethod.WithStatements(newStatements)
Dim newBlock = newmethod.WithAdditionalAnnotations(Formatting.Formatter.Annotation)
' Get the new document with the applied changes
Return document.WithSyntaxRoot((Await document.GetSyntaxRootAsync(cancellationToken)).ReplaceNode(method, newBlock))
End Function

Private Shared Function FindGuardIndex(ParametersNames() As ModifiedIdentifierSyntax, Id As IdentifierNameSyntax) As Integer?
For j = 0 To ParametersNames.Count - 1
If String.Compare(ParametersNames(j).Identifier.Text,
Id.Identifier.Text, StringComparison.Ordinal) = 0 Then Return New Integer?(j)
Next j
Return New Integer?()
End Function

Private Shared Function FindGuardIndex(ParametersNames() As ModifiedIdentifierSyntax, Id As ModifiedIdentifierSyntax) As Integer?
For j = 0 To ParametersNames.Count - 1
If String.Compare(ParametersNames(j).Identifier.Text,
Id.Identifier.Text, StringComparison.Ordinal) = 0 Then Return New Integer?(j)
Next j
Return New Integer?()
End Function



Private Shared Function GetGuardStatement(parameterStmt As ParameterSyntax) As SingleLineIfStatementSyntax
Return String.Format("If {0} Then {1}", GetIsNothingExpr(parameterStmt), GetThrowStatementForParameter(parameterStmt)).ToSExpr(Of SingleLineIfStatementSyntax)
End Function

Private Shared Function GetThrowStatementForParameter(parameterStmt As ParameterSyntax) As ThrowStatementSyntax
Return String.Format(" Throw New System.ArgumentNullException({0})", GetParameterName(parameterStmt)).ToSExpr(Of ThrowStatementSyntax)
End Function

Private Shared Function GetIsNothingExpr(forParameter As ParameterSyntax) As BinaryExpressionSyntax
Return String.Format(" {0} Is Nothing ", forParameter.Identifier.Identifier.Text).ToExpr(Of BinaryExpressionSyntax)
End Function

Private Shared Function GetMultiLineGuardStatement(parameterStmt As ParameterSyntax) As MultiLineIfBlockSyntax
Return String.Format("If {0} Then
{1}
End If",
GetIsNothingExpr(parameterStmt),
GetThrowStatementForParameter(parameterStmt)).ToSExpr(Of MultiLineIfBlockSyntax)

End Function

Private Shared Function GetParameterName(parameterStmt As ParameterSyntax) As LiteralExpressionSyntax
' Note: If I can find the nameof feature in VB.net, then I'll change this line to reflect that
Return StringLiteralExpression(Literal(parameterStmt.Identifier.Identifier.Text))
End Function

End Class

Public Module Exts

<Runtime.CompilerServices.Extension>
Public Function WhereNonNull(Of T As Class)(xs As IEnumerable(Of T)) As IEnumerable(Of T)
Return xs.Where(Function(x) x IsNot Nothing)
End Function

<Runtime.CompilerServices.Extension>
Public Function OfType(Of xT, T1 As xT, T2 As xT)(xs As IEnumerable(Of xT)) As IEnumerable(Of xT)
Return xs.Where(Function(x) (TypeOf xs Is T1) OrElse (TypeOf xs Is T2))
End Function


'<Runtime.CompilerServices.Extension>
'Public Function AddEOL(Of T0 As SyntaxNode)(node As T0) As T0
' Return node.WithTrailingTrivia(SyntaxFactory.SyntaxTrivia(SyntaxKind.EndOfLineTrivia, Environment.NewLine))
'End Function
End Module
29 changes: 29 additions & 0 deletions src/NullParameterCheckRefactoring.VB/My Project/AssemblyInfo.vb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Imports System.Reflection
Imports System.Runtime.InteropServices

' General Information about an assembly is controlled through the following
' set of attributes. Change these attribute values to modify the information
' associated with an assembly.

' Review the values of the assembly attributes

<Assembly: AssemblyTitle("NullParameterCheckRefactoring.VB")>
<Assembly: AssemblyDescription("")>
<Assembly: AssemblyCompany("")>
<Assembly: AssemblyProduct("NullParameterCheckRefactoring.VB")>
<Assembly: AssemblyCopyright("Copyright © 2014")>
<Assembly: AssemblyTrademark("")>

<Assembly: ComVisible(False)>

' Version information for an assembly consists of the following four values:
'
' Major Version
' Minor Version
' Build Number
' Revision
'
' You can specify all the values or you can default the Build and Revision Numbers
' by using the '*' as shown below:
<Assembly: AssemblyVersion("1.0.*")>
<Assembly: AssemblyFileVersion("1.0.0.0")>
Loading