Skip to content

Commit 8ba2d97

Browse files
rjmholtbergmeister
andauthored
Use AST to determine SupportsShouldProcess when an error is thrown (#1397)
* Use AST to determine SupportsShouldProcess when an error is thrown * Update Rules/UseShouldProcessCorrectly.cs Co-Authored-By: Christoph Bergmeister [MVP] <[email protected]> * Reuse SafeGetValue implementation for PS3 * Fix settings tests * Fix method name * Update Rules/UseShouldProcessCorrectly.cs Co-Authored-By: Christoph Bergmeister [MVP] <[email protected]> * Update Rules/UseShouldProcessCorrectly.cs Co-Authored-By: Christoph Bergmeister [MVP] <[email protected]> Co-authored-by: Christoph Bergmeister [MVP] <[email protected]>
1 parent 7393a81 commit 8ba2d97

File tree

4 files changed

+264
-212
lines changed

4 files changed

+264
-212
lines changed

Engine/Helper.cs

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1853,6 +1853,188 @@ public Version GetPSVersion()
18531853
return psVersionTable == null ? null : psVersionTable.PSVersion;
18541854
}
18551855

1856+
/// <summary>
1857+
/// Evaluates all statically evaluable, side-effect-free expressions under an
1858+
/// expression AST to return a value.
1859+
/// Throws if an expression cannot be safely evaluated.
1860+
/// Attempts to replicate the GetSafeValue() method on PowerShell AST methods from PSv5.
1861+
/// </summary>
1862+
/// <param name="exprAst">The expression AST to try to evaluate.</param>
1863+
/// <returns>The .NET value represented by the PowerShell expression.</returns>
1864+
public static object GetSafeValueFromExpressionAst(ExpressionAst exprAst)
1865+
{
1866+
switch (exprAst)
1867+
{
1868+
case ConstantExpressionAst constExprAst:
1869+
// Note, this parses top-level command invocations as bareword strings
1870+
// However, forbidding this causes hashtable parsing to fail
1871+
// It is probably not worth the complexity to isolate this case
1872+
return constExprAst.Value;
1873+
1874+
case VariableExpressionAst varExprAst:
1875+
// $true and $false are VariableExpressionAsts, so look for them here
1876+
switch (varExprAst.VariablePath.UserPath.ToLowerInvariant())
1877+
{
1878+
case "true":
1879+
return true;
1880+
1881+
case "false":
1882+
return false;
1883+
1884+
case "null":
1885+
return null;
1886+
1887+
default:
1888+
throw CreateInvalidDataExceptionFromAst(varExprAst);
1889+
}
1890+
1891+
case ArrayExpressionAst arrExprAst:
1892+
1893+
// Most cases are handled by the inner array handling,
1894+
// but we may have an empty array
1895+
if (arrExprAst.SubExpression?.Statements == null)
1896+
{
1897+
throw CreateInvalidDataExceptionFromAst(arrExprAst);
1898+
}
1899+
1900+
if (arrExprAst.SubExpression.Statements.Count == 0)
1901+
{
1902+
return new object[0];
1903+
}
1904+
1905+
var listComponents = new List<object>();
1906+
// Arrays can either be array expressions (1, 2, 3) or array literals with statements @(1 `n 2 `n 3)
1907+
// Or they can be a combination of these
1908+
// We go through each statement (line) in an array and read the whole subarray
1909+
// This will also mean that @(1; 2) is parsed as an array of two elements, but there's not much point defending against this
1910+
foreach (StatementAst statement in arrExprAst.SubExpression.Statements)
1911+
{
1912+
if (!(statement is PipelineAst pipelineAst))
1913+
{
1914+
throw CreateInvalidDataExceptionFromAst(arrExprAst);
1915+
}
1916+
1917+
ExpressionAst pipelineExpressionAst = pipelineAst.GetPureExpression();
1918+
if (pipelineExpressionAst == null)
1919+
{
1920+
throw CreateInvalidDataExceptionFromAst(arrExprAst);
1921+
}
1922+
1923+
object arrayValue = GetSafeValueFromExpressionAst(pipelineExpressionAst);
1924+
// We might hit arrays like @(\n1,2,3\n4,5,6), which the parser sees as two statements containing array expressions
1925+
if (arrayValue is object[] subArray)
1926+
{
1927+
listComponents.AddRange(subArray);
1928+
continue;
1929+
}
1930+
1931+
listComponents.Add(arrayValue);
1932+
}
1933+
return listComponents.ToArray();
1934+
1935+
1936+
case ArrayLiteralAst arrLiteralAst:
1937+
return GetSafeValuesFromArrayAst(arrLiteralAst);
1938+
1939+
case HashtableAst hashtableAst:
1940+
return GetSafeValueFromHashtableAst(hashtableAst);
1941+
1942+
default:
1943+
// Other expression types are too complicated or fundamentally unsafe
1944+
throw CreateInvalidDataExceptionFromAst(exprAst);
1945+
}
1946+
}
1947+
1948+
/// <summary>
1949+
/// Create a hashtable value from a PowerShell AST representing one,
1950+
/// provided that the PowerShell expression is statically evaluable and safe.
1951+
/// </summary>
1952+
/// <param name="hashtableAst">The PowerShell representation of the hashtable value.</param>
1953+
/// <returns>The Hashtable as a hydrated .NET value.</returns>
1954+
internal static Hashtable GetSafeValueFromHashtableAst(HashtableAst hashtableAst)
1955+
{
1956+
if (hashtableAst == null)
1957+
{
1958+
throw new ArgumentNullException(nameof(hashtableAst));
1959+
}
1960+
1961+
if (hashtableAst.KeyValuePairs == null)
1962+
{
1963+
throw CreateInvalidDataExceptionFromAst(hashtableAst);
1964+
}
1965+
1966+
var hashtable = new Hashtable();
1967+
foreach (Tuple<ExpressionAst, StatementAst> entry in hashtableAst.KeyValuePairs)
1968+
{
1969+
// Get the key
1970+
object key = GetSafeValueFromExpressionAst(entry.Item1);
1971+
if (key == null)
1972+
{
1973+
throw CreateInvalidDataExceptionFromAst(entry.Item1);
1974+
}
1975+
1976+
// Get the value
1977+
ExpressionAst valueExprAst = (entry.Item2 as PipelineAst)?.GetPureExpression();
1978+
if (valueExprAst == null)
1979+
{
1980+
throw CreateInvalidDataExceptionFromAst(entry.Item2);
1981+
}
1982+
1983+
// Add the key/value entry into the hydrated hashtable
1984+
hashtable[key] = GetSafeValueFromExpressionAst(valueExprAst);
1985+
}
1986+
1987+
return hashtable;
1988+
}
1989+
1990+
/// <summary>
1991+
/// Process a PowerShell array literal with statically evaluable/safe contents
1992+
/// into a .NET value.
1993+
/// </summary>
1994+
/// <param name="arrLiteralAst">The PowerShell array AST to turn into a value.</param>
1995+
/// <returns>The .NET value represented by PowerShell syntax.</returns>
1996+
private static object[] GetSafeValuesFromArrayAst(ArrayLiteralAst arrLiteralAst)
1997+
{
1998+
if (arrLiteralAst == null)
1999+
{
2000+
throw new ArgumentNullException(nameof(arrLiteralAst));
2001+
}
2002+
2003+
if (arrLiteralAst.Elements == null)
2004+
{
2005+
throw CreateInvalidDataExceptionFromAst(arrLiteralAst);
2006+
}
2007+
2008+
var elements = new List<object>();
2009+
foreach (ExpressionAst exprAst in arrLiteralAst.Elements)
2010+
{
2011+
elements.Add(GetSafeValueFromExpressionAst(exprAst));
2012+
}
2013+
2014+
return elements.ToArray();
2015+
}
2016+
2017+
private static InvalidDataException CreateInvalidDataExceptionFromAst(Ast ast)
2018+
{
2019+
if (ast == null)
2020+
{
2021+
throw new ArgumentNullException(nameof(ast));
2022+
}
2023+
2024+
return CreateInvalidDataException(ast.Extent);
2025+
}
2026+
2027+
private static InvalidDataException CreateInvalidDataException(IScriptExtent extent)
2028+
{
2029+
return new InvalidDataException(string.Format(
2030+
CultureInfo.CurrentCulture,
2031+
Strings.WrongValueFormat,
2032+
extent.StartLineNumber,
2033+
extent.StartColumnNumber,
2034+
extent.File ?? ""));
2035+
}
2036+
2037+
18562038
#endregion Methods
18572039
}
18582040

Engine/Settings.cs

Lines changed: 1 addition & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ private void parseSettingsFile(string settingsFilePath)
475475
{
476476
// ideally we should use HashtableAst.SafeGetValue() but since
477477
// it is not available on PSv3, we resort to our own narrow implementation.
478-
hashtable = GetSafeValueFromHashtableAst(hashTableAst);
478+
hashtable = Helper.GetSafeValueFromHashtableAst(hashTableAst);
479479
}
480480
catch (InvalidOperationException e)
481481
{
@@ -494,187 +494,6 @@ private void parseSettingsFile(string settingsFilePath)
494494
parseSettingsHashtable(hashtable);
495495
}
496496

497-
/// <summary>
498-
/// Evaluates all statically evaluable, side-effect-free expressions under an
499-
/// expression AST to return a value.
500-
/// Throws if an expression cannot be safely evaluated.
501-
/// Attempts to replicate the GetSafeValue() method on PowerShell AST methods from PSv5.
502-
/// </summary>
503-
/// <param name="exprAst">The expression AST to try to evaluate.</param>
504-
/// <returns>The .NET value represented by the PowerShell expression.</returns>
505-
private static object GetSafeValueFromExpressionAst(ExpressionAst exprAst)
506-
{
507-
switch (exprAst)
508-
{
509-
case ConstantExpressionAst constExprAst:
510-
// Note, this parses top-level command invocations as bareword strings
511-
// However, forbidding this causes hashtable parsing to fail
512-
// It is probably not worth the complexity to isolate this case
513-
return constExprAst.Value;
514-
515-
case VariableExpressionAst varExprAst:
516-
// $true and $false are VariableExpressionAsts, so look for them here
517-
switch (varExprAst.VariablePath.UserPath.ToLowerInvariant())
518-
{
519-
case "true":
520-
return true;
521-
522-
case "false":
523-
return false;
524-
525-
case "null":
526-
return null;
527-
528-
default:
529-
throw CreateInvalidDataExceptionFromAst(varExprAst);
530-
}
531-
532-
case ArrayExpressionAst arrExprAst:
533-
534-
// Most cases are handled by the inner array handling,
535-
// but we may have an empty array
536-
if (arrExprAst.SubExpression?.Statements == null)
537-
{
538-
throw CreateInvalidDataExceptionFromAst(arrExprAst);
539-
}
540-
541-
if (arrExprAst.SubExpression.Statements.Count == 0)
542-
{
543-
return new object[0];
544-
}
545-
546-
var listComponents = new List<object>();
547-
// Arrays can either be array expressions (1, 2, 3) or array literals with statements @(1 `n 2 `n 3)
548-
// Or they can be a combination of these
549-
// We go through each statement (line) in an array and read the whole subarray
550-
// This will also mean that @(1; 2) is parsed as an array of two elements, but there's not much point defending against this
551-
foreach (StatementAst statement in arrExprAst.SubExpression.Statements)
552-
{
553-
if (!(statement is PipelineAst pipelineAst))
554-
{
555-
throw CreateInvalidDataExceptionFromAst(arrExprAst);
556-
}
557-
558-
ExpressionAst pipelineExpressionAst = pipelineAst.GetPureExpression();
559-
if (pipelineExpressionAst == null)
560-
{
561-
throw CreateInvalidDataExceptionFromAst(arrExprAst);
562-
}
563-
564-
object arrayValue = GetSafeValueFromExpressionAst(pipelineExpressionAst);
565-
// We might hit arrays like @(\n1,2,3\n4,5,6), which the parser sees as two statements containing array expressions
566-
if (arrayValue is object[] subArray)
567-
{
568-
listComponents.AddRange(subArray);
569-
continue;
570-
}
571-
572-
listComponents.Add(arrayValue);
573-
}
574-
return listComponents.ToArray();
575-
576-
577-
case ArrayLiteralAst arrLiteralAst:
578-
return GetSafeValuesFromArrayAst(arrLiteralAst);
579-
580-
case HashtableAst hashtableAst:
581-
return GetSafeValueFromHashtableAst(hashtableAst);
582-
583-
default:
584-
// Other expression types are too complicated or fundamentally unsafe
585-
throw CreateInvalidDataExceptionFromAst(exprAst);
586-
}
587-
}
588-
589-
/// <summary>
590-
/// Process a PowerShell array literal with statically evaluable/safe contents
591-
/// into a .NET value.
592-
/// </summary>
593-
/// <param name="arrLiteralAst">The PowerShell array AST to turn into a value.</param>
594-
/// <returns>The .NET value represented by PowerShell syntax.</returns>
595-
private static object[] GetSafeValuesFromArrayAst(ArrayLiteralAst arrLiteralAst)
596-
{
597-
if (arrLiteralAst == null)
598-
{
599-
throw new ArgumentNullException(nameof(arrLiteralAst));
600-
}
601-
602-
if (arrLiteralAst.Elements == null)
603-
{
604-
throw CreateInvalidDataExceptionFromAst(arrLiteralAst);
605-
}
606-
607-
var elements = new List<object>();
608-
foreach (ExpressionAst exprAst in arrLiteralAst.Elements)
609-
{
610-
elements.Add(GetSafeValueFromExpressionAst(exprAst));
611-
}
612-
613-
return elements.ToArray();
614-
}
615-
616-
/// <summary>
617-
/// Create a hashtable value from a PowerShell AST representing one,
618-
/// provided that the PowerShell expression is statically evaluable and safe.
619-
/// </summary>
620-
/// <param name="hashtableAst">The PowerShell representation of the hashtable value.</param>
621-
/// <returns>The Hashtable as a hydrated .NET value.</returns>
622-
private static Hashtable GetSafeValueFromHashtableAst(HashtableAst hashtableAst)
623-
{
624-
if (hashtableAst == null)
625-
{
626-
throw new ArgumentNullException(nameof(hashtableAst));
627-
}
628-
629-
if (hashtableAst.KeyValuePairs == null)
630-
{
631-
throw CreateInvalidDataExceptionFromAst(hashtableAst);
632-
}
633-
634-
var hashtable = new Hashtable();
635-
foreach (Tuple<ExpressionAst, StatementAst> entry in hashtableAst.KeyValuePairs)
636-
{
637-
// Get the key
638-
object key = GetSafeValueFromExpressionAst(entry.Item1);
639-
if (key == null)
640-
{
641-
throw CreateInvalidDataExceptionFromAst(entry.Item1);
642-
}
643-
644-
// Get the value
645-
ExpressionAst valueExprAst = (entry.Item2 as PipelineAst)?.GetPureExpression();
646-
if (valueExprAst == null)
647-
{
648-
throw CreateInvalidDataExceptionFromAst(entry.Item2);
649-
}
650-
651-
// Add the key/value entry into the hydrated hashtable
652-
hashtable[key] = GetSafeValueFromExpressionAst(valueExprAst);
653-
}
654-
655-
return hashtable;
656-
}
657-
658-
private static InvalidDataException CreateInvalidDataExceptionFromAst(Ast ast)
659-
{
660-
if (ast == null)
661-
{
662-
throw new ArgumentNullException(nameof(ast));
663-
}
664-
665-
return CreateInvalidDataException(ast.Extent);
666-
}
667-
668-
private static InvalidDataException CreateInvalidDataException(IScriptExtent extent)
669-
{
670-
return new InvalidDataException(string.Format(
671-
CultureInfo.CurrentCulture,
672-
Strings.WrongValueFormat,
673-
extent.StartLineNumber,
674-
extent.StartColumnNumber,
675-
extent.File ?? ""));
676-
}
677-
678497
private static bool IsBuiltinSettingPreset(object settingPreset)
679498
{
680499
var preset = settingPreset as string;

0 commit comments

Comments
 (0)