Fix PowerShell code block inlining in compiler

This commit enhances the compiler's ability to inline PowerShell code
blocks. Previously, the compiler attempted to inline all lines ending
with brackets (`}` and `{`) using semicolons, which leads to syntax
errors. This improvement allows for more flexible PowerShell code
writing with reliable outcomes.

Key Changes:

- Update InlinePowerShell pipe to handle code blocks specifically
- Extend unit tests for the InlinePowerShell pipe

Other supporting changes:

- Refactor InlinePowerShell tests for improved scalability
- Enhance pipe unit test running with regex support
- Expand test coverage for various PowerShell syntax used in
  privacy.sexy
- Update related interfaces to align with new code conventions, dropping
  `I` prefix
- Optimize line merging to skip lines already ending with semicolons
- Increase timeout in E2E tests to accommodate for slower application
  load caused by more processing introduced in this commit.
This commit is contained in:
undergroundwires
2024-08-05 19:44:30 +02:00
parent f89c2322b0
commit d77c3cbbe2
26 changed files with 3837 additions and 495 deletions

View File

@@ -1,7 +1,7 @@
import { describe } from 'vitest';
import { EscapeDoubleQuotes } from '@/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/EscapeDoubleQuotes';
import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
import { runPipeTests } from './PipeTestRunner';
import { runPipeTests, type PipeTestScenario } from './PipeTestRunner';
describe('EscapeDoubleQuotes', () => {
// arrange
@@ -9,23 +9,23 @@ describe('EscapeDoubleQuotes', () => {
// act
runPipeTests(sut, [
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
.map((testCase) => ({
name: 'returns as it is when if input is missing',
.map((testCase): PipeTestScenario => ({
description: `returns empty when if input is missing (${testCase.valueName})`,
input: testCase.absentValue,
expectedOutput: testCase.absentValue,
expectedOutput: '',
})),
{
name: 'using "',
description: 'using "',
input: 'hello "world"',
expectedOutput: 'hello "^""world"^""',
},
{
name: 'not using any double quotes',
description: 'not using any double quotes',
input: 'hello world',
expectedOutput: 'hello world',
},
{
name: 'consecutive double quotes',
description: 'consecutive double quotes',
input: '""hello world""',
expectedOutput: '"^"""^""hello world"^"""^""',
},

View File

@@ -1,465 +1,50 @@
import { describe } from 'vitest';
import { InlinePowerShell } from '@/application/Parser/Executable/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell';
import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
import { type IPipeTestCase, runPipeTests } from './PipeTestRunner';
import { runPipeTests, type PipeTestScenario } from './PipeTestRunner';
import { createTryCatchFinallyTests } from './InlinePowerShellTests/CreateTryCatchFinallyTests';
import { createAbsentCodeTests } from './InlinePowerShellTests/CreateAbsentCodeTests';
import { createCommentedCodeTests } from './InlinePowerShellTests/CreateCommentedCodeTests';
import { createIfStatementTests } from './InlinePowerShellTests/CreateIfStatementTests';
import { createLineContinuationBacktickCases } from './InlinePowerShellTests/CreateLineContinuationBacktickTests';
import { createDoWhileTests } from './InlinePowerShellTests/CreateDoWhileTests';
import { createDoUntilTests } from './InlinePowerShellTests/CreateDoUntilTests';
import { createForeachTests } from './InlinePowerShellTests/CreateForeachTests';
import { createWhileTests } from './InlinePowerShellTests/CreateWhileTests';
import { createForLoopTests } from './InlinePowerShellTests/CreateForLoopTests';
import { createSwitchTests } from './InlinePowerShellTests/CreateSwitchTests';
import { createHereStringTests } from './InlinePowerShellTests/CreateHereStringTests';
import { createNewlineTests } from './InlinePowerShellTests/CreateNewlineTests';
import { createFunctionTests } from './InlinePowerShellTests/CreateFunctionTests';
import { createScriptBlockTests } from './InlinePowerShellTests/CreateScriptBlockTests';
describe('InlinePowerShell', () => {
// arrange
const sut = new InlinePowerShell();
// act
runPipeTests(sut, [
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
.map((testCase) => ({
name: 'returns as it is when if input is missing',
input: testCase.absentValue,
expectedOutput: '',
})),
...prefixTests('newline', getNewLineCases()),
...prefixTests('comment', getCommentCases()),
...prefixTests('here-string', hereStringCases()),
...prefixTests('backtick', backTickCases()),
...prefixTests('absent code', createAbsentCodeTests()),
...prefixTests('newline', createNewlineTests()),
...prefixTests('comment', createCommentedCodeTests()),
...prefixTests('here-string', createHereStringTests()),
...prefixTests('line continuation backtick', createLineContinuationBacktickCases()),
...prefixTests('try-catch-finally', createTryCatchFinallyTests()),
...prefixTests('if statement', createIfStatementTests()),
...prefixTests('do-while loop', createDoWhileTests()),
...prefixTests('do-until loop', createDoUntilTests()),
...prefixTests('foreach loop', createForeachTests()),
...prefixTests('while loop', createWhileTests()),
...prefixTests('for loop', createForLoopTests()),
...prefixTests('switch statement', createSwitchTests()),
...prefixTests('function', createFunctionTests()),
...prefixTests('script block', createScriptBlockTests()),
]);
});
function hereStringCases(): IPipeTestCase[] {
const expectLinesInDoubleQuotes = (...lines: string[]) => lines.join('`r`n');
const expectLinesInSingleQuotes = (...lines: string[]) => lines.join('\'+"`r`n"+\'');
return [
{
name: 'adds newlines for double quotes',
input: getWindowsLines(
'@"',
'Lorem',
'ipsum',
'dolor sit amet',
'"@',
),
expectedOutput: expectLinesInDoubleQuotes(
'"Lorem',
'ipsum',
'dolor sit amet"',
),
},
{
name: 'adds newlines for single quotes',
input: getWindowsLines(
'@\'',
'Lorem',
'ipsum',
'dolor sit amet',
'\'@',
),
expectedOutput: expectLinesInSingleQuotes(
'\'Lorem',
'ipsum',
'dolor sit amet\'',
),
},
{
name: 'does not match with character after here string header',
input: getWindowsLines(
'@" invalid syntax',
'I will not be processed as here-string',
'"@',
),
expectedOutput: getSingleLinedOutput(
'@" invalid syntax',
'I will not be processed as here-string',
'"@',
),
},
{
name: 'does not match if there\'s character before here-string terminator',
input: getWindowsLines(
'@\'',
'do not match here',
' \'@',
'character \'@',
),
expectedOutput: getSingleLinedOutput(
'@\'',
'do not match here',
' \'@',
'character \'@',
),
},
{
name: 'does not match with different here-string header/terminator',
input: getWindowsLines(
'@\'',
'lorem',
'"@',
),
expectedOutput: getSingleLinedOutput(
'@\'',
'lorem',
'"@',
),
},
{
name: 'matches with inner single quoted here-string',
input: getWindowsLines(
'$hasInnerDoubleQuotedTerminator = @"',
'inner text',
'@\'',
'inner terminator text',
'\'@',
'"@',
),
expectedOutput: expectLinesInDoubleQuotes(
'$hasInnerDoubleQuotedTerminator = "inner text',
'@\'',
'inner terminator text',
'\'@"',
),
},
{
name: 'matches with inner double quoted string',
input: getWindowsLines(
'$hasInnerSingleQuotedTerminator = @\'',
'inner text',
'@"',
'inner terminator text',
'"@',
'\'@',
),
expectedOutput: expectLinesInSingleQuotes(
'$hasInnerSingleQuotedTerminator = \'inner text',
'@"',
'inner terminator text',
'"@\'',
),
},
{
name: 'matches if there\'s character after here-string terminator',
input: getWindowsLines(
'@\'',
'lorem',
'\'@ after',
),
expectedOutput: expectLinesInSingleQuotes(
'\'lorem\' after',
),
},
{
name: 'escapes double quotes inside double quotes',
input: getWindowsLines(
'@"',
'For help, type "get-help"',
'"@',
),
expectedOutput: '"For help, type `"get-help`""',
},
{
name: 'escapes single quotes inside single quotes',
input: getWindowsLines(
'@\'',
'For help, type \'get-help\'',
'\'@',
),
expectedOutput: '\'For help, type \'\'get-help\'\'\'',
},
{
name: 'converts when here-string header is not at line start',
input: getWindowsLines(
'$page = [XML] @"',
'multi-lined',
'and "quoted"',
'"@',
),
expectedOutput: expectLinesInDoubleQuotes(
'$page = [XML] "multi-lined',
'and `"quoted`""',
),
},
{
name: 'trims after here-string header',
input: getWindowsLines(
'@" \t',
'text with whitespaces at here-string start',
'"@',
),
expectedOutput: '"text with whitespaces at here-string start"',
},
{
name: 'preserves whitespaces in lines',
input: getWindowsLines(
'@\'',
'\ttext with tabs around\t\t',
' text with whitespaces around ',
'\'@',
),
expectedOutput: expectLinesInSingleQuotes(
'\'\ttext with tabs around\t\t',
' text with whitespaces around \'',
),
},
];
}
function backTickCases(): IPipeTestCase[] {
return [
{
name: 'wraps newlines with trailing backtick',
input: getWindowsLines(
'Get-Service * `',
'| Format-Table -AutoSize',
),
expectedOutput: 'Get-Service * | Format-Table -AutoSize',
},
{
name: 'wraps newlines with trailing backtick and different line endings',
input: 'Get-Service `\n'
+ '* `\r'
+ '| Sort-Object StartType `\r\n'
+ '| Format-Table -AutoSize',
expectedOutput: 'Get-Service * | Sort-Object StartType | Format-Table -AutoSize',
},
{
name: 'trims tabs and whitespaces on next lines when wrapping with trailing backtick',
input: getWindowsLines(
'Get-Service * `',
'\t| Sort-Object StartType `',
' | Format-Table -AutoSize',
),
expectedOutput: 'Get-Service * | Sort-Object StartType | Format-Table -AutoSize',
},
{
name: 'does not wrap without whitespace before backtick',
input: getWindowsLines(
'Get-Service *`',
'| Format-Table -AutoSize',
),
expectedOutput: getSingleLinedOutput(
'Get-Service *`',
'| Format-Table -AutoSize',
),
},
{
name: 'does not wrap with characters after',
input: getWindowsLines(
'line start ` after',
'should not be wrapped',
),
expectedOutput: getSingleLinedOutput(
'line start ` after',
'should not be wrapped',
),
},
];
}
function getCommentCases(): IPipeTestCase[] {
return [
{
name: 'converts hash comments in the line end',
input: getWindowsLines(
'$text = "Hello"\t# Comment after tab',
'$text+= #Comment without space after hash',
'Write-Host $text# Comment without space before hash',
),
expectedOutput: getSingleLinedOutput(
'$text = "Hello"\t<# Comment after tab #>',
'$text+= <# Comment without space after hash #>',
'Write-Host $text<# Comment without space before hash #>',
),
},
{
name: 'converts hash comment line',
input: getWindowsLines(
'# Comment in first line',
'Write-Host "Hello"',
'# Comment in the middle',
'Write-Host "World"',
'# Consecutive comments',
'# Last line comment without line ending in the end',
),
expectedOutput: getSingleLinedOutput(
'<# Comment in first line #>',
'Write-Host "Hello"',
'<# Comment in the middle #>',
'Write-Host "World"',
'<# Consecutive comments #>',
'<# Last line comment without line ending in the end #>',
),
},
{
name: 'can convert comment with inline comment parts inside',
input: getWindowsLines(
'$text+= #Comment with < inside',
'$text+= #Comment ending with >',
'$text+= #Comment with <# inline comment #>',
),
expectedOutput: getSingleLinedOutput(
'$text+= <# Comment with < inside #>',
'$text+= <# Comment ending with > #>',
'$text+= <# Comment with <# inline comment #> #>',
),
},
{
name: 'can convert comment with inline comment parts around', // Pretty uncommon
input: getWindowsLines(
'Write-Host "hi" # Comment ending line inline comment but not one #>',
'Write-Host "hi" #>Comment starting like inline comment end but not one',
// Following line does not compile as valid PowerShell due to missing #> for inline comment.
'Write-Host "hi" <#Comment starting like inline comment start but not one',
),
expectedOutput: getSingleLinedOutput(
'Write-Host "hi" <# Comment ending line inline comment but not one #> #>',
'Write-Host "hi" <# >Comment starting like inline comment end but not one #>',
'Write-Host "hi" <<# Comment starting like inline comment start but not one #>',
),
},
{
name: 'converts empty hash comment',
input: getWindowsLines(
'Write-Host "Comment without text" #',
'Write-Host "Non-empty line"',
),
expectedOutput: getSingleLinedOutput(
'Write-Host "Comment without text" <##>',
'Write-Host "Non-empty line"',
),
},
{
name: 'adds whitespaces around to match',
input: getWindowsLines(
'#Comment line with no whitespaces around',
'Write-Host "Hello"#Comment in the end with no whitespaces around',
),
expectedOutput: getSingleLinedOutput(
'<# Comment line with no whitespaces around #>',
'Write-Host "Hello"<# Comment in the end with no whitespaces around #>',
),
},
{
name: 'trims whitespaces around comment',
input: getWindowsLines(
'# Comment with whitespaces around ',
'#\tComment with tabs around\t\t',
'#\t Comment with tabs and whitespaces around \t \t',
),
expectedOutput: getSingleLinedOutput(
'<# Comment with whitespaces around #>',
'<# Comment with tabs around #>',
'<# Comment with tabs and whitespaces around #>',
),
},
{
name: 'does not convert block comments',
input: getWindowsLines(
'$text = "Hello"\t<# block comment #> + "World"',
'$text = "Hello"\t+<#comment#>"World"',
'<# Block comment in a line #>',
'Write-Host "Hello world <# Block comment in the end of line #>',
),
expectedOutput: getSingleLinedOutput(
'$text = "Hello"\t<# block comment #> + "World"',
'$text = "Hello"\t+<#comment#>"World"',
'<# Block comment in a line #>',
'Write-Host "Hello world <# Block comment in the end of line #>',
),
},
{
name: 'does not process if there are no multi lines',
input: 'Write-Host "expected" # as it is!',
expectedOutput: 'Write-Host "expected" # as it is!',
},
];
}
function getNewLineCases(): IPipeTestCase[] {
return [
{
name: 'no new line',
input: 'Write-Host \'Hello, World!\'',
expectedOutput: 'Write-Host \'Hello, World!\'',
},
{
name: '\\n new line',
input:
'$things = Get-ChildItem C:\\Windows\\'
+ '\nforeach ($thing in $things) {'
+ '\nWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\n}',
expectedOutput: getSingleLinedOutput(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
{
name: '\\n double empty lines are ignored',
input:
'$things = Get-ChildItem C:\\Windows\\'
+ '\n\nforeach ($thing in $things) {'
+ '\n\nWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\n\n\n}',
expectedOutput: getSingleLinedOutput(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
{
name: '\\r new line',
input:
'$things = Get-ChildItem C:\\Windows\\'
+ '\rforeach ($thing in $things) {'
+ '\rWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\r}',
expectedOutput: getSingleLinedOutput(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
{
name: '\\r and \\n newlines combined',
input:
'$things = Get-ChildItem C:\\Windows\\'
+ '\r\nforeach ($thing in $things) {'
+ '\n\rWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\n\r}',
expectedOutput: getSingleLinedOutput(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
{
name: 'trims whitespaces on lines',
input:
' $things = Get-ChildItem C:\\Windows\\ '
+ '\nforeach ($thing in $things) {'
+ '\n\tWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\r \n}',
expectedOutput: getSingleLinedOutput(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
];
}
function prefixTests(prefix: string, tests: IPipeTestCase[]): IPipeTestCase[] {
function prefixTests(prefix: string, tests: PipeTestScenario[]): PipeTestScenario[] {
return tests.map((test) => ({
name: `[${prefix}] ${test.name}`,
input: test.input,
expectedOutput: test.expectedOutput,
...test,
...{
description: `[${prefix}] ${test.description}`,
},
}));
}
function getWindowsLines(...lines: string[]) {
return lines.join('\r\n');
}
function getSingleLinedOutput(...lines: string[]) {
return lines.map((line) => line.trim()).join('; ');
}

View File

@@ -0,0 +1,26 @@
import { RegexBuilder } from '../PipeTestRunner';
export function joinAsWindowsLines(
...lines: string[]
): string {
return lines.join('\r\n');
}
/**
* Builds a relaxed regular expression pattern for matching inlined multiple lines of code
* with basic semicolon merging.
*/
export function getInlinedOutputWithSemicolons(
...lines: string[]
): RegExp {
const trimmedLines = lines.map((line) => line.trim());
const builder = new RegexBuilder();
trimmedLines.forEach((line, index) => {
builder.withLiteralString(line);
builder.withOptionalSemicolon(); // Semi colon at the end compiles fine
if (index !== trimmedLines.length - 1) {
builder.withOptionalWhitespaceButNoNewline();
}
});
return builder.buildRegex();
}

View File

@@ -0,0 +1,28 @@
import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
import type { PipeTestScenario } from '../PipeTestRunner';
export function createAbsentCodeTests(): PipeTestScenario[] {
return [
...getAbsentStringTestCases({ excludeNull: true, excludeUndefined: true })
.map((testCase): PipeTestScenario => ({
description: `absent string (${testCase.valueName})`,
input: testCase.absentValue,
expectedOutput: '',
})),
{
description: 'whitespace-only input',
input: ' \t\n\r\f\v\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000',
expectedOutput: '',
},
{
description: 'newline-only input',
input: '\n\r\u2028\u2029',
expectedOutput: '',
},
{
description: 'newline-only input',
input: ' \t\n\r\f\v\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\n\r\u2028\u2029',
expectedOutput: '',
},
];
}

View File

@@ -0,0 +1,122 @@
import { getInlinedOutputWithSemicolons, joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities';
import type { PipeTestScenario } from '../PipeTestRunner';
export function createCommentedCodeTests(): PipeTestScenario[] {
// https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comment_based_help?view=powershell-7.4
return [
{
description: 'converts hash comments at line end',
input: joinAsWindowsLines(
'$text = "Hello"\t# Comment after tab',
'$text+= #Comment without space after hash',
'Write-Host $text# Comment without space before hash',
),
expectedOutput: getInlinedOutputWithSemicolons(
'$text = "Hello"\t<# Comment after tab #>',
'$text+= <# Comment without space after hash #>',
'Write-Host $text<# Comment without space before hash #>',
),
},
{
description: 'converts hash comment lines',
input: joinAsWindowsLines(
'# Comment in first line',
'Write-Host "Hello"',
'# Comment in the middle',
'Write-Host "World"',
'# Consecutive comments',
'# Last line comment without line ending in the end',
),
expectedOutput: getInlinedOutputWithSemicolons(
'<# Comment in first line #>',
'Write-Host "Hello"',
'<# Comment in the middle #>',
'Write-Host "World"',
'<# Consecutive comments #>',
'<# Last line comment without line ending in the end #>',
),
},
{
description: 'converts comments with inline comment parts inside',
input: joinAsWindowsLines(
'$text+= #Comment with < inside',
'$text+= #Comment ending with >',
'$text+= #Comment with <# inline comment #>',
),
expectedOutput: getInlinedOutputWithSemicolons(
'$text+= <# Comment with < inside #>',
'$text+= <# Comment ending with > #>',
'$text+= <# Comment with <# inline comment #> #>',
),
},
{
description: 'converts comments with inline comment parts around', // Pretty uncommon
input: joinAsWindowsLines(
'Write-Host "hi" # Comment ending line inline comment but not one #>',
'Write-Host "hi" #>Comment starting like inline comment end but not one',
// Following line does not compile as valid PowerShell due to missing #> for inline comment.
'Write-Host "hi" <#Comment starting like inline comment start but not one',
),
expectedOutput: getInlinedOutputWithSemicolons(
'Write-Host "hi" <# Comment ending line inline comment but not one #> #>',
'Write-Host "hi" <# >Comment starting like inline comment end but not one #>',
'Write-Host "hi" <<# Comment starting like inline comment start but not one #>',
),
},
{
description: 'converts empty hash comments',
input: joinAsWindowsLines(
'Write-Host "Comment without text" #',
'Write-Host "Non-empty line"',
),
expectedOutput: getInlinedOutputWithSemicolons(
'Write-Host "Comment without text" <##>',
'Write-Host "Non-empty line"',
),
},
{
description: 'adds whitespaces around comments',
input: joinAsWindowsLines(
'#Comment line with no whitespaces around',
'Write-Host "Hello"#Comment in the end with no whitespaces around',
),
expectedOutput: getInlinedOutputWithSemicolons(
'<# Comment line with no whitespaces around #>',
'Write-Host "Hello"<# Comment in the end with no whitespaces around #>',
),
},
{
description: 'trims whitespaces around comments',
input: joinAsWindowsLines(
'# Comment with whitespaces around ',
'#\tComment with tabs around\t\t',
'#\t Comment with tabs and whitespaces around \t \t',
),
expectedOutput: getInlinedOutputWithSemicolons(
'<# Comment with whitespaces around #>',
'<# Comment with tabs around #>',
'<# Comment with tabs and whitespaces around #>',
),
},
{
description: 'preserves block comments',
input: joinAsWindowsLines(
'$text = "Hello"\t<# block comment #> + "World"',
'$text = "Hello"\t+<#comment#>"World"',
'<# Block comment in a line #>',
'Write-Host "Hello world <# Block comment in the end of line #>',
),
expectedOutput: getInlinedOutputWithSemicolons(
'$text = "Hello"\t<# block comment #> + "World"',
'$text = "Hello"\t+<#comment#>"World"',
'<# Block comment in a line #>',
'Write-Host "Hello world <# Block comment in the end of line #>',
),
},
{
description: 'preserves single-line input',
input: 'Write-Host "expected" # as it is!',
expectedOutput: 'Write-Host "expected" # as it is!',
},
];
}

View File

@@ -0,0 +1,407 @@
import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner';
import { joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities';
export function createDoUntilTests(): PipeTestScenario[] {
// https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_do?view=powershell-7.4
return [
{
description: 'do-until loop without newlines',
input: 'do { $i++; Write-Host $i } until ($i -ge 5)',
expectedOutput: 'do { $i++; Write-Host $i } until ($i -ge 5)',
},
{
description: 'simple do-until loop (single line inside do block)',
input: joinAsWindowsLines(
'do {',
' $i++',
'} until ($i -lt 5)',
),
expectedOutput: new RegexBuilder()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i++')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('until')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($i -lt 5)')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'multiline do-until loop',
input: joinAsWindowsLines(
'do {',
' $i++',
' Write-Host $i',
'} until ($i -ge 5)',
),
expectedOutput: new RegexBuilder()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i++')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host $i')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('until')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($i -ge 5)')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'nested do-until loops',
input: joinAsWindowsLines(
'do {',
' $outer = 0',
' do {',
' $inner = 0',
' do {',
' $inner++',
' Write-Host "Inner: $inner"',
' } until ($inner -ge 3)',
' $outer++',
' Write-Host "Outer: $outer"',
' } until ($outer -ge 2)',
' $mainCounter++',
' Write-Host "Main: $mainCounter"',
'} until ($mainCounter -ge 2)',
),
expectedOutput: new RegexBuilder()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$outer = 0')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$inner = 0')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$inner++')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Inner: $inner"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('until')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($inner -ge 3)')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$outer++')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Outer: $outer"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('until')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($outer -ge 2)')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$mainCounter++')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Main: $mainCounter"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('until')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($mainCounter -ge 2)')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'do-until loop with condition on separate line',
input: joinAsWindowsLines(
'do {',
' $i++',
' Write-Host $i',
'}',
'until ($i -ge 5)',
),
expectedOutput: new RegexBuilder()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i++')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host $i')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}') // No semicolon after this to prevent runtime errors
.withOptionalWhitespaceButNoNewline()
.withLiteralString('until')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($i -ge 5)')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'do-until loop with complex condition',
input: joinAsWindowsLines(
'do {',
' $i++',
' $j--',
'} until ( `', // Marked: inline-conditions | Merging multiline conditions is not yet supported, so using backticks
' $i -ge 10 -or `',
' $j -le 0 `',
')',
),
expectedOutput: new RegexBuilder()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i++')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$j--')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('until')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('(')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i -ge 10 -or')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$j -le 0')
.withOptionalWhitespaceButNoNewline()
.withLiteralString(')')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'do-until loop with nested if statement',
input: joinAsWindowsLines(
'do {',
' $i++',
' if ($i % 2 -eq 0) {',
' Write-Host "Even: $i"',
' } else {',
' Write-Host "Odd: $i"',
' }',
'} until ($i -ge 5)',
),
expectedOutput: new RegexBuilder()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i++')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('if ($i % 2 -eq 0)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Even: $i"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('else')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Odd: $i"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('until')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($i -ge 5)')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'do-until loop with semicolon after closing brace',
input: joinAsWindowsLines(
'do {',
' $i++',
' Write-Host $i',
'};',
'until ($i -ge 5)',
),
expectedOutput: new RegexBuilder()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i++')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host $i')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('until')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($i -ge 5)')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: '-until loop with pipeline in condition',
input: joinAsWindowsLines(
'do {',
' $result = Get-Something',
' Process-Result $result',
'} until ($result | Test-Condition)',
),
expectedOutput: new RegexBuilder()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = Get-Something')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Process-Result $result')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('until')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('(')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result | Test-Condition')
.withOptionalWhitespaceButNoNewline()
.withLiteralString(')')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'do-until loop with multiline condition',
input: joinAsWindowsLines(
'do {',
' $result = Get-Something',
' Process-Result $result',
'} until ( `', // Marked: inline-conditions | Merging multiline conditions is not yet supported, so using backticks
' $result -and (-Not $result) `',
')',
),
expectedOutput: new RegexBuilder()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = Get-Something')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Process-Result $result')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('until')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('(')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result -and (-Not $result)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString(')')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'do-until loop with script block condition',
input: joinAsWindowsLines(
'do {',
' $i++',
' Write-Host $i',
'} until ( `', // Marked: inline-conditions | Merging multiline conditions is not yet supported, so using backticks
' & {',
' param($val)',
' $val -ge 5',
' } $i `',
')',
),
expectedOutput: new RegexBuilder()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i++')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host $i')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('until')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('(')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('&')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('param($val)')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$val -ge 5')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i')
.withOptionalWhitespaceButNoNewline()
.withLiteralString(')')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'do-until after closing bracket',
input: joinAsWindowsLines(
'switch ($value) { default { Write-Host "Default" } }',
'do { $i++ } until ($i -ge 5)',
),
expectedOutput: new RegexBuilder()
.withLiteralString('switch ($value) { default { Write-Host "Default" } }')
.withSemicolon() // Semicolon here to prevent runtime errors
.withOptionalWhitespaceButNoNewline()
.withLiteralString('do { $i++ } until ($i -ge 5)')
.withOptionalSemicolon()
.buildRegex(),
},
];
}

View File

@@ -0,0 +1,281 @@
import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner';
import { joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities';
export function createDoWhileTests(): PipeTestScenario[] {
// https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_do?view=powershell-7.4
return [
{
description: 'do-while loop without newlines',
input: 'do { $i++ } while ($i -lt 5)',
expectedOutput: 'do { $i++ } while ($i -lt 5)',
},
{
description: 'simple do-while loop (single line inside do block)',
input: joinAsWindowsLines(
'do {',
' $i++',
'} while ($i -lt 5)',
),
expectedOutput: new RegexBuilder()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i++')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('while')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($i -lt 5)')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'multiline do-while loop',
input: joinAsWindowsLines(
'do {',
' $i++',
' Write-Host $i',
'} while ($i -lt 5)',
),
expectedOutput: new RegexBuilder()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i++')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host $i')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('while')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($i -lt 5)')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'nested do-while loops',
input: joinAsWindowsLines(
'do {',
' $i++',
' do {',
' $j++',
' } while ($j -lt 3)',
' Write-Host "$i, $j"',
'} while ($i -lt 5)',
),
expectedOutput: new RegexBuilder()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i++')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$j++')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('while')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($j -lt 3)')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "$i, $j"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('while')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($i -lt 5)')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'do-while loop with condition on separate line',
input: joinAsWindowsLines(
'do {',
' $i++',
'}',
'while ($i -lt 5)',
),
expectedOutput: new RegexBuilder()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i++')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}') // No semicolon after this to prevent runtime errors
.withOptionalWhitespaceButNoNewline()
.withLiteralString('while')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($i -lt 5)')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'do-while loop with multiline condition',
input: joinAsWindowsLines(
'do {',
' $i++',
' $j--',
'} while ( `', // Marked: inline-conditions | Merging multiline conditions is not yet supported, so using backticks
' $i -lt 10 -and `',
' $j -gt 0 `',
')',
),
expectedOutput: new RegexBuilder()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i++')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$j--')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('while')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('(')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i -lt 10 -and')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$j -gt 0')
.withOptionalWhitespaceButNoNewline()
.withLiteralString(')')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'do-while loop with nested if statement',
input: joinAsWindowsLines(
'do {',
' $i++',
' if ($i % 2 -eq 0) {',
' Write-Host "Even"',
' } else {',
' Write-Host "Odd"',
' }',
'} while ($i -lt 5)',
),
expectedOutput: new RegexBuilder()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i++')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('if ($i % 2 -eq 0)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Even"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('else')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Odd"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('while')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($i -lt 5)')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'do-while loop with semicolon after closing brace',
input: joinAsWindowsLines(
'do {',
' $i++',
'};',
'while ($i -lt 5)',
),
expectedOutput: new RegexBuilder()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i++')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('while')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($i -lt 5)')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'do-while loop with pipeline in condition',
input: joinAsWindowsLines(
'do {',
' $result = Get-Something',
'} while ( $result | Test-Condition )',
),
expectedOutput: new RegexBuilder()
.withLiteralString('do')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = Get-Something')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('while')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('(')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result | Test-Condition')
.withOptionalWhitespaceButNoNewline()
.withLiteralString(')')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'do-while after closing bracket',
input: joinAsWindowsLines(
'if ($someCondition) { $variable = "Some value" }',
'do { $i++; Write-Host $i } while ($i -lt 5)',
),
expectedOutput: new RegexBuilder()
.withLiteralString('if ($someCondition) { $variable = "Some value" }')
.withSemicolon() // Semicolon here to prevent runtime errors
.withOptionalWhitespaceButNoNewline()
.withLiteralString('do { $i++; Write-Host $i } while ($i -lt 5)')
.withOptionalSemicolon()
.buildRegex(),
},
];
}

View File

@@ -0,0 +1,214 @@
import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner';
import { joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities';
export function createForLoopTests(): PipeTestScenario[] {
// https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_for?view=powershell-7.4
// Known limitations:
// - Multiline for loop with sections without semicolon, e.g. `for(\n$i =0\n$i - l5\n$i++\n)`
return [
{
description: 'for loop without newlines',
input: 'for ($i = 0; $i -lt 5; $i++) { Write-Host $i }',
expectedOutput: 'for ($i = 0; $i -lt 5; $i++) { Write-Host $i }',
},
{
description: 'simple for loop (single line inside code block)',
input: joinAsWindowsLines(
'for ($i = 0; $i -lt 5; $i++) {',
' Write-Host $i',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('for')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($i = 0; $i -lt 5; $i++)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host $i')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'multiline for loop',
input: joinAsWindowsLines(
'for ($i = 0; $i -lt 5; $i++) {',
' Write-Host "Current value: $i"',
' $result += $i',
' Do-SomethingWith $i',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('for')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($i = 0; $i -lt 5; $i++)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Current value: $i"')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result += $i')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Do-SomethingWith $i')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'nested for loops',
input: joinAsWindowsLines(
'for ($i = 0; $i -lt 3; $i++) {',
' for ($j = 0; $j -lt 2; $j++) {',
' Write-Host "i: $i, j: $j"',
' }',
' Write-Host "Outer loop: $i"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('for')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($i = 0; $i -lt 3; $i++)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('for')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($j = 0; $j -lt 2; $j++)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "i: $i, j: $j"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Outer loop: $i"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'for loop without spaces in header',
input: joinAsWindowsLines(
'for($i=0;$i-lt5;$i++){',
' Write-Host $i',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('for')
.withLiteralString('($i=0;$i-lt5;$i++)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host $i')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'for loop with empty sections',
input: joinAsWindowsLines(
'for (;;) {',
' $i++',
' if ($i -ge 5) { break }',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('for')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('(;;)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i++')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('if ($i -ge 5) { break }')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'for loop with multiple statements in each section',
input: joinAsWindowsLines(
'for ($i = 0, $j = 10; $i -lt 5 -and $j -gt 0; $i++, $j--) {',
' Write-Host "i: $i, j: $j"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('for')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($i = 0, $j = 10; $i -lt 5 -and $j -gt 0; $i++, $j--)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "i: $i, j: $j"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'for loop with multiline sections',
input: joinAsWindowsLines(
'for ( `', // Marked: inline-conditions | Merging multiline conditions is not yet supported, so using backticks
' $i = 0; `',
' $i -lt 5; `',
' $i++ `',
') {',
' Write-Host $i',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('for')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('(')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i = 0;')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i -lt 5;')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i++')
.withOptionalWhitespaceButNoNewline()
.withLiteralString(')')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host $i')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'for after closing bracket',
input: joinAsWindowsLines(
'$scriptBlock = { Write-Host "Inside script block" }',
'for ($i = 0; $i -lt 5; $i++) { Write-Host $i }',
),
expectedOutput: new RegexBuilder()
.withLiteralString('$scriptBlock = { Write-Host "Inside script block" }')
.withSemicolon() // Semicolon here to prevent runtime errors
.withOptionalWhitespaceButNoNewline()
.withLiteralString('for ($i = 0; $i -lt 5; $i++) { Write-Host $i }')
.withOptionalSemicolon()
.buildRegex(),
},
];
}

View File

@@ -0,0 +1,284 @@
import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner';
import { joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities';
export function createForeachTests(): PipeTestScenario[] {
return [
// https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_foreach?view=powershell-7.4
{
description: 'foreach loop without newlines',
input: 'foreach ($item in $collection) { Write-Host $item }',
expectedOutput: 'foreach ($item in $collection) { Write-Host $item }',
},
{
description: 'simple foreach loop (single line inside code block)',
input: joinAsWindowsLines(
'foreach ($item in $collection) {',
' Write-Host $item',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('foreach')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($item in $collection)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host $item')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'multiline foreach loop',
input: joinAsWindowsLines(
'foreach ($item in $collection) {',
' $processedItem = $item.ToUpper()',
' Write-Host "Processing: $processedItem"',
' $result += $processedItem',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('foreach')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($item in $collection)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$processedItem = $item.ToUpper()')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Processing: $processedItem"')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result += $processedItem')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'nested foreach loops',
input: joinAsWindowsLines(
'foreach ($outer in $outerCollection) {',
' Write-Host "Outer: $outer"',
' foreach ($inner in $innerCollection) {',
' Write-Host " Inner: $inner"',
' $result = "$outer-$inner"',
' $combinedResults += $result',
' }',
' Write-Host "Completed inner loop for $outer"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('foreach')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($outer in $outerCollection)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Outer: $outer"')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('foreach')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($inner in $innerCollection)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host " Inner: $inner"')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = "$outer-$inner"')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$combinedResults += $result')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Completed inner loop for $outer"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'foreach loop with multiline condition',
input: joinAsWindowsLines(
'foreach ( `', // Marked: inline-conditions | Merging multiline conditions is not yet supported, so using backticks
' $item `',
' in `',
' $collection `',
') {',
' Write-Host $item',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('foreach')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('(')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$item')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('in')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$collection')
.withOptionalWhitespaceButNoNewline()
.withLiteralString(')')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host $item')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'foreach loop with pipeline in collection',
input: joinAsWindowsLines(
'foreach ($item in Get-Process | Where-Object { $_.CPU -gt 50 }) {',
' Write-Host $item.Name',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('foreach')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($item in Get-Process |')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Where-Object { $_.CPU -gt 50 })')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host $item.Name')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'foreach loop with complex collection expression',
input: joinAsWindowsLines(
'foreach ($item in ( `',
' $array1 + `', // Marked: inline-conditions | Merging multiline conditions is not yet supported, so using backticks
' $array2 | `',
' Where-Object { $_ -ne $null } `',
')) {',
' Write-Host $item',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('foreach')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($item in (')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$array1 +')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$array2 |')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Where-Object { $_ -ne $null }')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('))')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host $item')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'foreach loop with script block in collection',
input: joinAsWindowsLines(
'foreach ($item in & {',
' param($start, $end)',
' $start..$end',
'} 1 10) {',
' Write-Host $item',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('foreach')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($item in &')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('param($start, $end)')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$start..$end')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('1 10)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host $item')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'foreach loop with closing brace on same line as last statement',
input: joinAsWindowsLines(
'foreach ($item in $collection) {',
' Write-Host $item',
' Process-Item $item }',
),
expectedOutput: new RegexBuilder()
.withLiteralString('foreach')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($item in $collection)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host $item')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Process-Item $item')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'foreach after closing bracket',
input: joinAsWindowsLines(
'function Test-Function { Write-Host "Test" }',
'foreach ($item in @(1,2,3)) { Write-Host $item }',
),
expectedOutput: new RegexBuilder()
.withLiteralString('function Test-Function { Write-Host "Test" }')
.withSemicolon() // Semicolon here to prevent runtime errors
.withOptionalWhitespaceButNoNewline()
.withLiteralString('foreach ($item in @(1,2,3)) { Write-Host $item }')
.withOptionalSemicolon()
.buildRegex(),
},
];
}

View File

@@ -0,0 +1,229 @@
import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner';
import { joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities';
export function createFunctionTests(): PipeTestScenario[] {
// https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions?view=powershell-7.4
// Known limitations:
// - Functions with advanced parameters are not yet supported.
return [
{
description: 'function without newlines',
input: 'function Get-Name { param($FirstName, $LastName) Write-Output "$FirstName $LastName" }',
expectedOutput: 'function Get-Name { param($FirstName, $LastName) Write-Output "$FirstName $LastName" }',
},
{
description: 'simple function (single line inside code block)',
input: joinAsWindowsLines(
'function Say-Hello {',
' Write-Host "Hello, World!"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('function')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Say-Hello')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Hello, World!"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'function with multiple statements',
input: joinAsWindowsLines(
'function Do-Something {',
' $result = Get-Something',
' Process-Result $result',
' Write-Output "Done processing"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('function')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Do-Something')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = Get-Something')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Process-Result $result')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Output "Done processing"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'function with begin, process, and end blocks',
input: joinAsWindowsLines(
'function Process-Collection {',
' begin {',
' $total = 0',
' }',
' process {',
' $total += $_',
' }',
' end {',
' Write-Output "Total: $total"',
' }',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('function')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Process-Collection')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('begin')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$total = 0')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('process')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$total += $_')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('end')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Output "Total: $total"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'nested functions',
input: joinAsWindowsLines(
'function Outer-Function {',
' param($outerParam)',
' function Inner-Function {',
' param($innerParam)',
' Write-Output "Inner: $innerParam"',
' }',
' Write-Output "Outer: $outerParam"',
' Inner-Function -innerParam "Hello from inner"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('function')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Outer-Function')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('param($outerParam)')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('function')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Inner-Function')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('param($innerParam)')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Output "Inner: $innerParam"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Output "Outer: $outerParam"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Inner-Function -innerParam "Hello from inner"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'function without space before opening brace',
input: joinAsWindowsLines(
'function Get-Something{',
' return "Something"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('function')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Get-Something')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('return "Something"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'function with single-line param block',
input: joinAsWindowsLines(
'function Set-Value {',
' param([string]$key, [object]$value)',
' $hash[$key] = $value',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('function')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Set-Value')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('param([string]$key, [object]$value)')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$hash[$key] = $value')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'function after closing bracket',
input: joinAsWindowsLines(
'if ($condition) { $value = 10 }',
'function Test-Function { param($param) Write-Host $param }',
),
expectedOutput: new RegexBuilder()
.withLiteralString('if ($condition) { $value = 10 }')
.withSemicolon() // Semicolon here to prevent runtime errors
.withOptionalWhitespaceButNoNewline()
.withLiteralString('function Test-Function { param($param) Write-Host $param }')
.withOptionalSemicolon()
.buildRegex(),
},
];
}

View File

@@ -0,0 +1,240 @@
import { getInlinedOutputWithSemicolons, joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities';
import type { PipeTestScenario } from '../PipeTestRunner';
export function createHereStringTests(): PipeTestScenario[] {
// https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#here-strings
return [
{
description: 'double-quoted here-string',
input: joinAsWindowsLines(
'@"',
'Lorem',
'ipsum',
'dolor sit amet',
'"@',
),
expectedOutput: getInlinedOutputWithSemicolons(
joinLinesForDoubleQuotedString(
'"Lorem',
'ipsum',
'dolor sit amet"',
),
),
},
{
description: 'single-quoted here-string',
input: joinAsWindowsLines(
'@\'',
'Lorem',
'ipsum',
'dolor sit amet',
'\'@',
),
expectedOutput: getInlinedOutputWithSemicolons(
joinLinesForSingleQuotedString(
'Lorem',
'ipsum',
'dolor sit amet',
),
),
},
{
description: 'preserves invalid here-string syntax',
input: joinAsWindowsLines(
'@" invalid syntax',
'I will not be processed as here-string',
'"@',
),
expectedOutput: getInlinedOutputWithSemicolons(
'@" invalid syntax',
'I will not be processed as here-string',
'"@',
),
},
{
description: 'preserves here-string with character before terminator',
input: joinAsWindowsLines(
'@\'',
'do not match here',
' \'@',
'character \'@',
),
expectedOutput: getInlinedOutputWithSemicolons(
'@\'',
'do not match here',
' \'@',
'character \'@',
),
},
{
description: 'preserves here-string with mismatched delimiters',
input: joinAsWindowsLines(
'@\'',
'lorem',
'"@',
),
expectedOutput: getInlinedOutputWithSemicolons(
'@\'',
'lorem',
'"@',
),
},
{
description: 'single quoted here-string with nested single quoted here-string',
input: joinAsWindowsLines(
'$hasInnerDoubleQuotedTerminator = @"',
'inner text',
'@\'',
'inner terminator text',
'\'@',
'"@',
),
expectedOutput: getInlinedOutputWithSemicolons(
joinLinesForDoubleQuotedString(
'$hasInnerDoubleQuotedTerminator = "inner text',
'@\'',
'inner terminator text',
'\'@"',
),
),
},
{
description: 'single quoted here-string with inner double-quoted string',
input: joinAsWindowsLines(
'$hasInnerSingleQuotedTerminator = @\'',
'inner text',
'@"',
'inner terminator text',
'"@',
'\'@',
),
expectedOutput: getInlinedOutputWithSemicolons(
joinLinesForSingleQuotedString(
'$hasInnerSingleQuotedTerminator = \'inner text',
'@"',
'inner terminator text',
'"@\'',
),
),
},
{
description: 'here-string with character after terminator',
input: joinAsWindowsLines(
'@\'',
'lorem',
'\'@ after',
),
expectedOutput: getInlinedOutputWithSemicolons(
'\'lorem\' after',
),
},
{
description: 'escapes double quotes in double-quoted here-string',
input: joinAsWindowsLines(
'@"',
'For help, type "get-help"',
'"@',
),
expectedOutput: getInlinedOutputWithSemicolons(
'"For help, type `"get-help`""',
),
},
{
description: 'escapes single quotes in single-quoted here-string',
input: joinAsWindowsLines(
'@\'',
'For help, type \'get-help\'',
'\'@',
),
expectedOutput: getInlinedOutputWithSemicolons(
'\'For help, type \'\'get-help\'\'\'',
),
},
{
description: 'here-string not at line start',
input: joinAsWindowsLines(
'$page = [XML] @"',
'multi-lined',
'and "quoted"',
'"@',
),
expectedOutput: getInlinedOutputWithSemicolons(
joinLinesForDoubleQuotedString(
'$page = [XML] "multi-lined',
'and `"quoted`""',
),
),
},
{
description: 'trims whitespace after here-string header',
input: joinAsWindowsLines(
'@" \t',
'text with whitespaces at here-string start',
'"@',
),
expectedOutput: getInlinedOutputWithSemicolons(
'"text with whitespaces at here-string start"',
),
},
{
description: 'preserves whitespace in here-string lines',
input: joinAsWindowsLines(
'@\'',
'\ttext with tabs around\t\t',
' text with whitespace around ',
'\'@',
),
expectedOutput: getInlinedOutputWithSemicolons(
joinLinesForSingleQuotedString(
'\'\ttext with tabs around\t\t',
' text with whitespace around \'',
),
),
},
{
description: 'preserves code inside here-string',
input: joinAsWindowsLines( // Triggering a some code inlining logic:
'@"',
'if (',
' $condition1 -and',
' $condition2',
') {',
' Write-Host "True"',
' Write-Warning "Not false"',
'} else',
'{',
' Get-Process `',
' | Where-Object { $_.CPU -gt 50 }',
'}',
'"@',
),
expectedOutput: getInlinedOutputWithSemicolons(
joinLinesForDoubleQuotedString( // Identical to input
'"if (',
' $condition1 -and',
' $condition2',
') {',
' Write-Host `"True`"',
' Write-Warning `"Not false`"',
'} else',
'{',
' Get-Process `',
' | Where-Object { $_.CPU -gt 50 }',
'}"',
),
),
},
];
}
function joinLinesForDoubleQuotedString(
...lines: readonly string[]
): string {
return lines.join('`r`n');
}
function joinLinesForSingleQuotedString(
...lines: readonly string[]
): string {
return lines.join('\'+"`r`n"+\'');
}

View File

@@ -0,0 +1,426 @@
import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner';
import { joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities';
export function createIfStatementTests(): PipeTestScenario[] {
// https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_if?view=powershell-7.4
return [
{
description: 'if/else statement without newlines',
input: 'if ($condition) { Write-Host "True" } else { Write-Host "False" }',
expectedOutput: 'if ($condition) { Write-Host "True" } else { Write-Host "False" }',
},
{
description: 'simple if statement (single line inside code block)',
input: joinAsWindowsLines(
'if ($true) {',
' Write-Host "True"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('if')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($true)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "True"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.buildRegex(),
},
{
description: 'if/else statement',
input: joinAsWindowsLines(
'if ($condition) {',
' Write-Host "True"',
'} else {',
' Write-Host "False"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('if')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($condition)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "True"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('else')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "False"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.buildRegex(),
},
{
description: 'if/elseif/else statement',
input: joinAsWindowsLines(
'if ($condition1) {',
' Write-Host "Condition 1"',
'} elseif ($condition2) {',
' Write-Host "Condition 2"',
'} else {',
' Write-Host "None"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('if')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($condition1)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Condition 1"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('elseif')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($condition2)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Condition 2"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('else')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "None"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.buildRegex(),
},
{
description: 'if statement with multiple lines',
input: joinAsWindowsLines(
'if ($condition) {',
' $result = 10 * 5',
' Write-Host "Calculation done"',
' $finalResult = $result + 20',
' Write-Host "Final result: $finalResult"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('if')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($condition)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = 10 * 5')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Calculation done"')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$finalResult = $result + 20')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Final result: $finalResult"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.buildRegex(),
},
{
description: 'nested if statements',
input: joinAsWindowsLines(
'if ($outerCondition) {',
' $outerResult = 10',
' if ($innerCondition1) {',
' Write-Host "Inner condition 1 met"',
' } elseif ($innerCondition2) {',
' Write-Host "Inner condition 2 met"',
' } else {',
' Write-Host "No inner conditions met"',
' }',
' Write-Host "Outer condition processing complete"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('if')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($outerCondition)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$outerResult = 10')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('if')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($innerCondition1)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Inner condition 1 met"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('elseif')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($innerCondition2)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Inner condition 2 met"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('else')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "No inner conditions met"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Outer condition processing complete"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'if/elseif/else with closing brackets on separate lines',
input: joinAsWindowsLines(
'if ($condition1) {',
' Write-Host "Condition 1"',
'}',
'elseif ($condition2) {',
' Write-Host "Condition 2"',
'}',
'else {',
' Write-Host "No condition met"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('if')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($condition1)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Condition 1"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}') // No semicolon after this to prevent runtime errors
.withOptionalWhitespaceButNoNewline()
.withLiteralString('elseif')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($condition2)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Condition 2"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}') // No semicolon after this to prevent runtime errors
.withOptionalWhitespaceButNoNewline()
.withLiteralString('else')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "No condition met"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'if statement without space before opening parenthesis',
input: joinAsWindowsLines(
'if($condition) {',
' Write-Host "True"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('if($condition)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "True"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.buildRegex(),
},
{
description: 'if statement without space after closing parenthesis',
input: joinAsWindowsLines(
'if ($condition){',
' Write-Host "True"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('if ($condition)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "True"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'if statement with extra spaces in condition',
input: joinAsWindowsLines(
'if ( $condition ) {',
' Write-Host "True"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('if')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('(')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$condition')
.withOptionalWhitespaceButNoNewline()
.withLiteralString(')')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "True"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.buildRegex(),
},
{
description: 'if statement with multiline condition',
input: joinAsWindowsLines(
'if ( `', // Marked: inline-conditions | Merging multiline conditions is not yet supported, so using backticks
' $condition1 -and `',
' $condition2 `',
') {',
' Write-Host "True"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('if')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('(')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$condition1 -and')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$condition2')
.withOptionalWhitespaceButNoNewline()
.withLiteralString(')')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "True"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.buildRegex(),
},
{
description: 'if/elseif/else with mixed brace styles',
input: joinAsWindowsLines(
'if ($condition1) {',
' Write-Host "Condition 1"',
'} elseif ($condition2)',
'{',
' Write-Host "Condition 2"',
'} else {',
' Write-Host "None"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('if')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($condition1)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Condition 1"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('elseif')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($condition2)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Condition 2"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('else')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "None"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.buildRegex(),
},
{
description: 'if statement with pipeline in condition',
input: joinAsWindowsLines(
'if (Get-Process | Where-Object { $_.CPU -gt 50 }) {',
' Write-Host "High CPU usage detected"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('if')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('(Get-Process |')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Where-Object { $_.CPU -gt 50 })')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "High CPU usage detected"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.buildRegex(),
},
{
description: 'if statement after closing bracket',
input: joinAsWindowsLines(
'$testArray = @(1, 2, 3, 4, 5)',
'$result = $testArray | ForEach-Object { $_ * 2 }',
'if ($result.Count -gt 0) { Write-Host "Array has items" }',
),
expectedOutput: new RegexBuilder()
.withLiteralString('$testArray = @(1, 2, 3, 4, 5)')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = $testArray | ForEach-Object { $_ * 2 }')
.withSemicolon() // Semicolon here to prevent runtime errors
.withOptionalWhitespaceButNoNewline()
.withLiteralString('if ($result.Count -gt 0) { Write-Host "Array has items" }')
.withOptionalSemicolon()
.buildRegex(),
},
];
}

View File

@@ -0,0 +1,61 @@
import { getInlinedOutputWithSemicolons, joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities';
import type { PipeTestScenario } from '../PipeTestRunner';
export function createLineContinuationBacktickCases(): PipeTestScenario[] {
// https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing?view=powershell-7.4#line-continuation
return [
{
description: 'inlines newlines with trailing backtick',
input: joinAsWindowsLines(
'Get-Service * `',
'| Format-Table -AutoSize',
),
expectedOutput: getInlinedOutputWithSemicolons(
'Get-Service * | Format-Table -AutoSize',
),
},
{
description: 'inlines newlines with trailing backtick and different line endings',
input: 'Get-Service `\n'
+ '* `\r'
+ '| Sort-Object StartType `\r\n'
+ '| Format-Table -AutoSize',
expectedOutput: getInlinedOutputWithSemicolons(
'Get-Service * | Sort-Object StartType | Format-Table -AutoSize',
),
},
{
description: 'trims whitespace when inlining with trailing backtick',
input: joinAsWindowsLines(
'Get-Service * `',
'\t| Sort-Object StartType `',
' | Format-Table -AutoSize',
),
expectedOutput: getInlinedOutputWithSemicolons(
'Get-Service * | Sort-Object StartType | Format-Table -AutoSize',
),
},
{
description: 'preserves line without whitespace before backtick',
input: joinAsWindowsLines(
'Get-Service *`',
'| Format-Table -AutoSize',
),
expectedOutput: getInlinedOutputWithSemicolons(
'Get-Service *`',
'| Format-Table -AutoSize',
),
},
{
description: 'preserves line with characters after backtick',
input: joinAsWindowsLines(
'line start ` after',
'should not be wrapped',
),
expectedOutput: getInlinedOutputWithSemicolons(
'line start ` after',
'should not be wrapped',
),
},
];
}

View File

@@ -0,0 +1,103 @@
import { getInlinedOutputWithSemicolons, joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities';
import type { PipeTestScenario } from '../PipeTestRunner';
export function createNewlineTests(): PipeTestScenario[] {
return [
{
description: 'does not add semicolon to single line input',
input: 'Write-Host "Single line input"',
expectedOutput: 'Write-Host "Single line input"',
},
{
description: 'does not add semicolon to last line of multiline input',
input: joinAsWindowsLines(
'Write-Host "First line"',
'Write-Host "Second line"',
),
expectedOutput: 'Write-Host "First line"; Write-Host "Second line"',
},
{
description: 'preserves existing semicolons',
input: joinAsWindowsLines(
'line-without-semicolon',
'line-with-semicolon;',
'ending-line',
),
expectedOutput: getInlinedOutputWithSemicolons(
'line-without-semicolon',
'line-with-semicolon',
'ending-line',
),
},
{
description: 'inlines code with \\n newlines',
input:
'$things = Get-ChildItem C:\\Windows\\'
+ '\nforeach ($thing in $things) {'
+ '\nWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\n}',
expectedOutput: getInlinedOutputWithSemicolons(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
{
description: 'removes empty lines',
input:
'$things = Get-ChildItem C:\\Windows\\'
+ '\n\nforeach ($thing in $things) {'
+ '\n\nWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\n\n\n}',
expectedOutput: getInlinedOutputWithSemicolons(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
{
description: 'inlines code with \\r newlines',
input:
'$things = Get-ChildItem C:\\Windows\\'
+ '\rforeach ($thing in $things) {'
+ '\rWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\r}',
expectedOutput: getInlinedOutputWithSemicolons(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
{
description: 'inlines code with mixed newline types',
input:
'$things = Get-ChildItem C:\\Windows\\'
+ '\r\nforeach ($thing in $things) {'
+ '\n\rWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\n\r}',
expectedOutput: getInlinedOutputWithSemicolons(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
{
description: 'trims whitespace from lines',
input:
' $things = Get-ChildItem C:\\Windows\\ '
+ '\nforeach ($thing in $things) {'
+ '\n\tWrite-Host $thing.Name -ForegroundColor Magenta'
+ '\r \n}',
expectedOutput: getInlinedOutputWithSemicolons(
'$things = Get-ChildItem C:\\Windows\\',
'foreach ($thing in $things) {',
'Write-Host $thing.Name -ForegroundColor Magenta',
'}',
),
},
];
}

View File

@@ -0,0 +1,255 @@
import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner';
import { joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities';
export function createScriptBlockTests(): PipeTestScenario[] {
// https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_script_blocks%3Fview=powershell-7.4
return [
{
description: 'simple script block without newlines',
input: '{ Write-Host "Hello, World!" }',
expectedOutput: '{ Write-Host "Hello, World!" }',
},
{
description: 'multiline script block',
input: joinAsWindowsLines(
'{',
' $result = 10 * 5',
' Write-Host "The result is: $result"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = 10 * 5')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "The result is: $result"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.buildRegex(),
},
{
description: 'script block with parameters',
input: joinAsWindowsLines(
'{',
' param($p1, $p2)',
' Write-Host "p1: $p1, p2: $p2"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('param($p1, $p2)')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "p1: $p1, p2: $p2"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.buildRegex(),
},
{
description: 'script block with typed parameters',
input: joinAsWindowsLines(
'{',
' param([int]$p1, [string]$p2)',
' Write-Host "p1: $p1, p2: $p2"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('param([int]$p1, [string]$p2)')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "p1: $p1, p2: $p2"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.buildRegex(),
},
{
description: 'script block with begin, process, and end blocks',
input: joinAsWindowsLines(
'{',
' begin { $total = 0 }',
' process { $total += $_ }',
' end { Write-Host "Total: $total" }',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('begin')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$total = 0')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('process')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$total += $_')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('end')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Total: $total"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'nested script blocks',
input: joinAsWindowsLines(
'{',
' $innerBlock = {',
' param($x)',
' Write-Host "Inner: $x"',
' }',
' & $innerBlock -x "Hello"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$innerBlock =')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('param($x)')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Inner: $x"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('& $innerBlock -x "Hello"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.buildRegex(),
},
{
description: 'script block with return statement',
input: joinAsWindowsLines(
'{',
' $result = 10 * 5',
' return $result',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = 10 * 5')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('return $result')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.buildRegex(),
},
{
description: 'script block with pipeline input',
input: joinAsWindowsLines(
'{',
' process {',
' $_ | Where-Object { $_ % 2 -eq 0 }',
' }',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('process')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$_ | Where-Object { $_ % 2 -eq 0 }')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'script block with dynamicparam block',
input: joinAsWindowsLines(
'{',
' dynamicparam {',
' $paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary',
' return $paramDictionary',
' }',
' process {',
' Write-Host "Processing"',
' }',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('dynamicparam')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('return $paramDictionary')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('process')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Processing"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'script block after closing bracket',
input: joinAsWindowsLines(
'if ($condition) { $value = 10 }',
'$scriptBlock = { param($x) Write-Host $x }',
),
expectedOutput: new RegexBuilder()
.withLiteralString('if ($condition) { $value = 10 }')
.withSemicolon() // Semicolon here to prevent runtime errors
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$scriptBlock = { param($x) Write-Host $x }')
.withOptionalSemicolon()
.buildRegex(),
},
];
}

View File

@@ -0,0 +1,330 @@
import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner';
import { joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities';
export function createSwitchTests(): PipeTestScenario[] {
// https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_switch?view=powershell-7.4
return [
{
description: 'switch statement with no newlines',
input: 'switch ($value) { 1 { Write-Host "One" }; 2 { Write-Host "Two" }; default { Write-Host "Other" } }',
expectedOutput: 'switch ($value) { 1 { Write-Host "One" }; 2 { Write-Host "Two" }; default { Write-Host "Other" } }',
},
{
description: 'simple switch statement (single line inside code block)',
input: joinAsWindowsLines(
'switch ($value) {',
' 1 { Write-Host "One" }',
' 2 { Write-Host "Two" }',
' default { Write-Host "Other" }',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('switch')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($value)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('1')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "One"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('2')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Two"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('default')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Other"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'multiline switch statement',
input: joinAsWindowsLines(
'switch ($value) {',
' 1 {',
' Write-Host "One"',
' $result = 1',
' }',
' 2 {',
' Write-Host "Two"',
' $result = 2',
' }',
' default {',
' Write-Host "Other"',
' $result = 0',
' }',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('switch')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($value)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('1')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "One"')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = 1')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('2')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Two"')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = 2')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('default')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Other"')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = 0')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'nested switch statements',
input: joinAsWindowsLines(
'switch ($outer) {',
' 1 {',
' switch ($inner) {',
' "A" { Write-Host "1A" }',
' "B" { Write-Host "1B" }',
' }',
' }',
' 2 {',
' switch ($inner) {',
' "A" { Write-Host "2A" }',
' "B" { Write-Host "2B" }',
' }',
' }',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('switch')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($outer)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('1')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('switch')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($inner)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('"A"')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "1A"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('"B"')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "1B"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('2')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('switch')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($inner)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('"A"')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "2A"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('"B"')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "2B"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'switch statement with no spaces',
input: joinAsWindowsLines(
'switch($value){1{Write-Host"One"}2{Write-Host"Two"}default{Write-Host"Other"}}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('switch')
.withLiteralString('($value)')
.withLiteralString('{')
.withLiteralString('1')
.withLiteralString('{')
.withLiteralString('Write-Host"One"')
.withOptionalSemicolon()
.withLiteralString('}')
.withLiteralString('2')
.withLiteralString('{')
.withLiteralString('Write-Host"Two"')
.withOptionalSemicolon()
.withLiteralString('}')
.withLiteralString('default')
.withLiteralString('{')
.withLiteralString('Write-Host"Other"')
.withOptionalSemicolon()
.withLiteralString('}')
.withLiteralString('}')
.buildRegex(),
},
{
description: 'switch statement with regex matches',
input: joinAsWindowsLines(
'switch -Regex ($value) {',
' "^A.*" { Write-Host "Starts with A" }',
' ".*Z$" { Write-Host "Ends with Z" }',
' Default { Write-Host "No match" }',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('switch')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('-Regex')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($value)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('"^A.*"')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Starts with A"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('".*Z$"')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Ends with Z"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Default')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "No match"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.buildRegex(),
},
{
description: 'switch after closing bracket',
input: joinAsWindowsLines(
'if ($condition) { $value = 10 }',
'switch ($value) { 1 { "One" } 2 { "Two" } default { "Other" } }',
),
expectedOutput: new RegexBuilder()
.withLiteralString('if ($condition) { $value = 10 }')
.withSemicolon() // Semicolon here to prevent runtime errors
.withOptionalWhitespaceButNoNewline()
.withLiteralString('switch ($value) { 1 { "One" } 2 { "Two" } default { "Other" } }')
.withOptionalSemicolon()
.buildRegex(),
},
];
}

View File

@@ -0,0 +1,451 @@
import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner';
import { getInlinedOutputWithSemicolons, joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities';
export function createTryCatchFinallyTests(): PipeTestScenario[] {
// https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_try_catch_finally?view=powershell-7.4
return [
{
description: 'try/catch/finally block without newlines',
input: 'try { $result = 10 / 0 } catch { Write-Host "An error occurred" } finally { Write-Host "Cleanup" }',
expectedOutput: 'try { $result = 10 / 0 } catch { Write-Host "An error occurred" } finally { Write-Host "Cleanup" }',
},
{
description: 'simple try/catch block (single line inside code blocks)',
input: joinAsWindowsLines(
'try {',
' $result = 10 / 0',
'} catch {',
' Write-Warning "An error occurred"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('try')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = 10 / 0')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('catch {')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Warning "An error occurred"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'multiline try/catch block',
input: joinAsWindowsLines(
'try {',
' $result = 10 / 0',
' Write-Host "Succesfully completed"',
'} catch {',
' Write-Warning "An error occurred"',
' Write-Host "Wrong division?"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('try')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = 10 / 0')
.withSemicolon() // Ensure it adds semicolon to multiline text
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Succesfully completed"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('catch {')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Warning "An error occurred"')
.withSemicolon() // Ensure it adds semicolon to multiline text
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Wrong division?"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'try/finally block',
input: joinAsWindowsLines(
'try {',
' $result = 10 / 0',
'} finally {',
' Write-Warning "An error occurred"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('try')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = 10 / 0')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('finally {')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Warning "An error occurred"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'try/catch/finally block',
input: joinAsWindowsLines(
'try {',
' $result = 10 / 0',
'} catch {',
' Write-Host "An error occurred"',
'} finally {',
' Write-Host "Cleanup"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('try')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = 10 / 0')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('catch {')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "An error occurred"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('finally {')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Cleanup"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'try/catch/finally with closing brackets on separate lines',
input: joinAsWindowsLines(
'try {',
' $result = 10 / 0',
'}',
'catch {',
' Write-Host "An error occurred"',
'}',
'finally {',
' Write-Host "Cleanup"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('try')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = 10 / 0')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline() // No semicolon after this to prevent runtime errors
.withLiteralString('catch')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "An error occurred"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline() // No semicolon after this to prevent runtime errors
.withLiteralString('finally')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Cleanup"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'try/catch with empty catch block',
input: joinAsWindowsLines(
'try {',
' $result = 10 / 0',
'} catch {}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('try')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = 10 / 0')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('catch {}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: '/catch/finally with empty blocks',
input: joinAsWindowsLines(
'try {} catch {} finally {}',
),
expectedOutput: getInlinedOutputWithSemicolons(
'try {} catch {} finally {}',
),
},
{
description: 'try/catch with specific exception type',
input: joinAsWindowsLines(
'try {',
' throw [System.DivideByZeroException]::new()',
'} catch [System.DivideByZeroException] {',
' Write-Host "Caught divide by zero exception"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('try')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('throw [System.DivideByZeroException]::new()')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('catch [System.DivideByZeroException]')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Caught divide by zero exception"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'try/catch with multiple specific exception types',
input: joinAsWindowsLines(
'try {',
' throw [System.IO.FileNotFoundException]::new()',
'} catch [System.IO.FileNotFoundException], [System.IO.DirectoryNotFoundException] {',
' Write-Host "Caught file or directory not found exception"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('try')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('throw [System.IO.FileNotFoundException]::new()')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('catch [System.IO.FileNotFoundException], [System.IO.DirectoryNotFoundException]')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Caught file or directory not found exception"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'try/catch with multiple catch blocks',
input: joinAsWindowsLines(
'try {',
' $result = 10 / 0',
'} catch [System.DivideByZeroException] {',
' Write-Host "Caught divide by zero exception"',
'} catch [System.Exception] {',
' Write-Host "Caught general exception"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('try')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$result = 10 / 0')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('catch [System.DivideByZeroException]')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Caught divide by zero exception"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('catch [System.Exception]')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Caught general exception"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'try/catch with exception variable',
input: joinAsWindowsLines(
'try {',
' throw "Custom error"',
'} catch {',
' Write-Host "Error: $($_.Exception.Message)"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('try')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('throw "Custom error"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('catch')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Error: $($_.Exception.Message)"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'try/catch/finally with return in try block',
input: joinAsWindowsLines(
'try {',
' return "Success"',
'} catch {',
' Write-Host "An error occurred"',
'} finally {',
' Write-Host "Cleanup"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('try')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('return "Success"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('catch')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "An error occurred"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('finally')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Cleanup"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'nested try/catch blocks',
input: joinAsWindowsLines(
'try {',
' try {',
' throw "Inner exception"',
' } catch {',
' throw "Outer exception"',
' }',
'} catch {',
' Write-Host "Caught in outer catch: $($_.Exception.Message)"',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('try')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('try')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('throw "Inner exception"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('catch')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('throw "Outer exception"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('catch')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Caught in outer catch: $($_.Exception.Message)"')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'try after closing bracket',
input: joinAsWindowsLines(
'if ($condition) { $value = 10 }',
'try { $result = 10 / $value }',
),
expectedOutput: new RegexBuilder()
.withLiteralString('if ($condition) { $value = 10 }')
.withSemicolon() // Semicolon here to prevent runtime errors
.withOptionalWhitespaceButNoNewline()
.withLiteralString('try { $result = 10 / $value }')
.withOptionalSemicolon()
.buildRegex(),
},
];
}

View File

@@ -0,0 +1,222 @@
import { RegexBuilder, type PipeTestScenario } from '../PipeTestRunner';
import { joinAsWindowsLines } from './CommonInlinePowerShellTestUtilities';
export function createWhileTests(): PipeTestScenario[] {
return [
{
description: 'while loop without newlines',
input: 'while ($val -ne 3) { $val++ Write-Host $val }',
expectedOutput: 'while ($val -ne 3) { $val++ Write-Host $val }',
},
{
description: 'simple while loop (single line inside code block)',
input: joinAsWindowsLines(
'while ($i -lt 5) {',
' $i++',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('while')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($i -lt 5)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$i++')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'multiline while loop',
input: joinAsWindowsLines(
'while ($condition) {',
' Do-Something',
' Write-Host "Processing..."',
' $condition = Test-Condition',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('while')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($condition)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Do-Something')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host "Processing..."')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$condition = Test-Condition')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'nested while loops',
input: joinAsWindowsLines(
'while ($outerCondition) {',
' $innerCounter = 0',
' while ($innerCounter -lt 3) {',
' Do-InnerTask',
' $innerCounter++',
' }',
' Do-OuterTask',
' $outerCondition = Test-OuterCondition',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('while')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($outerCondition)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$innerCounter = 0')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('while')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($innerCounter -lt 3)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Do-InnerTask')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$innerCounter++')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Do-OuterTask')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$outerCondition = Test-OuterCondition')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'while loop without space before opening parenthesis',
input: joinAsWindowsLines(
'while($val -ne 3) {',
' $val++',
' Write-Host $val',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('while')
.withLiteralString('($val -ne 3)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$val++')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host $val')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'while loop without space after closing parenthesis',
input: joinAsWindowsLines(
'while ($val -ne 3){',
' $val++',
' Write-Host $val',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('while')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($val -ne 3)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$val++')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host $val')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'while loop and trims extra spaces before statements',
input: joinAsWindowsLines(
'while ($val -ne 3) {',
' $val++',
' Write-Host $val',
'}',
),
expectedOutput: new RegexBuilder()
.withLiteralString('while')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($val -ne 3)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$val++')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host $val')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'while loop with closing brace on same line as last statement',
input: joinAsWindowsLines(
'while ($val -ne 3) {',
' $val++',
' Write-Host $val }',
),
expectedOutput: new RegexBuilder()
.withLiteralString('while')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('($val -ne 3)')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('{')
.withOptionalWhitespaceButNoNewline()
.withLiteralString('$val++')
.withSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('Write-Host $val')
.withOptionalSemicolon()
.withOptionalWhitespaceButNoNewline()
.withLiteralString('}')
.withOptionalSemicolon()
.buildRegex(),
},
{
description: 'while after closing bracket',
input: joinAsWindowsLines(
'try { throw "Error" } catch { Write-Host "Caught error" }',
'while ($true) { break }',
),
expectedOutput: new RegexBuilder()
.withLiteralString('try { throw "Error" } catch { Write-Host "Caught error" }')
.withSemicolon() // Semicolon here to prevent runtime errors
.withOptionalWhitespaceButNoNewline()
.withLiteralString('while ($true) { break }')
.withOptionalSemicolon()
.buildRegex(),
},
];
}

View File

@@ -1,19 +1,71 @@
import { it, expect } from 'vitest';
import type { IPipe } from '@/application/Parser/Executable/Script/Compiler/Expressions/Pipes/IPipe';
import type { Pipe } from '@/application/Parser/Executable/Script/Compiler/Expressions/Pipes/Pipe';
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
import { indentText } from '@/application/Common/Text/IndentText';
export interface IPipeTestCase {
readonly name: string;
export interface PipeTestScenario {
readonly description: string;
readonly input: string;
readonly expectedOutput: string;
readonly expectedOutput: RegExp | string;
}
export function runPipeTests(sut: IPipe, testCases: IPipeTestCase[]) {
for (const testCase of testCases) {
it(testCase.name, () => {
export function runPipeTests(
pipe: Pipe,
testScenarios: readonly PipeTestScenario[],
) {
testScenarios.forEach((
{ input, description, expectedOutput: expectedInlinedOutput },
) => {
it(description, () => {
// act
const actual = sut.apply(testCase.input);
const actualOutput = pipe.apply(input);
// assert
expect(actual).to.equal(testCase.expectedOutput);
if (typeof expectedInlinedOutput === 'string') {
expect(actualOutput).to.equal(expectedInlinedOutput);
} else {
expect(actualOutput).to.match(expectedInlinedOutput, formatAssertionMessage([
'Regex did not match the output.',
'Expected regex pattern:',
indentText(expectedInlinedOutput.toString()),
'Actual output:',
indentText(actualOutput),
'Given input:',
indentText(input),
]));
}
});
});
}
export class RegexBuilder {
private rawRegex: string = '';
public withLiteralString(string: string): this {
this.rawRegex += string.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
return this;
}
public withSomeWhitespaceButNoNewLine(): this {
this.rawRegex += '[ \\t\\f]+';
return this;
}
public withOptionalWhitespaceButNoNewline(): this {
this.rawRegex += '[ \\t\\f]*';
return this;
}
public withOptionalSemicolon(): this {
this.rawRegex += ';?';
return this;
}
public withSemicolon(): this {
this.rawRegex += ';';
return this;
}
public buildRegex(): RegExp {
return new RegExp(this.rawRegex, 'g');
}
}