Skip to content

Commit 970c668

Browse files
Clarify sealed hierarchy depth semantics for cycle members and test paths to sealed roots
1 parent e0f3354 commit 970c668

3 files changed

Lines changed: 35 additions & 14 deletions

File tree

‎codebase-graph-builder/src/main/java/org/hjug/graphbuilder/metrics/ClassMetrics.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,9 @@ void freeze() {
152152
*
153153
* <p>Cyclic-depth semantics: the depth is the longest acyclic
154154
* ancestor path terminating at a sealed root within the analyzed
155-
* batch; a class with no such path (cycle member, or a chain
156-
* dead-ending at a non-sealed in-batch class) has depth 0. Ancestors
155+
* batch; a class with no such path (including a cycle member whose
156+
* ancestor paths do not reach a sealed root, or a chain dead-ending at a
157+
* non-sealed in-batch class) has depth 0. Ancestors
157158
* absent from the batch preserve a minimum depth of 2 as a
158159
* relationship signal. (Cycle safety: issue #215.)
159160
*/

‎codebase-graph-builder/src/main/java/org/hjug/graphbuilder/metrics/GraphMetricsCollector.java‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,9 @@ public void finalizeMetrics() {
262262
* <li>{@link ClassMetrics#setSealedHierarchyDepth(int)} — root sealed
263263
* class has depth 1; each direct permittee inherits depth 2, and
264264
* so on. The depth is the longest acyclic ancestor path that
265-
* terminates at a sealed root; classes with no such path (cycle
266-
* members, non-sealed dead-ends) get depth 0 and are not marked.
265+
* terminates at a sealed root; classes with no such path (including
266+
* cycle members without a path to a sealed root and non-sealed
267+
* dead-ends) get depth 0 and are not marked.
267268
* Third-party ancestors absent from the batch preserve a minimum
268269
* depth of 2 as a relationship signal. (Cycle safety: issue #215.)</li>
269270
* <li>{@link ClassMetrics#setHasExplicitLogic(boolean)} — true when a
@@ -311,10 +312,11 @@ private int computeSealedDepth(ClassMetrics metrics) {
311312
* <p><b>Cyclic-depth semantics:</b> a class's depth is the length of the
312313
* longest acyclic ancestor path that terminates at a sealed root
313314
* ({@code isSealed() == true} with no ancestors, depth 1) within the
314-
* analyzed batch. A class with no such path — a cycle member, a class
315-
* whose only paths enter a cycle, or a class whose chain dead-ends at a
316-
* non-sealed class in the batch — has depth 0: it is not an observable
317-
* member of any sealed hierarchy. Ancestors absent from the batch
315+
* analyzed batch. A class with no such path — including a cycle member
316+
* whose ancestor paths do not reach a sealed root, a class whose only
317+
* paths enter a cycle, or a class whose chain dead-ends at a non-sealed
318+
* class in the batch — has depth 0: it is not an observable member of any
319+
* sealed hierarchy. Ancestors absent from the batch
318320
* (third-party) preserve the minimum depth of 2 as a relationship signal.
319321
*
320322
* @param visiting the class FQNs on the current ancestor-traversal path

‎codebase-graph-builder/src/test/java/org/hjug/graphbuilder/metrics/GraphMetricsCollectorSealedDepthTest.java‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@
2121
* <p>A class's depth is the length of the longest acyclic ancestor path that
2222
* terminates at a sealed root ({@code isSealed() == true} with no ancestors,
2323
* depth 1) <em>within the analyzed batch</em>. A class with no such path —
24-
* cycle members, classes whose only ancestor paths enter a cycle, or classes
25-
* whose chain dead-ends at a non-sealed class in the batch — has depth 0: it
26-
* is not a member of any observable sealed hierarchy. External ancestors (not
27-
* present in the batch) preserve the pre-existing minimum depth of 2 as a
28-
* relationship signal. When both cyclic and valid paths exist, the valid
29-
* path's depth wins.
24+
* including a cycle member whose ancestor paths do not reach a sealed root,
25+
* a class whose only ancestor paths enter a cycle, or a class whose chain
26+
* dead-ends at a non-sealed class in the batch — has depth 0: it is not a
27+
* member of any observable sealed hierarchy. External ancestors (not present
28+
* in the batch) preserve the pre-existing minimum depth of 2 as a relationship
29+
* signal. When both cyclic and valid paths exist, the valid path's depth wins.
3030
*/
3131
class GraphMetricsCollectorSealedDepthTest {
3232

@@ -145,6 +145,24 @@ void mixedCyclicAndValidPaths_validPathWins() {
145145
assertEquals(1, root.getSealedHierarchyDepth());
146146
}
147147

148+
@DisplayName("a cycle member with a sealed-root path receives its valid depth")
149+
@Test
150+
void cycleMemberWithSealedRootPath_hasValidDepth() {
151+
GraphMetricsCollector collector = newCollector();
152+
ClassMetrics root = collector.getOrCreateClassMetrics("com.example.Shape");
153+
root.setSealed(true);
154+
ClassMetrics a = collector.getOrCreateClassMetrics("com.example.A");
155+
ClassMetrics b = collector.getOrCreateClassMetrics("com.example.B");
156+
a.addSealedHierarchyAncestor("com.example.B");
157+
b.addSealedHierarchyAncestor("com.example.A");
158+
b.addSealedHierarchyAncestor("com.example.Shape");
159+
160+
collector.finalizeMetrics();
161+
162+
assertEquals(3, a.getSealedHierarchyDepth(), "A reaches the Shape root through B");
163+
assertEquals(2, b.getSealedHierarchyDepth(), "B reaches the Shape root directly");
164+
}
165+
148166
@DisplayName("a chain ending at a non-sealed class in the batch has depth 0")
149167
@Test
150168
void chainEndingAtNonSealedClass_hasDepthZero() {

0 commit comments

Comments
 (0)