mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
test(type-resolution): add integration tests for Milestone D across all 11 languages + fix Kotlin null-check narrowing
Adds 17 new fixture directories and 23 new describe blocks covering every
feature in Milestone D (Phases A, B, C) with full cross-language integration
test coverage:
Phase A — Fixpoint Completeness:
- TS/JS object destructuring (const { field } = obj → fieldAccess resolution)
- TS/JS post-fixpoint for-loop replay (iterable var resolved by fixpoint)
- Rust struct_pattern destructuring (let Point { x, y } = p)
Phase B — Inheritance & Receivers:
- Grandparent MRO (depth-2 C→B→A) for all 9 OOP languages:
TS, Kotlin, C#, C++, Java, PHP, Python, Ruby, JS
- Go inc/dec write access (obj.Field++/-- emit ACCESSES write edges)
Phase C — Branch-Sensitive Narrowing:
- Null-check narrowing for TS (!==null, !=null, !==undefined),
C# (!=null, is not null), and Kotlin (!=null)
Bug fix — Kotlin null-check narrowing (3 issues in jvm.ts):
1. patternBindingNodeTypes registered 'comparison_expression' but
tree-sitter-kotlin produces 'equality_expression' for !=
2. Handler checked for 'null_literal' named child but 'null' is an
anonymous node in the Kotlin grammar
3. extractKotlinParameter only searched for 'user_type' direct child,
missing 'nullable_type' wrapper (so x: User? never got a base binding)
17 fixtures, 23 describe blocks, 705 new lines of test code, 0 failures.
This commit is contained in:
parent
7c72cefd8d
commit
e9ccec1a52
79 changed files with 1146 additions and 18 deletions
|
|
@ -1,7 +1,7 @@
|
|||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **GitNexus** (2094 symbols, 4982 relationships, 159 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
This project is indexed by GitNexus as **GitNexus** (2169 symbols, 5213 relationships, 165 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
|
||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **GitNexus** (2094 symbols, 4982 relationships, 159 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
This project is indexed by GitNexus as **GitNexus** (2169 symbols, 5213 relationships, 165 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
|
||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||
|
||||
|
|
|
|||
|
|
@ -341,7 +341,8 @@ const extractKotlinParameter: ParameterExtractor = (node: SyntaxNode, env: Map<s
|
|||
|
||||
// Fallback: Kotlin `parameter` nodes use positional children, not named fields
|
||||
if (!nameNode) nameNode = findChildByType(node, 'simple_identifier');
|
||||
if (!typeNode) typeNode = findChildByType(node, 'user_type');
|
||||
if (!typeNode) typeNode = findChildByType(node, 'user_type')
|
||||
?? findChildByType(node, 'nullable_type');
|
||||
|
||||
if (!nameNode || !typeNode) return;
|
||||
const varName = extractVarName(nameNode);
|
||||
|
|
@ -678,22 +679,22 @@ const extractKotlinPatternBinding: PatternBindingExtractor = (node, scopeEnv, de
|
|||
}
|
||||
|
||||
// Null-check narrowing: if (x != null) { ... }
|
||||
// Kotlin AST: comparison_expression > simple_identifier, "!=", null_literal
|
||||
if (node.type === 'comparison_expression') {
|
||||
// Kotlin AST: equality_expression > simple_identifier, "!=" [anon], "null" [anon]
|
||||
// Note: `null` is an anonymous node in tree-sitter-kotlin, not `null_literal`.
|
||||
if (node.type === 'equality_expression') {
|
||||
const op = node.children.find(c => !c.isNamed && c.text === '!=');
|
||||
if (!op) return undefined;
|
||||
|
||||
const left = node.namedChild(0);
|
||||
const right = node.namedChild(1);
|
||||
if (!left || !right) return undefined;
|
||||
|
||||
// `null` is anonymous in Kotlin grammar — use positional child scan
|
||||
let varNode: SyntaxNode | undefined;
|
||||
if (left.type === 'simple_identifier' && right.type === 'null_literal') {
|
||||
varNode = left;
|
||||
} else if (right.type === 'simple_identifier' && left.type === 'null_literal') {
|
||||
varNode = right;
|
||||
let hasNull = false;
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (!c) continue;
|
||||
if (c.type === 'simple_identifier') varNode = c;
|
||||
if (!c.isNamed && c.text === 'null') hasNull = true;
|
||||
}
|
||||
if (!varNode) return undefined;
|
||||
if (!varNode || !hasNull) return undefined;
|
||||
|
||||
const varName = varNode.text;
|
||||
const resolvedType = scopeEnv.get(varName);
|
||||
|
|
@ -729,7 +730,7 @@ export const kotlinTypeConfig: LanguageTypeConfig = {
|
|||
allowPatternBindingOverwrite: true,
|
||||
declarationNodeTypes: KOTLIN_DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: KOTLIN_FOR_LOOP_NODE_TYPES,
|
||||
patternBindingNodeTypes: new Set(['type_test', 'comparison_expression']),
|
||||
patternBindingNodeTypes: new Set(['type_test', 'equality_expression']),
|
||||
extractDeclaration: extractKotlinDeclaration,
|
||||
extractParameter: extractKotlinParameter,
|
||||
extractInitializer: extractKotlinInitializer,
|
||||
|
|
|
|||
7
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/A.h
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/A.h
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#pragma once
|
||||
#include "Greeting.h"
|
||||
|
||||
class A {
|
||||
public:
|
||||
Greeting greet() { return Greeting(); }
|
||||
};
|
||||
4
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/B.h
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/B.h
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#pragma once
|
||||
#include "A.h"
|
||||
|
||||
class B : public A {};
|
||||
4
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/C.h
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/C.h
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#pragma once
|
||||
#include "B.h"
|
||||
|
||||
class C : public B {};
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/Greeting.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/Greeting.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
class Greeting {
|
||||
public:
|
||||
void save() {}
|
||||
};
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/app.cpp
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#include "C.h"
|
||||
|
||||
void process() {
|
||||
C c;
|
||||
c.greet().save();
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/A.cs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/A.cs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace Grandparent.Models
|
||||
{
|
||||
public class A
|
||||
{
|
||||
public Greeting Greet() => new Greeting();
|
||||
}
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/B.cs
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/B.cs
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
namespace Grandparent.Models
|
||||
{
|
||||
public class B : A {}
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/C.cs
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/C.cs
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
namespace Grandparent.Models
|
||||
{
|
||||
public class C : B {}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/Greeting.cs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/Greeting.cs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace Grandparent.Models
|
||||
{
|
||||
public class Greeting
|
||||
{
|
||||
public void Save() {}
|
||||
}
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Services/App.cs
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Services/App.cs
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using Grandparent.Models;
|
||||
|
||||
namespace Grandparent.Services
|
||||
{
|
||||
public class App
|
||||
{
|
||||
public void Process()
|
||||
{
|
||||
var c = new C();
|
||||
c.Greet().Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Models/Repo.cs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Models/Repo.cs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace NullCheck.Models
|
||||
{
|
||||
public class Repo
|
||||
{
|
||||
public void Save() {}
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Models/User.cs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Models/User.cs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace NullCheck.Models
|
||||
{
|
||||
public class User
|
||||
{
|
||||
public void Save() {}
|
||||
}
|
||||
}
|
||||
23
gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Services/App.cs
vendored
Normal file
23
gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Services/App.cs
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using NullCheck.Models;
|
||||
|
||||
namespace NullCheck.Services
|
||||
{
|
||||
public class App
|
||||
{
|
||||
public void ProcessInequality(User x)
|
||||
{
|
||||
if (x != null)
|
||||
{
|
||||
x.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessIsNotNull(User x)
|
||||
{
|
||||
if (x is not null)
|
||||
{
|
||||
x.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
gitnexus/test/fixtures/lang-resolution/go-inc-dec-write-access/main.go
vendored
Normal file
15
gitnexus/test/fixtures/lang-resolution/go-inc-dec-write-access/main.go
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package main
|
||||
|
||||
type Counter struct {
|
||||
Count int
|
||||
Total int
|
||||
}
|
||||
|
||||
func increment(c *Counter) {
|
||||
c.Count++
|
||||
c.Total++
|
||||
}
|
||||
|
||||
func decrement(c *Counter) {
|
||||
c.Count--
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/A.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/A.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models;
|
||||
|
||||
public class A {
|
||||
public Greeting greet() { return new Greeting(); }
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/B.java
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/B.java
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
package models;
|
||||
|
||||
public class B extends A {}
|
||||
3
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/C.java
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/C.java
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
package models;
|
||||
|
||||
public class C extends B {}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/Greeting.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/Greeting.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models;
|
||||
|
||||
public class Greeting {
|
||||
public void save() {}
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/services/App.java
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/services/App.java
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package services;
|
||||
|
||||
import models.C;
|
||||
|
||||
public class App {
|
||||
public void process() {
|
||||
C c = new C();
|
||||
c.greet().save();
|
||||
}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/js-fixpoint-for-loop/app.js
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/js-fixpoint-for-loop/app.js
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
const { getUsers } = require('./models');
|
||||
|
||||
function process() {
|
||||
const users = getUsers();
|
||||
for (const u of users) {
|
||||
u.save();
|
||||
}
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/js-fixpoint-for-loop/models.js
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/js-fixpoint-for-loop/models.js
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* @class
|
||||
*/
|
||||
class User {
|
||||
save() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {User[]}
|
||||
*/
|
||||
function getUsers() { return []; }
|
||||
|
||||
module.exports = { User, getUsers };
|
||||
7
gitnexus/test/fixtures/lang-resolution/js-object-destructuring/app.js
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/js-object-destructuring/app.js
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
const { getUser } = require('./service');
|
||||
|
||||
function processDestructured() {
|
||||
const user = getUser();
|
||||
const { address } = user;
|
||||
address.save();
|
||||
}
|
||||
19
gitnexus/test/fixtures/lang-resolution/js-object-destructuring/models.js
vendored
Normal file
19
gitnexus/test/fixtures/lang-resolution/js-object-destructuring/models.js
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* @class
|
||||
*/
|
||||
class Address {
|
||||
/** @returns {boolean} */
|
||||
save() { return true; }
|
||||
}
|
||||
|
||||
/**
|
||||
* @class
|
||||
*/
|
||||
class User {
|
||||
constructor() {
|
||||
/** @type {Address} */
|
||||
this.address = new Address();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Address, User };
|
||||
8
gitnexus/test/fixtures/lang-resolution/js-object-destructuring/service.js
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/js-object-destructuring/service.js
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
const { User } = require('./models');
|
||||
|
||||
/**
|
||||
* @returns {User}
|
||||
*/
|
||||
function getUser() { return new User(); }
|
||||
|
||||
module.exports = { getUser };
|
||||
5
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/A.kt
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/A.kt
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models
|
||||
|
||||
open class A {
|
||||
fun greet(): Greeting = Greeting()
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/B.kt
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/B.kt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
package models
|
||||
|
||||
open class B : A()
|
||||
3
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/C.kt
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/C.kt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
package models
|
||||
|
||||
class C : B()
|
||||
5
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/Greeting.kt
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/Greeting.kt
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models
|
||||
|
||||
class Greeting {
|
||||
fun save() {}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/services/App.kt
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/services/App.kt
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package services
|
||||
|
||||
import models.C
|
||||
|
||||
fun process() {
|
||||
val c = C()
|
||||
c.greet().save()
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/models/Repo.kt
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/models/Repo.kt
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models
|
||||
|
||||
class Repo {
|
||||
fun save() {}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/models/User.kt
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/models/User.kt
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models
|
||||
|
||||
class User {
|
||||
fun save() {}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/services/App.kt
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/services/App.kt
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package services
|
||||
|
||||
import models.User
|
||||
|
||||
fun processNullable(x: User?) {
|
||||
if (x != null) {
|
||||
x.save()
|
||||
}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/A.php
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/A.php
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class A
|
||||
{
|
||||
public function greet(): Greeting { return new Greeting(); }
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/B.php
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/B.php
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class B extends A {}
|
||||
5
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/C.php
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/C.php
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class C extends B {}
|
||||
8
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/Greeting.php
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/Greeting.php
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class Greeting
|
||||
{
|
||||
public function save(): void {}
|
||||
}
|
||||
14
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Services/App.php
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Services/App.php
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\C;
|
||||
|
||||
class App
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$c = new C();
|
||||
$c->greet()->save();
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/composer.json
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/composer.json
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/"
|
||||
}
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/app.py
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/app.py
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from models.c import C
|
||||
|
||||
def process():
|
||||
c = C()
|
||||
c.greet().save()
|
||||
0
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/__init__.py
vendored
Normal file
0
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/__init__.py
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/a.py
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/a.py
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .greeting import Greeting
|
||||
|
||||
class A:
|
||||
def greet(self) -> Greeting:
|
||||
return Greeting()
|
||||
4
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/b.py
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/b.py
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .a import A
|
||||
|
||||
class B(A):
|
||||
pass
|
||||
4
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/c.py
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/c.py
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .b import B
|
||||
|
||||
class C(B):
|
||||
pass
|
||||
3
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/greeting.py
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/greeting.py
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class Greeting:
|
||||
def save(self) -> None:
|
||||
pass
|
||||
6
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/app.rb
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/app.rb
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
require_relative 'models/c'
|
||||
|
||||
def process
|
||||
c = C.new
|
||||
c.greet.save
|
||||
end
|
||||
7
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/a.rb
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/a.rb
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
require_relative 'greeting'
|
||||
|
||||
class A
|
||||
def greet
|
||||
Greeting.new
|
||||
end
|
||||
end
|
||||
4
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/b.rb
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/b.rb
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
require_relative 'a'
|
||||
|
||||
class B < A
|
||||
end
|
||||
4
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/c.rb
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/c.rb
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
require_relative 'b'
|
||||
|
||||
class C < B
|
||||
end
|
||||
5
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/greeting.rb
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/greeting.rb
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class Greeting
|
||||
def save
|
||||
true
|
||||
end
|
||||
end
|
||||
12
gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/main.rs
vendored
Normal file
12
gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/main.rs
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
mod point;
|
||||
mod vec2;
|
||||
|
||||
use crate::point::Point;
|
||||
|
||||
fn process(p: Point) {
|
||||
let Point { x, y } = p;
|
||||
x.save();
|
||||
y.save();
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
6
gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/point.rs
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/point.rs
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use crate::vec2::Vec2;
|
||||
|
||||
pub struct Point {
|
||||
pub x: Vec2,
|
||||
pub y: Vec2,
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/vec2.rs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/vec2.rs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
pub struct Vec2 {
|
||||
pub value: f32,
|
||||
}
|
||||
|
||||
impl Vec2 {
|
||||
pub fn save(&self) {}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/ts-fixpoint-for-loop/src/app.ts
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/ts-fixpoint-for-loop/src/app.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { getUsers } from './models';
|
||||
|
||||
function process() {
|
||||
const users = getUsers();
|
||||
for (const u of users) {
|
||||
u.save();
|
||||
}
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/ts-fixpoint-for-loop/src/models.ts
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/ts-fixpoint-for-loop/src/models.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export class User {
|
||||
save(): void {}
|
||||
}
|
||||
export function getUsers(): User[] {
|
||||
return [];
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/app.ts
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/app.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { C } from './derived';
|
||||
|
||||
function process() {
|
||||
const c = new C();
|
||||
c.greet().save();
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/base.ts
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/base.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Greeting } from './greeting';
|
||||
|
||||
export class A {
|
||||
greet(): Greeting { return new Greeting(); }
|
||||
}
|
||||
2
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/derived.ts
vendored
Normal file
2
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/derived.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import { B } from './middle';
|
||||
export class C extends B {}
|
||||
3
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/greeting.ts
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/greeting.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export class Greeting {
|
||||
save(): void {}
|
||||
}
|
||||
2
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/middle.ts
vendored
Normal file
2
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/middle.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import { A } from './base';
|
||||
export class B extends A {}
|
||||
19
gitnexus/test/fixtures/lang-resolution/ts-null-check-narrowing/src/app.ts
vendored
Normal file
19
gitnexus/test/fixtures/lang-resolution/ts-null-check-narrowing/src/app.ts
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { User } from './models';
|
||||
|
||||
function processStrict(x: User | null) {
|
||||
if (x !== null) {
|
||||
x.save();
|
||||
}
|
||||
}
|
||||
|
||||
function processLoose(x: User | null) {
|
||||
if (x != null) {
|
||||
x.save();
|
||||
}
|
||||
}
|
||||
|
||||
function processUndefined(x: User | undefined) {
|
||||
if (x !== undefined) {
|
||||
x.save();
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/ts-null-check-narrowing/src/models.ts
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/ts-null-check-narrowing/src/models.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export class User {
|
||||
save(): void {}
|
||||
}
|
||||
|
||||
export class Repo {
|
||||
save(): void {}
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/app.ts
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/app.ts
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { getUser } from './service';
|
||||
|
||||
function processDestructured() {
|
||||
const user = getUser();
|
||||
const { address } = user;
|
||||
address.save();
|
||||
}
|
||||
|
||||
function processMultiField() {
|
||||
const user = getUser();
|
||||
const { name, address } = user;
|
||||
address.save();
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/models.ts
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/models.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export class Address {
|
||||
city: string = '';
|
||||
save(): boolean { return true; }
|
||||
}
|
||||
|
||||
export class User {
|
||||
name: string = '';
|
||||
address: Address = new Address();
|
||||
}
|
||||
2
gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/service.ts
vendored
Normal file
2
gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/service.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import { User } from './models';
|
||||
export function getUser(): User { return new User(); }
|
||||
|
|
@ -1015,3 +1015,49 @@ describe('C++ method chain binding via unified fixpoint (Phase 9C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: Deep MRO — walkParentChain() at depth 2 (C→B→A)
|
||||
// greet() is defined on A, accessed via C. Tests BFS depth-2 parent traversal.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C++ grandparent method resolution via MRO (Phase B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-grandparent-resolution'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects A, B, C, Greeting classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('A');
|
||||
expect(classes).toContain('B');
|
||||
expect(classes).toContain('C');
|
||||
expect(classes).toContain('Greeting');
|
||||
});
|
||||
|
||||
it('emits EXTENDS edges: B→A, C→B', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(edgeSet(extends_)).toContain('B → A');
|
||||
expect(edgeSet(extends_)).toContain('C → B');
|
||||
});
|
||||
|
||||
it('resolves c.greet().save() to Greeting#save via depth-2 MRO lookup', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('Greeting'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves c.greet() to A#greet (method found via MRO walk)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const greetCall = calls.find(c =>
|
||||
c.target === 'greet' && c.targetFilePath.includes('A.h'),
|
||||
);
|
||||
expect(greetCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1362,3 +1362,94 @@ describe('C# method chain binding via unified fixpoint (Phase 9C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: Deep MRO — walkParentChain() at depth 2 (C→B→A)
|
||||
// Greet() is defined on A, accessed via C. Tests BFS depth-2 parent traversal.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C# grandparent method resolution via MRO (Phase B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'csharp-grandparent-resolution'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects A, B, C, Greeting classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('A');
|
||||
expect(classes).toContain('B');
|
||||
expect(classes).toContain('C');
|
||||
expect(classes).toContain('Greeting');
|
||||
});
|
||||
|
||||
it('emits EXTENDS edges: B→A, C→B', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(edgeSet(extends_)).toContain('B → A');
|
||||
expect(edgeSet(extends_)).toContain('C → B');
|
||||
});
|
||||
|
||||
it('resolves c.Greet().Save() to Greeting#Save via depth-2 MRO lookup', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'Save' && c.targetFilePath.includes('Greeting'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves c.Greet() to A#Greet (method found via MRO walk)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const greetCall = calls.find(c =>
|
||||
c.target === 'Greet' && c.targetFilePath.includes('A.cs'),
|
||||
);
|
||||
expect(greetCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase C: C# null-check narrowing — if (x != null) and if (x is not null)
|
||||
// Both patterns emit patternOverrides for the if-body position range
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C# null-check narrowing resolution (Phase C)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'csharp-null-check-narrowing'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User and Repo classes', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
|
||||
});
|
||||
|
||||
it('resolves x.Save() inside != null guard (ProcessInequality) to User#Save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'Save' && c.source === 'ProcessInequality' && c.targetFilePath.includes('User'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves x.Save() inside is not null guard (ProcessIsNotNull) to User#Save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'Save' && c.source === 'ProcessIsNotNull' && c.targetFilePath.includes('User'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('does NOT cross-resolve to Repo#Save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const wrongCall = calls.find(c =>
|
||||
c.target === 'Save' && c.targetFilePath.includes('Repo'),
|
||||
);
|
||||
expect(wrongCall).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1145,3 +1145,40 @@ describe('Go method chain binding via unified fixpoint (Phase 9C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: Go inc_statement / dec_statement write access
|
||||
// obj.Field++ and obj.Field-- emit ACCESSES write edges
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Go inc/dec write access tracking (Phase B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'go-inc-dec-write-access'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('emits ACCESSES write edge for Count++ in increment', () => {
|
||||
const accesses = getRelationships(result, 'ACCESSES');
|
||||
const writes = accesses.filter(e => e.rel.reason === 'write');
|
||||
const countInc = writes.find(e => e.target === 'Count' && e.source === 'increment');
|
||||
expect(countInc).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits ACCESSES write edge for Total++ in increment', () => {
|
||||
const accesses = getRelationships(result, 'ACCESSES');
|
||||
const writes = accesses.filter(e => e.rel.reason === 'write');
|
||||
const totalInc = writes.find(e => e.target === 'Total' && e.source === 'increment');
|
||||
expect(totalInc).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits ACCESSES write edge for Count-- in decrement', () => {
|
||||
const accesses = getRelationships(result, 'ACCESSES');
|
||||
const writes = accesses.filter(e => e.rel.reason === 'write');
|
||||
const countDec = writes.find(e => e.target === 'Count' && e.source === 'decrement');
|
||||
expect(countDec).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1278,3 +1278,49 @@ describe('Java method chain binding via unified fixpoint (Phase 9C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: Deep MRO — walkParentChain() at depth 2 (C→B→A)
|
||||
// greet() is defined on A, accessed via C. Tests BFS depth-2 parent traversal.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Java grandparent method resolution via MRO (Phase B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'java-grandparent-resolution'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects A, B, C, Greeting classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('A');
|
||||
expect(classes).toContain('B');
|
||||
expect(classes).toContain('C');
|
||||
expect(classes).toContain('Greeting');
|
||||
});
|
||||
|
||||
it('emits EXTENDS edges: B→A, C→B', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(edgeSet(extends_)).toContain('B → A');
|
||||
expect(edgeSet(extends_)).toContain('C → B');
|
||||
});
|
||||
|
||||
it('resolves c.greet().save() to Greeting#save via depth-2 MRO lookup', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('Greeting'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves c.greet() to A#greet (method found via MRO walk)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const greetCall = calls.find(c =>
|
||||
c.target === 'greet' && c.targetFilePath.includes('A.java'),
|
||||
);
|
||||
expect(greetCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -302,3 +302,55 @@ describe('Write access tracking (JavaScript)', () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase A: JS object destructuring — const { field } = receiver → fieldAccess PendingAssignment
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('JavaScript object destructuring resolution (Phase A)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'js-object-destructuring'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User, Address classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('User');
|
||||
expect(classes).toContain('Address');
|
||||
});
|
||||
|
||||
it('resolves address.save() to Address#save via object destructuring', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('models'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase A: Post-fixpoint for-loop replay — iterable resolved via callResult fixpoint
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('JavaScript post-fixpoint for-loop replay (Phase A ex-9B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'js-fixpoint-for-loop'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('resolves u.save() to User#save via post-fixpoint for-loop replay', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'process' && c.targetFilePath.includes('models'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1441,3 +1441,86 @@ describe('Kotlin method chain binding via unified fixpoint (Phase 9C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: Deep MRO — walkParentChain() at depth 2 (C→B→A)
|
||||
// greet() is defined on A, accessed via C. Tests BFS depth-2 parent traversal.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Kotlin grandparent method resolution via MRO (Phase B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'kotlin-grandparent-resolution'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects A, B, C, Greeting classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('A');
|
||||
expect(classes).toContain('B');
|
||||
expect(classes).toContain('C');
|
||||
expect(classes).toContain('Greeting');
|
||||
});
|
||||
|
||||
it('emits EXTENDS edges: B→A, C→B', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(edgeSet(extends_)).toContain('B → A');
|
||||
expect(edgeSet(extends_)).toContain('C → B');
|
||||
});
|
||||
|
||||
it('resolves c.greet().save() to Greeting#save via depth-2 MRO lookup', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('Greeting'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves c.greet() to A#greet (method found via MRO walk)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const greetCall = calls.find(c =>
|
||||
c.target === 'greet' && c.targetFilePath.includes('A.kt'),
|
||||
);
|
||||
expect(greetCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase C: Kotlin null-check narrowing — if (x != null) { x.save() }
|
||||
// NOTE: depends on nullable_type capture being fixed in jvm.ts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Kotlin null-check narrowing resolution (Phase C)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'kotlin-null-check-narrowing'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User and Repo classes', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
|
||||
});
|
||||
|
||||
it('resolves x.save() inside != null guard to User#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processNullable' && c.targetFilePath.includes('User'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('does NOT resolve to Repo#save (no cross-contamination)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const wrongCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('Repo'),
|
||||
);
|
||||
expect(wrongCall).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1382,3 +1382,49 @@ describe('PHP method chain binding via unified fixpoint (Phase 9C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: Deep MRO — walkParentChain() at depth 2 (C→B→A)
|
||||
// greet() is defined on A, accessed via C. Tests BFS depth-2 parent traversal.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('PHP grandparent method resolution via MRO (Phase B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'php-grandparent-resolution'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects A, B, C, Greeting classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('A');
|
||||
expect(classes).toContain('B');
|
||||
expect(classes).toContain('C');
|
||||
expect(classes).toContain('Greeting');
|
||||
});
|
||||
|
||||
it('emits EXTENDS edges: B→A, C→B', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(edgeSet(extends_)).toContain('B → A');
|
||||
expect(edgeSet(extends_)).toContain('C → B');
|
||||
});
|
||||
|
||||
it('resolves $c->greet()->save() to Greeting#save via depth-2 MRO lookup', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('Greeting'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves $c->greet() to A#greet (method found via MRO walk)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const greetCall = calls.find(c =>
|
||||
c.target === 'greet' && c.targetFilePath.includes('A.php'),
|
||||
);
|
||||
expect(greetCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1428,3 +1428,49 @@ describe('Python method chain binding via unified fixpoint (Phase 9C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: Deep MRO — walkParentChain() at depth 2 (C→B→A)
|
||||
// greet() is defined on A, accessed via C. Tests BFS depth-2 parent traversal.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Python grandparent method resolution via MRO (Phase B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'python-grandparent-resolution'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects A, B, C, Greeting classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('A');
|
||||
expect(classes).toContain('B');
|
||||
expect(classes).toContain('C');
|
||||
expect(classes).toContain('Greeting');
|
||||
});
|
||||
|
||||
it('emits EXTENDS edges: B→A, C→B', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(edgeSet(extends_)).toContain('B → A');
|
||||
expect(edgeSet(extends_)).toContain('C → B');
|
||||
});
|
||||
|
||||
it('resolves c.greet().save() to Greeting#save via depth-2 MRO lookup', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('greeting'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves c.greet() to A#greet (method found via MRO walk)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const greetCall = calls.find(c =>
|
||||
c.target === 'greet' && c.targetFilePath.includes('a.py'),
|
||||
);
|
||||
expect(greetCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1012,3 +1012,49 @@ describe('Ruby method chain binding via unified fixpoint (Phase 9C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: Deep MRO — walkParentChain() at depth 2 (C→B→A)
|
||||
// greet is defined on A, accessed via C. Tests BFS depth-2 parent traversal.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Ruby grandparent method resolution via MRO (Phase B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ruby-grandparent-resolution'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects A, B, C, Greeting classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('A');
|
||||
expect(classes).toContain('B');
|
||||
expect(classes).toContain('C');
|
||||
expect(classes).toContain('Greeting');
|
||||
});
|
||||
|
||||
it('emits EXTENDS edges: B→A, C→B', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(edgeSet(extends_)).toContain('B → A');
|
||||
expect(edgeSet(extends_)).toContain('C → B');
|
||||
});
|
||||
|
||||
it('resolves c.greet.save to Greeting#save via depth-2 MRO lookup', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('greeting'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves c.greet to A#greet (method found via MRO walk)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const greetCall = calls.find(c =>
|
||||
c.target === 'greet' && c.targetFilePath.includes('a.rb'),
|
||||
);
|
||||
expect(greetCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1475,3 +1475,40 @@ describe('Rust method chain binding via unified fixpoint (Phase 9C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase A: Rust struct_pattern destructuring — let Point { x, y } = p
|
||||
// Each field emits a fieldAccess PendingAssignment; fixpoint resolves x/y → Vec2
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Rust struct_pattern destructuring resolution (Phase A)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'rust-struct-destructuring'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects Point and Vec2 structs', () => {
|
||||
const classes = getNodesByLabel(result, 'Struct');
|
||||
expect(classes).toContain('Point');
|
||||
expect(classes).toContain('Vec2');
|
||||
});
|
||||
|
||||
it('resolves x.save() to Vec2#save via struct destructuring', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'process' && c.targetFilePath.includes('vec2'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves both x.save() and y.save() — emits at least 1 CALLS to Vec2#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCalls = calls.filter(c => c.target === 'save' && c.targetFilePath.includes('vec2'));
|
||||
// Both x and y are Vec2 — the same function, so calls may deduplicate to 1
|
||||
expect(saveCalls.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2054,3 +2054,163 @@ describe('TypeScript method chain binding via unified fixpoint (Phase 9C)', () =
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase A: Object destructuring — const { field } = receiver → fieldAccess PendingAssignment
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('TypeScript object destructuring resolution (Phase A)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ts-object-destructuring'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User, Address classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('User');
|
||||
expect(classes).toContain('Address');
|
||||
});
|
||||
|
||||
it('resolves address.save() to Address#save via object destructuring', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('models'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('does NOT resolve save() to a wrong target', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCalls = calls.filter(c => c.target === 'save');
|
||||
for (const call of saveCalls) {
|
||||
expect(call.targetFilePath).toContain('models');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase A: Post-fixpoint for-loop replay — iterable resolved via callResult fixpoint
|
||||
// Differs from ts-for-of-call-expression: iterable is an identifier, not inline call
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('TypeScript post-fixpoint for-loop replay (Phase A ex-9B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ts-fixpoint-for-loop'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('resolves u.save() to User#save via post-fixpoint for-loop replay', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'process' && c.targetFilePath.includes('models'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: Deep MRO — walkParentChain() at depth 2 (C→B→A)
|
||||
// greet() is defined on A, accessed via C. Tests BFS depth-2 parent traversal.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('TypeScript grandparent method resolution via MRO (Phase B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ts-grandparent-resolution'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects 3 classes in inheritance chain (A, B, C) plus Greeting', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('A');
|
||||
expect(classes).toContain('B');
|
||||
expect(classes).toContain('C');
|
||||
expect(classes).toContain('Greeting');
|
||||
});
|
||||
|
||||
it('emits EXTENDS edges: B→A, C→B', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(edgeSet(extends_)).toContain('B → A');
|
||||
expect(edgeSet(extends_)).toContain('C → B');
|
||||
});
|
||||
|
||||
it('resolves c.greet().save() to Greeting#save via depth-2 MRO lookup', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('greeting'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves c.greet() to A#greet (method found via MRO walk)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const greetCall = calls.find(c =>
|
||||
c.target === 'greet' && c.targetFilePath.includes('base'),
|
||||
);
|
||||
expect(greetCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase C: TS null-check narrowing — if (x !== null) { x.save() }
|
||||
// patternOverrides stores narrowed type for the if-body position range
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('TypeScript null-check narrowing resolution (Phase C)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ts-null-check-narrowing'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User and Repo classes', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
|
||||
});
|
||||
|
||||
it('resolves x.save() inside !== null guard to User#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processStrict' && c.targetFilePath.includes('models'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('does NOT resolve to Repo#save (no cross-contamination)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const wrongCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetLabel === 'Repo',
|
||||
);
|
||||
expect(wrongCall).toBeUndefined();
|
||||
});
|
||||
|
||||
it('resolves x.save() in loose != null check (processLoose)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processLoose' && c.targetFilePath.includes('models'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves x.save() in !== undefined check (processUndefined)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processUndefined' && c.targetFilePath.includes('models'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3797,9 +3797,7 @@ function process(x) {
|
|||
expect(flatGet(env, 'x')).toBe('User');
|
||||
});
|
||||
|
||||
// TODO: Kotlin nullable parameter type capture needs tree-sitter grammar investigation
|
||||
// The nullable_type node may not be captured via the current declarationTypeNodes path
|
||||
it.skip('Kotlin: if (x != null) narrows nullable type inside if-body', () => {
|
||||
it('Kotlin: if (x != null) narrows nullable type inside if-body', () => {
|
||||
const code = `
|
||||
fun process(x: User?) {
|
||||
if (x != null) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue