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,4 +1,4 @@
export interface IPipe {
export interface Pipe {
readonly name: string;
apply(input: string): string;
}

View File

@@ -1,11 +1,11 @@
import type { IPipe } from '../IPipe';
import type { Pipe } from '../Pipe';
export class EscapeDoubleQuotes implements IPipe {
export class EscapeDoubleQuotes implements Pipe {
public readonly name: string = 'escapeDoubleQuotes';
public apply(raw: string): string {
if (!raw) {
return raw;
return '';
}
return raw.replaceAll('"', '"^""');
/* eslint-disable vue/max-len */

View File

@@ -1,7 +1,7 @@
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
import type { IPipe } from '../IPipe';
import type { Pipe } from '../Pipe';
export class InlinePowerShell implements IPipe {
export class InlinePowerShell implements Pipe {
public readonly name: string = 'inlinePowerShell';
public apply(code: string): string {
@@ -9,9 +9,11 @@ export class InlinePowerShell implements IPipe {
return code;
}
const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent"
// Order is important
inlineComments,
mergeLinesWithBacktick,
mergeHereStrings,
mergeLinesWithBacktick,
mergeLinesWithBracketCodeBlocks,
mergeNewLines,
]).reduce((a, b) => (data) => b(a(data)));
const newCode = processor(code);
@@ -105,12 +107,12 @@ function mergeHereStrings(code: string) {
return quoted;
});
}
interface IInlinedHereString {
interface InlinedHereString {
readonly quotesAround: string;
readonly escapedQuotes: string;
readonly separator: string;
}
function getHereStringHandler(quotes: string): IInlinedHereString {
function getHereStringHandler(quotes: string): InlinedHereString {
/*
We handle @' and @" differently.
Single quotes are interpreted literally and doubles are expandable.
@@ -155,9 +157,33 @@ function mergeLinesWithBacktick(code: string) {
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
}
function mergeNewLines(code: string) {
return splitTextIntoLines(code)
.map((line) => line.trim())
.filter((line) => line.length > 0)
.join('; ');
/**
* Inlines code blocks in PowerShell scripts while preserving correct syntax.
* It removes unnecessary newlines and spaces around brackets,
* inlining the code where possible.
* This prevents syntax errors like "Unexpected token '}'" when inlining brackets.
*/
function mergeLinesWithBracketCodeBlocks(code: string): string {
return code
// Opening bracket: [whitespace] Opening bracket (newline)
.replace(/(?<=.*)\s*{[\r\n][\s\r\n]*/g, ' { ')
// Closing bracket: [whitespace] Closing bracket (newline) (continuation keyword)
.replace(/\s*}[\r\n][\s\r\n]*(?=elseif|else|catch|finally|until)/g, ' } ')
.replace(/(?<=do\s*{.*)[\r\n\s]*}[\r\n][\r\n\s]*(?=while)/g, ' } '); // Do-While
}
function mergeNewLines(code: string) {
const nonEmptyLines = splitTextIntoLines(code)
.map((line) => line.trim())
.filter((line) => line.length > 0);
return nonEmptyLines
.map((line, index) => {
const isLastLine = index === nonEmptyLines.length - 1;
if (isLastLine) {
return line;
}
return line.endsWith(';') ? line : `${line};`;
})
.join(' ');
}

View File

@@ -1,6 +1,6 @@
import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
import type { IPipe } from './IPipe';
import type { Pipe } from './Pipe';
const RegisteredPipes = [
new EscapeDoubleQuotes(),
@@ -8,19 +8,19 @@ const RegisteredPipes = [
];
export interface IPipeFactory {
get(pipeName: string): IPipe;
get(pipeName: string): Pipe;
}
export class PipeFactory implements IPipeFactory {
private readonly pipes = new Map<string, IPipe>();
private readonly pipes = new Map<string, Pipe>();
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
constructor(pipes: readonly Pipe[] = RegisteredPipes) {
for (const pipe of pipes) {
this.registerPipe(pipe);
}
}
public get(pipeName: string): IPipe {
public get(pipeName: string): Pipe {
validatePipeName(pipeName);
const pipe = this.pipes.get(pipeName);
if (!pipe) {
@@ -29,7 +29,7 @@ export class PipeFactory implements IPipeFactory {
return pipe;
}
private registerPipe(pipe: IPipe): void {
private registerPipe(pipe: Pipe): void {
validatePipeName(pipe.name);
if (this.pipes.has(pipe.name)) {
throw new Error(`Pipe name must be unique: "${pipe.name}"`);