I have written some simple code to detect cyclic dependencies for a module loading system and I'd like some feedback on how I can go about improving it. The code itself finds the first cycle in the dependency graph that starts at the provided node and returns a list of all of the nodes traversed along the way.
Here's an example:
The white node is the one we are starting the traversal at. The JS representation of this graph would look like this:
{
'A': ['B', 'C'],
'B': [],
'C': ['D'],
'D': ['A']
}
The output from my function is ['A', 'C', 'D', 'A']
, which is a list of the nodes traversed before finding a cycle.
Here is the code I have written to generate the array:
function findCyclicDependencies(definitions, identifier) {
var stack = [];
// Internal search function.
var internalSearch = function(currentIdentifier) {
// If we have visited this node, return whether or not it is the one we
// are looking for.
if (stack.indexOf(currentIdentifier) !== -1) {
return currentIdentifier === identifier;
}
stack.push(currentIdentifier);
// Check all of the child nodes to see if they contain the node we are
// looking for.
var found = definitions[currentIdentifier].some(internalSearch);
// Remove the current node from the stack if it's children do not
// contain the node we are looking for.
if (!found) {
stack.splice(stack.indexOf(currentIdentifier), 1);
}
return found;
};
// If there isn't a cyclic dependency then we return an empty array,
// otherwise we return the stack.
return internalSearch(identifier) ? stack.concat(identifier) : [];
}
And some not-so great code for doing the tests:
[
{
name: 'Non-cyclic',
definitions: {
'A': ['B', 'C'],
'B': [],
'C': ['D'],
'D': []
},
identifier: 'A',
expected: []
},
{
name: 'Cyclic',
definitions: {
'A': ['B', 'C'],
'B': [],
'C': ['D'],
'D': ['A']
},
identifier: 'A',
expected: ['A', 'C', 'D', 'A']
}
].forEach(function(test) {
var result = findCyclicDependencies(test.definitions, test.identifier),
passed = true;
if (test.expected.length === result.length) {
for (var i = 0; i < test.expected.length; i++) {
if (test.expected[i] !== result[i]) {
passed = false;
break;
}
}
} else {
passed = false;
}
if (passed) {
console.log(test.name + ': pass');
} else {
var message = test.name + ': fail\n' +
' Got: ' + JSON.stringify(result) + '\n' +
' Expected: ' + JSON.stringify(test.expected);
console.warn(message);
}
});
I'm not all that bothered about the quality of the test code as it's mainly for demonstration purposes to show my function works.
One thing I'm not sure about with my code (that I'm not even sure is in the scope of Code Review):
Is there any case where found
is false
but the stack doesn't contain currentIdentifier
(i.e. stack.indexOf(currentIdentifier) === -1
? I'm wondering if I need to check whether or not currentIdentifier
is in the stack before trying to remove it, as trying to remove something not present ends up with the last element being removed erroneously (indexOf
returns -1
and splice(-1, 1)
removes the last element from the array).