11import type Parser from "web-tree-sitter" ;
2- import type { Case , EdgeType } from "./cfg-defs" ;
2+ import type { EdgeType } from "./cfg-defs" ;
33import type { Context } from "./generic-cfg-builder" ;
44import { pairwise } from "./itertools.ts" ;
5+
56export interface SwitchOptions {
67 /// A Go `select` blocks until one of the branches matches.
78 /// This means that we never add an alternative edge from the
89 /// head to the merge node. There is no implicit-default.
910 noImplicitDefault ?: boolean ;
1011}
1112
13+ /**
14+ * Represents a single case inside a switch statement.
15+ *
16+ * Cases are build of a condition and a consequence,
17+ * where the condition _may_ lead to the consequence.
18+ */
19+ export interface Case {
20+ /**
21+ * The condition entry node
22+ */
23+ conditionEntry : string ;
24+ /**
25+ * The condition exit node
26+ */
27+ conditionExit : string ;
28+ consequenceEntry : string ;
29+ consequenceExit : string | null ;
30+ alternativeExit : string ;
31+ /**
32+ * Does this case fall-through to the next one?
33+ */
34+ hasFallthrough : boolean ;
35+ /**
36+ * Is this the default case?
37+ */
38+ isDefault : boolean ;
39+ /**
40+ * A case may be entirely empty
41+ */
42+ isEmpty : boolean ;
43+ }
44+
1245export function buildSwitch (
1346 cases : Case [ ] ,
1447 mergeNode : string ,
1548 switchHeadNode : string ,
1649 options : SwitchOptions ,
1750 ctx : Context ,
1851) {
52+ // Fallthrough from the previous case
1953 let fallthrough : string | null = null ;
54+ let hasDefaultCase = false ;
2055 let previous : string | null = switchHeadNode ;
2156 for ( const thisCase of cases ) {
2257 if ( ctx . options . flatSwitch ) {
2358 ctx . builder . addEdge ( switchHeadNode , thisCase . conditionEntry ) ;
24- ctx . builder . addEdge ( thisCase . conditionExit , thisCase . consequenceEntry ) ;
25- if ( fallthrough ) {
26- ctx . builder . addEdge ( fallthrough , thisCase . consequenceEntry ) ;
27- }
28- if ( thisCase . isDefault ) {
29- // If we have any default node - then we don't connect the head to the merge node.
30- previous = null ;
59+ if ( thisCase . isEmpty && thisCase . hasFallthrough ) {
60+ // When we have an empty fallthrough case, we ignore its consequence node.
61+ // Instead, we link it directly to the condition node of the next case.
62+ // This allows for nice chaining while avoiding the tree-like artifacts
63+ // found in https://github.com/tmr232/function-graph-overview/issues/77
64+ if ( fallthrough ) {
65+ ctx . builder . addEdge ( fallthrough , thisCase . conditionEntry ) ;
66+ }
67+ fallthrough = thisCase . conditionExit ;
68+ } else {
69+ ctx . builder . addEdge ( thisCase . conditionExit , thisCase . consequenceEntry ) ;
70+
71+ if ( fallthrough ) {
72+ ctx . builder . addEdge ( fallthrough , thisCase . consequenceEntry ) ;
73+ }
74+
75+ if ( ! thisCase . hasFallthrough && thisCase . consequenceExit ) {
76+ ctx . builder . addEdge ( thisCase . consequenceExit , mergeNode , "regular" ) ;
77+ }
78+ // Update for next case
79+ fallthrough = thisCase . hasFallthrough ? thisCase . consequenceExit : null ;
3180 }
3281 } else {
82+ /* Model the switch as an if-elif-else chain */
3383 if ( fallthrough ) {
3484 ctx . builder . addEdge ( fallthrough , thisCase . consequenceEntry ) ;
3585 }
86+
3687 if ( previous && thisCase . conditionEntry ) {
3788 ctx . builder . addEdge (
3889 previous ,
@@ -50,18 +101,19 @@ export function buildSwitch(
50101
51102 // Update for next case
52103 previous = thisCase . isDefault ? null : thisCase . alternativeExit ;
53- }
54104
55- // Fallthrough is the same for both flat and non-flat layouts.
56- if ( ! thisCase . hasFallthrough && thisCase . consequenceExit ) {
57- ctx . builder . addEdge ( thisCase . consequenceExit , mergeNode , "regular" ) ;
105+ if ( ! thisCase . hasFallthrough && thisCase . consequenceExit ) {
106+ ctx . builder . addEdge ( thisCase . consequenceExit , mergeNode , "regular" ) ;
107+ }
108+ // Update for next case
109+ fallthrough = thisCase . hasFallthrough ? thisCase . consequenceExit : null ;
58110 }
59- // Update for next case
60- fallthrough = thisCase . hasFallthrough ? thisCase . consequenceExit : null ;
111+
112+ hasDefaultCase || = thisCase . isDefault ;
61113 }
62114 // Connect the last node to the merge node.
63115 // No need to handle `fallthrough` here as it is not allowed for the last case.
64- if ( previous && ! options . noImplicitDefault ) {
116+ if ( previous && ! hasDefaultCase && ! options . noImplicitDefault ) {
65117 ctx . builder . addEdge ( previous , mergeNode , "alternative" ) ;
66118 }
67119
@@ -113,6 +165,15 @@ export function collectCases(
113165 ) ;
114166 }
115167
168+ // We want to mark empty nodes, so that we can avoid linking their
169+ // consequence nodes.
170+ // It is true that Go's cases are "empty" even if they have a `fallthrough`
171+ // keyword as their only statement, but we can safely ignore those.
172+ // That is because a Go switch allows multiple conditions, making
173+ // the common case of a huge switch with many cases less common.
174+ // If it comes up in practice - we'll address it.
175+ const isEmpty = consequence . length === 0 ;
176+
116177 cases . push ( {
117178 conditionEntry : conditionNode ,
118179 conditionExit : conditionNode ,
@@ -121,6 +182,7 @@ export function collectCases(
121182 alternativeExit : conditionNode ,
122183 hasFallthrough,
123184 isDefault,
185+ isEmpty,
124186 } ) ;
125187 }
126188
0 commit comments