Skip to content

Add an Invoke-Formatter cmdlet to provide code formatting #772

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

Merged
merged 38 commits into from
Jun 9, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
10ddacc
Fix PlaceOpenBrace documentation
May 22, 2017
b71bcd7
Add a skeleteon of a cmdlet to format script
May 23, 2017
f36a956
Apply edits in a sequential manner
May 24, 2017
50f7e2f
Add InvokeFormatterCommand file to csproj
May 24, 2017
2778ba3
Return self instead of new object ater EditableText.ApplyEdit
May 24, 2017
6aab926
Add checks to prevent formatter loop from getting stuck
May 24, 2017
0e93348
Add test to verify formatter implementation
May 24, 2017
71dfcd0
Populate CodeFormatting settings with default values
May 24, 2017
d11b626
Move settings creation to settings file
May 25, 2017
45e7f93
Make InvokeSA command to use common settings logic
May 25, 2017
71a54ff
Add Formatter class to provide code formatting capability
May 27, 2017
d924831
Parameterize format method
Jun 1, 2017
2b92851
Remove unused members from formatter class
Jun 1, 2017
87aac09
Add Formatter.cs file to engine csproj
Jun 1, 2017
f0b5fea
Fix updating settings in invoke-formatter
Jun 1, 2017
fac7632
Move formatting to its own class
Jun 1, 2017
7fa659d
Move helper initialization to format method
Jun 1, 2017
3057c03
Create a method to get current settings
Jun 1, 2017
73aefd8
Move the fixing in formatting to ScriptAnalyzer class
Jun 6, 2017
4ae32a3
Fix correction extent sorting logic
Jun 7, 2017
4ae9a90
Add range formatting capability
Jun 7, 2017
dd381e6
Add an out parameter to relay updated range
Jun 7, 2017
db12b5e
Add range parameter to InvokeFormatter cmdlet
Jun 7, 2017
82a277f
Update Formatter.GetCurrentSettings implementation
Jun 7, 2017
32dde30
Remove unused usings from formatter.cs
Jun 7, 2017
a4eb6f3
Remove unused usings from invokeformattercommand.cs
Jun 8, 2017
585fbdf
Enable InvokeFormatter range only in debug build
Jun 8, 2017
30cd2b5
Add external help markdown for invoke-formatter cmdlet
Jun 8, 2017
d6d3eef
Remove online help link from markdown help and fix typo
Jun 8, 2017
06a5de2
Add argument completer for invoke-formatter cmdlet
Jun 8, 2017
2f8b6da
Add xml documentation
Jun 8, 2017
88da14b
Update verbose messages for invoke-formatter
Jun 8, 2017
768e734
Make netstandard1.6 pdb portable type
Jun 9, 2017
7d00f7b
Add argument checks for Formatter.Format method
Jun 9, 2017
e0bf961
Update nuget.config
Jun 9, 2017
b0fa911
Update s.m.a version in project.json files
Jun 9, 2017
3b41088
Add xml documentation
Jun 9, 2017
2bd30ae
Fix invoke-formatter markdown help file
Jun 9, 2017
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
130 changes: 130 additions & 0 deletions Engine/Commands/InvokeFormatterCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//
// Copyright (c) Microsoft Corporation.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

using System;
using System.Globalization;
using System.Management.Automation;

namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands
{
using PSSASettings = Microsoft.Windows.PowerShell.ScriptAnalyzer.Settings;

/// <summary>
/// A cmdlet to format a PowerShell script text.
/// </summary>
[Cmdlet(VerbsLifecycle.Invoke, "Formatter")]
public class InvokeFormatterCommand : PSCmdlet, IOutputWriter
{
private const string defaultSettingsPreset = "CodeFormatting";
private Settings defaultSettings;
private Settings inputSettings;

/// <summary>
/// The script text to be formated.
///
/// *NOTE*: Unlike ScriptBlock parameter, the ScriptDefinition parameter require a string value.
/// </summary>
[ParameterAttribute(Mandatory = true)]
[ValidateNotNull]
public string ScriptDefinition { get; set; }

/// <summary>
/// A settings hashtable or a path to a PowerShell data file (.psd1) file that contains the settings.
/// </summary>
[Parameter(Mandatory = false)]
[ValidateNotNull]
public object Settings { get; set; }

#if DEBUG
[Parameter(Mandatory = false)]
public Range Range { get; set; }

[Parameter(Mandatory = false, ParameterSetName = "NoRange")]
public int StartLineNumber { get; set; } = -1;
[Parameter(Mandatory = false, ParameterSetName = "NoRange")]
public int StartColumnNumber { get; set; } = -1;
[Parameter(Mandatory = false, ParameterSetName = "NoRange")]
public int EndLineNumber { get; set; } = -1;
[Parameter(Mandatory = false, ParameterSetName = "NoRange")]
public int EndColumnNumber { get; set; } = -1;

/// <summary>
/// Attaches to an instance of a .Net debugger
/// </summary>
[Parameter(Mandatory = false)]
public SwitchParameter AttachAndDebug
{
get { return attachAndDebug; }
set { attachAndDebug = value; }
}

private bool attachAndDebug = false;
#endif

protected override void BeginProcessing()
{
#if DEBUG
if (attachAndDebug)
{
if (System.Diagnostics.Debugger.IsAttached)
{
System.Diagnostics.Debugger.Break();
}
else
{
System.Diagnostics.Debugger.Launch();
}
}
#endif

try
{
inputSettings = PSSASettings.Create(Settings, null, this);
if (inputSettings == null)
{
inputSettings = new PSSASettings(
defaultSettingsPreset,
PSSASettings.GetSettingPresetFilePath);
}
}
catch
{
this.WriteWarning(String.Format(CultureInfo.CurrentCulture, Strings.SettingsNotParsable));
return;
}
}

protected override void ProcessRecord()
{
// todo add tests to check range formatting
string formattedScriptDefinition;
#if DEBUG
var range = Range;
if (this.ParameterSetName.Equals("NoRange"))
{
range = new Range(StartLineNumber, StartColumnNumber, EndLineNumber, EndColumnNumber);
}

formattedScriptDefinition = Formatter.Format(ScriptDefinition, inputSettings, range, this);
#endif // DEBUG

formattedScriptDefinition = Formatter.Format(ScriptDefinition, inputSettings, null, this);
this.WriteObject(formattedScriptDefinition);
}

private void ValidateInputSettings()
{
// todo implement this
return;
}
}
}
73 changes: 18 additions & 55 deletions Engine/Commands/InvokeScriptAnalyzerCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@

namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands
{
using PSSASettings = Microsoft.Windows.PowerShell.ScriptAnalyzer.Settings;

/// <summary>
/// InvokeScriptAnalyzerCommand: Cmdlet to statically check PowerShell scripts.
/// </summary>
[Cmdlet(VerbsLifecycle.Invoke,
"ScriptAnalyzer",
DefaultParameterSetName="File",
DefaultParameterSetName = "File",
HelpUri = "http://go.microsoft.com/fwlink/?LinkId=525914")]
public class InvokeScriptAnalyzerCommand : PSCmdlet, IOutputWriter
{
Expand Down Expand Up @@ -208,6 +210,9 @@ public SwitchParameter SaveDscDependency
#endif // !PSV3

#if DEBUG
/// <summary>
/// Attaches to an instance of a .Net debugger
/// </summary>
[Parameter(Mandatory = false)]
public SwitchParameter AttachAndDebug
{
Expand Down Expand Up @@ -260,64 +265,22 @@ protected override void BeginProcessing()
ProcessPath();
}

object settingsFound;
var settingsMode = PowerShell.ScriptAnalyzer.Settings.FindSettingsMode(
this.settings,
processedPaths == null || processedPaths.Count == 0 ? null : processedPaths[0],
out settingsFound);

switch (settingsMode)
{
case SettingsMode.Auto:
this.WriteVerbose(
String.Format(
CultureInfo.CurrentCulture,
Strings.SettingsNotProvided,
path));
this.WriteVerbose(
String.Format(
CultureInfo.CurrentCulture,
Strings.SettingsAutoDiscovered,
(string)settingsFound));
break;

case SettingsMode.Preset:
case SettingsMode.File:
this.WriteVerbose(
String.Format(
CultureInfo.CurrentCulture,
Strings.SettingsUsingFile,
(string)settingsFound));
break;

case SettingsMode.Hashtable:
this.WriteVerbose(
String.Format(
CultureInfo.CurrentCulture,
Strings.SettingsUsingHashtable));
break;

default: // case SettingsMode.None
this.WriteVerbose(
String.Format(
CultureInfo.CurrentCulture,
Strings.SettingsCannotFindFile));
break;
}

if (settingsMode != SettingsMode.None)
try
{
try
var settingsObj = PSSASettings.Create(
settings,
processedPaths == null || processedPaths.Count == 0 ? null : processedPaths[0],
this);
if (settingsObj != null)
{
var settingsObj = new Settings(settingsFound);
ScriptAnalyzer.Instance.UpdateSettings(settingsObj);
}
catch
{
this.WriteWarning(String.Format(CultureInfo.CurrentCulture, Strings.SettingsNotParsable));
stopProcessing = true;
return;
}
}
catch
{
this.WriteWarning(String.Format(CultureInfo.CurrentCulture, Strings.SettingsNotParsable));
stopProcessing = true;
return;
}

ScriptAnalyzer.Instance.Initialize(
Expand Down
3 changes: 2 additions & 1 deletion Engine/EditableText.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ public EditableText ApplyEdit(TextEdit textEdit)
currentLineNumber++;
}

return new EditableText(String.Join(NewLine, lines));
// returning self allows us to chain ApplyEdit calls.
return this;
}

// TODO Add a method that takes multiple edits, checks if they are unique and applies them.
Expand Down
80 changes: 80 additions & 0 deletions Engine/Formatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System;
using System.Collections;
using System.Management.Automation;

namespace Microsoft.Windows.PowerShell.ScriptAnalyzer
{
/// <summary>
/// A class to provide code formatting capability.
/// </summary>
public class Formatter
{
/// <summary>
/// Format a powershell script.
/// </summary>
/// <param name="scriptDefinition">A string representing a powershell script.</param>
/// <param name="settings">Settings to be used for formatting</param>
/// <param name="range">The range in which formatting should take place.</param>
/// <param name="cmdlet">The cmdlet object that calls this method.</param>
/// <returns></returns>
public static string Format<TCmdlet>(
string scriptDefinition,
Settings settings,
Range range,
TCmdlet cmdlet) where TCmdlet : PSCmdlet, IOutputWriter
{
// todo implement notnull attribute for such a check
ValidateNotNull(scriptDefinition, "scriptDefinition");
ValidateNotNull(settings, "settings");
ValidateNotNull(cmdlet, "cmdlet");

Helper.Instance = new Helper(cmdlet.SessionState.InvokeCommand, cmdlet);
Helper.Instance.Initialize();

var ruleOrder = new string[]
{
"PSPlaceCloseBrace",
"PSPlaceOpenBrace",
"PSUseConsistentWhitespace",
"PSUseConsistentIndentation",
"PSAlignAssignmentStatement"
};

var text = new EditableText(scriptDefinition);
foreach (var rule in ruleOrder)
{
if (!settings.RuleArguments.ContainsKey(rule))
{
continue;
}

var currentSettings = GetCurrentSettings(settings, rule);
ScriptAnalyzer.Instance.UpdateSettings(currentSettings);
ScriptAnalyzer.Instance.Initialize(cmdlet, null, null, null, null, true, false);

Range updatedRange;
text = ScriptAnalyzer.Instance.Fix(text, range, out updatedRange);
range = updatedRange;
}

return text.ToString();
}

private static void ValidateNotNull<T>(T obj, string name)
{
if (obj == null)
{
throw new ArgumentNullException(name);
}
}

private static Settings GetCurrentSettings(Settings settings, string rule)
{
return new Settings(new Hashtable()
{
{"IncludeRules", new string[] {rule}},
{"Rules", new Hashtable() { { rule, new Hashtable(settings.RuleArguments[rule]) } } }
});
}
}
}
2 changes: 1 addition & 1 deletion Engine/PSScriptAnalyzer.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ FormatsToProcess = @('ScriptAnalyzer.format.ps1xml')
FunctionsToExport = @()

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

# Variables to export from this module
VariablesToExport = @()
Expand Down
20 changes: 12 additions & 8 deletions Engine/PSScriptAnalyzer.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ $binaryModuleRoot = $PSModuleRoot
if (($PSVersionTable.Keys -contains "PSEdition") -and ($PSVersionTable.PSEdition -ne 'Desktop')) {
$binaryModuleRoot = Join-Path -Path $PSModuleRoot -ChildPath 'coreclr'
}
else
{
else {
if ($PSVersionTable.PSVersion -lt [Version]'5.0') {
$binaryModuleRoot = Join-Path -Path $PSModuleRoot -ChildPath 'PSv3'
}
Expand All @@ -29,18 +28,23 @@ $PSModule.OnRemove = {
Remove-Module -ModuleInfo $binaryModule
}

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

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

Function RuleNameCompleter
{
@('Invoke-ScriptAnalyzer', 'Invoke-Formatter') | ForEach-Object {
Register-ArgumentCompleter -CommandName $_ `
-ParameterName 'Settings' `
-ScriptBlock $settingPresetCompleter

}

Function RuleNameCompleter {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParmeter)

Get-ScriptAnalyzerRule *$wordToComplete* | `
Expand All @@ -50,4 +54,4 @@ if (Get-Command Register-ArgumentCompleter -ErrorAction Ignore)
Register-ArgumentCompleter -CommandName 'Invoke-ScriptAnalyzer' -ParameterName 'IncludeRule' -ScriptBlock $Function:RuleNameCompleter
Register-ArgumentCompleter -CommandName 'Invoke-ScriptAnalyzer' -ParameterName 'ExcludeRule' -ScriptBlock $Function:RuleNameCompleter
Register-ArgumentCompleter -CommandName 'Get-ScriptAnalyzerRule' -ParameterName 'Name' -ScriptBlock $Function:RuleNameCompleter
}
}
Loading