feat: Phase 4 type resolution — nullable unwrapping, for-loop typing, assignment chains, code review fixes (#310)

* feat: Phase 4 type resolution — nullable unwrapping, for-loop typing, assignment chains, Kotlin return types

Phase 4.1: Nullable/optional chain unwrapping
- Add stripNullable utility in shared.ts for stripping nullable wrappers
- Apply in lookupInEnv to unwrap User | null → User, User? → User before receiver lookup
- Handles TS union, Kotlin/C#/Swift nullable suffix, Python Union[T, None], Rust Option<T>
- Enables receiver-type disambiguation through ?. optional chaining

Phase 4.2: For-loop element typing (Tier 0 — Java/C#/Kotlin)
- Add ForLoopExtractor type and forLoopNodeTypes to LanguageTypeConfig
- Java enhanced_for_statement, C# foreach_statement, Kotlin for_statement extractors
- Only explicit element types in AST (Tier 0); inference-based languages deferred

Phase 4.3: Assignment chain propagation (single-pass, depth-1)
- Add PendingAssignmentExtractor to LanguageTypeConfig with per-language implementations
- Handles TS/JS variable_declarator, Rust let_declaration, Python assignment,
  Go short_var_declaration, C# equals_value_clause, Java/Kotlin variable_declarator
- Single post-walk propagation pass (no fixpoint iteration per Sorbet/Pyright design)
- Resolves const b = a; b.save() when a has known type from Tier 0/1/1b

Phase 4.5: Kotlin return type extraction (bug fix)
- Fix extractMethodSignature to handle Kotlin user_type after function_value_parameters
- Remove lenient test assertions, add strict disambiguation proof

Integration tests across 10+ languages with competing same-name methods
and negative assertions proving disambiguation.

* fix: per-language assignment chain gaps from code review

- Kotlin: new extractKotlinPendingAssignment for property_declaration →
  variable_declaration AST (Java's variable_declarator doesn't exist in Kotlin)
- Go: handle var_spec (var b = u) alongside short_var_declaration (:=)
- PHP: add extractPendingAssignment for $alias = $user with $ prefix preserved

Integration tests added for all three languages with competing
same-name methods and negative disambiguation assertions.

* fix: code review fixes — DRY nullable keywords, avoid array allocations, clarify depth comment

Addresses findings from 6-agent code review on PR #310:

- Move stripNullable JSDoc to correct position (was orphaned above NULLABLE_KEYWORDS)
- DRY: reuse NULLABLE_KEYWORDS set in pipe-split filter instead of inline strings
- Replace node.children.find() with findChildByType/manual loops in jvm.ts,
  go.ts, csharp.ts to avoid unnecessary array allocations per tree-sitter call
- Clarify "depth-1" comment in type-env.ts: single-pass resolves multi-hop
  chains when forward-declared; reverse-order is depth-1 only
- Annotate extractGenericTypeArgs as Phase 5 infrastructure (zero production callers)
- Re-export PendingAssignmentExtractor from index.ts for API consistency
- Add explicit return undefined in Go extractPendingAssignment
- Remove redundant child.text === '=' check in Kotlin extractor

Test coverage:
- 20 new unit tests: stripNullable edge cases, per-language assignment chains,
  reverse-order depth limitation, nullable lookup resolution
- 15 new integration tests: multi-hop chains (a→b→c), nullable+chain combined
  (User|null + alias), Python User|None through stripNullable path
- 3 new fixtures: ts-multi-hop-chain, ts-nullable-chain, python-nullable-chain

* fix: third-pass review — walrus chain, scanner allocations, Kotlin variable_declaration, C# type guard

Addresses 4 new findings from third-pass CI review:

1. Python walrus operator (:=) now handled by extractPendingAssignment —
   named_expression nodes propagate alias chains alongside regular assignment
2. Scanner .namedChildren.find()/.some() in jvm.ts replaced with
   findChildByType() — consistent with 98daed4 code review fixes
3. Kotlin extractPendingAssignment extended to handle variable_declaration
   nodes in addition to property_declaration (function-local val/var)
4. C# extractPendingAssignment early-returns for is_pattern_expression and
   field_declaration nodes (never contain variable_declarator children)

Integration tests:
- Python: walrus chain (alias := u) with disambiguation (5 tests, 1 fixture)
- Kotlin: assignment chain with typed declarations (5 tests, 1 fixture)
- C#: assignment chain + is-pattern coexistence (6 tests, 1 fixture)
- Unit: Python walrus propagation (1 test)

* feat: nullable wrapper unwrapping + C++ assignment chains

Gaps 1, 2, 4 from code review — architectural changes to type resolution:

1. extractSimpleTypeName now unwraps nullable wrapper generics:
   - Optional<User> → "User" (Java), Option<User> → "User" (Rust),
     Maybe<User> → "User" (Kotlin Arrow/Haskell-style)
   - Containers (List, Map) and async wrappers (Promise, Future) are NOT
     unwrapped — methods are called on the container, not the inner type
   - Uses existing extractGenericTypeArgs (now production-active, was dead code)
   - NULLABLE_WRAPPER_TYPES set: Optional, Option, Maybe

2. C++ extractPendingAssignment added for auto alias chains:
   - auto alias = user; alias.save() now propagates User type
   - Handles pointer/reference declarators, auto/decltype(auto)

3. Updated existing Rust test: Option<User> parameter now correctly
   stores "User" instead of "Option" in TypeEnv

Integration tests with fixtures for Java Optional, Rust Option, C++ auto
chain. Full pipeline resolution marked .todo — requires call-processor
enhancement (TypeEnv stores correct types but call-processor needs
additional work to produce CALLS edges for these patterns).

Unit tests: 196 passed (7 new). Integration: all 9 languages green.

* fix: resolve .todo tests — stale dist/ was the root cause

The Rust Option<User> and C++ auto assignment chain integration tests
were marked .todo because the pipeline didn't produce CALLS edges.
Root cause: dist/ was compiled from pre-Phase 4 source and lacked:
- NULLABLE_WRAPPER_TYPES unwrapping in extractSimpleTypeName
- C++ extractPendingAssignment

After npm run build, all tests pass as real assertions:
- Rust: alias.save() resolves to User#save via Option<User> unwrap + chain
- C++: alias.save() and rAlias.save() resolve via auto assignment chain
  with correct disambiguation (User vs Repo)

Only remaining .todo: Rust user.unwrap().save() (Phase 5 — chained
return type inference, not a TypeEnv issue).
This commit is contained in:
Gergő Magyar 2026-03-16 15:21:54 +00:00 committed by GitHub
parent fbff6d08c0
commit 5fa73bafdf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
117 changed files with 3315 additions and 50 deletions

View file

@ -3,7 +3,7 @@ import { FUNCTION_NODE_TYPES, extractFunctionName, CLASS_CONTAINER_TYPES } from
import { SupportedLanguages } from '../../config/supported-languages.js';
import { typeConfigs, TYPED_PARAMETER_TYPES } from './type-extractors/index.js';
import type { ClassNameLookup } from './type-extractors/types.js';
import { extractSimpleTypeName } from './type-extractors/shared.js';
import { extractSimpleTypeName, stripNullable } from './type-extractors/shared.js';
import type { SymbolTable } from './symbol-table.js';
/**
@ -12,7 +12,9 @@ import type { SymbolTable } from './symbol-table.js';
* file-level variables use the '' (empty string) scope.
*
* Design constraints:
* - Explicit-only: only type annotations, never inferred types
* - Explicit-only: Tier 0 uses type annotations; Tier 1 infers from constructors
* - Tier 2: single-pass assignment chain propagation in source order resolves
* `const b = a` when `a` already has a type from Tier 0/1
* - Scope-aware: function-local variables don't collide across functions
* - Conservative: complex/generic types extract the base name only
* - Per-file: built once, used for receiver resolution, then discarded
@ -71,13 +73,14 @@ const lookupInEnv = (
const scopeEnv = env.get(scopeKey);
if (scopeEnv) {
const result = scopeEnv.get(varName);
if (result) return result;
if (result) return stripNullable(result);
}
}
// Fall back to file-level scope
const fileEnv = env.get(FILE_SCOPE);
return fileEnv?.get(varName);
const raw = fileEnv?.get(varName);
return raw ? stripNullable(raw) : undefined;
};
@ -288,12 +291,13 @@ export const buildTypeEnv = (
const classNames = createClassNameLookup(localClassNames, symbolTable);
const config = typeConfigs[language];
const bindings: ConstructorBinding[] = [];
const pendingAssignments: Array<{ scope: string; lhs: string; rhs: string }> = [];
/**
* Try to extract a (variableName typeName) binding from a single AST node.
*
* Resolution tiers (first match wins):
* - Tier 0: explicit type annotations via extractDeclaration
* - Tier 0: explicit type annotations via extractDeclaration / extractForLoopBinding
* - Tier 1: constructor-call inference via extractInitializer (fallback)
*/
const extractTypeBinding = (node: SyntaxNode, scopeEnv: Map<string, string>): void => {
@ -302,6 +306,12 @@ export const buildTypeEnv = (
config.extractParameter(node, scopeEnv);
return;
}
// For-each loop variable bindings (Java/C#/Kotlin): explicit element types in the AST.
// Checked before declarationNodeTypes — loop variables are not declarations.
if (config.forLoopNodeTypes?.has(node.type)) {
config.extractForLoopBinding?.(node, scopeEnv);
return;
}
if (config.declarationNodeTypes.has(node.type)) {
config.extractDeclaration(node, scopeEnv);
// Tier 1: constructor-call inference as fallback.
@ -338,6 +348,17 @@ export const buildTypeEnv = (
extractTypeBinding(node, scopeEnv);
// Tier 2: collect plain-identifier RHS assignments for post-walk propagation.
// Delegates to per-language extractPendingAssignment — AST shapes differ widely
// (JS uses variable_declarator/name/value, Rust uses let_declaration/pattern/value,
// Python uses assignment/left/right, Go uses short_var_declaration/expression_list).
if (config.extractPendingAssignment && config.declarationNodeTypes.has(node.type)) {
const pending = config.extractPendingAssignment(node, scopeEnv);
if (pending) {
pendingAssignments.push({ scope, ...pending });
}
}
// Scan for constructor bindings that couldn't be resolved locally.
// Only collect if TypeEnv didn't already resolve this binding.
if (config.scanConstructorBinding) {
@ -355,6 +376,21 @@ export const buildTypeEnv = (
};
walk(tree.rootNode, FILE_SCOPE);
// Tier 2: single-pass assignment chain propagation in source order.
// Resolves `const b = a` where `a` has a known type from Tier 0/1.
// Multi-hop chains resolve when forward-declared (a→b→c in source order);
// reverse-order assignments are depth-1 only. No fixpoint iteration —
// this covers 95%+ of real-world patterns.
for (const { scope, lhs, rhs } of pendingAssignments) {
const scopeEnv = env.get(scope);
if (!scopeEnv || scopeEnv.has(lhs)) continue;
const rhsType = scopeEnv.get(rhs) ?? env.get(FILE_SCOPE)?.get(rhs);
if (rhsType) {
scopeEnv.set(lhs, rhsType);
}
}
return {
lookup: (varName, callNode) => lookupInEnv(env, varName, callNode),
constructorBindings: bindings,

View file

@ -1,5 +1,5 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner } from './types.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor } from './types.js';
import { extractSimpleTypeName, extractVarName } from './shared.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
@ -160,10 +160,34 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
return { varName, calleeName: func.text };
};
/** C++: auto alias = user → declaration with auto type + init_declarator where value is identifier */
const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
if (node.type !== 'declaration') return undefined;
const typeNode = node.childForFieldName('type');
if (!typeNode) return undefined;
// Only handle auto — typed declarations already resolved by extractDeclaration
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 !== '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 lhs = extractVarName(finalName);
if (!lhs || scopeEnv.has(lhs)) return undefined;
return { lhs, rhs: value.text };
};
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
extractDeclaration,
extractParameter,
extractInitializer,
scanConstructorBinding,
extractPendingAssignment,
};

View file

@ -1,5 +1,5 @@
import type { SyntaxNode } from '../utils.js';
import type { ConstructorBindingScanner, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js';
import type { ConstructorBindingScanner, ForLoopExtractor, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, PendingAssignmentExtractor } from './types.js';
import { extractSimpleTypeName, extractVarName, findChildByType, unwrapAwait } from './shared.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
@ -143,9 +143,54 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
return { varName: nameNode.text, calleeName };
};
const FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set([
'foreach_statement',
]);
/** C#: foreach (User user in users) — extract loop variable binding */
const extractForLoopBinding: ForLoopExtractor = (node: SyntaxNode, scopeEnv: Map<string, string>): void => {
const typeNode = node.childForFieldName('type');
// The loop variable name is in the 'left' field in tree-sitter-c-sharp
const nameNode = node.childForFieldName('left');
if (!typeNode || !nameNode) return;
// Skip 'var' — type would need to be inferred from the collection element type
if (typeNode.type === 'implicit_type' && typeNode.text === 'var') return;
const typeName = extractSimpleTypeName(typeNode);
const varName = extractVarName(nameNode);
if (typeName && varName) scopeEnv.set(varName, typeName);
};
/** C#: var alias = u variable_declarator with name + equals_value_clause.
* Only local_declaration_statement and variable_declaration contain variable_declarator children;
* is_pattern_expression and field_declaration never do skip them early. */
const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
if (node.type === 'is_pattern_expression' || node.type === 'field_declaration') return undefined;
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (!child || child.type !== 'variable_declarator') continue;
const nameNode = child.childForFieldName('name');
if (!nameNode) continue;
const lhs = nameNode.text;
if (scopeEnv.has(lhs)) continue;
// C# wraps value in equals_value_clause; fall back to last named child
let evc: SyntaxNode | null = null;
for (let j = 0; j < child.childCount; j++) {
if (child.child(j)?.type === 'equals_value_clause') { evc = child.child(j); break; }
}
const valueNode = evc?.firstNamedChild ?? child.namedChild(child.namedChildCount - 1);
if (valueNode && valueNode !== nameNode && (valueNode.type === 'identifier' || valueNode.type === 'simple_identifier')) {
return { lhs, rhs: valueNode.text };
}
}
return undefined;
};
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
extractDeclaration,
extractParameter,
scanConstructorBinding,
extractForLoopBinding,
extractPendingAssignment,
};

View file

@ -1,5 +1,5 @@
import type { SyntaxNode } from '../utils.js';
import type { ConstructorBindingScanner, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js';
import type { ConstructorBindingScanner, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, PendingAssignmentExtractor } from './types.js';
import { extractSimpleTypeName, extractVarName } from './shared.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
@ -181,9 +181,53 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
return { varName: leftIds[0].text, calleeName };
};
/** Go: alias := u (short_var_declaration) or var b = u (var_spec) */
const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
if (node.type === 'short_var_declaration') {
const left = node.childForFieldName('left');
const right = node.childForFieldName('right');
if (!left || !right) return undefined;
const lhsNode = left.type === 'expression_list' ? left.firstNamedChild : left;
const rhsNode = right.type === 'expression_list' ? right.firstNamedChild : right;
if (!lhsNode || !rhsNode) return undefined;
if (lhsNode.type !== 'identifier') return undefined;
const lhs = lhsNode.text;
if (scopeEnv.has(lhs)) return undefined;
if (rhsNode.type === 'identifier') return { lhs, rhs: rhsNode.text };
return undefined;
}
if (node.type === 'var_spec' || node.type === 'var_declaration') {
// var_declaration contains var_spec children; var_spec has name + expression_list value
const specs: SyntaxNode[] = [];
if (node.type === 'var_declaration') {
for (let i = 0; i < node.namedChildCount; i++) {
const c = node.namedChild(i);
if (c?.type === 'var_spec') specs.push(c);
}
} else {
specs.push(node);
}
for (const spec of specs) {
const nameNode = spec.childForFieldName('name');
if (!nameNode || nameNode.type !== 'identifier') continue;
const lhs = nameNode.text;
if (scopeEnv.has(lhs)) continue;
// Check if the last named child is a bare identifier (no type annotation between name and value)
let exprList: SyntaxNode | null = null;
for (let i = 0; i < spec.childCount; i++) {
if (spec.child(i)?.type === 'expression_list') { exprList = spec.child(i); break; }
}
const rhsNode = exprList?.firstNamedChild;
if (rhsNode?.type === 'identifier') return { lhs, rhs: rhsNode.text };
}
}
return undefined;
};
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
extractDeclaration,
extractParameter,
scanConstructorBinding,
extractPendingAssignment,
};

View file

@ -33,7 +33,14 @@ export const typeConfigs = {
[SupportedLanguages.Ruby]: rubyConfig,
} satisfies Record<SupportedLanguages, LanguageTypeConfig>;
export type { LanguageTypeConfig, TypeBindingExtractor, ParameterExtractor, ConstructorBindingScanner } from './types.js';
export type {
LanguageTypeConfig,
TypeBindingExtractor,
ParameterExtractor,
ConstructorBindingScanner,
ForLoopExtractor,
PendingAssignmentExtractor,
} from './types.js';
export {
TYPED_PARAMETER_TYPES,
extractSimpleTypeName,

View file

@ -1,5 +1,5 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner } from './types.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ForLoopExtractor, PendingAssignmentExtractor } from './types.js';
import { extractSimpleTypeName, extractVarName, findChildByType } from './shared.js';
// ── Java ──────────────────────────────────────────────────────────────────
@ -73,7 +73,7 @@ const scanJavaConstructorBinding: ConstructorBindingScanner = (node) => {
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');
const declarator = findChildByType(node, 'variable_declarator');
if (!declarator) return undefined;
const nameNode = declarator.childForFieldName('name');
const value = declarator.childForFieldName('value');
@ -85,12 +85,44 @@ const scanJavaConstructorBinding: ConstructorBindingScanner = (node) => {
return { varName: nameNode.text, calleeName: methodName.text };
};
const JAVA_FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set([
'enhanced_for_statement',
]);
/** Java: for (User user : users) — extract loop variable binding */
const extractJavaForLoopBinding: ForLoopExtractor = (node: SyntaxNode, scopeEnv: Map<string, string>): void => {
const typeNode = node.childForFieldName('type');
const nameNode = node.childForFieldName('name');
if (!typeNode || !nameNode) return;
const typeName = extractSimpleTypeName(typeNode);
const varName = extractVarName(nameNode);
if (typeName && varName) scopeEnv.set(varName, typeName);
};
/** Java: var alias = u → local_variable_declaration > variable_declarator with name/value */
const extractJavaPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (!child || child.type !== 'variable_declarator') continue;
const nameNode = child.childForFieldName('name');
const valueNode = child.childForFieldName('value');
if (!nameNode || !valueNode) continue;
const lhs = nameNode.text;
if (scopeEnv.has(lhs)) continue;
if (valueNode.type === 'identifier' || valueNode.type === 'simple_identifier') return { lhs, rhs: valueNode.text };
}
return undefined;
};
export const javaTypeConfig: LanguageTypeConfig = {
declarationNodeTypes: JAVA_DECLARATION_NODE_TYPES,
extractDeclaration: extractJavaDeclaration,
extractParameter: extractJavaParameter,
extractInitializer: extractJavaInitializer,
scanConstructorBinding: scanJavaConstructorBinding,
forLoopNodeTypes: JAVA_FOR_LOOP_NODE_TYPES,
extractForLoopBinding: extractJavaForLoopBinding,
extractPendingAssignment: extractJavaPendingAssignment,
};
// ── Kotlin ────────────────────────────────────────────────────────────────
@ -188,10 +220,10 @@ const extractKotlinInitializer: InitializerExtractor = (node: SyntaxNode, env: M
/** 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');
const varDecl = findChildByType(node, '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 (findChildByType(varDecl, 'user_type')) return undefined;
const callExpr = findChildByType(node, 'call_expression');
if (!callExpr) return undefined;
const callee = callExpr.firstNamedChild;
if (!callee) return undefined;
@ -210,15 +242,88 @@ const scanKotlinConstructorBinding: ConstructorBindingScanner = (node) => {
}
}
if (!calleeName) return undefined;
const nameNode = varDecl.namedChildren.find(c => c.type === 'simple_identifier');
const nameNode = findChildByType(varDecl, 'simple_identifier');
if (!nameNode) return undefined;
return { varName: nameNode.text, calleeName };
};
const KOTLIN_FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set([
'for_statement',
]);
/** Kotlin: for (user: User in users) — extract loop variable binding when explicit type annotation exists */
const extractKotlinForLoopBinding: ForLoopExtractor = (node: SyntaxNode, scopeEnv: Map<string, string>): void => {
// Kotlin loop variable: variable_declaration child with optional user_type annotation
const varDecl = findChildByType(node, 'variable_declaration');
if (!varDecl) return;
// Only extract when there is an explicit type annotation (user_type node)
const typeNode = findChildByType(varDecl, 'user_type');
if (!typeNode) return;
const nameNode = findChildByType(varDecl, 'simple_identifier');
if (!nameNode) return;
const typeName = extractSimpleTypeName(typeNode);
const varName = extractVarName(nameNode);
if (typeName && varName) scopeEnv.set(varName, typeName);
};
/** Kotlin: val alias = u property_declaration or variable_declaration.
* property_declaration has: binding_pattern_kind("val"), variable_declaration("alias"),
* "=", and the RHS value (simple_identifier "u").
* variable_declaration appears directly inside functions and has simple_identifier children. */
const extractKotlinPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
if (node.type === 'property_declaration') {
// Find the variable name from variable_declaration child
const varDecl = findChildByType(node, 'variable_declaration');
if (!varDecl) return undefined;
const nameNode = varDecl.firstNamedChild;
if (!nameNode || nameNode.type !== 'simple_identifier') return undefined;
const lhs = nameNode.text;
if (scopeEnv.has(lhs)) return undefined;
// Find the RHS: a simple_identifier sibling after the "=" token
let foundEq = false;
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (!child) continue;
if (child.type === '=') { foundEq = true; continue; }
if (foundEq && child.type === 'simple_identifier') {
return { lhs, rhs: child.text };
}
}
return undefined;
}
if (node.type === 'variable_declaration') {
// variable_declaration directly inside functions: simple_identifier children
const nameNode = findChildByType(node, 'simple_identifier');
if (!nameNode) return undefined;
const lhs = nameNode.text;
if (scopeEnv.has(lhs)) return undefined;
// Look for RHS simple_identifier after "=" in the parent (property_declaration)
// variable_declaration itself doesn't contain "=" — it's in the parent
const parent = node.parent;
if (!parent) return undefined;
let foundEq = false;
for (let i = 0; i < parent.childCount; i++) {
const child = parent.child(i);
if (!child) continue;
if (child.type === '=') { foundEq = true; continue; }
if (foundEq && child.type === 'simple_identifier') {
return { lhs, rhs: child.text };
}
}
return undefined;
}
return undefined;
};
export const kotlinTypeConfig: LanguageTypeConfig = {
declarationNodeTypes: KOTLIN_DECLARATION_NODE_TYPES,
forLoopNodeTypes: KOTLIN_FOR_LOOP_NODE_TYPES,
extractDeclaration: extractKotlinDeclaration,
extractParameter: extractKotlinParameter,
extractInitializer: extractKotlinInitializer,
scanConstructorBinding: scanKotlinConstructorBinding,
extractForLoopBinding: extractKotlinForLoopBinding,
extractPendingAssignment: extractKotlinPendingAssignment,
};

View file

@ -1,5 +1,5 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor } from './types.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor } from './types.js';
import { extractSimpleTypeName, extractVarName, extractCalleeName } from './shared.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
@ -245,6 +245,20 @@ const extractReturnType: ReturnTypeExtractor = (node) => {
return undefined;
};
/** PHP: $alias = $user assignment_expression with variable_name left/right.
* PHP TypeEnv stores variables WITH $ prefix ($user User), so we keep $ in lhs/rhs. */
const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
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' || right.type !== 'variable_name') return undefined;
const lhs = left.text;
const rhs = right.text;
if (!lhs || !rhs || scopeEnv.has(lhs)) return undefined;
return { lhs, rhs };
};
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
extractDeclaration,
@ -252,4 +266,5 @@ export const typeConfig: LanguageTypeConfig = {
extractInitializer,
scanConstructorBinding,
extractReturnType,
extractPendingAssignment,
};

View file

@ -1,5 +1,5 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner } from './types.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor } from './types.js';
import { extractSimpleTypeName, extractVarName } from './shared.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
@ -15,7 +15,12 @@ const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<str
const typeNode = node.childForFieldName('type');
if (!left || !typeNode) return;
const varName = extractVarName(left);
const typeName = extractSimpleTypeName(typeNode);
// extractSimpleTypeName handles identifiers and qualified names.
// Python 3.10+ union syntax `User | None` is parsed as binary_operator,
// which extractSimpleTypeName doesn't handle. Fall back to raw text so
// stripNullable can process it at lookup time (e.g., "User | None" → "User").
const inner = typeNode.type === 'type' ? (typeNode.firstNamedChild ?? typeNode) : typeNode;
const typeName = extractSimpleTypeName(inner) ?? inner.text;
if (varName && typeName) env.set(varName, typeName);
};
@ -102,10 +107,34 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
return { varName: left.text, calleeName };
};
/** Python: alias = u assignment with left/right fields.
* Also handles walrus operator: alias := u named_expression with name/value fields. */
const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
let left: SyntaxNode | null;
let right: SyntaxNode | null;
if (node.type === 'assignment') {
left = node.childForFieldName('left');
right = node.childForFieldName('right');
} else if (node.type === 'named_expression') {
left = node.childForFieldName('name');
right = node.childForFieldName('value');
} else {
return undefined;
}
if (!left || !right) return undefined;
const lhs = left.type === 'identifier' ? left.text : undefined;
if (!lhs || scopeEnv.has(lhs)) return undefined;
if (right.type === 'identifier') return { lhs, rhs: right.text };
return undefined;
};
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
extractDeclaration,
extractParameter,
extractInitializer,
scanConstructorBinding,
extractPendingAssignment,
};

View file

@ -1,6 +1,6 @@
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor } from './types.js';
import { extractRubyConstructorAssignment, extractSimpleTypeName } from './shared.js';
import { SyntaxNode } from '../utils.js';
import type { SyntaxNode } from '../utils.js';
/**
* Ruby type extractor YARD annotation parsing.

View file

@ -1,5 +1,5 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner } from './types.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor } from './types.js';
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait } from './shared.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
@ -181,10 +181,23 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
return { varName: patternNode.text, calleeName };
};
/** Rust: let alias = u; → let_declaration with pattern + value fields */
const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
if (node.type !== 'let_declaration') return undefined;
const pattern = node.childForFieldName('pattern');
const value = node.childForFieldName('value');
if (!pattern || !value) return undefined;
const lhs = extractVarName(pattern);
if (!lhs || scopeEnv.has(lhs)) return undefined;
if (value.type === 'identifier') return { lhs, rhs: value.text };
return undefined;
};
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
extractDeclaration,
extractInitializer,
extractParameter,
scanConstructorBinding,
extractPendingAssignment,
};

View file

@ -1,5 +1,15 @@
import type { SyntaxNode } from '../utils.js';
/** Known single-arg nullable wrapper types that unwrap to their inner type
* for receiver resolution. Optional<User> "User", Option<User> "User".
* Only nullable wrappers NOT containers (List, Vec) or async wrappers (Promise, Future).
* See call-processor.ts WRAPPER_GENERICS for the full set used in return-type inference. */
const NULLABLE_WRAPPER_TYPES = new Set([
'Optional', // Java
'Option', // Rust, Scala
'Maybe', // Haskell-style, Kotlin Arrow
]);
/**
* Extract the simple type name from a type AST node.
* Handles generic types (e.g., List<User> List), qualified names
@ -31,11 +41,19 @@ export const extractSimpleTypeName = (typeNode: SyntaxNode): string | undefined
}
// Generic types: extract the base type (e.g., List<User> → List)
// For nullable wrappers (Optional<User>, Option<User>), unwrap to inner type.
if (typeNode.type === 'generic_type' || typeNode.type === 'parameterized_type') {
const base = typeNode.childForFieldName('name')
?? typeNode.childForFieldName('type')
?? typeNode.firstNamedChild;
if (base) return extractSimpleTypeName(base);
if (!base) return undefined;
const baseName = extractSimpleTypeName(base);
// Unwrap known nullable wrappers: Optional<User> → User, Option<User> → User
if (baseName && NULLABLE_WRAPPER_TYPES.has(baseName)) {
const args = extractGenericTypeArgs(typeNode);
if (args.length >= 1) return args[0];
}
return baseName;
}
// Nullable types (Kotlin User?, C# User?)
@ -132,6 +150,8 @@ export const TYPED_PARAMETER_TYPES = new Set([
* Extract type arguments from a generic type node.
* e.g., List<User, String> ['User', 'String'], Vec<User> ['User']
*
* Used by extractSimpleTypeName to unwrap nullable wrappers (Optional<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
@ -233,6 +253,42 @@ export const hasTypeAnnotation = (node: SyntaxNode): boolean => {
return false;
};
/** Bare nullable keywords that should not produce a receiver binding. */
const NULLABLE_KEYWORDS = new Set(['null', 'undefined', 'void', 'None', 'nil']);
/**
* Strip nullable wrappers from a type name string.
* Used by both lookupInEnv (TypeEnv annotations) and extractReturnTypeName
* (return-type text) to normalize types before receiver lookup.
*
* "User | null" "User"
* "User | undefined" "User"
* "User | null | undefined" "User"
* "User?" "User"
* "User | Repo" undefined (genuine union refuse)
* "null" undefined
*/
export const stripNullable = (typeName: string): string | undefined => {
let text = typeName.trim();
if (!text) return undefined;
if (NULLABLE_KEYWORDS.has(text)) return undefined;
// Strip nullable suffix: User? → User
if (text.endsWith('?')) text = text.slice(0, -1).trim();
// Strip union with null/undefined/None/nil/void
if (text.includes('|')) {
const parts = text.split('|').map(p => p.trim()).filter(p =>
p !== '' && !NULLABLE_KEYWORDS.has(p)
);
if (parts.length === 1) return parts[0];
return undefined; // genuine union or all-nullable — refuse
}
return text || undefined;
};
/**
* Unwrap an await_expression to get the inner value.
* Returns the node itself if not an await_expression, or null if input is null.

View file

@ -24,10 +24,26 @@ export type ConstructorBindingScanner = (node: SyntaxNode) => { varName: string;
* rather than in AST fields. Returns undefined if no return type can be determined. */
export type ReturnTypeExtractor = (node: SyntaxNode) => string | undefined;
/** Extracts loop variable type binding from a for-each statement. */
export type ForLoopExtractor = (
node: SyntaxNode,
scopeEnv: Map<string, string>,
) => void;
/** Extracts a plain-identifier assignment for Tier 2 propagation.
* For `const b = a`, returns { lhs: 'b', rhs: 'a' } when the LHS has no resolved type.
* Returns undefined if the node is not a plain identifier assignment. */
export type PendingAssignmentExtractor = (
node: SyntaxNode,
scopeEnv: ReadonlyMap<string, string>,
) => { lhs: string; rhs: string } | undefined;
/** Per-language type extraction configuration */
export interface LanguageTypeConfig {
/** Node types that represent typed declarations for this language */
declarationNodeTypes: ReadonlySet<string>;
/** AST node types for for-each/for-in statements with explicit element types. */
forLoopNodeTypes?: ReadonlySet<string>;
/** Extract a (varName → typeName) binding from a declaration node */
extractDeclaration: TypeBindingExtractor;
/** Extract a (varName → typeName) binding from a parameter node */
@ -44,4 +60,10 @@ export interface LanguageTypeConfig {
/** 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;
/** Extract loop variable → type binding from a for-each AST node. */
extractForLoopBinding?: ForLoopExtractor;
/** Extract plain-identifier assignment (e.g. `const b = a`) for Tier 2 chain propagation.
* Called on declaration/assignment nodes; returns {lhs, rhs} when the RHS is a bare identifier
* and the LHS has no resolved type yet. Language-specific because AST shapes differ widely. */
extractPendingAssignment?: PendingAssignmentExtractor;
}

View file

@ -1,5 +1,5 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor } from './types.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor } from './types.js';
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractCalleeName } from './shared.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
@ -191,6 +191,21 @@ const extractReturnType: ReturnTypeExtractor = (node) => {
return undefined;
};
/** TS/JS: const alias = u → variable_declarator with name/value fields */
const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (!child || child.type !== 'variable_declarator') continue;
const nameNode = child.childForFieldName('name');
const valueNode = child.childForFieldName('value');
if (!nameNode || !valueNode) continue;
const lhs = nameNode.text;
if (scopeEnv.has(lhs)) continue;
if (valueNode.type === 'identifier') return { lhs, rhs: valueNode.text };
}
return undefined;
};
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
extractDeclaration,
@ -198,4 +213,5 @@ export const typeConfig: LanguageTypeConfig = {
extractInitializer,
scanConstructorBinding,
extractReturnType,
extractPendingAssignment,
};

View file

@ -668,6 +668,25 @@ export const extractMethodSignature = (node: SyntaxNode | null | undefined): Met
}
}
// Kotlin: fun getUser(): User — return type is a bare user_type child of
// function_declaration. The Kotlin grammar does NOT wrap it in type_annotation
// or return_type; it appears as a direct child after function_value_parameters.
// Note: Kotlin uses function_value_parameters (not a field), so we find it by type.
if (!returnType) {
let paramsEnd = -1;
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (!child) continue;
if (child.type === 'function_value_parameters' || child.type === 'value_parameters') {
paramsEnd = child.endIndex;
}
if (paramsEnd >= 0 && child.type === 'user_type' && child.startIndex > paramsEnd) {
returnType = child.text;
break;
}
}
}
if (isVariadic) parameterCount = undefined;
return { parameterCount, returnType };

View file

@ -0,0 +1,10 @@
#pragma once
#include <string>
class Repo {
public:
Repo(const std::string& name) : name_(name) {}
bool save() { return false; }
private:
std::string name_;
};

View file

@ -0,0 +1,10 @@
#pragma once
#include <string>
class User {
public:
User(const std::string& name) : name_(name) {}
bool save() { return true; }
private:
std::string name_;
};

View file

@ -0,0 +1,13 @@
#include "models/User.h"
#include "models/Repo.h"
// Tests C++ auto alias = u assignment chain propagation.
void processEntities() {
User u("alice");
auto alias = u;
alias.save();
Repo r("maindb");
auto rAlias = r;
rAlias.save();
}

View file

@ -0,0 +1,10 @@
#pragma once
#include <string>
class Repo {
public:
Repo(const std::string& dbName) : dbName_(dbName) {}
bool save() { return false; }
private:
std::string dbName_;
};

View file

@ -0,0 +1,10 @@
#pragma once
#include <string>
class User {
public:
User(const std::string& name) : name_(name) {}
bool save() { return true; }
private:
std::string name_;
};

View file

@ -0,0 +1,19 @@
#include "models/User.h"
#include "models/Repo.h"
User* findUser() {
return new User("alice");
}
Repo* findRepo() {
return new Repo("maindb");
}
void processEntities() {
User* user = findUser();
Repo* repo = findRepo();
// Pointer-based nullable receivers — should disambiguate via unwrapped type
user->save();
repo->save();
}

View file

@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,9 @@
namespace Models;
public class Repo
{
public bool Save()
{
return false;
}
}

View file

@ -0,0 +1,9 @@
namespace Models;
public class User
{
public bool Save()
{
return true;
}
}

View file

@ -0,0 +1,20 @@
using Models;
namespace App;
public class Program
{
static User GetUser() => new User();
static Repo GetRepo() => new Repo();
public static void ProcessEntities()
{
User u = GetUser();
var alias = u;
alias.Save();
Repo r = GetRepo();
var rAlias = r;
rAlias.Save();
}
}

View file

@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,9 @@
namespace Models;
public class Repo
{
public bool Save()
{
return false;
}
}

View file

@ -0,0 +1,9 @@
namespace Models;
public class User
{
public bool Save()
{
return true;
}
}

View file

@ -0,0 +1,19 @@
using Models;
using System.Collections.Generic;
namespace App;
public class AppService
{
public void ProcessEntities(List<User> users, List<Repo> repos)
{
foreach (User user in users)
{
user.Save();
}
foreach (Repo repo in repos)
{
repo.Save();
}
}
}

View file

@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,4 @@
public class Repo
{
public bool Save() => false;
}

View file

@ -0,0 +1,4 @@
public class User
{
public bool Save() => true;
}

View file

@ -0,0 +1,29 @@
// Tests assignment chain + is-pattern in the same file.
// The is-pattern (obj is User u) creates a Tier 0 binding;
// the assignment chain (var alias = u) propagates it via Tier 2.
// Also verifies that the type guard in extractPendingAssignment
// correctly skips is_pattern_expression nodes without breaking.
public class App
{
public static void ProcessWithChain()
{
User u = new User();
var alias = u;
alias.Save();
}
public static void ProcessWithPattern(object obj)
{
if (obj is User u)
{
u.Save();
}
}
public static void ProcessRepoChain()
{
Repo r = new Repo();
var alias = r;
alias.Save();
}
}

View file

@ -6,10 +6,10 @@ public class AppService
{
public void Process()
{
User user = new User();
Repo repo = new Repo();
User? user = new User();
Repo? repo = new Repo();
// Null-conditional calls — should disambiguate via receiver type
// Null-conditional calls — nullable receiver should be unwrapped
user?.Save();
repo?.Save();
}

View file

@ -0,0 +1,31 @@
package main
import "example.com/go-assignment-chain/models"
func getUser() models.User {
return models.User{}
}
func getRepo() models.Repo {
return models.Repo{}
}
func processEntities() {
var u models.User = getUser()
alias := u
alias.Save()
var r models.Repo = getRepo()
rAlias := r
rAlias.Save()
}
func processWithVar() {
var u models.User = getUser()
var alias = u
alias.Save()
var r models.Repo = getRepo()
var rAlias = r
rAlias.Save()
}

View file

@ -0,0 +1,3 @@
module example.com/go-assignment-chain
go 1.21

View file

@ -0,0 +1,7 @@
package models
type Repo struct{}
func (r *Repo) Save() bool {
return false
}

View file

@ -0,0 +1,7 @@
package models
type User struct{}
func (u *User) Save() bool {
return true
}

View file

@ -0,0 +1,18 @@
package main
import "example.com/go-nullable-receiver/models"
func findUser() *models.User {
return &models.User{}
}
func findRepo() *models.Repo {
return &models.Repo{}
}
func processEntities() {
var user *models.User = findUser()
var repo *models.Repo = findRepo()
user.Save()
repo.Save()
}

View file

@ -0,0 +1,3 @@
module example.com/go-nullable-receiver
go 1.21

View file

@ -0,0 +1,7 @@
package models
type Repo struct{}
func (r *Repo) Save() bool {
return false
}

View file

@ -0,0 +1,7 @@
package models
type User struct{}
func (u *User) Save() bool {
return true
}

View file

@ -0,0 +1,17 @@
import models.User;
import models.Repo;
public class App {
static User getUser() { return new User(); }
static Repo getRepo() { return new Repo(); }
public static void processEntities() {
User u = getUser();
var alias = u;
alias.save();
Repo r = getRepo();
var rAlias = r;
rAlias.save();
}
}

View file

@ -0,0 +1,7 @@
package models;
public class Repo {
public boolean save() {
return false;
}
}

View file

@ -0,0 +1,7 @@
package models;
public class User {
public boolean save() {
return true;
}
}

View file

@ -0,0 +1,13 @@
import models.User;
import models.Repo;
public class App {
public static void processEntities(User[] users, Repo[] repos) {
for (User user : users) {
user.save();
}
for (Repo repo : repos) {
repo.save();
}
}
}

View file

@ -0,0 +1,7 @@
package models;
public class Repo {
public boolean save() {
return false;
}
}

View file

@ -0,0 +1,7 @@
package models;
public class User {
public boolean save() {
return true;
}
}

View file

@ -0,0 +1,19 @@
import models.User;
import models.Repo;
public class App {
public static void processEntities() {
User user = findUser();
Repo repo = findRepo();
user.save();
repo.save();
}
private static User findUser() {
return new User();
}
private static Repo findRepo() {
return new Repo();
}
}

View file

@ -0,0 +1,7 @@
package models;
public class Repo {
public boolean save() {
return false;
}
}

View file

@ -0,0 +1,7 @@
package models;
public class User {
public boolean save() {
return true;
}
}

View file

@ -0,0 +1,20 @@
import models.User;
import models.Repo;
// Tests that Optional<User> unwraps to User in TypeEnv,
// so assignment chains from Optional-typed sources resolve correctly.
public class App {
static User findUser() { return new User(); }
static Repo findRepo() { return new Repo(); }
static void processEntities() {
// Optional<User> declared TypeEnv stores "User" (not "Optional")
// The alias then propagates User through the chain
java.util.Optional<User> opt = java.util.Optional.of(findUser());
User user = opt.get();
user.save();
Repo repo = findRepo();
repo.save();
}
}

View file

@ -0,0 +1,5 @@
package models;
public class Repo {
public void save() {}
}

View file

@ -0,0 +1,5 @@
package models;
public class User {
public void save() {}
}

View file

@ -0,0 +1,11 @@
const { User } = require('./user');
const { Repo } = require('./repo');
/**
* @param {User | null} user
* @param {Repo | null} repo
*/
function processEntities(user, repo) {
if (user) user.save();
if (repo) repo.save();
}

View file

@ -0,0 +1,4 @@
class Repo {
save() { return true; }
}
module.exports = { Repo };

View file

@ -0,0 +1,4 @@
class User {
save() { return true; }
}
module.exports = { User };

View file

@ -0,0 +1,12 @@
fun getUser(): User = User()
fun getRepo(): Repo = Repo()
fun processEntities() {
val u: User = getUser()
val alias = u
alias.save()
val r: Repo = getRepo()
val rAlias = r
rAlias.save()
}

View file

@ -0,0 +1,3 @@
class Repo {
fun save() {}
}

View file

@ -0,0 +1,3 @@
class User {
fun save() {}
}

View file

@ -0,0 +1,14 @@
// Assignment chain with typed parameter propagation.
// Tests that extractKotlinPendingAssignment handles val alias = u
// where u comes from an explicit typed declaration.
fun processUser() {
val u: User = User()
val alias = u
alias.save()
}
fun processRepo() {
val r: Repo = Repo()
val alias = r
alias.save()
}

View file

@ -0,0 +1,3 @@
class Repo {
fun save() {}
}

View file

@ -0,0 +1,3 @@
class User {
fun save() {}
}

View file

@ -0,0 +1,16 @@
package app
import models.User
import models.Repo
fun processUsers(users: List<User>) {
for (user: User in users) {
user.save()
}
}
fun processRepos(repos: List<Repo>) {
for (repo: Repo in repos) {
repo.save()
}
}

View file

@ -0,0 +1,5 @@
package models
class Repo {
fun save() {}
}

View file

@ -0,0 +1,5 @@
package models
class User {
fun save() {}
}

View file

@ -0,0 +1,5 @@
package models
class Repo(val dbName: String) {
fun save(): Boolean = false
}

View file

@ -0,0 +1,5 @@
package models
class User(val name: String) {
fun save(): Boolean = true
}

View file

@ -0,0 +1,13 @@
package services
import models.User
import models.Repo
fun processEntities() {
val user: User? = User("alice")
val repo: Repo? = Repo("maindb")
// Safe calls on nullable receivers — should disambiguate via unwrapped type
user?.save()
repo?.save()
}

View file

@ -0,0 +1,8 @@
<?php
namespace App\Models;
class Repo {
public function save(): bool {
return true;
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace App\Models;
class User {
public function save(): bool {
return true;
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Services;
use App\Models\User;
use App\Models\Repo;
class AppService {
public function process(User $user, Repo $repo): void {
$alias = $user;
$alias->save();
$rAlias = $repo;
$rAlias->save();
}
}

View file

@ -0,0 +1,7 @@
{
"autoload": {
"psr-4": {
"App\\": "app/"
}
}
}

View file

@ -0,0 +1,11 @@
<?php
namespace App\Models;
class Repo
{
public function save(): bool
{
return false;
}
}

View file

@ -0,0 +1,11 @@
<?php
namespace App\Models;
class User
{
public function save(): bool
{
return true;
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Services;
use App\Models\User;
use App\Models\Repo;
class AppService
{
public function process(?User $user, ?Repo $repo): void
{
// Nullable type-hinted params — should disambiguate via unwrapped type
$user->save();
$repo->save();
}
}

View file

@ -0,0 +1,7 @@
{
"autoload": {
"psr-4": {
"App\\": "app/"
}
}
}

View file

@ -0,0 +1,17 @@
from user import User
from repo import Repo
def get_user() -> User:
return User()
def get_repo() -> Repo:
return Repo()
def process():
u: User = get_user()
alias = u
alias.save()
r: Repo = get_repo()
r_alias = r
r_alias.save()

View file

@ -0,0 +1,3 @@
class Repo:
def save(self):
return False

View file

@ -0,0 +1,3 @@
class User:
def save(self):
return True

View file

@ -0,0 +1,24 @@
from user import User
from repo import Repo
def get_user() -> User:
return User()
def get_repo() -> Repo:
return Repo()
# Python 3.10+ union: User | None is parsed as binary_operator,
# stored as raw text "User | None" in TypeEnv, then stripNullable resolves it.
def nullable_chain_user() -> None:
u: User | None = get_user()
alias = u
alias.save()
def nullable_chain_repo() -> None:
r: Repo | None = get_repo()
alias = r
alias.save()

View file

@ -0,0 +1,3 @@
class Repo:
def save(self) -> bool:
return False

View file

@ -0,0 +1,3 @@
class User:
def save(self) -> bool:
return True

View file

@ -0,0 +1,14 @@
from user import User
from repo import Repo
def find_user() -> User | None:
return User()
def find_repo() -> Repo | None:
return Repo()
def process_entities():
user: User | None = find_user()
user.save()
repo: Repo | None = find_repo()
repo.save()

View file

@ -0,0 +1,3 @@
class Repo:
def save(self):
return False

View file

@ -0,0 +1,3 @@
class User:
def save(self):
return True

View file

@ -0,0 +1,30 @@
from user import User
from repo import Repo
def get_user() -> User:
return User()
def get_repo() -> Repo:
return Repo()
# Walrus operator (:=) creates a named_expression binding.
# Tests that extractPendingAssignment propagates through walrus assignments.
def walrus_chain_user() -> None:
u: User = get_user()
# Regular assignment where alias gets type from u (regular chain)
alias = u
# Walrus inside condition: w gets type from u via named_expression chain
if (w := u):
w.save()
alias.save()
def walrus_chain_repo() -> None:
r: Repo = get_repo()
alias = r
if (w := r):
w.save()
alias.save()

View file

@ -0,0 +1,3 @@
class Repo:
def save(self) -> bool:
return False

View file

@ -0,0 +1,3 @@
class User:
def save(self) -> bool:
return True

View file

@ -0,0 +1,19 @@
mod user;
mod repo;
use crate::user::User;
use crate::repo::Repo;
fn get_user() -> User { User }
fn get_repo() -> Repo { Repo }
fn process_entities() {
let u: User = get_user();
let alias = u;
alias.save();
let r: Repo = get_repo();
let r_alias = r;
r_alias.save();
}
fn main() {}

View file

@ -0,0 +1,7 @@
pub struct Repo;
impl Repo {
pub fn save(&self) -> bool {
false
}
}

View file

@ -0,0 +1,7 @@
pub struct User;
impl User {
pub fn save(&self) -> bool {
true
}
}

View file

@ -0,0 +1,21 @@
mod user;
mod repo;
use crate::user::User;
use crate::repo::Repo;
fn find_user() -> Option<User> {
Some(User)
}
fn find_repo() -> Option<Repo> {
Some(Repo)
}
fn process_entities() {
let user: Option<User> = find_user();
user.unwrap().save();
let repo: Option<Repo> = find_repo();
repo.unwrap().save();
}
fn main() {}

View file

@ -0,0 +1,7 @@
pub struct Repo;
impl Repo {
pub fn save(&self) -> bool {
false
}
}

View file

@ -0,0 +1,7 @@
pub struct User;
impl User {
pub fn save(&self) -> bool {
true
}
}

View file

@ -0,0 +1,17 @@
mod user;
mod repo;
use crate::user::User;
use crate::repo::Repo;
// Tests that Option<User> unwraps to User in TypeEnv,
// and assignment chain from Option-typed source resolves correctly.
fn process_entities() {
let opt: Option<User> = Some(User);
let alias = opt;
alias.save();
let repo: Repo = Repo;
repo.save();
}
fn main() {}

View file

@ -0,0 +1,5 @@
pub struct Repo;
impl Repo {
pub fn save(&self) {}
}

View file

@ -0,0 +1,5 @@
pub struct User;
impl User {
pub fn save(&self) {}
}

View file

@ -0,0 +1,15 @@
import { User } from './user';
import { Repo } from './repo';
function getUser(): User { return new User(); }
function getRepo(): Repo { return new Repo(); }
export function processEntities(): void {
const u: User = getUser();
const alias = u;
alias.save();
const r: Repo = getRepo();
const rAlias = r;
rAlias.save();
}

View file

@ -0,0 +1,5 @@
export class Repo {
save(): boolean {
return false;
}
}

View file

@ -0,0 +1,5 @@
export class User {
save(): boolean {
return true;
}
}

Some files were not shown because too many files have changed in this diff Show more