Browse Source

Editor context actions - added context action which inserts null checks - for expressions like "a.b.c" inserts "if (a != null && a.b != null)".

pull/15/head
mkonicek 15 years ago
parent
commit
c20aafaee4
  1. 1
      src/AddIns/Misc/SharpRefactoring/Project/SharpRefactoring.addin
  2. 1
      src/AddIns/Misc/SharpRefactoring/Project/SharpRefactoring.csproj
  3. 18
      src/AddIns/Misc/SharpRefactoring/Project/Src/ContextActions/CheckAssignmentCache.cs
  4. 11
      src/AddIns/Misc/SharpRefactoring/Project/Src/ContextActions/CheckAssignmentNotNull.cs
  5. 8
      src/AddIns/Misc/SharpRefactoring/Project/Src/ContextActions/CheckAssignmentNull.cs
  6. 133
      src/AddIns/Misc/SharpRefactoring/Project/Src/ContextActions/CheckMemberNotNull.cs
  7. 52
      src/AddIns/Misc/SharpRefactoring/Project/Src/ContextActions/Extensions.cs
  8. 9
      src/Libraries/NRefactory/Project/Src/Visitors/SetRegionInclusionVisitor.cs

1
src/AddIns/Misc/SharpRefactoring/Project/SharpRefactoring.addin

@ -48,6 +48,7 @@
<Class class="SharpRefactoring.ContextActions.CheckAssignmentNotNull" /> <Class class="SharpRefactoring.ContextActions.CheckAssignmentNotNull" />
<Class class="SharpRefactoring.ContextActions.ParamCheckForNull" /> <Class class="SharpRefactoring.ContextActions.ParamCheckForNull" />
<Class class="SharpRefactoring.ContextActions.ParamRangeCheck" /> <Class class="SharpRefactoring.ContextActions.ParamRangeCheck" />
<Class class="SharpRefactoring.ContextActions.CheckMemberNotNull" />
</Path> </Path>
<Path name="/SharpDevelop/ViewContent/TextEditor/OverrideCompletionHandler"> <Path name="/SharpDevelop/ViewContent/TextEditor/OverrideCompletionHandler">

1
src/AddIns/Misc/SharpRefactoring/Project/SharpRefactoring.csproj

@ -77,6 +77,7 @@
<Compile Include="Src\ContextActions\CheckAssignmentCache.cs" /> <Compile Include="Src\ContextActions\CheckAssignmentCache.cs" />
<Compile Include="Src\ContextActions\CheckAssignmentNull.cs" /> <Compile Include="Src\ContextActions\CheckAssignmentNull.cs" />
<Compile Include="Src\ContextActions\CheckAssignmentNotNull.cs" /> <Compile Include="Src\ContextActions\CheckAssignmentNotNull.cs" />
<Compile Include="Src\ContextActions\CheckMemberNotNull.cs" />
<Compile Include="Src\ContextActions\Extensions.cs" /> <Compile Include="Src\ContextActions\Extensions.cs" />
<Compile Include="Src\ContextActions\FindTypeDeclarationsVisitor.cs" /> <Compile Include="Src\ContextActions\FindTypeDeclarationsVisitor.cs" />
<Compile Include="Src\ContextActions\GenerateMember.cs" /> <Compile Include="Src\ContextActions\GenerateMember.cs" />

18
src/AddIns/Misc/SharpRefactoring/Project/Src/ContextActions/CheckAssignmentCache.cs

@ -19,7 +19,8 @@ namespace SharpRefactoring.ContextActions
{ {
this.VariableName = GetVariableName(context); this.VariableName = GetVariableName(context);
this.CodeGenerator = GetCodeGenerator(context); this.CodeGenerator = GetCodeGenerator(context);
this.ElementRegion = GetStatementRegion(context); this.Element = GetElement(context);
this.ElementRegion = (Element == null) ? DomRegion.Empty : DomRegion.FromLocation(Element.StartLocation, Element.EndLocation);
} }
public bool IsActionAvailable public bool IsActionAvailable
@ -33,13 +34,17 @@ namespace SharpRefactoring.ContextActions
public CodeGenerator CodeGenerator { get; private set; } public CodeGenerator CodeGenerator { get; private set; }
/// <summary>
/// Either AssignmentExpression or LocalVariableDeclaration.
/// </summary>
public AbstractNode Element { get; private set; }
public DomRegion ElementRegion { get; private set; } public DomRegion ElementRegion { get; private set; }
protected string GetVariableName(EditorContext context) protected string GetVariableName(EditorContext context)
{ {
// a = Foo() : AssignmentExpression.Left == IdentifierExpression(*identifier*) // a = Foo() : AssignmentExpression.Left == IdentifierExpression(*identifier*)
// var a = Foo() : VariableDeclaration(*name*).Initializer != empty // var a = Foo() : VariableDeclaration(*name*).Initializer != empty
var variableName = GetVariableNameFromAssignment(context.GetContainingElement<AssignmentExpression>()); var variableName = GetVariableNameFromAssignment(context.GetContainingElement<AssignmentExpression>());
if (variableName != null) if (variableName != null)
return variableName; return variableName;
@ -50,19 +55,18 @@ namespace SharpRefactoring.ContextActions
return null; return null;
} }
protected DomRegion GetStatementRegion(EditorContext context) protected AbstractNode GetElement(EditorContext context)
{ {
// a = Foo() : AssignmentExpression.Left == IdentifierExpression(*identifier*) // a = Foo() : AssignmentExpression.Left == IdentifierExpression(*identifier*)
// var a = Foo() : VariableDeclaration(*name*).Initializer != empty // var a = Foo() : VariableDeclaration(*name*).Initializer != empty
var assignment = context.GetContainingElement<AssignmentExpression>(); var assignment = context.GetContainingElement<AssignmentExpression>();
if (assignment != null) if (assignment != null)
return DomRegion.FromLocation(assignment.StartLocation, assignment.EndLocation); return assignment;
var declaration = context.GetContainingElement<LocalVariableDeclaration>(); var declaration = context.GetContainingElement<LocalVariableDeclaration>();
if (declaration != null) if (declaration != null)
return DomRegion.FromLocation(declaration.StartLocation, declaration.EndLocation); return declaration;
return DomRegion.Empty; return null;
} }
string GetVariableNameFromAssignment(AssignmentExpression assignment) string GetVariableNameFromAssignment(AssignmentExpression assignment)

11
src/AddIns/Misc/SharpRefactoring/Project/Src/ContextActions/CheckAssignmentNotNull.cs

@ -32,12 +32,12 @@ namespace SharpRefactoring.ContextActions
var ifStatement = GenerateAstToInsert(cache.VariableName); var ifStatement = GenerateAstToInsert(cache.VariableName);
var editor = context.Editor; var editor = context.Editor;
string indent = DocumentUtilitites.GetWhitespaceAfter(editor.Document, editor.Document.GetLineStartOffset(cache.ElementRegion.GetStart())); var doc = editor.Document;
string code = cache.CodeGenerator.GenerateCode(ifStatement, indent);
int insertOffset = editor.Document.GetLineEndOffset(cache.ElementRegion.GetEnd());
using (var undoGroup = editor.Document.OpenUndoGroup()) { using (var undoGroup = editor.Document.OpenUndoGroup()) {
editor.Document.Insert(insertOffset, code); editor.InsertCodeAfter(cache.Element, ifStatement);
var caretPos = editor.Document.Text.IndexOf(caretMarker, insertOffset); // set caret to the position of the special marker
var caretPos = doc.Text.IndexOf(caretMarker, doc.GetLineEndOffset(cache.Element.EndLocation));
editor.Caret.Offset = caretPos; editor.Caret.Offset = caretPos;
editor.Document.RemoveRestOfLine(caretPos); editor.Document.RemoveRestOfLine(caretPos);
} }
@ -46,6 +46,7 @@ namespace SharpRefactoring.ContextActions
AbstractNode GenerateAstToInsert(string variableName) AbstractNode GenerateAstToInsert(string variableName)
{ {
var block = new BlockStatement(); var block = new BlockStatement();
// mark the place where to put caret
block.AddChild(new ExpressionStatement(new IdentifierExpression(caretMarker))); block.AddChild(new ExpressionStatement(new IdentifierExpression(caretMarker)));
return new IfElseStatement( return new IfElseStatement(
new BinaryOperatorExpression(new IdentifierExpression(variableName), BinaryOperatorType.InEquality, new PrimitiveExpression(null)), new BinaryOperatorExpression(new IdentifierExpression(variableName), BinaryOperatorType.InEquality, new PrimitiveExpression(null)),

8
src/AddIns/Misc/SharpRefactoring/Project/Src/ContextActions/CheckAssignmentNull.cs

@ -29,13 +29,7 @@ namespace SharpRefactoring.ContextActions
var cache = context.GetCached<CheckAssignmentCache>(); var cache = context.GetCached<CheckAssignmentCache>();
var ifStatement = GenerateAstToInsert(cache.VariableName); var ifStatement = GenerateAstToInsert(cache.VariableName);
context.Editor.InsertCodeAfter(cache.Element, ifStatement, true);
var editor = context.Editor;
string indent = DocumentUtilitites.GetWhitespaceAfter(editor.Document, editor.Document.GetLineStartOffset(cache.ElementRegion.GetStart()));
string code = cache.CodeGenerator.GenerateCode(ifStatement, indent);
int insertOffset = editor.Document.GetLineEndOffset(cache.ElementRegion.GetEnd());
editor.Document.Insert(insertOffset, code);
editor.Caret.Offset = insertOffset + code.Length - 1;
} }
AbstractNode GenerateAstToInsert(string variableName) AbstractNode GenerateAstToInsert(string variableName)

133
src/AddIns/Misc/SharpRefactoring/Project/Src/ContextActions/CheckMemberNotNull.cs

@ -0,0 +1,133 @@
// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
using System;
using System.Collections.Generic;
using ICSharpCode.NRefactory.Ast;
using ICSharpCode.SharpDevelop.Dom;
using ICSharpCode.SharpDevelop.Editor;
using ICSharpCode.SharpDevelop.Refactoring;
namespace SharpRefactoring.ContextActions
{
/// <summary>
/// For expressions like "a.b.c" adds checks for null "if (a != null && a.b != null)".
/// </summary>
public class CheckMemberNotNull : ContextAction
{
public override string Title {
get { return "Add null checks"; }
}
/// <summary>
/// Expression at caret.
/// </summary>
Expression currentExpr;
/// <summary>
/// Expression to be checked for null.
/// </summary>
Expression targetExpr;
public override bool IsAvailable(EditorContext context)
{
this.currentExpr = context.GetContainingElement<Expression>();
if (currentExpr is InvocationExpression) {
// InvocationExpression (e.g. "e.Foo()") has MemberReferenceExpression as TargetObject (e.g. "e.Foo")
// "e.Foo() -> e"
this.targetExpr = GetTarget(((InvocationExpression)currentExpr).TargetObject);
} else {
// "a.b" -> "a"
this.targetExpr = GetTarget(currentExpr);
}
return
this.targetExpr is MemberReferenceExpression ||
this.targetExpr is CastExpression ||
this.targetExpr is ParenthesizedExpression;
//this.targetExpr is IdentifierExpression; // "don't offer the action for just a.b, only for a.b.c"
}
/// <summary>
/// Gets "a.b" from "a.b.c"
/// </summary>
Expression GetTarget(Expression memberReferenceExpr)
{
if (memberReferenceExpr is MemberReferenceExpression) {
return ((MemberReferenceExpression)memberReferenceExpr).TargetObject;
}
return null;
}
public override void Execute(EditorContext context)
{
var conditionExpr = BuildCondition(this.targetExpr);
if (conditionExpr == null)
return;
var ifExpr = new IfElseStatement(conditionExpr, new BlockStatement());
context.Editor.InsertCodeBefore(this.currentExpr, ifExpr);
}
/// <summary>
/// Turns "(a.b as T).d.e" into "(a != null) && (a.b is T) && ((a.b as T).d != null)"
/// </summary>
Expression BuildCondition(Expression targetExpr)
{
var parts = GetConditionParts(targetExpr);
Expression condition = null;
foreach (var part in parts) {
if (condition == null) {
// first
condition = new ParenthesizedExpression(part);
} else {
condition = new BinaryOperatorExpression(new ParenthesizedExpression(part), BinaryOperatorType.LogicalAnd, condition);
}
}
return condition;
}
List<Expression> GetConditionParts(Expression targetExpr)
{
var result = new List<Expression>();
Expression current = targetExpr;
while (current != null)
{
// process current expr
if (current is MemberReferenceExpression) {
var memberExpr = (MemberReferenceExpression)current;
// expr != null
result.Add(new BinaryOperatorExpression(memberExpr, BinaryOperatorType.InEquality, new PrimitiveExpression(null)));
} else if (current is CastExpression) {
var castExpr = (CastExpression)current;
// expr is T
result.Add(new TypeOfIsExpression(castExpr.Expression, castExpr.CastTo));
// no need to check (a != null) && (a is T): (a is T) is enough - skip the "a"
current = StepIn(current);
}
else if (current is IdentifierExpression) {
result.Add(new BinaryOperatorExpression((IdentifierExpression)current, BinaryOperatorType.InEquality, new PrimitiveExpression(null)));
}
// step down the AST (e.g. from "a as T" to "a")
current = StepIn(current);
}
return result;
}
Expression StepIn(Expression expr)
{
if (expr is MemberReferenceExpression) {
return ((MemberReferenceExpression)expr).TargetObject;
} else if (expr is CastExpression) {
return ((CastExpression)expr).Expression;
}
else if (expr is ParenthesizedExpression) {
return ((ParenthesizedExpression)expr).Expression;
}
else if (expr is IdentifierExpression) {
return null;
} else {
return null;
}
}
}
}

52
src/AddIns/Misc/SharpRefactoring/Project/Src/ContextActions/Extensions.cs

@ -4,9 +4,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using ICSharpCode.NRefactory; using ICSharpCode.NRefactory;
using ICSharpCode.NRefactory.Ast; using ICSharpCode.NRefactory.Ast;
using ICSharpCode.NRefactory.Visitors;
using ICSharpCode.SharpDevelop; using ICSharpCode.SharpDevelop;
using ICSharpCode.SharpDevelop.Dom; using ICSharpCode.SharpDevelop.Dom;
using ICSharpCode.SharpDevelop.Dom.Refactoring; using ICSharpCode.SharpDevelop.Dom.Refactoring;
@ -77,6 +77,15 @@ namespace SharpRefactoring.ContextActions
return line.Offset + line.TotalLength; return line.Offset + line.TotalLength;
} }
public static int GetPreviousLineEndOffset(this IDocument document, Location location)
{
var line = document.GetLineForOffset(document.PositionToOffset(location));
if (line.LineNumber == 1)
return -1;
var previousLine = document.GetLine(line.LineNumber - 1);
return previousLine.Offset + previousLine.TotalLength;
}
public static void RemoveRestOfLine(this IDocument document, int offset) public static void RemoveRestOfLine(this IDocument document, int offset)
{ {
var line = document.GetLineForOffset(offset); var line = document.GetLineForOffset(offset);
@ -84,6 +93,47 @@ namespace SharpRefactoring.ContextActions
document.Remove(offset, lineEndOffset - offset); document.Remove(offset, lineEndOffset - offset);
} }
public static DomRegion DomRegion(this INode node)
{
return new DomRegion(node.StartLocation.Line, node.StartLocation.Column, node.EndLocation.Line, node.EndLocation.Column);
}
/// <summary>
/// Inserts code at the next line after target AST node.
/// </summary>
public static void InsertCodeAfter(this ITextEditor editor, AbstractNode target, AbstractNode insert, bool updateCaretPos = false)
{
InsertCode(editor, target, insert, editor.Document.GetLineEndOffset(target.EndLocation), updateCaretPos);
}
/// <summary>
/// Inserts code at the line before target AST node.
/// </summary>
public static void InsertCodeBefore(this ITextEditor editor, AbstractNode target, AbstractNode insert)
{
InsertCode(editor, target, insert, editor.Document.GetPreviousLineEndOffset(target.StartLocation), false);
}
public static void InsertCode(this ITextEditor editor, AbstractNode target, AbstractNode insert, int insertOffset, bool updateCaretPos)
{
if (insertOffset < 0)
return;
var regionCorrectVisitor = new SetRegionInclusionVisitor();
insert.AcceptVisitor(regionCorrectVisitor, null);
var doc = editor.Document;
var codeGen = editor.Language.Properties.CodeGenerator;
string indent = DocumentUtilitites.GetWhitespaceAfter(doc, doc.GetLineStartOffset(target.StartLocation));
string code = codeGen.GenerateCode(insert, indent);
doc.Insert(insertOffset, code);
if (updateCaretPos) {
editor.Caret.Offset = insertOffset + code.Length - 1;
}
}
/// <summary> /// <summary>
/// C# only. /// C# only.
/// </summary> /// </summary>

9
src/Libraries/NRefactory/Project/Src/Visitors/SetRegionInclusionVisitor.cs

@ -17,6 +17,11 @@ namespace ICSharpCode.NRefactory.Visitors
{ {
Stack<INode> parentNodes = new Stack<INode>(); Stack<INode> parentNodes = new Stack<INode>();
/// <summary>
/// Sets StartLocation and EndLocation (region) of every node to union of regions of its children.
/// Parsers don't do this by default:
/// e.g. "a.Foo()" is InvocationExpression, its region includes only the "()" and it has a child MemberReferenceExpression, with region ".Foo".
/// </summary>
public SetRegionInclusionVisitor() public SetRegionInclusionVisitor()
{ {
parentNodes.Push(null); parentNodes.Push(null);
@ -34,7 +39,7 @@ namespace ICSharpCode.NRefactory.Visitors
if (node is PropertyDeclaration) { if (node is PropertyDeclaration) {
// PropertyDeclaration has correctly set BodyStart and BodyEnd by the parser, // PropertyDeclaration has correctly set BodyStart and BodyEnd by the parser,
// but it has no subnode "body", just 2 children GetRegion and SetRegion which don't span // but it has no subnode "body", just 2 children GetRegion and SetRegion which don't span
// the whole (BodyStart, BodyEnd) region => We have to handle PropertyDeclaration as a special case. // the whole (BodyStart, BodyEnd) region => we have to handle PropertyDeclaration as a special case.
node.EndLocation = ((PropertyDeclaration)node).BodyEnd; node.EndLocation = ((PropertyDeclaration)node).BodyEnd;
} }
@ -60,7 +65,7 @@ namespace ICSharpCode.NRefactory.Visitors
parent.EndLocation = node.EndLocation; parent.EndLocation = node.EndLocation;
} }
// Block statement as a special case - we want it without the '{' and '}' // Block statement as a special case - we want block contents without the '{' and '}'
if (node is BlockStatement) { if (node is BlockStatement) {
var firstSatement = node.Children.FirstOrDefault(); var firstSatement = node.Children.FirstOrDefault();
if (firstSatement != null) { if (firstSatement != null) {

Loading…
Cancel
Save