MegaBites-AI/Windows-powershell
0372
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System.Diagnostics.CodeAnalysis;5using System.Globalization;6using System.Text;7#if !STANDALONE8using System.Management.Automation.Internal;9#endif10 11namespace System.Management.Automation.Language12{13 #region Public Interfaces14 15 /// <summary>16 /// Represents a single point in a script. The script may come from a file or interactive input.17 /// </summary>18#nullable enable19 public interface IScriptPosition20 {21 /// <summary>22 /// The name of the file, or if the script did not come from a file, then null.23 /// </summary>24 string? File { get; }25 26 /// <summary>27 /// The line number of the position, with the value 1 being the first line.28 /// </summary>29 int LineNumber { get; }30 31 /// <summary>32 /// The column number of the position, with the value 1 being the first column.33 /// </summary>34 int ColumnNumber { get; }35 36 /// <summary>37 /// The offset from the beginning of the script.38 /// </summary>39 int Offset { get; }40 41 /// <summary>42 /// The complete text of the line that this position is included on.43 /// </summary>44 string Line { get; }45 46 /// <summary>47 /// The complete script that this position is included in.48 /// </summary>49 string? GetFullScript();50 }51#nullable restore52 53 /// <summary>54 /// Represents the a span of text in a script.55 /// </summary>56#nullable enable57 public interface IScriptExtent58 {59 /// <summary>60 /// The filename the extent includes, or null if the extent is not included in any file.61 /// </summary>62 string? File { get; }63 64 /// <summary>65 /// The starting position of the extent.66 /// </summary>67 IScriptPosition StartScriptPosition { get; }68 69 /// <summary>70 /// The end position of the extent. This position is actually 1 character past the end of the extent.71 /// </summary>72 IScriptPosition EndScriptPosition { get; }73 74 /// <summary>75 /// The line number at the beginning of the extent, with the value 1 being the first line.76 /// </summary>77 int StartLineNumber { get; }78 79 /// <summary>80 /// The column number at the beginning of the extent, with the value 1 being the first column.81 /// </summary>82 int StartColumnNumber { get; }83 84 /// <summary>85 /// The line number at the end of the extent, with the value 1 being the first line.86 /// </summary>87 int EndLineNumber { get; }88 89 /// <summary>90 /// The column number at the end of the extent, with the value 1 being the first column.91 /// </summary>92 int EndColumnNumber { get; }93 94 /// <summary>95 /// The script text that the extent includes.96 /// </summary>97 string Text { get; }98 99 /// <summary>100 /// The starting offset of the extent.101 /// </summary>102 int StartOffset { get; }103 104 /// <summary>105 /// The ending offset of the extent.106 /// </summary>107 int EndOffset { get; }108 }109#nullable restore110 111 /// <summary>112 /// A few utility functions for script positions.113 /// </summary>114 internal static class PositionUtilities115 {116 /// <summary>117 /// Return a unique position representing an empty or missing position.118 /// </summary>119 public static IScriptPosition EmptyPosition { get; } = new EmptyScriptPosition();120 121 /// <summary>122 /// Return a unique extent representing an empty or missing extent.123 /// </summary>124 public static IScriptExtent EmptyExtent { get; } = new EmptyScriptExtent();125 126 /// <summary>127 /// Return a message that looks like:128 ///129 /// At {filename}:{line} char:{column}130 /// + $x + @y131 /// + ~132 /// </summary>133 internal static string VerboseMessage(IScriptExtent position)134 {135 if (PositionUtilities.EmptyExtent.Equals(position))136 {137 return string.Empty;138 }139 140 string fileName = position.File;141 if (string.IsNullOrEmpty(fileName))142 {143 fileName = ParserStrings.TextForWordLine;144 }145 146 string sourceLine = position.StartScriptPosition.Line.TrimEnd();147 148 string message = string.Empty;149 if (!string.IsNullOrEmpty(sourceLine))150 {151 int spacesBeforeError = position.StartColumnNumber - 1;152 int errorLength = (position.StartLineNumber == position.EndLineNumber && position.EndColumnNumber <= sourceLine.Length + 1)153 ? position.EndColumnNumber - position.StartColumnNumber154 : sourceLine.Length - position.StartColumnNumber + 1;155 156 // Expand tabs before figuring out if we need to truncate the line157 if (sourceLine.Contains('\t'))158 {159 var copyLine = new StringBuilder(sourceLine.Length * 2);160 161 var beforeError = sourceLine.Substring(0, spacesBeforeError).Replace("\t", " ");162 var error = sourceLine.Substring(spacesBeforeError, errorLength).Replace("\t", " ");163 164 copyLine.Append(beforeError);165 copyLine.Append(error);166 copyLine.Append(sourceLine.Substring(spacesBeforeError + errorLength).Replace("\t", " "));167 168 spacesBeforeError = beforeError.Length;169 errorLength = error.Length;170 sourceLine = copyLine.ToString();171 }172 173 // Max width is 69 because:174 // * sometimes PowerShell is opened with width 80175 // * we always prepend "+ "176 // * we sometimes prepend "... "177 // * we sometimes append " ..."178 // * wrapping kicks in if we hit the width exactly, so -1 to avoid that.179 const int maxLineLength = 69;180 bool needsPrefixDots = false;181 bool needsSuffixDots = false;182 183 int lineLength = sourceLine.Length;184 var sb = new StringBuilder(sourceLine.Length * 2 + 4);185 if (lineLength > maxLineLength)186 {187 // Need to truncate - include as much of the error as we can, but with188 // some preceding context if possible.189 190 int totalPrefix = spacesBeforeError;191 int prefix = Math.Min(totalPrefix, 12);192 193 int totalSuffix = lineLength - errorLength - spacesBeforeError;194 int suffix = Math.Min(totalSuffix, 8);195 196 int candidateLength = prefix + errorLength + suffix;197 if (candidateLength >= maxLineLength)198 {199 // Too long. The suffix is truncated automatically by200 // the Substring call, but we might need some of the201 // squiggles removed as well.202 if (prefix + errorLength >= maxLineLength)203 {204 errorLength = maxLineLength - prefix;205 }206 207 needsSuffixDots = true;208 }209 else210 {211 // We can shift prefix to suffix or vice versa to fill in212 // more of the line. Prefer shifting to prefix.213 int prefixAvailable = totalPrefix - prefix;214 if (prefixAvailable > 0)215 {216 prefix += Math.Min(prefixAvailable, maxLineLength - candidateLength);217 candidateLength = prefix + errorLength + suffix;218 }219 220 if (candidateLength < maxLineLength && totalSuffix > 0)221 {222 suffix += Math.Min(totalSuffix, maxLineLength - candidateLength);223 }224 225 needsSuffixDots = (suffix < totalSuffix);226 }227 228 needsPrefixDots = (prefix < totalPrefix);229 230 var startIndex = Math.Max(spacesBeforeError - prefix, 0);231 sourceLine = sourceLine.Substring(startIndex, maxLineLength);232 spacesBeforeError = Math.Min(spacesBeforeError, prefix);233 errorLength = Math.Min(errorLength, maxLineLength - spacesBeforeError);234 }235 236 if (needsPrefixDots)237 {238 sb.Append("\u2026 "); // Unicode ellipsis character239 }240 241 sb.Append(sourceLine);242 243 if (needsSuffixDots)244 {245 sb.Append(" \u2026"); // Unicode ellipsis character246 }247 248 sb.Append(Environment.NewLine);249 sb.Append("+ ");250 sb.Append(' ', spacesBeforeError + (needsPrefixDots ? 2 : 0));251 // errorLength of 0 happens at EOF - always write out 1.252 sb.Append('~', errorLength > 0 ? errorLength : 1);253 254 message = sb.ToString();255 }256 257 return StringUtil.Format(258 ParserStrings.TextForPositionMessage,259 fileName,260 position.StartLineNumber,261 position.StartColumnNumber,262 message);263 }264 265 /// <summary>266 /// Return a message that looks like:267 /// 12+ $x + <<<< $b.268 /// </summary>269 internal static string BriefMessage(IScriptPosition position)270 {271 StringBuilder message = new StringBuilder(position.Line);272 if (position.ColumnNumber > message.Length)273 {274 message.Append(" <<<< ");275 }276 else277 {278 message.Insert(position.ColumnNumber - 1, " >>>> ");279 }280 281 return StringUtil.Format(ParserStrings.TraceScriptLineMessage, position.LineNumber, message.ToString());282 }283 284 internal static IScriptExtent NewScriptExtent(IScriptExtent start, IScriptExtent end)285 {286 if (start == end)287 {288 return start;289 }290 291 if (start == EmptyExtent)292 {293 return end;294 }295 296 if (end == EmptyExtent)297 {298 return start;299 }300 301 InternalScriptExtent startExtent = start as InternalScriptExtent;302 InternalScriptExtent endExtent = end as InternalScriptExtent;303 Diagnostics.Assert(startExtent != null && endExtent != null, "This function only handles internal and empty extents");304 Diagnostics.Assert(startExtent.PositionHelper == endExtent.PositionHelper, "Extents must be from same source");305 306 return new InternalScriptExtent(startExtent.PositionHelper, startExtent.StartOffset, endExtent.EndOffset);307 }308 309 internal static bool IsBefore(this IScriptExtent extentToTest, IScriptExtent startExtent)310 {311 if (extentToTest.EndLineNumber < startExtent.StartLineNumber)312 {313 return true;314 }315 316 if (extentToTest.EndLineNumber == startExtent.StartLineNumber)317 {318 return extentToTest.EndColumnNumber <= startExtent.StartColumnNumber;319 }320 321 return false;322 }323 324 internal static bool IsAfter(this IScriptExtent extentToTest, IScriptExtent endExtent)325 {326 if (extentToTest.StartLineNumber > endExtent.EndLineNumber)327 {328 return true;329 }330 331 if (extentToTest.StartLineNumber == endExtent.EndLineNumber)332 {333 return extentToTest.StartColumnNumber >= endExtent.EndColumnNumber;334 }335 336 return false;337 }338 339 internal static bool IsWithin(this IScriptExtent extentToTest, IScriptExtent extent)340 {341 return extentToTest.StartOffset >= extent.StartOffset && extentToTest.EndOffset <= extent.EndOffset;342 }343 344 internal static bool IsAfter(this IScriptExtent extent, int line, int column)345 {346 if (line < extent.StartLineNumber)347 return true;348 349 return (line == extent.StartLineNumber && column < extent.StartColumnNumber);350 }351 352 internal static bool ContainsLineAndColumn(this IScriptExtent extent, int line, int column)353 {354 if (extent.StartLineNumber == line)355 {356 if (column == 0)357 {358 return true;359 }360 361 if (column >= extent.StartColumnNumber)362 {363 if (extent.EndLineNumber != extent.StartLineNumber)364 {365 return true;366 }367 368 return (column < extent.EndColumnNumber);369 }370 371 return false;372 }373 374 if (extent.StartLineNumber > line)375 return false;376 377 if (line > extent.EndLineNumber)378 return false;379 380 if (extent.EndLineNumber == line)381 return column < extent.EndColumnNumber;382 383 return true;384 }385 }386 387 #endregion Public Interfaces388 389 #region Internal Position390 391 internal class PositionHelper392 {393 private int[] _lineStartMap;394 395 internal PositionHelper(string filename, string scriptText)396 {397 File = filename;398 ScriptText = scriptText;399 }400 401 internal string ScriptText { get; }402 403 internal int[] LineStartMap404 {405 set { _lineStartMap = value; }406 }407 408 public string File { get; }409 410 internal int LineFromOffset(int offset)411 {412 int line = Array.BinarySearch<int>(_lineStartMap, offset);413 if (line < 0)414 {415 line = ~line - 1;416 }417 418 return line + 1;419 }420 421 internal int ColumnFromOffset(int offset)422 {423 return offset - _lineStartMap[LineFromOffset(offset) - 1] + 1;424 }425 426 internal string Text(int line)427 {428 int start = _lineStartMap[line - 1];429 if (line < _lineStartMap.Length)430 {431 int length = _lineStartMap[line] - start;432 return ScriptText.Substring(start, length);433 }434 435 return ScriptText.Substring(start);436 }437 }438 439 internal sealed class InternalScriptPosition : IScriptPosition440 {441 private readonly PositionHelper _positionHelper;442 443 internal InternalScriptPosition(PositionHelper _positionHelper, int offset)444 {445 this._positionHelper = _positionHelper;446 Offset = offset;447 }448 449 public string File { get { return _positionHelper.File; } }450 451 public int LineNumber { get { return _positionHelper.LineFromOffset(Offset); } }452 453 public int ColumnNumber { get { return _positionHelper.ColumnFromOffset(Offset); } }454 455 public string Line { get { return _positionHelper.Text(LineNumber); } }456 457 public int Offset { get; }458 459 internal InternalScriptPosition CloneWithNewOffset(int offset)460 {461 return new InternalScriptPosition(_positionHelper, offset);462 }463 464 public string GetFullScript()465 {466 return _positionHelper.ScriptText;467 }468 }469 470 internal sealed class InternalScriptExtent : IScriptExtent471 {472 internal InternalScriptExtent(PositionHelper _positionHelper, int startOffset, int endOffset)473 {474 this.PositionHelper = _positionHelper;475 StartOffset = startOffset;476 EndOffset = endOffset;477 }478 479 public string File480 {481 get { return PositionHelper.File; }482 }483 484 public IScriptPosition StartScriptPosition485 {486 get { return new InternalScriptPosition(PositionHelper, StartOffset); }487 }488 489 public IScriptPosition EndScriptPosition490 {491 get { return new InternalScriptPosition(PositionHelper, EndOffset); }492 }493 494 public int StartLineNumber495 {496 get { return PositionHelper.LineFromOffset(StartOffset); }497 }498 499 public int StartColumnNumber500 {501 get { return PositionHelper.ColumnFromOffset(StartOffset); }502 }503 504 public int EndLineNumber505 {506 get { return PositionHelper.LineFromOffset(EndOffset); }507 }508 509 public int EndColumnNumber510 {511 get { return PositionHelper.ColumnFromOffset(EndOffset); }512 }513 514 public string Text515 {516 get517 {518 // StartOffset can be > the length for the EOF token.519 if (StartOffset > PositionHelper.ScriptText.Length)520 {521 return string.Empty;522 }523 524 return PositionHelper.ScriptText.Substring(StartOffset, EndOffset - StartOffset);525 }526 }527 528 public override string ToString()529 {530 return Text;531 }532 533 internal PositionHelper PositionHelper { get; }534 535 public int StartOffset { get; }536 537 public int EndOffset { get; }538 }539 540 #endregion Internal Position541 542 #region "Empty" Positions543 544 internal sealed class EmptyScriptPosition : IScriptPosition545 {546 public string File { get { return null; } }547 548 public int LineNumber { get { return 0; } }549 550 public int ColumnNumber { get { return 0; } }551 552 public int Offset { get { return 0; } }553 554 public string Line { get { return string.Empty; } }555 556 public string GetFullScript() { return null; }557 }558 559 internal sealed class EmptyScriptExtent : IScriptExtent560 {561 public string File { get { return null; } }562 563 public IScriptPosition StartScriptPosition { get { return PositionUtilities.EmptyPosition; } }564 565 public IScriptPosition EndScriptPosition { get { return PositionUtilities.EmptyPosition; } }566 567 public int StartLineNumber { get { return 0; } }568 569 public int StartColumnNumber { get { return 0; } }570 571 public int EndLineNumber { get { return 0; } }572 573 public int EndColumnNumber { get { return 0; } }574 575 public int StartOffset { get { return 0; } }576 577 public int EndOffset { get { return 0; } }578 579 public string Text { get { return string.Empty; } }580 581 public override bool Equals(object obj)582 {583 if (!(obj is IScriptExtent otherPosition))584 {585 return false;586 }587 588 if ((string.IsNullOrEmpty(otherPosition.File)) &&589 (otherPosition.StartLineNumber == StartLineNumber) &&590 (otherPosition.StartColumnNumber == StartColumnNumber) &&591 (otherPosition.EndLineNumber == EndLineNumber) &&592 (otherPosition.EndColumnNumber == EndColumnNumber) &&593 (string.IsNullOrEmpty(otherPosition.Text)))594 {595 return true;596 }597 else598 {599 return false;600 }601 }602 603 public override int GetHashCode()604 {605 return base.GetHashCode();606 }607 }608 609 #endregion "Empty" Positions610 611 /// <summary>612 /// Represents a single point in a script. The script may come from a file or interactive input.613 /// </summary>614 public sealed class ScriptPosition : IScriptPosition615 {616 private readonly string _fullScript;617 618 /// <summary>619 /// Creates a new script position, which represents a point in a script.620 /// </summary>621 /// <param name="scriptName">The name of the file, or if the script did not come from a file, then null.</param>622 /// <param name="scriptLineNumber">The line number of the position, with the value 1 being the first line.</param>623 /// <param name="offsetInLine">The column number of the position, with the value 1 being the first column.</param>624 /// <param name="line">The complete text of the line that this position is included on.</param>625 [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly")]626 public ScriptPosition(string scriptName, int scriptLineNumber, int offsetInLine, string line)627 {628 File = scriptName;629 LineNumber = scriptLineNumber;630 ColumnNumber = offsetInLine;631 632 if (string.IsNullOrEmpty(line))633 {634 Line = string.Empty;635 }636 else637 {638 Line = line;639 }640 }641 642 /// <summary>643 /// Creates a new script position, which represents a point in a script.644 /// </summary>645 /// <param name="scriptName">The name of the file, or if the script did not come from a file, then null.</param>646 /// <param name="scriptLineNumber">The line number of the position, with the value 1 being the first line.</param>647 /// <param name="offsetInLine">The column number of the position, with the value 1 being the first column.</param>648 /// <param name="line">The complete text of the line that this position is included on.</param>649 /// <param name="fullScript">The complete script text. Optional, can be null.</param>650 [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly")]651 public ScriptPosition(652 string scriptName,653 int scriptLineNumber,654 int offsetInLine,655 string line,656 string fullScript)657 : this(scriptName, scriptLineNumber, offsetInLine, line)658 {659 _fullScript = fullScript;660 }661 662 /// <summary>663 /// The name of the file, or if the script did not come from a file, then null.664 /// </summary>665 public string File { get; }666 667 /// <summary>668 /// The line number of the position, with the value 1 being the first line.669 /// </summary>670 public int LineNumber { get; }671 672 /// <summary>673 /// The column number of the position, with the value 1 being the first column.674 /// </summary>675 public int ColumnNumber { get; }676 677 /// <summary>678 /// The offset from the beginning of the script, always return 0.679 /// </summary>680 public int Offset { get { return 0; } }681 682 /// <summary>683 /// The complete text of the line that this position is included on.684 /// </summary>685 public string Line { get; }686 687 /// <summary>688 /// The complete script that this position is included in.689 /// </summary>690 public string GetFullScript() { return _fullScript; }691 }692 693 /// <summary>694 /// A script extent used to customize the display of error location information.695 /// </summary>696 public sealed class ScriptExtent : IScriptExtent697 {698 private ScriptPosition _startPosition;699 private ScriptPosition _endPosition;700 701 private ScriptExtent()702 {703 }704 705 /// <summary>706 /// Creates a new ScriptExtent class.707 /// </summary>708 public ScriptExtent(ScriptPosition startPosition, ScriptPosition endPosition)709 {710 _startPosition = startPosition;711 _endPosition = endPosition;712 }713 714 /// <summary>715 /// The name of the file, or if the script did not come from a file, then null.716 /// </summary>717 public string File { get { return _startPosition.File; } }718 719 /// <summary>720 /// The starting position of the extent.721 /// </summary>722 public IScriptPosition StartScriptPosition { get { return _startPosition; } }723 724 /// <summary>725 /// The end position of the extent. This position is actually 1 character past the end of the extent.726 /// </summary>727 public IScriptPosition EndScriptPosition { get { return _endPosition; } }728 729 /// <summary>730 /// The line number at the beginning of the extent, with the value 1 being the first line.731 /// </summary>732 public int StartLineNumber { get { return _startPosition.LineNumber; } }733 734 /// <summary>735 /// The column number at the beginning of the extent, with the value 1 being the first column.736 /// </summary>737 public int StartColumnNumber { get { return _startPosition.ColumnNumber; } }738 739 /// <summary>740 /// The line number at the end of the extent, with the value 1 being the first line.741 /// </summary>742 public int EndLineNumber { get { return _endPosition.LineNumber; } }743 744 /// <summary>745 /// The column number at the end of the extent, with the value 1 being the first column.746 /// </summary>747 public int EndColumnNumber { get { return _endPosition.ColumnNumber; } }748 749 /// <summary>750 /// The start offset (always returns 0)751 /// </summary>752 public int StartOffset { get { return 0; } }753 754 /// <summary>755 /// The end offset (always returns 0)756 /// </summary>757 public int EndOffset { get { return 0; } }758 759 /// <summary>760 /// The script text that the extent includes.761 /// </summary>762 public string Text763 {764 get765 {766 if (EndColumnNumber > 0)767 {768 if (StartLineNumber == EndLineNumber)769 {770 return _startPosition.Line.Substring(_startPosition.ColumnNumber - 1,771 _endPosition.ColumnNumber - _startPosition.ColumnNumber);772 }773 774 var start = _startPosition.Line.AsSpan(_startPosition.ColumnNumber);775 var end = _endPosition.Line.AsSpan(0, _endPosition.ColumnNumber);776 return string.Create(CultureInfo.InvariantCulture, $"{start}...{end}");777 }778 else779 {780 return string.Empty;781 }782 }783 }784 785 internal void ToPSObjectForRemoting(PSObject dest)786 {787 RemotingEncoder.AddNoteProperty(dest, "ScriptExtent_File", () => File);788 RemotingEncoder.AddNoteProperty(dest, "ScriptExtent_StartLineNumber", () => StartLineNumber);789 RemotingEncoder.AddNoteProperty(dest, "ScriptExtent_StartColumnNumber", () => StartColumnNumber);790 RemotingEncoder.AddNoteProperty(dest, "ScriptExtent_EndLineNumber", () => EndLineNumber);791 RemotingEncoder.AddNoteProperty(dest, "ScriptExtent_EndColumnNumber", () => EndColumnNumber);792 }793 794 private void PopulateFromSerializedInfo(PSObject serializedScriptExtent)795 {796 string file = RemotingDecoder.GetPropertyValue<string>(serializedScriptExtent, "ScriptExtent_File");797 int startLineNumber = RemotingDecoder.GetPropertyValue<int>(serializedScriptExtent, "ScriptExtent_StartLineNumber");798 int startColumnNumber = RemotingDecoder.GetPropertyValue<int>(serializedScriptExtent, "ScriptExtent_StartColumnNumber");799 int endLineNumber = RemotingDecoder.GetPropertyValue<int>(serializedScriptExtent, "ScriptExtent_EndLineNumber");800 int endColumnNumber = RemotingDecoder.GetPropertyValue<int>(serializedScriptExtent, "ScriptExtent_EndColumnNumber");801 802 ScriptPosition startPosition = new ScriptPosition(file, startLineNumber, startColumnNumber, null);803 ScriptPosition endPosition = new ScriptPosition(file, endLineNumber, endColumnNumber, null);804 805 _startPosition = startPosition;806 _endPosition = endPosition;807 }808 809 internal static ScriptExtent FromPSObjectForRemoting(PSObject serializedScriptExtent)810 {811 ScriptExtent extent = new ScriptExtent();812 extent.PopulateFromSerializedInfo(serializedScriptExtent);813 return extent;814 }815 }816}817 