Skip to content

Commit 4f47ef9

Browse files
author
Kapil Borle
authored
Merge pull request #772 from PowerShell/kapilmb/add-formatter
Add an Invoke-Formatter cmdlet to provide code formatting
2 parents 81fafcc + 2bd30ae commit 4f47ef9

18 files changed

+602
-98
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
//
2+
// Copyright (c) Microsoft Corporation.
3+
//
4+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
5+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
6+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
7+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
8+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
9+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
10+
// THE SOFTWARE.
11+
//
12+
13+
using System;
14+
using System.Globalization;
15+
using System.Management.Automation;
16+
17+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands
18+
{
19+
using PSSASettings = Microsoft.Windows.PowerShell.ScriptAnalyzer.Settings;
20+
21+
/// <summary>
22+
/// A cmdlet to format a PowerShell script text.
23+
/// </summary>
24+
[Cmdlet(VerbsLifecycle.Invoke, "Formatter")]
25+
public class InvokeFormatterCommand : PSCmdlet, IOutputWriter
26+
{
27+
private const string defaultSettingsPreset = "CodeFormatting";
28+
private Settings defaultSettings;
29+
private Settings inputSettings;
30+
31+
/// <summary>
32+
/// The script text to be formated.
33+
///
34+
/// *NOTE*: Unlike ScriptBlock parameter, the ScriptDefinition parameter require a string value.
35+
/// </summary>
36+
[ParameterAttribute(Mandatory = true)]
37+
[ValidateNotNull]
38+
public string ScriptDefinition { get; set; }
39+
40+
/// <summary>
41+
/// A settings hashtable or a path to a PowerShell data file (.psd1) file that contains the settings.
42+
/// </summary>
43+
[Parameter(Mandatory = false)]
44+
[ValidateNotNull]
45+
public object Settings { get; set; }
46+
47+
#if DEBUG
48+
[Parameter(Mandatory = false)]
49+
public Range Range { get; set; }
50+
51+
[Parameter(Mandatory = false, ParameterSetName = "NoRange")]
52+
public int StartLineNumber { get; set; } = -1;
53+
[Parameter(Mandatory = false, ParameterSetName = "NoRange")]
54+
public int StartColumnNumber { get; set; } = -1;
55+
[Parameter(Mandatory = false, ParameterSetName = "NoRange")]
56+
public int EndLineNumber { get; set; } = -1;
57+
[Parameter(Mandatory = false, ParameterSetName = "NoRange")]
58+
public int EndColumnNumber { get; set; } = -1;
59+
60+
/// <summary>
61+
/// Attaches to an instance of a .Net debugger
62+
/// </summary>
63+
[Parameter(Mandatory = false)]
64+
public SwitchParameter AttachAndDebug
65+
{
66+
get { return attachAndDebug; }
67+
set { attachAndDebug = value; }
68+
}
69+
70+
private bool attachAndDebug = false;
71+
#endif
72+
73+
protected override void BeginProcessing()
74+
{
75+
#if DEBUG
76+
if (attachAndDebug)
77+
{
78+
if (System.Diagnostics.Debugger.IsAttached)
79+
{
80+
System.Diagnostics.Debugger.Break();
81+
}
82+
else
83+
{
84+
System.Diagnostics.Debugger.Launch();
85+
}
86+
}
87+
#endif
88+
89+
try
90+
{
91+
inputSettings = PSSASettings.Create(Settings, null, this);
92+
if (inputSettings == null)
93+
{
94+
inputSettings = new PSSASettings(
95+
defaultSettingsPreset,
96+
PSSASettings.GetSettingPresetFilePath);
97+
}
98+
}
99+
catch
100+
{
101+
this.WriteWarning(String.Format(CultureInfo.CurrentCulture, Strings.SettingsNotParsable));
102+
return;
103+
}
104+
}
105+
106+
protected override void ProcessRecord()
107+
{
108+
// todo add tests to check range formatting
109+
string formattedScriptDefinition;
110+
#if DEBUG
111+
var range = Range;
112+
if (this.ParameterSetName.Equals("NoRange"))
113+
{
114+
range = new Range(StartLineNumber, StartColumnNumber, EndLineNumber, EndColumnNumber);
115+
}
116+
117+
formattedScriptDefinition = Formatter.Format(ScriptDefinition, inputSettings, range, this);
118+
#endif // DEBUG
119+
120+
formattedScriptDefinition = Formatter.Format(ScriptDefinition, inputSettings, null, this);
121+
this.WriteObject(formattedScriptDefinition);
122+
}
123+
124+
private void ValidateInputSettings()
125+
{
126+
// todo implement this
127+
return;
128+
}
129+
}
130+
}

Engine/Commands/InvokeScriptAnalyzerCommand.cs

Lines changed: 18 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@
3030

3131
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands
3232
{
33+
using PSSASettings = Microsoft.Windows.PowerShell.ScriptAnalyzer.Settings;
34+
3335
/// <summary>
3436
/// InvokeScriptAnalyzerCommand: Cmdlet to statically check PowerShell scripts.
3537
/// </summary>
3638
[Cmdlet(VerbsLifecycle.Invoke,
3739
"ScriptAnalyzer",
38-
DefaultParameterSetName="File",
40+
DefaultParameterSetName = "File",
3941
HelpUri = "http://go.microsoft.com/fwlink/?LinkId=525914")]
4042
public class InvokeScriptAnalyzerCommand : PSCmdlet, IOutputWriter
4143
{
@@ -208,6 +210,9 @@ public SwitchParameter SaveDscDependency
208210
#endif // !PSV3
209211

210212
#if DEBUG
213+
/// <summary>
214+
/// Attaches to an instance of a .Net debugger
215+
/// </summary>
211216
[Parameter(Mandatory = false)]
212217
public SwitchParameter AttachAndDebug
213218
{
@@ -260,64 +265,22 @@ protected override void BeginProcessing()
260265
ProcessPath();
261266
}
262267

263-
object settingsFound;
264-
var settingsMode = PowerShell.ScriptAnalyzer.Settings.FindSettingsMode(
265-
this.settings,
266-
processedPaths == null || processedPaths.Count == 0 ? null : processedPaths[0],
267-
out settingsFound);
268-
269-
switch (settingsMode)
270-
{
271-
case SettingsMode.Auto:
272-
this.WriteVerbose(
273-
String.Format(
274-
CultureInfo.CurrentCulture,
275-
Strings.SettingsNotProvided,
276-
path));
277-
this.WriteVerbose(
278-
String.Format(
279-
CultureInfo.CurrentCulture,
280-
Strings.SettingsAutoDiscovered,
281-
(string)settingsFound));
282-
break;
283-
284-
case SettingsMode.Preset:
285-
case SettingsMode.File:
286-
this.WriteVerbose(
287-
String.Format(
288-
CultureInfo.CurrentCulture,
289-
Strings.SettingsUsingFile,
290-
(string)settingsFound));
291-
break;
292-
293-
case SettingsMode.Hashtable:
294-
this.WriteVerbose(
295-
String.Format(
296-
CultureInfo.CurrentCulture,
297-
Strings.SettingsUsingHashtable));
298-
break;
299-
300-
default: // case SettingsMode.None
301-
this.WriteVerbose(
302-
String.Format(
303-
CultureInfo.CurrentCulture,
304-
Strings.SettingsCannotFindFile));
305-
break;
306-
}
307-
308-
if (settingsMode != SettingsMode.None)
268+
try
309269
{
310-
try
270+
var settingsObj = PSSASettings.Create(
271+
settings,
272+
processedPaths == null || processedPaths.Count == 0 ? null : processedPaths[0],
273+
this);
274+
if (settingsObj != null)
311275
{
312-
var settingsObj = new Settings(settingsFound);
313276
ScriptAnalyzer.Instance.UpdateSettings(settingsObj);
314277
}
315-
catch
316-
{
317-
this.WriteWarning(String.Format(CultureInfo.CurrentCulture, Strings.SettingsNotParsable));
318-
stopProcessing = true;
319-
return;
320-
}
278+
}
279+
catch
280+
{
281+
this.WriteWarning(String.Format(CultureInfo.CurrentCulture, Strings.SettingsNotParsable));
282+
stopProcessing = true;
283+
return;
321284
}
322285

323286
ScriptAnalyzer.Instance.Initialize(

Engine/EditableText.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ public EditableText ApplyEdit(TextEdit textEdit)
100100
currentLineNumber++;
101101
}
102102

103-
return new EditableText(String.Join(NewLine, lines));
103+
// returning self allows us to chain ApplyEdit calls.
104+
return this;
104105
}
105106

106107
// TODO Add a method that takes multiple edits, checks if they are unique and applies them.

Engine/Formatter.cs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System;
2+
using System.Collections;
3+
using System.Management.Automation;
4+
5+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer
6+
{
7+
/// <summary>
8+
/// A class to provide code formatting capability.
9+
/// </summary>
10+
public class Formatter
11+
{
12+
/// <summary>
13+
/// Format a powershell script.
14+
/// </summary>
15+
/// <param name="scriptDefinition">A string representing a powershell script.</param>
16+
/// <param name="settings">Settings to be used for formatting</param>
17+
/// <param name="range">The range in which formatting should take place.</param>
18+
/// <param name="cmdlet">The cmdlet object that calls this method.</param>
19+
/// <returns></returns>
20+
public static string Format<TCmdlet>(
21+
string scriptDefinition,
22+
Settings settings,
23+
Range range,
24+
TCmdlet cmdlet) where TCmdlet : PSCmdlet, IOutputWriter
25+
{
26+
// todo implement notnull attribute for such a check
27+
ValidateNotNull(scriptDefinition, "scriptDefinition");
28+
ValidateNotNull(settings, "settings");
29+
ValidateNotNull(cmdlet, "cmdlet");
30+
31+
Helper.Instance = new Helper(cmdlet.SessionState.InvokeCommand, cmdlet);
32+
Helper.Instance.Initialize();
33+
34+
var ruleOrder = new string[]
35+
{
36+
"PSPlaceCloseBrace",
37+
"PSPlaceOpenBrace",
38+
"PSUseConsistentWhitespace",
39+
"PSUseConsistentIndentation",
40+
"PSAlignAssignmentStatement"
41+
};
42+
43+
var text = new EditableText(scriptDefinition);
44+
foreach (var rule in ruleOrder)
45+
{
46+
if (!settings.RuleArguments.ContainsKey(rule))
47+
{
48+
continue;
49+
}
50+
51+
var currentSettings = GetCurrentSettings(settings, rule);
52+
ScriptAnalyzer.Instance.UpdateSettings(currentSettings);
53+
ScriptAnalyzer.Instance.Initialize(cmdlet, null, null, null, null, true, false);
54+
55+
Range updatedRange;
56+
text = ScriptAnalyzer.Instance.Fix(text, range, out updatedRange);
57+
range = updatedRange;
58+
}
59+
60+
return text.ToString();
61+
}
62+
63+
private static void ValidateNotNull<T>(T obj, string name)
64+
{
65+
if (obj == null)
66+
{
67+
throw new ArgumentNullException(name);
68+
}
69+
}
70+
71+
private static Settings GetCurrentSettings(Settings settings, string rule)
72+
{
73+
return new Settings(new Hashtable()
74+
{
75+
{"IncludeRules", new string[] {rule}},
76+
{"Rules", new Hashtable() { { rule, new Hashtable(settings.RuleArguments[rule]) } } }
77+
});
78+
}
79+
}
80+
}

Engine/PSScriptAnalyzer.psd1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ FormatsToProcess = @('ScriptAnalyzer.format.ps1xml')
6565
FunctionsToExport = @()
6666

6767
# Cmdlets to export from this module
68-
CmdletsToExport = @('Get-ScriptAnalyzerRule','Invoke-ScriptAnalyzer')
68+
CmdletsToExport = @('Get-ScriptAnalyzerRule', 'Invoke-ScriptAnalyzer', 'Invoke-Formatter')
6969

7070
# Variables to export from this module
7171
VariablesToExport = @()

Engine/PSScriptAnalyzer.psm1

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ $binaryModuleRoot = $PSModuleRoot
1414
if (($PSVersionTable.Keys -contains "PSEdition") -and ($PSVersionTable.PSEdition -ne 'Desktop')) {
1515
$binaryModuleRoot = Join-Path -Path $PSModuleRoot -ChildPath 'coreclr'
1616
}
17-
else
18-
{
17+
else {
1918
if ($PSVersionTable.PSVersion -lt [Version]'5.0') {
2019
$binaryModuleRoot = Join-Path -Path $PSModuleRoot -ChildPath 'PSv3'
2120
}
@@ -29,18 +28,23 @@ $PSModule.OnRemove = {
2928
Remove-Module -ModuleInfo $binaryModule
3029
}
3130

32-
if (Get-Command Register-ArgumentCompleter -ErrorAction Ignore)
33-
{
34-
Register-ArgumentCompleter -CommandName 'Invoke-ScriptAnalyzer' -ParameterName 'Settings' -ScriptBlock {
31+
if (Get-Command Register-ArgumentCompleter -ErrorAction Ignore) {
32+
$settingPresetCompleter = {
3533
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParmeter)
3634

3735
[Microsoft.Windows.PowerShell.ScriptAnalyzer.Settings]::GetSettingPresets() | `
3836
Where-Object {$_ -like "$wordToComplete*"} | `
3937
ForEach-Object { New-Object System.Management.Automation.CompletionResult $_ }
4038
}
4139

42-
Function RuleNameCompleter
43-
{
40+
@('Invoke-ScriptAnalyzer', 'Invoke-Formatter') | ForEach-Object {
41+
Register-ArgumentCompleter -CommandName $_ `
42+
-ParameterName 'Settings' `
43+
-ScriptBlock $settingPresetCompleter
44+
45+
}
46+
47+
Function RuleNameCompleter {
4448
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParmeter)
4549

4650
Get-ScriptAnalyzerRule *$wordToComplete* | `
@@ -50,4 +54,4 @@ if (Get-Command Register-ArgumentCompleter -ErrorAction Ignore)
5054
Register-ArgumentCompleter -CommandName 'Invoke-ScriptAnalyzer' -ParameterName 'IncludeRule' -ScriptBlock $Function:RuleNameCompleter
5155
Register-ArgumentCompleter -CommandName 'Invoke-ScriptAnalyzer' -ParameterName 'ExcludeRule' -ScriptBlock $Function:RuleNameCompleter
5256
Register-ArgumentCompleter -CommandName 'Get-ScriptAnalyzerRule' -ParameterName 'Name' -ScriptBlock $Function:RuleNameCompleter
53-
}
57+
}

0 commit comments

Comments
 (0)