Skip to content

Commit

Permalink
Fix PowerShell code block inlining in compiler
Browse files Browse the repository at this point in the history
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
  • Loading branch information
undergroundwires committed Aug 5, 2024
1 parent f89c232 commit 15d7877
Show file tree
Hide file tree
Showing 25 changed files with 3,610 additions and 492 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface IPipe {
export interface Pipe {
readonly name: string;
apply(input: string): string;
}
Original file line number Diff line number Diff line change
@@ -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 */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
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 {
if (!code || !hasLines(code)) {
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);
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -155,9 +157,32 @@ function mergeLinesWithBacktick(code: string) {
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
}

/**
* 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 | <newline> Opening bracket <newline>
.replace(/(?<=.*)\s*{[\r\n][\s\r\n]*/g, ' { ')
// Closing bracket: Inline `}` at the end of line
.replace(/\s*}[\r\n][\s\r\n]*/g, ' } ');
}

function mergeNewLines(code: string) {
return splitTextIntoLines(code)
const nonEmptyLines = splitTextIntoLines(code)
.map((line) => line.trim())
.filter((line) => line.length > 0)
.join('; ');
.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(' ');
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
import type { IPipe } from './IPipe';
import type { Pipe } from './Pipe';

const RegisteredPipes = [
new EscapeDoubleQuotes(),
new InlinePowerShell(),
];

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) {
Expand All @@ -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}"`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
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
const sut = new 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"^"""^""',
},
Expand Down
Loading

0 comments on commit 15d7877

Please sign in to comment.