Skip to content

Commit caf8d76

Browse files
committed
pmdwrapper for ast dump
1 parent cd70561 commit caf8d76

2 files changed

Lines changed: 265 additions & 0 deletions

File tree

packages/code-analyzer-pmd-engine/src/pmd-wrapper.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ export type PmdProcessingError = {
4646
detail: string
4747
}
4848

49+
export type PmdAstDumpInputData = {
50+
language: string
51+
fileToDump: string
52+
encoding?: string
53+
}
54+
55+
export type PmdAstDumpResults = {
56+
file: string
57+
ast: string | null
58+
error: PmdProcessingError | null
59+
}
60+
4961
const STDOUT_PROGRESS_MARKER = '[Progress]';
5062
const STDOUT_ERROR_MARKER = '[Error] ';
5163
const STDOUT_WARNING_MARKER = '[Warning] ';
@@ -148,6 +160,62 @@ export class PmdWrapperInvoker {
148160
throw new Error(getMessageFromCatalog(SHARED_MESSAGE_CATALOG, 'ErrorParsingOutputFile', resultsOutputFile, errMsg), {cause: err});
149161
}
150162
}
163+
164+
async invokeAstDumpCommand(
165+
language: string,
166+
fileToDump: string,
167+
workingFolder: string,
168+
encoding: string = 'UTF-8',
169+
emitProgress: (percComplete: number) => void
170+
): Promise<PmdAstDumpResults> {
171+
172+
emitProgress(5);
173+
174+
// Prepare input data
175+
const inputData: PmdAstDumpInputData = {
176+
language: language,
177+
fileToDump: fileToDump,
178+
encoding: encoding
179+
};
180+
181+
const inputFile: string = path.join(workingFolder, 'astDumpInput.json');
182+
await fs.promises.writeFile(inputFile, JSON.stringify(inputData), 'utf-8');
183+
emitProgress(10);
184+
185+
const resultsOutputFile: string = path.join(workingFolder, 'astDumpResults.json');
186+
const javaCmdArgs: string[] = [PMD_WRAPPER_JAVA_CLASS, 'ast-dump', inputFile, resultsOutputFile];
187+
const javaClassPaths: string[] = [
188+
path.join(PMD_WRAPPER_LIB_FOLDER, '*'),
189+
...this.userProvidedJavaClasspathEntries.map(toJavaClasspathEntry)
190+
];
191+
192+
this.emitLogEvent(LogLevel.Fine, `Calling AST dump for file: ${fileToDump}`);
193+
194+
await this.javaCommandExecutor.exec(javaCmdArgs, javaClassPaths, (stdOutMsg: string) => {
195+
if (stdOutMsg.startsWith(STDOUT_ERROR_MARKER)) {
196+
const errorMessage: string = stdOutMsg.slice(STDOUT_ERROR_MARKER.length).replaceAll('{NEWLINE}','\n');
197+
throw new Error(errorMessage);
198+
} else if (stdOutMsg.startsWith(STDOUT_WARNING_MARKER)) {
199+
const warningMessage: string = stdOutMsg.slice(STDOUT_WARNING_MARKER.length).replaceAll('{NEWLINE}','\n');
200+
this.emitLogEvent(LogLevel.Warn, `[JAVA StdOut]: ${warningMessage}`);
201+
} else {
202+
this.emitLogEvent(LogLevel.Fine, `[JAVA StdOut]: ${stdOutMsg}`);
203+
}
204+
});
205+
206+
emitProgress(95);
207+
208+
// Read and parse results
209+
try {
210+
const resultsFileContents: string = await fs.promises.readFile(resultsOutputFile, 'utf-8');
211+
const results: PmdAstDumpResults = JSON.parse(resultsFileContents);
212+
emitProgress(100);
213+
return results;
214+
} catch (err) /* istanbul ignore next */ {
215+
const errMsg: string = err instanceof Error ? err.message : String(err);
216+
throw new Error(getMessageFromCatalog(SHARED_MESSAGE_CATALOG, 'ErrorParsingOutputFile', resultsOutputFile, errMsg), {cause: err});
217+
}
218+
}
151219
}
152220

153221
function createRuleSetFileContentsFor(pmdRuleInfoList: PmdRuleInfo[]): string {
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import {changeWorkingDirectoryToPackageRoot} from "./test-helpers";
2+
import {LogLevel} from "@salesforce/code-analyzer-engine-api";
3+
import {JavaCommandExecutor} from "@salesforce/code-analyzer-engine-api/utils";
4+
import {PmdWrapperInvoker, PmdAstDumpResults} from "../src/pmd-wrapper";
5+
import path from "node:path";
6+
import fs from "node:fs";
7+
import os from "node:os";
8+
9+
changeWorkingDirectoryToPackageRoot();
10+
11+
const TEST_DATA_FOLDER: string = path.join(__dirname, 'test-data');
12+
13+
describe('Tests for invokeAstDumpCommand method of PmdWrapperInvoker', () => {
14+
let javaCommandExecutor: JavaCommandExecutor;
15+
let pmdWrapperInvoker: PmdWrapperInvoker;
16+
let workingFolder: string;
17+
let logEvents: Array<{level: LogLevel, message: string}>;
18+
19+
beforeEach(() => {
20+
javaCommandExecutor = new JavaCommandExecutor();
21+
logEvents = [];
22+
pmdWrapperInvoker = new PmdWrapperInvoker(
23+
javaCommandExecutor,
24+
[],
25+
(level: LogLevel, message: string) => {
26+
logEvents.push({level, message});
27+
}
28+
);
29+
workingFolder = fs.mkdtempSync(path.join(os.tmpdir(), 'pmd-ast-dump-test-'));
30+
});
31+
32+
afterEach(() => {
33+
// Clean up working folder
34+
if (fs.existsSync(workingFolder)) {
35+
fs.rmSync(workingFolder, {recursive: true, force: true});
36+
}
37+
});
38+
39+
it('When calling invokeAstDumpCommand with valid Apex file, then AST is generated successfully', async () => {
40+
const apexFile = path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'sampleViolations', 'AvoidDebugStatements.cls');
41+
const progressEvents: number[] = [];
42+
43+
const results: PmdAstDumpResults = await pmdWrapperInvoker.invokeAstDumpCommand(
44+
'apex',
45+
apexFile,
46+
workingFolder,
47+
'UTF-8',
48+
(progress: number) => progressEvents.push(progress)
49+
);
50+
51+
// Assert results
52+
expect(results.file).toBe(apexFile);
53+
expect(results.ast).toBeDefined();
54+
expect(results.ast).not.toBeNull();
55+
expect(results.ast!).toContain('<?xml version');
56+
expect(results.ast!).toContain('<ApexFile');
57+
expect(results.error).toBeUndefined();
58+
59+
// Assert progress events
60+
expect(progressEvents).toContain(5);
61+
expect(progressEvents).toContain(10);
62+
expect(progressEvents).toContain(95);
63+
expect(progressEvents).toContain(100);
64+
65+
// Assert log events
66+
const fineLogEvents = logEvents.filter(e => e.level === LogLevel.Fine);
67+
expect(fineLogEvents.length).toBeGreaterThan(0);
68+
expect(fineLogEvents.some(e => e.message.includes('Calling AST dump'))).toBe(true);
69+
});
70+
71+
it('When calling invokeAstDumpCommand with valid Visualforce file, then AST is generated successfully', async () => {
72+
const vfFile = path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'sampleViolations', 'VfUnescapeEl.page');
73+
const progressEvents: number[] = [];
74+
75+
const results: PmdAstDumpResults = await pmdWrapperInvoker.invokeAstDumpCommand(
76+
'visualforce',
77+
vfFile,
78+
workingFolder,
79+
'UTF-8',
80+
(progress: number) => progressEvents.push(progress)
81+
);
82+
83+
// Assert results
84+
expect(results.file).toBe(vfFile);
85+
expect(results.ast).toBeDefined();
86+
expect(results.ast).not.toBeNull();
87+
expect(results.ast!).toContain('<?xml version');
88+
expect(results.error).toBeUndefined();
89+
90+
// Assert progress events include start and end
91+
expect(progressEvents).toContain(5);
92+
expect(progressEvents).toContain(100);
93+
});
94+
95+
it('When calling invokeAstDumpCommand with non-existent file, then error is returned', async () => {
96+
const nonExistentFile = path.join(workingFolder, 'DoesNotExist.cls');
97+
98+
const results: PmdAstDumpResults = await pmdWrapperInvoker.invokeAstDumpCommand(
99+
'apex',
100+
nonExistentFile,
101+
workingFolder,
102+
'UTF-8',
103+
() => {}
104+
);
105+
106+
// Assert error is returned
107+
expect(results.file).toBe(nonExistentFile);
108+
expect(results.ast).toBeFalsy(); // null or undefined
109+
expect(results.error).toBeDefined();
110+
expect(results.error!.file).toBe(nonExistentFile);
111+
expect(results.error!.message).toContain('File not found');
112+
});
113+
114+
it('When calling invokeAstDumpCommand with invalid language, then error is returned', async () => {
115+
const apexFile = path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'sampleViolations', 'AvoidDebugStatements.cls');
116+
117+
const results: PmdAstDumpResults = await pmdWrapperInvoker.invokeAstDumpCommand(
118+
'invalid_language',
119+
apexFile,
120+
workingFolder,
121+
'UTF-8',
122+
() => {}
123+
);
124+
125+
// Assert error is returned
126+
expect(results.file).toBe(apexFile);
127+
expect(results.ast).toBeFalsy(); // null or undefined
128+
expect(results.error).toBeDefined();
129+
expect(results.error!.message).toContain('Language not supported');
130+
});
131+
132+
it('When calling invokeAstDumpCommand with invalid Apex syntax, then error is returned', async () => {
133+
// Create a file with invalid Apex syntax
134+
const invalidApexFile = path.join(workingFolder, 'Invalid.cls');
135+
const invalidApexCode = 'public class Invalid {\n #### SYNTAX ERROR ####\n}';
136+
fs.writeFileSync(invalidApexFile, invalidApexCode, 'utf-8');
137+
138+
const results: PmdAstDumpResults = await pmdWrapperInvoker.invokeAstDumpCommand(
139+
'apex',
140+
invalidApexFile,
141+
workingFolder,
142+
'UTF-8',
143+
() => {}
144+
);
145+
146+
// Assert error is returned
147+
expect(results.file).toBe(invalidApexFile);
148+
expect(results.ast).toBeFalsy(); // null or undefined
149+
expect(results.error).toBeDefined();
150+
expect(results.error!.file).toBe(invalidApexFile);
151+
// Error message should indicate parsing issue
152+
expect(results.error!.message.length).toBeGreaterThan(0);
153+
});
154+
155+
it('When calling invokeAstDumpCommand with valid encoding parameter, then AST is generated', async () => {
156+
const apexFile = path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'sampleViolations', 'AvoidDebugStatements.cls');
157+
158+
const results: PmdAstDumpResults = await pmdWrapperInvoker.invokeAstDumpCommand(
159+
'apex',
160+
apexFile,
161+
workingFolder,
162+
'UTF-8', // Use UTF-8 since the file is actually UTF-8
163+
() => {}
164+
);
165+
166+
// Should succeed
167+
expect(results.file).toBe(apexFile);
168+
expect(results.ast).toBeDefined();
169+
expect(results.ast).not.toBeNull();
170+
expect(results.error).toBeUndefined();
171+
});
172+
173+
it('When calling invokeAstDumpCommand, then input and output files are created in working folder', async () => {
174+
const apexFile = path.join(TEST_DATA_FOLDER, 'samplePmdWorkspace', 'sampleViolations', 'AvoidDebugStatements.cls');
175+
176+
await pmdWrapperInvoker.invokeAstDumpCommand(
177+
'apex',
178+
apexFile,
179+
workingFolder,
180+
'UTF-8',
181+
() => {}
182+
);
183+
184+
// Verify input file was created
185+
const inputFile = path.join(workingFolder, 'astDumpInput.json');
186+
expect(fs.existsSync(inputFile)).toBe(true);
187+
188+
const inputData = JSON.parse(fs.readFileSync(inputFile, 'utf-8'));
189+
expect(inputData.language).toBe('apex');
190+
expect(inputData.fileToDump).toBe(apexFile);
191+
expect(inputData.encoding).toBe('UTF-8');
192+
193+
// Verify output file was created
194+
const outputFile = path.join(workingFolder, 'astDumpResults.json');
195+
expect(fs.existsSync(outputFile)).toBe(true);
196+
});
197+
});

0 commit comments

Comments
 (0)