diff --git a/Engine/EditableText.cs b/Engine/EditableText.cs index 22e072fd0..555dd02de 100644 --- a/Engine/EditableText.cs +++ b/Engine/EditableText.cs @@ -61,7 +61,7 @@ public EditableText ApplyEdit(TextEdit textEdit) { ValidateTextEdit(textEdit); - var editLines = textEdit.Lines; + string[] editLines = textEdit.Lines; // Get the first fragment of the first line string firstLineFragment = diff --git a/Engine/Formatter.cs b/Engine/Formatter.cs index d86127f2f..7ddc4402c 100644 --- a/Engine/Formatter.cs +++ b/Engine/Formatter.cs @@ -3,7 +3,9 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Management.Automation; +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; namespace Microsoft.Windows.PowerShell.ScriptAnalyzer { @@ -12,6 +14,16 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer /// public class Formatter { + private static readonly IEnumerable s_formattingRulesInOrder = new [] + { + "PSPlaceCloseBrace", + "PSPlaceOpenBrace", + "PSUseConsistentWhitespace", + "PSUseConsistentIndentation", + "PSAlignAssignmentStatement", + "PSUseCorrectCasing" + }; + /// /// Format a powershell script. /// @@ -27,45 +39,34 @@ public static string Format( TCmdlet cmdlet) where TCmdlet : PSCmdlet, IOutputWriter { // todo implement notnull attribute for such a check - ValidateNotNull(scriptDefinition, "scriptDefinition"); - ValidateNotNull(settings, "settings"); - ValidateNotNull(cmdlet, "cmdlet"); + ValidateNotNull(scriptDefinition, nameof(scriptDefinition)); + ValidateNotNull(settings, nameof(settings)); + ValidateNotNull(cmdlet, nameof(cmdlet)); Helper.Instance = new Helper(cmdlet.SessionState.InvokeCommand, cmdlet); Helper.Instance.Initialize(); - var ruleOrder = new string[] - { - "PSPlaceCloseBrace", - "PSPlaceOpenBrace", - "PSUseConsistentWhitespace", - "PSUseConsistentIndentation", - "PSAlignAssignmentStatement", - "PSUseCorrectCasing" - }; + Settings currentSettings = GetCurrentSettings(settings); + ScriptAnalyzer.Instance.UpdateSettings(currentSettings); + ScriptAnalyzer.Instance.Initialize(cmdlet, includeDefaultRules: true); - var text = new EditableText(scriptDefinition); - foreach (var rule in ruleOrder) + try { - 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; - bool fixesWereApplied; - text = ScriptAnalyzer.Instance.Fix(text, range, out updatedRange, out fixesWereApplied); - range = updatedRange; + return ScriptAnalyzer.Instance.Fix(scriptDefinition, range); + } + catch (FormattingException e) + { + cmdlet.ThrowTerminatingError( + new ErrorRecord( + e, + "FIX_ERROR", + ErrorCategory.InvalidOperation, + e.Corrections)); + return null; } - - return text.ToString(); } - private static void ValidateNotNull(T obj, string name) + private static void ValidateNotNull(object obj, string name) { if (obj == null) { @@ -73,13 +74,35 @@ private static void ValidateNotNull(T obj, string name) } } - private static Settings GetCurrentSettings(Settings settings, string rule) + private static Settings GetCurrentSettings(Settings settings) { + var ruleSettings = new Hashtable(); + foreach (string rule in s_formattingRulesInOrder) + { + if (settings.RuleArguments.TryGetValue(rule, out Dictionary ruleConfiguration)) + { + ruleSettings[rule] = ruleConfiguration; + } + } + return new Settings(new Hashtable() { - {"IncludeRules", new string[] {rule}}, - {"Rules", new Hashtable() { { rule, new Hashtable(settings.RuleArguments[rule]) } } } + { "IncludeRules", s_formattingRulesInOrder }, + { "Rules", ruleSettings } }); } } + + public class FormattingException : Exception + { + public FormattingException( + string message, + IReadOnlyList corrections) + : base(message) + { + Corrections = corrections; + } + + public IReadOnlyList Corrections { get; } + } } diff --git a/Engine/Generic/CorrectionExtent.cs b/Engine/Generic/CorrectionExtent.cs index caad49cdb..09486a059 100644 --- a/Engine/Generic/CorrectionExtent.cs +++ b/Engine/Generic/CorrectionExtent.cs @@ -105,4 +105,54 @@ public CorrectionExtent( } } + + internal struct CorrectionComparer : IComparer, IEqualityComparer + { + public int Compare(CorrectionExtent x, CorrectionExtent y) + { + if (x.StartLineNumber > y.StartLineNumber) + { + return 1; + } + + if (x.StartLineNumber < y.StartLineNumber) + { + return -1; + } + + if (x.StartColumnNumber > y.StartColumnNumber) + { + return 1; + } + + if (x.StartColumnNumber < y.StartColumnNumber) + { + return -1; + } + + if (x.Text.Length > y.Text.Length) + { + return 1; + } + + if (x.Text.Length < y.Text.Length) + { + return -1; + } + + return 0; + } + + public bool Equals(CorrectionExtent x, CorrectionExtent y) + { + return Compare(x, y) == 0; + } + + public int GetHashCode(CorrectionExtent obj) + { + return obj != null + ? obj.GetHashCode() + : 0; + } + } } diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs index 18fc06bf8..995a28852 100644 --- a/Engine/ScriptAnalyzer.cs +++ b/Engine/ScriptAnalyzer.cs @@ -29,6 +29,8 @@ public sealed class ScriptAnalyzer { #region Private members + private readonly CorrectionComparer s_correctionComparer = new CorrectionComparer(); + private IOutputWriter outputWriter; private Dictionary settings; private readonly Regex s_aboutHelpRegex = new Regex("^about_.*help\\.txt$", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -1574,22 +1576,24 @@ public EditableText Fix(EditableText text, Range range, out Range updatedRange, throw new ArgumentNullException(nameof(text)); } - var isRangeNull = range == null; + bool isRangeNull = range == null; if (!isRangeNull && !text.IsValidRange(range)) { this.outputWriter.ThrowTerminatingError(new ErrorRecord( - new ArgumentException("Invalid Range", nameof(range)), - "FIX_ERROR", - ErrorCategory.InvalidArgument, - range)); + new ArgumentException( + "Invalid Range", + nameof(range)), + "FIX_ERROR", + ErrorCategory.InvalidArgument, + range)); } range = isRangeNull ? null : SnapToEdges(text, range); - var previousLineCount = text.LineCount; - var previousUnusedCorrections = 0; + int previousLineCount = text.LineCount; + int previousUnusedCorrections = 0; do { - var records = AnalyzeScriptDefinition(text.ToString()); + IEnumerable records = AnalyzeScriptDefinition(text.ToString()); var corrections = records .Select(r => r.SuggestedCorrections) .Where(sc => sc != null && sc.Any()) @@ -1598,9 +1602,8 @@ public EditableText Fix(EditableText text, Range range, out Range updatedRange, .ToList(); this.outputWriter.WriteVerbose($"Found {corrections.Count} violations."); - int unusedCorrections; - Fix(text, corrections, out unusedCorrections); - var numberOfFixedViolatons = corrections.Count - unusedCorrections; + Fix(text, corrections, out int unusedCorrections); + int numberOfFixedViolatons = corrections.Count - unusedCorrections; fixesWereApplied = numberOfFixedViolatons > 0; this.outputWriter.WriteVerbose($"Fixed {numberOfFixedViolatons} violations."); @@ -1616,7 +1619,7 @@ public EditableText Fix(EditableText text, Range range, out Range updatedRange, } previousUnusedCorrections = unusedCorrections; - var lineCount = text.LineCount; + int lineCount = text.LineCount; if (!isRangeNull && lineCount != previousLineCount) { range = new Range( @@ -1632,6 +1635,108 @@ public EditableText Fix(EditableText text, Range range, out Range updatedRange, return text; } + internal string Fix(string scriptContent, Range fixRange) + { + if (scriptContent == null) + { + throw new ArgumentNullException(nameof(scriptContent)); + } + + var scriptText = new TextDocumentBuilder(scriptContent); + + if (fixRange != null) + { + var fixTextRange = new TextRange( + new TextPosition(fixRange.Start.Line - 1, fixRange.Start.Column - 1), + new TextPosition(fixRange.End.Line - 1, fixRange.End.Column - 1)); + + if (!scriptText.IsValidRange(fixTextRange)) + { + this.outputWriter.ThrowTerminatingError(new ErrorRecord( + new ArgumentException( + "Invalid Range", + nameof(fixRange)), + "FIX_ERROR", + ErrorCategory.InvalidArgument, + fixRange)); + } + } + + int unappliedCorrectionCount = -1; + int previousUnappliedCorrections = -1; + int fixCount = 0; + do + { + previousUnappliedCorrections = unappliedCorrectionCount; + unappliedCorrectionCount = 0; + + // First filter out diagnostics with no corrections, + // or ones not applied in the valid range + var possiblyOverlappingCorrections = new List(); + foreach (DiagnosticRecord record in AnalyzeScriptDefinition(scriptText.ToString())) + { + if (record.SuggestedCorrections == null + || !record.SuggestedCorrections.Any()) + { + continue; + } + + CorrectionExtent correction = record.SuggestedCorrections.First(); + + if (fixRange != null + && !(correction.Start >= fixRange.Start && correction.End <= fixRange.End)) + { + continue; + } + + possiblyOverlappingCorrections.Add(correction); + } + + // We now need the list to be sorted for a second pass + possiblyOverlappingCorrections.Sort(s_correctionComparer); + + // Remove corrections that lie within the range of a predecessor. + // The sorting function we use + CorrectionExtent previousCorrection = null; + var corrections = new List(possiblyOverlappingCorrections.Count); + var unappliedCorrections = new List(); + foreach (CorrectionExtent correction in possiblyOverlappingCorrections) + { + if (previousCorrection != null + && (correction.Start >= previousCorrection.Start && correction.Start <= previousCorrection.End + || correction.End >= previousCorrection.Start && correction.End <= previousCorrection.End)) + { + unappliedCorrectionCount++; + continue; + } + + corrections.Add(correction); + previousCorrection = correction; + } + + if (unappliedCorrectionCount > 0 + && unappliedCorrectionCount == previousUnappliedCorrections) + { + throw new FormattingException( + "Unable to apply all fixes to script", + unappliedCorrections); + } + + this.outputWriter.WriteVerbose($"Found {corrections.Count} violations."); + + if (corrections.Count > 0) + { + fixCount += corrections.Count; + scriptText.ApplyCorrections(corrections); + } + + } while (unappliedCorrectionCount > 0); + + this.outputWriter.WriteVerbose($"Fixed {fixCount} violations."); + + return scriptText.ToString(); + } + private static Encoding GetFileEncoding(string path) { using (var stream = new FileStream(path, FileMode.Open)) diff --git a/Engine/text.cs b/Engine/text.cs new file mode 100644 index 000000000..e76c0c936 --- /dev/null +++ b/Engine/text.cs @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer +{ + internal struct TextPosition + { + public TextPosition(int line, int column) + { + Line = line; + Column = column; + } + + public int Line { get; } + + public int Column { get; } + } + + internal struct TextRange + { + public TextRange(TextPosition start, TextPosition end) + { + Start = start; + End = end; + } + + public TextPosition Start { get; } + + public TextPosition End { get; } + } + + internal class TextDocumentBuilder + { + private class CharBuffer + { + private char[] _charArray; + + private int _validLength; + + public CharBuffer() + { + _charArray = new char[128]; + _validLength = 0; + } + + public void CopyFrom(string content, int startIndex, int length) + { + if (length > _charArray.Length) + { + int newArrayLength = _charArray.Length; + do + { + newArrayLength *= 2; + } while (length > newArrayLength); + + _charArray = new char[newArrayLength]; + } + + content.CopyTo(startIndex, _charArray, 0, length); + _validLength = length; + } + + public void CopyTo(StringBuilder buffer) + { + buffer.Append(_charArray, 0, _validLength); + } + } + + private static readonly char[] s_newlineStartChars = new [] + { + '\r', + '\n' + }; + + private readonly CharBuffer _spanBuffer; + + private string _content; + + public TextDocumentBuilder(string content) + { + _content = content; + _spanBuffer = new CharBuffer(); + } + + public override string ToString() + { + return _content; + } + + public TextRange GetValidColumnIndexRange(TextRange textRange) + { + return new TextRange( + new TextPosition(textRange.Start.Line - 1, Math.Min(1, textRange.Start.Column - 1)), + new TextPosition(textRange.End.Line - 1, Math.Max(textRange.End.Column - 1, GetLastColumnLength()))); + } + + public bool IsValidRange(TextRange range) + { + return range.Start.Line <= range.End.Line + && range.End.Line <= GetLineCount() + 1 + && range.Start.Column <= GetColumnLength(range.Start.Line) + 1 + && range.End.Column <= GetColumnLength(range.End.Line) + 1; + } + + public void ApplyCorrections(IReadOnlyList corrections) + { + var newContent = new StringBuilder(_content.Length); + var effectiveOldPosition = new TextPosition(0, 0); + int currentIndex = 0; + + foreach (CorrectionExtent correction in corrections) + { + var correctionStartPosition = new TextPosition(correction.StartLineNumber - 1, correction.StartColumnNumber - 1); + CopyNextSpan(ref currentIndex, newContent, effectiveOldPosition, correctionStartPosition); + newContent.Append(correction.Text); + currentIndex += GetContentReplacedLength( + currentIndex, + correctionStartPosition, + new TextPosition(correction.EndLineNumber - 1, correction.EndColumnNumber - 1)); + effectiveOldPosition = new TextPosition(correction.EndLineNumber - 1, correction.EndColumnNumber - 1); + } + CopyToEnd(currentIndex, newContent); + _content = newContent.ToString(); + } + + private int GetContentReplacedLength(int startIndex, TextPosition startPosition, TextPosition endPosition) + { + int linesToRead = endPosition.Line - startPosition.Line; + int index = startIndex; + + if (linesToRead == 0) + { + return endPosition.Column - startPosition.Column; + } + + for (int i = 0; i < linesToRead; i++) + { + ScanToNextLine(ref index); + } + return index - startIndex + endPosition.Column; + } + + private void CopyNextSpan( + ref int index, + StringBuilder destinationBuffer, + TextPosition effectiveOldPosition, + TextPosition correctionStartPosition) + { + // Seek from the current index to the start of the next correction + int nextIndex = index; + int linesToRead = correctionStartPosition.Line - effectiveOldPosition.Line; + if (linesToRead == 0) + { + nextIndex += correctionStartPosition.Column - effectiveOldPosition.Column; + } + else + { + for (int i = 0; i < linesToRead; i++) + { + ScanToNextLine(ref nextIndex); + } + nextIndex += correctionStartPosition.Column; + } + + // Copy the characters over + _spanBuffer.CopyFrom(_content, index, nextIndex - index); + _spanBuffer.CopyTo(destinationBuffer); + + // Update the index + index = nextIndex; + } + + private void ScanToNextLine(ref int index) + { + index = _content.IndexOfAny(s_newlineStartChars, index); + + char c = _content[index]; + if (c == '\n') + { + index++; + return; + } + + // Must be looking at "\r\n" + index += 2; + return; + } + + private void CopyToEnd(int currentIndex, StringBuilder destinationBuffer) + { + _spanBuffer.CopyFrom(_content, currentIndex, _content.Length - currentIndex); + _spanBuffer.CopyTo(destinationBuffer); + } + + private int GetColumnLength(int lineNumber) + { + int lineIndex = GetLineIndex(lineNumber); + return _content.IndexOfAny(s_newlineStartChars, lineIndex); + } + + private int GetLastColumnLength() + { + return _content.Length - _content.LastIndexOfAny(s_newlineStartChars); + } + + private int GetLineCount() + { + int lineCount = 0; + foreach (char c in _content) + { + if (c == '\n') { lineCount++; } + } + return lineCount; + } + + private int GetLineIndex(int lineNumber) + { + int index = 0; + for (int i = 0; i < lineNumber; i++) + { + ScanToNextLine(ref index); + } + return index; + } + } +} \ No newline at end of file diff --git a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Query/RuntimeData.cs b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Query/RuntimeData.cs index 98db0616c..2fcf4c693 100644 --- a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Query/RuntimeData.cs +++ b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Query/RuntimeData.cs @@ -128,7 +128,7 @@ private static IReadOnlyDictionary> CreateAli IReadOnlyDictionary> modules, IReadOnlyDictionary> commands) { - var aliasTable = new Dictionary>(); + var aliasTable = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair> module in modules) { foreach (KeyValuePair moduleVersion in module.Value)