diff --git a/.gitignore b/.gitignore index f4281ce..a54098c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ project.lock.json # VSCode directories that are not at the repository root /**/.vscode/ + +# Visual Studio directories +.vs/ diff --git a/Modules/Microsoft.PowerShell.AstTools/src/Microsoft.PowerShell.AstTools.csproj b/Modules/Microsoft.PowerShell.AstTools/src/Microsoft.PowerShell.AstTools.csproj new file mode 100644 index 0000000..ab692df --- /dev/null +++ b/Modules/Microsoft.PowerShell.AstTools/src/Microsoft.PowerShell.AstTools.csproj @@ -0,0 +1,25 @@ + + + + netcoreapp3.1;netstandard2.0 + Microsoft.PowerShell.AstTools + + + + + $(DefineConstants);PS7 + + + + $(DefineConstants);PSSTD + + + + + + + + + + + diff --git a/Modules/Microsoft.PowerShell.AstTools/src/PrettyPrinter.cs b/Modules/Microsoft.PowerShell.AstTools/src/PrettyPrinter.cs new file mode 100644 index 0000000..830b6f2 --- /dev/null +++ b/Modules/Microsoft.PowerShell.AstTools/src/PrettyPrinter.cs @@ -0,0 +1,2069 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Management.Automation.Language; +using System.Text; + +namespace Microsoft.PowerShell.AstTools +{ + public class StringPrettyPrinter : PrettyPrinter + { + private string _result; + + private StringWriter _sw; + + public string PrettyPrintInput(string input) + { + DoPrettyPrintInput(input); + return _result; + } + + public string PrettyPrintFile(string filePath) + { + DoPrettyPrintFile(filePath); + return _result; + } + + public string PrettyPrintAst(Ast ast, IReadOnlyList tokens) + { + DoPrettyPrintAst(ast, tokens); + return _result; + } + + protected override TextWriter CreateTextWriter() + { + _sw = new StringWriter(); + return _sw; + } + + protected override void DoPostPrintAction() + { + _result = _sw.ToString(); + } + } + + /// + /// Prints a PowerShell AST based on its structure rather than text captured in extents. + /// + public abstract class PrettyPrinter + { + private readonly PrettyPrintingVisitor _visitor; + + /// + /// Create a new pretty printer for use. + /// + protected PrettyPrinter() + { + _visitor = new PrettyPrintingVisitor(); + } + + protected abstract TextWriter CreateTextWriter(); + + protected virtual void DoPostPrintAction() + { + } + + /// + /// Pretty print a PowerShell script provided as an inline string. + /// + /// The inline PowerShell script to parse and pretty print. + /// A pretty-printed version of the given PowerShell script. + protected void DoPrettyPrintInput(string input) + { + Ast ast = Parser.ParseInput(input, out Token[] tokens, out ParseError[] errors); + + if (errors != null && errors.Length > 0) + { + throw new ParseException(errors); + } + + DoPrettyPrintAst(ast, tokens); + } + + /// + /// Pretty print the contents of a PowerShell file. + /// + /// The path of the PowerShell file to pretty print. + /// The pretty-printed file contents. + protected void DoPrettyPrintFile(string filePath) + { + Ast ast = Parser.ParseFile(filePath, out Token[] tokens, out ParseError[] errors); + + if (errors != null && errors.Length > 0) + { + throw new ParseException(errors); + } + + DoPrettyPrintAst(ast, tokens); + } + + /// + /// Pretty print a given PowerShell AST. + /// + /// The PowerShell AST to print. + /// The token array generated when the AST was parsed. May be null. + /// The pretty-printed PowerShell AST in string form. + protected void DoPrettyPrintAst(Ast ast, IReadOnlyList tokens) + { + using (TextWriter textWriter = CreateTextWriter()) + { + _visitor.Run(textWriter, ast, tokens); + DoPostPrintAction(); + } + } + } + + internal class PrettyPrintingVisitor : AstVisitor2 + { + private TextWriter _tw; + + private readonly string _newline; + + private readonly string _indentStr; + + private readonly string _comma; + + private int _tokenIndex; + + private IReadOnlyList _tokens; + + private int _indent; + + public PrettyPrintingVisitor() + { + _newline = "\n"; + _indentStr = " "; + _comma = ", "; + _indent = 0; + } + + public void Run( + TextWriter tw, + Ast ast, + IReadOnlyList tokens) + { + _tw = tw; + _tokenIndex = 0; + _tokens = tokens; + ast.Visit(this); + _tw = null; + } + + public override AstVisitAction VisitArrayExpression(ArrayExpressionAst arrayExpressionAst) + { + WriteCommentsToAstPosition(arrayExpressionAst); + + _tw.Write("@("); + WriteStatementBlock(arrayExpressionAst.SubExpression.Statements, arrayExpressionAst.SubExpression.Traps); + _tw.Write(")"); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) + { + Intersperse(arrayLiteralAst.Elements, _comma); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) + { + assignmentStatementAst.Left.Visit(this); + + _tw.Write(' '); + _tw.Write(GetTokenString(assignmentStatementAst.Operator)); + _tw.Write(' '); + + assignmentStatementAst.Right.Visit(this); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitAttribute(AttributeAst attributeAst) + { + WriteCommentsToAstPosition(attributeAst); + + _tw.Write('['); + _tw.Write(attributeAst.TypeName); + _tw.Write('('); + + bool hadPositionalArgs = false; + if (!IsEmpty(attributeAst.PositionalArguments)) + { + hadPositionalArgs = true; + Intersperse(attributeAst.PositionalArguments, _comma); + } + + if (!IsEmpty(attributeAst.NamedArguments)) + { + if (hadPositionalArgs) + { + _tw.Write(_comma); + } + + Intersperse(attributeAst.NamedArguments, _comma); + } + + _tw.Write(")]"); + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitAttributedExpression(AttributedExpressionAst attributedExpressionAst) + { + attributedExpressionAst.Attribute.Visit(this); + attributedExpressionAst.Child.Visit(this); + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitBaseCtorInvokeMemberExpression(BaseCtorInvokeMemberExpressionAst baseCtorInvokeMemberExpressionAst) + { + WriteCommentsToAstPosition(baseCtorInvokeMemberExpressionAst); + + if (!IsEmpty(baseCtorInvokeMemberExpressionAst.Arguments)) + { + _tw.Write("base("); + Intersperse(baseCtorInvokeMemberExpressionAst.Arguments, ", "); + _tw.Write(')'); + } + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst) + { + binaryExpressionAst.Left.Visit(this); + + _tw.Write(' '); + _tw.Write(GetTokenString(binaryExpressionAst.Operator)); + _tw.Write(' '); + + binaryExpressionAst.Right.Visit(this); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitBlockStatement(BlockStatementAst blockStatementAst) + { + throw new NotImplementedException(); + } + + public override AstVisitAction VisitBreakStatement(BreakStatementAst breakStatementAst) + { + WriteCommentsToAstPosition(breakStatementAst); + WriteControlFlowStatement("break", breakStatementAst.Label); + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitCatchClause(CatchClauseAst catchClauseAst) + { + WriteCommentsToAstPosition(catchClauseAst); + + _tw.Write("catch"); + if (!IsEmpty(catchClauseAst.CatchTypes)) + { + foreach (TypeConstraintAst typeConstraint in catchClauseAst.CatchTypes) + { + _tw.Write(' '); + typeConstraint.Visit(this); + } + } + + catchClauseAst.Body.Visit(this); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitCommand(CommandAst commandAst) + { + WriteCommentsToAstPosition(commandAst); + + if (commandAst.InvocationOperator != TokenKind.Unknown) + { + _tw.Write(GetTokenString(commandAst.InvocationOperator)); + _tw.Write(' '); + } + + Intersperse(commandAst.CommandElements, " "); + + if (!IsEmpty(commandAst.Redirections)) + { + _tw.Write(' '); + Intersperse(commandAst.Redirections, " "); + } + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitCommandExpression(CommandExpressionAst commandExpressionAst) + { + commandExpressionAst.Expression.Visit(this); + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitCommandParameter(CommandParameterAst commandParameterAst) + { + WriteCommentsToAstPosition(commandParameterAst); + + _tw.Write('-'); + _tw.Write(commandParameterAst.ParameterName); + + if (commandParameterAst.Argument != null) + { + _tw.Write(':'); + commandParameterAst.Argument.Visit(this); + } + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst) + { + throw new NotImplementedException(); + } + + public override AstVisitAction VisitConstantExpression(ConstantExpressionAst constantExpressionAst) + { + WriteCommentsToAstPosition(constantExpressionAst); + + if (constantExpressionAst.Value == null) + { + _tw.Write("$null"); + } + else if (constantExpressionAst.StaticType == typeof(bool)) + { + _tw.Write((bool)constantExpressionAst.Value ? "$true" : "$false"); + } + else + { + _tw.Write(constantExpressionAst.Value); + } + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitContinueStatement(ContinueStatementAst continueStatementAst) + { + WriteCommentsToAstPosition(continueStatementAst); + WriteControlFlowStatement("continue", continueStatementAst.Label); + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitConvertExpression(ConvertExpressionAst convertExpressionAst) + { + convertExpressionAst.Attribute.Visit(this); + convertExpressionAst.Child.Visit(this); + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitDataStatement(DataStatementAst dataStatementAst) + { + throw new NotImplementedException(); + } + + public override AstVisitAction VisitDoUntilStatement(DoUntilStatementAst doUntilStatementAst) + { + WriteCommentsToAstPosition(doUntilStatementAst); + _tw.Write("do"); + doUntilStatementAst.Body.Visit(this); + _tw.Write(" until ("); + doUntilStatementAst.Condition.Visit(this); + _tw.Write(')'); + EndStatement(); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst) + { + WriteCommentsToAstPosition(doWhileStatementAst); + _tw.Write("do"); + doWhileStatementAst.Body.Visit(this); + _tw.Write(" while ("); + doWhileStatementAst.Condition.Visit(this); + _tw.Write(')'); + EndStatement(); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitDynamicKeywordStatement(DynamicKeywordStatementAst dynamicKeywordStatementAst) + { + throw new NotImplementedException(); + } + + public override AstVisitAction VisitErrorExpression(ErrorExpressionAst errorExpressionAst) + { + throw new NotImplementedException(); + } + + public override AstVisitAction VisitErrorStatement(ErrorStatementAst errorStatementAst) + { + throw new NotImplementedException(); + } + + public override AstVisitAction VisitExitStatement(ExitStatementAst exitStatementAst) + { + WriteControlFlowStatement("exit", exitStatementAst.Pipeline); + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) + { + WriteCommentsToAstPosition(expandableStringExpressionAst); + _tw.Write('"'); + _tw.Write(expandableStringExpressionAst.Value); + _tw.Write('"'); + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitFileRedirection(FileRedirectionAst redirectionAst) + { + WriteCommentsToAstPosition(redirectionAst); + + if (redirectionAst.FromStream != RedirectionStream.Output) + { + _tw.Write(GetStreamIndicator(redirectionAst.FromStream)); + } + + _tw.Write('>'); + + redirectionAst.Location.Visit(this); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitForEachStatement(ForEachStatementAst forEachStatementAst) + { + WriteCommentsToAstPosition(forEachStatementAst); + + _tw.Write("foreach ("); + forEachStatementAst.Variable.Visit(this); + _tw.Write(" in "); + forEachStatementAst.Condition.Visit(this); + _tw.Write(")"); + forEachStatementAst.Body.Visit(this); + EndStatement(); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitForStatement(ForStatementAst forStatementAst) + { + WriteCommentsToAstPosition(forStatementAst); + + _tw.Write("for ("); + forStatementAst.Initializer.Visit(this); + _tw.Write("; "); + forStatementAst.Condition.Visit(this); + _tw.Write("; "); + forStatementAst.Iterator.Visit(this); + _tw.Write(')'); + forStatementAst.Body.Visit(this); + EndStatement(); + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) + { + WriteCommentsToAstPosition(functionDefinitionAst); + + _tw.Write(functionDefinitionAst.IsFilter ? "filter " : "function "); + _tw.Write(functionDefinitionAst.Name); + Newline(); + functionDefinitionAst.Body.Visit(this); + EndStatement(); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitFunctionMember(FunctionMemberAst functionMemberAst) + { + WriteCommentsToAstPosition(functionMemberAst); + + if (!functionMemberAst.IsConstructor) + { + if (functionMemberAst.IsStatic) + { + _tw.Write("static "); + } + + if (functionMemberAst.IsHidden) + { + _tw.Write("hidden "); + } + + if (functionMemberAst.ReturnType != null) + { + functionMemberAst.ReturnType.Visit(this); + } + } + + _tw.Write(functionMemberAst.Name); + _tw.Write('('); + WriteInlineParameters(functionMemberAst.Parameters); + _tw.Write(')'); + + IReadOnlyList statementAsts = functionMemberAst.Body.EndBlock.Statements; + + if (functionMemberAst.IsConstructor) + { + var baseCtorCall = (BaseCtorInvokeMemberExpressionAst)((CommandExpressionAst)functionMemberAst.Body.EndBlock.Statements[0]).Expression; + + if (!IsEmpty(baseCtorCall.Arguments)) + { + _tw.Write(" : "); + baseCtorCall.Visit(this); + } + + var newStatementAsts = new StatementAst[functionMemberAst.Body.EndBlock.Statements.Count - 1]; + for (int i = 0; i < newStatementAsts.Length; i++) + { + newStatementAsts[i] = functionMemberAst.Body.EndBlock.Statements[i + 1]; + } + statementAsts = newStatementAsts; + } + + if (IsEmpty(statementAsts) && IsEmpty(functionMemberAst.Body.EndBlock.Traps)) + { + Newline(); + _tw.Write('{'); + Newline(); + _tw.Write('}'); + return AstVisitAction.SkipChildren; + } + + BeginBlock(); + WriteStatementBlock(statementAsts, functionMemberAst.Body.EndBlock.Traps); + EndBlock(); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitHashtable(HashtableAst hashtableAst) + { + WriteCommentsToAstPosition(hashtableAst); + + _tw.Write("@{"); + + if (IsEmpty(hashtableAst.KeyValuePairs)) + { + _tw.Write('}'); + return AstVisitAction.SkipChildren; + } + + Indent(); + + Intersperse( + hashtableAst.KeyValuePairs, + WriteHashtableEntry, + Newline); + + Dedent(); + _tw.Write('}'); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitIfStatement(IfStatementAst ifStmtAst) + { + WriteCommentsToAstPosition(ifStmtAst); + + _tw.Write("if ("); + ifStmtAst.Clauses[0].Item1.Visit(this); + _tw.Write(')'); + ifStmtAst.Clauses[0].Item2.Visit(this); + + for (int i = 1; i < ifStmtAst.Clauses.Count; i++) + { + Newline(); + _tw.Write("elseif ("); + ifStmtAst.Clauses[i].Item1.Visit(this); + _tw.Write(')'); + ifStmtAst.Clauses[i].Item2.Visit(this); + } + + if (ifStmtAst.ElseClause != null) + { + Newline(); + _tw.Write("else"); + ifStmtAst.ElseClause.Visit(this); + } + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitIndexExpression(IndexExpressionAst indexExpressionAst) + { + WriteCommentsToAstPosition(indexExpressionAst); + + indexExpressionAst.Target.Visit(this); + _tw.Write('['); + indexExpressionAst.Index.Visit(this); + _tw.Write(']'); + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitInvokeMemberExpression(InvokeMemberExpressionAst methodCallAst) + { + methodCallAst.Expression.Visit(this); + _tw.Write(methodCallAst.Static ? "::" : "."); + methodCallAst.Member.Visit(this); + _tw.Write('('); + Intersperse(methodCallAst.Arguments, ", "); + _tw.Write(')'); + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitMemberExpression(MemberExpressionAst memberExpressionAst) + { + WriteCommentsToAstPosition(memberExpressionAst); + + memberExpressionAst.Expression.Visit(this); + _tw.Write(memberExpressionAst.Static ? "::" : "."); + memberExpressionAst.Member.Visit(this); + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitMergingRedirection(MergingRedirectionAst redirectionAst) + { + WriteCommentsToAstPosition(redirectionAst); + + _tw.Write(GetStreamIndicator(redirectionAst.FromStream)); + _tw.Write(">&"); + _tw.Write(GetStreamIndicator(redirectionAst.ToStream)); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitNamedAttributeArgument(NamedAttributeArgumentAst namedAttributeArgumentAst) + { + WriteCommentsToAstPosition(namedAttributeArgumentAst); + + _tw.Write(namedAttributeArgumentAst.ArgumentName); + + if (!namedAttributeArgumentAst.ExpressionOmitted && namedAttributeArgumentAst.Argument != null) + { + _tw.Write(" = "); + namedAttributeArgumentAst.Argument.Visit(this); + } + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitNamedBlock(NamedBlockAst namedBlockAst) + { + WriteCommentsToAstPosition(namedBlockAst); + + if (!namedBlockAst.Unnamed) + { + _tw.Write(GetTokenString(namedBlockAst.BlockKind)); + } + + BeginBlock(); + + WriteStatementBlock(namedBlockAst.Statements, namedBlockAst.Traps); + + EndBlock(); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitParamBlock(ParamBlockAst paramBlockAst) + { + WriteCommentsToAstPosition(paramBlockAst); + + if (!IsEmpty(paramBlockAst.Attributes)) + { + foreach (AttributeAst attributeAst in paramBlockAst.Attributes) + { + attributeAst.Visit(this); + Newline(); + } + } + + _tw.Write("param("); + + if (IsEmpty(paramBlockAst.Parameters)) + { + _tw.Write(')'); + return AstVisitAction.SkipChildren; + } + + Indent(); + + Intersperse( + paramBlockAst.Parameters, + () => { _tw.Write(','); Newline(count: 2); }); + + Dedent(); + _tw.Write(')'); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitParameter(ParameterAst parameterAst) + { + WriteCommentsToAstPosition(parameterAst); + + if (!IsEmpty(parameterAst.Attributes)) + { + foreach (AttributeBaseAst attribute in parameterAst.Attributes) + { + attribute.Visit(this); + Newline(); + } + } + + parameterAst.Name.Visit(this); + + if (parameterAst.DefaultValue != null) + { + _tw.Write(" = "); + parameterAst.DefaultValue.Visit(this); + } + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitParenExpression(ParenExpressionAst parenExpressionAst) + { + WriteCommentsToAstPosition(parenExpressionAst); + _tw.Write('('); + parenExpressionAst.Pipeline.Visit(this); + _tw.Write(')'); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitPipeline(PipelineAst pipelineAst) + { + WriteCommentsToAstPosition(pipelineAst); + + Intersperse(pipelineAst.PipelineElements, " | "); +#if PS7 + if (pipelineAst.Background) + { + _tw.Write(" &"); + } +#endif + return AstVisitAction.SkipChildren; + } + +#if PS7 + public override AstVisitAction VisitPipelineChain(PipelineChainAst statementChain) + { + WriteCommentsToAstPosition(statementChain); + statementChain.LhsPipelineChain.Visit(this); + _tw.Write(' '); + _tw.Write(GetTokenString(statementChain.Operator)); + _tw.Write(' '); + statementChain.RhsPipeline.Visit(this); + if (statementChain.Background) + { + _tw.Write(" &"); + } + return AstVisitAction.SkipChildren; + } +#endif + + public override AstVisitAction VisitPropertyMember(PropertyMemberAst propertyMemberAst) + { + WriteCommentsToAstPosition(propertyMemberAst); + + if (propertyMemberAst.IsStatic) + { + _tw.Write("static "); + } + + if (propertyMemberAst.IsHidden) + { + _tw.Write("hidden "); + } + + if (propertyMemberAst.PropertyType != null) + { + propertyMemberAst.PropertyType.Visit(this); + } + + _tw.Write('$'); + _tw.Write(propertyMemberAst.Name); + + if (propertyMemberAst.InitialValue != null) + { + _tw.Write(" = "); + propertyMemberAst.InitialValue.Visit(this); + } + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitReturnStatement(ReturnStatementAst returnStatementAst) + { + WriteCommentsToAstPosition(returnStatementAst); + WriteControlFlowStatement("return", returnStatementAst.Pipeline); + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitScriptBlock(ScriptBlockAst scriptBlockAst) + { + WriteCommentsToAstPosition(scriptBlockAst); + + if (scriptBlockAst.Parent != null) + { + _tw.Write('{'); + Indent(); + } + + bool needNewline = false; + if (scriptBlockAst.ParamBlock != null) + { + needNewline = true; + scriptBlockAst.ParamBlock.Visit(this); + } + + Intersperse(scriptBlockAst.UsingStatements, Newline); + + bool useExplicitEndBlock = false; + + if (scriptBlockAst.DynamicParamBlock != null) + { + needNewline = useExplicitEndBlock = true; + if (needNewline) + { + Newline(count: 2); + } + + scriptBlockAst.DynamicParamBlock.Visit(this); + } + + if (scriptBlockAst.BeginBlock != null) + { + needNewline = useExplicitEndBlock = true; + if (needNewline) + { + Newline(count: 2); + } + + scriptBlockAst.BeginBlock.Visit(this); + } + + if (scriptBlockAst.ProcessBlock != null) + { + needNewline = useExplicitEndBlock = true; + if (needNewline) + { + Newline(count: 2); + } + + scriptBlockAst.ProcessBlock.Visit(this); + } + + if (scriptBlockAst.EndBlock != null + && (!IsEmpty(scriptBlockAst.EndBlock.Statements) || !IsEmpty(scriptBlockAst.EndBlock.Traps))) + { + if (useExplicitEndBlock) + { + Newline(count: 2); + scriptBlockAst.EndBlock.Visit(this); + } + else + { + if (needNewline) + { + Newline(count: 2); + } + + WriteStatementBlock(scriptBlockAst.EndBlock.Statements, scriptBlockAst.EndBlock.Traps); + } + } + + if (scriptBlockAst.Parent != null) + { + Dedent(); + _tw.Write('}'); + } + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst) + { + scriptBlockExpressionAst.ScriptBlock.Visit(this); + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitStatementBlock(StatementBlockAst statementBlockAst) + { + WriteCommentsToAstPosition(statementBlockAst); + BeginBlock(); + WriteStatementBlock(statementBlockAst.Statements, statementBlockAst.Traps); + EndBlock(); + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitStringConstantExpression(StringConstantExpressionAst stringConstantExpressionAst) + { + WriteCommentsToAstPosition(stringConstantExpressionAst); + switch (stringConstantExpressionAst.StringConstantType) + { + case StringConstantType.BareWord: + _tw.Write(stringConstantExpressionAst.Value); + break; + + case StringConstantType.SingleQuoted: + _tw.Write('\''); + _tw.Write(stringConstantExpressionAst.Value.Replace("'", "''")); + _tw.Write('\''); + break; + + case StringConstantType.DoubleQuoted: + WriteDoubleQuotedString(stringConstantExpressionAst.Value); + break; + + case StringConstantType.SingleQuotedHereString: + _tw.Write("@'\n"); + _tw.Write(stringConstantExpressionAst.Value); + _tw.Write("\n'@"); + break; + + case StringConstantType.DoubleQuotedHereString: + _tw.Write("@\"\n"); + _tw.Write(stringConstantExpressionAst.Value); + _tw.Write("\n\"@"); + break; + + default: + throw new ArgumentException($"Bad string contstant expression: '{stringConstantExpressionAst}' of type {stringConstantExpressionAst.StringConstantType}"); + } + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitSubExpression(SubExpressionAst subExpressionAst) + { + WriteCommentsToAstPosition(subExpressionAst); + _tw.Write("$("); + WriteStatementBlock(subExpressionAst.SubExpression.Statements, subExpressionAst.SubExpression.Traps); + _tw.Write(')'); + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitSwitchStatement(SwitchStatementAst switchStatementAst) + { + WriteCommentsToAstPosition(switchStatementAst); + + if (switchStatementAst.Label != null) + { + _tw.Write(':'); + _tw.Write(switchStatementAst.Label); + _tw.Write(' '); + } + + _tw.Write("switch ("); + switchStatementAst.Condition.Visit(this); + _tw.Write(')'); + + BeginBlock(); + + bool hasCases = false; + if (!IsEmpty(switchStatementAst.Clauses)) + { + hasCases = true; + + Intersperse( + switchStatementAst.Clauses, + (caseClause) => { caseClause.Item1.Visit(this); caseClause.Item2.Visit(this); }, + () => Newline(count: 2)); + } + + if (switchStatementAst.Default != null) + { + if (hasCases) + { + Newline(count: 2); + } + + _tw.Write("default"); + switchStatementAst.Default.Visit(this); + } + + EndBlock(); + + return AstVisitAction.SkipChildren; + } + +#if PS7 + public override AstVisitAction VisitTernaryExpression(TernaryExpressionAst ternaryExpressionAst) + { + WriteCommentsToAstPosition(ternaryExpressionAst); + + ternaryExpressionAst.Condition.Visit(this); + _tw.Write(" ? "); + ternaryExpressionAst.IfTrue.Visit(this); + _tw.Write(" : "); + ternaryExpressionAst.IfFalse.Visit(this); + return AstVisitAction.SkipChildren; + } +#endif + + public override AstVisitAction VisitThrowStatement(ThrowStatementAst throwStatementAst) + { + WriteCommentsToAstPosition(throwStatementAst); + + WriteControlFlowStatement("throw", throwStatementAst.Pipeline); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitTrap(TrapStatementAst trapStatementAst) + { + WriteCommentsToAstPosition(trapStatementAst); + + _tw.Write("trap"); + + if (trapStatementAst.TrapType != null) + { + _tw.Write(' '); + trapStatementAst.TrapType.Visit(this); + } + + trapStatementAst.Body.Visit(this); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitTryStatement(TryStatementAst tryStatementAst) + { + WriteCommentsToAstPosition(tryStatementAst); + + _tw.Write("try"); + tryStatementAst.Body.Visit(this); + + if (!IsEmpty(tryStatementAst.CatchClauses)) + { + foreach (CatchClauseAst catchClause in tryStatementAst.CatchClauses) + { + Newline(); + catchClause.Visit(this); + } + } + + if (tryStatementAst.Finally != null) + { + Newline(); + _tw.Write("finally"); + tryStatementAst.Finally.Visit(this); + } + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitTypeConstraint(TypeConstraintAst typeConstraintAst) + { + WriteCommentsToAstPosition(typeConstraintAst); + _tw.Write('['); + WriteTypeName(typeConstraintAst.TypeName); + _tw.Write(']'); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitTypeDefinition(TypeDefinitionAst typeDefinitionAst) + { + WriteCommentsToAstPosition(typeDefinitionAst); + + if (typeDefinitionAst.IsClass) + { + _tw.Write("class "); + } + else if (typeDefinitionAst.IsInterface) + { + _tw.Write("interface "); + } + else if (typeDefinitionAst.IsEnum) + { + _tw.Write("enum "); + } + else + { + throw new ArgumentException($"Unknown PowerShell type definition type: '{typeDefinitionAst}'"); + } + + _tw.Write(typeDefinitionAst.Name); + + if (!IsEmpty(typeDefinitionAst.BaseTypes)) + { + _tw.Write(" : "); + + Intersperse( + typeDefinitionAst.BaseTypes, + (baseType) => WriteTypeName(baseType.TypeName), + () => _tw.Write(_comma)); + } + + if (IsEmpty(typeDefinitionAst.Members)) + { + Newline(); + _tw.Write('{'); + Newline(); + _tw.Write('}'); + + return AstVisitAction.SkipChildren; + } + + BeginBlock(); + + if (typeDefinitionAst.Members != null) + { + if (typeDefinitionAst.IsEnum) + { + Intersperse(typeDefinitionAst.Members, () => + { + _tw.Write(','); + Newline(); + }); + } + else if (typeDefinitionAst.IsClass) + { + Intersperse(typeDefinitionAst.Members, () => + { + Newline(count: 2); + }); + } + } + + EndBlock(); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitTypeExpression(TypeExpressionAst typeExpressionAst) + { + WriteCommentsToAstPosition(typeExpressionAst); + _tw.Write('['); + WriteTypeName(typeExpressionAst.TypeName); + _tw.Write(']'); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitUnaryExpression(UnaryExpressionAst unaryExpressionAst) + { + WriteCommentsToAstPosition(unaryExpressionAst); + + switch (unaryExpressionAst.TokenKind) + { + case TokenKind.PlusPlus: + _tw.Write("++"); + unaryExpressionAst.Child.Visit(this); + break; + + case TokenKind.MinusMinus: + _tw.Write("--"); + unaryExpressionAst.Child.Visit(this); + break; + + case TokenKind.PostfixPlusPlus: + unaryExpressionAst.Child.Visit(this); + _tw.Write("++"); + break; + + case TokenKind.PostfixMinusMinus: + unaryExpressionAst.Child.Visit(this); + _tw.Write("--"); + break; + + default: + _tw.Write(GetTokenString(unaryExpressionAst.TokenKind)); + _tw.Write(' '); + unaryExpressionAst.Child.Visit(this); + break; + + } + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitUsingExpression(UsingExpressionAst usingExpressionAst) + { + WriteCommentsToAstPosition(usingExpressionAst); + _tw.Write("$using:"); + _tw.Write(((VariableExpressionAst)usingExpressionAst.SubExpression).VariablePath.UserPath); + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitUsingStatement(UsingStatementAst usingStatementAst) + { + WriteCommentsToAstPosition(usingStatementAst); + + _tw.Write("using "); + + switch (usingStatementAst.UsingStatementKind) + { + case UsingStatementKind.Assembly: + _tw.Write("assembly "); + break; + + case UsingStatementKind.Command: + _tw.Write("command "); + break; + + case UsingStatementKind.Module: + _tw.Write("module "); + break; + + case UsingStatementKind.Namespace: + _tw.Write("namespace "); + break; + + case UsingStatementKind.Type: + _tw.Write("type "); + break; + + default: + throw new ArgumentException($"Unknown using statement kind: '{usingStatementAst.UsingStatementKind}'"); + } + + if (usingStatementAst.ModuleSpecification != null) + { + _tw.Write("@{ "); + + Intersperse( + usingStatementAst.ModuleSpecification.KeyValuePairs, + (kvp) => + { + WriteCommentsToAstPosition(kvp.Item1); + kvp.Item1.Visit(this); + _tw.Write(" = "); + WriteCommentsToAstPosition(kvp.Item2); + kvp.Item2.Visit(this); + }, + () => { _tw.Write("; "); }); + + _tw.Write(" }"); + EndStatement(); + + return AstVisitAction.SkipChildren; + } + + if (usingStatementAst.Name != null) + { + usingStatementAst.Name.Visit(this); + } + + if (usingStatementAst.Alias != null) + { + _tw.Write(" = "); + usingStatementAst.Alias.Visit(this); + } + + EndStatement(); + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) + { + WriteCommentsToAstPosition(variableExpressionAst); + _tw.Write(variableExpressionAst.Splatted ? '@' : '$'); + _tw.Write(variableExpressionAst.VariablePath.UserPath); + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitWhileStatement(WhileStatementAst whileStatementAst) + { + WriteCommentsToAstPosition(whileStatementAst); + _tw.Write("while ("); + whileStatementAst.Condition.Visit(this); + _tw.Write(")"); + whileStatementAst.Body.Visit(this); + + return AstVisitAction.SkipChildren; + } + + private void WriteInlineParameters(IReadOnlyList parameters) + { + if (IsEmpty(parameters)) + { + return; + } + + foreach (ParameterAst parameterAst in parameters) + { + WriteInlineParameter(parameterAst); + } + } + + private void WriteInlineParameter(ParameterAst parameter) + { + foreach (AttributeBaseAst attribute in parameter.Attributes) + { + attribute.Visit(this); + } + + parameter.Name.Visit(this); + + if (parameter.DefaultValue != null) + { + _tw.Write(" = "); + parameter.DefaultValue.Visit(this); + } + } + + + private void WriteControlFlowStatement(string keyword, Ast childAst) + { + _tw.Write(keyword); + + if (childAst != null) + { + _tw.Write(' '); + childAst.Visit(this); + } + } + + private void WriteTypeName(ITypeName typeName) + { + switch (typeName) + { + case ArrayTypeName arrayTypeName: + WriteTypeName(arrayTypeName.ElementType); + if (arrayTypeName.Rank == 1) + { + _tw.Write("[]"); + } + else + { + _tw.Write('['); + for (int i = 1; i < arrayTypeName.Rank; i++) + { + _tw.Write(','); + } + _tw.Write(']'); + } + break; + + case GenericTypeName genericTypeName: + _tw.Write(genericTypeName.FullName); + _tw.Write('['); + + Intersperse( + genericTypeName.GenericArguments, + (gtn) => WriteTypeName(gtn), + () => _tw.Write(_comma)); + + _tw.Write(']'); + break; + + case TypeName simpleTypeName: + _tw.Write(simpleTypeName.FullName); + break; + + default: + throw new ArgumentException($"Unknown type name type: '{typeName.GetType().FullName}'"); + } + } + + private void WriteDoubleQuotedString(string strVal) + { + _tw.Write('"'); + + foreach (char c in strVal) + { + switch (c) + { + case '\0': + _tw.Write("`0"); + break; + + case '\a': + _tw.Write("`a"); + break; + + case '\b': + _tw.Write("`b"); + break; + + case '\f': + _tw.Write("`f"); + break; + + case '\n': + _tw.Write("`n"); + break; + + case '\r': + _tw.Write("`r"); + break; + + case '\t': + _tw.Write("`t"); + break; + + case '\v': + _tw.Write("`v"); + break; + + case '`': + _tw.Write("``"); + break; + + case '"': + _tw.Write("`\""); + break; + + case '$': + _tw.Write("`$"); + break; + + case '\u001b': + _tw.Write("`e"); + break; + + default: + if (c < 128) + { + _tw.Write(c); + break; + } + + _tw.Write("`u{"); + _tw.Write(((int)c).ToString("X")); + _tw.Write('}'); + break; + } + } + + _tw.Write('"'); + } + + private void WriteStatementBlock(IReadOnlyList statements, IReadOnlyList traps = null) + { + bool wroteTrap = false; + if (!IsEmpty(traps)) + { + wroteTrap = true; + foreach (TrapStatementAst trap in traps) + { + trap.Visit(this); + } + } + + if (!IsEmpty(statements)) + { + if (wroteTrap) + { + Newline(); + } + + statements[0].Visit(this); + StatementAst previousStatement = statements[0]; + + for (int i = 1; i < statements.Count; i++) + { + if (IsBlockStatement(previousStatement)) + { + _tw.Write(_newline); + } + Newline(); + statements[i].Visit(this); + previousStatement = statements[i]; + } + } + } + + private void WriteHashtableEntry(Tuple entry) + { + entry.Item1.Visit(this); + _tw.Write(" = "); + entry.Item2.Visit(this); + } + + private void BeginBlock() + { + Newline(); + _tw.Write('{'); + Indent(); + } + + private void EndBlock() + { + Dedent(); + _tw.Write('}'); + } + + private void Newline() + { + _tw.Write(_newline); + + for (int i = 0; i < _indent; i++) + { + _tw.Write(_indentStr); + } + } + + private void Newline(int count) + { + for (int i = 0; i < count - 1; i++) + { + _tw.Write(_newline); + } + + Newline(); + } + + private void EndStatement() + { + _tw.Write(_newline); + } + + private void Indent() + { + _indent++; + Newline(); + } + + private void Dedent() + { + _indent--; + Newline(); + } + + private void Intersperse(IReadOnlyList asts, string separator) + { + if (IsEmpty(asts)) + { + return; + } + + asts[0].Visit(this); + + for (int i = 1; i < asts.Count; i++) + { + _tw.Write(separator); + asts[i].Visit(this); + } + } + + private void Intersperse(IReadOnlyList asts, Action writeSeparator) + { + if (IsEmpty(asts)) + { + return; + } + + asts[0].Visit(this); + + for (int i = 1; i < asts.Count; i++) + { + writeSeparator(); + asts[i].Visit(this); + } + } + + private void Intersperse(IReadOnlyList astObjects, Action writeObject, Action writeSeparator) + { + if (IsEmpty(astObjects)) + { + return; + } + + writeObject(astObjects[0]); + + for (int i = 1; i < astObjects.Count; i++) + { + writeSeparator(); + writeObject(astObjects[i]); + } + } + + private void WriteCommentsToAstPosition(Ast ast) + { + if (_tokens == null) + { + return; + } + + Token currToken = _tokens[_tokenIndex]; + while (currToken.Extent.EndOffset < ast.Extent.StartOffset) + { + if (currToken.Kind == TokenKind.Comment) + { + _tw.Write(currToken.Text); + + if (currToken.Text.StartsWith("#")) + { + Newline(); + } + } + + _tokenIndex++; + currToken = _tokens[_tokenIndex]; + } + } + + private bool IsBlockStatement(StatementAst statementAst) + { + switch (statementAst) + { + case PipelineBaseAst _: + case ReturnStatementAst _: + case ThrowStatementAst _: + case ExitStatementAst _: + case BreakStatementAst _: + case ContinueStatementAst _: + return false; + + default: + return true; + } + } + + private string GetTokenString(TokenKind tokenKind) + { + switch (tokenKind) + { + case TokenKind.Ampersand: + return "&"; + + case TokenKind.And: + return "-and"; + + case TokenKind.AndAnd: + return "&&"; + + case TokenKind.As: + return "-as"; + + case TokenKind.Assembly: + return "assembly"; + + case TokenKind.AtCurly: + return "@{"; + + case TokenKind.AtParen: + return "@("; + + case TokenKind.Band: + return "-band"; + + case TokenKind.Base: + return "base"; + + case TokenKind.Begin: + return "begin"; + + case TokenKind.Bnot: + return "-bnot"; + + case TokenKind.Bor: + return "-bnor"; + + case TokenKind.Break: + return "break"; + + case TokenKind.Bxor: + return "-bxor"; + + case TokenKind.Catch: + return "catch"; + + case TokenKind.Ccontains: + return "-ccontains"; + + case TokenKind.Ceq: + return "-ceq"; + + case TokenKind.Cge: + return "-cge"; + + case TokenKind.Cgt: + return "-cgt"; + + case TokenKind.Cin: + return "-cin"; + + case TokenKind.Class: + return "class"; + + case TokenKind.Cle: + return "-cle"; + + case TokenKind.Clike: + return "-clike"; + + case TokenKind.Clt: + return "-clt"; + + case TokenKind.Cmatch: + return "-cmatch"; + + case TokenKind.Cne: + return "-cne"; + + case TokenKind.Cnotcontains: + return "-cnotcontains"; + + case TokenKind.Cnotin: + return "-cnotin"; + + case TokenKind.Cnotlike: + return "-cnotlike"; + + case TokenKind.Cnotmatch: + return "-cnotmatch"; + + case TokenKind.Colon: + return ":"; + + case TokenKind.ColonColon: + return "::"; + + case TokenKind.Comma: + return ","; + + case TokenKind.Configuration: + return "configuration"; + + case TokenKind.Continue: + return "continue"; + + case TokenKind.Creplace: + return "-creplace"; + + case TokenKind.Csplit: + return "-csplit"; + + case TokenKind.Data: + return "data"; + + case TokenKind.Define: + return "define"; + + case TokenKind.Divide: + return "/"; + + case TokenKind.DivideEquals: + return "/="; + + case TokenKind.Do: + return "do"; + + case TokenKind.DollarParen: + return "$("; + + case TokenKind.Dot: + return "."; + + case TokenKind.DotDot: + return ".."; + + case TokenKind.Dynamicparam: + return "dynamicparam"; + + case TokenKind.Else: + return "else"; + + case TokenKind.ElseIf: + return "elseif"; + + case TokenKind.End: + return "end"; + + case TokenKind.Enum: + return "enum"; + + case TokenKind.Equals: + return "="; + + case TokenKind.Exclaim: + return "!"; + + case TokenKind.Exit: + return "exit"; + + case TokenKind.Filter: + return "filter"; + + case TokenKind.Finally: + return "finally"; + + case TokenKind.For: + return "for"; + + case TokenKind.Foreach: + return "foreach"; + + case TokenKind.Format: + return "-f"; + + case TokenKind.From: + return "from"; + + case TokenKind.Function: + return "function"; + + case TokenKind.Hidden: + return "hidden"; + + case TokenKind.Icontains: + return "-contains"; + + case TokenKind.Ieq: + return "-eq"; + + case TokenKind.If: + return "if"; + + case TokenKind.Ige: + return "-ge"; + + case TokenKind.Igt: + return "-gt"; + + case TokenKind.Iin: + return "-in"; + + case TokenKind.Ile: + return "-le"; + + case TokenKind.Ilike: + return "-like"; + + case TokenKind.Ilt: + return "-lt"; + + case TokenKind.Imatch: + return "-match"; + + case TokenKind.In: + return "-in"; + + case TokenKind.Ine: + return "-ne"; + + case TokenKind.InlineScript: + return "inlinescript"; + + case TokenKind.Inotcontains: + return "-notcontains"; + + case TokenKind.Inotin: + return "-notin"; + + case TokenKind.Inotlike: + return "-notlike"; + + case TokenKind.Inotmatch: + return "-notmatch"; + + case TokenKind.Interface: + return "interface"; + + case TokenKind.Ireplace: + return "-replace"; + + case TokenKind.Is: + return "-is"; + + case TokenKind.IsNot: + return "-isnot"; + + case TokenKind.Isplit: + return "-split"; + + case TokenKind.Join: + return "-join"; + + case TokenKind.LBracket: + return "["; + + case TokenKind.LCurly: + return "{"; + + case TokenKind.LParen: + return "("; + + case TokenKind.Minus: + return "-"; + + case TokenKind.MinusEquals: + return "-="; + + case TokenKind.MinusMinus: + return "--"; + + case TokenKind.Module: + return "module"; + + case TokenKind.Multiply: + return "*"; + + case TokenKind.MultiplyEquals: + return "*="; + + case TokenKind.Namespace: + return "namespace"; + + case TokenKind.NewLine: + return Environment.NewLine; + + case TokenKind.Not: + return "-not"; + + case TokenKind.Or: + return "-or"; + + case TokenKind.OrOr: + return "||"; + + case TokenKind.Parallel: + return "parallel"; + + case TokenKind.Param: + return "param"; + + case TokenKind.Pipe: + return "|"; + + case TokenKind.Plus: + return "+"; + + case TokenKind.PlusEquals: + return "+="; + + case TokenKind.PlusPlus: + return "++"; + + case TokenKind.PostfixMinusMinus: + return "--"; + + case TokenKind.PostfixPlusPlus: + return "++"; + + case TokenKind.Private: + return "private"; + + case TokenKind.Process: + return "process"; + + case TokenKind.Public: + return "public"; + + case TokenKind.RBracket: + return "]"; + + case TokenKind.RCurly: + return "}"; + + case TokenKind.Rem: + return "%"; + + case TokenKind.RemainderEquals: + return "%="; + + case TokenKind.Return: + return "return"; + + case TokenKind.RParen: + return ")"; + + case TokenKind.Semi: + return ";"; + + case TokenKind.Sequence: + return "sequence"; + + case TokenKind.Shl: + return "-shl"; + + case TokenKind.Shr: + return "-shr"; + + case TokenKind.Static: + return "static"; + + case TokenKind.Switch: + return "switch"; + + case TokenKind.Throw: + return "throw"; + + case TokenKind.Trap: + return "trap"; + + case TokenKind.Try: + return "try"; + + case TokenKind.Until: + return "until"; + + case TokenKind.Using: + return "using"; + + case TokenKind.Var: + return "var"; + + case TokenKind.While: + return "while"; + + case TokenKind.Workflow: + return "workflow"; + + case TokenKind.Xor: + return "-xor"; + + default: + throw new ArgumentException($"Unable to stringify token kind '{tokenKind}'"); + } + } + + private char GetStreamIndicator(RedirectionStream stream) + { + switch (stream) + { + case RedirectionStream.All: + return '*'; + + case RedirectionStream.Debug: + return '5'; + + case RedirectionStream.Error: + return '2'; + + case RedirectionStream.Information: + return '6'; + + case RedirectionStream.Output: + return '1'; + + case RedirectionStream.Verbose: + return '4'; + + case RedirectionStream.Warning: + return '3'; + + default: + throw new ArgumentException($"Unknown redirection stream: '{stream}'"); + } + } + + private static bool IsEmpty(IReadOnlyCollection collection) + { + return collection == null + || collection.Count == 0; + } + } + + public class ParseException : Exception + { + public ParseException(IReadOnlyList parseErrors) + : base("A parse error was encountered while parsing the input script") + { + ParseErrors = parseErrors; + } + + public IReadOnlyList ParseErrors { get; } + } +} diff --git a/Modules/Microsoft.PowerShell.AstTools/test/PrettyPrintingTests.cs b/Modules/Microsoft.PowerShell.AstTools/test/PrettyPrintingTests.cs new file mode 100644 index 0000000..a4b1674 --- /dev/null +++ b/Modules/Microsoft.PowerShell.AstTools/test/PrettyPrintingTests.cs @@ -0,0 +1,751 @@ +using Microsoft.PowerShell.AstTools; +using System; +using System.IO; +using System.Management.Automation.Language; +using Xunit; + +namespace test +{ + public class PrettyPrinterTests + { + private readonly StringPrettyPrinter _pp; + + public PrettyPrinterTests() + { + _pp = new StringPrettyPrinter(); + } + + [Theory()] + [InlineData("$x")] + [InlineData("- $x")] + [InlineData("- 1")] + [InlineData("-1")] + [InlineData("$x++")] + [InlineData("$i--")] + [InlineData("--$i")] + [InlineData("++$i")] + [InlineData("- --$i")] + [InlineData("-not $true")] + [InlineData("$x + $y")] + [InlineData("'{0}' -f 'Hi'")] + [InlineData("'1,2,3' -split ','")] + [InlineData("1, 2, 3 -join ' '")] + [InlineData("Get-ChildItem")] + [InlineData("gci >test.txt")] + [InlineData("gci >test.txt 2>errs.txt")] + [InlineData("gci 2>&1")] + [InlineData("Get-ChildItem -Recurse -Path ./here")] + [InlineData("Get-ChildItem -Recurse -Path \"$PWD\\there\"")] + [InlineData("exit 1")] + [InlineData("return ($result + 3)")] + [InlineData("throw [System.Exception]'Bad'")] + [InlineData("break outer")] + [InlineData("continue anotherLoop")] + [InlineData("3 + $(Get-Random)")] + [InlineData("'banana cake'")] + [InlineData("'banana''s cake'")] + [InlineData("Get-ChildItem | ? Name -like 'banana' | % FullPath")] + [InlineData("[type]::GetThings()")] + [InlineData("[type]::Member")] + [InlineData("$x.DoThings($x, 8, $y)")] + [InlineData("$x.Property")] + [InlineData("$x.$property")] + [InlineData("[type]::$property")] + [InlineData("$type::$property")] + [InlineData("[type]::$method(1, 2, 'x')")] + [InlineData("$k.$method(1, 2, 'x')")] + [InlineData(@"""I like ducks""")] + [InlineData(@"""I`nlike`nducks""")] + [InlineData(@"""I`tlike`n`rducks""")] + [InlineData(@"$x[0]")] + [InlineData(@"$x[$i + 1]")] + [InlineData(@"$x.Item[$i + 1]")] + [InlineData(@"1, 2, 3")] + [InlineData(@"1, 'Hi', 3")] + [InlineData(@"@(1, 'Hi', 3)")] +#if PS7 + [InlineData("Invoke-Expression 'runCommand' &")] + [InlineData(@"""I`e[31mlike`e[0mducks""")] + [InlineData("1 && 2")] + [InlineData("sudo apt update && sudo apt upgrade")] + [InlineData("firstthing && secondthing &")] + [InlineData("Get-Item ./thing || $(throw 'Bad')")] + [InlineData(@"$true ? 'true' : 'false'")] +#endif + public void TestPrettyPrintingIdempotentForSimpleStatements(string input) + { + AssertPrettyPrintedStatementIdentical(input); + } + + [Fact] + public void TestEmptyHashtable() + { + string script = "@{}"; + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestSimpleHashtable() + { + string script = @" +@{ + One = 'One' + Two = $x + $banana = 7 +} +"; + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestComplexHashtable() + { + string script = @" +@{ + One = @{ + SubOne = 1 + SubTwo = { + $x + } + } + Two = $x + $banana = @(7, 3, 4) +} +"; + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestFunction() + { + string script = @" +function Test-Function +{ + Write-Host 'Hello!' +} +"; + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestAdvancedFunction() + { + string script = @" +function Test-Greeting +{ + [CmdletBinding()] + param( + [Parameter()] + [string] + $Greeting + ) + + Write-Host $Greeting +} +"; + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestScriptBlock() + { + string script = @" +{ + $args[0] + 2 +}"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestScriptBlockInvocation() + { + string script = @" +& { + $args[0] + 2 +}"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestScriptBlockDotSource() + { + string script = @" +. { + $args[0] + 2 +}"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestScriptBlockEmptyParams() + { + string script = @" +{ + param() +}"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestScriptBlockParams() + { + string script = @" +{ + param( + $String, + + $Switch + ) +}"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestScriptBlockAttributedParams() + { + string script = @" +{ + param( + [Parameter()] + [string] + $String, + + [Parameter()] + [switch] + $Switch + ) +}"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestScriptBlockParamAttributesWithArguments() + { + string script = @" +{ + param( + [Parameter(Mandatory)] + [string] + $String, + + [Parameter(Mandatory = $true)] + [AnotherAttribute(1, 2)] + [ThirdAttribute(1, 2, Fun = $true)] + [ThirdAttribute(1, 2, Fun)] + [switch] + $Switch + ) +}"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestWhileLoop() + { + string script = @" +while ($i -lt 10) +{ + $i++ +} +"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestForeachLoop() + { + string script = @" +foreach ($n in 1, 2, 3) +{ + Write-Output ($n + 1) +} +"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestForLoop() + { + string script = @" +for ($i = 0; $i -lt $args.Count; $i++) +{ + $args[$i] +} +"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestSwitchCase() + { + string script = @" +switch ($x) +{ + 1 + { + 'One' + break + } + + 2 + { + 'Two' + break + } + + 3 + { + 'Three' + break + } +} +"; + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestDoWhileLoop() + { + string script = @" +do +{ + $x++ +} while ($x -lt 10) +"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestDoUntilLoop() + { + string script = @" +do +{ + $x++ +} until ($x -eq 10) +"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestIf() + { + string script = @" +if ($x) +{ + $x.Fun() +} +"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestElseIf() + { + string script = @" +if ($x) +{ + $x.Fun() +} +elseif ($y) +{ + $y.NoFun() +} +"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestElse() + { + string script = @" +if ($x) +{ + $x.Fun() +} +else +{ + 'nothing' +} +"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestFullIfElse() + { + string script = @" +if ($x) +{ + $x.Fun() +} +elseif ($y) +{ + $y.NoFun() +} +else +{ + 'nothing' +} +"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestTryCatch() + { + string script = @" +try +{ + Write-Error 'Bad' -ErrorAction Stop +} +catch +{ + $_ +} +"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestTryCatchWithType() + { + string script = @" +try +{ + Write-Error 'Bad' -ErrorAction Stop +} +catch [System.Exception] +{ + $_ +} +"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestTryFinally() + { + string script = @" +try +{ + Write-Error 'Bad' -ErrorAction Stop +} +finally +{ + Write-Host 'done' +} +"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestTryCatchFinally() + { + string script = @" +try +{ + Write-Error 'Bad' -ErrorAction Stop +} +catch +{ + $_ +} +finally +{ + Write-Host 'done' +} +"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestSimpleClass() + { + string script = @" +class Duck +{ +} +"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestClassProperty() + { + string script = @" +class Duck +{ + [string]$Name +} +"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestClassMethod() + { + string script = @" +class Duck +{ + [string]GetGreeting([string]$Name) + { + return ""Hi $Name"" + } +} +"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestClassConstructor() + { + string script = @" +class Duck +{ + Duck($name) + { + $this.Name = $name + } +} +"; + + AssertPrettyPrintedStatementIdentical(script); + } + + + [Fact] + public void TestClassConstructorWithBaseClass() + { + string script = @" +class MyHashtable : hashtable +{ + MyHashtable([int]$count) : base($count) + { + } +} +"; + + AssertPrettyPrintedStatementIdentical(script); + } + + [Fact] + public void TestUsingNamespace() + { + string script = "using namespace System.Collections.Generic\n"; + AssertPrettyPrintedUsingStatementIdentical(script); + } + + [Fact] + public void TestUsingModule() + { + string script = "using module PrettyPrintingTestModule\n"; + using (ModuleContext.Create("PrettyPrintingTestModule", new Version(1, 0))) + { + AssertPrettyPrintedUsingStatementIdentical(script); + } + } + + [Fact] + public void TestUsingModuleWithHashtable() + { + string script = "using module @{ ModuleName = 'PrettyPrintingTestModule'; ModuleVersion = '1.18' }\n"; + using (ModuleContext.Create("PrettyPrintingTestModule", new Version(1, 18))) + { + AssertPrettyPrintedUsingStatementIdentical(script); + } + } + + [Fact] + public void TestFullScript1() + { + string script = @" +[CmdletBinding(DefaultParameterSetName = ""BuildOne"")] +param( + [Parameter(ParameterSetName = ""BuildAll"")] + [switch] + $All, + + [Parameter(ParameterSetName = ""BuildOne"")] + [ValidateRange(3, 7)] + [int] + $PSVersion = $PSVersionTable.PSVersion.Major, + + [Parameter(ParameterSetName = ""BuildOne"")] + [Parameter(ParameterSetName = ""BuildAll"")] + [ValidateSet(""Debug"", ""Release"")] + [string] + $Configuration = ""Debug"", + + [Parameter(ParameterSetName = ""BuildDocumentation"")] + [switch] + $Documentation, + + [Parameter(ParameterSetName = 'BuildAll')] + [Parameter(ParameterSetName = 'BuildOne')] + [switch] + $Clobber, + + [Parameter(Mandatory = $true, ParameterSetName = 'Clean')] + [switch] + $Clean, + + [Parameter(Mandatory = $true, ParameterSetName = 'Test')] + [switch] + $Test, + + [Parameter(ParameterSetName = 'Test')] + [switch] + $InProcess, + + [Parameter(ParameterSetName = 'Bootstrap')] + [switch] + $Bootstrap +) + +begin +{ + if ($PSVersion -gt 6) + { + Write-Host ""Building PowerShell Core version"" + $PSVersion = 6 + } +} + +end +{ + Import-Module -Force (Join-Path $PSScriptRoot build.psm1) + if ($Clean -or $Clobber) + { + Remove-Build + if ($PSCmdlet.ParameterSetName -eq ""Clean"") + { + return + } + } + + $setName = $PSCmdlet.ParameterSetName + switch ($setName) + { + ""BuildAll"" + { + Start-ScriptAnalyzerBuild -All -Configuration $Configuration + } + + ""BuildDocumentation"" + { + Start-ScriptAnalyzerBuild -Documentation + } + + ""BuildOne"" + { + $buildArgs = @{ + PSVersion = $PSVersion + Configuration = $Configuration + } + Start-ScriptAnalyzerBuild @buildArgs + } + + ""Bootstrap"" + { + Install-DotNet + return + } + + ""Test"" + { + Test-ScriptAnalyzer -InProcess:$InProcess + return + } + + default + { + throw ""Unexpected parameter set '$setName'"" + } + } +} +"; + + AssertPrettyPrintedScriptIdentical(script); + } + + private void AssertPrettyPrintedStatementIdentical(string input) + { + Assert.Equal(NormalizeScript(input), NormalizeScript(_pp.PrettyPrintInput(input))); + } + + private void AssertPrettyPrintedUsingStatementIdentical(string input) + { + Assert.Equal(NormalizeScript(input), NormalizeScript(_pp.PrettyPrintInput(input))); + } + + private void AssertPrettyPrintedScriptIdentical(string input) + { + Assert.Equal(NormalizeScript(input), NormalizeScript(_pp.PrettyPrintInput(input))); + } + + private static string NormalizeScript(string input) + { + return input.Trim().Replace(Environment.NewLine, "\n"); + } + } + + internal class ModuleContext : IDisposable + { + public static ModuleContext Create(string moduleName, Version moduleVersion) + { + string tmpDirPath = Path.GetTempPath(); + Directory.CreateDirectory(tmpDirPath); + string modulePath = Path.Combine(tmpDirPath, moduleName); + Directory.CreateDirectory(modulePath); + string manifestPath = Path.Combine(modulePath, $"{moduleName}.psd1"); + File.WriteAllText(manifestPath, $"@{{ ModuleVersion = '{moduleVersion}' }}"); + + string oldPSModulePath = Environment.GetEnvironmentVariable("PSModulePath"); + Environment.SetEnvironmentVariable("PSModulePath", tmpDirPath); + + return new ModuleContext(modulePath, oldPSModulePath); + } + + private readonly string _psModulePath; + + private readonly string _modulePath; + + public ModuleContext( + string modulePath, + string psModulePath) + { + _modulePath = modulePath; + _psModulePath = psModulePath; + } + + public void Dispose() + { + Directory.Delete(_modulePath, recursive: true); + Environment.SetEnvironmentVariable("PSModulePath", _psModulePath); + } + } +} diff --git a/Modules/Microsoft.PowerShell.AstTools/test/test.csproj b/Modules/Microsoft.PowerShell.AstTools/test/test.csproj new file mode 100644 index 0000000..a88bb47 --- /dev/null +++ b/Modules/Microsoft.PowerShell.AstTools/test/test.csproj @@ -0,0 +1,31 @@ + + + + netcoreapp3.1;net471 + false + + + + $(DefineConstants);PS7 + + + + + + + + + + + + + + + + + + + + + +