0

I found the output of the dependency:packageCycles constraint shipped with jQAssistant hard to interpret. Specifically I'm keen on finding an example instance of classes that make up the cyclic dependency.

Given I found a cyclce of packages, for each pair of adjunct packages I would need to find two classes that connect them.

This is my first attempt at the Cypher query, but there are still some relevant parts missing:

MATCH nodes = (p1:Package)-[:DEPENDS_ON]->(p2: Package)-[:DEPENDS_ON*]->(p1)
WHERE p1 <> p2
WITH extract(x IN relationships(nodes) |
  (:Type)<--(:Package)-[x]->(:Package)-->(:Type)) AS cs
RETURN cs

Specifically, in order to really connect the two packages, the two types should be related to each other with DEPENDS_ON as shown below:

(:Type)<--(:Package)-[x]->(:Package)-->(:Type)
   |                                      ^
   |             DEPENDS_ON               |
   +--------------------------------------+

For above pattern I would have to return the two types (and not the packages, for instance). Preferably the output for a single cyclic dependency consists of a list of qualified class names (otherwise multiple one cannot possibly distinguish the class chains of more than one cyclic dependency).

For this specific purpose I find Cypher to be very limited, support for identifying and collecting new graph patterns during path traversal does not seem to be the easiest thing to do. Also the attempt to give names to the (:Type) nodes resulted in syntax errors.

Also I messed a lot around with UNWIND, but to no avail. It lets you introduce new MATCH clauses on per-element basis (say, the elements of relationships(nodes)), but I do not know of another method to undo the damaging effects of unwind: the surrounding list structure is removed, such that the traces of multiple cyclic dependencies merge into each other. Additionally the results appear permuted to me. That being said below query is conceptually also very close on what I am trying to achieve but does not work:

MATCH nodes = (p1:Package)-[:DEPENDS_ON]->(p2: Package)-[:DEPENDS_ON*]->(p1)
WHERE p1 <> p2
WITH relationships(nodes) as rel
UNWIND rel AS x
  MATCH (t0:Type)<-[:CONTAINS]-(:Package)-[x]->(:Package)-[:CONTAINS]->(t1:Type),
  (t0)-[:DEPENDS_ON]->(t1)
RETURN t0.fqn, t1.fqn

I do appreciate that there seems to be some scripting support within jQAssistant. However, this would really be my last resort, since it is surely more difficult to maintain than a Cypher query.

To rephrase it: given a path, I'm looking for a method to identify a sub-pattern for each element, project a node out of that match, and to collect the result. Do you have any ideas on how could one accomplish that with Cypher?


Edit #1: Within one package, one has also to consider that the class that is target to the inbound edge of type DEPENDS_ON may not be the same class that is source to the outgoing edge. In other words, as a result

  • two classes of the same package may be part of the trace
  • if one wanted to express the cyclic dependency trace as a path, one must take into account detours that navigate to classes in the same package. For instance (edges in bold mark package entry / exit; an edge of type DEPENDS_ON is absent between the two types):

-[:DEPENDS_ON]->(:Type)<-[:CONTAINS]-(:Package)-[:CONTAINS]->(:Type)-[DEPENDS_ON]->

Maybe it gets a little clearer using the following picture:

Example Package Cycle

Clearly "a, b, c" is a package cycle and "TestA, TestB1, TestB2, TestC" is a type-level trace for justifying the package-level dependency.

f4lco
  • 3,728
  • 5
  • 28
  • 53
  • Quoting your question "Finally I would have to return the two types and only these" and applying that to the example you gave, you're looking for TestC depends directly on TestA? With cycles it's hard to determine the starting point. – Kriegel May 06 '18 at 15:57
  • @Kriegel Two parts: (1) "return only these two types" referred to the fact that I did not find a way to achieve projection as part of the `extract` clause (the packages are also part of the result of the first query). I rephrased that sentence. (2) Yes, the notion of "starting point" is tricky. Current assumption (also of the currently built-in constraint) is that any node may serve as starting point. In case _n_ nodes participate in the cycle, the result consists of _n_ entries together with a "shifted" version of the path. – f4lco May 06 '18 at 18:15

1 Answers1

0

The following query extends the package cycle constraint by drilling down on type level:

MATCH
  (p1:Package)-[:DEPENDS_ON]->(p2:Package),
  path=shortestPath((p2)-[:DEPENDS_ON*]->(p1))
WHERE
  p1 <> p2
WITH
  p1, p2
MATCH
  (p1)-[:CONTAINS]->(t1:Type),
  (p2)-[:CONTAINS]->(t2:Type),
  (p1)-[:CONTAINS]->(t3:Type),
  (t1)-[:DEPENDS_ON]->(t2),
  path=shortestPath((t2)-[:DEPENDS_ON*]->(t3))
RETURN
  p1.fqn, t1.fqn, EXTRACT(t IN nodes(path) | t.fqn) AS Cycle

I'm not sure how well the query will work on large projects, we'll need to give it a try.

Edit 1: Updated the query to match on any type t3 which is located in the same package as t1.

Edit 2: The answer is not correct, see discussion below.

Dirk Mahler
  • 1,186
  • 1
  • 6
  • 7
  • Thanks. Concerning `path=shortestPath((t2)-[:DEPENDS_ON*]->(t1))`: one cannot safely conclude that all of the types are connected with a `DEPENDS_ON` relationship. Above query wil only catch the cases where a single class of the package is responsible for both incoming and outgoing dependency relationships. Please see the question edit. – f4lco May 06 '18 at 15:39
  • thank you so much for your time & effort. However, the fact that there is a "disconnect" in terms of the `DEPENDS_ON` relationship on the type-level cyclic dependency can be true for any of the packages involved, not only a single one. Right now the query matches the blue arrow of the example exactly. It might not generalize well since the pattern exhibited by package 'b' may repeat multiple times. See Edit 1, 2nd bullet point. Perhaps this one is really geared towards a programmatic solution. – f4lco May 07 '18 at 06:37
  • Damn' again... Will think about it and come back. – Dirk Mahler May 07 '18 at 10:41