mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-12 08:41:34 +00:00
feat: return type inference, doc-comment parsing, and per-language type extractors (#284)
* feat: Phase 3 — return type inference, generic args extraction, Ruby YARD type extractor
Three architectural improvements to the type resolution system:
1. Return type inference — wire extractMethodSignature returnType through
SymbolDefinition into call-processor. When var = callee() and callee
has a known return type, bind var to that type. Handles Promise<T>
unwrapping, nullable stripping, pointer/reference removal.
2. Generic type argument extraction — new extractGenericTypeArgs() utility
that extracts type parameters from List<User> → ['User']. Handles
TS/Java/Kotlin/C#/Rust generic syntax. Building block for for-loop
variable typing.
3. Ruby dedicated type extractor — replaces the stub with YARD annotation
parsing (@param name [Type]), handling qualified types, nullable types,
and singleton methods. Ruby now has real type resolution.
Unit tests: 127 → 192+ (type-env) + 65 (symbol-table, call-processor) + 18 (generics)
Integration tests: 8+ new test cases with fixtures across TS/Python/Go/Java/Ruby
* fix: Phase 3 gaps — WRAPPER_GENERICS correctness, Ruby :: qualifier, namespaced constructors
- Remove collection types (List, Array, Vec, Set) from WRAPPER_GENERICS to prevent
false CALLS edges (e.g. List<User> no longer unwraps to User)
- Add :: qualifier handling in extractReturnTypeName for Ruby/C++/Rust namespaced types
- Add Ruby `constant` and `scope_resolution` node types to shared extractors
- Extract shared extractRubyConstructorAssignment helper (dedup type-env.ts + ruby.ts)
- Add integration tests for return type inference: Python, TypeScript, Go, Java, Ruby
- Add Ruby namespaced constructor fixture (Models::UserService.new)
- Add unit tests for collection reclassification and :: qualifiers
* feat: Phase 4 — CONSTRUCTOR_BINDING_SCANNERS for all languages + return type inference tests
Add CONSTRUCTOR_BINDING_SCANNERS for 6 missing languages, completing
return type inference coverage across all 11 supported languages:
- TypeScript/JS: variable_declarator with call_expression, unwraps await
- Go: short_var_declaration single-assignment (skips multi-return, new/make)
- Java: local_variable_declaration with `var` type + method_invocation
- C#: variable_declaration with implicit_type (var) + invocation_expression
- Rust: let_declaration without type annotation, handles mut_pattern
- PHP: assignment_expression with function_call_expression
Also adds property_identifier to extractSimpleTypeName for qualified
member calls (repo.getUser → getUser), fixing namespaced constructor
inference that was previously a known limitation.
Integration tests added for all 11 languages with correct label
assertions (Function vs Method per language's tree-sitter queries).
* refactor: merge CONSTRUCTOR_BINDING_SCANNERS into per-language LanguageTypeConfig
Eliminates the parallel dispatch map in type-env.ts by moving all 11
constructor binding scanners into their respective type-extractors/*.ts
files as `scanConstructorBinding` on LanguageTypeConfig.
- Add ConstructorBindingScanner type to types.ts
- Add shared helpers: hasTypeAnnotation, unwrapAwait, extractCalleeName
- Move scanners to typescript.ts, jvm.ts, python.ts, php.ts, go.ts,
rust.ts, swift.ts, c-cpp.ts, csharp.ts, ruby.ts
- Fix `any` types in C# scanner → SyntaxNode | null
- Delete ~300 lines from type-env.ts (CONSTRUCTOR_BINDING_SCANNERS map)
- Update buildTypeEnv to use config.scanConstructorBinding
All 143 type-env unit tests and all 10 language integration suites pass.
* fix: remove unused import, fix any type in Java scanner, update stale comment
- Remove unused extractCalleeName import from jvm.ts
- Fix (c: any) → (c: SyntaxNode) in Java scanner
- Update stale CONSTRUCTOR_BINDING_SCANNERS reference in ruby.ts comment
* fix: C# and PHP return type inference — scanner fixes, method signature extraction, and cross-file resolution
Addresses code review findings on PR #284:
C# scanner (csharp.ts):
- Fix type node lookup: iterate children instead of childForFieldName('type')
which returns undefined in tree-sitter-c-sharp
- Fix initializer lookup: handle direct invocation_expression children
(no equals_value_clause wrapper in tree-sitter-c-sharp)
C# return type extraction (utils.ts):
- Add 'returns' field check to extractMethodSignature — tree-sitter-c-sharp
uses 'returns', not 'type', for method return types
C# cross-file resolution (call-processor.ts + fixture):
- Add constructor binding verification to sequential processCalls path
(was only in the worker processCallsFromExtracted path)
- Add ReturnType.csproj to csharp-return-type fixture
- Update fixture namespaces to use ReturnType.Models/ReturnType.Services
prefix (matches real C# project conventions)
PHP scanner (php.ts):
- Extend scanConstructorBinding to handle member_call_expression
($this->getUser() patterns), not just function_call_expression
Shared (shared.ts):
- Add member_access_expression to extractSimpleTypeName qualified-names
block (C# method calls like svc.GetUser())
Tests:
- Add Repo.cs/Repo.php disambiguation fixtures (two Save methods)
- Strengthen C# and PHP return type tests with hard disambiguation assertions
- Add C# scanner unit tests and return type extraction test
* feat: per-language ReturnTypeExtractor + doc-comment @param parsing for PHP, JS, Ruby
Add ReturnTypeExtractor to LanguageTypeConfig interface with implementations
for Ruby (YARD @return), PHP (PHPDoc @return), and JS/TS (JSDoc @returns).
The fallback is wired in both parsing-processor and parse-worker paths,
activating only when extractMethodSignature finds no AST-based return type.
Also add doc-comment @param type extraction for PHP and JS/TS, following
Ruby's existing collectYardParams pattern. This enables parameter.method()
resolution in loosely-typed codebases using PHPDoc @param or JSDoc @param.
Additional fixes from PR #284 code review:
- Go: add selector_expression + field_identifier to extractSimpleTypeName
(enables package-qualified factory calls like models.NewUser())
- Ruby: broaden scanConstructorBinding to capture plain call assignments
(user = get_user()) in addition to Class.new patterns
- Ruby: harden return-type fixture with disambiguation (two save methods)
Test coverage: +14 new integration tests across Go, Ruby, PHP, JS/TS
* fix: JSDoc async return type, PHP attribute walkers, and $this receiver disambiguation
Three fixes from fourth-pass code review on PR #284:
1. JSDoc `@returns {Promise<User>}` no longer stripped to `Promise` — extractReturnType
now uses sanitizeReturnType (preserves generics) instead of normalizeJsDocType
(which stripped them before extractReturnTypeName could unwrap WRAPPER_GENERICS).
2. PHP 8+ `#[Attribute]` and JS `@decorator` nodes no longer break doc-comment walkers.
Both extractReturnType and collect*Params functions now skip attribute_list/decorator
nodes instead of breaking on them as named siblings.
3. PHP `$this->method()` now provides receiverClassName for disambiguation.
When two classes define the same method, the enclosing class narrows candidates
via ownerId matching in call-processor, preventing false no-binding results.
* fix: sanitizeReturnType dot corruption, JS test assertions, Ruby constant receiver
- Remove redundant dot-path stripping from sanitizeReturnType that corrupted
qualified names inside generics (e.g. Promise<models.User> → User>)
- Split JS async fixture into separate files and add negative assertions
to properly verify disambiguation (mirroring PHP test pattern)
- Accept 'constant' node type in Ruby scanConstructorBinding for factory
call assignments (SERVICE = build_service())
- Add 'constant' to SIMPLE_RECEIVER_TYPES so extractReceiverName handles
Ruby constant receivers (SERVICE.process)
* fix: nested generic arg splitting, JS/Ruby test false positives
- Replace naive comma split in extractReturnTypeName with bracket-balanced
extractFirstGenericArg so nested types like Future<Result<User, Error>>
unwrap correctly instead of producing malformed "Result<User"
- Add CompletableFuture to WRAPPER_GENERICS for Java async unwrapping
- Split js-jsdoc-return-type fixture models.js into user.js/repo.js and
add negative assertions to prove disambiguation (not just file match)
- Split ruby-constant-factory-call fixture into separate service files
and add negative assertions against AdminService resolution
* fix: review findings — receiverClassName parity, Rust wrappers, Go multi-return, Kotlin/Swift qualified calls
P1: Sequential path now includes receiverClassName narrowing for PHP
$this->method() disambiguation (was missing vs worker path).
P2: Added Rc/Arc/Weak/MutexGuard/Cow + 6 more Rust Deref types to
WRAPPER_GENERICS (Box excluded — Java Swing collision). Extended
Kotlin/Swift scanners to handle navigation_expression callees.
Added Go multi-return support (user, err := f()) with blank/_/err/ok
guard + AST-level first-return extraction in extractMethodSignature.
P3: Extracted shared verifyConstructorBindings() eliminating 60 lines
of duplication between sequential and worker paths. Added return-type
inference integration tests for C++, Rust, Swift with competing
methods and negative disambiguation assertions.
* fix: Swift navigation_suffix unwrapping, Rust lifetime skipping, Kotlin disambiguation tests
- Swift scanConstructorBinding: handle tree-sitter wrapping qualified
identifiers in navigation_suffix nodes
- Add extractFirstTypeArg to skip Rust lifetime parameters ('a, '_)
when unwrapping wrapper generics like Ref<'_, User>
- Kotlin tests: add Repo class fixture with competing save() methods
to prove disambiguation; assert no spurious edges on known gap
- Remove tree-sitter-kotlin from optionalDependencies (now regular dep)
* fix: C# null-conditional calls, Ruby YARD bracket-balanced split, PHPDoc alternate order, escapeValue hardening
- Add C# null-conditional call support (user?.Save()): tree-sitter query for
conditional_access_expression, member_binding_expression in MEMBER_ACCESS_NODE_TYPES,
receiver extraction via conditional_access_expression parent walk
- Fix Ruby YARD type parsing for nested generics (Hash<Symbol, User>): replace
naive split(',') with bracket-balanced splitter respecting <> depth
- Add alternate YARD format (@param [Type] name) alongside standard (@param name [Type])
- Add alternate PHPDoc format (@param $name Type) alongside standard (@param Type $name)
- Harden escapeValue in kuzu-adapter.ts: escape \n and \r to prevent Cypher injection
- Integration tests: C# null-conditional fixture (5 tests), Ruby YARD generics fixture (6 tests)
- Unit tests: PHPDoc alternate order (2 tests), C# null-conditional call-form (updated)
* test: add Python static/classmethod integration tests (issue #289)
Verifies that classes using only @staticmethod/@classmethod have HAS_METHOD
edges connecting them to their child methods. This was the root cause of
issue #289 where context() and impact() returned empty for such classes.
Tests cover: HAS_METHOD edge emission, unique static method resolution
(create_user, delete_user), and ambiguous same-named method handling
(find_user on both UserService and AdminService — safely refused).
* fix: lbug batch escapeValue newline hardening, Rust ::default() scanner exclusion
- Apply \n/\r escaping to batch upsert escapeValue in lbug-adapter.ts:429
(missed instance of the CREATE-path fix from ec4dca4)
- Exclude Rust ::default() from scanConstructorBinding to match
extractInitializer behavior — avoids wasted cross-file lookups on
the broadly-implemented Default trait
- Unit tests: 2 new scanner exclusion tests (::default and ::new)
- Integration tests: 6 new Rust ::default() constructor resolution tests
with disambiguation fixture (User::default vs Repo::default)
* fix: C#/Rust async await unwrap, PHP backslash namespace, fallback escaping
- C# scanConstructorBinding: unwrap await_expression to find invocation_expression
(var user = await svc.GetUserAsync() now produces constructor binding)
- Rust scanConstructorBinding: unwrap .await postfix via shared unwrapAwait helper
(let user = get_user().await now produces constructor binding)
- extractReturnTypeName: handle PHP backslash namespace separator (\App\Models\User → User)
- fallbackRelationshipInserts: match batch escapeValue hardening with \n/\r escaping
Tests: 2 unit (type-env), 3 unit (call-processor), 7 integration (csharp+rust), 7 fixtures
* fix: C#/Rust async-binding test false positives — add competing types and negative assertions
C# fixture: add Order.cs with Order.Save(), change OrderService to return
Task<Order> via GetOrderAsync, add negative assertion proving user.Save()
does not resolve to Order#Save.
Rust fixture: split models.rs into user.rs/repo.rs, make process_user and
process_repo async fn, add bidirectional negative assertions proving no
cross-contamination between User#save and Repo#save.
* fix: C# async-binding broken assertion, bare wrapper type leak, JSDoc optional params
- Split Program.cs Main into ProcessUser/ProcessOrder so negative
assertions use strict toBeUndefined() (matching Rust pattern)
- Guard bare wrapper types (Task, Promise, Option…) in
extractReturnTypeName — return undefined instead of the wrapper name
- Update JSDOC_PARAM_RE to capture @param {Type} [optionalName] syntax
* fix: update symbol and relationship counts in documentation
This commit is contained in:
parent
fe55fd2451
commit
23c4ddcdb0
128 changed files with 4727 additions and 518 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -62,4 +62,9 @@ docs/plans/
|
|||
|
||||
gitnexus/test/fixtures/mini-repo/*.md
|
||||
gitnexus/test/fixtures/mini-repo/.claude
|
||||
gitnexus/test/fixtures/mini-repo/.gitignore
|
||||
gitnexus/test/fixtures/mini-repo/.gitignore
|
||||
|
||||
# Ignore csharp generated obj and bin folders
|
||||
gitnexus/test/fixtures/lang-resolution/**/obj
|
||||
gitnexus/test/fixtures/lang-resolution/**/bin
|
||||
GitNexus.sln
|
||||
22
AGENTS.md
22
AGENTS.md
|
|
@ -1,7 +1,7 @@
|
|||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **GitNexus** (1927 symbols, 4372 relationships, 143 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
This project is indexed by GitNexus as **GitNexus** (1999 symbols, 4681 relationships, 149 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.
|
||||
|
||||
|
|
@ -97,25 +97,5 @@ To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.
|
|||
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
|
||||
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
|
||||
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
|
||||
| Work in the Ingestion area (184 symbols) | `.claude/skills/generated/ingestion/SKILL.md` |
|
||||
| Work in the Cli area (71 symbols) | `.claude/skills/generated/cli/SKILL.md` |
|
||||
| Work in the Workers area (58 symbols) | `.claude/skills/generated/workers/SKILL.md` |
|
||||
| Work in the Wiki area (50 symbols) | `.claude/skills/generated/wiki/SKILL.md` |
|
||||
| Work in the Kuzu area (45 symbols) | `.claude/skills/generated/kuzu/SKILL.md` |
|
||||
| Work in the Components area (40 symbols) | `.claude/skills/generated/components/SKILL.md` |
|
||||
| Work in the Embeddings area (32 symbols) | `.claude/skills/generated/embeddings/SKILL.md` |
|
||||
| Work in the Local area (31 symbols) | `.claude/skills/generated/local/SKILL.md` |
|
||||
| Work in the Mcp area (31 symbols) | `.claude/skills/generated/mcp/SKILL.md` |
|
||||
| Work in the Services area (25 symbols) | `.claude/skills/generated/services/SKILL.md` |
|
||||
| Work in the Resolvers area (15 symbols) | `.claude/skills/generated/resolvers/SKILL.md` |
|
||||
| Work in the Eval area (15 symbols) | `.claude/skills/generated/eval/SKILL.md` |
|
||||
| Work in the Llm area (14 symbols) | `.claude/skills/generated/llm/SKILL.md` |
|
||||
| Work in the Hooks area (14 symbols) | `.claude/skills/generated/hooks/SKILL.md` |
|
||||
| Work in the Bridge area (13 symbols) | `.claude/skills/generated/bridge/SKILL.md` |
|
||||
| Work in the Environments area (11 symbols) | `.claude/skills/generated/environments/SKILL.md` |
|
||||
| Work in the Analysis area (10 symbols) | `.claude/skills/generated/analysis/SKILL.md` |
|
||||
| Work in the Server area (9 symbols) | `.claude/skills/generated/server/SKILL.md` |
|
||||
| Work in the Type-extractors area (8 symbols) | `.claude/skills/generated/type-extractors/SKILL.md` |
|
||||
| Work in the Unit area (7 symbols) | `.claude/skills/generated/unit/SKILL.md` |
|
||||
|
||||
<!-- gitnexus:end -->
|
||||
|
|
|
|||
22
CLAUDE.md
22
CLAUDE.md
|
|
@ -1,7 +1,7 @@
|
|||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **GitNexus** (1927 symbols, 4372 relationships, 143 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
This project is indexed by GitNexus as **GitNexus** (1999 symbols, 4681 relationships, 149 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.
|
||||
|
||||
|
|
@ -97,25 +97,5 @@ To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.
|
|||
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
|
||||
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
|
||||
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
|
||||
| Work in the Ingestion area (184 symbols) | `.claude/skills/generated/ingestion/SKILL.md` |
|
||||
| Work in the Cli area (71 symbols) | `.claude/skills/generated/cli/SKILL.md` |
|
||||
| Work in the Workers area (58 symbols) | `.claude/skills/generated/workers/SKILL.md` |
|
||||
| Work in the Wiki area (50 symbols) | `.claude/skills/generated/wiki/SKILL.md` |
|
||||
| Work in the Kuzu area (45 symbols) | `.claude/skills/generated/kuzu/SKILL.md` |
|
||||
| Work in the Components area (40 symbols) | `.claude/skills/generated/components/SKILL.md` |
|
||||
| Work in the Embeddings area (32 symbols) | `.claude/skills/generated/embeddings/SKILL.md` |
|
||||
| Work in the Local area (31 symbols) | `.claude/skills/generated/local/SKILL.md` |
|
||||
| Work in the Mcp area (31 symbols) | `.claude/skills/generated/mcp/SKILL.md` |
|
||||
| Work in the Services area (25 symbols) | `.claude/skills/generated/services/SKILL.md` |
|
||||
| Work in the Resolvers area (15 symbols) | `.claude/skills/generated/resolvers/SKILL.md` |
|
||||
| Work in the Eval area (15 symbols) | `.claude/skills/generated/eval/SKILL.md` |
|
||||
| Work in the Llm area (14 symbols) | `.claude/skills/generated/llm/SKILL.md` |
|
||||
| Work in the Hooks area (14 symbols) | `.claude/skills/generated/hooks/SKILL.md` |
|
||||
| Work in the Bridge area (13 symbols) | `.claude/skills/generated/bridge/SKILL.md` |
|
||||
| Work in the Environments area (11 symbols) | `.claude/skills/generated/environments/SKILL.md` |
|
||||
| Work in the Analysis area (10 symbols) | `.claude/skills/generated/analysis/SKILL.md` |
|
||||
| Work in the Server area (9 symbols) | `.claude/skills/generated/server/SKILL.md` |
|
||||
| Work in the Type-extractors area (8 symbols) | `.claude/skills/generated/type-extractors/SKILL.md` |
|
||||
| Work in the Unit area (7 symbols) | `.claude/skills/generated/unit/SKILL.md` |
|
||||
|
||||
<!-- gitnexus:end -->
|
||||
|
|
|
|||
|
|
@ -1,259 +0,0 @@
|
|||
---
|
||||
title: "feat: Close type resolution gaps across all 12 languages"
|
||||
type: feat
|
||||
status: active
|
||||
date: 2026-03-14
|
||||
---
|
||||
|
||||
# Close Type Resolution Gaps Across All 12 Languages
|
||||
|
||||
## Overview
|
||||
|
||||
The type resolution system (TypeEnv + call-processor) has been hardened through 6 rounds of review on `feat/type-resolution-constructor-inference`. This plan closes the remaining gaps identified by the comprehensive 4-agent gap analysis covering all 12 supported languages.
|
||||
|
||||
Every task MUST include per-language integration tests following the established pattern:
|
||||
- Fixture directory: `test/fixtures/lang-resolution/<lang>-<pattern>/`
|
||||
- Integration test: `test/integration/resolvers/<lang>.test.ts`
|
||||
- Unit tests: `test/unit/type-env.test.ts` where applicable
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The current TypeEnv extracts types from explicit annotations (Tier 0) and constructor calls (Tier 1). Many common patterns across all languages produce no type binding, causing receiver-type resolution to fail silently. The gap analysis identified 14 categories of missing patterns affecting call resolution accuracy.
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Quick Wins (Independent — can run in parallel)
|
||||
|
||||
Each task is self-contained. No dependencies between them.
|
||||
|
||||
#### Task 1.1: Python walrus operator `:=`
|
||||
- **File**: `src/core/ingestion/type-extractors/python.ts`
|
||||
- **Change**: Add `named_expression` to `DECLARATION_NODE_TYPES`
|
||||
- **Why**: `if (user := get_user()):` is common Python 3.8+ pattern; `user` gets no type binding
|
||||
- **Pattern**: `named_expression` has `name` (identifier) and `value` (call) children
|
||||
- **Tests**:
|
||||
- Unit: `type-env.test.ts` — walrus with call, walrus with annotated type
|
||||
- Integration fixture: `test/fixtures/lang-resolution/python-walrus-operator/`
|
||||
- Integration test: `python.test.ts` — `user := User(); user.save()` resolves
|
||||
|
||||
#### Task 1.2: PHP typed class properties
|
||||
- **File**: `src/core/ingestion/type-extractors/php.ts`
|
||||
- **Change**: Add `property_declaration` to `DECLARATION_NODE_TYPES`, implement `extractDeclaration` for it
|
||||
- **Why**: `private User $repo;` is the primary PHP 7.4+ property declaration pattern
|
||||
- **Pattern**: `property_declaration` has `type` (named_type) and child `property_element` with `name` (variable_name)
|
||||
- **Tests**:
|
||||
- Unit: `type-env.test.ts` — typed property extraction
|
||||
- Integration fixture: `test/fixtures/lang-resolution/php-typed-properties/`
|
||||
- Integration test: `php.test.ts` — `private UserRepo $repo; $repo->save()` resolves
|
||||
|
||||
#### Task 1.3: Nullable receiver unwrapping
|
||||
- **File**: `src/core/ingestion/utils.ts` — `extractReceiverName` function (~line 800)
|
||||
- **Change**: Before returning `undefined` for non-simple receivers, check if the receiver node wraps a simple identifier with an optional chain operator
|
||||
- **Patterns to handle**:
|
||||
- TS/JS: `optional_chain_expression` wrapping `member_expression`
|
||||
- Kotlin: `safe_navigation_expression`
|
||||
- C#: `conditional_access_expression`
|
||||
- Swift: `optional_chaining_expression`
|
||||
- **Approach**: Add the wrapped expression types to `SIMPLE_RECEIVER_TYPES` or unwrap one level before checking
|
||||
- **Tests**:
|
||||
- Integration fixtures per language: `ts-nullable-receiver/`, `kotlin-nullable-receiver/`, etc.
|
||||
- Integration tests: `user?.save()` resolves same as `user.save()`
|
||||
|
||||
#### Task 1.4: Go `make()` builtin
|
||||
- **File**: `src/core/ingestion/type-extractors/go.ts`
|
||||
- **Change**: In `extractGoShortVarDeclaration`, add a `call_expression` branch for `make` (similar to existing `new` branch)
|
||||
- **Pattern**: `make([]User, 0)` — first arg is `slice_type`/`map_type` with element type
|
||||
- **Approach**: Extract element type from the first argument's type node
|
||||
- **Tests**:
|
||||
- Unit: `type-env.test.ts` — `make([]User, 0)`, `make(map[string]User)`
|
||||
- Integration fixture: `test/fixtures/lang-resolution/go-make-builtin/`
|
||||
- Integration test: `go.test.ts`
|
||||
|
||||
#### Task 1.5: Go type assertions
|
||||
- **File**: `src/core/ingestion/type-extractors/go.ts`
|
||||
- **Change**: Handle `type_assertion_expression` in short var declarations
|
||||
- **Pattern**: `user, ok := iface.(User)` — `type_assertion_expression` has a `type` field
|
||||
- **Tests**:
|
||||
- Unit: `type-env.test.ts` — type assertion single and comma-ok forms
|
||||
- Integration fixture: `test/fixtures/lang-resolution/go-type-assertion/`
|
||||
- Integration test: `go.test.ts`
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Medium Effort (Some dependencies noted)
|
||||
|
||||
#### Task 2.1: C++ range-for loop variables
|
||||
- **File**: `src/core/ingestion/type-extractors/c-cpp.ts`
|
||||
- **Change**: Add `for_range_loop` to `DECLARATION_NODE_TYPES`, extract the type from the declaration part
|
||||
- **Pattern**: `for (auto& user : users)` — the `for_range_loop` has a `type` and `declarator` child
|
||||
- **Note**: When type is `auto`, would need collection element type inference (deferred to Phase 3). For explicit types `for (User& u : users)` this works now.
|
||||
- **Tests**:
|
||||
- Unit: explicit type in range-for
|
||||
- Integration fixture: `test/fixtures/lang-resolution/cpp-range-for/`
|
||||
- Integration test: `cpp.test.ts`
|
||||
|
||||
#### Task 2.2: Rust `if let` / `while let` bindings
|
||||
- **File**: `src/core/ingestion/type-extractors/rust.ts`
|
||||
- **Change**: Add `if_let_expression` and `while_let_expression` to `DECLARATION_NODE_TYPES` or handle in `extractDeclaration`
|
||||
- **Pattern**: `if let Some(user) = opt { user.save() }` — pattern binding inside conditional
|
||||
- **Approach**: Extract the pattern variable and the matched type from the source expression
|
||||
- **Note**: Full generic unwrapping (Some<User> → User) is Phase 3. Initial version extracts the binding variable with no type — still useful for scope tracking.
|
||||
- **Tests**:
|
||||
- Unit: if-let with annotated type, while-let
|
||||
- Integration fixture: `test/fixtures/lang-resolution/rust-if-let/`
|
||||
- Integration test: `rust.test.ts`
|
||||
|
||||
#### Task 2.3: Swift `guard let` / `if let` bindings
|
||||
- **File**: `src/core/ingestion/type-extractors/swift.ts`
|
||||
- **Change**: Add `guard_statement` and `if_statement` with `optional_binding_condition` to declaration handling
|
||||
- **Pattern**: `guard let user = fetchUser() else { return }` — `optional_binding_condition` has `pattern` and `value`
|
||||
- **Note**: Swift parser availability varies (Node 22 issue). Tests must use `describe.skipIf(!swiftAvailable)`.
|
||||
- **Tests**:
|
||||
- Integration fixture: `test/fixtures/lang-resolution/swift-guard-let/`
|
||||
- Integration test: `swift.test.ts` with skipIf guard
|
||||
|
||||
#### Task 2.4: C# pattern matching `is Type variable`
|
||||
- **File**: `src/core/ingestion/type-extractors/csharp.ts`
|
||||
- **Change**: Handle `is_pattern_expression` or `declaration_pattern` in the type extractor
|
||||
- **Pattern**: `if (obj is User user) { user.Save(); }` — `declaration_pattern` has `type` and `name`
|
||||
- **Tests**:
|
||||
- Unit: `type-env.test.ts` — is-pattern with type
|
||||
- Integration fixture: `test/fixtures/lang-resolution/csharp-pattern-matching/`
|
||||
- Integration test: `csharp.test.ts`
|
||||
|
||||
#### Task 2.5: Python class-level type annotations
|
||||
- **File**: `src/core/ingestion/type-extractors/python.ts`
|
||||
- **Change**: Ensure `assignment` declarationNodeTypes also captures class body assignments with type annotations
|
||||
- **Pattern**: `class User: name: str = "default"` — tree-sitter may use `expression_statement` > `assignment` inside class body
|
||||
- **Note**: Check if this already works via existing `assignment` handling — may just need scope key fix
|
||||
- **Tests**:
|
||||
- Unit: class-level annotation
|
||||
- Integration fixture: `test/fixtures/lang-resolution/python-class-annotations/`
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Architecture Changes (Sequential — requires design decisions)
|
||||
|
||||
#### Task 3.1: Return type inference
|
||||
- **Files**: `src/core/ingestion/type-env.ts`, `src/core/ingestion/call-processor.ts`
|
||||
- **Change**: When processing calls, look up the callee's return type from SymbolTable and bind the assignment target
|
||||
- **Approach**:
|
||||
1. `extractMethodSignature` already stores `returnType` in symbol metadata
|
||||
2. In `buildTypeEnv` or as a post-processing step, for assignments like `let x = foo()`, look up `foo` in SymbolTable
|
||||
3. If `foo.returnType` exists, add binding `x → returnType`
|
||||
4. Must strip nullable wrappers and generic containers to get base type
|
||||
- **Risk**: Circular dependencies if two functions return each other's types. Mitigate with depth limit.
|
||||
- **Tests**: Per-language integration tests for return-type-inferred receiver resolution
|
||||
|
||||
#### Task 3.2: Chained property access resolution
|
||||
- **Files**: `src/core/ingestion/utils.ts` (`extractReceiverName`), `src/core/ingestion/call-processor.ts`
|
||||
- **Change**: When receiver is `this.repo`, resolve `this` → class type, then look up `repo` property type on that class
|
||||
- **Approach**:
|
||||
1. Extend `extractReceiverName` to return structured data: `{ chain: ['this', 'repo'] }` for multi-level access
|
||||
2. In call-processor, resolve chain iteratively: lookup first element, find property type, continue
|
||||
3. Requires property-type tracking: store class property types in TypeEnv or SymbolTable
|
||||
- **Risk**: Performance impact from recursive lookups. Limit chain depth to 3.
|
||||
- **Tests**: Per-language `this.repo.save()` / `self.db.query()` patterns
|
||||
|
||||
#### Task 3.3: For-loop variable typing
|
||||
- **Files**: Per-language type extractors + `src/core/ingestion/type-env.ts`
|
||||
- **Change**: Extract loop variable type from explicit annotations or inferred from collection type
|
||||
- **Patterns per language**:
|
||||
- TS/JS: `for (const x of collection)` — `for_in_statement` with `left` and `right`
|
||||
- Java/C#: `for (Type x : collection)` — explicit type already works
|
||||
- Go: `for _, v := range slice` — `range_clause` with identifier
|
||||
- Python: `for x in collection` — `for_statement` with `left`
|
||||
- Rust: `for x in iter` — `for_expression` with `pattern`
|
||||
- **Depends on**: Task 3.1 (return type inference) for inferring collection element types
|
||||
- **Tests**: Per-language integration tests
|
||||
|
||||
#### Task 3.4: Generic type parameter extraction
|
||||
- **File**: `src/core/ingestion/type-extractors/shared.ts` — `extractSimpleTypeName`
|
||||
- **Change**: Add optional `extractGenericArgs` mode that returns type parameters alongside base type
|
||||
- **Current**: `List<User>` → `List` (base only)
|
||||
- **New**: `List<User>` → `{ base: 'List', args: ['User'] }` when requested
|
||||
- **Use case**: For-loop element type inference, collection method resolution
|
||||
- **Tests**: Unit tests for each language's generic syntax
|
||||
|
||||
#### Task 3.5: Block-level type narrowing
|
||||
- **File**: `src/core/ingestion/type-env.ts` — scope handling
|
||||
- **Change**: Add sub-function scope support for if/match/guard blocks
|
||||
- **Current**: Scope keys are `funcName@startIndex` or `''` (file)
|
||||
- **New**: Add block scope keys like `funcName@startIndex#if@lineN`
|
||||
- **Risk**: Scope lookup complexity increases. Need to walk up scope chain on miss.
|
||||
- **Tests**: Per-language pattern matching, instanceof, type guards
|
||||
|
||||
#### Task 3.6: Ruby dedicated type extractor
|
||||
- **File**: New `src/core/ingestion/type-extractors/ruby.ts`
|
||||
- **Change**: Replace the stub in `index.ts` with a real type extractor
|
||||
- **Features**:
|
||||
- YARD annotation parsing (`@param name [Type]`, `@return [Type]`)
|
||||
- Instance variable type inference from constructor assignments
|
||||
- Block parameter typing heuristics
|
||||
- **Tests**: Integration fixture: `test/fixtures/lang-resolution/ruby-typed-methods/`
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### Functional Requirements
|
||||
- [ ] Each Phase 1 task has unit tests AND per-language integration tests with fixtures
|
||||
- [ ] Each Phase 2 task has per-language integration tests with fixtures
|
||||
- [ ] Each Phase 3 task has cross-language integration tests
|
||||
- [ ] All existing 101 unit tests continue to pass
|
||||
- [ ] All existing integration tests continue to pass
|
||||
- [ ] No regressions in existing call resolution
|
||||
|
||||
### Quality Gates
|
||||
- [ ] `npx vitest run test/unit/type-env.test.ts` — all pass
|
||||
- [ ] `npx vitest run test/integration/resolvers/ --no-file-parallelism` — all pass
|
||||
- [ ] No new TypeScript compilation errors
|
||||
|
||||
### Testing Pattern (MANDATORY for every task)
|
||||
```
|
||||
1. Create fixture: test/fixtures/lang-resolution/<lang>-<pattern>/
|
||||
- Source file with the pattern (e.g., main.py, models.py)
|
||||
- Class/struct definitions with methods to resolve against
|
||||
2. Add describe block: test/integration/resolvers/<lang>.test.ts
|
||||
- beforeAll: runPipelineFromRepo(fixture_path)
|
||||
- Tests: verify CALLS edges resolve to correct file/method
|
||||
3. Add unit tests: test/unit/type-env.test.ts (where applicable)
|
||||
- Parse code snippet, verify TypeEnv bindings
|
||||
```
|
||||
|
||||
## Task Dependency Graph
|
||||
|
||||
```
|
||||
Phase 1 (all parallel, no dependencies):
|
||||
1.1 Python walrus ─┐
|
||||
1.2 PHP properties ├─ All independent
|
||||
1.3 Nullable unwrap ├─ Can run as parallel swarm agents
|
||||
1.4 Go make() │
|
||||
1.5 Go type assert ─┘
|
||||
|
||||
Phase 2 (mostly parallel):
|
||||
2.1 C++ range-for ─┐
|
||||
2.2 Rust if-let ├─ Independent of each other
|
||||
2.3 Swift guard-let ├─ Can run as parallel swarm agents
|
||||
2.4 C# pattern match │
|
||||
2.5 Python class ann ─┘
|
||||
|
||||
Phase 3 (sequential dependencies):
|
||||
3.1 Return type inference ──→ 3.3 For-loop typing
|
||||
↑
|
||||
3.4 Generic extraction ────────────┘
|
||||
|
||||
3.2 Chained property access (independent)
|
||||
3.5 Block-level scoping (independent)
|
||||
3.6 Ruby type extractor (independent)
|
||||
```
|
||||
|
||||
## Sources & References
|
||||
|
||||
- PR #274: `feat/type-resolution-constructor-inference` — 6 rounds of review fixes
|
||||
- Existing patterns: `test/fixtures/lang-resolution/` (150 fixture dirs)
|
||||
- Integration tests: `test/integration/resolvers/*.test.ts` (12 languages)
|
||||
- Type extractors: `src/core/ingestion/type-extractors/*.ts` (9 files + index)
|
||||
- TypeEnv: `src/core/ingestion/type-env.ts`
|
||||
- Call processor: `src/core/ingestion/call-processor.ts`
|
||||
- Receiver extraction: `src/core/ingestion/utils.ts:extractReceiverName`
|
||||
1
gitnexus/package-lock.json
generated
1
gitnexus/package-lock.json
generated
|
|
@ -35,6 +35,7 @@
|
|||
"tree-sitter-python": "^0.21.0",
|
||||
"tree-sitter-ruby": "^0.23.1",
|
||||
"tree-sitter-rust": "^0.21.0",
|
||||
"tree-sitter-swift": "^0.6.0",
|
||||
"tree-sitter-typescript": "^0.21.0",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@
|
|||
"tree-sitter-go": "^0.21.0",
|
||||
"tree-sitter-java": "^0.21.0",
|
||||
"tree-sitter-javascript": "^0.21.0",
|
||||
"tree-sitter-kotlin": "^0.3.8",
|
||||
"tree-sitter-php": "^0.23.12",
|
||||
"tree-sitter-python": "^0.21.0",
|
||||
"tree-sitter-ruby": "^0.23.1",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
findEnclosingClassId,
|
||||
} from './utils.js';
|
||||
import { buildTypeEnv } from './type-env.js';
|
||||
import type { ConstructorBinding } from './type-env.js';
|
||||
import { getTreeSitterBufferSize } from './constants.js';
|
||||
import type { ExtractedCall, ExtractedHeritage, ExtractedRoute, FileConstructorBindings } from './workers/parse-worker.js';
|
||||
import { callRouters } from './call-routing.js';
|
||||
|
|
@ -54,6 +55,65 @@ const findEnclosingFunction = (
|
|||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify constructor bindings against SymbolTable and infer receiver types.
|
||||
* Shared between sequential (processCalls) and worker (processCallsFromExtracted) paths.
|
||||
*/
|
||||
const verifyConstructorBindings = (
|
||||
bindings: readonly ConstructorBinding[],
|
||||
filePath: string,
|
||||
ctx: ResolutionContext,
|
||||
graph?: KnowledgeGraph,
|
||||
): Map<string, string> => {
|
||||
const verified = new Map<string, string>();
|
||||
|
||||
for (const { scope, varName, calleeName, receiverClassName } of bindings) {
|
||||
const tiered = ctx.resolve(calleeName, filePath);
|
||||
const isClass = tiered?.candidates.some(def => def.type === 'Class') ?? false;
|
||||
|
||||
if (isClass) {
|
||||
verified.set(receiverKey(extractFuncNameFromScope(scope), varName), calleeName);
|
||||
} else {
|
||||
let callableDefs = tiered?.candidates.filter(d =>
|
||||
d.type === 'Function' || d.type === 'Method'
|
||||
);
|
||||
|
||||
// When receiver class is known (e.g. $this->method() in PHP), narrow
|
||||
// candidates to methods owned by that class to avoid false disambiguation failures.
|
||||
if (callableDefs && callableDefs.length > 1 && receiverClassName) {
|
||||
if (graph) {
|
||||
// Worker path: use graph.getNode (fast, already in-memory)
|
||||
const narrowed = callableDefs.filter(d => {
|
||||
if (!d.ownerId) return false;
|
||||
const owner = graph.getNode(d.ownerId);
|
||||
return owner?.properties.name === receiverClassName;
|
||||
});
|
||||
if (narrowed.length > 0) callableDefs = narrowed;
|
||||
} else {
|
||||
// Sequential path: use ctx.resolve (no graph available)
|
||||
const classResolved = ctx.resolve(receiverClassName, filePath);
|
||||
if (classResolved && classResolved.candidates.length > 0) {
|
||||
const classNodeIds = new Set(classResolved.candidates.map(c => c.nodeId));
|
||||
const narrowed = callableDefs.filter(d =>
|
||||
d.ownerId && classNodeIds.has(d.ownerId)
|
||||
);
|
||||
if (narrowed.length > 0) callableDefs = narrowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (callableDefs && callableDefs.length === 1 && callableDefs[0].returnType) {
|
||||
const typeName = extractReturnTypeName(callableDefs[0].returnType);
|
||||
if (typeName) {
|
||||
verified.set(receiverKey(extractFuncNameFromScope(scope), varName), typeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return verified;
|
||||
};
|
||||
|
||||
export const processCalls = async (
|
||||
graph: KnowledgeGraph,
|
||||
files: { path: string; content: string }[],
|
||||
|
|
@ -110,6 +170,10 @@ export const processCalls = async (
|
|||
const typeEnv = lang ? buildTypeEnv(tree, lang, ctx.symbols) : null;
|
||||
const callRouter = callRouters[language];
|
||||
|
||||
const verifiedReceivers = typeEnv && typeEnv.constructorBindings.length > 0
|
||||
? verifyConstructorBindings(typeEnv.constructorBindings, file.path, ctx)
|
||||
: new Map<string, string>();
|
||||
|
||||
ctx.enableCache(file.path);
|
||||
|
||||
matches.forEach(match => {
|
||||
|
|
@ -184,7 +248,14 @@ export const processCalls = async (
|
|||
const callNode = captureMap['call'];
|
||||
const callForm = inferCallForm(callNode, nameNode);
|
||||
const receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined;
|
||||
const receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
|
||||
let receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
|
||||
// Fall back to verified constructor bindings for return type inference
|
||||
if (!receiverTypeName && receiverName && verifiedReceivers.size > 0) {
|
||||
const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
|
||||
const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : '';
|
||||
receiverTypeName = verifiedReceivers.get(receiverKey(funcName, receiverName))
|
||||
?? verifiedReceivers.get(receiverKey('', receiverName));
|
||||
}
|
||||
|
||||
const resolved = resolveCallTarget({
|
||||
calledName,
|
||||
|
|
@ -343,6 +414,135 @@ const resolveCallTarget = (
|
|||
return toResolveResult(filteredCandidates[0], tiered.tier);
|
||||
};
|
||||
|
||||
// ── Return type text helpers ─────────────────────────────────────────────
|
||||
// extractSimpleTypeName works on AST nodes; this operates on raw return-type
|
||||
// text already stored in SymbolDefinition (e.g. "User", "Promise<User>",
|
||||
// "User | null", "*User"). Extracts the base user-defined type name.
|
||||
|
||||
/** Primitive / built-in types that should NOT produce a receiver binding. */
|
||||
const PRIMITIVE_TYPES = new Set([
|
||||
'string', 'number', 'boolean', 'void', 'int', 'float', 'double', 'long',
|
||||
'short', 'byte', 'char', 'bool', 'str', 'i8', 'i16', 'i32', 'i64',
|
||||
'u8', 'u16', 'u32', 'u64', 'f32', 'f64', 'usize', 'isize',
|
||||
'undefined', 'null', 'None', 'nil',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Extract a simple type name from raw return-type text.
|
||||
* Handles common patterns:
|
||||
* "User" → "User"
|
||||
* "Promise<User>" → "User" (unwrap wrapper generics)
|
||||
* "Option<User>" → "User"
|
||||
* "Result<User, Error>" → "User" (first type arg)
|
||||
* "User | null" → "User" (strip nullable union)
|
||||
* "User?" → "User" (strip nullable suffix)
|
||||
* "*User" → "User" (Go pointer)
|
||||
* "&User" → "User" (Rust reference)
|
||||
* Returns undefined for complex types or primitives.
|
||||
*/
|
||||
const WRAPPER_GENERICS = new Set([
|
||||
'Promise', 'Observable', 'Future', 'CompletableFuture', 'Task', 'ValueTask', // async wrappers
|
||||
'Option', 'Some', 'Optional', 'Maybe', // nullable wrappers
|
||||
'Result', 'Either', // result wrappers
|
||||
// Rust smart pointers (Deref to inner type)
|
||||
'Rc', 'Arc', 'Weak', // pointer types
|
||||
'MutexGuard', 'RwLockReadGuard', 'RwLockWriteGuard', // guard types
|
||||
'Ref', 'RefMut', // RefCell guards
|
||||
'Cow', // copy-on-write
|
||||
// Containers (List, Array, Vec, Set, etc.) are intentionally excluded —
|
||||
// methods are called on the container, not the element type.
|
||||
// Non-wrapper generics return the base type (e.g., List) via the else branch.
|
||||
]);
|
||||
|
||||
/**
|
||||
* Extracts the first type argument from a comma-separated generic argument string,
|
||||
* respecting nested angle brackets. For example:
|
||||
* "Result<User, Error>" → "Result<User, Error>" (no top-level comma)
|
||||
* "User, Error" → "User"
|
||||
* "Map<K, V>, string" → "Map<K, V>"
|
||||
*/
|
||||
function extractFirstGenericArg(args: string): string {
|
||||
let depth = 0;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '<') depth++;
|
||||
else if (args[i] === '>') depth--;
|
||||
else if (args[i] === ',' && depth === 0) return args.slice(0, i).trim();
|
||||
}
|
||||
return args.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the first non-lifetime type argument from a generic argument string.
|
||||
* Skips Rust lifetime parameters (e.g., `'a`, `'_`) to find the actual type.
|
||||
* "'_, User" → "User"
|
||||
* "'a, User" → "User"
|
||||
* "User, Error" → "User" (no lifetime — delegates to extractFirstGenericArg)
|
||||
*/
|
||||
function extractFirstTypeArg(args: string): string {
|
||||
let remaining = args;
|
||||
while (remaining) {
|
||||
const first = extractFirstGenericArg(remaining);
|
||||
if (!first.startsWith("'")) return first;
|
||||
// Skip past this lifetime arg + the comma separator
|
||||
const commaIdx = remaining.indexOf(',', first.length);
|
||||
if (commaIdx < 0) return first; // only lifetimes — fall through
|
||||
remaining = remaining.slice(commaIdx + 1).trim();
|
||||
}
|
||||
return args.trim();
|
||||
}
|
||||
|
||||
export const extractReturnTypeName = (raw: string): string | undefined => {
|
||||
let text = raw.trim();
|
||||
if (!text) return undefined;
|
||||
|
||||
// Strip pointer/reference prefixes: *User, &User, &mut User
|
||||
text = text.replace(/^[&*]+\s*(mut\s+)?/, '');
|
||||
|
||||
// Strip nullable suffix: User?
|
||||
text = text.replace(/\?$/, '');
|
||||
|
||||
// Handle union types: "User | null" → "User"
|
||||
if (text.includes('|')) {
|
||||
const parts = text.split('|').map(p => p.trim()).filter(p =>
|
||||
p !== 'null' && p !== 'undefined' && p !== 'void' && p !== 'None' && p !== 'nil'
|
||||
);
|
||||
if (parts.length === 1) text = parts[0];
|
||||
else return undefined; // genuine union — too complex
|
||||
}
|
||||
|
||||
// Handle generics: Promise<User> → unwrap if wrapper, else take base
|
||||
const genericMatch = text.match(/^(\w+)\s*<(.+)>$/);
|
||||
if (genericMatch) {
|
||||
const [, base, args] = genericMatch;
|
||||
if (WRAPPER_GENERICS.has(base)) {
|
||||
// Take the first non-lifetime type argument, using bracket-balanced splitting
|
||||
// so that nested generics like Result<User, Error> are not split at the inner
|
||||
// comma. Lifetime parameters (Rust 'a, '_) are skipped.
|
||||
const firstArg = extractFirstTypeArg(args);
|
||||
return extractReturnTypeName(firstArg);
|
||||
}
|
||||
// Non-wrapper generic: return the base type (e.g., Map<K,V> → Map)
|
||||
return PRIMITIVE_TYPES.has(base.toLowerCase()) ? undefined : base;
|
||||
}
|
||||
|
||||
// Bare wrapper type without generic argument (e.g. Task, Promise, Option)
|
||||
// should not produce a binding — these are meaningless without a type parameter
|
||||
if (WRAPPER_GENERICS.has(text)) return undefined;
|
||||
|
||||
// Handle qualified names: models.User → User, Models::User → User, \App\Models\User → User
|
||||
if (text.includes('::') || text.includes('.') || text.includes('\\')) {
|
||||
text = text.split(/::|[.\\]/).pop()!;
|
||||
}
|
||||
|
||||
// Final check: skip primitives
|
||||
if (PRIMITIVE_TYPES.has(text) || PRIMITIVE_TYPES.has(text.toLowerCase())) return undefined;
|
||||
|
||||
// Must start with uppercase (class/type convention) or be a valid identifier
|
||||
if (!/^[A-Z_]\w*$/.test(text)) return undefined;
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
// ── Scope key helpers ────────────────────────────────────────────────────
|
||||
// Scope keys use the format "funcName@startIndex" (produced by type-env.ts).
|
||||
// Source IDs use "Label:filepath:funcName" (produced by parse-worker.ts).
|
||||
|
|
@ -380,13 +580,9 @@ export const processCallsFromExtracted = async (
|
|||
const fileReceiverTypes = new Map<string, Map<string, string>>();
|
||||
if (constructorBindings) {
|
||||
for (const { filePath, bindings } of constructorBindings) {
|
||||
for (const { scope, varName, calleeName } of bindings) {
|
||||
const tiered = ctx.resolve(calleeName, filePath);
|
||||
const isClass = tiered?.candidates.some(def => def.type === 'Class') ?? false;
|
||||
if (isClass) {
|
||||
if (!fileReceiverTypes.has(filePath)) fileReceiverTypes.set(filePath, new Map());
|
||||
fileReceiverTypes.get(filePath)!.set(receiverKey(extractFuncNameFromScope(scope), varName), calleeName);
|
||||
}
|
||||
const verified = verifyConstructorBindings(bindings, filePath, ctx, graph);
|
||||
if (verified.size > 0) {
|
||||
fileReceiverTypes.set(filePath, verified);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { ASTCache } from './ast-cache.js';
|
|||
import { getLanguageFromFilename, yieldToEventLoop, DEFINITION_CAPTURE_KEYS, getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSignature } from './utils.js';
|
||||
import { isNodeExported } from './export-detection.js';
|
||||
import { detectFrameworkFromAST } from './framework-detection.js';
|
||||
import { typeConfigs } from './type-extractors/index.js';
|
||||
import { WorkerPool } from './workers/worker-pool.js';
|
||||
import type { ParseWorkerResult, ParseWorkerInput, ExtractedImport, ExtractedCall, ExtractedHeritage, ExtractedRoute, FileConstructorBindings } from './workers/parse-worker.js';
|
||||
import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from './constants.js';
|
||||
|
|
@ -79,6 +80,7 @@ const processParsingWithWorkers = async (
|
|||
for (const sym of result.symbols) {
|
||||
symbolTable.add(sym.filePath, sym.name, sym.nodeId, sym.type, {
|
||||
parameterCount: sym.parameterCount,
|
||||
returnType: sym.returnType,
|
||||
ownerId: sym.ownerId,
|
||||
});
|
||||
}
|
||||
|
|
@ -214,6 +216,14 @@ const processParsingSequential = async (
|
|||
? extractMethodSignature(definitionNode)
|
||||
: undefined;
|
||||
|
||||
// Language-specific return type fallback (e.g. Ruby YARD @return [Type])
|
||||
if (methodSig && !methodSig.returnType && definitionNode) {
|
||||
const tc = typeConfigs[language as keyof typeof typeConfigs];
|
||||
if (tc?.extractReturnType) {
|
||||
methodSig.returnType = tc.extractReturnType(definitionNode);
|
||||
}
|
||||
}
|
||||
|
||||
const node: GraphNode = {
|
||||
id: nodeId,
|
||||
label: nodeLabel as any,
|
||||
|
|
@ -244,6 +254,7 @@ const processParsingSequential = async (
|
|||
|
||||
symbolTable.add(file.path, nodeName, nodeId, nodeLabel, {
|
||||
parameterCount: methodSig?.parameterCount,
|
||||
returnType: methodSig?.returnType,
|
||||
ownerId: enclosingClassId ?? undefined,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ export interface SymbolDefinition {
|
|||
filePath: string;
|
||||
type: string; // 'Function', 'Class', etc.
|
||||
parameterCount?: number;
|
||||
/** Raw return type text extracted from AST (e.g. 'User', 'Promise<User>') */
|
||||
returnType?: string;
|
||||
/** Links Method/Constructor to owning Class/Struct/Trait nodeId */
|
||||
ownerId?: string;
|
||||
}
|
||||
|
|
@ -16,7 +18,7 @@ export interface SymbolTable {
|
|||
name: string,
|
||||
nodeId: string,
|
||||
type: string,
|
||||
metadata?: { parameterCount?: number; ownerId?: string }
|
||||
metadata?: { parameterCount?: number; returnType?: string; ownerId?: string }
|
||||
) => void;
|
||||
|
||||
/**
|
||||
|
|
@ -62,13 +64,14 @@ export const createSymbolTable = (): SymbolTable => {
|
|||
name: string,
|
||||
nodeId: string,
|
||||
type: string,
|
||||
metadata?: { parameterCount?: number; ownerId?: string }
|
||||
metadata?: { parameterCount?: number; returnType?: string; ownerId?: string }
|
||||
) => {
|
||||
const def: SymbolDefinition = {
|
||||
nodeId,
|
||||
filePath,
|
||||
type,
|
||||
...(metadata?.parameterCount !== undefined ? { parameterCount: metadata.parameterCount } : {}),
|
||||
...(metadata?.returnType !== undefined ? { returnType: metadata.returnType } : {}),
|
||||
...(metadata?.ownerId !== undefined ? { ownerId: metadata.ownerId } : {}),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -365,6 +365,13 @@ export const CSHARP_QUERIES = `
|
|||
(invocation_expression function: (identifier) @call.name) @call
|
||||
(invocation_expression function: (member_access_expression name: (identifier) @call.name)) @call
|
||||
|
||||
; Null-conditional method calls: user?.Save()
|
||||
; Parses as: invocation_expression → conditional_access_expression → member_binding_expression → identifier
|
||||
(invocation_expression
|
||||
function: (conditional_access_expression
|
||||
(member_binding_expression
|
||||
(identifier) @call.name))) @call
|
||||
|
||||
; Constructor calls: new Foo() and new Foo { Props }
|
||||
(object_creation_expression type: (identifier) @call.name) @call
|
||||
|
||||
|
|
|
|||
|
|
@ -270,20 +270,14 @@ const createClassNameLookup = (
|
|||
};
|
||||
|
||||
/**
|
||||
* Build a scoped TypeEnv from a tree-sitter AST for a given language.
|
||||
* Single-pass: collects class/struct names AND type bindings in one walk.
|
||||
* Class names are accumulated incrementally — this is safe because no
|
||||
* language allows constructing a class before its definition.
|
||||
* Build a TypeEnvironment from a tree-sitter AST for a given language.
|
||||
* Single-pass: collects class/struct names, type bindings, AND constructor
|
||||
* bindings that couldn't be resolved locally — all in one AST walk.
|
||||
*
|
||||
* When a symbolTable is provided (call-processor path), class names from across
|
||||
* the project are available for constructor inference in languages like Kotlin
|
||||
* where constructors are syntactically identical to function calls.
|
||||
*/
|
||||
/**
|
||||
* Build a TypeEnvironment from a tree-sitter AST for a given language.
|
||||
* Single-pass: collects class/struct names, type bindings, AND constructor
|
||||
* bindings that couldn't be resolved locally — all in one AST walk.
|
||||
*/
|
||||
export const buildTypeEnv = (
|
||||
tree: { rootNode: SyntaxNode },
|
||||
language: SupportedLanguages,
|
||||
|
|
@ -293,7 +287,6 @@ export const buildTypeEnv = (
|
|||
const localClassNames = new Set<string>();
|
||||
const classNames = createClassNameLookup(localClassNames, symbolTable);
|
||||
const config = typeConfigs[language];
|
||||
const scanner = CONSTRUCTOR_BINDING_SCANNERS[language];
|
||||
const bindings: ConstructorBinding[] = [];
|
||||
|
||||
/**
|
||||
|
|
@ -347,8 +340,8 @@ export const buildTypeEnv = (
|
|||
|
||||
// Scan for constructor bindings that couldn't be resolved locally.
|
||||
// Only collect if TypeEnv didn't already resolve this binding.
|
||||
if (scanner) {
|
||||
const result = scanner(node);
|
||||
if (config.scanConstructorBinding) {
|
||||
const result = config.scanConstructorBinding(node);
|
||||
if (result && !scopeEnv.has(result.varName)) {
|
||||
bindings.push({ scope, ...result });
|
||||
}
|
||||
|
|
@ -381,149 +374,8 @@ export interface ConstructorBinding {
|
|||
varName: string;
|
||||
/** Name of the callee (potential class constructor) */
|
||||
calleeName: string;
|
||||
/** Enclosing class name when callee is a method on a known receiver (e.g. $this) */
|
||||
receiverClassName?: string;
|
||||
}
|
||||
|
||||
/** C/C++: auto x = User() where function is an identifier (not type_identifier) */
|
||||
const extractCppConstructorBinding = (node: SyntaxNode): { varName: string; calleeName: string } | undefined => {
|
||||
if (node.type !== 'declaration') return undefined;
|
||||
const typeNode = node.childForFieldName('type');
|
||||
if (!typeNode) return undefined;
|
||||
const typeText = typeNode.text;
|
||||
if (typeText !== 'auto' && typeText !== 'decltype(auto)' && typeNode.type !== 'placeholder_type_specifier') return undefined;
|
||||
const declarator = node.childForFieldName('declarator');
|
||||
if (!declarator || declarator.type !== 'init_declarator') return undefined;
|
||||
const value = declarator.childForFieldName('value');
|
||||
if (!value || value.type !== 'call_expression') return undefined;
|
||||
const func = value.childForFieldName('function');
|
||||
// Match plain identifiers (type_identifier is already resolved by extractInitializer)
|
||||
// and qualified/scoped identifiers for namespaced calls like ns::HttpClient()
|
||||
if (!func) return undefined;
|
||||
if (func.type === 'qualified_identifier' || func.type === 'scoped_identifier') {
|
||||
// ns::HttpClient → extract "HttpClient" (last segment)
|
||||
const last = func.lastNamedChild;
|
||||
if (!last) return undefined;
|
||||
const nameNode = declarator.childForFieldName('declarator');
|
||||
if (!nameNode) return undefined;
|
||||
const finalName = nameNode.type === 'pointer_declarator' || nameNode.type === 'reference_declarator'
|
||||
? nameNode.firstNamedChild : nameNode;
|
||||
if (!finalName) return undefined;
|
||||
return { varName: finalName.text, calleeName: last.text };
|
||||
}
|
||||
if (func.type !== 'identifier') return undefined;
|
||||
const nameNode = declarator.childForFieldName('declarator');
|
||||
if (!nameNode) return undefined;
|
||||
const finalName = nameNode.type === 'pointer_declarator' || nameNode.type === 'reference_declarator'
|
||||
? nameNode.firstNamedChild : nameNode;
|
||||
if (!finalName) return undefined;
|
||||
const varName = finalName.text;
|
||||
if (!varName) return undefined;
|
||||
return { varName, calleeName: func.text };
|
||||
};
|
||||
|
||||
/** Ruby: user = User.new — assignment with call where method is 'new' and receiver is a constant */
|
||||
const extractRubyConstructorBinding = (node: SyntaxNode): { varName: string; calleeName: string } | undefined => {
|
||||
if (node.type !== 'assignment') return undefined;
|
||||
const left = node.childForFieldName('left');
|
||||
const right = node.childForFieldName('right');
|
||||
if (!left || !right) return undefined;
|
||||
// Support both local variables (identifier) and constants (USER = User.new)
|
||||
if (left.type !== 'identifier' && left.type !== 'constant') return undefined;
|
||||
if (right.type !== 'call') return undefined;
|
||||
const method = right.childForFieldName('method');
|
||||
if (!method || method.text !== 'new') return undefined;
|
||||
const receiver = right.childForFieldName('receiver');
|
||||
if (!receiver || receiver.type !== 'constant') return undefined;
|
||||
return { varName: left.text, calleeName: receiver.text };
|
||||
};
|
||||
|
||||
/** Language-specific constructor-binding scanners. */
|
||||
const CONSTRUCTOR_BINDING_SCANNERS: Partial<Record<SupportedLanguages, (node: SyntaxNode) => { varName: string; calleeName: string } | undefined>> = {
|
||||
// Kotlin: val x = User(...) — property_declaration with call_expression
|
||||
[SupportedLanguages.Kotlin]: (node) => {
|
||||
if (node.type !== 'property_declaration') return undefined;
|
||||
const varDecl = node.namedChildren.find(c => c.type === 'variable_declaration');
|
||||
if (!varDecl) return undefined;
|
||||
if (varDecl.namedChildren.some(c => c.type === 'user_type')) return undefined;
|
||||
const callExpr = node.namedChildren.find(c => c.type === 'call_expression');
|
||||
if (!callExpr) return undefined;
|
||||
const callee = callExpr.firstNamedChild;
|
||||
if (!callee || callee.type !== 'simple_identifier') return undefined;
|
||||
const nameNode = varDecl.namedChildren.find(c => c.type === 'simple_identifier');
|
||||
if (!nameNode) return undefined;
|
||||
return { varName: nameNode.text, calleeName: callee.text };
|
||||
},
|
||||
|
||||
// Python: user = User("alice") — assignment with call
|
||||
// Also handles walrus operator: (user := User("alice"))
|
||||
[SupportedLanguages.Python]: (node) => {
|
||||
let left: SyntaxNode | null;
|
||||
let right: SyntaxNode | null;
|
||||
|
||||
if (node.type === 'named_expression') {
|
||||
// Walrus operator: (user := User("alice"))
|
||||
left = node.childForFieldName('name');
|
||||
right = node.childForFieldName('value');
|
||||
} else if (node.type === 'assignment') {
|
||||
left = node.childForFieldName('left');
|
||||
right = node.childForFieldName('right');
|
||||
// Skip annotated assignments — extractDeclaration handles those
|
||||
if (node.childForFieldName('type')) return undefined;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!left || !right) return undefined;
|
||||
if (left.type !== 'identifier') return undefined;
|
||||
if (right.type !== 'call') return undefined;
|
||||
const func = right.childForFieldName('function');
|
||||
if (!func) return undefined;
|
||||
// Support both direct calls (User()) and qualified calls (models.User())
|
||||
const calleeName = extractSimpleTypeName(func);
|
||||
if (!calleeName) return undefined;
|
||||
return { varName: left.text, calleeName };
|
||||
},
|
||||
|
||||
// Swift: let user = User(name: "alice") — property_declaration with call_expression
|
||||
[SupportedLanguages.Swift]: (node) => {
|
||||
if (node.type !== 'property_declaration') return undefined;
|
||||
// Skip if has type annotation
|
||||
if (node.childForFieldName('type')) return undefined;
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
if (node.namedChild(i)?.type === 'type_annotation') return undefined;
|
||||
}
|
||||
const pattern = node.childForFieldName('pattern');
|
||||
if (!pattern) return undefined;
|
||||
const varName = pattern.text;
|
||||
if (!varName) return undefined;
|
||||
// Find call_expression child
|
||||
let callExpr: SyntaxNode | null = null;
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child?.type === 'call_expression') { callExpr = child; break; }
|
||||
}
|
||||
if (!callExpr) return undefined;
|
||||
const callee = callExpr.firstNamedChild;
|
||||
if (!callee) return undefined;
|
||||
// Direct call: User(name: "alice") — simple_identifier callee
|
||||
if (callee.type === 'simple_identifier') {
|
||||
return { varName, calleeName: callee.text };
|
||||
}
|
||||
// Explicit init: User.init(name: "alice") — navigation_expression with .init suffix
|
||||
if (callee.type === 'navigation_expression') {
|
||||
const receiver = callee.firstNamedChild;
|
||||
const suffix = callee.lastNamedChild;
|
||||
if (receiver?.type === 'simple_identifier' && suffix?.text === 'init') {
|
||||
return { varName, calleeName: receiver.text };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
// C++: auto x = User() where User is parsed as identifier (cross-file)
|
||||
// Note: C is excluded — C has no constructors and `auto` is a storage-class specifier, not type inference.
|
||||
[SupportedLanguages.CPlusPlus]: extractCppConstructorBinding,
|
||||
|
||||
// Ruby: user = User.new — assignment with call where method is 'new' and receiver is a constant
|
||||
[SupportedLanguages.Ruby]: extractRubyConstructorBinding,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
|
|
@ -126,9 +126,44 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
|
|||
if (varName && typeName) env.set(varName, typeName);
|
||||
};
|
||||
|
||||
/** C/C++: auto x = User() where function is an identifier (not type_identifier) */
|
||||
const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||
if (node.type !== 'declaration') return undefined;
|
||||
const typeNode = node.childForFieldName('type');
|
||||
if (!typeNode) return undefined;
|
||||
const typeText = typeNode.text;
|
||||
if (typeText !== 'auto' && typeText !== 'decltype(auto)' && typeNode.type !== 'placeholder_type_specifier') return undefined;
|
||||
const declarator = node.childForFieldName('declarator');
|
||||
if (!declarator || declarator.type !== 'init_declarator') return undefined;
|
||||
const value = declarator.childForFieldName('value');
|
||||
if (!value || value.type !== 'call_expression') return undefined;
|
||||
const func = value.childForFieldName('function');
|
||||
if (!func) return undefined;
|
||||
if (func.type === 'qualified_identifier' || func.type === 'scoped_identifier') {
|
||||
const last = func.lastNamedChild;
|
||||
if (!last) return undefined;
|
||||
const nameNode = declarator.childForFieldName('declarator');
|
||||
if (!nameNode) return undefined;
|
||||
const finalName = nameNode.type === 'pointer_declarator' || nameNode.type === 'reference_declarator'
|
||||
? nameNode.firstNamedChild : nameNode;
|
||||
if (!finalName) return undefined;
|
||||
return { varName: finalName.text, calleeName: last.text };
|
||||
}
|
||||
if (func.type !== 'identifier') return undefined;
|
||||
const nameNode = declarator.childForFieldName('declarator');
|
||||
if (!nameNode) return undefined;
|
||||
const finalName = nameNode.type === 'pointer_declarator' || nameNode.type === 'reference_declarator'
|
||||
? nameNode.firstNamedChild : nameNode;
|
||||
if (!finalName) return undefined;
|
||||
const varName = finalName.text;
|
||||
if (!varName) return undefined;
|
||||
return { varName, calleeName: func.text };
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
extractInitializer,
|
||||
scanConstructorBinding,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, findChildByType } from './shared.js';
|
||||
import type { ConstructorBindingScanner, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, findChildByType, unwrapAwait } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'local_declaration_statement',
|
||||
|
|
@ -103,8 +103,49 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
|
|||
if (varName && typeName) env.set(varName, typeName);
|
||||
};
|
||||
|
||||
/** C#: var x = SomeFactory(...) → bind x to SomeFactory (constructor-like call) */
|
||||
const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||
if (node.type !== 'variable_declaration') return undefined;
|
||||
// Find type and declarator children by iterating (C# grammar doesn't expose 'type' as a named field)
|
||||
let typeNode: SyntaxNode | null = null;
|
||||
let declarator: SyntaxNode | null = null;
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (!child) continue;
|
||||
if (child.type === 'variable_declarator') { if (!declarator) declarator = child; }
|
||||
else if (!typeNode) { typeNode = child; }
|
||||
}
|
||||
// Only handle implicit_type (var) — explicit types handled by extractDeclaration
|
||||
if (!typeNode || typeNode.type !== 'implicit_type') return undefined;
|
||||
if (!declarator) return undefined;
|
||||
const nameNode = declarator.childForFieldName('name') ?? declarator.firstNamedChild;
|
||||
if (!nameNode || nameNode.type !== 'identifier') return undefined;
|
||||
// Find the initializer value: either inside equals_value_clause or as a direct child
|
||||
// (tree-sitter-c-sharp puts invocation_expression directly inside variable_declarator)
|
||||
let value: SyntaxNode | null = null;
|
||||
for (let i = 0; i < declarator.namedChildCount; i++) {
|
||||
const child = declarator.namedChild(i);
|
||||
if (!child) continue;
|
||||
if (child.type === 'equals_value_clause') { value = child.firstNamedChild; break; }
|
||||
if (child.type === 'invocation_expression' || child.type === 'object_creation_expression' || child.type === 'await_expression') { value = child; break; }
|
||||
}
|
||||
if (!value) return undefined;
|
||||
// Unwrap await: `var user = await svc.GetUserAsync()` → await_expression wraps invocation_expression
|
||||
value = unwrapAwait(value);
|
||||
if (!value) return undefined;
|
||||
// Skip object_creation_expression (new User()) — handled by extractInitializer
|
||||
if (value.type === 'object_creation_expression') return undefined;
|
||||
if (value.type !== 'invocation_expression') return undefined;
|
||||
const func = value.firstNamedChild;
|
||||
if (!func) return undefined;
|
||||
const calleeName = extractSimpleTypeName(func);
|
||||
if (!calleeName) return undefined;
|
||||
return { varName: nameNode.text, calleeName };
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
scanConstructorBinding,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js';
|
||||
import type { ConstructorBindingScanner, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
|
|
@ -141,8 +141,49 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
|
|||
if (varName && typeName) env.set(varName, typeName);
|
||||
};
|
||||
|
||||
/** Go: user := NewUser(...) — infer type from single-assignment call expression */
|
||||
const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||
if (node.type !== 'short_var_declaration') return undefined;
|
||||
const left = node.childForFieldName('left');
|
||||
const right = node.childForFieldName('right');
|
||||
if (!left || !right) return undefined;
|
||||
const leftIds = left.type === 'expression_list' ? left.namedChildren : [left];
|
||||
const rightExprs = right.type === 'expression_list' ? right.namedChildren : [right];
|
||||
|
||||
// Multi-return: user, err := NewUser() — bind first var when second is err/ok/_
|
||||
if (leftIds.length === 2 && rightExprs.length === 1) {
|
||||
const secondVar = leftIds[1];
|
||||
const isErrorOrDiscard =
|
||||
secondVar.text === '_' ||
|
||||
secondVar.text === 'err' ||
|
||||
secondVar.text === 'ok' ||
|
||||
secondVar.text === 'error';
|
||||
if (isErrorOrDiscard && leftIds[0].type === 'identifier') {
|
||||
if (rightExprs[0].type !== 'call_expression') return undefined;
|
||||
const func = rightExprs[0].childForFieldName('function');
|
||||
if (!func) return undefined;
|
||||
if (func.text === 'new' || func.text === 'make') return undefined;
|
||||
const calleeName = extractSimpleTypeName(func);
|
||||
if (!calleeName) return undefined;
|
||||
return { varName: leftIds[0].text, calleeName };
|
||||
}
|
||||
}
|
||||
|
||||
// Single assignment only
|
||||
if (leftIds.length !== 1 || leftIds[0].type !== 'identifier') return undefined;
|
||||
if (rightExprs.length !== 1 || rightExprs[0].type !== 'call_expression') return undefined;
|
||||
const func = rightExprs[0].childForFieldName('function');
|
||||
if (!func) return undefined;
|
||||
// Skip new() and make() — already handled by extractDeclaration
|
||||
if (func.text === 'new' || func.text === 'make') return undefined;
|
||||
const calleeName = extractSimpleTypeName(func);
|
||||
if (!calleeName) return undefined;
|
||||
return { varName: leftIds[0].text, calleeName };
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
scanConstructorBinding,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { typeConfig as pythonConfig } from './python.js';
|
|||
import { typeConfig as swiftConfig } from './swift.js';
|
||||
import { typeConfig as cCppConfig } from './c-cpp.js';
|
||||
import { typeConfig as phpConfig } from './php.js';
|
||||
import { typeConfig as rubyConfig } from './ruby.js';
|
||||
|
||||
export const typeConfigs = {
|
||||
[SupportedLanguages.JavaScript]: typescriptConfig,
|
||||
|
|
@ -29,12 +30,15 @@ export const typeConfigs = {
|
|||
[SupportedLanguages.C]: cCppConfig,
|
||||
[SupportedLanguages.CPlusPlus]: cCppConfig,
|
||||
[SupportedLanguages.PHP]: phpConfig,
|
||||
[SupportedLanguages.Ruby]: {
|
||||
declarationNodeTypes: new Set<string>(),
|
||||
extractDeclaration: () => {},
|
||||
extractParameter: () => {},
|
||||
} as LanguageTypeConfig,
|
||||
[SupportedLanguages.Ruby]: rubyConfig,
|
||||
} satisfies Record<SupportedLanguages, LanguageTypeConfig>;
|
||||
|
||||
export type { LanguageTypeConfig, TypeBindingExtractor, ParameterExtractor } from './types.js';
|
||||
export { TYPED_PARAMETER_TYPES, extractSimpleTypeName, extractVarName, findChildByType } from './shared.js';
|
||||
export type { LanguageTypeConfig, TypeBindingExtractor, ParameterExtractor, ConstructorBindingScanner } from './types.js';
|
||||
export {
|
||||
TYPED_PARAMETER_TYPES,
|
||||
extractSimpleTypeName,
|
||||
extractGenericTypeArgs,
|
||||
extractVarName,
|
||||
findChildByType,
|
||||
extractRubyConstructorAssignment
|
||||
} from './shared.js';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, findChildByType } from './shared.js';
|
||||
|
||||
// ── Java ──────────────────────────────────────────────────────────────────
|
||||
|
|
@ -67,11 +67,30 @@ const extractJavaParameter: ParameterExtractor = (node: SyntaxNode, env: Map<str
|
|||
if (varName && typeName) env.set(varName, typeName);
|
||||
};
|
||||
|
||||
/** Java: var x = SomeFactory.create() — constructor binding for `var` with method_invocation */
|
||||
const scanJavaConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||
if (node.type !== 'local_variable_declaration') return undefined;
|
||||
const typeNode = node.childForFieldName('type');
|
||||
if (!typeNode) return undefined;
|
||||
if (typeNode.text !== 'var') return undefined;
|
||||
const declarator = node.namedChildren.find((c: SyntaxNode) => c.type === 'variable_declarator');
|
||||
if (!declarator) return undefined;
|
||||
const nameNode = declarator.childForFieldName('name');
|
||||
const value = declarator.childForFieldName('value');
|
||||
if (!nameNode || !value) return undefined;
|
||||
if (value.type === 'object_creation_expression') return undefined;
|
||||
if (value.type !== 'method_invocation') return undefined;
|
||||
const methodName = value.childForFieldName('name');
|
||||
if (!methodName) return undefined;
|
||||
return { varName: nameNode.text, calleeName: methodName.text };
|
||||
};
|
||||
|
||||
export const javaTypeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: JAVA_DECLARATION_NODE_TYPES,
|
||||
extractDeclaration: extractJavaDeclaration,
|
||||
extractParameter: extractJavaParameter,
|
||||
extractInitializer: extractJavaInitializer,
|
||||
scanConstructorBinding: scanJavaConstructorBinding,
|
||||
};
|
||||
|
||||
// ── Kotlin ────────────────────────────────────────────────────────────────
|
||||
|
|
@ -166,9 +185,40 @@ const extractKotlinInitializer: InitializerExtractor = (node: SyntaxNode, env: M
|
|||
if (varName) env.set(varName, calleeName);
|
||||
};
|
||||
|
||||
/** Kotlin: val x = User(...) — constructor binding for property_declaration with call_expression */
|
||||
const scanKotlinConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||
if (node.type !== 'property_declaration') return undefined;
|
||||
const varDecl = node.namedChildren.find(c => c.type === 'variable_declaration');
|
||||
if (!varDecl) return undefined;
|
||||
if (varDecl.namedChildren.some(c => c.type === 'user_type')) return undefined;
|
||||
const callExpr = node.namedChildren.find(c => c.type === 'call_expression');
|
||||
if (!callExpr) return undefined;
|
||||
const callee = callExpr.firstNamedChild;
|
||||
if (!callee) return undefined;
|
||||
|
||||
let calleeName: string | undefined;
|
||||
if (callee.type === 'simple_identifier') {
|
||||
calleeName = callee.text;
|
||||
} else if (callee.type === 'navigation_expression') {
|
||||
// Extract method name from qualified call: service.getUser() → getUser
|
||||
const suffix = callee.lastNamedChild;
|
||||
if (suffix?.type === 'navigation_suffix') {
|
||||
const methodName = suffix.lastNamedChild;
|
||||
if (methodName?.type === 'simple_identifier') {
|
||||
calleeName = methodName.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!calleeName) return undefined;
|
||||
const nameNode = varDecl.namedChildren.find(c => c.type === 'simple_identifier');
|
||||
if (!nameNode) return undefined;
|
||||
return { varName: nameNode.text, calleeName };
|
||||
};
|
||||
|
||||
export const kotlinTypeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: KOTLIN_DECLARATION_NODE_TYPES,
|
||||
extractDeclaration: extractKotlinDeclaration,
|
||||
extractParameter: extractKotlinParameter,
|
||||
extractInitializer: extractKotlinInitializer,
|
||||
scanConstructorBinding: scanKotlinConstructorBinding,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName } from './shared.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, extractCalleeName } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'assignment_expression', // For constructor inference: $x = new User()
|
||||
'property_declaration', // PHP 7.4+ typed properties: private UserRepo $repo;
|
||||
'assignment_expression', // For constructor inference: $x = new User()
|
||||
'property_declaration', // PHP 7.4+ typed properties: private UserRepo $repo;
|
||||
'method_declaration', // PHPDoc @param on class methods
|
||||
'function_definition', // PHPDoc @param on top-level functions
|
||||
]);
|
||||
|
||||
/** Walk up the AST to find the enclosing class declaration. */
|
||||
|
|
@ -45,8 +47,90 @@ const resolvePhpKeyword = (keyword: string, node: SyntaxNode): string | undefine
|
|||
return undefined;
|
||||
};
|
||||
|
||||
/** PHP: typed class properties (PHP 7.4+): private UserRepo $repo; */
|
||||
const normalizePhpType = (raw: string): string | undefined => {
|
||||
// Strip nullable prefix: ?User → User
|
||||
let type = raw.startsWith('?') ? raw.slice(1) : raw;
|
||||
// Strip array suffix: User[] → User
|
||||
type = type.replace(/\[\]$/, '');
|
||||
// Strip union with null/false/void: User|null → User
|
||||
const parts = type.split('|').filter(p => p !== 'null' && p !== 'false' && p !== 'void' && p !== 'mixed');
|
||||
if (parts.length !== 1) return undefined;
|
||||
type = parts[0];
|
||||
// Strip namespace: \App\Models\User → User
|
||||
const segments = type.split('\\');
|
||||
type = segments[segments.length - 1];
|
||||
// Skip uninformative types
|
||||
if (type === 'mixed' || type === 'void' || type === 'self' || type === 'static' || type === 'object') return undefined;
|
||||
if (/^\w+$/.test(type)) return type;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Node types to skip when walking backwards to find doc-comments.
|
||||
* PHP 8+ attributes (#[Route(...)]) appear as named siblings between PHPDoc and method. */
|
||||
const SKIP_NODE_TYPES: ReadonlySet<string> = new Set(['attribute_list', 'attribute']);
|
||||
|
||||
/** Regex to extract PHPDoc @param annotations: `@param Type $name` (standard order) */
|
||||
const PHPDOC_PARAM_RE = /@param\s+(\S+)\s+\$(\w+)/g;
|
||||
/** Alternate PHPDoc order: `@param $name Type` (name first) */
|
||||
const PHPDOC_PARAM_ALT_RE = /@param\s+\$(\w+)\s+(\S+)/g;
|
||||
|
||||
/**
|
||||
* Collect PHPDoc @param type bindings from comment nodes preceding a method/function.
|
||||
* Returns a map of paramName → typeName (without $ prefix).
|
||||
*/
|
||||
const collectPhpDocParams = (methodNode: SyntaxNode): Map<string, string> => {
|
||||
const commentTexts: string[] = [];
|
||||
let sibling = methodNode.previousSibling;
|
||||
while (sibling) {
|
||||
if (sibling.type === 'comment') {
|
||||
commentTexts.unshift(sibling.text);
|
||||
} else if (sibling.isNamed && !SKIP_NODE_TYPES.has(sibling.type)) {
|
||||
break;
|
||||
}
|
||||
sibling = sibling.previousSibling;
|
||||
}
|
||||
if (commentTexts.length === 0) return new Map();
|
||||
|
||||
const params = new Map<string, string>();
|
||||
const commentBlock = commentTexts.join('\n');
|
||||
PHPDOC_PARAM_RE.lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = PHPDOC_PARAM_RE.exec(commentBlock)) !== null) {
|
||||
const typeName = normalizePhpType(match[1]);
|
||||
const paramName = match[2]; // without $ prefix
|
||||
if (typeName) {
|
||||
// Store with $ prefix to match how PHP variables appear in the env
|
||||
params.set('$' + paramName, typeName);
|
||||
}
|
||||
}
|
||||
|
||||
// Also check alternate PHPDoc order: @param $name Type
|
||||
PHPDOC_PARAM_ALT_RE.lastIndex = 0;
|
||||
while ((match = PHPDOC_PARAM_ALT_RE.exec(commentBlock)) !== null) {
|
||||
const paramName = match[1];
|
||||
if (params.has('$' + paramName)) continue; // standard format takes priority
|
||||
const typeName = normalizePhpType(match[2]);
|
||||
if (typeName) {
|
||||
params.set('$' + paramName, typeName);
|
||||
}
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
/**
|
||||
* PHP: typed class properties (PHP 7.4+): private UserRepo $repo;
|
||||
* Also: PHPDoc @param annotations on method/function definitions.
|
||||
*/
|
||||
const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
|
||||
// PHPDoc @param on methods/functions — pre-populate env with param types
|
||||
if (node.type === 'method_declaration' || node.type === 'function_definition') {
|
||||
const phpDocParams = collectPhpDocParams(node);
|
||||
for (const [paramName, typeName] of phpDocParams) {
|
||||
if (!env.has(paramName)) env.set(paramName, typeName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type !== 'property_declaration') return;
|
||||
|
||||
const typeNode = node.childForFieldName('type');
|
||||
|
|
@ -110,9 +194,62 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
|
|||
if (varName && typeName) env.set(varName, typeName);
|
||||
};
|
||||
|
||||
/** PHP: $x = SomeFactory() or $x = $this->getUser() — bind variable to call return type */
|
||||
const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||
if (node.type !== 'assignment_expression') return undefined;
|
||||
const left = node.childForFieldName('left');
|
||||
const right = node.childForFieldName('right');
|
||||
if (!left || !right) return undefined;
|
||||
if (left.type !== 'variable_name') return undefined;
|
||||
// Skip object_creation_expression (new User()) — handled by extractInitializer
|
||||
if (right.type === 'object_creation_expression') return undefined;
|
||||
// Handle both standalone function calls and method calls ($this->getUser())
|
||||
if (right.type === 'function_call_expression') {
|
||||
const calleeName = extractCalleeName(right);
|
||||
if (!calleeName) return undefined;
|
||||
return { varName: left.text, calleeName };
|
||||
}
|
||||
if (right.type === 'member_call_expression') {
|
||||
const methodName = right.childForFieldName('name');
|
||||
if (!methodName) return undefined;
|
||||
// When receiver is $this/self/static, qualify with enclosing class for disambiguation
|
||||
const receiver = right.childForFieldName('object');
|
||||
const receiverText = receiver?.text;
|
||||
let receiverClassName: string | undefined;
|
||||
if (receiverText === '$this' || receiverText === 'self' || receiverText === 'static') {
|
||||
const cls = findEnclosingClass(node);
|
||||
const clsName = cls?.childForFieldName('name');
|
||||
if (clsName) receiverClassName = clsName.text;
|
||||
}
|
||||
return { varName: left.text, calleeName: methodName.text, receiverClassName };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Regex to extract PHPDoc @return annotations: `@return User` */
|
||||
const PHPDOC_RETURN_RE = /@return\s+(\S+)/;
|
||||
|
||||
/**
|
||||
* Extract return type from PHPDoc `@return Type` annotation preceding a method.
|
||||
* Walks backwards through preceding siblings looking for comment nodes.
|
||||
*/
|
||||
const extractReturnType: ReturnTypeExtractor = (node) => {
|
||||
let sibling = node.previousSibling;
|
||||
while (sibling) {
|
||||
if (sibling.type === 'comment') {
|
||||
const match = PHPDOC_RETURN_RE.exec(sibling.text);
|
||||
if (match) return normalizePhpType(match[1]);
|
||||
} else if (sibling.isNamed && !SKIP_NODE_TYPES.has(sibling.type)) break;
|
||||
sibling = sibling.previousSibling;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
extractInitializer,
|
||||
scanConstructorBinding,
|
||||
extractReturnType,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
|
|
@ -75,9 +75,37 @@ const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<str
|
|||
}
|
||||
};
|
||||
|
||||
/** Python: user = User("alice") — scan assignment/walrus for constructor-like calls.
|
||||
* Returns {varName, calleeName} without checking classNames (caller validates). */
|
||||
const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||
let left: SyntaxNode | null;
|
||||
let right: SyntaxNode | null;
|
||||
|
||||
if (node.type === 'named_expression') {
|
||||
left = node.childForFieldName('name');
|
||||
right = node.childForFieldName('value');
|
||||
} else if (node.type === 'assignment') {
|
||||
left = node.childForFieldName('left');
|
||||
right = node.childForFieldName('right');
|
||||
if (node.childForFieldName('type')) return undefined;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!left || !right) return undefined;
|
||||
if (left.type !== 'identifier') return undefined;
|
||||
if (right.type !== 'call') return undefined;
|
||||
const func = right.childForFieldName('function');
|
||||
if (!func) return undefined;
|
||||
const calleeName = extractSimpleTypeName(func);
|
||||
if (!calleeName) return undefined;
|
||||
return { varName: left.text, calleeName };
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
extractInitializer,
|
||||
scanConstructorBinding,
|
||||
};
|
||||
|
|
|
|||
271
gitnexus/src/core/ingestion/type-extractors/ruby.ts
Normal file
271
gitnexus/src/core/ingestion/type-extractors/ruby.ts
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor } from './types.js';
|
||||
import { extractRubyConstructorAssignment, extractSimpleTypeName } from './shared.js';
|
||||
import { SyntaxNode } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Ruby type extractor — YARD annotation parsing.
|
||||
*
|
||||
* Ruby has no static type system, but the YARD documentation convention
|
||||
* provides de facto type annotations via comments:
|
||||
*
|
||||
* # @param name [String] the user's name
|
||||
* # @param repo [UserRepo] the repository
|
||||
* # @return [User]
|
||||
* def create(name, repo)
|
||||
* repo.save
|
||||
* end
|
||||
*
|
||||
* This extractor parses `@param name [Type]` patterns from comment nodes
|
||||
* preceding method definitions and binds parameter names to their types.
|
||||
*
|
||||
* Resolution tiers:
|
||||
* - Tier 0: YARD @param annotations (extractDeclaration pre-populates env)
|
||||
* - Tier 1: Constructor inference via `user = User.new` (handled by scanConstructorBinding in typeConfig)
|
||||
*/
|
||||
|
||||
/** Regex to extract @param annotations: `@param name [Type]` */
|
||||
const YARD_PARAM_RE = /@param\s+(\w+)\s+\[([^\]]+)\]/g;
|
||||
/** Alternate YARD order: `@param [Type] name` */
|
||||
const YARD_PARAM_ALT_RE = /@param\s+\[([^\]]+)\]\s+(\w+)/g;
|
||||
|
||||
/** Regex to extract @return annotations: `@return [Type]` */
|
||||
const YARD_RETURN_RE = /@return\s+\[([^\]]+)\]/;
|
||||
|
||||
/**
|
||||
* Extract the simple type name from a YARD type string.
|
||||
* Handles:
|
||||
* - Simple types: "String" → "String"
|
||||
* - Qualified types: "Models::User" → "User"
|
||||
* - Generic types: "Array<User>" → "Array"
|
||||
* - Nullable types: "String, nil" → "String"
|
||||
* - Union types: "String, Integer" → undefined (ambiguous)
|
||||
*/
|
||||
const extractYardTypeName = (yardType: string): string | undefined => {
|
||||
const trimmed = yardType.trim();
|
||||
|
||||
// Handle nullable: "Type, nil" or "nil, Type"
|
||||
// Use bracket-balanced split to avoid breaking on commas inside generics like Hash<Symbol, User>
|
||||
const parts: string[] = [];
|
||||
let depth = 0, start = 0;
|
||||
for (let i = 0; i < trimmed.length; i++) {
|
||||
if (trimmed[i] === '<') depth++;
|
||||
else if (trimmed[i] === '>') depth--;
|
||||
else if (trimmed[i] === ',' && depth === 0) {
|
||||
parts.push(trimmed.slice(start, i).trim());
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
parts.push(trimmed.slice(start).trim());
|
||||
const filtered = parts.filter(p => p !== '' && p !== 'nil');
|
||||
if (filtered.length !== 1) return undefined; // ambiguous union
|
||||
|
||||
const typePart = filtered[0];
|
||||
|
||||
// Handle qualified: "Models::User" → "User"
|
||||
const segments = typePart.split('::');
|
||||
const last = segments[segments.length - 1];
|
||||
|
||||
// Handle generic: "Array<User>" → "Array"
|
||||
const genericMatch = last.match(/^(\w+)\s*[<{(]/);
|
||||
if (genericMatch) return genericMatch[1];
|
||||
|
||||
// Simple identifier check
|
||||
if (/^\w+$/.test(last)) return last;
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Collect YARD @param annotations from comment nodes preceding a method definition.
|
||||
* Returns a map of paramName → typeName.
|
||||
*
|
||||
* In tree-sitter-ruby, comments are sibling nodes that appear before the method node.
|
||||
* We walk backwards through preceding siblings collecting consecutive comment nodes.
|
||||
*/
|
||||
const collectYardParams = (methodNode: SyntaxNode): Map<string, string> => {
|
||||
const params = new Map<string, string>();
|
||||
|
||||
// In tree-sitter-ruby, YARD comments preceding a method inside a class body
|
||||
// are placed as children of the `class` node, NOT as siblings of the `method`
|
||||
// inside `body_statement`. The AST structure is:
|
||||
//
|
||||
// class
|
||||
// constant = "ClassName"
|
||||
// comment = "# @param ..." ← sibling of body_statement
|
||||
// comment = "# @param ..." ← sibling of body_statement
|
||||
// body_statement
|
||||
// method ← method is here, no preceding siblings
|
||||
//
|
||||
// For top-level methods (outside classes), comments ARE direct siblings.
|
||||
// We handle both by checking: if method has no preceding comment siblings,
|
||||
// look at parent (body_statement) siblings instead.
|
||||
const commentTexts: string[] = [];
|
||||
|
||||
const collectComments = (startNode: SyntaxNode): void => {
|
||||
let sibling = startNode.previousSibling;
|
||||
while (sibling) {
|
||||
if (sibling.type === 'comment') {
|
||||
commentTexts.unshift(sibling.text);
|
||||
} else if (sibling.isNamed) {
|
||||
break;
|
||||
}
|
||||
sibling = sibling.previousSibling;
|
||||
}
|
||||
};
|
||||
|
||||
// Try method's own siblings first (top-level methods)
|
||||
collectComments(methodNode);
|
||||
|
||||
// If no comments found and parent is body_statement, check parent's siblings
|
||||
if (commentTexts.length === 0 && methodNode.parent?.type === 'body_statement') {
|
||||
collectComments(methodNode.parent);
|
||||
}
|
||||
|
||||
// Parse all comment lines for @param annotations
|
||||
const commentBlock = commentTexts.join('\n');
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
// Reset regex state
|
||||
YARD_PARAM_RE.lastIndex = 0;
|
||||
while ((match = YARD_PARAM_RE.exec(commentBlock)) !== null) {
|
||||
const paramName = match[1];
|
||||
const rawType = match[2];
|
||||
const typeName = extractYardTypeName(rawType);
|
||||
if (typeName) {
|
||||
params.set(paramName, typeName);
|
||||
}
|
||||
}
|
||||
|
||||
// Also check alternate YARD order: @param [Type] name
|
||||
YARD_PARAM_ALT_RE.lastIndex = 0;
|
||||
while ((match = YARD_PARAM_ALT_RE.exec(commentBlock)) !== null) {
|
||||
const rawType = match[1];
|
||||
const paramName = match[2];
|
||||
if (params.has(paramName)) continue; // standard format takes priority
|
||||
const typeName = extractYardTypeName(rawType);
|
||||
if (typeName) {
|
||||
params.set(paramName, typeName);
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
/**
|
||||
* Ruby node types that may carry type bindings.
|
||||
* - `method`/`singleton_method`: YARD @param annotations (via extractDeclaration)
|
||||
* - `assignment`: Constructor inference like `user = User.new` (via extractInitializer;
|
||||
* extractDeclaration returns early for these nodes)
|
||||
*/
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'method',
|
||||
'singleton_method',
|
||||
'assignment',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Extract YARD annotations from method definitions.
|
||||
* Pre-populates the scope env with parameter types before the
|
||||
* standard parameter walk (which won't find types since Ruby has none).
|
||||
*/
|
||||
const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
|
||||
if (node.type !== 'method' && node.type !== 'singleton_method') return;
|
||||
|
||||
const yardParams = collectYardParams(node);
|
||||
if (yardParams.size === 0) return;
|
||||
|
||||
// Pre-populate env with YARD type bindings for each parameter
|
||||
for (const [paramName, typeName] of yardParams) {
|
||||
env.set(paramName, typeName);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Ruby parameter extraction.
|
||||
* Ruby parameters (identifiers inside method_parameters) have no inline
|
||||
* type annotations. YARD types are already populated by extractDeclaration,
|
||||
* so this is a no-op — the bindings are already in the env.
|
||||
*
|
||||
* We still register this to maintain the LanguageTypeConfig contract.
|
||||
*/
|
||||
const extractParameter: ParameterExtractor = (_node: SyntaxNode, _env: Map<string, string>): void => {
|
||||
// Ruby parameters have no type annotations.
|
||||
// YARD types are pre-populated by extractDeclaration.
|
||||
};
|
||||
|
||||
/**
|
||||
* Ruby constructor inference: user = User.new or service = Models::User.new
|
||||
* Uses the shared extractRubyConstructorAssignment helper for AST matching,
|
||||
* then resolves against locally-known class names.
|
||||
*/
|
||||
const extractInitializer: InitializerExtractor = (node, env, classNames): void => {
|
||||
const result = extractRubyConstructorAssignment(node);
|
||||
if (!result) return;
|
||||
if (env.has(result.varName)) return;
|
||||
if (classNames.has(result.calleeName)) {
|
||||
env.set(result.varName, result.calleeName);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract return type from YARD `@return [Type]` annotation preceding a method.
|
||||
* Reuses the same comment-walking strategy as collectYardParams: try direct
|
||||
* siblings first, fall back to parent (body_statement) siblings for class methods.
|
||||
*/
|
||||
const extractReturnType: ReturnTypeExtractor = (node) => {
|
||||
const search = (startNode: SyntaxNode): string | undefined => {
|
||||
let sibling = startNode.previousSibling;
|
||||
while (sibling) {
|
||||
if (sibling.type === 'comment') {
|
||||
const match = YARD_RETURN_RE.exec(sibling.text);
|
||||
if (match) return extractYardTypeName(match[1]);
|
||||
} else if (sibling.isNamed) {
|
||||
break;
|
||||
}
|
||||
sibling = sibling.previousSibling;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const result = search(node);
|
||||
if (result) return result;
|
||||
|
||||
if (node.parent?.type === 'body_statement') {
|
||||
return search(node.parent);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Ruby constructor binding scanner: captures both `user = User.new` and
|
||||
* plain call assignments like `user = get_user()`.
|
||||
* The `.new` pattern returns the class name directly; plain calls return the
|
||||
* callee name for return-type inference via SymbolTable lookup.
|
||||
*/
|
||||
const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||
// Try the .new pattern first (returns class name directly)
|
||||
const newResult = extractRubyConstructorAssignment(node);
|
||||
if (newResult) return newResult;
|
||||
|
||||
// Plain call assignment: user = get_user() / user = Models.create()
|
||||
if (node.type !== 'assignment') return undefined;
|
||||
const left = node.childForFieldName('left');
|
||||
const right = node.childForFieldName('right');
|
||||
if (!left || !right) return undefined;
|
||||
if (left.type !== 'identifier' && left.type !== 'constant') return undefined;
|
||||
if (right.type !== 'call') return undefined;
|
||||
const method = right.childForFieldName('method');
|
||||
if (!method) return undefined;
|
||||
const calleeName = extractSimpleTypeName(method);
|
||||
if (!calleeName) return undefined;
|
||||
return { varName: left.text, calleeName };
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
extractInitializer,
|
||||
scanConstructorBinding,
|
||||
extractReturnType,
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName } from './shared.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'let_declaration',
|
||||
|
|
@ -152,9 +152,39 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
|
|||
if (varName && typeName) env.set(varName, typeName);
|
||||
};
|
||||
|
||||
/** Rust: let user = get_user("alice") — let_declaration with call_expression value, no type annotation.
|
||||
* Skips `let user: User = ...` (explicit type annotation — handled by extractDeclaration).
|
||||
* Skips `let user = User::new()` (scoped_identifier callee named "new" — handled by extractInitializer).
|
||||
* Unwraps `let mut user = get_user()` by looking inside mut_pattern for the inner identifier.
|
||||
*/
|
||||
const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||
if (node.type !== 'let_declaration') return undefined;
|
||||
if (hasTypeAnnotation(node)) return undefined;
|
||||
let patternNode = node.childForFieldName('pattern');
|
||||
if (!patternNode) return undefined;
|
||||
if (patternNode.type === 'mut_pattern') {
|
||||
patternNode = patternNode.firstNamedChild;
|
||||
if (!patternNode) return undefined;
|
||||
}
|
||||
if (patternNode.type !== 'identifier') return undefined;
|
||||
// Unwrap `.await`: `let user = get_user().await` → await_expression wraps call_expression
|
||||
const value = unwrapAwait(node.childForFieldName('value'));
|
||||
if (!value || value.type !== 'call_expression') return undefined;
|
||||
const func = value.childForFieldName('function');
|
||||
if (!func) return undefined;
|
||||
if (func.type === 'scoped_identifier') {
|
||||
const methodName = func.lastNamedChild;
|
||||
if (methodName?.text === 'new' || methodName?.text === 'default') return undefined;
|
||||
}
|
||||
const calleeName = extractSimpleTypeName(func);
|
||||
if (!calleeName) return undefined;
|
||||
return { varName: patternNode.text, calleeName };
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
extractDeclaration,
|
||||
extractInitializer,
|
||||
extractParameter,
|
||||
scanConstructorBinding,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,20 +7,25 @@ import type { SyntaxNode } from '../utils.js';
|
|||
* Returns undefined for complex types (unions, intersections, function types).
|
||||
*/
|
||||
export const extractSimpleTypeName = (typeNode: SyntaxNode): string | undefined => {
|
||||
// Direct type identifier
|
||||
// Direct type identifier (includes Ruby 'constant' for class names)
|
||||
if (typeNode.type === 'type_identifier' || typeNode.type === 'identifier'
|
||||
|| typeNode.type === 'simple_identifier') {
|
||||
|| typeNode.type === 'simple_identifier' || typeNode.type === 'constant') {
|
||||
return typeNode.text;
|
||||
}
|
||||
|
||||
// Qualified/scoped names: take the last segment (e.g., models.User → User)
|
||||
// Qualified/scoped names: take the last segment (e.g., models.User → User, Models::User → User)
|
||||
if (typeNode.type === 'scoped_identifier' || typeNode.type === 'qualified_identifier'
|
||||
|| typeNode.type === 'scoped_type_identifier' || typeNode.type === 'qualified_name'
|
||||
|| typeNode.type === 'qualified_type'
|
||||
|| typeNode.type === 'member_expression' || typeNode.type === 'attribute') {
|
||||
|| typeNode.type === 'member_expression' || typeNode.type === 'member_access_expression'
|
||||
|| typeNode.type === 'attribute'
|
||||
|| typeNode.type === 'scope_resolution'
|
||||
|| typeNode.type === 'selector_expression') {
|
||||
const last = typeNode.lastNamedChild;
|
||||
if (last && (last.type === 'type_identifier' || last.type === 'identifier'
|
||||
|| last.type === 'simple_identifier' || last.type === 'name')) {
|
||||
|| last.type === 'simple_identifier' || last.type === 'name'
|
||||
|| last.type === 'constant' || last.type === 'property_identifier'
|
||||
|| last.type === 'field_identifier')) {
|
||||
return last.text;
|
||||
}
|
||||
}
|
||||
|
|
@ -95,7 +100,8 @@ export const extractSimpleTypeName = (typeNode: SyntaxNode): string | undefined
|
|||
*/
|
||||
export const extractVarName = (node: SyntaxNode): string | undefined => {
|
||||
if (node.type === 'identifier' || node.type === 'simple_identifier'
|
||||
|| node.type === 'variable_name' || node.type === 'name') {
|
||||
|| node.type === 'variable_name' || node.type === 'name'
|
||||
|| node.type === 'constant') {
|
||||
return node.text;
|
||||
}
|
||||
// variable_declarator (Java/C#): has a 'name' field
|
||||
|
|
@ -122,6 +128,130 @@ export const TYPED_PARAMETER_TYPES = new Set([
|
|||
'property_promotion_parameter', // PHP 8.0+ constructor promotion: __construct(private Foo $x)
|
||||
]);
|
||||
|
||||
/**
|
||||
* Extract type arguments from a generic type node.
|
||||
* e.g., List<User, String> → ['User', 'String'], Vec<User> → ['User']
|
||||
*
|
||||
* Handles language-specific AST structures:
|
||||
* - TS/Java/Rust/Go: generic_type > type_arguments > type nodes
|
||||
* - C#: generic_type > type_argument_list > type nodes
|
||||
* - Kotlin: generic_type > type_arguments > type_projection > type nodes
|
||||
*
|
||||
* Note: Go slices/maps use slice_type/map_type, not generic_type — those are
|
||||
* NOT handled here. Use language-specific extractors for Go container types.
|
||||
*
|
||||
* @param typeNode A generic_type or parameterized_type AST node (or any node —
|
||||
* returns [] for non-generic types).
|
||||
* @returns Array of resolved type argument names. Unresolvable arguments are omitted.
|
||||
*/
|
||||
export const extractGenericTypeArgs = (typeNode: SyntaxNode): string[] => {
|
||||
// Unwrap wrapper nodes that may sit above the generic_type
|
||||
if (typeNode.type === 'type_annotation' || typeNode.type === 'type'
|
||||
|| typeNode.type === 'user_type' || typeNode.type === 'nullable_type'
|
||||
|| typeNode.type === 'optional_type') {
|
||||
const inner = typeNode.firstNamedChild;
|
||||
if (inner) return extractGenericTypeArgs(inner);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Only process generic/parameterized type nodes
|
||||
if (typeNode.type !== 'generic_type' && typeNode.type !== 'parameterized_type') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Find the type_arguments / type_argument_list child
|
||||
let argsNode: SyntaxNode | null = null;
|
||||
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
||||
const child = typeNode.namedChild(i);
|
||||
if (child && (child.type === 'type_arguments' || child.type === 'type_argument_list')) {
|
||||
argsNode = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!argsNode) return [];
|
||||
|
||||
const result: string[] = [];
|
||||
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
||||
let argNode = argsNode.namedChild(i);
|
||||
if (!argNode) continue;
|
||||
|
||||
// Kotlin: type_arguments > type_projection > user_type > type_identifier
|
||||
if (argNode.type === 'type_projection') {
|
||||
argNode = argNode.firstNamedChild;
|
||||
if (!argNode) continue;
|
||||
}
|
||||
|
||||
const name = extractSimpleTypeName(argNode);
|
||||
if (name) result.push(name);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Match Ruby constructor assignment: `user = User.new` or `service = Models::User.new`.
|
||||
* Returns { varName, calleeName } or undefined if the node is not a Ruby constructor assignment.
|
||||
* Handles both simple constants and scope_resolution (namespaced) receivers.
|
||||
*/
|
||||
export const extractRubyConstructorAssignment = (
|
||||
node: SyntaxNode,
|
||||
): { varName: string; calleeName: string } | undefined => {
|
||||
if (node.type !== 'assignment') return undefined;
|
||||
const left = node.childForFieldName('left');
|
||||
const right = node.childForFieldName('right');
|
||||
if (!left || !right) return undefined;
|
||||
if (left.type !== 'identifier' && left.type !== 'constant') return undefined;
|
||||
if (right.type !== 'call') return undefined;
|
||||
const method = right.childForFieldName('method');
|
||||
if (!method || method.text !== 'new') return undefined;
|
||||
const receiver = right.childForFieldName('receiver');
|
||||
if (!receiver) return undefined;
|
||||
let calleeName: string;
|
||||
if (receiver.type === 'constant') {
|
||||
calleeName = receiver.text;
|
||||
} else if (receiver.type === 'scope_resolution') {
|
||||
// Models::User → extract last segment "User"
|
||||
const last = receiver.lastNamedChild;
|
||||
if (!last || last.type !== 'constant') return undefined;
|
||||
calleeName = last.text;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
return { varName: left.text, calleeName };
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if an AST node has an explicit type annotation.
|
||||
* Checks both named fields ('type') and child nodes ('type_annotation').
|
||||
* Used by constructor binding scanners to skip annotated declarations.
|
||||
*/
|
||||
export const hasTypeAnnotation = (node: SyntaxNode): boolean => {
|
||||
if (node.childForFieldName('type')) return true;
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
if (node.child(i)?.type === 'type_annotation') return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Unwrap an await_expression to get the inner value.
|
||||
* Returns the node itself if not an await_expression, or null if input is null.
|
||||
*/
|
||||
export const unwrapAwait = (node: SyntaxNode | null): SyntaxNode | null => {
|
||||
if (!node) return null;
|
||||
return node.type === 'await_expression' ? node.firstNamedChild : node;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract the callee name from a call_expression node.
|
||||
* Navigates to the 'function' field (or first named child) and extracts a simple type name.
|
||||
*/
|
||||
export const extractCalleeName = (callNode: SyntaxNode): string | undefined => {
|
||||
const func = callNode.childForFieldName('function') ?? callNode.firstNamedChild;
|
||||
if (!func) return undefined;
|
||||
return extractSimpleTypeName(func);
|
||||
};
|
||||
|
||||
/** Find the first named child with the given node type */
|
||||
export const findChildByType = (node: SyntaxNode, type: string): SyntaxNode | null => {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, findChildByType } from './shared.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, findChildByType, hasTypeAnnotation } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'property_declaration',
|
||||
|
|
@ -77,9 +77,51 @@ const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<str
|
|||
}
|
||||
};
|
||||
|
||||
/** Swift: let user = User(name: "alice") — scan property_declaration for constructor binding */
|
||||
const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||
if (node.type !== 'property_declaration') return undefined;
|
||||
if (hasTypeAnnotation(node)) return undefined;
|
||||
const pattern = node.childForFieldName('pattern');
|
||||
if (!pattern) return undefined;
|
||||
const varName = pattern.text;
|
||||
if (!varName) return undefined;
|
||||
let callExpr: SyntaxNode | null = null;
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child?.type === 'call_expression') { callExpr = child; break; }
|
||||
}
|
||||
if (!callExpr) return undefined;
|
||||
const callee = callExpr.firstNamedChild;
|
||||
if (!callee) return undefined;
|
||||
if (callee.type === 'simple_identifier') {
|
||||
return { varName, calleeName: callee.text };
|
||||
}
|
||||
if (callee.type === 'navigation_expression') {
|
||||
const receiver = callee.firstNamedChild;
|
||||
const suffix = callee.lastNamedChild;
|
||||
if (receiver?.type === 'simple_identifier' && suffix?.text === 'init') {
|
||||
return { varName, calleeName: receiver.text };
|
||||
}
|
||||
// General qualified call: service.getUser() → extract method name.
|
||||
// tree-sitter-swift may wrap the identifier in navigation_suffix, so
|
||||
// check both direct simple_identifier and navigation_suffix > simple_identifier.
|
||||
if (suffix?.type === 'simple_identifier') {
|
||||
return { varName, calleeName: suffix.text };
|
||||
}
|
||||
if (suffix?.type === 'navigation_suffix') {
|
||||
const inner = suffix.lastNamedChild;
|
||||
if (inner?.type === 'simple_identifier') {
|
||||
return { varName, calleeName: inner.text };
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
extractInitializer,
|
||||
scanConstructorBinding,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,6 +13,17 @@ export type ClassNameLookup = { has(name: string): boolean };
|
|||
/** Extracts type bindings from a constructor-call initializer, with access to known class names */
|
||||
export type InitializerExtractor = (node: SyntaxNode, env: Map<string, string>, classNames: ClassNameLookup) => void;
|
||||
|
||||
/** Scans an AST node for untyped `var = callee()` patterns for return-type inference.
|
||||
* Returns { varName, calleeName } if the node matches, undefined otherwise.
|
||||
* `receiverClassName` — optional hint for method calls on known receivers
|
||||
* (e.g. $this->getUser() in PHP provides the enclosing class name). */
|
||||
export type ConstructorBindingScanner = (node: SyntaxNode) => { varName: string; calleeName: string; receiverClassName?: string } | undefined;
|
||||
|
||||
/** Extracts a return type string from a method/function definition node.
|
||||
* Used for languages where return types are expressed in comments (e.g. YARD @return [Type])
|
||||
* rather than in AST fields. Returns undefined if no return type can be determined. */
|
||||
export type ReturnTypeExtractor = (node: SyntaxNode) => string | undefined;
|
||||
|
||||
/** Per-language type extraction configuration */
|
||||
export interface LanguageTypeConfig {
|
||||
/** Node types that represent typed declarations for this language */
|
||||
|
|
@ -26,4 +37,11 @@ export interface LanguageTypeConfig {
|
|||
* Only for languages with syntactic constructor markers (new, composite_literal, ::new).
|
||||
* Receives classNames — the set of class/struct names visible in the current file's AST. */
|
||||
extractInitializer?: InitializerExtractor;
|
||||
/** Scan for untyped `var = callee()` assignments for return-type inference.
|
||||
* Called on every AST node during buildTypeEnv walk; returns undefined for non-matches.
|
||||
* The callee binding is unverified — the caller must confirm against the SymbolTable. */
|
||||
scanConstructorBinding?: ConstructorBindingScanner;
|
||||
/** Extract return type from comment-based annotations (e.g. YARD @return [Type]).
|
||||
* Called as fallback when extractMethodSignature finds no AST-based return type. */
|
||||
extractReturnType?: ReturnTypeExtractor;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,85 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName } from './shared.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractCalleeName } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'lexical_declaration',
|
||||
'variable_declaration',
|
||||
'function_declaration', // JSDoc @param on function declarations
|
||||
'method_definition', // JSDoc @param on class methods
|
||||
]);
|
||||
|
||||
/** TypeScript: const x: Foo = ..., let x: Foo */
|
||||
const normalizeJsDocType = (raw: string): string | undefined => {
|
||||
let type = raw.trim();
|
||||
// Strip JSDoc nullable/non-nullable prefixes: ?User → User, !User → User
|
||||
if (type.startsWith('?') || type.startsWith('!')) type = type.slice(1);
|
||||
// Strip union with null/undefined/void: User|null → User
|
||||
const parts = type.split('|').map(p => p.trim()).filter(p =>
|
||||
p !== 'null' && p !== 'undefined' && p !== 'void'
|
||||
);
|
||||
if (parts.length !== 1) return undefined; // ambiguous union
|
||||
type = parts[0];
|
||||
// Strip module: prefix — module:models.User → models.User
|
||||
if (type.startsWith('module:')) type = type.slice(7);
|
||||
// Take last segment of dotted path: models.User → User
|
||||
const segments = type.split('.');
|
||||
type = segments[segments.length - 1];
|
||||
// Strip generic wrapper: Promise<User> → Promise (base type, not inner)
|
||||
const genericMatch = type.match(/^(\w+)\s*</);
|
||||
if (genericMatch) type = genericMatch[1];
|
||||
// Simple identifier check
|
||||
if (/^\w+$/.test(type)) return type;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Regex to extract JSDoc @param annotations: `@param {Type} name` */
|
||||
const JSDOC_PARAM_RE = /@param\s*\{([^}]+)\}\s+\[?(\w+)[\]=]?[^\s]*/g;
|
||||
|
||||
/**
|
||||
* Collect JSDoc @param type bindings from comment nodes preceding a function/method.
|
||||
* Returns a map of paramName → typeName.
|
||||
*/
|
||||
const collectJsDocParams = (funcNode: SyntaxNode): Map<string, string> => {
|
||||
const commentTexts: string[] = [];
|
||||
let sibling = funcNode.previousSibling;
|
||||
while (sibling) {
|
||||
if (sibling.type === 'comment') {
|
||||
commentTexts.unshift(sibling.text);
|
||||
} else if (sibling.isNamed && sibling.type !== 'decorator') {
|
||||
break;
|
||||
}
|
||||
sibling = sibling.previousSibling;
|
||||
}
|
||||
if (commentTexts.length === 0) return new Map();
|
||||
|
||||
const params = new Map<string, string>();
|
||||
const commentBlock = commentTexts.join('\n');
|
||||
JSDOC_PARAM_RE.lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = JSDOC_PARAM_RE.exec(commentBlock)) !== null) {
|
||||
const typeName = normalizeJsDocType(match[1]);
|
||||
const paramName = match[2];
|
||||
if (typeName) {
|
||||
params.set(paramName, typeName);
|
||||
}
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
/**
|
||||
* TypeScript: const x: Foo = ..., let x: Foo
|
||||
* Also: JSDoc @param annotations on function/method definitions (for .js files).
|
||||
*/
|
||||
const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
|
||||
// JSDoc @param on functions/methods — pre-populate env with param types
|
||||
if (node.type === 'function_declaration' || node.type === 'method_definition') {
|
||||
const jsDocParams = collectJsDocParams(node);
|
||||
for (const [paramName, typeName] of jsDocParams) {
|
||||
if (!env.has(paramName)) env.set(paramName, typeName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const declarator = node.namedChild(i);
|
||||
if (declarator?.type !== 'variable_declarator') continue;
|
||||
|
|
@ -65,9 +136,66 @@ const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<str
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* TypeScript/JavaScript: const user = getUser() — variable_declarator with call_expression value.
|
||||
* Only matches unannotated declarators; annotated ones are handled by extractDeclaration.
|
||||
* await is unwrapped: const user = await fetchUser() → callee = 'fetchUser'.
|
||||
*/
|
||||
const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||
if (node.type !== 'variable_declarator') return undefined;
|
||||
if (hasTypeAnnotation(node)) return undefined;
|
||||
const nameNode = node.childForFieldName('name');
|
||||
if (!nameNode || nameNode.type !== 'identifier') return undefined;
|
||||
const value = unwrapAwait(node.childForFieldName('value'));
|
||||
if (!value || value.type !== 'call_expression') return undefined;
|
||||
const calleeName = extractCalleeName(value);
|
||||
if (!calleeName) return undefined;
|
||||
return { varName: nameNode.text, calleeName };
|
||||
};
|
||||
|
||||
/** Regex to extract @returns or @return from JSDoc comments: `@returns {Type}` */
|
||||
const JSDOC_RETURN_RE = /@returns?\s*\{([^}]+)\}/;
|
||||
|
||||
/**
|
||||
* Minimal sanitization for JSDoc return types — preserves generic wrappers
|
||||
* (e.g. `Promise<User>`) so that extractReturnTypeName in call-processor
|
||||
* can apply WRAPPER_GENERICS unwrapping. Unlike normalizeJsDocType (which
|
||||
* strips generics), this only strips JSDoc-specific syntax markers.
|
||||
*/
|
||||
const sanitizeReturnType = (raw: string): string | undefined => {
|
||||
let type = raw.trim();
|
||||
// Strip JSDoc nullable/non-nullable prefixes: ?User → User, !User → User
|
||||
if (type.startsWith('?') || type.startsWith('!')) type = type.slice(1);
|
||||
// Strip module: prefix — module:models.User → models.User
|
||||
if (type.startsWith('module:')) type = type.slice(7);
|
||||
// Reject unions (ambiguous)
|
||||
if (type.includes('|')) return undefined;
|
||||
if (!type) return undefined;
|
||||
return type;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract return type from JSDoc `@returns {Type}` or `@return {Type}` annotation
|
||||
* preceding a function/method definition. Walks backwards through preceding siblings
|
||||
* looking for comment nodes containing the annotation.
|
||||
*/
|
||||
const extractReturnType: ReturnTypeExtractor = (node) => {
|
||||
let sibling = node.previousSibling;
|
||||
while (sibling) {
|
||||
if (sibling.type === 'comment') {
|
||||
const match = JSDOC_RETURN_RE.exec(sibling.text);
|
||||
if (match) return sanitizeReturnType(match[1]);
|
||||
} else if (sibling.isNamed && sibling.type !== 'decorator') break;
|
||||
sibling = sibling.previousSibling;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
extractInitializer,
|
||||
scanConstructorBinding,
|
||||
extractReturnType,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -618,9 +618,19 @@ export const extractMethodSignature = (node: SyntaxNode | null | undefined): Met
|
|||
// Go: 'result' field is either a type_identifier or parameter_list (multi-return)
|
||||
const goResult = node.childForFieldName?.('result');
|
||||
if (goResult) {
|
||||
returnType = goResult.type === 'parameter_list'
|
||||
? goResult.text // multi-return: "(string, error)"
|
||||
: goResult.text; // single return: "int"
|
||||
if (goResult.type === 'parameter_list') {
|
||||
// Multi-return: extract first parameter's type only (e.g. (*User, error) → *User)
|
||||
const firstParam = goResult.firstNamedChild;
|
||||
if (firstParam?.type === 'parameter_declaration') {
|
||||
const typeNode = firstParam.childForFieldName('type');
|
||||
if (typeNode) returnType = typeNode.text;
|
||||
} else if (firstParam) {
|
||||
// Unnamed return types: (string, error) — first child is a bare type node
|
||||
returnType = firstParam.text;
|
||||
}
|
||||
} else {
|
||||
returnType = goResult.text;
|
||||
}
|
||||
}
|
||||
|
||||
// Rust: 'return_type' field — the value IS the type node (e.g. primitive_type, type_identifier).
|
||||
|
|
@ -640,6 +650,14 @@ export const extractMethodSignature = (node: SyntaxNode | null | undefined): Met
|
|||
}
|
||||
}
|
||||
|
||||
// C#: 'returns' field on method_declaration
|
||||
if (!returnType) {
|
||||
const csReturn = node.childForFieldName?.('returns');
|
||||
if (csReturn && csReturn.text !== 'void') {
|
||||
returnType = csReturn.text;
|
||||
}
|
||||
}
|
||||
|
||||
// TS/Rust/Python/C#/Kotlin: type_annotation or return_type child
|
||||
if (!returnType) {
|
||||
for (const child of node.children) {
|
||||
|
|
@ -701,6 +719,7 @@ const MEMBER_ACCESS_NODE_TYPES = new Set([
|
|||
'field_expression', // Rust/C++: obj.method() / ptr->method()
|
||||
'selector_expression', // Go: obj.Method()
|
||||
'navigation_suffix', // Kotlin/Swift: obj.method() — nameNode sits inside navigation_suffix
|
||||
'member_binding_expression', // C#: user?.Method() — null-conditional access
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
@ -796,6 +815,7 @@ const SIMPLE_RECEIVER_TYPES = new Set([
|
|||
'super_expression', // Kotlin wraps super in super_expression
|
||||
'base', // C# base.Method()
|
||||
'parent', // PHP parent::method()
|
||||
'constant', // Ruby CONSTANT.method() (uppercase identifiers)
|
||||
]);
|
||||
|
||||
export const extractReceiverName = (
|
||||
|
|
@ -844,6 +864,14 @@ export const extractReceiverName = (
|
|||
}
|
||||
}
|
||||
|
||||
// C# null-conditional: user?.Save() → conditional_access_expression wraps member_binding_expression
|
||||
if (!receiver && parent.type === 'member_binding_expression') {
|
||||
const condAccess = parent.parent;
|
||||
if (condAccess?.type === 'conditional_access_expression') {
|
||||
receiver = condAccess.firstNamedChild;
|
||||
}
|
||||
}
|
||||
|
||||
// Kotlin/Swift: navigation_expression target is the first child
|
||||
if (!receiver && parent.type === 'navigation_suffix') {
|
||||
const navExpr = parent.parent;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import { buildTypeEnv } from '../type-env.js';
|
|||
import type { ConstructorBinding } from '../type-env.js';
|
||||
import { isNodeExported } from '../export-detection.js';
|
||||
import { detectFrameworkFromAST } from '../framework-detection.js';
|
||||
import { typeConfigs } from '../type-extractors/index.js';
|
||||
import { generateId } from '../../../lib/utils.js';
|
||||
import { extractNamedBindings } from '../named-binding-extraction.js';
|
||||
import { appendKotlinWildcard } from '../resolvers/index.js';
|
||||
|
|
@ -79,6 +80,7 @@ interface ParsedSymbol {
|
|||
nodeId: string;
|
||||
type: string;
|
||||
parameterCount?: number;
|
||||
returnType?: string;
|
||||
ownerId?: string;
|
||||
}
|
||||
|
||||
|
|
@ -1045,6 +1047,14 @@ const processFileGroup = (
|
|||
const sig = extractMethodSignature(definitionNode);
|
||||
parameterCount = sig.parameterCount;
|
||||
returnType = sig.returnType;
|
||||
|
||||
// Language-specific return type fallback (e.g. Ruby YARD @return [Type])
|
||||
if (!returnType && definitionNode) {
|
||||
const tc = typeConfigs[language as keyof typeof typeConfigs];
|
||||
if (tc?.extractReturnType) {
|
||||
returnType = tc.extractReturnType(definitionNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.nodes.push({
|
||||
|
|
@ -1078,6 +1088,7 @@ const processFileGroup = (
|
|||
nodeId,
|
||||
type: nodeLabel,
|
||||
...(parameterCount !== undefined ? { parameterCount } : {}),
|
||||
...(returnType !== undefined ? { returnType } : {}),
|
||||
...(enclosingClassId ? { ownerId: enclosingClassId } : {}),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -304,10 +304,11 @@ const fallbackRelationshipInserts = async (
|
|||
const confidence = parseFloat(confidenceStr) || 1.0;
|
||||
const step = parseInt(stepStr) || 0;
|
||||
|
||||
const esc = (s: string) => s.replace(/'/g, "''").replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/\r/g, '\\r');
|
||||
await conn.query(`
|
||||
MATCH (a:${escapeLabel(fromLabel)} {id: '${fromId.replace(/'/g, "''")}' }),
|
||||
(b:${escapeLabel(toLabel)} {id: '${toId.replace(/'/g, "''")}' })
|
||||
CREATE (a)-[:${REL_TABLE_NAME} {type: '${relType}', confidence: ${confidence}, reason: '${reason.replace(/'/g, "''")}', step: ${step}}]->(b)
|
||||
MATCH (a:${escapeLabel(fromLabel)} {id: '${esc(fromId)}' }),
|
||||
(b:${escapeLabel(toLabel)} {id: '${esc(toId)}' })
|
||||
CREATE (a)-[:${REL_TABLE_NAME} {type: '${esc(relType)}', confidence: ${confidence}, reason: '${esc(reason)}', step: ${step}}]->(b)
|
||||
`);
|
||||
} catch {
|
||||
// skip
|
||||
|
|
@ -365,7 +366,7 @@ export const insertNodeToLbug = async (
|
|||
if (v === null || v === undefined) return 'NULL';
|
||||
if (typeof v === 'number') return String(v);
|
||||
// Escape backslashes first (for Windows paths), then single quotes
|
||||
return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "''")}'`;
|
||||
return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "''").replace(/\n/g, '\\n').replace(/\r/g, '\\r')}'`;
|
||||
};
|
||||
|
||||
// Build INSERT query based on node type
|
||||
|
|
@ -425,8 +426,8 @@ export const batchInsertNodesToLbug = async (
|
|||
const escapeValue = (v: any): string => {
|
||||
if (v === null || v === undefined) return 'NULL';
|
||||
if (typeof v === 'number') return String(v);
|
||||
// Escape backslashes first (for Windows paths), then single quotes
|
||||
return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "''")}'`;
|
||||
// Escape backslashes first (for Windows paths), then single quotes, then newlines
|
||||
return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "''").replace(/\n/g, '\\n').replace(/\r/g, '\\r')}'`;
|
||||
};
|
||||
|
||||
// Open a single connection for all inserts
|
||||
|
|
|
|||
20
gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/app.cpp
vendored
Normal file
20
gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#include "user.h"
|
||||
#include "repo.h"
|
||||
|
||||
User getUser(const char* name) {
|
||||
return User(name);
|
||||
}
|
||||
|
||||
Repo getRepo(const char* name) {
|
||||
return Repo(name);
|
||||
}
|
||||
|
||||
void processUser() {
|
||||
auto user = getUser("alice");
|
||||
user.save();
|
||||
}
|
||||
|
||||
void processRepo() {
|
||||
auto repo = getRepo("main");
|
||||
repo.save();
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/repo.h
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/repo.h
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
class Repo {
|
||||
public:
|
||||
Repo(const char* name) : name_(name) {}
|
||||
bool save() { return true; }
|
||||
private:
|
||||
const char* name_;
|
||||
};
|
||||
9
gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/user.h
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/user.h
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
class User {
|
||||
public:
|
||||
User(const char* name) : name_(name) {}
|
||||
bool save() { return true; }
|
||||
private:
|
||||
const char* name_;
|
||||
};
|
||||
10
gitnexus/test/fixtures/lang-resolution/cpp-return-type/app.cpp
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/cpp-return-type/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#include "user.h"
|
||||
|
||||
User getUser(const char* name) {
|
||||
return User(name);
|
||||
}
|
||||
|
||||
void processUser() {
|
||||
auto user = getUser("alice");
|
||||
user.save();
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/cpp-return-type/user.h
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/cpp-return-type/user.h
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
class User {
|
||||
public:
|
||||
User(const char* name) : name_(name) {}
|
||||
void save() {}
|
||||
private:
|
||||
const char* name_;
|
||||
};
|
||||
|
||||
User getUser(const char* name);
|
||||
11
gitnexus/test/fixtures/lang-resolution/csharp-async-binding/Order.cs
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/csharp-async-binding/Order.cs
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
namespace CSharpAsyncBinding;
|
||||
|
||||
public class Order
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public void Save()
|
||||
{
|
||||
// persist order
|
||||
}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/csharp-async-binding/OrderService.cs
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/csharp-async-binding/OrderService.cs
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace CSharpAsyncBinding;
|
||||
|
||||
public class OrderService
|
||||
{
|
||||
public async Task<Order> GetOrderAsync(string name)
|
||||
{
|
||||
return new Order { Name = name };
|
||||
}
|
||||
}
|
||||
24
gitnexus/test/fixtures/lang-resolution/csharp-async-binding/Program.cs
vendored
Normal file
24
gitnexus/test/fixtures/lang-resolution/csharp-async-binding/Program.cs
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
namespace CSharpAsyncBinding;
|
||||
|
||||
public class Program
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
var userSvc = new UserService();
|
||||
var orderSvc = new OrderService();
|
||||
await ProcessUser(userSvc);
|
||||
await ProcessOrder(orderSvc);
|
||||
}
|
||||
|
||||
public static async Task ProcessUser(UserService userSvc)
|
||||
{
|
||||
var user = await userSvc.GetUserAsync("alice");
|
||||
user.Save();
|
||||
}
|
||||
|
||||
public static async Task ProcessOrder(OrderService orderSvc)
|
||||
{
|
||||
var order = await orderSvc.GetOrderAsync("bob");
|
||||
order.Save();
|
||||
}
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/csharp-async-binding/User.cs
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/csharp-async-binding/User.cs
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
namespace CSharpAsyncBinding;
|
||||
|
||||
public class User
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public void Save()
|
||||
{
|
||||
// persist user
|
||||
}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/csharp-async-binding/UserService.cs
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/csharp-async-binding/UserService.cs
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace CSharpAsyncBinding;
|
||||
|
||||
public class UserService
|
||||
{
|
||||
public async Task<User> GetUserAsync(string name)
|
||||
{
|
||||
return new User { Name = name };
|
||||
}
|
||||
}
|
||||
16
gitnexus/test/fixtures/lang-resolution/csharp-null-conditional/App.cs
vendored
Normal file
16
gitnexus/test/fixtures/lang-resolution/csharp-null-conditional/App.cs
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
using Models;
|
||||
|
||||
namespace App;
|
||||
|
||||
public class AppService
|
||||
{
|
||||
public void Process()
|
||||
{
|
||||
User user = new User();
|
||||
Repo repo = new Repo();
|
||||
|
||||
// Null-conditional calls — should disambiguate via receiver type
|
||||
user?.Save();
|
||||
repo?.Save();
|
||||
}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/csharp-null-conditional/Models/Repo.cs
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/csharp-null-conditional/Models/Repo.cs
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace Models;
|
||||
|
||||
public class Repo
|
||||
{
|
||||
public bool Save()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/csharp-null-conditional/Models/User.cs
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/csharp-null-conditional/Models/User.cs
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace Models;
|
||||
|
||||
public class User
|
||||
{
|
||||
public bool Save()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/csharp-null-conditional/NullConditional.csproj
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/csharp-null-conditional/NullConditional.csproj
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
9
gitnexus/test/fixtures/lang-resolution/csharp-return-type/Models/Repo.cs
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/csharp-return-type/Models/Repo.cs
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace ReturnType.Models;
|
||||
|
||||
public class Repo
|
||||
{
|
||||
public bool Save()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
24
gitnexus/test/fixtures/lang-resolution/csharp-return-type/Models/User.cs
vendored
Normal file
24
gitnexus/test/fixtures/lang-resolution/csharp-return-type/Models/User.cs
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
namespace ReturnType.Models;
|
||||
|
||||
public class User
|
||||
{
|
||||
private string _name;
|
||||
|
||||
public User(string name)
|
||||
{
|
||||
_name = name;
|
||||
}
|
||||
|
||||
public bool Save()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class UserService
|
||||
{
|
||||
public User GetUser(string name)
|
||||
{
|
||||
return new User(name);
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/csharp-return-type/ReturnType.csproj
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/csharp-return-type/ReturnType.csproj
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
13
gitnexus/test/fixtures/lang-resolution/csharp-return-type/Services/App.cs
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/csharp-return-type/Services/App.cs
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using ReturnType.Models;
|
||||
|
||||
namespace ReturnType.Services;
|
||||
|
||||
public class App
|
||||
{
|
||||
public void Run()
|
||||
{
|
||||
var svc = new UserService();
|
||||
var user = svc.GetUser("alice");
|
||||
user.Save();
|
||||
}
|
||||
}
|
||||
24
gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/cmd/main.go
vendored
Normal file
24
gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/cmd/main.go
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package main
|
||||
|
||||
import "example.com/multireturn/models"
|
||||
|
||||
func NewUser(name string) (*models.User, error) {
|
||||
return &models.User{Name: name}, nil
|
||||
}
|
||||
|
||||
func NewRepo(name string) (*models.Repo, error) {
|
||||
return &models.Repo{Name: name}, nil
|
||||
}
|
||||
|
||||
func processUser() {
|
||||
user, err := NewUser("alice")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
user.Save()
|
||||
}
|
||||
|
||||
func processRepo() {
|
||||
repo, _ := NewRepo("main")
|
||||
repo.Save()
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/go.mod
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/go.mod
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module example.com/multireturn
|
||||
|
||||
go 1.21
|
||||
9
gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/models/repo.go
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/models/repo.go
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package models
|
||||
|
||||
type Repo struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (r *Repo) Save() bool {
|
||||
return true
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/models/user.go
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/models/user.go
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package models
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (u *User) Save() bool {
|
||||
return true
|
||||
}
|
||||
27
gitnexus/test/fixtures/lang-resolution/go-return-type-inference/cmd/main.go
vendored
Normal file
27
gitnexus/test/fixtures/lang-resolution/go-return-type-inference/cmd/main.go
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package main
|
||||
|
||||
import "example.com/returntype/models"
|
||||
|
||||
func GetUser(name string) *models.User {
|
||||
return &models.User{Name: name}
|
||||
}
|
||||
|
||||
func processUser() {
|
||||
user := GetUser("alice")
|
||||
user.Save()
|
||||
}
|
||||
|
||||
// Cross-package factory call: models.NewUser() uses selector_expression in the AST
|
||||
func processUserCrossPackage() {
|
||||
user := models.NewUser("bob")
|
||||
user.Save()
|
||||
}
|
||||
|
||||
func GetRepo(name string) *models.Repo {
|
||||
return &models.Repo{Name: name}
|
||||
}
|
||||
|
||||
func processRepo() {
|
||||
repo := GetRepo("main")
|
||||
repo.Save()
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/go-return-type-inference/go.mod
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/go-return-type-inference/go.mod
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module example.com/returntype
|
||||
|
||||
go 1.21
|
||||
13
gitnexus/test/fixtures/lang-resolution/go-return-type-inference/models/repo.go
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/go-return-type-inference/models/repo.go
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package models
|
||||
|
||||
type Repo struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (r *Repo) Save() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func GetRepo(name string) *Repo {
|
||||
return &Repo{Name: name}
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/go-return-type-inference/models/user.go
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/go-return-type-inference/models/user.go
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package models
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (u *User) Save() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func NewUser(name string) *User {
|
||||
return &User{Name: name}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/java-return-type-inference/App.java
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/java-return-type-inference/App.java
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import services.UserService;
|
||||
|
||||
public class App {
|
||||
public static void processUser() {
|
||||
UserService svc = new UserService();
|
||||
var user = svc.getUser("alice");
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/java-return-type-inference/models/User.java
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/java-return-type-inference/models/User.java
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package models;
|
||||
|
||||
public class User {
|
||||
private String name;
|
||||
|
||||
public User(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public boolean save() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/java-return-type-inference/services/UserService.java
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/java-return-type-inference/services/UserService.java
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package services;
|
||||
|
||||
import models.User;
|
||||
|
||||
public class UserService {
|
||||
public User getUser(String name) {
|
||||
return new User(name);
|
||||
}
|
||||
}
|
||||
26
gitnexus/test/fixtures/lang-resolution/js-jsdoc-async-return-type/app.js
vendored
Normal file
26
gitnexus/test/fixtures/lang-resolution/js-jsdoc-async-return-type/app.js
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
const { User } = require('./user');
|
||||
const { Repo } = require('./repo');
|
||||
|
||||
/**
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
async function fetchUser(name) {
|
||||
return new User(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Repo>}
|
||||
*/
|
||||
async function fetchRepo(path) {
|
||||
return new Repo(path);
|
||||
}
|
||||
|
||||
async function processUser() {
|
||||
const user = await fetchUser('alice');
|
||||
user.save();
|
||||
}
|
||||
|
||||
async function processRepo() {
|
||||
const repo = await fetchRepo('/data');
|
||||
repo.save();
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/js-jsdoc-async-return-type/repo.js
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/js-jsdoc-async-return-type/repo.js
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
class Repo {
|
||||
constructor(path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
save() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Repo };
|
||||
11
gitnexus/test/fixtures/lang-resolution/js-jsdoc-async-return-type/user.js
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/js-jsdoc-async-return-type/user.js
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
class User {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
save() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { User };
|
||||
13
gitnexus/test/fixtures/lang-resolution/js-jsdoc-qualified-return-type/app.js
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/js-jsdoc-qualified-return-type/app.js
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
const { User } = require('./user');
|
||||
|
||||
/**
|
||||
* @returns {Promise<models.User>}
|
||||
*/
|
||||
async function fetchUser(name) {
|
||||
return new User(name);
|
||||
}
|
||||
|
||||
async function processUser() {
|
||||
const user = await fetchUser('alice');
|
||||
user.save();
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/js-jsdoc-qualified-return-type/user.js
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/js-jsdoc-qualified-return-type/user.js
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
class User {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
save() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { User };
|
||||
40
gitnexus/test/fixtures/lang-resolution/js-jsdoc-return-type/app.js
vendored
Normal file
40
gitnexus/test/fixtures/lang-resolution/js-jsdoc-return-type/app.js
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
const { User } = require('./user');
|
||||
const { Repo } = require('./repo');
|
||||
|
||||
/**
|
||||
* @returns {User}
|
||||
*/
|
||||
function getUser(name) {
|
||||
return new User(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Repo}
|
||||
*/
|
||||
function getRepo(path) {
|
||||
return new Repo(path);
|
||||
}
|
||||
|
||||
function processUser() {
|
||||
const user = getUser('alice');
|
||||
user.save();
|
||||
}
|
||||
|
||||
function processRepo() {
|
||||
const repo = getRepo('/data');
|
||||
repo.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {User} user the user to handle
|
||||
*/
|
||||
function handleUser(user) {
|
||||
user.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Repo} repo the repo to handle
|
||||
*/
|
||||
function handleRepo(repo) {
|
||||
repo.save();
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/js-jsdoc-return-type/repo.js
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/js-jsdoc-return-type/repo.js
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
class Repo {
|
||||
constructor(path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
save() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Repo };
|
||||
11
gitnexus/test/fixtures/lang-resolution/js-jsdoc-return-type/user.js
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/js-jsdoc-return-type/user.js
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
class User {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
save() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { User };
|
||||
9
gitnexus/test/fixtures/lang-resolution/kotlin-return-type/models/Repo.kt
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/kotlin-return-type/models/Repo.kt
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package models
|
||||
|
||||
class Repo(val name: String) {
|
||||
fun save() {}
|
||||
}
|
||||
|
||||
fun getRepo(name: String): Repo {
|
||||
return Repo(name)
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/kotlin-return-type/models/User.kt
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/kotlin-return-type/models/User.kt
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package models
|
||||
|
||||
class User(val name: String) {
|
||||
fun save() {}
|
||||
}
|
||||
|
||||
fun getUser(name: String): User {
|
||||
return User(name)
|
||||
}
|
||||
14
gitnexus/test/fixtures/lang-resolution/kotlin-return-type/services/App.kt
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/kotlin-return-type/services/App.kt
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package services
|
||||
|
||||
import models.getUser
|
||||
import models.getRepo
|
||||
|
||||
fun processUser() {
|
||||
val user = getUser("alice")
|
||||
user.save()
|
||||
}
|
||||
|
||||
fun processRepo() {
|
||||
val repo = getRepo("main")
|
||||
repo.save()
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/php-phpdoc-attribute-return-type/Models.php
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/php-phpdoc-attribute-return-type/Models.php
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
class User {
|
||||
public function save() { return true; }
|
||||
}
|
||||
|
||||
class Repo {
|
||||
public function save() { return true; }
|
||||
}
|
||||
46
gitnexus/test/fixtures/lang-resolution/php-phpdoc-attribute-return-type/Services.php
vendored
Normal file
46
gitnexus/test/fixtures/lang-resolution/php-phpdoc-attribute-return-type/Services.php
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
require_once 'Models.php';
|
||||
|
||||
class UserService {
|
||||
/**
|
||||
* @return User
|
||||
*/
|
||||
#[Route('/user')]
|
||||
public function getUser(string $name) {
|
||||
return new User();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Repo
|
||||
*/
|
||||
#[Route('/repo')]
|
||||
public function getRepo(string $path) {
|
||||
return new Repo();
|
||||
}
|
||||
|
||||
public function processUser() {
|
||||
$user = $this->getUser("alice");
|
||||
$user->save();
|
||||
}
|
||||
|
||||
public function processRepo() {
|
||||
$repo = $this->getRepo("/data");
|
||||
$repo->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user the user to handle
|
||||
*/
|
||||
#[Validate]
|
||||
public function handleUser($user) {
|
||||
$user->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Repo $repo the repo to handle
|
||||
*/
|
||||
#[Validate]
|
||||
public function handleRepo($repo) {
|
||||
$repo->save();
|
||||
}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/php-phpdoc-return-type/Models.php
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/php-phpdoc-return-type/Models.php
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
class User {
|
||||
public function save() { return true; }
|
||||
}
|
||||
|
||||
class Repo {
|
||||
public function save() { return true; }
|
||||
}
|
||||
42
gitnexus/test/fixtures/lang-resolution/php-phpdoc-return-type/Services.php
vendored
Normal file
42
gitnexus/test/fixtures/lang-resolution/php-phpdoc-return-type/Services.php
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
require_once 'Models.php';
|
||||
|
||||
class UserService {
|
||||
/**
|
||||
* @return User
|
||||
*/
|
||||
public function getUser(string $name) {
|
||||
return new User();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Repo
|
||||
*/
|
||||
public function getRepo(string $path) {
|
||||
return new Repo();
|
||||
}
|
||||
|
||||
public function processUser() {
|
||||
$user = $this->getUser("alice");
|
||||
$user->save();
|
||||
}
|
||||
|
||||
public function processRepo() {
|
||||
$repo = $this->getRepo("/data");
|
||||
$repo->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user the user to handle
|
||||
*/
|
||||
public function handleUser($user) {
|
||||
$user->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Repo $repo the repo to handle
|
||||
*/
|
||||
public function handleRepo($repo) {
|
||||
$repo->save();
|
||||
}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/php-return-type/app/Models/Repo.php
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/php-return-type/app/Models/Repo.php
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class Repo {
|
||||
public function save(): bool {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
15
gitnexus/test/fixtures/lang-resolution/php-return-type/app/Models/User.php
vendored
Normal file
15
gitnexus/test/fixtures/lang-resolution/php-return-type/app/Models/User.php
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class User {
|
||||
private string $name;
|
||||
|
||||
public function __construct(string $name) {
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function save(): bool {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
16
gitnexus/test/fixtures/lang-resolution/php-return-type/app/Services/UserService.php
vendored
Normal file
16
gitnexus/test/fixtures/lang-resolution/php-return-type/app/Services/UserService.php
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class UserService {
|
||||
public function getUser(string $name): User {
|
||||
return new User($name);
|
||||
}
|
||||
|
||||
public function processUser(): void {
|
||||
$user = $this->getUser("alice");
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
14
gitnexus/test/fixtures/lang-resolution/php-this-receiver-disambiguation/AdminService.php
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/php-this-receiver-disambiguation/AdminService.php
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
require_once 'Models.php';
|
||||
|
||||
class AdminService {
|
||||
/** @return Repo */
|
||||
public function getUser(string $name) {
|
||||
return new Repo();
|
||||
}
|
||||
|
||||
public function processAdmin() {
|
||||
$repo = $this->getUser("admin");
|
||||
$repo->save();
|
||||
}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/php-this-receiver-disambiguation/Models.php
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/php-this-receiver-disambiguation/Models.php
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
class User {
|
||||
public function save() { return true; }
|
||||
}
|
||||
|
||||
class Repo {
|
||||
public function save() { return true; }
|
||||
}
|
||||
14
gitnexus/test/fixtures/lang-resolution/php-this-receiver-disambiguation/UserService.php
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/php-this-receiver-disambiguation/UserService.php
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
require_once 'Models.php';
|
||||
|
||||
class UserService {
|
||||
/** @return User */
|
||||
public function getUser(string $name) {
|
||||
return new User();
|
||||
}
|
||||
|
||||
public function processUser() {
|
||||
$user = $this->getUser("alice");
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/python-return-type-inference/app.py
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/python-return-type-inference/app.py
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from service import get_user
|
||||
|
||||
def process_user():
|
||||
user = get_user('alice')
|
||||
user.save()
|
||||
6
gitnexus/test/fixtures/lang-resolution/python-return-type-inference/models.py
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/python-return-type-inference/models.py
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
class User:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
def save(self) -> bool:
|
||||
return True
|
||||
4
gitnexus/test/fixtures/lang-resolution/python-return-type-inference/service.py
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/python-return-type-inference/service.py
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from models import User
|
||||
|
||||
def get_user(name: str) -> User:
|
||||
return User(name)
|
||||
10
gitnexus/test/fixtures/lang-resolution/python-static-class-methods/app.py
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/python-static-class-methods/app.py
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from service import UserService, AdminService
|
||||
|
||||
|
||||
def process():
|
||||
user = UserService.find_user("alice")
|
||||
UserService.create_user("bob")
|
||||
svc = UserService.from_config({})
|
||||
|
||||
AdminService.find_user("charlie")
|
||||
AdminService.delete_user("charlie")
|
||||
22
gitnexus/test/fixtures/lang-resolution/python-static-class-methods/service.py
vendored
Normal file
22
gitnexus/test/fixtures/lang-resolution/python-static-class-methods/service.py
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
class UserService:
|
||||
@staticmethod
|
||||
def find_user(name: str) -> str:
|
||||
return name
|
||||
|
||||
@staticmethod
|
||||
def create_user(name: str) -> str:
|
||||
return name
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: dict) -> "UserService":
|
||||
return cls()
|
||||
|
||||
|
||||
class AdminService:
|
||||
@staticmethod
|
||||
def find_user(name: str) -> str:
|
||||
return name
|
||||
|
||||
@staticmethod
|
||||
def delete_user(name: str) -> bool:
|
||||
return True
|
||||
9
gitnexus/test/fixtures/lang-resolution/ruby-constant-factory-call/admin_service.rb
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/ruby-constant-factory-call/admin_service.rb
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
class AdminService
|
||||
def process
|
||||
true
|
||||
end
|
||||
|
||||
def validate
|
||||
true
|
||||
end
|
||||
end
|
||||
11
gitnexus/test/fixtures/lang-resolution/ruby-constant-factory-call/app.rb
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/ruby-constant-factory-call/app.rb
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
require_relative 'user_service'
|
||||
require_relative 'admin_service'
|
||||
|
||||
# @return [UserService]
|
||||
def build_service
|
||||
UserService.new
|
||||
end
|
||||
|
||||
SERVICE = build_service()
|
||||
SERVICE.process
|
||||
SERVICE.validate
|
||||
9
gitnexus/test/fixtures/lang-resolution/ruby-constant-factory-call/user_service.rb
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/ruby-constant-factory-call/user_service.rb
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
class UserService
|
||||
def process
|
||||
true
|
||||
end
|
||||
|
||||
def validate
|
||||
true
|
||||
end
|
||||
end
|
||||
5
gitnexus/test/fixtures/lang-resolution/ruby-namespaced-constructor/app.rb
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/ruby-namespaced-constructor/app.rb
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
require_relative 'models/user_service'
|
||||
|
||||
svc = Models::UserService.new
|
||||
svc.process('alice')
|
||||
svc.validate
|
||||
11
gitnexus/test/fixtures/lang-resolution/ruby-namespaced-constructor/models/user_service.rb
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/ruby-namespaced-constructor/models/user_service.rb
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
module Models
|
||||
class UserService
|
||||
def process(name)
|
||||
name.upcase
|
||||
end
|
||||
|
||||
def validate
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
12
gitnexus/test/fixtures/lang-resolution/ruby-return-type/app.rb
vendored
Normal file
12
gitnexus/test/fixtures/lang-resolution/ruby-return-type/app.rb
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
require_relative 'models'
|
||||
require_relative 'repo'
|
||||
|
||||
def process_user
|
||||
user = get_user('alice')
|
||||
user.save
|
||||
end
|
||||
|
||||
def process_repo
|
||||
repo = get_repo('/data')
|
||||
repo.save
|
||||
end
|
||||
14
gitnexus/test/fixtures/lang-resolution/ruby-return-type/models.rb
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/ruby-return-type/models.rb
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
class User
|
||||
def initialize(name)
|
||||
@name = name
|
||||
end
|
||||
|
||||
def save
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
# @return [User]
|
||||
def get_user(name)
|
||||
User.new(name)
|
||||
end
|
||||
14
gitnexus/test/fixtures/lang-resolution/ruby-return-type/repo.rb
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/ruby-return-type/repo.rb
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
class Repo
|
||||
def initialize(path)
|
||||
@path = path
|
||||
end
|
||||
|
||||
def save
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
# @return [Repo]
|
||||
def get_repo(path)
|
||||
Repo.new(path)
|
||||
end
|
||||
15
gitnexus/test/fixtures/lang-resolution/ruby-yard-annotations/models.rb
vendored
Normal file
15
gitnexus/test/fixtures/lang-resolution/ruby-yard-annotations/models.rb
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
class UserRepo
|
||||
def save
|
||||
true
|
||||
end
|
||||
|
||||
def find_by_name(name)
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
class User
|
||||
def greet
|
||||
"hello"
|
||||
end
|
||||
end
|
||||
11
gitnexus/test/fixtures/lang-resolution/ruby-yard-annotations/service.rb
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/ruby-yard-annotations/service.rb
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
require_relative './models'
|
||||
|
||||
class UserService
|
||||
# @param repo [UserRepo] the repository
|
||||
# @param user [User] the user to create
|
||||
# @return [Boolean]
|
||||
def create(repo, user)
|
||||
repo.save
|
||||
user.greet
|
||||
end
|
||||
end
|
||||
19
gitnexus/test/fixtures/lang-resolution/ruby-yard-generics/models.rb
vendored
Normal file
19
gitnexus/test/fixtures/lang-resolution/ruby-yard-generics/models.rb
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
class UserRepo
|
||||
def save
|
||||
true
|
||||
end
|
||||
|
||||
def find_all
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
class AdminRepo
|
||||
def save
|
||||
true
|
||||
end
|
||||
|
||||
def find_all
|
||||
[]
|
||||
end
|
||||
end
|
||||
16
gitnexus/test/fixtures/lang-resolution/ruby-yard-generics/service.rb
vendored
Normal file
16
gitnexus/test/fixtures/lang-resolution/ruby-yard-generics/service.rb
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
require_relative './models'
|
||||
|
||||
class DataService
|
||||
# @param repo [UserRepo] the user repository
|
||||
# @param cache [Hash<Symbol, UserRepo>] cache of repos by symbol key
|
||||
def sync(repo, cache)
|
||||
repo.save
|
||||
repo.find_all
|
||||
end
|
||||
|
||||
# @param [AdminRepo] admin_repo the admin repository (alternate YARD order)
|
||||
def audit(admin_repo)
|
||||
admin_repo.save
|
||||
admin_repo.find_all
|
||||
end
|
||||
end
|
||||
23
gitnexus/test/fixtures/lang-resolution/rust-async-binding/src/main.rs
vendored
Normal file
23
gitnexus/test/fixtures/lang-resolution/rust-async-binding/src/main.rs
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
mod user;
|
||||
mod repo;
|
||||
|
||||
use user::User;
|
||||
use repo::Repo;
|
||||
|
||||
async fn get_user() -> User {
|
||||
User { name: String::from("alice") }
|
||||
}
|
||||
|
||||
async fn get_repo() -> Repo {
|
||||
Repo { name: String::from("main") }
|
||||
}
|
||||
|
||||
async fn process_user() {
|
||||
let user = get_user().await;
|
||||
user.save();
|
||||
}
|
||||
|
||||
async fn process_repo() {
|
||||
let repo = get_repo().await;
|
||||
repo.save();
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/rust-async-binding/src/repo.rs
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/rust-async-binding/src/repo.rs
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
pub struct Repo {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Repo {
|
||||
pub fn save(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/rust-async-binding/src/user.rs
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/rust-async-binding/src/user.rs
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
pub struct User {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn save(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
20
gitnexus/test/fixtures/lang-resolution/rust-default-constructor/src/main.rs
vendored
Normal file
20
gitnexus/test/fixtures/lang-resolution/rust-default-constructor/src/main.rs
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
mod user;
|
||||
mod repo;
|
||||
use crate::user::User;
|
||||
use crate::repo::Repo;
|
||||
|
||||
fn process_with_new() {
|
||||
let user = User::new();
|
||||
let repo = Repo::new();
|
||||
user.save();
|
||||
repo.save();
|
||||
}
|
||||
|
||||
fn process_with_default() {
|
||||
let user = User::default();
|
||||
let repo = Repo::default();
|
||||
user.save();
|
||||
repo.save();
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
17
gitnexus/test/fixtures/lang-resolution/rust-default-constructor/src/repo.rs
vendored
Normal file
17
gitnexus/test/fixtures/lang-resolution/rust-default-constructor/src/repo.rs
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
pub struct Repo;
|
||||
|
||||
impl Repo {
|
||||
pub fn new() -> Self {
|
||||
Repo
|
||||
}
|
||||
|
||||
pub fn save(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Repo {
|
||||
fn default() -> Self {
|
||||
Repo
|
||||
}
|
||||
}
|
||||
17
gitnexus/test/fixtures/lang-resolution/rust-default-constructor/src/user.rs
vendored
Normal file
17
gitnexus/test/fixtures/lang-resolution/rust-default-constructor/src/user.rs
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
pub struct User;
|
||||
|
||||
impl User {
|
||||
pub fn new() -> Self {
|
||||
User
|
||||
}
|
||||
|
||||
pub fn save(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for User {
|
||||
fn default() -> Self {
|
||||
User
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue