Skip to content

Commit 041f10d

Browse files
address keith feedback
1 parent 7b2ba86 commit 041f10d

File tree

3 files changed

+203
-190
lines changed

3 files changed

+203
-190
lines changed

src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs

Lines changed: 3 additions & 187 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,9 @@ namespace Microsoft.PowerShell.EditorServices.Services
2020
{
2121
internal class BreakpointService
2222
{
23-
private const string s_psesGlobalVariableNamePrefix = "__psEditorServices_";
2423
private readonly ILogger<BreakpointService> _logger;
2524
private readonly PowerShellContextService _powerShellContextService;
2625

27-
private static int breakpointHitCounter;
28-
2926
public BreakpointService(
3027
ILoggerFactory factory,
3128
PowerShellContextService powerShellContextService)
@@ -79,7 +76,7 @@ public async Task<BreakpointDetails[]> SetBreakpointsAsync(string escapedScriptP
7976
!string.IsNullOrWhiteSpace(breakpoint.HitCondition))
8077
{
8178
ScriptBlock actionScriptBlock =
82-
GetBreakpointActionScriptBlock(breakpoint);
79+
BreakpointApiUtils.GetBreakpointActionScriptBlock(breakpoint);
8380

8481
// If there was a problem with the condition string,
8582
// move onto the next breakpoint.
@@ -140,7 +137,7 @@ public async Task<IEnumerable<CommandBreakpointDetails>> SetCommandBreakpoints(I
140137
!string.IsNullOrWhiteSpace(breakpoint.HitCondition))
141138
{
142139
ScriptBlock actionScriptBlock =
143-
GetBreakpointActionScriptBlock(breakpoint);
140+
BreakpointApiUtils.GetBreakpointActionScriptBlock(breakpoint);
144141

145142
// If there was a problem with the condition string,
146143
// move onto the next breakpoint.
@@ -200,7 +197,7 @@ public async Task RemoveAllBreakpointsAsync()
200197
}
201198
}
202199

203-
public async Task RemoveBreakpoints(IEnumerable<Breakpoint> breakpoints)
200+
public async Task RemoveBreakpointsAsync(IEnumerable<Breakpoint> breakpoints)
204201
{
205202
if (VersionUtils.IsPS7OrGreater)
206203
{
@@ -226,187 +223,6 @@ public async Task RemoveBreakpoints(IEnumerable<Breakpoint> breakpoints)
226223
}
227224
}
228225

229-
/// <summary>
230-
/// Inspects the condition, putting in the appropriate scriptblock template
231-
/// "if (expression) { break }". If errors are found in the condition, the
232-
/// breakpoint passed in is updated to set Verified to false and an error
233-
/// message is put into the breakpoint.Message property.
234-
/// </summary>
235-
/// <param name="breakpoint"></param>
236-
/// <returns></returns>
237-
private ScriptBlock GetBreakpointActionScriptBlock(
238-
BreakpointDetailsBase breakpoint)
239-
{
240-
try
241-
{
242-
ScriptBlock actionScriptBlock;
243-
int? hitCount = null;
244-
245-
// If HitCondition specified, parse and verify it.
246-
if (!(string.IsNullOrWhiteSpace(breakpoint.HitCondition)))
247-
{
248-
if (int.TryParse(breakpoint.HitCondition, out int parsedHitCount))
249-
{
250-
hitCount = parsedHitCount;
251-
}
252-
else
253-
{
254-
breakpoint.Verified = false;
255-
breakpoint.Message = $"The specified HitCount '{breakpoint.HitCondition}' is not valid. " +
256-
"The HitCount must be an integer number.";
257-
return null;
258-
}
259-
}
260-
261-
// Create an Action scriptblock based on condition and/or hit count passed in.
262-
if (hitCount.HasValue && string.IsNullOrWhiteSpace(breakpoint.Condition))
263-
{
264-
// In the HitCount only case, this is simple as we can just use the HitCount
265-
// property on the breakpoint object which is represented by $_.
266-
string action = $"if ($_.HitCount -eq {hitCount}) {{ break }}";
267-
actionScriptBlock = ScriptBlock.Create(action);
268-
}
269-
else if (!string.IsNullOrWhiteSpace(breakpoint.Condition))
270-
{
271-
// Must be either condition only OR condition and hit count.
272-
actionScriptBlock = ScriptBlock.Create(breakpoint.Condition);
273-
274-
// Check for simple, common errors that ScriptBlock parsing will not catch
275-
// e.g. $i == 3 and $i > 3
276-
if (!ValidateBreakpointConditionAst(actionScriptBlock.Ast, out string message))
277-
{
278-
breakpoint.Verified = false;
279-
breakpoint.Message = message;
280-
return null;
281-
}
282-
283-
// Check for "advanced" condition syntax i.e. if the user has specified
284-
// a "break" or "continue" statement anywhere in their scriptblock,
285-
// pass their scriptblock through to the Action parameter as-is.
286-
Ast breakOrContinueStatementAst =
287-
actionScriptBlock.Ast.Find(
288-
ast => (ast is BreakStatementAst || ast is ContinueStatementAst), true);
289-
290-
// If this isn't advanced syntax then the conditions string should be a simple
291-
// expression that needs to be wrapped in a "if" test that conditionally executes
292-
// a break statement.
293-
if (breakOrContinueStatementAst == null)
294-
{
295-
string wrappedCondition;
296-
297-
if (hitCount.HasValue)
298-
{
299-
Interlocked.Increment(ref breakpointHitCounter);
300-
301-
string globalHitCountVarName =
302-
$"$global:{s_psesGlobalVariableNamePrefix}BreakHitCounter_{breakpointHitCounter}";
303-
304-
wrappedCondition =
305-
$"if ({breakpoint.Condition}) {{ if (++{globalHitCountVarName} -eq {hitCount}) {{ break }} }}";
306-
}
307-
else
308-
{
309-
wrappedCondition = $"if ({breakpoint.Condition}) {{ break }}";
310-
}
311-
312-
actionScriptBlock = ScriptBlock.Create(wrappedCondition);
313-
}
314-
}
315-
else
316-
{
317-
// Shouldn't get here unless someone called this with no condition and no hit count.
318-
actionScriptBlock = ScriptBlock.Create("break");
319-
_logger.LogWarning("No condition and no hit count specified by caller.");
320-
}
321-
322-
return actionScriptBlock;
323-
}
324-
catch (ParseException ex)
325-
{
326-
// Failed to create conditional breakpoint likely because the user provided an
327-
// invalid PowerShell expression. Let the user know why.
328-
breakpoint.Verified = false;
329-
breakpoint.Message = ExtractAndScrubParseExceptionMessage(ex, breakpoint.Condition);
330-
return null;
331-
}
332-
}
333226

334-
private static bool ValidateBreakpointConditionAst(Ast conditionAst, out string message)
335-
{
336-
message = string.Empty;
337-
338-
// We are only inspecting a few simple scenarios in the EndBlock only.
339-
if (conditionAst is ScriptBlockAst scriptBlockAst &&
340-
scriptBlockAst.BeginBlock == null &&
341-
scriptBlockAst.ProcessBlock == null &&
342-
scriptBlockAst.EndBlock != null &&
343-
scriptBlockAst.EndBlock.Statements.Count == 1)
344-
{
345-
StatementAst statementAst = scriptBlockAst.EndBlock.Statements[0];
346-
string condition = statementAst.Extent.Text;
347-
348-
if (statementAst is AssignmentStatementAst)
349-
{
350-
message = FormatInvalidBreakpointConditionMessage(condition, "Use '-eq' instead of '=='.");
351-
return false;
352-
}
353-
354-
if (statementAst is PipelineAst pipelineAst
355-
&& pipelineAst.PipelineElements.Count == 1
356-
&& pipelineAst.PipelineElements[0].Redirections.Count > 0)
357-
{
358-
message = FormatInvalidBreakpointConditionMessage(condition, "Use '-gt' instead of '>'.");
359-
return false;
360-
}
361-
}
362-
363-
return true;
364-
}
365-
366-
private static string ExtractAndScrubParseExceptionMessage(ParseException parseException, string condition)
367-
{
368-
string[] messageLines = parseException.Message.Split('\n');
369-
370-
// Skip first line - it is a location indicator "At line:1 char: 4"
371-
for (int i = 1; i < messageLines.Length; i++)
372-
{
373-
string line = messageLines[i];
374-
if (line.StartsWith("+"))
375-
{
376-
continue;
377-
}
378-
379-
if (!string.IsNullOrWhiteSpace(line))
380-
{
381-
// Note '==' and '>" do not generate parse errors
382-
if (line.Contains("'!='"))
383-
{
384-
line += " Use operator '-ne' instead of '!='.";
385-
}
386-
else if (line.Contains("'<'") && condition.Contains("<="))
387-
{
388-
line += " Use operator '-le' instead of '<='.";
389-
}
390-
else if (line.Contains("'<'"))
391-
{
392-
line += " Use operator '-lt' instead of '<'.";
393-
}
394-
else if (condition.Contains(">="))
395-
{
396-
line += " Use operator '-ge' instead of '>='.";
397-
}
398-
399-
return FormatInvalidBreakpointConditionMessage(condition, line);
400-
}
401-
}
402-
403-
// If the message format isn't in a form we expect, just return the whole message.
404-
return FormatInvalidBreakpointConditionMessage(condition, parseException.Message);
405-
}
406-
407-
private static string FormatInvalidBreakpointConditionMessage(string condition, string message)
408-
{
409-
return $"'{condition}' is not a valid PowerShell expression. {message}";
410-
}
411227
}
412228
}

src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ public async Task<CommandBreakpointDetails[]> SetCommandBreakpointsAsync(
216216
if (clearExisting)
217217
{
218218
// Flatten dictionary values into one list and remove them all.
219-
await _breakpointService.RemoveBreakpoints(this.breakpointsPerFile.Values.SelectMany( i => i ).Where( i => i is CommandBreakpoint)).ConfigureAwait(false);
219+
await _breakpointService.RemoveBreakpointsAsync(this.breakpointsPerFile.Values.SelectMany( i => i ).Where( i => i is CommandBreakpoint)).ConfigureAwait(false);
220220
}
221221

222222
if (breakpoints.Length > 0)
@@ -676,7 +676,7 @@ private async Task ClearBreakpointsInFileAsync(ScriptFile scriptFile)
676676
{
677677
if (breakpoints.Count > 0)
678678
{
679-
await _breakpointService.RemoveBreakpoints(breakpoints).ConfigureAwait(false);
679+
await _breakpointService.RemoveBreakpointsAsync(breakpoints).ConfigureAwait(false);
680680

681681
// Clear the existing breakpoints list for the file
682682
breakpoints.Clear();

0 commit comments

Comments
 (0)