Skip to content

Commit 78e7392

Browse files
Merge pull request diffblue#545 from diffblue/jd/feature/make_di_java_compile
[SEC-604] Fix DI compilation errors
2 parents 23579f4 + 4b8a9d4 commit 78e7392

File tree

6 files changed

+247
-82
lines changed

6 files changed

+247
-82
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import * as fs from "fs-extra";
2+
import { ConfigParseError } from "./configParseError";
3+
import { Argument, EntryPoint, EntryPointConfig } from "./entryPointConfig";
4+
import { Cast, Class, Declaration, Expression, FnCall, FnStatement, Method, Null, Statement, StringConstant } from "./javaCode";
5+
import { Repository } from "./model";
6+
import { makeIdentifier } from "./utils";
7+
8+
export async function processEntryPointConfigAsync(filePath: string): Promise<Class> {
9+
let json: string;
10+
try {
11+
json = await fs.readFile(filePath, "utf8");
12+
} catch (err) {
13+
throw new ConfigParseError(`Can't open input file '${filePath}': ${err.message}`);
14+
}
15+
const entryPointConfig: EntryPointConfig = JSON.parse(json);
16+
return createEntryPointClass(entryPointConfig.entryPoints);
17+
}
18+
19+
export function createEntryPointClass(entryPoints: EntryPoint[]): Class {
20+
return new Class(
21+
"com.diffblue.security", "public", "SyntheticEntryPoints", undefined, [], entryPoints.map(createEntryPoint));
22+
}
23+
24+
function createEntryPoint(entryPoint: EntryPoint): Method {
25+
let isContextRequired = false;
26+
let mainClassConstruction;
27+
[ mainClassConstruction, isContextRequired ] = getClassConstructionCall(entryPoint.className, isContextRequired);
28+
let argConstructionCalls;
29+
[ argConstructionCalls, isContextRequired ] = entryPoint.method.arguments.reduce(
30+
function ([ args, icr ]: [ Expression[], boolean ], arg: Argument): [ Expression[], boolean ] {
31+
let argConstructionCall;
32+
[ argConstructionCall, icr ] = getClassConstructionCall(arg.type, icr);
33+
return [ [ ...args, argConstructionCall ], icr ];
34+
},
35+
<[ Expression[], boolean ]>[ [], isContextRequired ]);
36+
37+
const statements: Statement[] = [];
38+
if (isContextRequired)
39+
statements.push(
40+
new Declaration("org.springframework.context.support.ClassPathXmlApplicationContext", "appContext",
41+
new FnCall("new org.springframework.context.support.ClassPathXmlApplicationContext", [])));
42+
43+
statements.push(new Declaration(entryPoint.className, "mainClass", mainClassConstruction));
44+
const entryPointCall = new FnCall(`mainClass.${entryPoint.method.name}`, argConstructionCalls);
45+
// TODO: For particular types where we need an artificlal sink, test entryPoint.method.returnType, then insert a Declaration and a call to the sink
46+
statements.push(new FnStatement(entryPointCall));
47+
48+
return new Method(`public static void ${makeIdentifier(entryPoint.className)}_${makeIdentifier(entryPoint.method.name)}_entryPoint()`,
49+
statements, ["Exception"]);
50+
}
51+
52+
function getClassConstructionCall(className: string, isContextRequired: boolean): [ Expression, boolean ] {
53+
const classBeanId = Repository.tryGetIdByClass(className);
54+
if (classBeanId === undefined) {
55+
return [ new FnCall("org.cprover.CProver.nondetWithoutNull", [new Null()]), isContextRequired ];
56+
} else {
57+
const classBean = Repository.tryGet(classBeanId);
58+
if (classBean === undefined)
59+
throw new Error("Bean with identifier returned by tryGetIdByClass does not exist");
60+
return [ new Cast(className, new FnCall("appContext.getBean", [ new StringConstant(classBean.name) ])), true ];
61+
}
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export interface EntryPointConfig {
2+
entryPoints: EntryPoint[];
3+
}
4+
5+
export interface EntryPoint {
6+
className: string;
7+
method: Method;
8+
}
9+
10+
export interface Method {
11+
name: string;
12+
arguments: Argument[];
13+
returnType: string;
14+
signature: string;
15+
}
16+
17+
export interface Argument {
18+
type: string;
19+
}

env-model-generator/src/env-model-generator.ts

+38-75
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44

55
import * as program from "commander";
66
import * as fs from "fs-extra";
7-
import * as _ from "lodash";
87
import * as pathUtils from "path";
9-
import { BlankLine, CatchBlock, Class, FnCall, IfThenElse, Method, Null, Return, Statement, StringConstant, TryCatch } from "./javaCode";
10-
import * as model from "./model";
11-
import parseSpringConfigFiles from "./parserDriver";
8+
import { processEntryPointConfigAsync } from "./createEntryPoint";
9+
import { Class } from "./javaCode";
10+
import parseSpringConfigFiles, { createApplicationContextClass } from "./parserDriver";
1211
import { createPath } from "./utils";
1312

1413
function collectOption(value: string, collection?: string[]) {
@@ -21,7 +20,8 @@ function collectOption(value: string, collection?: string[]) {
2120
program
2221
.version("0.0.1")
2322
.arguments("<input-file>")
24-
.option("-i --input-file <path>", "Additional DI configuration file to read", collectOption)
23+
.option("-i --di-input-file <path>", "Additional DI configuration file to read", collectOption)
24+
.option("-e --entry-points-input-file <path>", "Entry point configuration file to read")
2525
.option("-o --output-path <output-path>", "Path to write output under")
2626
.description("Create code from DI configuration")
2727
.action(transformConfigFiles);
@@ -42,100 +42,63 @@ enum ReturnValue {
4242

4343
async function transformConfigFiles(fileName: string, options: any) {
4444
let fileNames = [ fileName ];
45-
if (options.inputFile)
46-
fileNames = fileNames.concat(options.inputFile);
45+
if (options.diInputFile)
46+
fileNames = fileNames.concat(options.diInputFile);
4747
try {
4848
await parseSpringConfigFiles(fileNames);
4949
} catch (err) {
5050
console.error(`Error parsing config file: ${err.message}`);
5151
process.exit(ReturnValue.ParseError);
5252
return;
5353
}
54-
const packageName = "org.springframework.context.support";
55-
let output = `package ${packageName};\n\n`;
54+
let applicationContext;
5655
try {
57-
const applicationContext =
58-
new Class(
59-
"public", "ClassPathXmlApplicationContext", undefined, [ "org.springframework.context.ApplicationContext" ],
60-
[
61-
new Method(
62-
"public ClassPathXmlApplicationContext(String[] args)",
63-
[]),
64-
new Method(
65-
"public Object getBean(String name)",
66-
[
67-
new TryCatch(
68-
(<Statement[]>[]).concat(
69-
model.Repository.getNamed()
70-
.sort((a, b) => a.name < b.name ? -1 : a.name === b.name ? 0 : 1)
71-
.map((bean) =>
72-
new IfThenElse(
73-
new FnCall("name.equals", [ new StringConstant(bean.name) ]),
74-
[
75-
new Return(new FnCall(bean.getter, [])),
76-
])),
77-
[...model.Repository.getAliases()].map(
78-
function ([ alias, beanId ]) {
79-
const bean = model.Repository.tryGet(beanId);
80-
if (bean === undefined)
81-
throw new Error("Found alias to bean that doesn't exist");
82-
return new IfThenElse(
83-
new FnCall("name.equals", [ new StringConstant(alias) ]),
84-
[
85-
new Return(new FnCall(bean.getter, [])),
86-
]);
87-
}),
88-
),
89-
[ new CatchBlock("Exception") ]),
90-
new Return(new Null()),
91-
],
92-
[],
93-
[ "OverlayMethodImplementation" ]
94-
),
95-
new BlankLine(),
96-
new BlankLine(),
97-
..._.flatMap([...model.Repository.getAll()], (bean) => bean.javaMembers),
98-
new BlankLine(),
99-
...model.PropertiesValue.utilityMethods(),
100-
...model.MapValue.utilityMethods(),
101-
...model.BeanValue.utilityMethods(),
102-
],
103-
[ "OverlayClassImplementation" ]);
104-
output += applicationContext.toString() + "\n";
56+
applicationContext = createApplicationContextClass();
10557
} catch (err) {
10658
console.error(`Error converting to Java: ${err.message}`);
10759
process.exit(ReturnValue.CreateJavaError);
10860
return;
10961
}
62+
63+
if (options.outputPath && !fs.existsSync(options.outputPath)) {
64+
console.error(`Given output folder '${options.outputPath}' does not exist`);
65+
process.exit(ReturnValue.OutputFolderNotFound);
66+
return;
67+
}
68+
11069
// Write out the generated Java
111-
let outputPath = options.outputPath;
112-
if (outputPath) {
113-
if (!fs.existsSync(outputPath)) {
114-
console.error(`Given output folder '${outputPath}' does not exist`);
115-
process.exit(ReturnValue.OutputFolderNotFound);
116-
return;
117-
}
118-
// Create folder to store it in based on the package name
119-
const appContextFolder = pathUtils.join(...packageName.split("."));
70+
await saveGeneratedFile(applicationContext, options);
71+
72+
if (options.entryPointsInputFile) {
73+
await saveGeneratedFile(await processEntryPointConfigAsync(options.entryPointsInputFile), options);
74+
}
75+
76+
// Success
77+
process.exit();
78+
}
79+
80+
async function saveGeneratedFile(saveableType: Class, options: any) {
81+
if (options.outputPath) {
82+
let outputPath;
12083
try {
121-
outputPath = createPath(appContextFolder, outputPath);
84+
outputPath = createPath(saveableType.folderPath, options.outputPath);
12285
} catch (err) {
123-
console.error(`Can't create output folder '${appContextFolder}': ${err.message}`);
86+
console.error(`Can't create output folder '${saveableType.folderPath}': ${err.message}`);
12487
process.exit(ReturnValue.OutputError);
12588
return;
12689
}
90+
12791
// Write file
128-
const outputFileName = pathUtils.resolve(outputPath, "ClassPathXmlApplicationContext.java");
92+
const outputFileName = pathUtils.resolve(outputPath, saveableType.fileName);
12993
try {
130-
await fs.writeFile(outputFileName, output, "utf8");
131-
console.log("Output written to %s", outputFileName);
94+
await fs.writeFile(outputFileName, saveableType.toString(), "utf8");
95+
console.log(`Wrote ${outputFileName}`);
13296
} catch (err) {
13397
console.error(`Can't write output file '${outputFileName}': ${err.message}`);
13498
process.exit(ReturnValue.OutputError);
13599
return;
136100
}
137-
} else
138-
console.log(output);
139-
// Success
140-
process.exit();
101+
} else {
102+
console.log(saveableType.toString());
103+
}
141104
}

env-model-generator/src/javaCode.ts

+65-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as _ from "lodash";
2+
import * as pathUtils from "path";
23

34
const maxLineLength = 150;
45

@@ -199,6 +200,24 @@ export class Assignment extends CodeElement implements Statement {
199200
}
200201
}
201202

203+
export class Cast extends CodeElement implements Expression {
204+
public isExpression: true = true;
205+
private castType: string;
206+
private expression: Expression;
207+
208+
public constructor(castType: string, expression: Expression) {
209+
super();
210+
this.castType = castType;
211+
this.expression = expression;
212+
}
213+
214+
public flatten(): string[] {
215+
return [
216+
`(${this.castType})${this.expression.flatten()}`,
217+
];
218+
}
219+
}
220+
202221
export class Declaration extends CodeElement implements Statement, Member {
203222
public isStatement: true = true;
204223
public isMember: true = true;
@@ -498,25 +517,39 @@ export class RangedFor extends CodeElement implements Statement {
498517
}
499518

500519
export class Class extends CodeElement {
520+
private packageName: string;
501521
private modifiers: string;
502522
private name: string;
503523
private baseClass: string | undefined;
504524
private interfaces: string[];
505525
private members: Member[];
506526
private annotations: string[];
527+
protected classType: "class" | "interface" | "@interface";
507528

508529
public constructor(
509-
modifiers: string, name: string, baseClass: string | undefined, interfaces: string[], members: Member[], annotations?: string[])
530+
packageName: string,
531+
modifiers: string,
532+
name: string,
533+
baseClass: string | undefined,
534+
interfaces: string[],
535+
members: Member[],
536+
annotations?: string[])
510537
{
511538
super();
539+
this.packageName = packageName;
512540
this.modifiers = modifiers;
513541
this.name = name;
514542
this.baseClass = baseClass;
515543
this.interfaces = interfaces;
516544
this.members = members;
517545
this.annotations = annotations === undefined ? [] : annotations;
546+
this.classType = "class";
518547
}
519548

549+
public get folderPath() { return pathUtils.join(...this.packageName.split(".")); }
550+
551+
public get fileName() { return this.name + ".java"; }
552+
520553
public flatten(): string[] {
521554
const members = _.flatMap(this.members, (value, index, array) =>
522555
index !== array.length - 1 && !(value instanceof BlankLine)
@@ -525,9 +558,39 @@ export class Class extends CodeElement {
525558
const interfacesString = this.interfaces.length === 0 ? "" : ` implements ${this.interfaces.join(", ")}`;
526559
return [
527560
...this.annotations.map((annotation) => "@" + annotation),
528-
`${this.modifiers} class ${this.name}${baseClassString}${interfacesString} {`,
561+
`${this.modifiers} ${this.classType} ${this.name}${baseClassString}${interfacesString} {`,
529562
..._.flatMap(members, (member) => member.flatten()).map(indent),
530563
"}",
531564
];
532565
}
566+
567+
public toString() { return `package ${this.packageName};\n\n` + super.toString() + "\n"; }
568+
}
569+
570+
export class Interface extends Class {
571+
public constructor(
572+
packageName: string,
573+
modifiers: string,
574+
name: string,
575+
baseClass: string | undefined,
576+
members?: Member[],
577+
annotations?: string[])
578+
{
579+
super(packageName, modifiers, name, baseClass, [], members ? members : [], annotations);
580+
this.classType = "interface";
581+
}
582+
}
583+
584+
export class AnnotationInterface extends Class {
585+
public constructor(
586+
packageName: string,
587+
modifiers: string,
588+
name: string,
589+
baseClass?: string | undefined,
590+
members?: Member[],
591+
annotations?: string[])
592+
{
593+
super(packageName, modifiers, name, baseClass, [], members ? members : [], annotations);
594+
this.classType = "@interface";
595+
}
533596
}

env-model-generator/src/model.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export class Bean implements IBean {
179179
}
180180
if (this.isLazyInit) {
181181
this._javaMembers.push(new Method(
182-
`public static ${typeName} ${this.getter}()`,
182+
`public ${typeName} ${this.getter}()`,
183183
[
184184
new Declaration(typeName, variableName, new FnCall(creatorFn, constructorArgs)),
185185
...initialisers,
@@ -190,7 +190,7 @@ export class Bean implements IBean {
190190
// Bean is singleton, generate static member variable to hold singleton instance
191191
this._javaMembers.push(new Declaration(`private static ${typeName}`, variableName));
192192
this._javaMembers.push(new Method(
193-
`public static ${typeName} ${this.getter}()`,
193+
`public ${typeName} ${this.getter}()`,
194194
[
195195
new IfThenElse(
196196
new BinaryOperator(new Symbol(variableName), "==", new Null()),
@@ -238,9 +238,9 @@ export class UndefinedBean implements IBean {
238238
public get javaMembers(): Member[] {
239239
return [
240240
new Method(
241-
`public static ${this.typeName} ${this.getter}()`,
241+
`public ${this.typeName} ${this.getter}()`,
242242
[
243-
new Return(new FnCall("org.cprover.CProver.nondetWithNull", [])),
243+
new Return(new FnCall("org.cprover.CProver.nondetWithNull", [new Null()])),
244244
],
245245
[]),
246246
new BlankLine(),
@@ -275,7 +275,7 @@ export class ApplicationContextBean implements IBean {
275275
public get javaMembers(): Member[] {
276276
return [
277277
new Method(
278-
`public static ${ApplicationContextBean.typeName} ${this.getter}()`,
278+
`public ${ApplicationContextBean.typeName} ${this.getter}()`,
279279
[
280280
new Return(new Symbol("this")),
281281
],

0 commit comments

Comments
 (0)