Skip to content
This repository has been archived by the owner. It is now read-only.

Contract requires string not null or empty #272

Merged
merged 2 commits into from
Feb 20, 2017
Merged
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
3 changes: 3 additions & 0 deletions RefactoringEssentials.2017/RefactoringEssentials.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@
<Compile Include="..\RefactoringEssentials\CSharp\CodeRefactorings\Synced\ContractEnsuresNotNullReturnRefactoringProvider.cs">
<Link>CSharp\CodeRefactorings\Synced\ContractEnsuresNotNullReturnRefactoringProvider.cs</Link>
</Compile>
<Compile Include="..\RefactoringEssentials\CSharp\CodeRefactorings\Synced\ContractRequiresStringNotNullOrEmptyCodeRefactoringProvider.cs">
<Link>CSharp\CodeRefactorings\Synced\ContractRequiresStringNotNullOrEmptyCodeRefactoringProvider.cs</Link>
</Compile>
<Compile Include="..\RefactoringEssentials\CSharp\CodeRefactorings\Synced\ContractRequiresNotNullCodeRefactoringProvider.cs">
<Link>CSharp\CodeRefactorings\Synced\ContractRequiresNotNullCodeRefactoringProvider.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ protected IEnumerable<CodeAction> GetActions(Document document, SyntaxNode root,

protected IEnumerable<CodeAction> GetActions(Document document, SyntaxNode root, AccessorDeclarationSyntax node)
{
var propertyOrIndexerDeclaration = node.Ancestors().Where(n => n.GetType().Equals(typeof(PropertyDeclarationSyntax)) || n.GetType().Equals(typeof(IndexerDeclarationSyntax))).FirstOrDefault();
var propertyOrIndexerDeclaration = node.Ancestors().FirstOrDefault(n => n.GetType().Equals(typeof(PropertyDeclarationSyntax)) || n.GetType().Equals(typeof(IndexerDeclarationSyntax)));

var nullableType = propertyOrIndexerDeclaration.ChildNodes().OfType<NullableTypeSyntax>().FirstOrDefault();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Linq;
using System.Threading;
using System.Collections.Generic;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Formatting;

namespace RefactoringEssentials.CSharp.CodeRefactorings
{
[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = "Add a Contract to specify the string parameter must not be null or empty")]
/// <summary>
/// Creates a 'Contract.Requires(param != null);' contract for a parameter.
/// </summary>
public class ContractRequiresStringNotNullOrEmptyCodeRefactoringProvider : CodeContractsCodeRefactoringProvider
{
#region ICodeActionProvider implementation
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var codeContractsContext = await CodeContractsContext(context);
if (codeContractsContext == null)
return;

var foundNode = (ParameterSyntax)codeContractsContext.Node.AncestorsAndSelf().FirstOrDefault(n => n is ParameterSyntax);
if (foundNode == null)
return;

foreach (var action in GetActions(codeContractsContext.Document, codeContractsContext.SemanticModel, codeContractsContext.Root, codeContractsContext.TextSpan, foundNode))
context.RegisterRefactoring(action);
}
#endregion

protected IEnumerable<CodeAction> GetActions(Document document, SemanticModel semanticModel, SyntaxNode root, TextSpan span, ParameterSyntax node)
{
if (!node.Identifier.Span.Contains(span))
yield break;

var parameter = node;
var bodyStatement = parameter.Parent.Parent.ChildNodes().OfType<BlockSyntax>().FirstOrDefault();
if (bodyStatement == null)
yield break;

var parameterSymbol = semanticModel.GetDeclaredSymbol(node);
var type = parameterSymbol.Type;
if (type.SpecialType != SpecialType.System_String || HasReturnContract(bodyStatement, parameterSymbol.Name))
yield break;

yield return CreateAction(
node.Identifier.Span
, t2 => {
var newBody = bodyStatement.WithStatements(SyntaxFactory.List<StatementSyntax>(new[] { CreateContractRequiresCall(node.Identifier.ToString()) }.Concat(bodyStatement.Statements)));

var newRoot = (CompilationUnitSyntax)root.ReplaceNode((SyntaxNode)bodyStatement, newBody);

if (UsingStatementNotPresent(newRoot)) newRoot = AddUsingStatement(node, newRoot);

return Task.FromResult(document.WithSyntaxRoot(newRoot));
}
, "Add contract requiring parameter must not be null or empty"
);
}

static StatementSyntax CreateContractRequiresCall(string parameterName)
{
return SyntaxFactory.ParseStatement($"Contract.Requires(string.IsNullOrEmpty({parameterName}) == false);\r\n").WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);
}

static bool HasReturnContract(BlockSyntax bodyStatement, string parameterName)
{
var workspace = new AdhocWorkspace();

foreach (var expression in bodyStatement.DescendantNodes().OfType<ExpressionStatementSyntax>())
{
var formatted = Formatter.Format(expression, workspace).ToString();

if (formatted == $"Contract.Requires(string.IsNullOrEmpty({parameterName}) == false);")
return true;

if (formatted == $"Contract.Requires(false == string.IsNullOrEmpty({parameterName}));")
return true;

if (formatted == $"Contract.Requires(!string.IsNullOrEmpty({parameterName}));")
return true;
}
return false;
}
}
}
3 changes: 2 additions & 1 deletion RefactoringEssentials/CodeAnalyzers.CSharp.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

-->
<h2>Supported Code Analyzers</h2>
<p>119 code analyzers for C#</p>
<p>120 code analyzers for C#</p>
<ul>
<li>Suggests using the class declaring a static function when calling it (AccessToStaticMemberViaDerivedTypeAnalyzer)</li>
<li>When initializing explicitly typed local variable or array type, array creation expression can be replaced with array initializer. (ArrayCreationCanBeReplacedWithArrayInitializerAnalyzer)</li>
Expand Down Expand Up @@ -131,6 +131,7 @@ <h2>Supported Code Analyzers</h2>
<li>Warns when a culture-aware 'IndexOf' call is used by default. (StringIndexOfIsCultureSpecificAnalyzer)</li>
<li>Warns when a culture-aware 'LastIndexOf' call is used by default. (StringLastIndexOfIsCultureSpecificAnalyzer)</li>
<li>Warns when a culture-aware 'StartsWith' call is used by default. (StringStartsWithIsCultureSpecificAnalyzer)</li>
<li>Use 'var' keyword when possible (SuggestUseVarKeywordEvidentAnalyzer)</li>
<li>[ThreadStatic] doesn't work with instance fields (ThreadStaticAtInstanceFieldAnalyzer)</li>
<li>Parameter is never used (UnusedParameterAnalyzer)</li>
<li>Type parameter is never used (UnusedTypeParameterAnalyzer)</li>
Expand Down
5 changes: 4 additions & 1 deletion RefactoringEssentials/CodeRefactorings.CSharp.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

-->
<h2>Supported Refactorings</h2>
<p>101 code refactorings for C#</p>
<p>104 code refactorings for C#</p>
<ul>
<li>Adds another accessor (AddAnotherAccessorCodeRefactoringProvider)</li>
<li>Add braces (AddBracesCodeRefactoringProvider)</li>
Expand All @@ -34,6 +34,7 @@ <h2>Supported Refactorings</h2>
<li>Compute constant value (ComputeConstantValueCodeRefactoringProvider)</li>
<li>Add a Contract to specify the return value must not be null (ContractEnsuresNotNullReturnCodeRefactoringProvider)</li>
<li>Add a Contract to specify the parameter must not be null (ContractRequiresNotNullCodeRefactoringProvider)</li>
<li>Add a Contract to specify the string parameter must not be null or empty (ContractRequiresStringNotNullOrEmptyCodeRefactoringProvider)</li>
<li>Convert anonymous method to lambda expression (ConvertAnonymousMethodToLambdaCodeRefactoringProvider)</li>
<li>Convert auto-property to computed propertyy (ConvertAutoPropertyToPropertyCodeRefactoringProvider)</li>
<li>Replace bitwise flag comparison with call to 'Enum.HasFlag' (ConvertBitwiseFlagComparisonToHasFlagsCodeRefactoringProvider)</li>
Expand Down Expand Up @@ -102,10 +103,12 @@ <h2>Supported Refactorings</h2>
<li>Replace assignment with postfix expression (ReplaceAssignmentWithPostfixExpressionCodeRefactoringProvider)</li>
<li>Replace auto-property with property that uses a backing field (ReplaceAutoPropertyWithPropertyAndBackingFieldCodeRefactoringProvider)</li>
<li>Convert cast to 'as'. (ReplaceDirectCastWithSafeCastCodeRefactoringProvider)</li>
<li>Replace type with 'var' (ReplaceExplicitTypeWithVarCodeRefactoringProvider)</li>
<li>Replace operator assignment with assignment (ReplaceOperatorAssignmentWithAssignmentCodeRefactoringProvider)</li>
<li>Replace postfix expression with assignment (ReplacePostfixExpressionWithAssignmentCodeRefactoringProvider)</li>
<li>Replace property that uses a backing field with auto-property (ReplacePropertyWithBackingFieldWithAutoPropertyCodeRefactoringProvider)</li>
<li>Convert 'as' to cast. (ReplaceSafeCastWithDirectCastCodeRefactoringProvider)</li>
<li>Replaces 'var' with explicit type specification (ReplaceVarWithExplicitTypeCodeRefactoringProvider)</li>
<li>Replace assignment with operator assignment (ReplaceWithOperatorAssignmentCodeRefactoringProvider)</li>
<li>Reverse the direction of a for (ReverseDirectionForForLoopCodeRefactoringProvider)</li>
<li>Split declaration list (SplitDeclarationListCodeRefactoringProvider)</li>
Expand Down
1 change: 1 addition & 0 deletions RefactoringEssentials/RefactoringEssentials.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
<Compile Include="Converter\CodeWithOptions.cs" />
<Compile Include="Converter\ConversionResult.cs" />
<Compile Include="CSharp\CodeRefactorings\Custom\AddNullCheckCodeRefactoringProvider.cs" />
<Compile Include="CSharp\CodeRefactorings\Synced\ContractRequiresStringNotNullOrEmptyCodeRefactoringProvider.cs" />
<Compile Include="CSharp\CodeRefactorings\Synced\ContractRequiresNotNullCodeRefactoringProvider.cs" />
<Compile Include="CSharp\CodeRefactorings\Synced\ContractEnsuresNotNullReturnRefactoringProvider.cs" />
<Compile Include="CSharp\CodeRefactorings\Synced\MergeNestedIfAction.cs" />
Expand Down
5 changes: 4 additions & 1 deletion Tests.2017/Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,10 @@
<Compile Include="..\Tests\CSharp\CodeRefactorings\ContractEnsuresNotNullReturnTests.cs">
<Link>CSharp\CodeRefactorings\ContractEnsuresNotNullReturnTests.cs</Link>
</Compile>
<Compile Include="..\Tests\CSharp\CodeRefactorings\ContractRequiresNotNullTests.cs">
<Compile Include="..\Tests\CSharp\CodeRefactorings\ContractRequiresStringNotNullOrEmptyTests.cs">
<Link>CSharp\CodeRefactorings\ContractRequiresNotNullTests.cs</Link>
</Compile>
<Compile Include="..\Tests\CSharp\CodeRefactorings\ContractRequiresStringNotNullOrEmptyTests.cs">
<Link>CSharp\CodeRefactorings\ContractRequiresNotNullTests.cs</Link>
</Compile>
<Compile Include="..\Tests\CSharp\CodeRefactorings\ConvertAnonymousMethodToLambdaTests.cs">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
using System;
using NUnit.Framework;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis;
using System.Collections.Immutable;
using RefactoringEssentials.CSharp.CodeRefactorings;

namespace RefactoringEssentials.Tests.CSharp.CodeRefactorings
{
[TestFixture]
public class ContractRequiresStringNotNullOrEmptyTests : CSharpCodeRefactoringTestBase
{
[Test]
public void TestLambda()
{
Test<ContractRequiresStringNotNullOrEmptyCodeRefactoringProvider>(@"class Foo
{
void Test ()
{
var lambda = (string $s, int e) => {
};
}
}", @"using System.Diagnostics.Contracts;

class Foo
{
void Test ()
{
var lambda = (string s, int e) => {
Contract.Requires(string.IsNullOrEmpty(s) == false);
};
}
}");
}

[Test]
public void TestAnonymousMethod()
{
Test<ContractRequiresStringNotNullOrEmptyCodeRefactoringProvider>(@"class Foo
{
void Test ()
{
var lambda = delegate(string $-[s]-, object e) {
};
}
}", @"using System.Diagnostics.Contracts;

class Foo
{
void Test ()
{
var lambda = delegate(string s, object e) {
Contract.Requires(string.IsNullOrEmpty(s) == false);
};
}
}");
}

[Test]
public void TestContractAlreadyPresentEqualsFalseFormat()
{
TestWrongContext<ContractRequiresStringNotNullOrEmptyCodeRefactoringProvider>(@"class Foo
{
void Test ()
{
var lambda = (string $s, int e) => {
Contract.Requires(string.IsNullOrEmpty(s) == false);
};
}
}");
}

[Test]
public void TestContractAlreadyPresentFalseEqualsFormat()
{
TestWrongContext<ContractRequiresStringNotNullOrEmptyCodeRefactoringProvider>(@"class Foo
{
void Test ()
{
var lambda = (string $s, int e) => {
Contract.Requires(false==string.IsNullOrEmpty(s));
};
}
}");
}

[Test]
public void TestContractAlreadyPresentNegateFormat()
{
TestWrongContext<ContractRequiresStringNotNullOrEmptyCodeRefactoringProvider>(@"class Foo
{
void Test ()
{
var lambda = (string $s, int e) => {
Contract.Requires(!string.IsNullOrEmpty(s));
};
}
}");
}

[Test]
public void TestDifferentContractAlreadyPresent()
{
Test<ContractRequiresStringNotNullOrEmptyCodeRefactoringProvider>(@"class Foo
{
void Test ()
{
var lambda = (string $s, int e) => {
Contract.Requires(string.IsNullOrEmpty(notS) == false);
};
}
}", @"using System.Diagnostics.Contracts;

class Foo
{
void Test ()
{
var lambda = (string s, int e) => {
Contract.Requires(string.IsNullOrEmpty(s) == false);
Contract.Requires(string.IsNullOrEmpty(notS) == false);
};
}
}");
}

[Test]
public void TestUsingStatementAlreadyPresent()
{
Test<ContractRequiresStringNotNullOrEmptyCodeRefactoringProvider>(@"using System.Diagnostics.Contracts;
class Foo
{
void Test ()
{
var lambda = (string $s, int e) => {
};
}
}", @"using System.Diagnostics.Contracts;
class Foo
{
void Test ()
{
var lambda = (string s, int e) => {
Contract.Requires(string.IsNullOrEmpty(s) == false);
};
}
}");
}

[Test]
public void TestPopupOnlyOnName()
{
TestWrongContext<ContractRequiresStringNotNullOrEmptyCodeRefactoringProvider>(@"class Foo
{
void Test ($string param)
{
}
}");
}

[Test]
public void Test_OldCSharp()
{
var parseOptions = new CSharpParseOptions(
LanguageVersion.CSharp5,
DocumentationMode.Diagnose | DocumentationMode.Parse,
SourceCodeKind.Regular,
ImmutableArray.Create("DEBUG", "TEST")
);

Test<ContractRequiresStringNotNullOrEmptyCodeRefactoringProvider>(@"class Foo
{
void Test (string $test)
{
}
}", @"using System.Diagnostics.Contracts;

class Foo
{
void Test (string test)
{
Contract.Requires(string.IsNullOrEmpty(test) == false);
}
}", parseOptions: parseOptions);
}
}
}
1 change: 1 addition & 0 deletions Tests/Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
<Compile Include="CSharp\CodeFixes\InvalidConversionTests.cs" />
<Compile Include="CSharp\CodeFixes\ReturnMustNotBeFollowedByAnyExpressionCodeFixProviderTests.cs" />
<Compile Include="CSharp\CodeFixes\UnreachableCodeTests.cs" />
<Compile Include="CSharp\CodeRefactorings\ContractRequiresStringNotNullOrEmptyTests.cs" />
<Compile Include="CSharp\CodeRefactorings\ContractRequiresNotNullTests.cs" />
<Compile Include="CSharp\CodeRefactorings\ContractEnsuresNotNullReturnTests.cs" />
<Compile Include="CSharp\CodeRefactorings\GenerateSwitchLabelsTests.cs" />
Expand Down