Skip to content

Commit 3e6987b

Browse files
bergmeisterChristoph Bergmeister
and
Christoph Bergmeister
authored
Formatter: Recycle parsed AST and tokens in between rule invocations when no correction was applied to improve performance (#1462)
* Recycle Parse result for formatter, if a rule does not return a DiagnosticRecord and therefore does not require re-parsing * fix issue where the parsing stage can only be skipped in the first loop iteration of the Fix API * Adapt LibraryUsage.tests.ps1 * Remove unnecessary call to ToList() and Count() where object is already a list * re-trigger ci Co-authored-by: Christoph Bergmeister <christoph.bergmeister@bjss.com>
1 parent 53ece47 commit 3e6987b

File tree

4 files changed

+40
-24
lines changed

4 files changed

+40
-24
lines changed

Engine/Commands/InvokeScriptAnalyzerCommand.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ private void ProcessInput()
422422
}
423423
else if (String.Equals(this.ParameterSetName, "ScriptDefinition", StringComparison.OrdinalIgnoreCase))
424424
{
425-
diagnosticsList = ScriptAnalyzer.Instance.AnalyzeScriptDefinition(scriptDefinition);
425+
diagnosticsList = ScriptAnalyzer.Instance.AnalyzeScriptDefinition(scriptDefinition, out _, out _);
426426
WriteToOutput(diagnosticsList);
427427
}
428428
}

Engine/Formatter.cs

+6-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections;
66
using System.Management.Automation;
7+
using System.Management.Automation.Language;
78

89
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer
910
{
@@ -46,6 +47,9 @@ public static string Format<TCmdlet>(
4647
};
4748

4849
var text = new EditableText(scriptDefinition);
50+
ScriptBlockAst scriptAst = null;
51+
Token[] scriptTokens = null;
52+
bool skipParsing = false;
4953
foreach (var rule in ruleOrder)
5054
{
5155
if (!settings.RuleArguments.ContainsKey(rule))
@@ -57,9 +61,8 @@ public static string Format<TCmdlet>(
5761
ScriptAnalyzer.Instance.UpdateSettings(currentSettings);
5862
ScriptAnalyzer.Instance.Initialize(cmdlet, null, null, null, null, true, false);
5963

60-
Range updatedRange;
61-
bool fixesWereApplied;
62-
text = ScriptAnalyzer.Instance.Fix(text, range, out updatedRange, out fixesWereApplied, skipVariableAnalysis: true);
64+
text = ScriptAnalyzer.Instance.Fix(text, range, skipParsing, out Range updatedRange, out bool fixesWereApplied, ref scriptAst, ref scriptTokens, skipVariableAnalysis: true);
65+
skipParsing = !fixesWereApplied;
6366
range = updatedRange;
6467
}
6568

Engine/ScriptAnalyzer.cs

+32-19
Original file line numberDiff line numberDiff line change
@@ -1478,8 +1478,7 @@ public IEnumerable<DiagnosticRecord> AnalyzeAndFixPath(string path, Func<string,
14781478
if (shouldProcess(scriptFilePath, $"Analyzing and fixing file {scriptFilePath}"))
14791479
{
14801480
var fileEncoding = GetFileEncoding(scriptFilePath);
1481-
bool fixesWereApplied;
1482-
var scriptFileContentWithFixes = Fix(File.ReadAllText(scriptFilePath, fileEncoding), out fixesWereApplied);
1481+
var scriptFileContentWithFixes = Fix(File.ReadAllText(scriptFilePath, fileEncoding), out bool fixesWereApplied);
14831482
if (fixesWereApplied)
14841483
{
14851484
File.WriteAllText(scriptFilePath, scriptFileContentWithFixes, fileEncoding);
@@ -1497,12 +1496,15 @@ public IEnumerable<DiagnosticRecord> AnalyzeAndFixPath(string path, Func<string,
14971496
/// <summary>
14981497
/// Analyzes a script definition in the form of a string input
14991498
/// </summary>
1500-
/// <param name="scriptDefinition">The script to be analyzed</param>
1499+
/// <param name="scriptDefinition">The script to be analyzed.</param>
1500+
/// <param name="scriptAst">Parsed AST of <paramref name="scriptDefinition"/>.</param>
1501+
/// <param name="scriptTokens">Parsed tokens of <paramref name="scriptDefinition"/.></param>
1502+
/// <param name="skipVariableAnalysis">Whether variable analysis can be skipped (applicable if rules do not use variable analysis APIs).</param>
15011503
/// <returns></returns>
1502-
public IEnumerable<DiagnosticRecord> AnalyzeScriptDefinition(string scriptDefinition, bool skipVariableAnalysis = false)
1504+
public IEnumerable<DiagnosticRecord> AnalyzeScriptDefinition(string scriptDefinition, out ScriptBlockAst scriptAst, out Token[] scriptTokens, bool skipVariableAnalysis = false)
15031505
{
1504-
ScriptBlockAst scriptAst = null;
1505-
Token[] scriptTokens = null;
1506+
scriptAst = null;
1507+
scriptTokens = null;
15061508
ParseError[] errors = null;
15071509

15081510
this.outputWriter.WriteVerbose(string.Format(CultureInfo.CurrentCulture, Strings.VerboseScriptDefinitionMessage));
@@ -1546,7 +1548,7 @@ public IEnumerable<DiagnosticRecord> AnalyzeScriptDefinition(string scriptDefini
15461548
/// Fix the violations in the given script text.
15471549
/// </summary>
15481550
/// <param name="scriptDefinition">The script text to be fixed.</param>
1549-
/// <param name="updatedRange">Whether any warnings were fixed.</param>
1551+
/// <param name="fixesWereApplied">Whether any warnings were fixed.</param>
15501552
/// <returns>The fixed script text.</returns>
15511553
public string Fix(string scriptDefinition, out bool fixesWereApplied)
15521554
{
@@ -1555,20 +1557,24 @@ public string Fix(string scriptDefinition, out bool fixesWereApplied)
15551557
throw new ArgumentNullException(nameof(scriptDefinition));
15561558
}
15571559

1558-
Range updatedRange;
1559-
return Fix(new EditableText(scriptDefinition), null, out updatedRange, out fixesWereApplied).ToString();
1560+
ScriptBlockAst scriptAst = null;
1561+
Token[] scriptTokens = null;
1562+
return Fix(new EditableText(scriptDefinition), range: null, skipParsing: false, out _, out fixesWereApplied, ref scriptAst, ref scriptTokens).ToString();
15601563
}
15611564

15621565
/// <summary>
15631566
/// Fix the violations in the given script text.
15641567
/// </summary>
1565-
/// <param name="text">An object of type `EditableText` that encapsulates the script text to be fixed.</param>
1568+
/// <param name="text">An object of type <see cref="EditableText"/> that encapsulates the script text to be fixed.</param>
15661569
/// <param name="range">The range in which the fixes are allowed.</param>
1570+
/// <param name="skipParsing">Whether to use the <paramref name="scriptAst"/> and <paramref name="scriptTokens"/> parameters instead of parsing the <paramref name="text"/> parameter.</param>
15671571
/// <param name="updatedRange">The updated range after the fixes have been applied.</param>
1568-
/// <param name="updatedRange">Whether any warnings were fixed.</param>
1572+
/// <param name="fixesWereApplied">Whether any warnings were fixed.</param>
1573+
/// <param name="scriptAst">Optionally pre-parsed AST.</param>
1574+
/// <param name="scriptTokens">Optionally pre-parsed tokens.</param>
15691575
/// <param name="skipVariableAnalysis">Whether to skip variable analysis.</param>
15701576
/// <returns>The same instance of `EditableText` that was passed to the method, but the instance encapsulates the fixed script text. This helps in chaining the Fix method.</returns>
1571-
public EditableText Fix(EditableText text, Range range, out Range updatedRange, out bool fixesWereApplied, bool skipVariableAnalysis = false)
1577+
public EditableText Fix(EditableText text, Range range, bool skipParsing, out Range updatedRange, out bool fixesWereApplied, ref ScriptBlockAst scriptAst, ref Token[] scriptTokens, bool skipVariableAnalysis = false)
15721578
{
15731579
if (text == null)
15741580
{
@@ -1590,7 +1596,15 @@ public EditableText Fix(EditableText text, Range range, out Range updatedRange,
15901596
var previousUnusedCorrections = 0;
15911597
do
15921598
{
1593-
var records = AnalyzeScriptDefinition(text.ToString(), skipVariableAnalysis);
1599+
IEnumerable<DiagnosticRecord> records;
1600+
if (skipParsing && previousUnusedCorrections == 0)
1601+
{
1602+
records = AnalyzeSyntaxTree(scriptAst, scriptTokens, String.Empty, skipVariableAnalysis);
1603+
}
1604+
else
1605+
{
1606+
records = AnalyzeScriptDefinition(text.ToString(), out scriptAst, out scriptTokens, skipVariableAnalysis);
1607+
}
15941608
var corrections = records
15951609
.Select(r => r.SuggestedCorrections)
15961610
.Where(sc => sc != null && sc.Any())
@@ -1684,22 +1698,21 @@ private static Range SnapToEdges(EditableText text, Range range)
16841698
}
16851699

16861700
private static IEnumerable<CorrectionExtent> GetCorrectionExtentsForFix(
1687-
IEnumerable<CorrectionExtent> correctionExtents)
1701+
List<CorrectionExtent> correctionExtents)
16881702
{
1689-
var ceList = correctionExtents.ToList();
1690-
ceList.Sort((x, y) =>
1703+
correctionExtents.Sort((x, y) =>
16911704
{
16921705
return x.StartLineNumber < y.StartLineNumber ?
16931706
1 :
16941707
(x.StartLineNumber == y.StartLineNumber ? 0 : -1);
16951708
});
16961709

1697-
return ceList.GroupBy(ce => ce.StartLineNumber).Select(g => g.First());
1710+
return correctionExtents.GroupBy(ce => ce.StartLineNumber).Select(g => g.First());
16981711
}
16991712

17001713
private static EditableText Fix(
17011714
EditableText text,
1702-
IEnumerable<CorrectionExtent> correctionExtents,
1715+
List<CorrectionExtent> correctionExtents,
17031716
out int unusedCorrections)
17041717
{
17051718
var correctionsToUse = GetCorrectionExtentsForFix(correctionExtents);
@@ -1710,7 +1723,7 @@ private static EditableText Fix(
17101723
text.ApplyEdit(correction);
17111724
}
17121725

1713-
unusedCorrections = correctionExtents.Count() - count;
1726+
unusedCorrections = correctionExtents.Count - count;
17141727
return text;
17151728
}
17161729

Tests/Engine/LibraryUsage.tests.ps1

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ function Invoke-ScriptAnalyzer {
9494
}
9595
else
9696
{
97-
$results = $scriptAnalyzer.AnalyzeScriptDefinition($ScriptDefinition);
97+
$results = $scriptAnalyzer.AnalyzeScriptDefinition($ScriptDefinition, [ref] $null, [ref] $null)
9898
}
9999

100100
$results

0 commit comments

Comments
 (0)