diff --git a/AGENTS.md b/AGENTS.md index 58eead9d6..cd70281f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # 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. diff --git a/CLAUDE.md b/CLAUDE.md index 58eead9d6..cd70281f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # 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. diff --git a/gitnexus/src/core/ingestion/type-extractors/jvm.ts b/gitnexus/src/core/ingestion/type-extractors/jvm.ts index c05a918d9..3604b50eb 100644 --- a/gitnexus/src/core/ingestion/type-extractors/jvm.ts +++ b/gitnexus/src/core/ingestion/type-extractors/jvm.ts @@ -341,7 +341,8 @@ const extractKotlinParameter: ParameterExtractor = (node: SyntaxNode, env: Map 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, diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/A.h b/gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/A.h new file mode 100644 index 000000000..f34c8dc32 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/A.h @@ -0,0 +1,7 @@ +#pragma once +#include "Greeting.h" + +class A { +public: + Greeting greet() { return Greeting(); } +}; diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/B.h b/gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/B.h new file mode 100644 index 000000000..dcc2220d1 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/B.h @@ -0,0 +1,4 @@ +#pragma once +#include "A.h" + +class B : public A {}; diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/C.h b/gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/C.h new file mode 100644 index 000000000..c664e8a5b --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/C.h @@ -0,0 +1,4 @@ +#pragma once +#include "B.h" + +class C : public B {}; diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/Greeting.h b/gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/Greeting.h new file mode 100644 index 000000000..b19ae1a81 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/Greeting.h @@ -0,0 +1,6 @@ +#pragma once + +class Greeting { +public: + void save() {} +}; diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/app.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/app.cpp new file mode 100644 index 000000000..3f702861a --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/app.cpp @@ -0,0 +1,6 @@ +#include "C.h" + +void process() { + C c; + c.greet().save(); +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/A.cs b/gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/A.cs new file mode 100644 index 000000000..5bcf24d93 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/A.cs @@ -0,0 +1,7 @@ +namespace Grandparent.Models +{ + public class A + { + public Greeting Greet() => new Greeting(); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/B.cs b/gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/B.cs new file mode 100644 index 000000000..d5e37656c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/B.cs @@ -0,0 +1,4 @@ +namespace Grandparent.Models +{ + public class B : A {} +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/C.cs b/gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/C.cs new file mode 100644 index 000000000..20e6d2e4c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/C.cs @@ -0,0 +1,4 @@ +namespace Grandparent.Models +{ + public class C : B {} +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/Greeting.cs b/gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/Greeting.cs new file mode 100644 index 000000000..30a252561 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/Greeting.cs @@ -0,0 +1,7 @@ +namespace Grandparent.Models +{ + public class Greeting + { + public void Save() {} + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Services/App.cs b/gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Services/App.cs new file mode 100644 index 000000000..1880f0484 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Services/App.cs @@ -0,0 +1,13 @@ +using Grandparent.Models; + +namespace Grandparent.Services +{ + public class App + { + public void Process() + { + var c = new C(); + c.Greet().Save(); + } + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Models/Repo.cs b/gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Models/Repo.cs new file mode 100644 index 000000000..e395663a4 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Models/Repo.cs @@ -0,0 +1,7 @@ +namespace NullCheck.Models +{ + public class Repo + { + public void Save() {} + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Models/User.cs b/gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Models/User.cs new file mode 100644 index 000000000..1dbc2d66b --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Models/User.cs @@ -0,0 +1,7 @@ +namespace NullCheck.Models +{ + public class User + { + public void Save() {} + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Services/App.cs b/gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Services/App.cs new file mode 100644 index 000000000..84b2fe0f1 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Services/App.cs @@ -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(); + } + } + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/go-inc-dec-write-access/main.go b/gitnexus/test/fixtures/lang-resolution/go-inc-dec-write-access/main.go new file mode 100644 index 000000000..51d2a4d2c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-inc-dec-write-access/main.go @@ -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-- +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/A.java b/gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/A.java new file mode 100644 index 000000000..f2a2c332d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/A.java @@ -0,0 +1,5 @@ +package models; + +public class A { + public Greeting greet() { return new Greeting(); } +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/B.java b/gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/B.java new file mode 100644 index 000000000..54149019e --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/B.java @@ -0,0 +1,3 @@ +package models; + +public class B extends A {} diff --git a/gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/C.java b/gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/C.java new file mode 100644 index 000000000..3514bf12c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/C.java @@ -0,0 +1,3 @@ +package models; + +public class C extends B {} diff --git a/gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/Greeting.java b/gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/Greeting.java new file mode 100644 index 000000000..fda3b67e5 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/Greeting.java @@ -0,0 +1,5 @@ +package models; + +public class Greeting { + public void save() {} +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/services/App.java b/gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/services/App.java new file mode 100644 index 000000000..f7ec17158 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/services/App.java @@ -0,0 +1,10 @@ +package services; + +import models.C; + +public class App { + public void process() { + C c = new C(); + c.greet().save(); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/js-fixpoint-for-loop/app.js b/gitnexus/test/fixtures/lang-resolution/js-fixpoint-for-loop/app.js new file mode 100644 index 000000000..6d25720eb --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/js-fixpoint-for-loop/app.js @@ -0,0 +1,8 @@ +const { getUsers } = require('./models'); + +function process() { + const users = getUsers(); + for (const u of users) { + u.save(); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/js-fixpoint-for-loop/models.js b/gitnexus/test/fixtures/lang-resolution/js-fixpoint-for-loop/models.js new file mode 100644 index 000000000..28cbfb5e8 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/js-fixpoint-for-loop/models.js @@ -0,0 +1,13 @@ +/** + * @class + */ +class User { + save() {} +} + +/** + * @returns {User[]} + */ +function getUsers() { return []; } + +module.exports = { User, getUsers }; diff --git a/gitnexus/test/fixtures/lang-resolution/js-object-destructuring/app.js b/gitnexus/test/fixtures/lang-resolution/js-object-destructuring/app.js new file mode 100644 index 000000000..0ce2b9da2 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/js-object-destructuring/app.js @@ -0,0 +1,7 @@ +const { getUser } = require('./service'); + +function processDestructured() { + const user = getUser(); + const { address } = user; + address.save(); +} diff --git a/gitnexus/test/fixtures/lang-resolution/js-object-destructuring/models.js b/gitnexus/test/fixtures/lang-resolution/js-object-destructuring/models.js new file mode 100644 index 000000000..31cb47c9a --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/js-object-destructuring/models.js @@ -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 }; diff --git a/gitnexus/test/fixtures/lang-resolution/js-object-destructuring/service.js b/gitnexus/test/fixtures/lang-resolution/js-object-destructuring/service.js new file mode 100644 index 000000000..e8fc4565b --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/js-object-destructuring/service.js @@ -0,0 +1,8 @@ +const { User } = require('./models'); + +/** + * @returns {User} + */ +function getUser() { return new User(); } + +module.exports = { getUser }; diff --git a/gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/A.kt b/gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/A.kt new file mode 100644 index 000000000..d83147da8 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/A.kt @@ -0,0 +1,5 @@ +package models + +open class A { + fun greet(): Greeting = Greeting() +} diff --git a/gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/B.kt b/gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/B.kt new file mode 100644 index 000000000..8c4f1811a --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/B.kt @@ -0,0 +1,3 @@ +package models + +open class B : A() diff --git a/gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/C.kt b/gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/C.kt new file mode 100644 index 000000000..1f2a32536 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/C.kt @@ -0,0 +1,3 @@ +package models + +class C : B() diff --git a/gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/Greeting.kt b/gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/Greeting.kt new file mode 100644 index 000000000..f458412e8 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/Greeting.kt @@ -0,0 +1,5 @@ +package models + +class Greeting { + fun save() {} +} diff --git a/gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/services/App.kt b/gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/services/App.kt new file mode 100644 index 000000000..4c9f14564 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/services/App.kt @@ -0,0 +1,8 @@ +package services + +import models.C + +fun process() { + val c = C() + c.greet().save() +} diff --git a/gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/models/Repo.kt b/gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/models/Repo.kt new file mode 100644 index 000000000..8e31ab2a0 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/models/Repo.kt @@ -0,0 +1,5 @@ +package models + +class Repo { + fun save() {} +} diff --git a/gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/models/User.kt b/gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/models/User.kt new file mode 100644 index 000000000..494376e42 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/models/User.kt @@ -0,0 +1,5 @@ +package models + +class User { + fun save() {} +} diff --git a/gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/services/App.kt b/gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/services/App.kt new file mode 100644 index 000000000..4578c07f5 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/services/App.kt @@ -0,0 +1,9 @@ +package services + +import models.User + +fun processNullable(x: User?) { + if (x != null) { + x.save() + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/A.php b/gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/A.php new file mode 100644 index 000000000..5e270007d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/A.php @@ -0,0 +1,8 @@ +greet()->save(); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/composer.json b/gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/composer.json new file mode 100644 index 000000000..386b0bd2d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/composer.json @@ -0,0 +1,7 @@ +{ + "autoload": { + "psr-4": { + "App\\": "app/" + } + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/app.py b/gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/app.py new file mode 100644 index 000000000..7a2fd07d8 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/app.py @@ -0,0 +1,5 @@ +from models.c import C + +def process(): + c = C() + c.greet().save() diff --git a/gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/__init__.py b/gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/a.py b/gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/a.py new file mode 100644 index 000000000..49d6098b9 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/a.py @@ -0,0 +1,5 @@ +from .greeting import Greeting + +class A: + def greet(self) -> Greeting: + return Greeting() diff --git a/gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/b.py b/gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/b.py new file mode 100644 index 000000000..26c582d28 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/b.py @@ -0,0 +1,4 @@ +from .a import A + +class B(A): + pass diff --git a/gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/c.py b/gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/c.py new file mode 100644 index 000000000..296322aec --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/c.py @@ -0,0 +1,4 @@ +from .b import B + +class C(B): + pass diff --git a/gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/greeting.py b/gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/greeting.py new file mode 100644 index 000000000..1774cb730 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/greeting.py @@ -0,0 +1,3 @@ +class Greeting: + def save(self) -> None: + pass diff --git a/gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/app.rb b/gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/app.rb new file mode 100644 index 000000000..50b2e9303 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/app.rb @@ -0,0 +1,6 @@ +require_relative 'models/c' + +def process + c = C.new + c.greet.save +end diff --git a/gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/a.rb b/gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/a.rb new file mode 100644 index 000000000..e604abff9 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/a.rb @@ -0,0 +1,7 @@ +require_relative 'greeting' + +class A + def greet + Greeting.new + end +end diff --git a/gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/b.rb b/gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/b.rb new file mode 100644 index 000000000..197088d2c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/b.rb @@ -0,0 +1,4 @@ +require_relative 'a' + +class B < A +end diff --git a/gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/c.rb b/gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/c.rb new file mode 100644 index 000000000..e463dcd63 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/c.rb @@ -0,0 +1,4 @@ +require_relative 'b' + +class C < B +end diff --git a/gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/greeting.rb b/gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/greeting.rb new file mode 100644 index 000000000..d4886c078 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/greeting.rb @@ -0,0 +1,5 @@ +class Greeting + def save + true + end +end diff --git a/gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/main.rs b/gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/main.rs new file mode 100644 index 000000000..92faf8afa --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/main.rs @@ -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() {} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/point.rs b/gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/point.rs new file mode 100644 index 000000000..690f27b46 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/point.rs @@ -0,0 +1,6 @@ +use crate::vec2::Vec2; + +pub struct Point { + pub x: Vec2, + pub y: Vec2, +} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/vec2.rs b/gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/vec2.rs new file mode 100644 index 000000000..be9ebe879 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/vec2.rs @@ -0,0 +1,7 @@ +pub struct Vec2 { + pub value: f32, +} + +impl Vec2 { + pub fn save(&self) {} +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-fixpoint-for-loop/src/app.ts b/gitnexus/test/fixtures/lang-resolution/ts-fixpoint-for-loop/src/app.ts new file mode 100644 index 000000000..b2c133b91 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-fixpoint-for-loop/src/app.ts @@ -0,0 +1,8 @@ +import { getUsers } from './models'; + +function process() { + const users = getUsers(); + for (const u of users) { + u.save(); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-fixpoint-for-loop/src/models.ts b/gitnexus/test/fixtures/lang-resolution/ts-fixpoint-for-loop/src/models.ts new file mode 100644 index 000000000..3ab5ca5e6 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-fixpoint-for-loop/src/models.ts @@ -0,0 +1,6 @@ +export class User { + save(): void {} +} +export function getUsers(): User[] { + return []; +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/app.ts b/gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/app.ts new file mode 100644 index 000000000..bb17f9e56 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/app.ts @@ -0,0 +1,6 @@ +import { C } from './derived'; + +function process() { + const c = new C(); + c.greet().save(); +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/base.ts b/gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/base.ts new file mode 100644 index 000000000..911a64970 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/base.ts @@ -0,0 +1,5 @@ +import { Greeting } from './greeting'; + +export class A { + greet(): Greeting { return new Greeting(); } +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/derived.ts b/gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/derived.ts new file mode 100644 index 000000000..16530f8cc --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/derived.ts @@ -0,0 +1,2 @@ +import { B } from './middle'; +export class C extends B {} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/greeting.ts b/gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/greeting.ts new file mode 100644 index 000000000..be956282a --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/greeting.ts @@ -0,0 +1,3 @@ +export class Greeting { + save(): void {} +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/middle.ts b/gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/middle.ts new file mode 100644 index 000000000..cc1535135 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/middle.ts @@ -0,0 +1,2 @@ +import { A } from './base'; +export class B extends A {} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-null-check-narrowing/src/app.ts b/gitnexus/test/fixtures/lang-resolution/ts-null-check-narrowing/src/app.ts new file mode 100644 index 000000000..b73cb15dc --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-null-check-narrowing/src/app.ts @@ -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(); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-null-check-narrowing/src/models.ts b/gitnexus/test/fixtures/lang-resolution/ts-null-check-narrowing/src/models.ts new file mode 100644 index 000000000..0d7a294b0 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-null-check-narrowing/src/models.ts @@ -0,0 +1,7 @@ +export class User { + save(): void {} +} + +export class Repo { + save(): void {} +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/app.ts b/gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/app.ts new file mode 100644 index 000000000..ed84e7763 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/app.ts @@ -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(); +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/models.ts b/gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/models.ts new file mode 100644 index 000000000..88c11bcd0 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/models.ts @@ -0,0 +1,9 @@ +export class Address { + city: string = ''; + save(): boolean { return true; } +} + +export class User { + name: string = ''; + address: Address = new Address(); +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/service.ts b/gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/service.ts new file mode 100644 index 000000000..7ca8d0f87 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/service.ts @@ -0,0 +1,2 @@ +import { User } from './models'; +export function getUser(): User { return new User(); } diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index 8b8e1835d..d40f1f15e 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -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(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/csharp.test.ts b/gitnexus/test/integration/resolvers/csharp.test.ts index 9aaa6816d..5a5f6caa6 100644 --- a/gitnexus/test/integration/resolvers/csharp.test.ts +++ b/gitnexus/test/integration/resolvers/csharp.test.ts @@ -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(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/go.test.ts b/gitnexus/test/integration/resolvers/go.test.ts index 5aeb9ac54..47349084a 100644 --- a/gitnexus/test/integration/resolvers/go.test.ts +++ b/gitnexus/test/integration/resolvers/go.test.ts @@ -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(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/java.test.ts b/gitnexus/test/integration/resolvers/java.test.ts index f26ab2d41..78e3d309a 100644 --- a/gitnexus/test/integration/resolvers/java.test.ts +++ b/gitnexus/test/integration/resolvers/java.test.ts @@ -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(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/javascript.test.ts b/gitnexus/test/integration/resolvers/javascript.test.ts index 58bbb54ec..7ba0328c6 100644 --- a/gitnexus/test/integration/resolvers/javascript.test.ts +++ b/gitnexus/test/integration/resolvers/javascript.test.ts @@ -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(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/kotlin.test.ts b/gitnexus/test/integration/resolvers/kotlin.test.ts index 6ea6cd2e5..b8442d39d 100644 --- a/gitnexus/test/integration/resolvers/kotlin.test.ts +++ b/gitnexus/test/integration/resolvers/kotlin.test.ts @@ -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(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/php.test.ts b/gitnexus/test/integration/resolvers/php.test.ts index 03c1f670b..8b7d4b24c 100644 --- a/gitnexus/test/integration/resolvers/php.test.ts +++ b/gitnexus/test/integration/resolvers/php.test.ts @@ -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(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/python.test.ts b/gitnexus/test/integration/resolvers/python.test.ts index ac9974192..4cfe0ccc1 100644 --- a/gitnexus/test/integration/resolvers/python.test.ts +++ b/gitnexus/test/integration/resolvers/python.test.ts @@ -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(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/ruby.test.ts b/gitnexus/test/integration/resolvers/ruby.test.ts index ef07fc114..d77a6d139 100644 --- a/gitnexus/test/integration/resolvers/ruby.test.ts +++ b/gitnexus/test/integration/resolvers/ruby.test.ts @@ -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(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/rust.test.ts b/gitnexus/test/integration/resolvers/rust.test.ts index 107adf2c5..26bbf9cb0 100644 --- a/gitnexus/test/integration/resolvers/rust.test.ts +++ b/gitnexus/test/integration/resolvers/rust.test.ts @@ -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); + }); +}); diff --git a/gitnexus/test/integration/resolvers/typescript.test.ts b/gitnexus/test/integration/resolvers/typescript.test.ts index 0d38f5bc2..ce4f30240 100644 --- a/gitnexus/test/integration/resolvers/typescript.test.ts +++ b/gitnexus/test/integration/resolvers/typescript.test.ts @@ -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(); + }); +}); diff --git a/gitnexus/test/unit/type-env.test.ts b/gitnexus/test/unit/type-env.test.ts index ab2a351d4..ce362c7a8 100644 --- a/gitnexus/test/unit/type-env.test.ts +++ b/gitnexus/test/unit/type-env.test.ts @@ -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) {