In case you are using jQAssistant with Gradle I described a while ago, you may encounter an exception like this after upgrading to jQA 1.8 and using the APOC plugin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java.lang.NoClassDefFoundError:
org/neo4j/driver/v1/Logging
        at java.base/java.lang.Class.getDeclaredMethods0(Native Method)
        at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3167)
        at java.base/java.lang.Class.getDeclaredMethods(Class.java:2310)
        at org.neo4j.kernel.impl.proc.ReflectiveProcedureCompiler.compileProcedure(ReflectiveProcedureCompiler.java:227)
        at org.neo4j.kernel.impl.proc.Procedures.registerProcedure(Procedures.java:168)
        at org.neo4j.kernel.impl.proc.Procedures.registerProcedure(Procedures.java:157)
        at org.neo4j.kernel.impl.proc.Procedures.registerProcedure(Procedures.java:147)
        at com.buschmais.jqassistant.neo4j.backend.neo4jv3.Neo4jV3CommunityNeoServer.initialize(Neo4jV3CommunityNeoServer.java:77)
        at com.buschmais.jqassistant.neo4j.backend.bootstrap.AbstractEmbeddedNeo4jServer.initialize(AbstractEmbeddedNeo4jServer.java:21)
        at com.buschmais.jqassistant.core.store.impl.EmbeddedGraphStore.initialize(EmbeddedGraphStore.java:71)
        at com.buschmais.jqassistant.core.store.impl.AbstractGraphStore.start(AbstractGraphStore.java:50)
        at com.buschmais.jqassistant.commandline.task.AbstractStoreTask.run(AbstractStoreTask.java:46)
        at com.buschmais.jqassistant.commandline.Main.executeTask(Main.java:254)
        at com.buschmais.jqassistant.commandline.Main.executeTasks(Main.java:203)
        at com.buschmais.jqassistant.commandline.Main.interpretCommandLine(Main.java:195)
        at com.buschmais.jqassistant.commandline.Main.run(Main.java:78)
        at com.buschmais.jqassistant.commandline.Main.main(Main.java:49)
Caused by: java.lang.ClassNotFoundException: org.neo4j.driver.v1.Logging
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:583)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
        ... 17 more

This is because:

  • jQA “store” depends on “xo.neo4j.remote” which depends on “org.neo4j.driver:neo4j-java-driver:4.0.0”
  • “jqassistant-apoc-plugin” depends on “org.neo4j.procedure:apoc” which depends on “org.neo4j.driver:neo4j-java-driver:1.7.3”

jQAssistant itself still runs on Neo4j 3.5, so downgrading the neo4j-java-driver seems to be an option. That can be done like this:

1
2
3
4
5
6
7
8
9
10
  jqa("com.buschmais.jqassistant.cli:jqassistant-commandline-neo4jv3:${project.jqaversion}") {
    exclude module: 'asm'
  }
  jqa("org.neo4j.driver:neo4j-java-driver:1.7.5") {
    // to resolve incompatible driver versions between CLI and APOC
    force = true
  }
  jqa("org.jqassistant.contrib.plugin:jqassistant-apoc-plugin:${project.jqaversion}")

  // all your other plugins go here

The blueprint for a Gradle project using jQAssistant was updated, so this from the previous blog post still works:

Add this line into your build.gradle:

1
apply from: 'https://raw.githubusercontent.com/kontext-e/jqa-gradle/master/jqa.gradle'

Now you can do

1
./gradlew clean check jqa 

in your project.

It was one small change from 1.3.x to 1.4.0, but unfortunately it may be a large change for you ()depending how many Cypher queries you already have): to harmonize relation names with jQAssistant main distribution.

In jQAssistant main distribution all relation names are in singular. The Kontext E plugins often used plural names. It felt natural for me because of the 1:N nature of the relations, and I didn’t notice until Dirk told me.

After some considerations I decided that a unique style of naming in the jQAssistant world weights more than the inconvenience of changing existing queries. So I changed the plural names to singular as described in the following sections.

:FindBugs:BugInstance

  • CLASS -> HAS_CLASS
  • METHODS -> HAS_METHOD
  • FIELDS -> HAS_FIELD

:FindBugs:SourceLineContainer

  • SOURCELINE -> HAS_SOURCELINE

:Asciidoc:List

  • HAS_ITEMS -> HAS_ITEM

:Asciidoc:Table

  • COLUMNS -> HAS_COLUMN
  • HEADER -> HAS_HEADER_ROW
  • BODY -> HAS_ROW
  • FOOTER -> HAS_FOOTER_ROW

:Asciidoc:Row

  • CONTAINS_CELLS -> CONTAINS_CELL

:Checkstyle:File

  • CHECKSTYLE_ERRORS -> CHECKSTYLE_ERROR

:Checkstyle:Report

  • CHECKSTYLE_FILES -> CHECKSTYLE_FILE

:Jacoco:Class

  • HAS_METHODS -> HAS_METHOD

:Jacoco:Method

  • HAS_COUNTERS -> HAS_COUNTER

:Jacoco:Package

  • HAS_CLASSES -> HAS_CLASS

:Jacoco:Report

  • HAS_PACKAGES -> HAS_PACKAGE

:Plaintext:Directory

  • HAS_FILES -> HAS_FILE
  • HAS_DIRECTORIES -> HAS_DIRECTORY

:Plaintext:File

  • HAS_LINES -> HAS_LINE

:PlantUml:Diagram

  • CONTAINS_GROUPS -> CONTAINS_GROUP

:PlantUml:File

  • CONTAINS_DIAGRAMS -> CONTAINS_DIAGRAM

:PlantUml:Group

  • HAS_CHILD_GROUPS -> HAS_CHILD_GROUP
  • HAS_LEAFS -> HAS_LEAF

:Pmd:File

  • HAS_VIOLATIONS -> HAS_VIOLATION

:Pmd:Report

  • HAS_FILES -> HAS_FILE

Ultrashort TL;DR:

Add this line into your build.gradle:

1
apply from: 'https://raw.githubusercontent.com/kontext-e/jqa-gradle/master/jqa.gradle'

Now you can do

1
./gradlew clean check jqa 

in your project.

The following longer version explains why and how it works.

jQAssistant comes with an excellent Maven support. But does it mean that you can’t use it with other build tools? Of course you can use it also with Gradle. Or Ant. Or whatever you are using. In this post I’ll show you how.

At the Get Started page you find only the options “Command Line” and “Maven”. Well, yes, we could use the “Run Process” capability of the build tool to start the jqassistant.cmd or jqassistant.sh. But there is also the artifact jqassistant-commandline available at Maven Central. Using that one I’ll show you the example of a Gradle integration.

Given you have already a build.gradle file, first you add some properties to make version handling easier:

1
2
3
project.ext["jqaversion"] = "1.3.0"
project.ext["jqapluginversion"] = "1.3"
project.ext["kejqapluginversion"] = "1.3.3"

The “jqaversion” is for the jQAssistant Core Framework. With a high probability you also want to use some plugins, e.g. for scanning Java class files. For the plugins is the “jqapluginversion”. And in this example let’s use also a plugin from an external contributor, so we need also the “kejqapluginversion”.

It is not really necessary, but for the reason of Separations of Concerns, I always like to define a separate “configuration” for jQAssistant dependencies:

1
2
3
configurations {
  jqa
}

Now let’s use this configuration to declare the dependencies:

1
2
3
4
5
6
7
8
9
10
11
12
13
dependencies {
  jqa("com.buschmais.jqassistant:jqassistant-commandline:${project.jqaversion}") {
    // because jQA 1.3 comes with Neo4j 2 and 3 support, there would be a classpath conflict
    exclude module: 'neo4j'
  }

  // list here all the plugins you want to use
  jqa("com.buschmais.jqassistant.plugin:java:${project.jqapluginversion}")
  jqa("com.buschmais.jqassistant.plugin:junit:${project.jqapluginversion}")

  // and plugins from other contributors
  jqa("de.kontext-e.jqassistant.plugin:jqassistant.plugin.git:${project.kejqapluginversion}")
}

And everything is set up to scan the project. To do so let’s define a new task:

1
2
3
4
5
6
7
8
9
task(jqascan, type: JavaExec) {
    main = 'com.buschmais.jqassistant.commandline.Main'
    classpath = configurations.jqa
    args 'scan'
    args '-f'

    args 'java:classpath::build/classes/main'
    args 'java:classpath::build/classes/test'
}

As you see in the task a Java program gets executed. We have to give the classpath because we defined a separate configuration, the class containing the main method and the parameters as described in the jQAssistant documentation

Finally it’s time to scan your project:

1
./gradlew clean check jqascan

The same way works defining a task for applying concepts and checking constraints:

1
2
3
4
5
task(jqaanalyze, type: JavaExec) {
  classpath = configurations.jqa
  main = 'com.buschmais.jqassistant.commandline.Main'
  args 'analyze'
}

Don’t forget to put some rules in your projects jqassistant/rules folder as described in the Get Started and jQAssistant documentation

and try

1
./gradlew jqaanalyze

Maybe you also want to start the server to explore your application. Let’s also define a task for this:

1
2
3
4
5
6
7
8
task(jqs, type: JavaExec) {
  classpath = configurations.jqa

  main = 'com.buschmais.jqassistant.commandline.Main'
  args 'server'

  standardInput = System.in
}

The jQA command line runs the server and prints “ServerTask - Press to finish." as the last line. To be able to Press we need to declare the

1
standardInput = System.in

as the last line in this task.

Basically that’s it. You may find it useful to define more tasks for the other command line commands like available-rules, available-rules, effective-rules and so on. Especially cleanup is useful. Because resetting the store may take a while, I found it useful to simply delete the store and report folders with this task:

1
2
3
4
task jqaclean(type: Delete) {
    delete 'jqassistant/report'
    delete 'jqassistant/store'
}

and execute this task also in the standard clean task:

1
clean.dependsOn jqaclean

But wait… what if I have a multi-module project? Do I have to list every of my subprojects here? And didn’t I also promise to use a third party plugin? At least the Git plugin is specified as dependency!

Ok, ok, here is a task definition for scan which is slightly longer:

1
2
3
4
5
6
7
8
9
10
11
12
13
task(jqascan, dependsOn: ['jqaclean'], type: JavaExec) {
  classpath = configurations.jqa
  main = 'com.buschmais.jqassistant.commandline.Main'
  args 'scan'
  args '-f'

  rootProject.subprojects {
    args 'java:classpath::'+it.name+'/build/classes/main'
    args 'java:classpath::'+it.name+'/build/classes/test'
  }

  args '.git'
}

This scans all subprojects and also the Git repository. If you may ask yourself here why you should scan the Git repo and what it has to do with a “Java Quality Assistant”, you find the answer in this excellent blog post

Here is the complete code with some additional convenciece tasks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
project.ext["jqaversion"] = "1.3.0"
project.ext["jqapluginversion"] = "1.3"
project.ext["kejqapluginversion"] = "1.3.3"

configurations {
    jqa
}

dependencies {
    jqa("com.buschmais.jqassistant:jqassistant-commandline:${project.jqaversion}") {
        // because jQA 1.3 comes with Neo4j 2 and 3 support, there would be a classpath conflict
        exclude module: 'neo4j'
    }

    // list here all the plugins you want to use
    jqa("com.buschmais.jqassistant.plugin:java:${project.jqapluginversion}")
    jqa("com.buschmais.jqassistant.plugin:junit:${project.jqapluginversion}")

    // and plugins from other contributors
    jqa("de.kontext-e.jqassistant.plugin:jqassistant.plugin.git:${project.kejqapluginversion}")
}

task jqaclean(type: Delete) {
    delete 'jqassistant/report'
    delete 'jqassistant/store'
}

task(jqascan, dependsOn: 'jqaclean', type: JavaExec) {
    main = 'com.buschmais.jqassistant.commandline.Main'
    classpath = configurations.jqa

    main = 'com.buschmais.jqassistant.commandline.Main'
    args 'scan'
    args '-f'

    args 'java:classpath::build/classes/main'
    args 'java:classpath::build/classes/test'

    /* in a multi subprojects project it would be:
    rootProject.subprojects {
        args 'java:classpath::'+it.name+'/build/classes/main'
        args 'java:classpath::'+it.name+'/build/classes/test'
    }
    */

    args '.git'
}

task(jqaanalyze, type: JavaExec) {
    classpath = configurations.jqa

    main = 'com.buschmais.jqassistant.commandline.Main'
    args 'analyze'
}

task(jqaavailablescopes, type: JavaExec) {
    classpath = configurations.jqa

    main = 'com.buschmais.jqassistant.commandline.Main'
    args 'available-scopes'
}

task(jqareset, type: JavaExec) {
    classpath = configurations.jqa

    main = 'com.buschmais.jqassistant.commandline.Main'
    args 'reset'
}

task(jqaeffectiverules, type: JavaExec) {
    classpath = configurations.jqa

    main = 'com.buschmais.jqassistant.commandline.Main'
    args 'effective-rules'
}

task(jqaavailablerules, type: JavaExec) {
    classpath = configurations.jqa

    main = 'com.buschmais.jqassistant.commandline.Main'
    args 'available-rules'
}

task(jqareport, type: JavaExec) {
    classpath = configurations.jqa

    main = 'com.buschmais.jqassistant.commandline.Main'
    args 'report'
}

task(jqa, dependsOn: ['jqascan','jqaanalyze']) {
    jqaanalyze.mustRunAfter jqascan
}

task(jqs, type: JavaExec) {
    classpath = configurations.jqa

    main = 'com.buschmais.jqassistant.commandline.Main'
    args 'server'

    standardInput = System.in
}

clean.dependsOn jqaclean

If you put it into a (reusable) jqa.gradle file, you only need one additional line in your build.gradle:

1
apply from: 'jqa.gradle'

You find a working Gradle Blueprint on GitHub

There is the common saying “Only the code tells the truth.” Yes - but the code tells not the whole truth. It is not only my personal opinion. I hear that often said by Grady Booch and Simon Brown. Therefore a little bit of architecture documentation pays off sooner or later. And if the information from architecture documentation is used to enrich the graph of scanned code, more and better constraints can be checked. So it is very likely that the ROI is sooner, not later.

What is the idea?

As my frequent blog readers know, I like the toolchain of jQAssistant, Neo4j, Asciidoc, PlantUML, and arc42. So when I already scanned the project into a Neo4j database and if there is more truth in the arc42 architecture documentation, it can’t be that hard to enrich the graph with information from the documentation, no?

The idea of “Building Higher-Level Abstractions of Source Code” or using Java Packages as layers/modules/components for analyzing dependencies between business subdomains is one key point of jQAssistant. My pain point with the “traditional” approach is, that the technical thing of using Cypher to manipulate the graph and the “business” thing (ok, here the business is architecture validation which is also quite technical, hence the quotation marks) are mixed. Let me quote Markus Harrer here to illustrate that. In his awesome post about “Building Higher-Level Abstractions of Source Code” he adds the concept of “Subdomains” to the graph using this Cypher statement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    UNWIND [
        { name: "Clinic" },
        { name: "Owner" },
        { name: "Person" }, 
        { name: "Pet" },
        { name: "Specialty" },
        { name: "Vet" }, 
        { name: "Visit" }
    ]
    AS properties
    CREATE (s:Subdomain) SET s = properties
    WITH s
        MATCH (t:Type)
            WHERE t.name CONTAINS s.name
        MERGE (t)-[:BELONGS_TO]->(s)
    RETURN s.name, t.name

Not very complicated and Cypher is nice to read, but I’m sure we can do better. To borrow some words from the Domain Specific Languages guys: We can stay in the language of describing architectures and make the language of Cypher and the Cypher query itself a reusable implementation detail.

Example: Mark Used Design Patterns via Regular Expressions

I’ve chosen an example that is very similar to Markus’, but a little bit more fine grained: let’s assume we are using a set of patterns in our design and some classes that implement a certain pattern can be found via regular expressions. In the simplest case a Command pattern has the suffix “Command”, a Factory pattern has the suffix “Factory” and so on.

Let’s also assume that the Pattern Language is defined and documented in Architecture Documentation this way:

Screenshot of Pattern Table

Now how can we set a Label of “Plugin” on all plugin classes, label command classes with “Command” and so on? As you will see, it needs only three easy steps.

First step: scan the documentation

As noted in the beginning, I like arc42 and Asciidoc, so the Plugin for reading Asciidoc documents. does the job here. Note that this plugin uses the same version of AsciidoctorJ as jQAssistant does to prevent runtime conflicts. I’m using here the current versions 1.3.0 of jQAssistant and 1.3.3 of the Asciidoc plugin.

To use the Asciidoc plugin, add it as jQAssistant dependency:

Maven

1
2
3
4
5
    <dependency>
        <groupId>de.kontext-e.jqassistant.plugin</groupId>
        <artifactId>jqassistant.plugin.asciidoc</artifactId>
        <version>1.3.3</version>
    </dependency>

Gradle

1
	jqaRt("de.kontext-e.jqassistant.plugin:jqassistant.plugin.asciidoc:1.3.3")

Once we brought the code and the documentation next to each other in the same database, we are on a good way and ready for the next step:

Second step: Finding the relevant information in the documentation

Since version 1.3.3 the Asciidoc plugin also imports attributes on tables, sections, and lists. We can mark our table shown in the picture in the documentation with an attribute label=”Pattern” Then the Asciidoc source looks like this:

    .Design Pattern of Classes Matched by Regular Expressions
    [options="header", label="Pattern"]
    |===
    | Regex                                 | Pattern
    | de.kontext_e.jqassistant.*Plugin      | Plugin
    | de.kontext_e.jqassistant.*Command     | Command
    | de.kontext_e.jqassistant.*Factory     | Factory
    |===

To make the live easier for later Cypher queries, let’s add some labels to the table cells we are interested in. The jQAssistant concept in Asciidoc form looks like this:

    [[structure:MarkAsciidocTypeRegex]]
    [source,cypher,role=concept]
    .Mark Asciidoc Table Cells that contain regular expressions for types that have to be marked with given labels.
    ----
        MATCH
            (a:Asciidoc:Table)-[:BODY]->(body),
            (a)-[:HAS_ATTRIBUTE]->(att:Asciidoc:Attribute)
        WHERE
            att.name='label' AND att.value='Pattern'
        WITH
            body
        MATCH
            (body)-[:CONTAINS_CELLS]->(regexCell:Asciidoc:Cell {colnumber: 0}),
            (body)-[:CONTAINS_CELLS]->(labelCell:Asciidoc:Cell {colnumber: 1})
        SET
            regexCell:RegularExpressionCell,
            labelCell:LabelCell
        CREATE UNIQUE
            (regexCell)-[:REGEX_FOR_LABEL]->(labelCell)
        RETURN
            regexCell, labelCell
    ----

Don’t get confused here. There are two Asciidoc directories in this made up example project:

  • one contains the arc42 architecture documentation of the project
  • the other contains the jQAssistant concepts and constraints which could also be written in XML

So the table is located in arc42, the concept is located in jQAssistant document. You see that now the technical thing and the “business” thing is not mixed anymore.

Ok, we are only one step away from our goal and need only to:

Third step: Set the Pattern Names from Table as Labels

Well, there is a little obstacle here: the names of the labels to be set are stored as properties in the nodes we labeled with “LabelCell” in the query above. And we can’t set labels with property values. But a little known feature of jQAssistant comes to the rescue: jQAssistant allows the use of scripting languages in concepts. Wow! Did you know that? Awesome, the graph cannot only manipulated with Cypher, also JavaScript, Ruby, Groovy, whatever scripting language is available in the classpath can be used. JavaScript works with every newer JDK out of the box, so let’s use this and craft a jQAssistant concept:

    [[structure:LabelTypesMatchedByRegex]]
    [source,js,role=concept,requiresConcepts="structure:MarkAsciidocTypeRegex"]
    .Mark types with labels matched by regex.
    ----
        var graphDatabaseService = store.getGraphDatabaseService();
        // Define the columns returned by the constraint
        var columnNames = java.util.Arrays.asList("Type");
        // Define the list of rows returned by the constraint
        var rows = new java.util.ArrayList();
    
        var result = graphDatabaseService.execute("    MATCH\n" +
                                                           "        (type:Type),\n" +
                                                           "        (regexCell:RegularExpressionCell)-[:REGEX_FOR_LABEL]->(labelCell:LabelCell)\n" +
                                                           "    WHERE\n" +
                                                           "        type.fqn =~ regexCell.text\n" +
                                                           "    RETURN\n" +
                                                           "        type, labelCell.text as label\n");
    
        while(result.hasNext()) {
            var next = result.next();
            var node = next.get("type");
            var label = next.get("label");
            node.addLabel(org.neo4j.graphdb.DynamicLabel.label(label));
            var resultRow = new java.util.HashMap();
            resultRow.put("Class", node);
            rows.add(resultRow);
        }
    
        // Return the result
        var status = com.buschmais.jqassistant.core.analysis.api.Result.Status.SUCCESS;
        new com.buschmais.jqassistant.core.analysis.api.Result(rule, status, severity, columnNames, rows);
    ----

It boils down to two steps:

  • find the Java Type nodes and Asciidoc table cells using a Cypher query
  • iterate over the result and use node.addLabel to set the label at the Java Type nodes

Note that again the jQAssistant stuff does not contain any project specific information. As long as some conventions are matched like marking the table with label=”Pattern” the jQAssistant magic can be reused in every project and all the relevant and project specific information is contained in the architecture documentation.

In the last post I described in detail how to use the same document to check the defined against the actual architecture. If you tried it out with the example project, you found some dependencies in the wrong direction. And perhaps also in your own projects. This time I’ll show you how to handle this Technical Debt in a way that both the CI build gets green again and it is also documented in the arc42 architecture documentation.

Documenting the unwanted dependencies

To make it visible and clear that there is Technical Debt regarding building block dependencies, it is a good idea to document the unwanted dependencies just below the description of the building blocks and the desired dependencies. I used for that an Asciidoc table like this:

.Unwantend Module Dependencies
[options="header"]
|===
| From                          | To                          | What should be done
| de.kontext_e.demo.business    | de.kontext_e.demo.core      | Because ...; Todo: ...
| de.kontext_e.demo.exporter    | de.kontext_e.demo.facade    | Because ...; Todo: ...
| de.kontext_e.demo.exporter    | de.kontext_e.demo.processor | Because ...; Todo: ...
| de.kontext_e.demo.importer    | de.kontext_e.demo.parser    | Because ...; Todo: ...
|===

and it gets rendered this way:

rendered Technical Debt table

In a real project I found it useful the have a third column to describe why the wrong dependency is currently there and to propose refactorings.

Add Asciidoc documents to the jQAssistant database

The freshly released version 1.2.0 of the Kontext E jQAssistant plugin suite contains also a Plugin for reading Asciidoc documents. Note that this plugin uses the same version of AsciidoctorJ as jQAssistant does to prevent runtime conflicts. So this Plugin works only with 1.2+ versions of jQAssistant because former versions use a too old AsciidoctorJ version.

To use the Asciidoc plugin, add it as jQAssistant dependency:

Maven

1
2
3
4
5
    <dependency>
        <groupId>de.kontext-e.jqassistant.plugin</groupId>
        <artifactId>jqassistant.plugin.asciidoc</artifactId>
        <version>${jqassistant.kePlugins.version}</version>
    </dependency>

Gradle

1
	jqaRt("de.kontext-e.jqassistant.plugin:jqassistant.plugin.asciidoc:1.2.0")

The documentation folder is already part of the pathes to be scanned. If it works, you should now have lots of :Asciidoc labeled nodes in your database.

Add a jQAssistant Concept for the Technical Debt

Next thing is to mark dependencies between Java packages as Technical Debt. So I first find our Asciidoc table which I titled “Unwantend Module Dependencies” and use the body:

1
2
3
4
5
6
MATCH 
    (a:Asciidoc:Table)-[:BODY]->(body)
WHERE 
    a.title='Unwantend Module Dependencies'
WITH 
    body

In the first two columns I put the source and target package names, so select them:

1
2
3
4
MATCH 
    (c1:Asciidoc:Cell {colnumber: 0})<-[:CONTAINS_CELLS]-(body)-[:CONTAINS_CELLS]->(c2:Asciidoc:Cell {colnumber: 1})
WITH 
    c1, c2

Now I find the matching Java packages:

1
2
3
4
5
6
MATCH
    (m1:Java:Package), (m2:Java:Package)
WHERE
    m1.fqn = c1.text
AND
    m2.fqn = c2.text

and create a relationship between them:

1
2
3
4
MERGE
    (m1)-[:TECHNICAL_DEBT]->(m2)
RETURN
    m1, m2;

The resulting Cypher query is this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
MATCH 
    (a:Asciidoc:Table)-[:BODY]->(body)
WHERE 
    a.title='Unwantend Module Dependencies'
WITH 
    body
MATCH 
    (c1:Asciidoc:Cell {colnumber: 0})<-[:CONTAINS_CELLS]-(body)-[:CONTAINS_CELLS]->(c2:Asciidoc:Cell {colnumber: 1})
WITH 
    c1, c2
MATCH
    (m1:Java:Package), (m2:Java:Package)
WHERE
    m1.fqn = c1.text
AND
    m2.fqn = c2.text
MERGE
    (m1)-[:TECHNICAL_DEBT]->(m2)
RETURN
    m1, m2;

Now I can put the query into the jQAssistant rules file. As usual I show examples for Asciidoc as well as for XML.

Asciidoc

[[documented:TechnicalDebt]]
.Creates a relationship between two Packages for Technical Debt.
[source,cypher,role=concept]
----
MATCH 
    (a:Asciidoc:Table)-[:BODY]->(body)
WHERE 
    a.title='Unwantend Module Dependencies'
WITH 
    body
MATCH 
    (c1:Asciidoc:Cell {colnumber: 0})<-[:CONTAINS_CELLS]-(body)-[:CONTAINS_CELLS]->(c2:Asciidoc:Cell {colnumber: 1})
WITH 
    c1, c2
MATCH
    (m1:Java:Package), (m2:Java:Package)
WHERE
    m1.fqn = c1.text
AND
    m2.fqn = c2.text
MERGE
    (m1)-[:TECHNICAL_DEBT]->(m2)
RETURN
    m1, m2;
----

XML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    <concept id="documented:TechnicalDebt">
        <description>
            Creates a relationship between two Packages for Technical Debt.
        </description>
        <cypher><![CDATA[
            MATCH 
                (a:Asciidoc:Table)-[:BODY]->(body)
            WHERE 
                a.title='Unwantend Module Dependencies'
            WITH 
                body
            MATCH 
                (c1:Asciidoc:Cell {colnumber: 0})
                <-[:CONTAINS_CELLS]-(body)-[:CONTAINS_CELLS]->
                (c2:Asciidoc:Cell {colnumber: 1})
            WITH 
                c1, c2
            MATCH
                (m1:Java:Package), (m2:Java:Package)
            WHERE
                m1.fqn = c1.text
            AND
                m2.fqn = c2.text
            MERGE
                (m1)-[:TECHNICAL_DEBT]->(m2)
            RETURN
                m1, m2;
        ]]></cypher>
    </concept>

Modify the jQAssistant Constraint WrongDirection

I modified the Constraint WrongDirection in a way that it also requires “documented:TechnicalDebt” and that the Cypher query excludes documented Technical Debt dependencies:

Asciidoc

[[dependency:WrongDirection]]
.Finds package dependencies which are in the wrong direction according to the documentation.
[source,cypher,role=constraint,requiresConcepts="dependency:TransitivePackageDependencies, documented:TechnicalDebt",severity=critical]
----
MATCH
    (p1:PlantUml:Package)-[:MAY_DEPEND_ON]->(p2:PlantUml:Package),
    (p3:Java:Package)-[:DEPENDS_ON]->(p4:Java:Package)
WHERE
    p1.fqn = p4.fqn
    AND p2.fqn = p3.fqn
    AND NOT (p3)-[:TECHNICAL_DEBT]->(p4)
RETURN
    p3.fqn + "-->" + p4.fqn AS WrongDirection;
----

XML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    <constraint id="dependency:WrongDirection" severity="critical">
        <requiresConcept refId="dependency:Package"/>
        <requiresConcept refId="dependency:TransitivePackageDependencies"/>
        <requiresConcept refId="dependency:WrongDirection"/>
        <description>
            Finds package dependencies which are in the wrong direction 
            according to the documentation.
        </description>
        <cypher><![CDATA[
            MATCH
                (p1:PlantUml:Package)-[:MAY_DEPEND_ON]->(p2:PlantUml:Package),
                (p3:Java:Package)-[:DEPENDS_ON]->(p4:Java:Package)
            WHERE
                p1.fqn = p4.fqn
                AND p2.fqn = p3.fqn
                AND NOT (p3)-[:TECHNICAL_DEBT]->(p4)
            RETURN
                p3.fqn + "-->" + p4.fqn AS WrongDirection;
        ]]></cypher>
    </constraint>

Running the checks again

If you run the checks in the example project again, the four constraint violations should be gone.

Some closing words

If dependencies in a project got never checked, there are most like unwanted dependencies. Not surprising and it also hit me with the C++ project I wrote about Breaking the build with any wrong dependency makes the build red for a long time. Resolving those dependencies is not a five minute task. But documenting them and defining exceptions from the dependency architecture rules with exactly the same table gives us a good chance to improve the architecture step by step over several iterations.

As you may have guessed from the title, more posts about handling Technical Debt are in the pipeline. But before that, I have to introduce you to some other things first.