mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-31 06:52:49 +00:00
feat: Implement virtual dispatch and overload disambiguation enhancements
- Updated AGENTS.md and CLAUDE.md to reflect new indexing metrics. - Enhanced call-processor.ts to support cross-file inheritance tracking and improved virtual dispatch resolution. - Added support for TypeScript overload signatures in tree-sitter queries. - Improved type extraction for C++, C#, and Kotlin to handle smart pointers and constructor types. - Introduced inferLiteralType for overload disambiguation across multiple languages. - Added tests for C++ smart pointer dispatch and Kotlin virtual dispatch scenarios. - Updated type-resolution-roadmap.md to reflect completion of phases P.1 to P.3 and outline future work on covariant return types.
This commit is contained in:
parent
d4c5e7e253
commit
0004aeeac8
20 changed files with 405 additions and 62 deletions
|
|
@ -1,7 +1,7 @@
|
|||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **GitNexus** (2174 symbols, 5218 relationships, 166 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
This project is indexed by GitNexus as **GitNexus** (2184 symbols, 5245 relationships, 167 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
|
||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **GitNexus** (2174 symbols, 5218 relationships, 166 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
This project is indexed by GitNexus as **GitNexus** (2184 symbols, 5245 relationships, 167 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import type { ExtractedCall, ExtractedAssignment, ExtractedHeritage, ExtractedRo
|
|||
import { callRouters } from './call-routing.js';
|
||||
import { extractReturnTypeName, stripNullable } from './type-extractors/shared.js';
|
||||
import { typeConfigs } from './type-extractors/index.js';
|
||||
import type { LiteralTypeInferrer } from './type-extractors/types.js';
|
||||
import type { SyntaxNode } from './utils.js';
|
||||
|
||||
// Stdlib methods that preserve the receiver's type identity. When TypeEnv already
|
||||
|
|
@ -141,6 +142,12 @@ export const processCalls = async (
|
|||
const parser = await loadParser();
|
||||
const collectedHeritage: ExtractedHeritage[] = [];
|
||||
const pendingWrites: { receiverTypeName: string; propertyName: string; filePath: string; srcId: string }[] = [];
|
||||
// Phase P cross-file: accumulate heritage across files for cross-file isSubclassOf.
|
||||
// Used as a secondary check when per-file parentMap lacks the relationship — helps
|
||||
// when the heritage-declaring file is processed before the call site file.
|
||||
// For remaining cases (reverse file order), the SymbolTable class-type fallback applies.
|
||||
const globalParentMap = new Map<string, string[]>();
|
||||
const globalParentSeen = new Map<string, Set<string>>();
|
||||
const logSkipped = isVerboseIngestionEnabled();
|
||||
const skippedByLang = logSkipped ? new Map<string, number>() : null;
|
||||
|
||||
|
|
@ -204,6 +211,17 @@ export const processCalls = async (
|
|||
}
|
||||
}
|
||||
const parentMap: ReadonlyMap<string, readonly string[]> = fileParentMap;
|
||||
// Merge per-file heritage into globalParentMap for cross-file isSubclassOf lookups.
|
||||
// Uses a parallel Set (globalParentSeen) for O(1) deduplication instead of O(n) includes().
|
||||
for (const [cls, parents] of fileParentMap) {
|
||||
let global = globalParentMap.get(cls);
|
||||
let seen = globalParentSeen.get(cls);
|
||||
if (!global) { global = []; globalParentMap.set(cls, global); }
|
||||
if (!seen) { seen = new Set(); globalParentSeen.set(cls, seen); }
|
||||
for (const p of parents) {
|
||||
if (!seen.has(p)) { seen.add(p); global.push(p); }
|
||||
}
|
||||
}
|
||||
|
||||
const typeEnv = lang ? buildTypeEnv(tree, lang, { symbolTable: ctx.symbols, parentMap }) : null;
|
||||
const callRouter = callRouters[language];
|
||||
|
|
@ -329,22 +347,35 @@ export const processCalls = async (
|
|||
let receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
|
||||
// Phase P: virtual dispatch override — when the declared type is a base class but
|
||||
// the constructor created a known subclass, prefer the more specific type.
|
||||
// Same-file only (parentMap is per-file from heritage pre-pass).
|
||||
// Scans constructorTypeMap for entries matching `*\0receiverName` since
|
||||
// reconstructing the exact scope key from call-processor context is fragile.
|
||||
// Checks per-file parentMap first, then falls back to globalParentMap for
|
||||
// cross-file heritage (e.g. Dog extends Animal declared in a different file).
|
||||
// Reconstructs the exact scope key (funcName@startIndex\0varName) from the
|
||||
// enclosing function AST node for a correct, O(1) map lookup.
|
||||
if (receiverTypeName && receiverName && typeEnv && typeEnv.constructorTypeMap.size > 0) {
|
||||
// Quick scan for matching receiver name in constructorTypeMap
|
||||
let ctorType: string | undefined;
|
||||
for (const [key, val] of typeEnv.constructorTypeMap) {
|
||||
const nul = key.indexOf('\0');
|
||||
if (nul >= 0 && key.slice(nul + 1) === receiverName) {
|
||||
ctorType = val;
|
||||
break;
|
||||
// Reconstruct scope key to match constructorTypeMap's scope\0varName format
|
||||
let scope = '';
|
||||
let p = callNode.parent;
|
||||
while (p) {
|
||||
if (FUNCTION_NODE_TYPES.has(p.type)) {
|
||||
const { funcName } = extractFunctionName(p);
|
||||
if (funcName) { scope = `${funcName}@${p.startIndex}`; break; }
|
||||
}
|
||||
p = p.parent;
|
||||
}
|
||||
const ctorType = typeEnv.constructorTypeMap.get(`${scope}\0${receiverName}`);
|
||||
if (ctorType && ctorType !== receiverTypeName) {
|
||||
// Verify subclass relationship via same-file parentMap
|
||||
if (isSubclassOf(ctorType, receiverTypeName, parentMap)) {
|
||||
// Verify subclass relationship: per-file parentMap first, then cross-file
|
||||
// globalParentMap, then fall back to SymbolTable class verification.
|
||||
// The SymbolTable fallback handles cross-file cases where heritage is declared
|
||||
// in a file not yet processed (e.g. Dog extends Animal in models/Dog.kt when
|
||||
// processing services/App.kt). Since constructorTypeMap only records entries
|
||||
// when a type annotation AND constructor are both present (val x: Base = Sub()),
|
||||
// confirming both are class-like types is sufficient — the original code would
|
||||
// not compile if Sub didn't extend Base.
|
||||
if (isSubclassOf(ctorType, receiverTypeName, parentMap)
|
||||
|| isSubclassOf(ctorType, receiverTypeName, globalParentMap)
|
||||
|| (ctx.symbols.lookupFuzzy(ctorType).some(d => d.type === 'Class' || d.type === 'Struct')
|
||||
&& ctx.symbols.lookupFuzzy(receiverTypeName).some(d => d.type === 'Class' || d.type === 'Struct' || d.type === 'Interface'))) {
|
||||
receiverTypeName = ctorType;
|
||||
}
|
||||
}
|
||||
|
|
@ -525,7 +556,7 @@ const toResolveResult = (
|
|||
* Only available on the sequential path (has AST); worker path passes undefined. */
|
||||
interface OverloadHints {
|
||||
callNode: SyntaxNode;
|
||||
inferLiteralType: (node: SyntaxNode) => string | undefined;
|
||||
inferLiteralType: LiteralTypeInferrer;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -582,10 +613,12 @@ const tryOverloadDisambiguation = (
|
|||
for (const arg of argList.namedChildren) {
|
||||
if (arg.type === 'comment') continue;
|
||||
// Unwrap argument wrapper nodes before passing to inferLiteralType:
|
||||
// - Kotlin value_argument: literal is the first (and only) named child
|
||||
// - C# argument: no named fields — the literal is the only named child
|
||||
// - Kotlin value_argument: has 'value' field containing the literal
|
||||
// - C# argument: has 'expression' field (handles named args like `name: "alice"`
|
||||
// where firstNamedChild would return name_colon instead of the value)
|
||||
// - Java/others: arg IS the literal directly (no unwrapping needed)
|
||||
const valueNode = arg.childForFieldName?.('value')
|
||||
?? arg.childForFieldName?.('expression')
|
||||
?? (arg.type === 'argument' || arg.type === 'value_argument'
|
||||
? arg.firstNamedChild ?? arg
|
||||
: arg);
|
||||
|
|
@ -596,7 +629,10 @@ const tryOverloadDisambiguation = (
|
|||
if (argTypes.every(t => t === undefined)) return null;
|
||||
|
||||
const matched = candidates.filter(c => {
|
||||
if (!c.parameterTypes) return true; // Keep candidates without type info
|
||||
// Keep candidates without type info — conservative: partially-annotated codebases
|
||||
// (e.g. C++ with some missing declarations) may have mixed typed/untyped overloads.
|
||||
// If one typed and one untyped both survive, matched.length > 1 → returns null (no edge).
|
||||
if (!c.parameterTypes) return true;
|
||||
return c.parameterTypes.every((pType, i) => {
|
||||
if (i >= argTypes.length || !argTypes[i]) return true;
|
||||
// Normalise Kotlin boxed type names (Int→int, Boolean→boolean, etc.) so
|
||||
|
|
@ -605,7 +641,15 @@ const tryOverloadDisambiguation = (
|
|||
});
|
||||
});
|
||||
|
||||
return matched.length === 1 ? matched[0] : null;
|
||||
if (matched.length === 1) return matched[0];
|
||||
// Multiple survivors may share the same nodeId (e.g. TypeScript overload signatures +
|
||||
// implementation body all collide via generateId). Deduplicate by nodeId — if all
|
||||
// matched candidates resolve to the same graph node, disambiguation succeeded.
|
||||
if (matched.length > 1) {
|
||||
const uniqueIds = new Set(matched.map(c => c.nodeId));
|
||||
if (uniqueIds.size === 1) return matched[0];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -665,8 +709,8 @@ const resolveCallTarget = (
|
|||
}
|
||||
// E. Try overload disambiguation on the narrowed pool
|
||||
if ((fileFiltered.length > 1 || ownerFiltered.length > 1) && overloadHints) {
|
||||
const pool = ownerFiltered.length > 1 ? ownerFiltered : fileFiltered;
|
||||
const disambiguated = tryOverloadDisambiguation(pool, overloadHints);
|
||||
const overloadPool = ownerFiltered.length > 1 ? ownerFiltered : fileFiltered;
|
||||
const disambiguated = tryOverloadDisambiguation(overloadPool, overloadHints);
|
||||
if (disambiguated) return toResolveResult(disambiguated, tiered.tier);
|
||||
}
|
||||
if (fileFiltered.length > 1 || ownerFiltered.length > 1) return null;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ export interface SymbolDefinition {
|
|||
type: NodeLabel;
|
||||
parameterCount?: number;
|
||||
/** Per-parameter type names for overload disambiguation (e.g. ['int', 'String']).
|
||||
* Only populated for overloading languages (Java, Kotlin, C#, C++). */
|
||||
* Populated when parameter types are resolvable from AST (any typed language).
|
||||
* Used for disambiguation in overloading languages (Java, Kotlin, C#, C++). */
|
||||
parameterTypes?: string[];
|
||||
/** Raw return type text extracted from AST (e.g. 'User', 'Promise<User>') */
|
||||
returnType?: string;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ export const TYPESCRIPT_QUERIES = `
|
|||
(function_declaration
|
||||
name: (identifier) @name) @definition.function
|
||||
|
||||
; TypeScript overload signatures (function_signature is a separate node type from function_declaration)
|
||||
(function_signature
|
||||
name: (identifier) @name) @definition.function
|
||||
|
||||
(method_definition
|
||||
name: (property_identifier) @name) @definition.method
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { SyntaxNode } from './utils.js';
|
||||
import { FUNCTION_NODE_TYPES, extractFunctionName, CLASS_CONTAINER_TYPES, isBuiltInOrNoise } from './utils.js';
|
||||
import { FUNCTION_NODE_TYPES, extractFunctionName, CLASS_CONTAINER_TYPES, CALL_EXPRESSION_TYPES, isBuiltInOrNoise } from './utils.js';
|
||||
import { SupportedLanguages } from '../../config/supported-languages.js';
|
||||
import { typeConfigs, TYPED_PARAMETER_TYPES } from './type-extractors/index.js';
|
||||
import type { ClassNameLookup, ReturnTypeLookup, ForLoopExtractorContext, PendingAssignment } from './type-extractors/types.js';
|
||||
|
|
@ -409,9 +409,12 @@ const createClassDefCache = (symbolTable?: SymbolTable) => {
|
|||
};
|
||||
};
|
||||
|
||||
/** AST node types representing constructor expressions across languages. */
|
||||
/** AST node types representing constructor expressions across languages.
|
||||
* Note: C# also has `implicit_object_creation_expression` (`new()` with type
|
||||
* inference) which is NOT captured — the type is inferred, not explicit.
|
||||
* Kotlin constructors use `call_expression` (no `new` keyword) — not detected. */
|
||||
const CONSTRUCTOR_EXPR_TYPES = new Set([
|
||||
'new_expression', // TS/JS: new Dog()
|
||||
'new_expression', // TS/JS/C++: new Dog()
|
||||
'object_creation_expression', // Java/C#: new Dog()
|
||||
]);
|
||||
|
||||
|
|
@ -434,14 +437,19 @@ const extractConstructorTypeName = (node: SyntaxNode, depth = 0): string | undef
|
|||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (!child) continue;
|
||||
// Don't descend into nested functions/classes
|
||||
if (FUNCTION_NODE_TYPES.has(child.type) || CLASS_CONTAINER_TYPES.has(child.type)) continue;
|
||||
// Don't descend into nested functions/classes or call expressions (prevents
|
||||
// finding constructor args inside method calls, e.g. processAll(new Dog()))
|
||||
if (FUNCTION_NODE_TYPES.has(child.type) || CLASS_CONTAINER_TYPES.has(child.type)
|
||||
|| CALL_EXPRESSION_TYPES.has(child.type)) continue;
|
||||
const result = extractConstructorTypeName(child, depth + 1);
|
||||
if (result) return result;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Max depth for MRO parent chain walking. Real-world inheritance rarely exceeds 3-4 levels. */
|
||||
const MAX_MRO_DEPTH = 5;
|
||||
|
||||
/** Check if `child` is a subclass of `parent` using the parentMap.
|
||||
* BFS up from child, depth-limited (5), cycle-safe. */
|
||||
export const isSubclassOf = (
|
||||
|
|
@ -466,9 +474,6 @@ export const isSubclassOf = (
|
|||
return false;
|
||||
};
|
||||
|
||||
/** Max depth for MRO parent chain walking. Real-world inheritance rarely exceeds 3-4 levels. */
|
||||
const MAX_MRO_DEPTH = 5;
|
||||
|
||||
/** Walk up the parent class chain to find a field or method on an ancestor.
|
||||
* BFS-like traversal with depth limit and cycle detection. First match wins.
|
||||
* Used by resolveFieldType and resolveMethodReturnType when direct lookup fails. */
|
||||
|
|
@ -795,7 +800,20 @@ export const buildTypeEnv = (
|
|||
if (c?.type === 'variable_declaration') { wrapped = c; break; }
|
||||
}
|
||||
}
|
||||
if (wrapped) typeNode = wrapped.childForFieldName('type');
|
||||
if (wrapped) {
|
||||
typeNode = wrapped.childForFieldName('type');
|
||||
// Kotlin: variable_declaration stores the type as user_type / nullable_type
|
||||
// child rather than a named 'type' field.
|
||||
if (!typeNode) {
|
||||
for (let i = 0; i < wrapped.namedChildCount; i++) {
|
||||
const c = wrapped.namedChild(i);
|
||||
if (c && (c.type === 'user_type' || c.type === 'nullable_type')) {
|
||||
typeNode = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeNode) {
|
||||
const nameNode = node.childForFieldName('name')
|
||||
|
|
@ -837,8 +855,16 @@ export const buildTypeEnv = (
|
|||
if (keysBefore.has(varName)) continue;
|
||||
const declaredType = scopeEnv.get(varName);
|
||||
if (!declaredType) continue;
|
||||
const ctorType = extractConstructorTypeName(node);
|
||||
if (ctorType && ctorType !== declaredType) {
|
||||
const ctorType = extractConstructorTypeName(node)
|
||||
?? config.detectConstructorType?.(node, classNames);
|
||||
if (!ctorType || ctorType === declaredType) continue;
|
||||
// Unwrap wrapper types (e.g., C++ shared_ptr<Animal> → Animal) for an
|
||||
// accurate isSubclassOf comparison. Language-specific via config hook.
|
||||
const declTypeNode = declarationTypeNodes.get(`${scope}\0${varName}`);
|
||||
const effectiveDeclaredType = (declTypeNode && config.unwrapDeclaredType)
|
||||
? (config.unwrapDeclaredType(declaredType, declTypeNode) ?? declaredType)
|
||||
: declaredType;
|
||||
if (ctorType !== effectiveDeclaredType) {
|
||||
constructorTypeMap.set(`${scope}\0${varName}`, ctorType);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,34 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor, ForLoopExtractor } from './types.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor, ForLoopExtractor, LiteralTypeInferrer, ConstructorTypeDetector, DeclaredTypeUnwrapper } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, resolveIterableElementType, methodToTypeArgPosition, type TypeArgPosition } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'declaration',
|
||||
]);
|
||||
|
||||
/** Smart pointer factory function names that create a typed object. */
|
||||
const SMART_PTR_FACTORIES = new Set([
|
||||
'make_shared', 'make_unique', 'make_shared_for_overwrite',
|
||||
]);
|
||||
|
||||
/** Smart pointer wrapper type names. When the declared type is a smart pointer,
|
||||
* the inner template type is extracted for virtual dispatch comparison. */
|
||||
const SMART_PTR_WRAPPERS = new Set(['shared_ptr', 'unique_ptr', 'weak_ptr']);
|
||||
|
||||
/** Extract the first type name from a template_argument_list child.
|
||||
* Unwraps type_descriptor wrappers common in tree-sitter-cpp ASTs.
|
||||
* Returns undefined if no template arguments or no type found. */
|
||||
export const extractFirstTemplateTypeArg = (parentNode: SyntaxNode): string | undefined => {
|
||||
const templateArgs = parentNode.children.find((c: any) => c.type === 'template_argument_list');
|
||||
if (!templateArgs?.firstNamedChild) return undefined;
|
||||
let argNode: any = templateArgs.firstNamedChild;
|
||||
if (argNode.type === 'type_descriptor') {
|
||||
const inner = argNode.childForFieldName('type');
|
||||
if (inner) argNode = inner;
|
||||
}
|
||||
return extractSimpleTypeName(argNode) ?? undefined;
|
||||
};
|
||||
|
||||
/** C++: Type x = ...; Type* x; Type& x; */
|
||||
const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
|
||||
const typeNode = node.childForFieldName('type');
|
||||
|
|
@ -88,6 +111,27 @@ const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<str
|
|||
} else if (func.type === 'identifier') {
|
||||
const text = func.text;
|
||||
if (text && classNames.has(text)) env.set(varName, text);
|
||||
} else {
|
||||
// auto x = std::make_shared<Dog>() — smart pointer factory via template_function.
|
||||
// AST: call_expression > function: qualified_identifier > template_function
|
||||
// or: call_expression > function: template_function (unqualified)
|
||||
const templateFunc = func.type === 'template_function'
|
||||
? func
|
||||
: (func.type === 'qualified_identifier' || func.type === 'scoped_identifier')
|
||||
? func.namedChildren.find((c: any) => c.type === 'template_function') ?? null
|
||||
: null;
|
||||
if (templateFunc) {
|
||||
const nameNode = templateFunc.firstNamedChild;
|
||||
if (nameNode) {
|
||||
const funcName = (nameNode.type === 'qualified_identifier' || nameNode.type === 'scoped_identifier')
|
||||
? nameNode.lastNamedChild?.text ?? ''
|
||||
: nameNode.text;
|
||||
if (SMART_PTR_FACTORIES.has(funcName)) {
|
||||
const typeName = extractFirstTemplateTypeArg(templateFunc);
|
||||
if (typeName) env.set(varName, typeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -372,7 +416,7 @@ const extractForLoopBinding: ForLoopExtractor = (node, { scopeEnv, declarationTy
|
|||
};
|
||||
|
||||
/** Infer the type of a literal AST node for C++ overload disambiguation. */
|
||||
const inferLiteralType = (node: SyntaxNode): string | undefined => {
|
||||
const inferLiteralType: LiteralTypeInferrer = (node) => {
|
||||
switch (node.type) {
|
||||
case 'number_literal': {
|
||||
const t = node.text;
|
||||
|
|
@ -400,6 +444,43 @@ const inferLiteralType = (node: SyntaxNode): string | undefined => {
|
|||
}
|
||||
};
|
||||
|
||||
/** C++: detect constructor type from smart pointer factory calls (make_shared<Dog>()).
|
||||
* Extracts the template type argument as the constructor type for virtual dispatch. */
|
||||
const detectCppConstructorType: ConstructorTypeDetector = (node, classNames) => {
|
||||
// Navigate to the initializer value in the declaration
|
||||
const declarator = node.childForFieldName('declarator');
|
||||
const initDecl = declarator?.type === 'init_declarator' ? declarator : undefined;
|
||||
if (!initDecl) return undefined;
|
||||
const value = initDecl.childForFieldName('value');
|
||||
if (!value || value.type !== 'call_expression') return undefined;
|
||||
|
||||
// Check for template_function pattern: make_shared<Dog>()
|
||||
const func = value.childForFieldName('function');
|
||||
if (!func || func.type !== 'template_function') return undefined;
|
||||
|
||||
// Extract function name (possibly qualified: std::make_shared)
|
||||
const nameNode = func.firstNamedChild;
|
||||
if (!nameNode) return undefined;
|
||||
let funcName: string;
|
||||
if (nameNode.type === 'qualified_identifier' || nameNode.type === 'scoped_identifier') {
|
||||
funcName = nameNode.lastNamedChild?.text ?? '';
|
||||
} else {
|
||||
funcName = nameNode.text;
|
||||
}
|
||||
if (!SMART_PTR_FACTORIES.has(funcName)) return undefined;
|
||||
|
||||
// Extract template type argument
|
||||
return extractFirstTemplateTypeArg(func);
|
||||
};
|
||||
|
||||
/** Unwrap a C++ smart pointer declared type to its inner template type.
|
||||
* E.g., shared_ptr<Animal> → Animal. Returns the original name if not a smart pointer. */
|
||||
const unwrapCppDeclaredType: DeclaredTypeUnwrapper = (declaredType, typeNode) => {
|
||||
if (!SMART_PTR_WRAPPERS.has(declaredType)) return declaredType;
|
||||
if (typeNode.type !== 'template_type') return declaredType;
|
||||
return extractFirstTemplateTypeArg(typeNode) ?? declaredType;
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
|
||||
|
|
@ -410,4 +491,6 @@ export const typeConfig: LanguageTypeConfig = {
|
|||
extractForLoopBinding,
|
||||
extractPendingAssignment,
|
||||
inferLiteralType,
|
||||
detectConstructorType: detectCppConstructorType,
|
||||
unwrapDeclaredType: unwrapCppDeclaredType,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { ConstructorBindingScanner, ForLoopExtractor, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, PendingAssignmentExtractor, PatternBindingExtractor } from './types.js';
|
||||
import type { ConstructorBindingScanner, ForLoopExtractor, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, PendingAssignmentExtractor, PatternBindingExtractor, LiteralTypeInferrer } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, findChildByType, unwrapAwait, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, extractElementTypeFromString, type TypeArgPosition } from './shared.js';
|
||||
|
||||
/** Known container property accessors that operate on the container itself (e.g., dict.Keys, dict.Values) */
|
||||
|
|
@ -464,7 +464,7 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
|||
};
|
||||
|
||||
/** Infer the type of a literal AST node for C# overload disambiguation. */
|
||||
const inferLiteralType = (node: SyntaxNode): string | undefined => {
|
||||
const inferLiteralType: LiteralTypeInferrer = (node) => {
|
||||
switch (node.type) {
|
||||
case 'integer_literal':
|
||||
if (node.text.endsWith('L') || node.text.endsWith('l')) return 'long';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ForLoopExtractor, PendingAssignmentExtractor, PatternBindingExtractor } from './types.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ForLoopExtractor, PendingAssignmentExtractor, PatternBindingExtractor, LiteralTypeInferrer, ConstructorTypeDetector } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, findChildByType, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, extractElementTypeFromString, type TypeArgPosition } from './shared.js';
|
||||
|
||||
// ── Java ──────────────────────────────────────────────────────────────────
|
||||
|
|
@ -269,7 +269,7 @@ const extractJavaPatternBinding: PatternBindingExtractor = (node) => {
|
|||
};
|
||||
|
||||
/** Infer the type of a literal AST node for Java/Kotlin overload disambiguation. */
|
||||
const inferJvmLiteralType = (node: SyntaxNode): string | undefined => {
|
||||
const inferJvmLiteralType: LiteralTypeInferrer = (node) => {
|
||||
switch (node.type) {
|
||||
case 'decimal_integer_literal':
|
||||
case 'integer_literal':
|
||||
|
|
@ -384,26 +384,30 @@ const extractKotlinParameter: ParameterExtractor = (node: SyntaxNode, env: Map<s
|
|||
if (varName && typeName) env.set(varName, typeName);
|
||||
};
|
||||
|
||||
/** Find the constructor callee name in a Kotlin property_declaration's initializer.
|
||||
* Returns the class name if the callee is a verified class constructor, undefined otherwise. */
|
||||
const findKotlinConstructorCallee = (node: SyntaxNode, classNames: ClassNameLookup): string | undefined => {
|
||||
if (node.type !== 'property_declaration') return undefined;
|
||||
const value = node.childForFieldName('value')
|
||||
?? findChildByType(node, 'call_expression');
|
||||
if (!value || value.type !== 'call_expression') return undefined;
|
||||
const callee = value.firstNamedChild;
|
||||
if (!callee || callee.type !== 'simple_identifier') return undefined;
|
||||
const calleeName = callee.text;
|
||||
if (!calleeName || !classNames.has(calleeName)) return undefined;
|
||||
return calleeName;
|
||||
};
|
||||
|
||||
/** Kotlin: val user = User() — infer type from call_expression when callee is a known class.
|
||||
* Kotlin constructors are syntactically identical to function calls, so we verify
|
||||
* against classNames (which may include cross-file SymbolTable lookups). */
|
||||
const extractKotlinInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<string, string>, classNames: ClassNameLookup): void => {
|
||||
if (node.type !== 'property_declaration') return;
|
||||
// Skip if there's an explicit type annotation — Tier 0 already handled it
|
||||
const varDecl = findChildByType(node, 'variable_declaration');
|
||||
if (varDecl && findChildByType(varDecl, 'user_type')) return;
|
||||
|
||||
// Get the initializer value — the call_expression after '='
|
||||
const value = node.childForFieldName('value')
|
||||
?? findChildByType(node, 'call_expression');
|
||||
if (!value || value.type !== 'call_expression') return;
|
||||
|
||||
// The callee is the first child of call_expression (simple_identifier for direct calls)
|
||||
const callee = value.firstNamedChild;
|
||||
if (!callee || callee.type !== 'simple_identifier') return;
|
||||
|
||||
const calleeName = callee.text;
|
||||
if (!calleeName || !classNames.has(calleeName)) return;
|
||||
const calleeName = findKotlinConstructorCallee(node, classNames);
|
||||
if (!calleeName) return;
|
||||
|
||||
// Extract the variable name from the variable_declaration inside property_declaration
|
||||
const nameNode = varDecl
|
||||
|
|
@ -415,6 +419,14 @@ const extractKotlinInitializer: InitializerExtractor = (node: SyntaxNode, env: M
|
|||
if (varName) env.set(varName, calleeName);
|
||||
};
|
||||
|
||||
/** Kotlin: detect constructor type from call_expression in typed declarations.
|
||||
* Unlike extractKotlinInitializer (which SKIPS typed declarations), this detects
|
||||
* the constructor type EVEN when a type annotation exists, enabling virtual dispatch
|
||||
* for patterns like `val a: Animal = Dog()`. */
|
||||
const detectKotlinConstructorType: ConstructorTypeDetector = (node, classNames) => {
|
||||
return findKotlinConstructorCallee(node, classNames);
|
||||
};
|
||||
|
||||
/** Kotlin: val x = User(...) — constructor binding for property_declaration with call_expression */
|
||||
const scanKotlinConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||
if (node.type !== 'property_declaration') return undefined;
|
||||
|
|
@ -773,4 +785,5 @@ export const kotlinTypeConfig: LanguageTypeConfig = {
|
|||
extractPendingAssignment: extractKotlinPendingAssignment,
|
||||
extractPatternBinding: extractKotlinPatternBinding,
|
||||
inferLiteralType: inferJvmLiteralType,
|
||||
detectConstructorType: detectKotlinConstructorType,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,6 +24,23 @@ 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;
|
||||
|
||||
/** Infer the type name of a literal AST node for overload disambiguation.
|
||||
* Returns the canonical type name (e.g. 'int', 'String', 'boolean') or undefined
|
||||
* for non-literal nodes. Only used when resolveCallTarget has multiple candidates
|
||||
* with parameterTypes — ~1-3% of call sites. */
|
||||
export type LiteralTypeInferrer = (node: SyntaxNode) => string | undefined;
|
||||
|
||||
/** Detect constructor-style call expressions that don't use `new` keyword.
|
||||
* Returns the constructor class name if the node's initializer is a constructor call,
|
||||
* or undefined otherwise. Used for virtual dispatch in languages like Kotlin
|
||||
* where constructors are syntactically identical to function calls, and C++
|
||||
* where smart pointer factory functions (make_shared/make_unique) wrap constructors. */
|
||||
export type ConstructorTypeDetector = (node: SyntaxNode, classNames: ClassNameLookup) => string | undefined;
|
||||
|
||||
/** Unwrap a declared type name to its inner type for virtual dispatch comparison.
|
||||
* E.g., C++ shared_ptr<Animal> → Animal. Returns undefined if no unwrapping applies. */
|
||||
export type DeclaredTypeUnwrapper = (declaredType: string, typeNode: SyntaxNode) => string | undefined;
|
||||
|
||||
/** Narrow lookup interface for resolving a callee name → return type name.
|
||||
* Backed by SymbolTable.lookupFuzzyCallable; passed via ForLoopExtractorContext.
|
||||
* Conservative: returns undefined when the callee is ambiguous (0 or 2+ matches). */
|
||||
|
|
@ -149,9 +166,7 @@ export interface LanguageTypeConfig {
|
|||
* The extractor receives the current scope's resolved bindings (read-only) to look up the
|
||||
* source variable's type. Returns undefined for non-matching nodes or unknown source types. */
|
||||
extractPatternBinding?: PatternBindingExtractor;
|
||||
/** Infer the type name of a literal AST node for overload disambiguation.
|
||||
* Returns the canonical type name (e.g. 'int', 'String', 'boolean') or undefined
|
||||
* for non-literal nodes. Only used when resolveCallTarget has multiple candidates
|
||||
* with parameterTypes — ~1-3% of call sites. */
|
||||
inferLiteralType?: (node: SyntaxNode) => string | undefined;
|
||||
inferLiteralType?: LiteralTypeInferrer;
|
||||
detectConstructorType?: ConstructorTypeDetector;
|
||||
unwrapDeclaredType?: DeclaredTypeUnwrapper;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor, PendingAssignment, ForLoopExtractor, PatternBindingExtractor } from './types.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor, PendingAssignment, ForLoopExtractor, PatternBindingExtractor, LiteralTypeInferrer } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractCalleeName, extractElementTypeFromString, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, type TypeArgPosition } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
|
|
@ -597,6 +597,28 @@ const extractPatternBinding: PatternBindingExtractor = (node, scopeEnv, declarat
|
|||
};
|
||||
};
|
||||
|
||||
/** Infer the type of a literal AST node for TypeScript overload disambiguation. */
|
||||
const inferTsLiteralType: LiteralTypeInferrer = (node) => {
|
||||
switch (node.type) {
|
||||
case 'number':
|
||||
return 'number';
|
||||
case 'string':
|
||||
case 'template_string':
|
||||
return 'string';
|
||||
case 'true':
|
||||
case 'false':
|
||||
return 'boolean';
|
||||
case 'null':
|
||||
return 'null';
|
||||
case 'undefined':
|
||||
return 'undefined';
|
||||
case 'regex':
|
||||
return 'RegExp';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
|
||||
|
|
@ -609,4 +631,5 @@ export const typeConfig: LanguageTypeConfig = {
|
|||
extractForLoopBinding,
|
||||
extractPendingAssignment,
|
||||
extractPatternBinding,
|
||||
inferLiteralType: inferTsLiteralType,
|
||||
};
|
||||
|
|
|
|||
17
gitnexus/test/fixtures/lang-resolution/cpp-smart-ptr-dispatch/src/app.cpp
vendored
Normal file
17
gitnexus/test/fixtures/lang-resolution/cpp-smart-ptr-dispatch/src/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class Animal {
|
||||
public:
|
||||
virtual std::string speak() { return "..."; }
|
||||
};
|
||||
|
||||
class Dog : public Animal {
|
||||
public:
|
||||
std::string speak() override { return "woof"; }
|
||||
};
|
||||
|
||||
void process() {
|
||||
auto dog = std::make_shared<Dog>();
|
||||
dog->speak();
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/kotlin-virtual-dispatch/models/Animal.kt
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/kotlin-virtual-dispatch/models/Animal.kt
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models
|
||||
|
||||
open class Animal {
|
||||
open fun speak(): String = "..."
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/kotlin-virtual-dispatch/models/Dog.kt
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/kotlin-virtual-dispatch/models/Dog.kt
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models
|
||||
|
||||
class Dog : Animal() {
|
||||
override fun speak(): String = "woof"
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/kotlin-virtual-dispatch/services/App.kt
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/kotlin-virtual-dispatch/services/App.kt
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package services
|
||||
|
||||
import models.Animal
|
||||
import models.Dog
|
||||
|
||||
fun process() {
|
||||
val animal: Animal = Dog()
|
||||
animal.speak()
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/ts-overload-disambiguation/src/app.ts
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/ts-overload-disambiguation/src/app.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
function lookup(id: number): string;
|
||||
function lookup(name: string): string;
|
||||
function lookup(key: number | string): string {
|
||||
return String(key);
|
||||
}
|
||||
|
||||
function process() {
|
||||
lookup(42);
|
||||
lookup("alice");
|
||||
}
|
||||
|
|
@ -1088,3 +1088,29 @@ describe('C++ overload disambiguation by parameter types', () => {
|
|||
expect(lookupCalls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// C++ smart pointer virtual dispatch via std::make_shared<T>()
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C++ smart pointer virtual dispatch via make_shared', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-smart-ptr-dispatch'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects Dog and Animal classes', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('Animal');
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('Dog');
|
||||
});
|
||||
|
||||
it('emits CALLS edge from process → speak', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const speakCall = calls.find(c => c.source === 'process' && c.target === 'speak');
|
||||
expect(speakCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1559,3 +1559,29 @@ describe('Kotlin overload disambiguation by parameter types', () => {
|
|||
expect(lookupCalls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Phase P: Virtual Dispatch via Constructor Type (cross-file) ──────────
|
||||
|
||||
describe('Kotlin virtual dispatch via constructor type (cross-file)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'kotlin-virtual-dispatch'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects Dog class', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('Dog');
|
||||
});
|
||||
|
||||
it('resolves animal.speak() to models/Dog.kt via constructor type override', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const speakCall = calls.find(c =>
|
||||
c.source === 'process' && c.target === 'speak' && c.targetFilePath === 'models/Dog.kt',
|
||||
);
|
||||
expect(speakCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import path from 'path';
|
||||
import {
|
||||
FIXTURES, getRelationships, getNodesByLabel, edgeSet,
|
||||
FIXTURES, getRelationships, getNodesByLabel, getNodesByLabelFull, edgeSet,
|
||||
runPipelineFromRepo, type PipelineResult,
|
||||
} from './helpers.js';
|
||||
|
||||
|
|
@ -2258,3 +2258,35 @@ describe('TypeScript virtual dispatch via constructor type (same-file)', () => {
|
|||
expect(fetchCalls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Phase P: Overload Disambiguation via inferLiteralType ────────────────
|
||||
|
||||
describe('TypeScript overload disambiguation via inferLiteralType', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ts-overload-disambiguation'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects lookup function with parameterTypes on graph node', () => {
|
||||
const functions = getNodesByLabelFull(result, 'Function');
|
||||
const lookupNodes = functions.filter(f => f.name === 'lookup');
|
||||
// generateId collision → 1 graph node, first overload's parameterTypes wins
|
||||
expect(lookupNodes.length).toBeGreaterThanOrEqual(1);
|
||||
// At least one lookup node has parameterTypes set
|
||||
const withParamTypes = lookupNodes.filter(n => n.properties.parameterTypes);
|
||||
expect(withParamTypes.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('emits CALLS edges from process() → lookup() via overload disambiguation', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const lookupCalls = calls.filter(c => c.source === 'process' && c.target === 'lookup');
|
||||
// Phase 0 (fileIndex stores both overloads) + Phase 2 (literal type matching)
|
||||
// enables resolution where previously 2 same-arity candidates → null.
|
||||
// Both calls resolve to same nodeId (ID collision) → 1 CALLS edge after dedup.
|
||||
expect(lookupCalls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -101,14 +101,14 @@ The goal is not to build a compiler. The goal is to support high-value static an
|
|||
**Plan:** `docs/plans/2026-03-19-feat-polymorphism-overloading-type-resolution-plan.md`
|
||||
|
||||
Four incremental phases:
|
||||
1. **Parameter type metadata** — extend `SymbolDefinition` with `parameterTypes: string[]` extracted during parsing
|
||||
2. **Overload disambiguation** — filter overloaded methods by argument literal types at call sites
|
||||
3. **Constructor-visible virtual dispatch** — `Base b = new Derived(); b.method()` resolves to `Derived#method` when constructor type is a known subclass
|
||||
1. **Parameter type metadata** — extend `SymbolDefinition` with `parameterTypes: string[]` extracted during parsing — **DELIVERED**
|
||||
2. **Overload disambiguation** — filter overloaded methods by argument literal types at call sites — **DELIVERED**
|
||||
3. **Constructor-visible virtual dispatch** — `Base b = new Derived(); b.method()` resolves to `Derived#method` when constructor type is a known subclass — **DELIVERED**
|
||||
4. **Covariant return type awareness** — prefer child's return type over inherited definition
|
||||
|
||||
Languages benefiting: Java, Kotlin, C#, C++, TypeScript (overloading). All OOP languages (virtual dispatch).
|
||||
|
||||
**Impact: High | Effort: High**
|
||||
**Impact: High | Effort: High** (P.1–P.3 delivered; P.4 remains open)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -165,6 +165,7 @@ Phase P (polymorphism) ───────────┤
|
|||
│
|
||||
Phase S (Swift parity) ───────────┘
|
||||
|
||||
Phase P.1–P.3 are delivered. P.4 (covariant return types) remains open.
|
||||
Phase P and Phase S are independent of each other and Phase 14.
|
||||
Phase 14 benefits from Phase P (better per-file resolution = fewer cross-file gaps).
|
||||
```
|
||||
|
|
@ -178,6 +179,9 @@ Phase 14 benefits from Phase P (better per-file resolution = fewer cross-file ga
|
|||
- Assignment chains (copy, callResult, fieldAccess, methodCallResult) → Phase S
|
||||
- `guard let` narrowing → Phase S
|
||||
|
||||
### Kotlin
|
||||
- Virtual dispatch: `Dog()` uses `call_expression` (no `new` keyword), not detected by `CONSTRUCTOR_EXPR_TYPES`
|
||||
|
||||
### All languages
|
||||
- Cross-file binding propagation → Phase 14
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue