Skip to content

Commit c58dafd

Browse files
author
Kapil Borle
authored
Merge pull request #758 from PowerShell/kapilmb/use-should-process-rule
Add UseSupportsShouldProcess rule
2 parents f45c20b + d6200d7 commit c58dafd

21 files changed

+2369
-113
lines changed

Engine/EditableText.cs

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Globalization;
5+
using System.Linq;
6+
using System.Management.Automation.Language;
7+
using System.Text;
8+
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Extensions;
9+
10+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer
11+
{
12+
/// <summary>
13+
/// A class to represent text to which `TextEdit`s can be applied.
14+
/// </summary>
15+
public class EditableText
16+
{
17+
private TextLines lines { get; set; }
18+
19+
/// <summary>
20+
/// The text that is available for editing.
21+
/// </summary>
22+
public string Text { get { return String.Join(NewLine, lines); } }
23+
24+
/// <summary>
25+
/// The lines in the Text.
26+
/// </summary>
27+
public string[] Lines { get { return lines.ToArray(); } }
28+
29+
/// <summary>
30+
/// The new line character in the Text.
31+
/// </summary>
32+
public string NewLine { get; private set; }
33+
34+
/// <summary>
35+
/// Construct an EditableText type object.
36+
/// </summary>
37+
/// <param name="text"></param>
38+
public EditableText(string text)
39+
{
40+
if (text == null)
41+
{
42+
throw new ArgumentNullException(nameof(text));
43+
}
44+
45+
string[] lines;
46+
NewLine = GetNewLineCharacters(text, out lines);
47+
this.lines = new TextLines(lines);
48+
}
49+
50+
/// <summary>
51+
/// Apply edits defined by a TextEdit object to Text.
52+
/// </summary>
53+
/// <param name="textEdit">A TextEdit object that encapsulates the text and the range that need to be replaced.</param>
54+
/// <returns>An editable object which contains the supplied edit.</returns>
55+
public EditableText ApplyEdit(TextEdit textEdit)
56+
{
57+
ValidateTextEdit(textEdit);
58+
59+
var editLines = textEdit.Lines;
60+
61+
// Get the first fragment of the first line
62+
string firstLineFragment =
63+
lines[textEdit.StartLineNumber - 1]
64+
.Substring(0, textEdit.StartColumnNumber - 1);
65+
66+
// Get the last fragment of the last line
67+
string endLine = lines[textEdit.EndLineNumber - 1];
68+
string lastLineFragment =
69+
endLine.Substring(
70+
textEdit.EndColumnNumber - 1,
71+
lines[textEdit.EndLineNumber - 1].Length - textEdit.EndColumnNumber + 1);
72+
73+
// Remove the old lines
74+
for (int i = 0; i <= textEdit.EndLineNumber - textEdit.StartLineNumber; i++)
75+
{
76+
lines.RemoveAt(textEdit.StartLineNumber - 1);
77+
}
78+
79+
// Build and insert the new lines
80+
int currentLineNumber = textEdit.StartLineNumber;
81+
for (int changeIndex = 0; changeIndex < editLines.Length; changeIndex++)
82+
{
83+
// Since we split the lines above using \n, make sure to
84+
// trim the ending \r's off as well.
85+
string finalLine = editLines[changeIndex].TrimEnd('\r');
86+
87+
// Should we add first or last line fragments?
88+
if (changeIndex == 0)
89+
{
90+
// Append the first line fragment
91+
finalLine = firstLineFragment + finalLine;
92+
}
93+
if (changeIndex == editLines.Length - 1)
94+
{
95+
// Append the last line fragment
96+
finalLine = finalLine + lastLineFragment;
97+
}
98+
99+
lines.Insert(currentLineNumber - 1, finalLine);
100+
currentLineNumber++;
101+
}
102+
103+
return new EditableText(String.Join(NewLine, lines));
104+
}
105+
106+
// TODO Add a method that takes multiple edits, checks if they are unique and applies them.
107+
108+
public override string ToString()
109+
{
110+
return Text;
111+
}
112+
113+
private void ValidateTextEdit(TextEdit textEdit)
114+
{
115+
if (textEdit == null)
116+
{
117+
throw new NullReferenceException(nameof(textEdit));
118+
}
119+
120+
ValidateTextEditExtent(textEdit);
121+
}
122+
123+
private void ValidateTextEditExtent(TextEdit textEdit)
124+
{
125+
if (textEdit.StartLineNumber > Lines.Length
126+
|| textEdit.EndLineNumber > Lines.Length
127+
|| textEdit.StartColumnNumber > Lines[textEdit.StartLineNumber - 1].Length
128+
|| textEdit.EndColumnNumber > Lines[textEdit.EndLineNumber - 1].Length + 1)
129+
{
130+
throw new ArgumentException(String.Format(
131+
CultureInfo.CurrentCulture,
132+
Strings.EditableTextRangeIsNotContained));
133+
}
134+
}
135+
136+
private static string GetNewLineCharacters(string text, out string[] lines)
137+
{
138+
int numNewLineChars = GetNumNewLineCharacters(text, out lines);
139+
if (lines.Length == 1)
140+
{
141+
return Environment.NewLine;
142+
}
143+
144+
return text.Substring(lines[0].Length, numNewLineChars);
145+
}
146+
147+
private static int GetNumNewLineCharacters(string text, out string[] lines)
148+
{
149+
lines = text.GetLines().ToArray();
150+
if (lines.Length == 1)
151+
{
152+
return Environment.NewLine.Length;
153+
}
154+
155+
var charsInLines = lines.Sum(line => line.Length);
156+
var numCharDiff = text.Length - charsInLines;
157+
int remainder = numCharDiff % (lines.Length - 1);
158+
if (remainder != 0)
159+
{
160+
throw new ArgumentException(
161+
String.Format(CultureInfo.CurrentCulture, Strings.EditableTextInvalidLineEnding),
162+
nameof(text));
163+
}
164+
165+
return numCharDiff / (lines.Length - 1);
166+
}
167+
}
168+
}

Engine/Extensions.cs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Management.Automation;
5+
using System.Management.Automation.Language;
6+
7+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Extensions
8+
{
9+
public static class Extensions
10+
{
11+
/// <summary>
12+
/// Return the lines in a text string.
13+
/// </summary>
14+
/// <param name="text">Text string to be split around new lines.</param>
15+
/// <returns></returns>
16+
public static IEnumerable<string> GetLines(this string text)
17+
{
18+
return text.Split('\n').Select(line => line.TrimEnd('\r'));
19+
}
20+
21+
/// <summary>
22+
/// Converts IScriptExtent to Range
23+
/// </summary>
24+
public static Range ToRange(this IScriptExtent extent)
25+
{
26+
return new Range(
27+
extent.StartLineNumber,
28+
extent.StartColumnNumber,
29+
extent.EndLineNumber,
30+
extent.EndColumnNumber);
31+
}
32+
33+
/// <summary>
34+
/// Get the parameter Asts from a function definition Ast.
35+
///
36+
/// If not parameters are found, return null.
37+
/// </summary>
38+
/// <param name="paramBlockAst">If a parameter block is present, set this argument's value to the parameter block.</param>
39+
/// <returns></returns>
40+
public static IEnumerable<ParameterAst> GetParameterAsts(
41+
this FunctionDefinitionAst functionDefinitionAst,
42+
out ParamBlockAst paramBlockAst)
43+
{
44+
paramBlockAst = null;
45+
if (functionDefinitionAst.Parameters != null)
46+
{
47+
return functionDefinitionAst.Parameters;
48+
}
49+
else if (functionDefinitionAst.Body.ParamBlock?.Parameters != null)
50+
{
51+
paramBlockAst = functionDefinitionAst.Body.ParamBlock;
52+
return functionDefinitionAst.Body.ParamBlock.Parameters;
53+
}
54+
55+
return null;
56+
}
57+
58+
/// <summary>
59+
/// Get the CmdletBinding attribute ast
60+
/// </summary>
61+
/// <param name="attributeAsts"></param>
62+
/// <returns>Returns CmdletBinding attribute ast if it exists, otherwise returns null</returns>
63+
public static AttributeAst GetCmdletBindingAttributeAst(this ParamBlockAst paramBlockAst)
64+
{
65+
var attributeAsts = paramBlockAst.Attributes;
66+
if (attributeAsts == null)
67+
{
68+
return null;
69+
}
70+
71+
foreach (var attributeAst in attributeAsts)
72+
{
73+
if (attributeAst != null && attributeAst.IsCmdletBindingAttributeAst())
74+
{
75+
return attributeAst;
76+
}
77+
}
78+
79+
return null;
80+
}
81+
82+
/// <summary>
83+
/// Check if an attribute Ast is of CmdletBindingAttribute type.
84+
/// </summary>
85+
public static bool IsCmdletBindingAttributeAst(this AttributeAst attributeAst)
86+
{
87+
return attributeAst.TypeName.GetReflectionAttributeType() == typeof(CmdletBindingAttribute);
88+
}
89+
90+
/// <summary>
91+
/// Given a CmdletBinding attribute ast, return the SupportsShouldProcess argument Ast.
92+
///
93+
/// If no SupportsShouldProcess argument is found, return null.
94+
/// </summary>
95+
public static NamedAttributeArgumentAst GetSupportsShouldProcessAst(this AttributeAst attributeAst)
96+
{
97+
if (!attributeAst.IsCmdletBindingAttributeAst()
98+
|| attributeAst.NamedArguments == null)
99+
{
100+
return null;
101+
}
102+
103+
foreach (var namedAttrAst in attributeAst.NamedArguments)
104+
{
105+
if (namedAttrAst != null
106+
&& namedAttrAst.ArgumentName.Equals(
107+
"SupportsShouldProcess",
108+
StringComparison.OrdinalIgnoreCase))
109+
{
110+
return namedAttrAst;
111+
}
112+
}
113+
114+
return null;
115+
}
116+
117+
/// <summary>
118+
/// Return the boolean value of a named attribute argument.
119+
/// </summary>
120+
/// <param name="argumentAst">The ast of the argument's value</param>
121+
public static bool GetValue(this NamedAttributeArgumentAst attrAst, out ExpressionAst argumentAst)
122+
{
123+
argumentAst = null;
124+
if (attrAst.ExpressionOmitted)
125+
{
126+
return true;
127+
}
128+
129+
var varExpAst = attrAst.Argument as VariableExpressionAst;
130+
argumentAst = attrAst.Argument;
131+
if (varExpAst == null)
132+
{
133+
var constExpAst = attrAst.Argument as ConstantExpressionAst;
134+
if (constExpAst == null)
135+
{
136+
return false;
137+
}
138+
139+
bool constExpVal;
140+
if (LanguagePrimitives.TryConvertTo(constExpAst.Value, out constExpVal))
141+
{
142+
return constExpVal;
143+
}
144+
}
145+
else
146+
{
147+
return varExpAst.VariablePath.UserPath.Equals(
148+
bool.TrueString,
149+
StringComparison.OrdinalIgnoreCase);
150+
}
151+
152+
return false;
153+
}
154+
}
155+
}

0 commit comments

Comments
 (0)