Skip to content

Commit 221a791

Browse files
committed
feat: extend dataflow analysis to all supported languages
Dataflow extraction was limited to JS/TS/TSX. This adds rules-based support for Python, Go, Rust, Java, C#, PHP, and Ruby following the established CFG/Complexity pattern. - DATAFLOW_DEFAULTS + makeDataflowRules() validation factory - Per-language rule objects mapping AST node types for functions, calls, returns, parameters, member access, and mutations - DATAFLOW_RULES Map + DATAFLOW_EXTENSIONS Set from LANGUAGE_REGISTRY - extractDataflow() accepts langId, all helpers use rules - buildDataflowEdges() uses DATAFLOW_EXTENSIONS instead of hardcoded extension checks, resolves langId from symbols._langId or extToLang - Language-specific handling: Go expression_list unwrapping, C# direct child initializer scanning, Java combined call+member mutations, PHP extra identifier types and argument wrappers - 7 new per-language test files (57 total tests across 8 languages) Impact: 25 functions changed, 14 affected
1 parent 6a1acf6 commit 221a791

File tree

9 files changed

+1422
-269
lines changed

9 files changed

+1422
-269
lines changed

src/dataflow.js

Lines changed: 753 additions & 268 deletions
Large diffs are not rendered by default.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Unit tests for extractDataflow() against parsed C# ASTs.
3+
*/
4+
import { beforeAll, describe, expect, it } from 'vitest';
5+
import { extractDataflow } from '../../src/dataflow.js';
6+
import { createParsers } from '../../src/parser.js';
7+
8+
describe('extractDataflow — C#', () => {
9+
let parsers;
10+
11+
beforeAll(async () => {
12+
parsers = await createParsers();
13+
});
14+
15+
function parseAndExtract(code) {
16+
const parser = parsers.get('csharp');
17+
if (!parser) return null;
18+
const tree = parser.parse(code);
19+
return extractDataflow(tree, 'Test.cs', [], 'csharp');
20+
}
21+
22+
describe('parameters', () => {
23+
it('extracts simple parameters', () => {
24+
const data = parseAndExtract(
25+
'class Test {\n int Add(int a, int b) {\n return a + b;\n }\n}\n',
26+
);
27+
expect(data.parameters).toEqual(
28+
expect.arrayContaining([
29+
expect.objectContaining({ funcName: 'Add', paramName: 'a', paramIndex: 0 }),
30+
expect.objectContaining({ funcName: 'Add', paramName: 'b', paramIndex: 1 }),
31+
]),
32+
);
33+
});
34+
});
35+
36+
describe('returns', () => {
37+
it('captures return expressions', () => {
38+
const data = parseAndExtract(
39+
'class Test {\n int Double(int x) {\n return x * 2;\n }\n}\n',
40+
);
41+
expect(data.returns).toEqual(
42+
expect.arrayContaining([
43+
expect.objectContaining({
44+
funcName: 'Double',
45+
referencedNames: expect.arrayContaining(['x']),
46+
}),
47+
]),
48+
);
49+
});
50+
});
51+
52+
describe('assignments', () => {
53+
it('tracks variable from invocation', () => {
54+
const data = parseAndExtract(
55+
'class Test {\n void Main() {\n var result = Compute();\n }\n}\n',
56+
);
57+
expect(data.assignments).toEqual(
58+
expect.arrayContaining([
59+
expect.objectContaining({
60+
varName: 'result',
61+
callerFunc: 'Main',
62+
sourceCallName: 'Compute',
63+
}),
64+
]),
65+
);
66+
});
67+
});
68+
69+
describe('argFlows', () => {
70+
it('detects parameter passed as argument', () => {
71+
const data = parseAndExtract(
72+
'class Test {\n void Process(string input) {\n Transform(input);\n }\n}\n',
73+
);
74+
expect(data.argFlows).toEqual(
75+
expect.arrayContaining([
76+
expect.objectContaining({
77+
callerFunc: 'Process',
78+
calleeName: 'Transform',
79+
argIndex: 0,
80+
argName: 'input',
81+
confidence: 1.0,
82+
}),
83+
]),
84+
);
85+
});
86+
});
87+
88+
describe('mutations', () => {
89+
it('detects Add on parameter collection', () => {
90+
const data = parseAndExtract(
91+
'class Test {\n void AddItem(List<string> items, string item) {\n items.Add(item);\n }\n}\n',
92+
);
93+
expect(data.mutations).toEqual(
94+
expect.arrayContaining([
95+
expect.objectContaining({
96+
funcName: 'AddItem',
97+
receiverName: 'items',
98+
}),
99+
]),
100+
);
101+
});
102+
});
103+
});

tests/parsers/dataflow-go.test.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Unit tests for extractDataflow() against parsed Go ASTs.
3+
*/
4+
import { beforeAll, describe, expect, it } from 'vitest';
5+
import { extractDataflow } from '../../src/dataflow.js';
6+
import { createParsers } from '../../src/parser.js';
7+
8+
describe('extractDataflow — Go', () => {
9+
let parsers;
10+
11+
beforeAll(async () => {
12+
parsers = await createParsers();
13+
});
14+
15+
function parseAndExtract(code) {
16+
const parser = parsers.get('go');
17+
if (!parser) return null;
18+
const tree = parser.parse(code);
19+
return extractDataflow(tree, 'test.go', [], 'go');
20+
}
21+
22+
describe('parameters', () => {
23+
it('extracts simple parameters', () => {
24+
const data = parseAndExtract(
25+
'package main\nfunc add(a int, b int) int {\n\treturn a + b\n}\n',
26+
);
27+
expect(data.parameters).toEqual(
28+
expect.arrayContaining([
29+
expect.objectContaining({ funcName: 'add', paramName: 'a', paramIndex: 0 }),
30+
expect.objectContaining({ funcName: 'add', paramName: 'b', paramIndex: 1 }),
31+
]),
32+
);
33+
});
34+
35+
it('extracts multi-name parameters', () => {
36+
const data = parseAndExtract('package main\nfunc add(a, b int) int {\n\treturn a + b\n}\n');
37+
expect(data.parameters).toEqual(
38+
expect.arrayContaining([
39+
expect.objectContaining({ funcName: 'add', paramName: 'a' }),
40+
expect.objectContaining({ funcName: 'add', paramName: 'b' }),
41+
]),
42+
);
43+
});
44+
});
45+
46+
describe('returns', () => {
47+
it('captures return expressions', () => {
48+
const data = parseAndExtract('package main\nfunc double(x int) int {\n\treturn x * 2\n}\n');
49+
expect(data.returns).toEqual(
50+
expect.arrayContaining([
51+
expect.objectContaining({
52+
funcName: 'double',
53+
referencedNames: expect.arrayContaining(['x']),
54+
}),
55+
]),
56+
);
57+
});
58+
});
59+
60+
describe('assignments', () => {
61+
it('tracks short var declaration from call', () => {
62+
const data = parseAndExtract(
63+
'package main\nfunc main() {\n\tresult := compute()\n\t_ = result\n}\n',
64+
);
65+
expect(data.assignments).toEqual(
66+
expect.arrayContaining([
67+
expect.objectContaining({
68+
varName: 'result',
69+
callerFunc: 'main',
70+
sourceCallName: 'compute',
71+
}),
72+
]),
73+
);
74+
});
75+
});
76+
77+
describe('argFlows', () => {
78+
it('detects parameter passed as argument', () => {
79+
const data = parseAndExtract(
80+
'package main\nfunc process(input string) {\n\ttransform(input)\n}\n',
81+
);
82+
expect(data.argFlows).toEqual(
83+
expect.arrayContaining([
84+
expect.objectContaining({
85+
callerFunc: 'process',
86+
calleeName: 'transform',
87+
argIndex: 0,
88+
argName: 'input',
89+
confidence: 1.0,
90+
}),
91+
]),
92+
);
93+
});
94+
});
95+
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Unit tests for extractDataflow() against parsed Java ASTs.
3+
*/
4+
import { beforeAll, describe, expect, it } from 'vitest';
5+
import { extractDataflow } from '../../src/dataflow.js';
6+
import { createParsers } from '../../src/parser.js';
7+
8+
describe('extractDataflow — Java', () => {
9+
let parsers;
10+
11+
beforeAll(async () => {
12+
parsers = await createParsers();
13+
});
14+
15+
function parseAndExtract(code) {
16+
const parser = parsers.get('java');
17+
if (!parser) return null;
18+
const tree = parser.parse(code);
19+
return extractDataflow(tree, 'Test.java', [], 'java');
20+
}
21+
22+
describe('parameters', () => {
23+
it('extracts simple parameters', () => {
24+
const data = parseAndExtract(
25+
'class Test {\n int add(int a, int b) {\n return a + b;\n }\n}\n',
26+
);
27+
expect(data.parameters).toEqual(
28+
expect.arrayContaining([
29+
expect.objectContaining({ funcName: 'add', paramName: 'a', paramIndex: 0 }),
30+
expect.objectContaining({ funcName: 'add', paramName: 'b', paramIndex: 1 }),
31+
]),
32+
);
33+
});
34+
});
35+
36+
describe('returns', () => {
37+
it('captures return expressions', () => {
38+
const data = parseAndExtract(
39+
'class Test {\n int double(int x) {\n return x * 2;\n }\n}\n',
40+
);
41+
expect(data.returns).toEqual(
42+
expect.arrayContaining([
43+
expect.objectContaining({
44+
funcName: 'double',
45+
referencedNames: expect.arrayContaining(['x']),
46+
}),
47+
]),
48+
);
49+
});
50+
});
51+
52+
describe('assignments', () => {
53+
it('tracks variable from method invocation', () => {
54+
const data = parseAndExtract(
55+
'class Test {\n void main() {\n String result = compute();\n }\n}\n',
56+
);
57+
expect(data.assignments).toEqual(
58+
expect.arrayContaining([
59+
expect.objectContaining({
60+
varName: 'result',
61+
callerFunc: 'main',
62+
sourceCallName: 'compute',
63+
}),
64+
]),
65+
);
66+
});
67+
});
68+
69+
describe('argFlows', () => {
70+
it('detects parameter passed as argument', () => {
71+
const data = parseAndExtract(
72+
'class Test {\n void process(String input) {\n transform(input);\n }\n}\n',
73+
);
74+
expect(data.argFlows).toEqual(
75+
expect.arrayContaining([
76+
expect.objectContaining({
77+
callerFunc: 'process',
78+
calleeName: 'transform',
79+
argIndex: 0,
80+
argName: 'input',
81+
confidence: 1.0,
82+
}),
83+
]),
84+
);
85+
});
86+
});
87+
88+
describe('mutations', () => {
89+
it('detects add on parameter collection', () => {
90+
const data = parseAndExtract(
91+
'class Test {\n void addItem(List<String> items, String item) {\n items.add(item);\n }\n}\n',
92+
);
93+
expect(data.mutations).toEqual(
94+
expect.arrayContaining([
95+
expect.objectContaining({
96+
funcName: 'addItem',
97+
receiverName: 'items',
98+
}),
99+
]),
100+
);
101+
});
102+
});
103+
});

tests/parsers/dataflow-javascript.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe('extractDataflow — JavaScript', () => {
1515
function parseAndExtract(code) {
1616
const parser = parsers.get('javascript');
1717
const tree = parser.parse(code);
18-
return extractDataflow(tree, 'test.js', []);
18+
return extractDataflow(tree, 'test.js', [], 'javascript');
1919
}
2020

2121
// ── Parameter extraction ──────────────────────────────────────────────

tests/parsers/dataflow-php.test.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* Unit tests for extractDataflow() against parsed PHP ASTs.
3+
*/
4+
import { beforeAll, describe, expect, it } from 'vitest';
5+
import { extractDataflow } from '../../src/dataflow.js';
6+
import { createParsers } from '../../src/parser.js';
7+
8+
describe('extractDataflow — PHP', () => {
9+
let parsers;
10+
11+
beforeAll(async () => {
12+
parsers = await createParsers();
13+
});
14+
15+
function parseAndExtract(code) {
16+
const parser = parsers.get('php');
17+
if (!parser) return null;
18+
const tree = parser.parse(code);
19+
return extractDataflow(tree, 'test.php', [], 'php');
20+
}
21+
22+
describe('parameters', () => {
23+
it('extracts simple parameters', () => {
24+
const data = parseAndExtract('<?php\nfunction add($a, $b) {\n return $a + $b;\n}\n');
25+
expect(data.parameters).toEqual(
26+
expect.arrayContaining([
27+
expect.objectContaining({ funcName: 'add', paramName: '$a', paramIndex: 0 }),
28+
expect.objectContaining({ funcName: 'add', paramName: '$b', paramIndex: 1 }),
29+
]),
30+
);
31+
});
32+
});
33+
34+
describe('returns', () => {
35+
it('captures return expressions', () => {
36+
const data = parseAndExtract('<?php\nfunction double($x) {\n return $x * 2;\n}\n');
37+
expect(data.returns).toEqual(
38+
expect.arrayContaining([expect.objectContaining({ funcName: 'double' })]),
39+
);
40+
});
41+
});
42+
43+
describe('assignments', () => {
44+
it('tracks variable from function call', () => {
45+
const data = parseAndExtract(
46+
'<?php\nfunction main() {\n $result = compute();\n return $result;\n}\n',
47+
);
48+
expect(data.assignments).toEqual(
49+
expect.arrayContaining([
50+
expect.objectContaining({
51+
varName: '$result',
52+
callerFunc: 'main',
53+
sourceCallName: 'compute',
54+
}),
55+
]),
56+
);
57+
});
58+
});
59+
60+
describe('argFlows', () => {
61+
it('detects parameter passed as argument', () => {
62+
const data = parseAndExtract(
63+
'<?php\nfunction process($input) {\n transform($input);\n}\n',
64+
);
65+
expect(data.argFlows).toEqual(
66+
expect.arrayContaining([
67+
expect.objectContaining({
68+
callerFunc: 'process',
69+
calleeName: 'transform',
70+
argIndex: 0,
71+
argName: '$input',
72+
confidence: 1.0,
73+
}),
74+
]),
75+
);
76+
});
77+
});
78+
});

0 commit comments

Comments
 (0)