Skip to content

Commit 280b83d

Browse files
Merge pull request diffblue#500 from diffblue/feature/annotation-driven-ai
Annotation driven DI [SEC-556]
2 parents 1b5f4fd + 3058af5 commit 280b83d

11 files changed

+259
-99
lines changed

env-model-generator/src/AnnotationConfig.ts renamed to env-model-generator/src/annotationConfig.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ export interface AnnotatedFields {
1616

1717
export interface AnnotatedField {
1818
type: string;
19-
}
19+
}

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import * as program from "commander";
66
import * as fs from "fs-extra";
77
import * as _ from "lodash";
88
import * as pathUtils from "path";
9-
import { BlankLine, Block, Class, FnCall, Method, Null, Return, Statement } from "./JavaCode";
9+
import { BlankLine, Block, Class, FnCall, Method, Null, Return, Statement } from "./javaCode";
1010
import * as model from "./model";
11-
import parseSpringXmlConfigFile from "./parse-spring-xml";
11+
import parseSpringConfigFiles from "./parserDriver";
1212
import { createPath } from "./utils";
1313

1414
function collectOption(value: string, collection?: string[]) {
@@ -24,7 +24,7 @@ program
2424
.option("-i --input-file <path>", "Additional DI configuration file to read", collectOption)
2525
.option("-o --output-path <output-path>", "Path to write output under")
2626
.description("Create code from DI configuration")
27-
.action(transformConfigFile);
27+
.action(transformConfigFiles);
2828
program.parse(process.argv);
2929

3030
// If program was called with no arguments then show help.
@@ -40,12 +40,12 @@ enum ReturnValue {
4040
OutputError,
4141
}
4242

43-
async function transformConfigFile(fileName: string, options: any) {
43+
async function transformConfigFiles(fileName: string, options: any) {
4444
let fileNames = [ fileName ];
4545
if (options.inputFile)
4646
fileNames = fileNames.concat(options.inputFile);
4747
try {
48-
await parseSpringXmlConfigFile(fileNames);
48+
await parseSpringConfigFiles(fileNames);
4949
} catch (err) {
5050
console.error(`Error parsing config file: ${err.message}`);
5151
process.exit(ReturnValue.ParseError);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export default class MapTrackingDuplicates<K, V>
2+
{
3+
private _map = new Map<K, V | null>();
4+
5+
public has(key: K): boolean {
6+
const value = this._map.get(key);
7+
return value !== undefined && value !== null;
8+
}
9+
10+
public hasDuplicates(key: K): boolean {
11+
return this._map.get(key) === null;
12+
}
13+
14+
public get(key: K): V | undefined {
15+
const value = this._map.get(key);
16+
return value === null ? undefined : value;
17+
}
18+
19+
public set(key: K, value: V): void {
20+
this._map.set(key, this._map.has(key) ? null : value);
21+
}
22+
}

env-model-generator/src/model.ts

+21-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as _ from "lodash";
2-
import { ConfigParseError } from "./ConfigParseError";
2+
import { ConfigParseError } from "./configParseError";
33
import {
44
Assignment,
55
BlankLine,
@@ -15,11 +15,12 @@ import {
1515
Return,
1616
StringConstant,
1717
Symbol,
18-
} from "./JavaCode";
19-
import NameMap from "./NameMap";
18+
} from "./javaCode";
19+
import MapTrackingDuplicates from "./mapTrackingDuplicates";
20+
import NameMap from "./nameMap";
2021
import { makeIdentifier, upperInitialChar } from "./utils";
2122

22-
interface BeanProperty {
23+
export interface BeanProperty {
2324
name: string;
2425
value: Value;
2526
}
@@ -214,7 +215,7 @@ export class Bean {
214215
Bean.aliases.set(alias, beanId);
215216
}
216217

217-
public static checkAliases(beans: Map<string, any>): void {
218+
public static checkAliases(beanWithIdExists: (beanId: string) => boolean): void {
218219
// If A -> B is added before B -> C then A -> B needs updating to A -> C now
219220
const aliases = new Map<string, string>();
220221
for (const [ alias, target ] of Bean.aliases) {
@@ -227,7 +228,7 @@ export class Bean {
227228
if (beanId === alias)
228229
throw new ConfigParseError(`Found circular alias from '${alias}'`);
229230
}
230-
if (beans.has(beanId) === undefined)
231+
if (!beanWithIdExists(beanId))
231232
throw new ConfigParseError(`Couldn't find bean '${beanId}' aliased from '${alias}'`);
232233
aliases.set(alias, beanId);
233234
}
@@ -237,6 +238,20 @@ export class Bean {
237238
public static getAliases(): Iterable<[ string, string ]> {
238239
return Bean.aliases;
239240
}
241+
242+
private static beanIdByClass = new MapTrackingDuplicates<string, string>();
243+
244+
public static registerIdForClass(qualifiedClassName: string, beanId: string) {
245+
Bean.beanIdByClass.set(qualifiedClassName, beanId);
246+
}
247+
248+
public static tryGetIdByClass(qualifiedClassName: string): string | undefined {
249+
return Bean.beanIdByClass.get(qualifiedClassName);
250+
}
251+
252+
public static hasMultipleBeansForClass(qualifiedClassName: string): boolean {
253+
return Bean.beanIdByClass.hasDuplicates(qualifiedClassName);
254+
}
240255
}
241256

242257
export abstract class Value {

env-model-generator/src/parseJson.ts

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import * as fs from "fs-extra";
2+
import * as jsonConfig from "./annotationConfig";
3+
import { ConfigParseError } from "./configParseError";
4+
import * as model from "./model";
5+
import { lowerInitialChar } from "./utils";
6+
7+
interface BeanWithName {
8+
qualifiedClassName: string;
9+
bean: jsonConfig.Bean;
10+
}
11+
12+
const beansById = new Map<string, BeanWithName>();
13+
14+
export async function collectBeansFromConfigFile(filePath: string): Promise<string[]> {
15+
let json: string;
16+
try {
17+
json = await fs.readFile(filePath, "utf8");
18+
} catch (err) {
19+
throw new ConfigParseError(`Can't open input file '${filePath}': ${err.message}`);
20+
}
21+
const annotationConfig: jsonConfig.AnnotationConfig = JSON.parse(json);
22+
for (const qualifiedClassName in annotationConfig.beans) {
23+
if (!annotationConfig.beans.hasOwnProperty(qualifiedClassName))
24+
continue;
25+
// Create bean ID
26+
// TODO: Handle name parameter to @Component constructor
27+
const id = lowerInitialChar(<string>qualifiedClassName.split(".").pop());
28+
// Check for duplicate ids from other beans
29+
if (beansById.has(id))
30+
throw new ConfigParseError(`Found multiple beans with id '${id}'`);
31+
const aliasedBeanId = model.Bean.getAlias(id);
32+
if (aliasedBeanId !== undefined)
33+
throw new ConfigParseError(`Found a bean with id same as an alias for '${aliasedBeanId}'`);
34+
// Create a bean model for the bean
35+
const bean = { qualifiedClassName: qualifiedClassName, bean: annotationConfig.beans[qualifiedClassName] };
36+
beansById.set(id, bean);
37+
model.Bean.registerIdForClass(bean.qualifiedClassName, id);
38+
}
39+
// TODO: Return paths to XML config files specified in code
40+
return [];
41+
}
42+
43+
export function beanWithIdExists(id: string): boolean {
44+
return beansById.has(id);
45+
}
46+
47+
export function parseAllBeans(): void {
48+
for (const [ beanId, bean ] of beansById) {
49+
if (model.Bean.tryGet(beanId) !== undefined)
50+
continue;
51+
// We don't store the results of this parsing, it is stored in internal data structures which do enough for us
52+
parseJsonBean(beanId, bean);
53+
}
54+
}
55+
56+
export function parseBean(id: string): model.Bean {
57+
const bean = beansById.get(id);
58+
if (bean === undefined)
59+
throw new Error("Called parseBean on a bean that did not exist");
60+
return parseJsonBean(id, bean);
61+
}
62+
63+
function parseJsonBean(id: string | undefined, namedBean: BeanWithName): model.Bean {
64+
const bean = namedBean.bean;
65+
const properties: model.BeanProperty[] = [];
66+
for (const fieldName in bean.fields) {
67+
if (!bean.fields.hasOwnProperty(fieldName))
68+
continue;
69+
const fieldClass = bean.fields[fieldName].type;
70+
71+
const fieldBeanId = model.Bean.tryGetIdByClass(fieldClass);
72+
if (fieldBeanId === undefined)
73+
throw new ConfigParseError(
74+
`Autowired field '${namedBean.qualifiedClassName}.${fieldName}' depends on a class '${fieldClass}' `
75+
+ `that has ${model.Bean.hasMultipleBeansForClass(fieldClass) ? "multiple" : "no"} implementations`);
76+
properties.push({ name: fieldName, value: new model.BeanRefValue(fieldBeanId) });
77+
}
78+
// TODO: Handle constructor arguments
79+
return new model.Bean(undefined, id, namedBean.qualifiedClassName, model.BeanCreationPolicy.Singleton, [], properties);
80+
}

0 commit comments

Comments
 (0)