Skip to content

Commit abe52a5

Browse files
authored
Merge pull request #11 from tmr232/cfgbuilder-refactor
This PR includes a significant refactor of all CFGBuilder classes. We moved from separate classes with code duplication, to a unified GenericCFGBuilder that take all the different syntax handlers as arguments. This reduces duplication, and creates a more consistent base to build on for future languages. It should also make new features, like navigation support, easier to implement.
2 parents 6f250d2 + 96b7299 commit abe52a5

18 files changed

+1708
-1551
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ dist
33
node_modules
44
.vscode-test/
55
*.vsix
6+
/.idea

CHANGELOG.md

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

77
## [Unreleased]
88

9+
### Fixed
10+
11+
- Rendering of `select` blocks in Go was broken.
12+
13+
### Changed
14+
15+
- Massive refactoring of `CFGBuilder` classes.
16+
New design now uses the same`GenericCGBuilder` class
17+
for all languages, and takes statement handlers as
18+
arguments.
19+
This reduces code duplication and makes it easier to add
20+
new languages in the future.
21+
922
## [0.0.5] - 2024-09-18
1023

1124
### Added

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export default tseslint.config(
3131
},
3232
],
3333
"@typescript-eslint/no-unnecessary-condition": "error",
34+
"@typescript-eslint/unbound-method": "error",
3435
},
3536
},
3637
);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,4 @@
104104
"engines": {
105105
"vscode": "^1.86.0"
106106
}
107-
}
107+
}

src/control-flow/block-matcher.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import Parser from "web-tree-sitter";
2+
import { type BasicBlock, BlockHandler } from "./cfg-defs.ts";
3+
4+
function matchQuery(
5+
syntax: Parser.SyntaxNode,
6+
queryString: string,
7+
): Parser.QueryMatch {
8+
const language = syntax.tree.getLanguage();
9+
const query = language.query(queryString);
10+
const matches = query.matches(syntax, { maxStartDepth: 0 });
11+
if (matches.length === 0) {
12+
throw new Error(`No match found for query.`);
13+
}
14+
return matches[0];
15+
}
16+
17+
export function matchExistsIn(
18+
syntax: Parser.SyntaxNode,
19+
mainName: string,
20+
queryString: string,
21+
): boolean {
22+
const language = syntax.tree.getLanguage();
23+
const query = language.query(queryString);
24+
const matches = query.matches(syntax);
25+
return matches.length > 0;
26+
}
27+
28+
function getSyntax(
29+
match: Parser.QueryMatch,
30+
name: string,
31+
): Parser.SyntaxNode | undefined {
32+
return getSyntaxMany(match, name)[0];
33+
}
34+
35+
function requireSyntax(
36+
match: Parser.QueryMatch,
37+
name: string,
38+
): Parser.SyntaxNode {
39+
const syntax = getSyntax(match, name);
40+
if (!syntax) {
41+
throw new Error(`Failed getting syntax for ${name}`);
42+
}
43+
return syntax;
44+
}
45+
46+
function getSyntaxMany(
47+
match: Parser.QueryMatch,
48+
name: string,
49+
): Parser.SyntaxNode[] {
50+
return match.captures
51+
.filter((capture) => capture.name === name)
52+
.map((capture) => capture.node);
53+
}
54+
55+
export class BlockMatcher {
56+
private blockHandler: BlockHandler = new BlockHandler();
57+
private processBlock: (syntax: Parser.SyntaxNode | null) => BasicBlock;
58+
public update = this.blockHandler.update.bind(this.blockHandler);
59+
60+
constructor(processBlock: BlockMatcher["processBlock"]) {
61+
this.processBlock = processBlock;
62+
}
63+
64+
public match(syntax: Parser.SyntaxNode, queryString: string): Match {
65+
const match = matchQuery(syntax, queryString);
66+
return new Match(match, this.blockHandler, this.processBlock);
67+
}
68+
69+
public tryMatch(
70+
syntax: Parser.SyntaxNode,
71+
queryString: string,
72+
): Match | null {
73+
try {
74+
return this.match(syntax, queryString);
75+
} catch {
76+
return null;
77+
}
78+
}
79+
80+
public get state() {
81+
return this.blockHandler;
82+
}
83+
}
84+
85+
export class Match {
86+
private match: Parser.QueryMatch;
87+
private blockHandler: BlockHandler;
88+
private processBlock: BlockMatcher["processBlock"];
89+
constructor(
90+
match: Parser.QueryMatch,
91+
blockHandler: BlockHandler,
92+
processBlock: BlockMatcher["processBlock"],
93+
) {
94+
this.match = match;
95+
this.blockHandler = blockHandler;
96+
this.processBlock = processBlock;
97+
}
98+
99+
public getSyntax(name: string): ReturnType<typeof getSyntax> {
100+
return getSyntax(this.match, name);
101+
}
102+
103+
public requireSyntax(name: string): ReturnType<typeof requireSyntax> {
104+
return requireSyntax(this.match, name);
105+
}
106+
107+
public getSyntaxMany(name: string): ReturnType<typeof getSyntaxMany> {
108+
return getSyntaxMany(this.match, name);
109+
}
110+
public getBlock(syntax: Parser.SyntaxNode | null | undefined) {
111+
return syntax ? this.blockHandler.update(this.processBlock(syntax)) : null;
112+
}
113+
}

src/control-flow/builder.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type {
2+
CFGGraph,
3+
Cluster,
4+
ClusterId,
5+
ClusterType,
6+
EdgeType,
7+
NodeType,
8+
} from "./cfg-defs.ts";
9+
import { MultiDirectedGraph } from "graphology";
10+
11+
export class Builder {
12+
private graph: CFGGraph = new MultiDirectedGraph();
13+
private nodeId: number = 0;
14+
private clusterId: ClusterId = 0;
15+
private activeClusters: Cluster[] = [];
16+
17+
private startCluster(type: ClusterType): Cluster {
18+
const parent =
19+
this.activeClusters.length === 0
20+
? undefined
21+
: this.activeClusters[this.activeClusters.length - 1];
22+
const cluster = {
23+
id: this.clusterId++,
24+
type,
25+
parent,
26+
depth: this.activeClusters.length + 1,
27+
};
28+
this.activeClusters.push(cluster);
29+
return cluster;
30+
}
31+
32+
private endCluster(_cluster: Cluster) {
33+
// We assume that all clusters form a stack.
34+
this.activeClusters.pop();
35+
}
36+
37+
public withCluster<T>(type: ClusterType, fn: (cluster: Cluster) => T): T {
38+
const cluster = this.startCluster(type);
39+
try {
40+
return fn(cluster);
41+
} finally {
42+
this.endCluster(cluster);
43+
}
44+
}
45+
46+
public addNode(type: NodeType, code: string, lines: number = 1): string {
47+
const id = `node${this.nodeId++}`;
48+
const cluster = this.activeClusters[this.activeClusters.length - 1];
49+
this.graph.addNode(id, {
50+
type,
51+
code,
52+
lines,
53+
markers: [],
54+
cluster,
55+
});
56+
return id;
57+
}
58+
59+
public cloneNode(node: string, overrides?: { cluster: Cluster }): string {
60+
const id = `node${this.nodeId++}`;
61+
const originalAttrs = this.graph.getNodeAttributes(node);
62+
const nodeAttrs = structuredClone(originalAttrs);
63+
nodeAttrs.cluster = originalAttrs.cluster;
64+
Object.assign(nodeAttrs, overrides);
65+
this.graph.addNode(id, nodeAttrs);
66+
return id;
67+
}
68+
69+
public addMarker(node: string, marker: string) {
70+
this.graph.getNodeAttributes(node).markers.push(marker);
71+
}
72+
73+
public addEdge(
74+
source: string,
75+
target: string,
76+
type: EdgeType = "regular",
77+
): void {
78+
if (!this.graph.hasEdge(source, target)) {
79+
this.graph.addEdge(source, target, { type });
80+
}
81+
}
82+
83+
public getGraph(): CFGGraph {
84+
return this.graph;
85+
}
86+
}

0 commit comments

Comments
 (0)