Skip to content

Commit

Permalink
Add cycle detection in dependency graph
Browse files Browse the repository at this point in the history
  • Loading branch information
B4nan committed May 6, 2017
1 parent eca4f72 commit 477ca47
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 9 deletions.
81 changes: 81 additions & 0 deletions lib/CycleFinder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use strict';

class CycleFinder {

/**
* @param {Object} graph
*/
constructor(graph) {
this.graph = graph;
}

/**
* @returns {Boolean}
*/
hasCycle() {
return Object.keys(this.graph).some(service => {
const visited = {};
const path = [];

if (this._dfs(service, visited, path)) {
this.foundCycle = [service, path];
return true;
}

return false;
});
}

/**
* @param {String} [glue]
* @returns {String|String[]}
*/
getFoundCycle(glue = null) {
let node = this.foundCycle[0];
const path = this.foundCycle[1];
const ret = [node];
const visited = {};

do {
visited[node] = true;
node = path[node];
ret.push(node);
} while (!visited[node]);

if (glue) {
return ret.join(glue);
}

return ret;
}

/**
* @param {String} service
* @param {Object} visited
* @param {String[]} path
* @param {String} [prev]
* @return {Boolean}
* @private
*/
_dfs(service, visited, path, prev = null) {
path[prev] = service;

if (visited[service]) {
return true;
}

// skip unknown dependency (e.g. scalar type like {string})
if (typeof this.graph[service] === 'undefined') {
return false;
}

visited[service] = true;
const ret = this.graph[service].some(child => this._dfs(child, visited, path, service));
visited[service] = false;

return ret;
}

}

module.exports = CycleFinder;
30 changes: 21 additions & 9 deletions lib/MikroDI.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const fs = require('fs');
const path = require('path');
const _merge = require('lodash.merge');
const CycleFinder = require('./CycleFinder');

class MikroDI {

Expand Down Expand Up @@ -47,14 +48,19 @@ class MikroDI {
this.options.logger(`DI container building started`);
}

const graph = {}; // dependency graph for cycle search

let o = `'use strict';\n\n`;
o += `module.exports = {\n\n`;
o += ` _map: {},\n\n`;
this.options.serviceDirs.forEach(dir => {
o += this._discover(dir);
});
this.options.serviceDirs.forEach(dir => o += this._discover(dir, graph));
o += '};\n';

const cf = new CycleFinder(graph);
if (cf.hasCycle()) {
throw new Error(`MikroDI: Cyclic dependency found at '${cf.getFoundCycle()}'!`);
}

const file = this.options.contextDir + '/' + this.options.contextName;
fs.writeFileSync(file, o);

Expand All @@ -68,9 +74,10 @@ class MikroDI {

/**
* @param {String} path
* @param {Object} graph
* @private
*/
_discover(path) {
_discover(path, graph) {
const files = fs.readdirSync(this.options.baseDir + '/' + path);
let ret = '';

Expand All @@ -83,24 +90,28 @@ class MikroDI {
this.options.logger(`- processing service ${file}`);
}

ret += this._processService(`${path}/${file}`);
ret += this._processService(`${path}/${file}`, graph);
});

return ret;
}

/**
* @param {String} file
* @param {Object} graph
* @private
*/
_processService(file) {
_processService(file, graph) {
// load source
const source = fs.readFileSync(`${this.options.baseDir}/${file}`).toString();
const classSyntax = new RegExp(/class\s+\w+\s*(?:extends\s+\w+)?\s*{/).test(source);
const dependencies = this._findDependencies(source, classSyntax);
const className = file.substring(file.lastIndexOf('/') + 1, file.lastIndexOf('.js'));
const path = this._getRelativePath(file);

// save local dependency map to search for cycles
graph[className] = dependencies;

let ret = '';
ret += ` /**\n`;
ret += ` * @returns {${className}}\n`;
Expand All @@ -109,10 +120,11 @@ class MikroDI {
ret += ` if (!this._map.${className}) {\n`;
ret += ` const ${className} = require('${path}');\n`;

const deps = dependencies.map(dep => 'this.' + dep).join(', ');
if (classSyntax) {
ret += ` this._map.${className} = new ${className}(${dependencies.join(', ')});\n`;
ret += ` this._map.${className} = new ${className}(${deps});\n`;
} else {
ret += ` ${className}.constructor(${dependencies.join(', ')});\n`;
ret += ` ${className}.constructor(${deps});\n`;
ret += ` this._map.${className} = ${className};\n`;
}

Expand Down Expand Up @@ -150,7 +162,7 @@ class MikroDI {
}
}

return dependencies.map(dep => 'this.' + dep);
return dependencies;
}

/**
Expand Down

0 comments on commit 477ca47

Please sign in to comment.