fix: close remaining Dart language support gaps (#524)

* fix: close remaining Dart language support gaps

Four issues that were not addressed in PR #204:

1. extractFunctionName: add function_signature/method_signature handlers
   and add both to FUNCTION_NODE_TYPES. Without this, findEnclosingFunctionId
   cannot resolve Dart function scopes — all calls inside Dart functions
   have no sourceId, breaking CALLS edge attribution.

2. formal_parameter_list: add to paramListTypes in extractMethodSignature.
   Dart's tree-sitter grammar uses this node type (not formal_parameters),
   so parameter counting returns 0 for all Dart functions.

3. Write-access queries: add @assignment patterns for obj.field = value
   and this.field = value. Without these, no ACCESSES write edges are
   emitted for Dart code.

4. initialized_identifier guard in extractDartDeclaration: comma-separated
   declarations (String a, b, c) produce initialized_identifier nodes
   which are in DART_DECLARATION_NODE_TYPES but were unhandled — the type
   lives on the parent node.

Also adds Dart column to the feature matrix in type-resolution-system.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(dart): field-type resolution, call attribution, import resolution, and integration tests

Fixes five Dart language support gaps with integration tests and
architectural alignment:

**Tree-sitter queries** — Add field declaration patterns for typed and
nullable class fields (`String name = ''`, `String? name`).  Without
these, Dart class fields were invisible to the pipeline (zero Property
nodes, zero HAS_PROPERTY edges).

**Import resolution** — Dart relative imports (`import 'models.dart'`)
don't use a leading `./`.  The standard resolver only recognises paths
starting with `.` as relative; bare paths fell through to a Java-style
dot-to-slash conversion that mangled `models.dart` into `models/dart`.
Fix: prepend `./` before calling resolveStandard.

**Call attribution** — Dart's tree-sitter grammar places `function_body`
as a sibling of `function_signature`, not as a child wrapping both.  The
`findEnclosingFunction` parent-walk never found the function because the
call lives inside `function_body` which is a sibling of the signature.
Fix: add `enclosingFunctionFinder` hook to LanguageProvider interface
(following the same strategy pattern as `labelOverride`), with the
Dart-specific logic in `languages/dart.ts`.  Both `parse-worker.ts` and
`call-processor.ts` consume the hook generically — no Dart-specific code
in the generic processors.

**Receiver chain extraction** — Add `unconditional_assignable_selector`
to `MEMBER_ACCESS_NODE_TYPES` so `inferCallForm` returns `'member'` for
Dart method calls.  Add Dart-specific receiver extraction blocks in
`extractReceiverName`, `extractReceiverNode`, and a `selector` handler
in `extractMixedChain` for Dart's flat sibling-selector model (vs the
nested member-expression model used by all other languages).

**Integration tests** — New `dart.test.ts` with field-type resolution
and call-result-binding describe blocks.  Fixtures: `dart-field-types/`
(models.dart + app.dart) and `dart-call-result-binding/` (models.dart +
app.dart).  9 passing tests, 1 skipped (ACCESSES edges for field reads
depend on type-env parameter binding propagation — tracked for follow-up).

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
This commit is contained in:
Flavius Miron
2026-03-26 13:45:24 +00:00
committed by GitHub
co-authored by Claude Opus 4.6 Gergo Magyar
parent 546128cdcb
commit 3c896cdbcd
15 changed files with 407 additions and 23 deletions
@@ -209,6 +209,26 @@ const findEnclosingFunction = (
return generateId(finalLabel, `${filePath}:${funcName}`);
}
}
// Language-specific enclosing function resolution (e.g., Dart where
// function_body is a sibling of function_signature, not a child).
if (provider.enclosingFunctionFinder) {
const customResult = provider.enclosingFunctionFinder(current);
if (customResult) {
// Try SymbolTable first (same pattern as the FUNCTION_NODE_TYPES branch above).
const resolved = ctx.resolve(customResult.funcName, filePath);
if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) {
return resolved.candidates[0].nodeId;
}
let finalLabel = customResult.label;
if (provider.labelOverride) {
const override = provider.labelOverride(current.previousSibling!, finalLabel);
if (override !== null) finalLabel = override;
}
return generateId(finalLabel, `${filePath}:${customResult.funcName}`);
}
}
current = current.parent;
}
@@ -39,6 +39,12 @@ export function resolveDartImport(
return null;
}
// Relative imports — use standard resolution
return resolveStandard(stripped, filePath, ctx, SupportedLanguages.Dart);
// Relative imports — use standard resolution.
// Dart relative imports don't require a leading "./" (e.g. `import 'models.dart'`).
// The standard resolver only recognises paths starting with "." as relative, so
// prepend "./" when the path doesn't already start with "." to ensure correct
// same-directory resolution (without this, "models.dart" would be mangled by the
// generic dot-to-slash conversion intended for Java-style package imports).
const relPath = stripped.startsWith('.') ? stripped : './' + stripped;
return resolveStandard(relPath, filePath, ctx, SupportedLanguages.Dart);
}
@@ -79,6 +79,17 @@ interface LanguageProviderConfig {
projectConfig: unknown,
) => void;
// ── Enclosing function resolution ───────────────────────────────
/** Resolve the enclosing function name + label from an AST ancestor node
* that is NOT a standard FUNCTION_NODE_TYPE. For languages where the
* function body is a sibling of the signature (e.g. Dart: function_body ↔
* function_signature are siblings under program/class_body), the default
* parent walk cannot find the enclosing function. This hook lets the
* language provider inspect each ancestor and return the resolved result.
* Return null to continue the default walk.
* Default: undefined (standard parent walk only). */
readonly enclosingFunctionFinder?: (ancestorNode: SyntaxNode) => { funcName: string; label: NodeLabel } | null;
// ── Labels ────────────────────────────────────────────────────────
/** Override the default node label for definition.function captures.
* Return null to skip (C/C++ duplicate), a different label to reclassify
@@ -5,8 +5,14 @@
* - importSemantics: 'wildcard' (Dart imports bring everything public into scope)
* - exportChecker: public if no leading underscore
* - Dart SDK imports (dart:*) and external packages are skipped
* - enclosingFunctionFinder: Dart's tree-sitter grammar places function_body
* as a sibling of function_signature/method_signature (not as a child).
* The hook resolves the enclosing function by inspecting the previous sibling.
*/
import type { SyntaxNode } from '../utils/ast-helpers.js';
import type { NodeLabel } from '../../graph/types.js';
import { FUNCTION_NODE_TYPES, extractFunctionName } from '../utils/ast-helpers.js';
import { SupportedLanguages } from '../../../config/supported-languages.js';
import { defineLanguage } from '../language-provider.js';
import { typeConfig as dartConfig } from '../type-extractors/dart.js';
@@ -14,6 +20,23 @@ import { dartExportChecker } from '../export-detection.js';
import { resolveDartImport } from '../import-resolvers/dart.js';
import { DART_QUERIES } from '../tree-sitter-queries.js';
/**
* Resolve the enclosing function from a `function_body` node by looking at its
* previous sibling. In Dart's tree-sitter grammar, function_signature and
* function_body are siblings under program or class_body, unlike most languages
* where the function declaration wraps both.
*
* Delegates name extraction to the shared `extractFunctionName` which already
* handles Dart's function_signature and method_signature node types.
*/
const dartEnclosingFunctionFinder = (node: SyntaxNode): { funcName: string; label: NodeLabel } | null => {
if (node.type !== 'function_body') return null;
const prev = node.previousSibling;
if (!prev || !FUNCTION_NODE_TYPES.has(prev.type)) return null;
const { funcName, label } = extractFunctionName(prev);
return funcName ? { funcName, label } : null;
};
const BUILT_INS: ReadonlySet<string> = new Set([
'setState', 'mounted', 'debugPrint',
'runApp', 'showDialog', 'showModalBottomSheet',
@@ -31,5 +54,6 @@ export const dartProvider = defineLanguage({
exportChecker: dartExportChecker,
importResolver: resolveDartImport,
importSemantics: 'wildcard',
enclosingFunctionFinder: dartEnclosingFunctionFinder,
builtInNames: BUILT_INS,
});
@@ -1054,6 +1054,20 @@ export const DART_QUERIES = `
(factory_constructor_signature
(identifier) @name . (formal_parameter_list))) @definition.constructor
; ── Field declarations (String name = '', Address address = Address()) ──────
(declaration
(type_identifier)
(initialized_identifier_list
(initialized_identifier
(identifier) @name))) @definition.property
; ── Nullable field declarations (String? name) ──────────────────────────────
(declaration
(nullable_type)
(initialized_identifier_list
(initialized_identifier
(identifier) @name))) @definition.property
; ── Getters ──────────────────────────────────────────────────────────────────
(method_signature
(getter_signature
@@ -1097,6 +1111,22 @@ export const DART_QUERIES = `
(library_export
(configurable_uri) @import.source)) @import
; ── Write access: obj.field = value ──────────────────────────────────────────
(assignment_expression
left: (assignable_expression
(identifier) @assignment.receiver
(unconditional_assignable_selector
(identifier) @assignment.property))
right: (_)) @assignment
; ── Write access: this.field = value ─────────────────────────────────────────
(assignment_expression
left: (assignable_expression
(this) @assignment.receiver
(unconditional_assignable_selector
(identifier) @assignment.property))
right: (_)) @assignment
; ── Heritage: extends ────────────────────────────────────────────────────────
(class_definition
name: (identifier) @heritage.class
@@ -111,6 +111,25 @@ function hasDartTypeAnnotation(node: SyntaxNode): boolean {
// ── Tier 0: Explicit Type Annotations ───────────────────────────────────
const extractDartDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
// initialized_identifier: comma-separated variable (String a, b, c) — type is on parent
if (node.type === 'initialized_identifier') {
const parent = node.parent;
if (!parent) return;
let typeNode = findChild(parent, 'type_identifier');
if (!typeNode) {
const nullable = findChild(parent, 'nullable_type');
if (nullable) typeNode = findChild(nullable, 'type_identifier');
}
if (!typeNode) return;
const typeName = extractSimpleTypeName(typeNode);
if (!typeName || typeName === 'dynamic') return;
const nameNode = findChild(node, 'identifier');
if (!nameNode) return;
const varName = extractVarName(nameNode);
if (varName) env.set(varName, typeName);
return;
}
let typeNode = findChild(node, 'type_identifier');
if (!typeNode) {
const nullable = findChild(node, 'nullable_type');
@@ -82,6 +82,9 @@ export const FUNCTION_NODE_TYPES = new Set([
// Ruby
'method', // def foo
'singleton_method', // def self.foo
// Dart
'function_signature',
'method_signature',
]);
/**
@@ -430,6 +433,34 @@ export const extractFunctionName = (node: SyntaxNode): { funcName: string | null
}
funcName = nameNode?.text;
label = 'Method';
} else if (node.type === 'function_signature') {
// Dart: top-level function signatures
let nameNode = node.childForFieldName?.('name');
if (!nameNode) {
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
if (c?.type === 'identifier') { nameNode = c; break; }
}
}
funcName = nameNode?.text ?? null;
} else if (node.type === 'method_signature') {
// Dart: method_signature wraps function_signature
let funcSig: SyntaxNode | null = null;
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
if (c?.type === 'function_signature') { funcSig = c; break; }
}
if (funcSig) {
let nameNode = funcSig.childForFieldName?.('name');
if (!nameNode) {
for (let i = 0; i < funcSig.childCount; i++) {
const c = funcSig.child(i);
if (c?.type === 'identifier') { nameNode = c; break; }
}
}
funcName = nameNode?.text ?? null;
}
label = 'Method';
}
return { funcName, label };
@@ -471,6 +502,7 @@ export const extractMethodSignature = (node: SyntaxNode | null | undefined): Met
const paramListTypes = new Set([
'formal_parameters', 'parameters', 'parameter_list',
'function_parameters', 'method_parameters', 'function_value_parameters',
'formal_parameter_list', // Dart
]);
// Node types that indicate variadic/rest parameters
@@ -64,6 +64,7 @@ const MEMBER_ACCESS_NODE_TYPES = new Set([
'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
'unconditional_assignable_selector', // Dart: obj.method() — nameNode inside selector > unconditional_assignable_selector
]);
/**
@@ -208,6 +209,16 @@ export const extractReceiverName = (
}
}
// Dart: unconditional_assignable_selector is inside a `selector`, which is a sibling
// of the receiver in the expression_statement. For `user.save()`, the previous named
// sibling of the `selector` is `identifier [user]`.
if (!receiver && parent.type === 'unconditional_assignable_selector') {
const selectorNode = parent.parent; // selector [.save]
if (selectorNode) {
receiver = selectorNode.previousNamedSibling;
}
}
// C# null-conditional: user?.Save() → conditional_access_expression wraps member_binding_expression
if (!receiver && parent.type === 'member_binding_expression') {
const condAccess = parent.parent;
@@ -291,6 +302,14 @@ export const extractReceiverNode = (
}
}
// Dart: unconditional_assignable_selector — receiver is previous sibling of the selector
if (!receiver && parent.type === 'unconditional_assignable_selector') {
const selectorNode = parent.parent;
if (selectorNode) {
receiver = selectorNode.previousNamedSibling;
}
}
if (!receiver && parent.type === 'member_binding_expression') {
const condAccess = parent.parent;
if (condAccess?.type === 'conditional_access_expression') {
@@ -527,6 +546,27 @@ export function extractMixedChain(
} else {
return { chain, baseReceiverName: innerObject.text || undefined };
}
} else if (current.type === 'selector') {
// ── Dart: flat selector siblings (user.address.save() uses selector nodes) ──
// Extract field name from unconditional_assignable_selector child
const uas = current.namedChildren?.find(
(c: SyntaxNode) => c.type === 'unconditional_assignable_selector',
);
const propertyName = uas?.namedChildren?.find(
(c: SyntaxNode) => c.type === 'identifier',
)?.text;
if (!propertyName) break;
chain.unshift({ kind: 'field', name: propertyName });
// Walk to previous sibling for the next step in the chain
const prev = current.previousNamedSibling;
if (!prev) break;
if (prev.type === 'selector') {
current = prev;
} else {
// Base receiver (identifier or other terminal)
return { chain, baseReceiverName: prev.text || undefined };
}
} else {
// Simple identifier — this is the base receiver
return chain.length > 0
@@ -318,6 +318,23 @@ const findEnclosingFunctionId = (node: any, filePath: string, provider: Language
return result;
}
}
// Language-specific enclosing function resolution (e.g., Dart where
// function_body is a sibling of function_signature, not a child).
if (provider.enclosingFunctionFinder) {
const customResult = provider.enclosingFunctionFinder(current);
if (customResult) {
let finalLabel: NodeLabel = customResult.label;
if (provider.labelOverride) {
const override = provider.labelOverride(current.previousSibling, finalLabel);
if (override !== null) finalLabel = override;
}
const result = generateId(finalLabel, `${filePath}:${customResult.funcName}`);
functionIdCache.set(node, result);
return result;
}
}
current = current.parent;
}
functionIdCache.set(node, null);
@@ -0,0 +1,6 @@
import 'models.dart';
void processUser() {
var user = getUser('alice');
user.save();
}
@@ -0,0 +1,11 @@
class User {
String name = '';
bool save() {
return true;
}
}
User getUser(String name) {
return User();
}
@@ -0,0 +1,5 @@
import 'models.dart';
void processUser(User user) {
user.address.save();
}
@@ -0,0 +1,16 @@
class Address {
String city = '';
void save() {
// persist address
}
}
class User {
String name = '';
Address address = Address();
String greet() {
return name;
}
}
@@ -0,0 +1,147 @@
/**
* Dart: field-type resolution and call-result binding.
* Verifies that class fields are captured as Property nodes with HAS_PROPERTY
* edges, and that calls (including chained and call-result-bound) are resolved.
*
* Remaining known Dart gaps (field-chain ACCESSES) are documented as
* it.todo() tests to be filled when the pipeline is extended.
*/
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'path';
import {
FIXTURES, getRelationships, getNodesByLabel, edgeSet,
runPipelineFromRepo, type PipelineResult,
} from './helpers.js';
import { isLanguageAvailable } from '../../../src/core/tree-sitter/parser-loader.js';
import { SupportedLanguages } from '../../../src/config/supported-languages.js';
const dartAvailable = isLanguageAvailable(SupportedLanguages.Dart);
// ── Phase 8: Field-type resolution ──────────────────────────────────────
describe.skipIf(!dartAvailable)('Dart field-type resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'dart-field-types'),
() => {},
);
}, 60000);
it('detects classes and their properties', () => {
expect(getNodesByLabel(result, 'Class')).toEqual(
expect.arrayContaining(['Address', 'User']),
);
const properties = getNodesByLabel(result, 'Property');
expect(properties).toContain('address');
expect(properties).toContain('city');
expect(properties).toContain('name');
});
it('emits HAS_PROPERTY edges from class to field', () => {
const propEdges = getRelationships(result, 'HAS_PROPERTY');
expect(edgeSet(propEdges)).toEqual(
expect.arrayContaining([
'User → address',
'User → name',
'Address → city',
]),
);
});
it('resolves save() call from field-chain user.address.save()', () => {
const calls = getRelationships(result, 'CALLS');
// Dart attributes calls to the enclosing Function
const saveCalls = calls.filter(
(c) => c.target === 'save' && c.sourceFilePath.includes('app.dart'),
);
expect(saveCalls.length).toBe(1);
expect(saveCalls[0]!.targetFilePath).toContain('models.dart');
});
it('attributes save() call source to processUser, not File', () => {
const calls = getRelationships(result, 'CALLS');
const saveCalls = calls.filter(
(c) => c.target === 'save' && c.sourceFilePath.includes('app.dart'),
);
expect(saveCalls.length).toBe(1);
expect(saveCalls[0]!.source).toBe('processUser');
expect(saveCalls[0]!.sourceLabel).toBe('Function');
});
it('creates IMPORTS edge between app.dart and models.dart', () => {
const imports = getRelationships(result, 'IMPORTS');
const appImports = imports.filter(
(e) => e.sourceFilePath.includes('app.dart') && e.targetFilePath.includes('models.dart'),
);
expect(appImports.length).toBe(1);
});
// Dart field-chain ACCESSES edges require the call-processor's chain-resolution
// tier (Step 1c) to fire. This needs the type-env's scoped parameter binding
// (processUser's `user: User`) to propagate to processCallsFromExtracted so
// walkMixedChain can resolve User → address → Address and emit ACCESSES.
// The chain extraction (extractMixedChain) and member detection
// (MEMBER_ACCESS_NODE_TYPES) are wired, but the base receiver type lookup
// from the type-env currently returns undefined for Dart function parameters
// in the call-processor context. Tracked for follow-up.
it.skip('emits ACCESSES edges for field reads in chains', () => {
const accesses = getRelationships(result, 'ACCESSES');
const addressReads = accesses.filter(
(e) => e.target === 'address' && e.rel.reason === 'read',
);
expect(addressReads.length).toBe(1);
expect(addressReads[0]!.source).toBe('processUser');
expect(addressReads[0]!.targetLabel).toBe('Property');
});
});
// ── Phase 9: Call-result binding ────────────────────────────────────────
describe.skipIf(!dartAvailable)('Dart call-result binding', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'dart-call-result-binding'),
() => {},
);
}, 60000);
it('detects classes, methods, and functions', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Function')).toEqual(
expect.arrayContaining(['getUser', 'processUser']),
);
});
it('resolves save() call via call-result binding', () => {
const calls = getRelationships(result, 'CALLS');
// Dart attributes calls to the enclosing Function
const saveCalls = calls.filter(
(c) => c.target === 'save' && c.sourceFilePath.includes('app.dart'),
);
expect(saveCalls.length).toBe(1);
expect(saveCalls[0]!.targetFilePath).toContain('models.dart');
});
it('resolves getUser() call', () => {
const calls = getRelationships(result, 'CALLS');
const getUserCalls = calls.filter(
(c) => c.target === 'getUser' && c.sourceFilePath.includes('app.dart'),
);
expect(getUserCalls.length).toBe(1);
});
it('attributes calls to processUser, not File', () => {
const calls = getRelationships(result, 'CALLS');
const appCalls = calls.filter(
(c) => c.sourceFilePath.includes('app.dart'),
);
for (const call of appCalls) {
expect(call.source).toBe('processUser');
expect(call.sourceLabel).toBe('Function');
}
});
});
+21 -21
View File
@@ -375,27 +375,27 @@ So return-type-aware receiver inference already exists in a constrained downstre
## Language Feature Matrix
| Feature | TS | JS | Java | Kotlin | C# | Go | Rust | Python | PHP | Ruby | Swift | C++ | C |
|---------|:--:|:--:|:----:|:------:|:--:|:--:|:----:|:------:|:---:|:----:|:-----:|:---:|:-:|
| Declarations | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Parameters | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Initializer / constructor inference | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Constructor binding scan | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| For-loop element types | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes††† | Yes | Yes |
| Pattern binding | Yes | Yes | Yes | Yes | No | Yes | Yes | No | No | No | Partial‡‡‡ | No | No |
| Assignment chains | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes |
| Field/property type resolution | Yes | No† | Yes | Yes | Yes | Yes | Yes | Yes* | Yes | YARD | No | Yes | No‡ |
| Comment-based types | JSDoc | JSDoc | No | No | No | No | No | No | PHPDoc | YARD | No | No | No |
| Return type extraction | JSDoc | JSDoc | No | No | No | No | No | No | PHPDoc | YARD | No | No | No |
| Call-result variable binding | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes¶ | Yes††† | Yes | No |
| Field access binding | Yes | No† | Yes | Yes | Yes | Yes | Yes | No‖ | Yes | N/A | Yes††† | Yes | No |
| Method-call-result binding | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes¶ | Yes††† | Yes | No |
| Write access (ACCESSES write) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes§ | Yes | Yes | Yes | No |
| Parameter types extracted | Yes** | No | Yes | Yes | Yes | Yes | Yes | Partial†† | No | No | No | Yes | No |
| Method overload disambiguation | Yes** | No | Yes | Yes | Yes | No | No | No | No | No | No | Yes | No |
| Constructor-visible virtual dispatch | Yes | No | Yes | Yes‡‡ | Yes | No | No | No | No | No | No | Yes§§ | No |
| Optional parameter arity resolution | Yes | No | No | Yes | Yes | No | No | Yes | Yes | Yes | No | Yes | No |
| Cross-file binding propagation | Yes | Yes | Yes‖‖ | Yes | Yes¶¶ | Yes*** | Yes | Yes | Partial | Yes*** | Yes*** | Yes*** | Yes*** |
| Feature | TS | JS | Java | Kotlin | C# | Go | Rust | Python | PHP | Ruby | Swift | C++ | C | Dart |
|---------|:--:|:--:|:----:|:------:|:--:|:--:|:----:|:------:|:---:|:----:|:-----:|:---:|:-:|:----:|
| Declarations | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Parameters | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Initializer / constructor inference | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Constructor binding scan | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| For-loop element types | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes††† | Yes | Yes | Yes |
| Pattern binding | Yes | Yes | Yes | Yes | No | Yes | Yes | No | No | No | Partial‡‡‡ | No | No | No |
| Assignment chains | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes |
| Field/property type resolution | Yes | No† | Yes | Yes | Yes | Yes | Yes | Yes* | Yes | YARD | No | Yes | No‡ | No |
| Comment-based types | JSDoc | JSDoc | No | No | No | No | No | No | PHPDoc | YARD | No | No | No | No |
| Return type extraction | JSDoc | JSDoc | No | No | No | No | No | No | PHPDoc | YARD | No | No | No | No |
| Call-result variable binding | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes¶ | Yes††† | Yes | No | Yes |
| Field access binding | Yes | No† | Yes | Yes | Yes | Yes | Yes | No‖ | Yes | N/A | Yes††† | Yes | No | Yes |
| Method-call-result binding | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes¶ | Yes††† | Yes | No | Yes |
| Write access (ACCESSES write) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes§ | Yes | Yes | Yes | No | Yes |
| Parameter types extracted | Yes** | No | Yes | Yes | Yes | Yes | Yes | Partial†† | No | No | No | Yes | No | No |
| Method overload disambiguation | Yes** | No | Yes | Yes | Yes | No | No | No | No | No | No | Yes | No | No |
| Constructor-visible virtual dispatch | Yes | No | Yes | Yes‡‡ | Yes | No | No | No | No | No | No | Yes§§ | No | Yes |
| Optional parameter arity resolution | Yes | No | No | Yes | Yes | No | No | Yes | Yes | Yes | No | Yes | No | No |
| Cross-file binding propagation | Yes | Yes | Yes‖‖ | Yes | Yes¶¶ | Yes*** | Yes | Yes | Partial | Yes*** | Yes*** | Yes*** | Yes*** | Yes*** |
\* Python class-level annotated attributes (`address: Address`) now resolve `declaredType` correctly. The `self.x` instance attribute pattern is not yet supported.