Skip to content

Commit 04bf3ad

Browse files
authored
Merge branch 'main' into graph-script-diagnostics
2 parents e950ece + f32ef41 commit 04bf3ad

File tree

12 files changed

+507
-166
lines changed

12 files changed

+507
-166
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
66

77
## [Unreleased]
88

9+
10+
### Fixed
11+
12+
- Comment-tests now fail if an unsupported test type is present instead of passing silently.
13+
14+
### Changed
15+
16+
- Empty switch-cases with fallthrough now chain in a cleaner way in flat-switches.
17+
918
## [0.0.14] - 2025-02-17
1019

1120
### Added

src/components/utils.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,7 @@ export function runTest(record: TestFuncRecord): TestResults[] {
6969
};
7070
const testResults = [];
7171
for (const [key, value] of Object.entries(testFunc.reqs)) {
72-
const reqHandler = requirementTests[key];
73-
if (!reqHandler) {
74-
continue;
75-
}
72+
const reqHandler = requirementTests[key as keyof typeof testFunc.reqs];
7673
testResults.push({
7774
reqName: key,
7875
reqValue: value,

src/control-flow/cfg-defs.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -303,15 +303,6 @@ export function mergeNodeAttrs(
303303
startOffset: startOffset,
304304
};
305305
}
306-
export interface Case {
307-
conditionEntry: string;
308-
conditionExit: string;
309-
consequenceEntry: string;
310-
consequenceExit: string | null;
311-
alternativeExit: string;
312-
hasFallthrough: boolean;
313-
isDefault: boolean;
314-
}
315306

316307
export interface BuilderOptions {
317308
/**

src/control-flow/switch-utils.ts

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,89 @@
11
import type Parser from "web-tree-sitter";
2-
import type { Case, EdgeType } from "./cfg-defs";
2+
import type { EdgeType } from "./cfg-defs";
33
import type { Context } from "./generic-cfg-builder";
44
import { pairwise } from "./itertools.ts";
5+
56
export 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+
1245
export 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

Comments
 (0)