Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23b25c1da4 | ||
|
|
bf731ec058 | ||
|
|
be1b34ec3d | ||
|
|
f847685bfe | ||
|
|
ae49c4cce7 | ||
|
|
f092b33e40 | ||
|
|
3c5a62982d | ||
|
|
08902f8a18 | ||
|
|
294bfddaf2 | ||
|
|
7a92dce6f4 | ||
|
|
c82aa58fe1 | ||
|
|
d05aa9ef6f | ||
|
|
b4986fdaba | ||
|
|
656af32e52 | ||
|
|
5f12ffccf2 | ||
|
|
f1df9a12c1 | ||
|
|
caa3310714 | ||
|
|
104f9cd311 | ||
|
|
d526ee927c | ||
|
|
1a90112495 | ||
|
|
96cbe8cdae | ||
|
|
00c90d476f | ||
|
|
0c98923729 | ||
|
|
186bd5cf34 |
@@ -1,7 +1,7 @@
|
||||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **GitNexus** (1999 symbols, 4681 relationships, 149 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
This project is indexed by GitNexus as **GitNexus** (2071 symbols, 4727 relationships, 154 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** (1999 symbols, 4681 relationships, 149 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
This project is indexed by GitNexus as **GitNexus** (2071 symbols, 4727 relationships, 154 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.
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ export const processCalls = async (
|
||||
const nodeId = generateId('Property', `${file.path}:${item.propName}`);
|
||||
graph.addNode({
|
||||
id: nodeId,
|
||||
label: 'Property' as any, // TODO: add 'Property' to graph node label union
|
||||
label: 'Property',
|
||||
properties: {
|
||||
name: item.propName, filePath: file.path,
|
||||
startLine: item.startLine, endLine: item.endLine,
|
||||
@@ -575,8 +575,12 @@ function extractFirstTypeArg(args: string): string {
|
||||
return args.trim();
|
||||
}
|
||||
|
||||
const MAX_RETURN_TYPE_INPUT_LENGTH = 2048;
|
||||
const MAX_RETURN_TYPE_LENGTH = 512;
|
||||
|
||||
export const extractReturnTypeName = (raw: string, depth = 0): string | undefined => {
|
||||
if (depth > 10) return undefined;
|
||||
if (raw.length > MAX_RETURN_TYPE_INPUT_LENGTH) return undefined;
|
||||
let text = raw.trim();
|
||||
if (!text) return undefined;
|
||||
|
||||
@@ -625,6 +629,9 @@ export const extractReturnTypeName = (raw: string, depth = 0): string | undefine
|
||||
// Must start with uppercase (class/type convention) or be a valid identifier
|
||||
if (!/^[A-Z_]\w*$/.test(text)) return undefined;
|
||||
|
||||
// If the final extracted type name is too long, reject it
|
||||
if (text.length > MAX_RETURN_TYPE_LENGTH) return undefined;
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
@@ -732,7 +739,6 @@ export const processCallsFromExtracted = async (
|
||||
if (!list) { list = []; byFile.set(call.filePath, list); }
|
||||
list.push(call);
|
||||
}
|
||||
|
||||
const totalFiles = byFile.size;
|
||||
let filesProcessed = 0;
|
||||
|
||||
|
||||
@@ -33,9 +33,15 @@ const CHUNK_BYTE_BUDGET = 20 * 1024 * 1024; // 20MB
|
||||
/** Max AST trees to keep in LRU cache */
|
||||
const AST_CACHE_CAP = 50;
|
||||
|
||||
export interface PipelineOptions {
|
||||
/** Skip MRO, community detection, and process extraction for faster test runs. */
|
||||
skipGraphPhases?: boolean;
|
||||
}
|
||||
|
||||
export const runPipelineFromRepo = async (
|
||||
repoPath: string,
|
||||
onProgress: (progress: PipelineProgress) => void
|
||||
onProgress: (progress: PipelineProgress) => void,
|
||||
options?: PipelineOptions,
|
||||
): Promise<PipelineResult> => {
|
||||
const graph = createKnowledgeGraph();
|
||||
const ctx = createResolutionContext();
|
||||
@@ -154,22 +160,29 @@ export const runPipelineFromRepo = async (
|
||||
stats: { filesProcessed: 0, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
|
||||
});
|
||||
|
||||
// Don't spawn workers for tiny repos — overhead exceeds benefit
|
||||
const MIN_FILES_FOR_WORKERS = 15;
|
||||
const MIN_BYTES_FOR_WORKERS = 512 * 1024;
|
||||
const totalBytes = parseableScanned.reduce((s, f) => s + f.size, 0);
|
||||
|
||||
// Create worker pool once, reuse across chunks
|
||||
let workerPool: WorkerPool | undefined;
|
||||
try {
|
||||
let workerUrl = new URL('./workers/parse-worker.js', import.meta.url);
|
||||
// When running under vitest, import.meta.url points to src/ where no .js exists.
|
||||
// Fall back to the compiled dist/ worker so the pool can spawn real worker threads.
|
||||
const thisDir = fileURLToPath(new URL('.', import.meta.url));
|
||||
if (!fs.existsSync(fileURLToPath(workerUrl))) {
|
||||
const distWorker = path.resolve(thisDir, '..', '..', '..', 'dist', 'core', 'ingestion', 'workers', 'parse-worker.js');
|
||||
if (fs.existsSync(distWorker)) {
|
||||
workerUrl = pathToFileURL(distWorker) as URL;
|
||||
if (totalParseable >= MIN_FILES_FOR_WORKERS || totalBytes >= MIN_BYTES_FOR_WORKERS) {
|
||||
try {
|
||||
let workerUrl = new URL('./workers/parse-worker.js', import.meta.url);
|
||||
// When running under vitest, import.meta.url points to src/ where no .js exists.
|
||||
// Fall back to the compiled dist/ worker so the pool can spawn real worker threads.
|
||||
const thisDir = fileURLToPath(new URL('.', import.meta.url));
|
||||
if (!fs.existsSync(fileURLToPath(workerUrl))) {
|
||||
const distWorker = path.resolve(thisDir, '..', '..', '..', 'dist', 'core', 'ingestion', 'workers', 'parse-worker.js');
|
||||
if (fs.existsSync(distWorker)) {
|
||||
workerUrl = pathToFileURL(distWorker) as URL;
|
||||
}
|
||||
}
|
||||
workerPool = createWorkerPool(workerUrl);
|
||||
} catch (err) {
|
||||
if (isDev) console.warn('Worker pool creation failed, using sequential fallback:', (err as Error).message);
|
||||
}
|
||||
workerPool = createWorkerPool(workerUrl);
|
||||
} catch (err) {
|
||||
if (isDev) console.warn('Worker pool creation failed, using sequential fallback:', (err as Error).message);
|
||||
}
|
||||
|
||||
let filesParsedSoFar = 0;
|
||||
@@ -322,130 +335,137 @@ export const runPipelineFromRepo = async (
|
||||
(importCtx as any).suffixIndex = null;
|
||||
(importCtx as any).normalizedFileList = null;
|
||||
|
||||
// ── Phase 4.5: Method Resolution Order ──────────────────────────────
|
||||
onProgress({
|
||||
phase: 'parsing',
|
||||
percent: 81,
|
||||
message: 'Computing method resolution order...',
|
||||
stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
|
||||
});
|
||||
let communityResult: Awaited<ReturnType<typeof processCommunities>> | undefined;
|
||||
let processResult: Awaited<ReturnType<typeof processProcesses>> | undefined;
|
||||
|
||||
const mroResult = computeMRO(graph);
|
||||
if (isDev && mroResult.entries.length > 0) {
|
||||
console.log(`🔀 MRO: ${mroResult.entries.length} classes analyzed, ${mroResult.ambiguityCount} ambiguities found, ${mroResult.overrideEdges} OVERRIDES edges`);
|
||||
}
|
||||
|
||||
// ── Phase 5: Communities ───────────────────────────────────────────
|
||||
onProgress({
|
||||
phase: 'communities',
|
||||
percent: 82,
|
||||
message: 'Detecting code communities...',
|
||||
stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
|
||||
});
|
||||
|
||||
const communityResult = await processCommunities(graph, (message, progress) => {
|
||||
const communityProgress = 82 + (progress * 0.10);
|
||||
if (!options?.skipGraphPhases) {
|
||||
// ── Phase 4.5: Method Resolution Order ──────────────────────────────
|
||||
onProgress({
|
||||
phase: 'communities',
|
||||
percent: Math.round(communityProgress),
|
||||
message,
|
||||
phase: 'parsing',
|
||||
percent: 81,
|
||||
message: 'Computing method resolution order...',
|
||||
stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
|
||||
});
|
||||
});
|
||||
|
||||
if (isDev) {
|
||||
console.log(`🏘️ Community detection: ${communityResult.stats.totalCommunities} communities found (modularity: ${communityResult.stats.modularity.toFixed(3)})`);
|
||||
}
|
||||
const mroResult = computeMRO(graph);
|
||||
if (isDev && mroResult.entries.length > 0) {
|
||||
console.log(`🔀 MRO: ${mroResult.entries.length} classes analyzed, ${mroResult.ambiguityCount} ambiguities found, ${mroResult.overrideEdges} OVERRIDES edges`);
|
||||
}
|
||||
|
||||
communityResult.communities.forEach(comm => {
|
||||
graph.addNode({
|
||||
id: comm.id,
|
||||
label: 'Community' as const,
|
||||
properties: {
|
||||
name: comm.label,
|
||||
filePath: '',
|
||||
heuristicLabel: comm.heuristicLabel,
|
||||
cohesion: comm.cohesion,
|
||||
symbolCount: comm.symbolCount,
|
||||
}
|
||||
// ── Phase 5: Communities ───────────────────────────────────────────
|
||||
onProgress({
|
||||
phase: 'communities',
|
||||
percent: 82,
|
||||
message: 'Detecting code communities...',
|
||||
stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
|
||||
});
|
||||
});
|
||||
|
||||
communityResult.memberships.forEach(membership => {
|
||||
graph.addRelationship({
|
||||
id: `${membership.nodeId}_member_of_${membership.communityId}`,
|
||||
type: 'MEMBER_OF',
|
||||
sourceId: membership.nodeId,
|
||||
targetId: membership.communityId,
|
||||
confidence: 1.0,
|
||||
reason: 'leiden-algorithm',
|
||||
});
|
||||
});
|
||||
|
||||
// ── Phase 6: Processes ─────────────────────────────────────────────
|
||||
onProgress({
|
||||
phase: 'processes',
|
||||
percent: 94,
|
||||
message: 'Detecting execution flows...',
|
||||
stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
|
||||
});
|
||||
|
||||
let symbolCount = 0;
|
||||
graph.forEachNode(n => { if (n.label !== 'File') symbolCount++; });
|
||||
const dynamicMaxProcesses = Math.max(20, Math.min(300, Math.round(symbolCount / 10)));
|
||||
|
||||
const processResult = await processProcesses(
|
||||
graph,
|
||||
communityResult.memberships,
|
||||
(message, progress) => {
|
||||
const processProgress = 94 + (progress * 0.05);
|
||||
communityResult = await processCommunities(graph, (message, progress) => {
|
||||
const communityProgress = 82 + (progress * 0.10);
|
||||
onProgress({
|
||||
phase: 'processes',
|
||||
percent: Math.round(processProgress),
|
||||
phase: 'communities',
|
||||
percent: Math.round(communityProgress),
|
||||
message,
|
||||
stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
|
||||
});
|
||||
},
|
||||
{ maxProcesses: dynamicMaxProcesses, minSteps: 3 }
|
||||
);
|
||||
});
|
||||
|
||||
if (isDev) {
|
||||
console.log(`🔄 Process detection: ${processResult.stats.totalProcesses} processes found (${processResult.stats.crossCommunityCount} cross-community)`);
|
||||
if (isDev) {
|
||||
console.log(`🏘️ Community detection: ${communityResult.stats.totalCommunities} communities found (modularity: ${communityResult.stats.modularity.toFixed(3)})`);
|
||||
}
|
||||
|
||||
communityResult.communities.forEach(comm => {
|
||||
graph.addNode({
|
||||
id: comm.id,
|
||||
label: 'Community' as const,
|
||||
properties: {
|
||||
name: comm.label,
|
||||
filePath: '',
|
||||
heuristicLabel: comm.heuristicLabel,
|
||||
cohesion: comm.cohesion,
|
||||
symbolCount: comm.symbolCount,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
communityResult.memberships.forEach(membership => {
|
||||
graph.addRelationship({
|
||||
id: `${membership.nodeId}_member_of_${membership.communityId}`,
|
||||
type: 'MEMBER_OF',
|
||||
sourceId: membership.nodeId,
|
||||
targetId: membership.communityId,
|
||||
confidence: 1.0,
|
||||
reason: 'leiden-algorithm',
|
||||
});
|
||||
});
|
||||
|
||||
// ── Phase 6: Processes ─────────────────────────────────────────────
|
||||
onProgress({
|
||||
phase: 'processes',
|
||||
percent: 94,
|
||||
message: 'Detecting execution flows...',
|
||||
stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
|
||||
});
|
||||
|
||||
let symbolCount = 0;
|
||||
graph.forEachNode(n => { if (n.label !== 'File') symbolCount++; });
|
||||
const dynamicMaxProcesses = Math.max(20, Math.min(300, Math.round(symbolCount / 10)));
|
||||
|
||||
processResult = await processProcesses(
|
||||
graph,
|
||||
communityResult.memberships,
|
||||
(message, progress) => {
|
||||
const processProgress = 94 + (progress * 0.05);
|
||||
onProgress({
|
||||
phase: 'processes',
|
||||
percent: Math.round(processProgress),
|
||||
message,
|
||||
stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
|
||||
});
|
||||
},
|
||||
{ maxProcesses: dynamicMaxProcesses, minSteps: 3 }
|
||||
);
|
||||
|
||||
if (isDev) {
|
||||
console.log(`🔄 Process detection: ${processResult.stats.totalProcesses} processes found (${processResult.stats.crossCommunityCount} cross-community)`);
|
||||
}
|
||||
|
||||
processResult.processes.forEach(proc => {
|
||||
graph.addNode({
|
||||
id: proc.id,
|
||||
label: 'Process' as const,
|
||||
properties: {
|
||||
name: proc.label,
|
||||
filePath: '',
|
||||
heuristicLabel: proc.heuristicLabel,
|
||||
processType: proc.processType,
|
||||
stepCount: proc.stepCount,
|
||||
communities: proc.communities,
|
||||
entryPointId: proc.entryPointId,
|
||||
terminalId: proc.terminalId,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
processResult.steps.forEach(step => {
|
||||
graph.addRelationship({
|
||||
id: `${step.nodeId}_step_${step.step}_${step.processId}`,
|
||||
type: 'STEP_IN_PROCESS',
|
||||
sourceId: step.nodeId,
|
||||
targetId: step.processId,
|
||||
confidence: 1.0,
|
||||
reason: 'trace-detection',
|
||||
step: step.step,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
processResult.processes.forEach(proc => {
|
||||
graph.addNode({
|
||||
id: proc.id,
|
||||
label: 'Process' as const,
|
||||
properties: {
|
||||
name: proc.label,
|
||||
filePath: '',
|
||||
heuristicLabel: proc.heuristicLabel,
|
||||
processType: proc.processType,
|
||||
stepCount: proc.stepCount,
|
||||
communities: proc.communities,
|
||||
entryPointId: proc.entryPointId,
|
||||
terminalId: proc.terminalId,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
processResult.steps.forEach(step => {
|
||||
graph.addRelationship({
|
||||
id: `${step.nodeId}_step_${step.step}_${step.processId}`,
|
||||
type: 'STEP_IN_PROCESS',
|
||||
sourceId: step.nodeId,
|
||||
targetId: step.processId,
|
||||
confidence: 1.0,
|
||||
reason: 'trace-detection',
|
||||
step: step.step,
|
||||
});
|
||||
});
|
||||
|
||||
onProgress({
|
||||
phase: 'complete',
|
||||
percent: 100,
|
||||
message: `Graph complete! ${communityResult.stats.totalCommunities} communities, ${processResult.stats.totalProcesses} processes detected.`,
|
||||
message: communityResult && processResult
|
||||
? `Graph complete! ${communityResult.stats.totalCommunities} communities, ${processResult.stats.totalProcesses} processes detected.`
|
||||
: 'Graph complete! (graph phases skipped)',
|
||||
stats: {
|
||||
filesProcessed: totalFiles,
|
||||
totalFiles,
|
||||
|
||||
@@ -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, stripNullable } from './type-extractors/shared.js';
|
||||
import { extractSimpleTypeName, extractVarName, stripNullable } from './type-extractors/shared.js';
|
||||
import type { SymbolTable } from './symbol-table.js';
|
||||
|
||||
/**
|
||||
@@ -48,11 +48,57 @@ export interface TypeEnvironment {
|
||||
readonly env: TypeEnv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Position-indexed pattern binding: active only within a specific AST range.
|
||||
* Used for smart-cast narrowing in mutually exclusive branches (e.g., Kotlin when arms).
|
||||
*/
|
||||
interface PatternOverride {
|
||||
rangeStart: number;
|
||||
rangeEnd: number;
|
||||
typeName: string;
|
||||
}
|
||||
|
||||
/** scope → varName → overrides (checked in order, first range match wins) */
|
||||
type PatternOverrides = Map<string, Map<string, PatternOverride[]>>;
|
||||
|
||||
/** AST node types that represent mutually exclusive branch containers for pattern bindings. */
|
||||
const PATTERN_BRANCH_TYPES = new Set([
|
||||
'when_entry', // Kotlin when
|
||||
'switch_block_label', // Java switch (enhanced)
|
||||
]);
|
||||
|
||||
/** Walk up the AST from a pattern node to find the enclosing branch container. */
|
||||
const findPatternBranchScope = (node: SyntaxNode): SyntaxNode | undefined => {
|
||||
let current = node.parent;
|
||||
while (current) {
|
||||
if (PATTERN_BRANCH_TYPES.has(current.type)) return current;
|
||||
if (FUNCTION_NODE_TYPES.has(current.type)) return undefined;
|
||||
current = current.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Bare nullable keywords that fastStripNullable must reject. */
|
||||
const FAST_NULLABLE_KEYWORDS = new Set(['null', 'undefined', 'void', 'None', 'nil']);
|
||||
|
||||
/**
|
||||
* Fast-path nullable check: 90%+ of type names are simple identifiers (e.g. "User")
|
||||
* that don't need the full stripNullable parse. Only call stripNullable when the
|
||||
* string contains nullable markers ('|' for union types, '?' for nullable suffix).
|
||||
*/
|
||||
const fastStripNullable = (typeName: string): string | undefined => {
|
||||
if (FAST_NULLABLE_KEYWORDS.has(typeName)) return undefined;
|
||||
return (typeName.indexOf('|') === -1 && typeName.indexOf('?') === -1)
|
||||
? typeName
|
||||
: stripNullable(typeName);
|
||||
};
|
||||
|
||||
/** Implementation of the lookup logic — shared between TypeEnvironment and the legacy export. */
|
||||
const lookupInEnv = (
|
||||
env: TypeEnv,
|
||||
varName: string,
|
||||
callNode: SyntaxNode,
|
||||
patternOverrides?: PatternOverrides,
|
||||
): string | undefined => {
|
||||
// Self/this receiver: resolve to enclosing class name via AST walk
|
||||
if (varName === 'self' || varName === 'this' || varName === '$this') {
|
||||
@@ -68,19 +114,33 @@ const lookupInEnv = (
|
||||
// Determine the enclosing function scope for the call
|
||||
const scopeKey = findEnclosingScopeKey(callNode);
|
||||
|
||||
// Check position-indexed pattern overrides first (e.g., Kotlin when/is smart casts).
|
||||
// These take priority over flat scopeEnv because they represent per-branch narrowing.
|
||||
if (scopeKey && patternOverrides) {
|
||||
const varOverrides = patternOverrides.get(scopeKey)?.get(varName);
|
||||
if (varOverrides) {
|
||||
const pos = callNode.startIndex;
|
||||
for (const override of varOverrides) {
|
||||
if (pos >= override.rangeStart && pos <= override.rangeEnd) {
|
||||
return fastStripNullable(override.typeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try function-local scope first
|
||||
if (scopeKey) {
|
||||
const scopeEnv = env.get(scopeKey);
|
||||
if (scopeEnv) {
|
||||
const result = scopeEnv.get(varName);
|
||||
if (result) return stripNullable(result);
|
||||
if (result) return fastStripNullable(result);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to file-level scope
|
||||
const fileEnv = env.get(FILE_SCOPE);
|
||||
const raw = fileEnv?.get(varName);
|
||||
return raw ? stripNullable(raw) : undefined;
|
||||
return raw ? fastStripNullable(raw) : undefined;
|
||||
};
|
||||
|
||||
|
||||
@@ -283,20 +343,52 @@ const createClassNameLookup = (
|
||||
* the project are available for constructor inference in languages like Kotlin
|
||||
* where constructors are syntactically identical to function calls.
|
||||
*/
|
||||
/**
|
||||
* Node types whose subtrees can NEVER contain type-relevant descendants
|
||||
* (declarations, parameters, for-loops, class definitions, pattern bindings).
|
||||
* Conservative leaf-only set — verified safe across all 12 supported language grammars.
|
||||
* IMPORTANT: Do NOT add expression containers (arguments, binary_expression, etc.) —
|
||||
* they can contain arrow functions with typed parameters.
|
||||
*/
|
||||
const SKIP_SUBTREE_TYPES = new Set([
|
||||
// Plain string literals (NOT template_string — it contains interpolated expressions
|
||||
// that can hold arrow functions with typed parameters, e.g. `${(x: T) => x}`)
|
||||
'string', 'string_literal',
|
||||
'string_content', 'string_fragment', 'heredoc_body',
|
||||
// Comments
|
||||
'comment', 'line_comment', 'block_comment',
|
||||
// Numeric/boolean/null literals
|
||||
'number', 'integer_literal', 'float_literal',
|
||||
'true', 'false', 'null',
|
||||
// Regex
|
||||
'regex', 'regex_pattern',
|
||||
]);
|
||||
|
||||
export const buildTypeEnv = (
|
||||
tree: { rootNode: SyntaxNode },
|
||||
language: SupportedLanguages,
|
||||
symbolTable?: SymbolTable,
|
||||
): TypeEnvironment => {
|
||||
const env: TypeEnv = new Map();
|
||||
const patternOverrides: PatternOverrides = new Map();
|
||||
const localClassNames = new Set<string>();
|
||||
const classNames = createClassNameLookup(localClassNames, symbolTable);
|
||||
const config = typeConfigs[language];
|
||||
const bindings: ConstructorBinding[] = [];
|
||||
|
||||
// Pre-compute combined set of node types that need extractTypeBinding.
|
||||
// Single Set.has() replaces 3 separate checks per node in walk().
|
||||
const interestingNodeTypes = new Set<string>();
|
||||
TYPED_PARAMETER_TYPES.forEach(t => interestingNodeTypes.add(t));
|
||||
config.declarationNodeTypes.forEach(t => interestingNodeTypes.add(t));
|
||||
config.forLoopNodeTypes?.forEach(t => interestingNodeTypes.add(t));
|
||||
const pendingAssignments: Array<{ scope: string; lhs: string; rhs: string }> = [];
|
||||
// Maps `scope\0varName` → the type annotation AST node from the original declaration.
|
||||
// Allows pattern extractors to navigate back to the declaration's generic type arguments
|
||||
// (e.g., to extract T from Result<T, E> for `if let Ok(x) = res`).
|
||||
// NOTE: This is a SUPERSET of scopeEnv — entries exist even when extractSimpleTypeName
|
||||
// returns undefined for container types (User[], []User, List[User]). This is intentional:
|
||||
// for-loop Strategy 1 needs the raw AST type node for exactly those container types.
|
||||
const declarationTypeNodes = new Map<string, SyntaxNode>();
|
||||
|
||||
/**
|
||||
@@ -314,35 +406,91 @@ export const buildTypeEnv = (
|
||||
const extractTypeBinding = (node: SyntaxNode, scopeEnv: Map<string, string>, scope: string): void => {
|
||||
// This guard eliminates 90%+ of calls before any language dispatch.
|
||||
if (TYPED_PARAMETER_TYPES.has(node.type)) {
|
||||
const keysBefore = new Set(scopeEnv.keys());
|
||||
config.extractParameter(node, scopeEnv);
|
||||
// Capture the type node for newly introduced parameter bindings
|
||||
const typeNode = node.childForFieldName('type');
|
||||
// Capture the raw type annotation BEFORE extractParameter.
|
||||
// Most languages use 'name' field; Rust uses 'pattern'; TS uses 'pattern' for some param types.
|
||||
// Kotlin `parameter` nodes use positional children instead of named fields,
|
||||
// so we fall back to scanning children by type when childForFieldName returns null.
|
||||
let typeNode = node.childForFieldName('type');
|
||||
if (typeNode) {
|
||||
for (const varName of scopeEnv.keys()) {
|
||||
if (!keysBefore.has(varName)) {
|
||||
const nameNode = node.childForFieldName('name')
|
||||
?? node.childForFieldName('pattern');
|
||||
if (nameNode) {
|
||||
const varName = extractVarName(nameNode);
|
||||
if (varName && !declarationTypeNodes.has(`${scope}\0${varName}`)) {
|
||||
declarationTypeNodes.set(`${scope}\0${varName}`, typeNode);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: positional children (Kotlin `parameter` → simple_identifier + user_type)
|
||||
let fallbackName: SyntaxNode | null = null;
|
||||
let fallbackType: SyntaxNode | null = null;
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (!child) continue;
|
||||
if (!fallbackName && (child.type === 'simple_identifier' || child.type === 'identifier')) {
|
||||
fallbackName = child;
|
||||
}
|
||||
if (!fallbackType && (child.type === 'user_type' || child.type === 'type_identifier'
|
||||
|| child.type === 'generic_type' || child.type === 'parameterized_type')) {
|
||||
fallbackType = child;
|
||||
}
|
||||
}
|
||||
if (fallbackName && fallbackType) {
|
||||
const varName = extractVarName(fallbackName);
|
||||
if (varName && !declarationTypeNodes.has(`${scope}\0${varName}`)) {
|
||||
declarationTypeNodes.set(`${scope}\0${varName}`, fallbackType);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
config.extractForLoopBinding?.(node, scopeEnv, declarationTypeNodes, scope);
|
||||
return;
|
||||
}
|
||||
if (config.declarationNodeTypes.has(node.type)) {
|
||||
const keysBefore = new Set(scopeEnv.keys());
|
||||
config.extractDeclaration(node, scopeEnv);
|
||||
// Capture the type annotation AST node for newly introduced bindings.
|
||||
// Only declarations with an explicit 'type' field are recorded — constructor
|
||||
// inferences (Tier 1) don't have a type annotation node to preserve.
|
||||
const typeNode = node.childForFieldName('type');
|
||||
// Capture the raw type annotation AST node BEFORE extractDeclaration.
|
||||
// This decouples type node capture from scopeEnv success — container types
|
||||
// (User[], []User, List[User]) that fail extractSimpleTypeName still get
|
||||
// their AST type node recorded for Strategy 1 for-loop resolution.
|
||||
// Try direct extraction first (works for Go var_spec, Python assignment, Rust let_declaration).
|
||||
// Try direct type field first, then unwrap wrapper nodes (C# field_declaration,
|
||||
// local_declaration_statement wrap their type inside a variable_declaration child).
|
||||
let typeNode = node.childForFieldName('type');
|
||||
if (!typeNode) {
|
||||
// C# field_declaration / local_declaration_statement wrap type inside variable_declaration.
|
||||
// Use manual loop instead of namedChildren.find() to avoid array allocation on hot path.
|
||||
let wrapped = node.childForFieldName('declaration');
|
||||
if (!wrapped) {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const c = node.namedChild(i);
|
||||
if (c?.type === 'variable_declaration') { wrapped = c; break; }
|
||||
}
|
||||
}
|
||||
if (wrapped) typeNode = wrapped.childForFieldName('type');
|
||||
}
|
||||
if (typeNode) {
|
||||
const nameNode = node.childForFieldName('name')
|
||||
?? node.childForFieldName('left')
|
||||
?? node.childForFieldName('pattern');
|
||||
if (nameNode) {
|
||||
const varName = extractVarName(nameNode);
|
||||
if (varName && !declarationTypeNodes.has(`${scope}\0${varName}`)) {
|
||||
declarationTypeNodes.set(`${scope}\0${varName}`, typeNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Run the language-specific declaration extractor (may or may not add to scopeEnv).
|
||||
const keysBefore = typeNode ? new Set(scopeEnv.keys()) : undefined;
|
||||
config.extractDeclaration(node, scopeEnv);
|
||||
// Fallback: for multi-declarator languages (TS, C#, Java) where the type field
|
||||
// is on variable_declarator children, capture via keysBefore/keysAfter diff.
|
||||
if (typeNode && keysBefore) {
|
||||
for (const varName of scopeEnv.keys()) {
|
||||
if (!keysBefore.has(varName)) {
|
||||
if (!keysBefore.has(varName) && !declarationTypeNodes.has(`${scope}\0${varName}`)) {
|
||||
declarationTypeNodes.set(`${scope}\0${varName}`, typeNode);
|
||||
}
|
||||
}
|
||||
@@ -358,6 +506,9 @@ export const buildTypeEnv = (
|
||||
};
|
||||
|
||||
const walk = (node: SyntaxNode, currentScope: string): void => {
|
||||
// Fast skip: subtrees that can never contain type-relevant nodes (leaf-like literals).
|
||||
if (SKIP_SUBTREE_TYPES.has(node.type)) return;
|
||||
|
||||
// Collect class/struct names as we encounter them (used by extractInitializer
|
||||
// to distinguish constructor calls from function calls, e.g. C++ `User()` vs `getUser()`)
|
||||
// Currently only C++ uses this locally; other languages rely on the SymbolTable path.
|
||||
@@ -375,20 +526,46 @@ export const buildTypeEnv = (
|
||||
if (funcName) scope = `${funcName}@${node.startIndex}`;
|
||||
}
|
||||
|
||||
// Get or create the sub-map for this scope
|
||||
if (!env.has(scope)) env.set(scope, new Map());
|
||||
const scopeEnv = env.get(scope)!;
|
||||
|
||||
extractTypeBinding(node, scopeEnv, scope);
|
||||
// Only create scope map and call extractTypeBinding for interesting node types.
|
||||
// Single Set.has() replaces 3 separate checks inside extractTypeBinding.
|
||||
if (interestingNodeTypes.has(node.type)) {
|
||||
if (!env.has(scope)) env.set(scope, new Map());
|
||||
const scopeEnv = env.get(scope)!;
|
||||
extractTypeBinding(node, scopeEnv, scope);
|
||||
}
|
||||
|
||||
// Pattern binding extraction: handles constructs that introduce NEW typed variables
|
||||
// via pattern matching (e.g. `if let Some(x) = opt`, `x instanceof T t`).
|
||||
// Runs after Tier 0/1 so scopeEnv already contains the source variable's type.
|
||||
// Conservative: extractor returns undefined when source type is unknown.
|
||||
if (config.extractPatternBinding) {
|
||||
if (config.extractPatternBinding && (!config.patternBindingNodeTypes || config.patternBindingNodeTypes.has(node.type))) {
|
||||
// Ensure scopeEnv exists for pattern binding reads/writes
|
||||
if (!env.has(scope)) env.set(scope, new Map());
|
||||
const scopeEnv = env.get(scope)!;
|
||||
const patternBinding = config.extractPatternBinding(node, scopeEnv, declarationTypeNodes, scope);
|
||||
if (patternBinding && !scopeEnv.has(patternBinding.varName)) {
|
||||
scopeEnv.set(patternBinding.varName, patternBinding.typeName);
|
||||
if (patternBinding) {
|
||||
if (config.allowPatternBindingOverwrite) {
|
||||
// Position-indexed: store per-branch binding for smart-cast narrowing.
|
||||
// Each when arm / switch case gets its own type for the variable,
|
||||
// preventing cross-arm contamination (e.g., Kotlin when/is).
|
||||
const branchNode = findPatternBranchScope(node);
|
||||
if (branchNode) {
|
||||
if (!patternOverrides.has(scope)) patternOverrides.set(scope, new Map());
|
||||
const varMap = patternOverrides.get(scope)!;
|
||||
if (!varMap.has(patternBinding.varName)) varMap.set(patternBinding.varName, []);
|
||||
varMap.get(patternBinding.varName)!.push({
|
||||
rangeStart: branchNode.startIndex,
|
||||
rangeEnd: branchNode.endIndex,
|
||||
typeName: patternBinding.typeName,
|
||||
});
|
||||
}
|
||||
// Also store in flat scopeEnv as fallback (last arm wins — same as before
|
||||
// for code that doesn't use position-indexed lookup).
|
||||
scopeEnv.set(patternBinding.varName, patternBinding.typeName);
|
||||
} else if (!scopeEnv.has(patternBinding.varName)) {
|
||||
// First-writer-wins for languages without smart-cast overwrite (Java instanceof, etc.)
|
||||
scopeEnv.set(patternBinding.varName, patternBinding.typeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,9 +574,14 @@ export const buildTypeEnv = (
|
||||
// (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 });
|
||||
// scopeEnv is guaranteed to exist here because declarationNodeTypes is a subset
|
||||
// of interestingNodeTypes, so extractTypeBinding already created the scope map above.
|
||||
const scopeEnv = env.get(scope);
|
||||
if (scopeEnv) {
|
||||
const pending = config.extractPendingAssignment(node, scopeEnv);
|
||||
if (pending) {
|
||||
pendingAssignments.push({ scope, ...pending });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,8 +589,11 @@ export const buildTypeEnv = (
|
||||
// Only collect if TypeEnv didn't already resolve this binding.
|
||||
if (config.scanConstructorBinding) {
|
||||
const result = config.scanConstructorBinding(node);
|
||||
if (result && !scopeEnv.has(result.varName)) {
|
||||
bindings.push({ scope, ...result });
|
||||
if (result) {
|
||||
const scopeEnv = env.get(scope);
|
||||
if (!scopeEnv?.has(result.varName)) {
|
||||
bindings.push({ scope, ...result });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,7 +621,7 @@ export const buildTypeEnv = (
|
||||
}
|
||||
|
||||
return {
|
||||
lookup: (varName, callNode) => lookupInEnv(env, varName, callNode),
|
||||
lookup: (varName, callNode) => lookupInEnv(env, varName, callNode, patternOverrides),
|
||||
constructorBindings: bindings,
|
||||
env,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName } from './shared.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor, ForLoopExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, resolveIterableElementType, methodToTypeArgPosition, type TypeArgPosition } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'declaration',
|
||||
'for_range_loop',
|
||||
]);
|
||||
|
||||
/** C++: Type x = ...; Type* x; Type& x; */
|
||||
@@ -183,11 +182,183 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
||||
return { lhs, rhs: value.text };
|
||||
};
|
||||
|
||||
// --- For-loop Tier 1c ---
|
||||
|
||||
const FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set(['for_range_loop']);
|
||||
|
||||
/** Extract template type arguments from a C++ template_type node.
|
||||
* C++ template_type uses template_argument_list (not type_arguments), and each
|
||||
* argument is a type_descriptor with a 'type' field containing the type_specifier. */
|
||||
const extractCppTemplateTypeArgs = (templateTypeNode: SyntaxNode): string[] => {
|
||||
const argsNode = templateTypeNode.childForFieldName('arguments');
|
||||
if (!argsNode || argsNode.type !== 'template_argument_list') return [];
|
||||
const result: string[] = [];
|
||||
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
||||
let argNode = argsNode.namedChild(i);
|
||||
if (!argNode) continue;
|
||||
// type_descriptor wraps the actual type specifier in a 'type' field
|
||||
if (argNode.type === 'type_descriptor') {
|
||||
const inner = argNode.childForFieldName('type');
|
||||
if (inner) argNode = inner;
|
||||
}
|
||||
const name = extractSimpleTypeName(argNode);
|
||||
if (name) result.push(name);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/** Extract element type from a C++ type annotation AST node.
|
||||
* Handles: template_type (vector<User>, map<string, User>),
|
||||
* pointer/reference types (User*, User&). */
|
||||
const extractCppElementTypeFromTypeNode = (typeNode: SyntaxNode, pos: TypeArgPosition = 'last', depth = 0): string | undefined => {
|
||||
if (depth > 50) return undefined;
|
||||
// template_type: vector<User>, map<string, User> — extract type arg based on position
|
||||
if (typeNode.type === 'template_type') {
|
||||
const args = extractCppTemplateTypeArgs(typeNode);
|
||||
if (args.length >= 1) return pos === 'first' ? args[0] : args[args.length - 1];
|
||||
}
|
||||
// reference/pointer types: unwrap and recurse (vector<User>& → vector<User>)
|
||||
if (typeNode.type === 'reference_type' || typeNode.type === 'pointer_type'
|
||||
|| typeNode.type === 'type_descriptor') {
|
||||
const inner = typeNode.lastNamedChild;
|
||||
if (inner) return extractCppElementTypeFromTypeNode(inner, pos, depth + 1);
|
||||
}
|
||||
// qualified/scoped types: std::vector<User> → unwrap to template_type child
|
||||
if (typeNode.type === 'qualified_identifier' || typeNode.type === 'scoped_type_identifier') {
|
||||
const inner = typeNode.lastNamedChild;
|
||||
if (inner) return extractCppElementTypeFromTypeNode(inner, pos, depth + 1);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Walk up from a for-range-loop to the enclosing function_definition and search parameters
|
||||
* for one named `iterableName`. Returns the element type from its annotation. */
|
||||
const findCppParamElementType = (iterableName: string, startNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
while (current) {
|
||||
if (current.type === 'function_definition') {
|
||||
const declarator = current.childForFieldName('declarator');
|
||||
// function_definition > declarator (function_declarator) > parameters (parameter_list)
|
||||
const paramsNode = declarator?.childForFieldName('parameters');
|
||||
if (paramsNode) {
|
||||
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
||||
const param = paramsNode.namedChild(i);
|
||||
if (!param || param.type !== 'parameter_declaration') continue;
|
||||
const paramDeclarator = param.childForFieldName('declarator');
|
||||
if (!paramDeclarator) continue;
|
||||
// Unwrap reference/pointer declarators: vector<User>& users → &users
|
||||
let identNode = paramDeclarator;
|
||||
if (identNode.type === 'reference_declarator' || identNode.type === 'pointer_declarator') {
|
||||
identNode = identNode.firstNamedChild ?? identNode;
|
||||
}
|
||||
if (identNode.text !== iterableName) continue;
|
||||
const typeNode = param.childForFieldName('type');
|
||||
if (typeNode) return extractCppElementTypeFromTypeNode(typeNode, pos);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** C++: for (auto& user : users) — extract loop variable binding.
|
||||
* Handles explicit types (for (User& user : users)) and auto (for (auto& user : users)).
|
||||
* For auto, resolves element type from the iterable's container type. */
|
||||
const extractForLoopBinding: ForLoopExtractor = (
|
||||
node: SyntaxNode,
|
||||
scopeEnv: Map<string, string>,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
scope: string,
|
||||
): void => {
|
||||
if (node.type !== 'for_range_loop') return;
|
||||
|
||||
const typeNode = node.childForFieldName('type');
|
||||
const declaratorNode = node.childForFieldName('declarator');
|
||||
const rightNode = node.childForFieldName('right');
|
||||
if (!typeNode || !declaratorNode || !rightNode) return;
|
||||
|
||||
// Unwrap reference/pointer declarator to get the loop variable name
|
||||
let nameNode = declaratorNode;
|
||||
if (nameNode.type === 'reference_declarator' || nameNode.type === 'pointer_declarator') {
|
||||
nameNode = nameNode.firstNamedChild ?? nameNode;
|
||||
}
|
||||
|
||||
// Handle structured bindings: auto& [key, value] or auto [key, value]
|
||||
// Bind the last identifier (value heuristic for [key, value] patterns)
|
||||
let loopVarName: string | undefined;
|
||||
if (nameNode.type === 'structured_binding_declarator') {
|
||||
const lastChild = nameNode.lastNamedChild;
|
||||
if (lastChild?.type === 'identifier') {
|
||||
loopVarName = lastChild.text;
|
||||
}
|
||||
} else if (declaratorNode.type === 'structured_binding_declarator') {
|
||||
const lastChild = declaratorNode.lastNamedChild;
|
||||
if (lastChild?.type === 'identifier') {
|
||||
loopVarName = lastChild.text;
|
||||
}
|
||||
}
|
||||
|
||||
const varName = loopVarName ?? extractVarName(nameNode);
|
||||
if (!varName) return;
|
||||
|
||||
// Check if the type is auto/placeholder — if not, use the explicit type directly
|
||||
const isAuto = typeNode.type === 'placeholder_type_specifier'
|
||||
|| typeNode.text === 'auto'
|
||||
|| typeNode.text === 'const auto'
|
||||
|| typeNode.text === 'decltype(auto)';
|
||||
|
||||
if (!isAuto) {
|
||||
// Explicit type: for (User& user : users) — extract directly
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
if (typeName) scopeEnv.set(varName, typeName);
|
||||
return;
|
||||
}
|
||||
|
||||
// auto/const auto/auto& — resolve from the iterable's container type
|
||||
// Extract iterable name + optional method
|
||||
let iterableName: string | undefined;
|
||||
let methodName: string | undefined;
|
||||
if (rightNode.type === 'identifier') {
|
||||
iterableName = rightNode.text;
|
||||
} else if (rightNode.type === 'field_expression') {
|
||||
const prop = rightNode.lastNamedChild;
|
||||
if (prop) iterableName = prop.text;
|
||||
} else if (rightNode.type === 'call_expression') {
|
||||
// users.begin() is NOT used in range-for, but container.items() etc. might be
|
||||
const fieldExpr = rightNode.childForFieldName('function');
|
||||
if (fieldExpr?.type === 'field_expression') {
|
||||
const obj = fieldExpr.firstNamedChild;
|
||||
if (obj?.type === 'identifier') iterableName = obj.text;
|
||||
const field = fieldExpr.lastNamedChild;
|
||||
if (field?.type === 'field_identifier') methodName = field.text;
|
||||
}
|
||||
} else if (rightNode.type === 'pointer_expression') {
|
||||
// Dereference: for (auto& user : *ptr) → pointer_expression > identifier
|
||||
// Only handles simple *identifier; *this->field and **ptr are not resolved.
|
||||
const operand = rightNode.lastNamedChild;
|
||||
if (operand?.type === 'identifier') iterableName = operand.text;
|
||||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
const containerTypeName = scopeEnv.get(iterableName);
|
||||
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractCppElementTypeFromTypeNode, findCppParamElementType,
|
||||
typeArgPos,
|
||||
);
|
||||
if (elementType) scopeEnv.set(varName, elementType);
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
extractInitializer,
|
||||
scanConstructorBinding,
|
||||
extractForLoopBinding,
|
||||
extractPendingAssignment,
|
||||
};
|
||||
|
||||
@@ -1,31 +1,18 @@
|
||||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { ConstructorBindingScanner, ForLoopExtractor, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, PendingAssignmentExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, findChildByType, unwrapAwait } from './shared.js';
|
||||
import type { ConstructorBindingScanner, ForLoopExtractor, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, PendingAssignmentExtractor, PatternBindingExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, findChildByType, unwrapAwait, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, type TypeArgPosition } from './shared.js';
|
||||
|
||||
/** Known container property accessors that operate on the container itself (e.g., dict.Keys, dict.Values) */
|
||||
const KNOWN_CONTAINER_PROPS: ReadonlySet<string> = new Set(['Keys', 'Values']);
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'local_declaration_statement',
|
||||
'variable_declaration',
|
||||
'field_declaration',
|
||||
'is_pattern_expression',
|
||||
]);
|
||||
|
||||
/** C#: Type x = ...; var x = new Type(); obj is Type x */
|
||||
/** C#: Type x = ...; var x = new Type(); */
|
||||
const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
|
||||
// C# pattern matching: `obj is User user` → is_pattern_expression > declaration_pattern
|
||||
if (node.type === 'is_pattern_expression') {
|
||||
const pattern = node.childForFieldName('pattern');
|
||||
if (pattern?.type === 'declaration_pattern') {
|
||||
const typeNode = pattern.childForFieldName('type');
|
||||
const nameNode = pattern.childForFieldName('name');
|
||||
if (typeNode && nameNode) {
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
const varName = extractVarName(nameNode);
|
||||
if (typeName && varName) env.set(varName, typeName);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// C# tree-sitter: local_declaration_statement > variable_declaration > ...
|
||||
// Recursively descend through wrapper nodes
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
@@ -147,17 +134,170 @@ 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 => {
|
||||
/** Extract element type from a C# type annotation AST node.
|
||||
* Handles generic_name (List<User>), array_type (User[]), nullable_type (?).
|
||||
* `pos` selects which type arg: 'first' for keys, 'last' for values (default). */
|
||||
const extractCSharpElementTypeFromTypeNode = (typeNode: SyntaxNode, pos: TypeArgPosition = 'last', depth = 0): string | undefined => {
|
||||
if (depth > 50) return undefined;
|
||||
// generic_name: List<User>, IEnumerable<User>, Dictionary<string, User>
|
||||
// C# uses generic_name (not generic_type)
|
||||
if (typeNode.type === 'generic_name') {
|
||||
const argList = findChildByType(typeNode, 'type_argument_list');
|
||||
if (argList && argList.namedChildCount >= 1) {
|
||||
if (pos === 'first') {
|
||||
const firstArg = argList.namedChild(0);
|
||||
if (firstArg) return extractSimpleTypeName(firstArg);
|
||||
} else {
|
||||
const lastArg = argList.namedChild(argList.namedChildCount - 1);
|
||||
if (lastArg) return extractSimpleTypeName(lastArg);
|
||||
}
|
||||
}
|
||||
}
|
||||
// array_type: User[]
|
||||
if (typeNode.type === 'array_type') {
|
||||
const elemNode = typeNode.firstNamedChild;
|
||||
if (elemNode) return extractSimpleTypeName(elemNode);
|
||||
}
|
||||
// nullable_type: unwrap and recurse (List<User>? → List<User> → User)
|
||||
if (typeNode.type === 'nullable_type') {
|
||||
const inner = typeNode.firstNamedChild;
|
||||
if (inner) return extractCSharpElementTypeFromTypeNode(inner, pos, depth + 1);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Walk up from a foreach to the enclosing method and search parameters. */
|
||||
const findCSharpParamElementType = (iterableName: string, startNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
while (current) {
|
||||
if (current.type === 'method_declaration' || current.type === 'local_function_statement') {
|
||||
const paramsNode = current.childForFieldName('parameters');
|
||||
if (paramsNode) {
|
||||
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
||||
const param = paramsNode.namedChild(i);
|
||||
if (!param || param.type !== 'parameter') continue;
|
||||
const nameNode = param.childForFieldName('name');
|
||||
if (nameNode?.text !== iterableName) continue;
|
||||
const typeNode = param.childForFieldName('type');
|
||||
if (typeNode) return extractCSharpElementTypeFromTypeNode(typeNode, pos);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** C#: foreach (User user in users) — extract loop variable binding.
|
||||
* Tier 1c: for `foreach (var user in users)`, resolves element type from iterable. */
|
||||
const extractForLoopBinding: ForLoopExtractor = (
|
||||
node: SyntaxNode,
|
||||
scopeEnv: Map<string, string>,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
scope: 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);
|
||||
if (!varName) return;
|
||||
|
||||
// Explicit type (existing behavior): foreach (User user in users)
|
||||
if (!(typeNode.type === 'implicit_type' && typeNode.text === 'var')) {
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
if (typeName) scopeEnv.set(varName, typeName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Tier 1c: implicit type (var) — resolve from iterable's container type
|
||||
const rightNode = node.childForFieldName('right');
|
||||
let iterableName: string | undefined;
|
||||
let methodName: string | undefined;
|
||||
|
||||
if (rightNode?.type === 'identifier') {
|
||||
iterableName = rightNode.text;
|
||||
} else if (rightNode?.type === 'member_access_expression') {
|
||||
// C# property access: data.Keys, data.Values → member_access_expression
|
||||
// Also handles bare member access: this.users, repo.users → use property as iterableName
|
||||
const obj = rightNode.childForFieldName('expression');
|
||||
const prop = rightNode.childForFieldName('name');
|
||||
const propText = prop?.type === 'identifier' ? prop.text : undefined;
|
||||
if (propText && KNOWN_CONTAINER_PROPS.has(propText)) {
|
||||
if (obj?.type === 'identifier') {
|
||||
iterableName = obj.text;
|
||||
} else if (obj?.type === 'member_access_expression') {
|
||||
// Nested member access: this.data.Values → obj is "this.data", extract "data"
|
||||
const innerProp = obj.childForFieldName('name');
|
||||
if (innerProp) iterableName = innerProp.text;
|
||||
}
|
||||
methodName = propText;
|
||||
} else if (propText) {
|
||||
// Bare member access: this.users → use property name for scopeEnv lookup
|
||||
iterableName = propText;
|
||||
}
|
||||
} else if (rightNode?.type === 'invocation_expression') {
|
||||
// C# method call: data.Select(...) → invocation_expression > member_access_expression
|
||||
const fn = rightNode.firstNamedChild;
|
||||
if (fn?.type === 'member_access_expression') {
|
||||
const obj = fn.childForFieldName('expression');
|
||||
const prop = fn.childForFieldName('name');
|
||||
if (obj?.type === 'identifier') iterableName = obj.text;
|
||||
if (prop?.type === 'identifier') methodName = prop.text;
|
||||
}
|
||||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
const containerTypeName = scopeEnv.get(iterableName);
|
||||
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractCSharpElementTypeFromTypeNode, findCSharpParamElementType,
|
||||
typeArgPos,
|
||||
);
|
||||
if (elementType) scopeEnv.set(varName, elementType);
|
||||
};
|
||||
|
||||
/**
|
||||
* C# pattern binding extractor for `obj is Type variable` (type pattern).
|
||||
*
|
||||
* AST structure:
|
||||
* is_pattern_expression
|
||||
* expression: (the variable being tested)
|
||||
* pattern: declaration_pattern
|
||||
* type: (the declared type)
|
||||
* name: single_variable_designation > identifier (the new variable name)
|
||||
*
|
||||
* Conservative: returns undefined when the pattern field is absent, is not a
|
||||
* declaration_pattern, or when the type/name cannot be extracted.
|
||||
* No scopeEnv lookup is needed — the pattern explicitly declares the new variable's type.
|
||||
*/
|
||||
const extractPatternBinding: PatternBindingExtractor = (node) => {
|
||||
// is_pattern_expression: `obj is User user` — has a declaration_pattern child
|
||||
if (node.type === 'is_pattern_expression') {
|
||||
const pattern = node.childForFieldName('pattern');
|
||||
if (pattern?.type !== 'declaration_pattern' && pattern?.type !== 'recursive_pattern') return undefined;
|
||||
const typeNode = pattern.childForFieldName('type');
|
||||
const nameNode = pattern.childForFieldName('name');
|
||||
if (!typeNode || !nameNode) return undefined;
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
const varName = extractVarName(nameNode);
|
||||
if (!typeName || !varName) return undefined;
|
||||
return { varName, typeName };
|
||||
}
|
||||
// declaration_pattern / recursive_pattern: standalone in switch statements and switch expressions
|
||||
// `case User u:` or `User u =>` or `User { Name: "Alice" } u =>`
|
||||
// Both use the same 'type' and 'name' fields.
|
||||
if (node.type === 'declaration_pattern' || node.type === 'recursive_pattern') {
|
||||
const typeNode = node.childForFieldName('type');
|
||||
const nameNode = node.childForFieldName('name');
|
||||
if (!typeNode || !nameNode) return undefined;
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
const varName = extractVarName(nameNode);
|
||||
if (!typeName || !varName) return undefined;
|
||||
return { varName, typeName };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** C#: var alias = u → variable_declarator with name + equals_value_clause.
|
||||
@@ -188,9 +328,11 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
|
||||
patternBindingNodeTypes: new Set(['is_pattern_expression', 'declaration_pattern', 'recursive_pattern']),
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
scanConstructorBinding,
|
||||
extractForLoopBinding,
|
||||
extractPendingAssignment,
|
||||
extractPatternBinding,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { ConstructorBindingScanner, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, PendingAssignmentExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName } from './shared.js';
|
||||
import type { ConstructorBindingScanner, ForLoopExtractor, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, PendingAssignmentExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, extractElementTypeFromString, extractGenericTypeArgs, findChildByType, resolveIterableElementType, methodToTypeArgPosition, type TypeArgPosition } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'var_declaration',
|
||||
@@ -181,6 +181,188 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||
return { varName: leftIds[0].text, calleeName };
|
||||
};
|
||||
|
||||
const FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'for_statement',
|
||||
]);
|
||||
|
||||
/** Go function/method node types that carry a parameter list. */
|
||||
const GO_FUNCTION_NODE_TYPES = new Set([
|
||||
'function_declaration', 'method_declaration', 'func_literal',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Extract element type from a Go type annotation AST node.
|
||||
* Handles:
|
||||
* slice_type "[]User" → element field → type_identifier "User"
|
||||
* array_type "[10]User" → element field → type_identifier "User"
|
||||
* Falls back to text-based extraction via extractElementTypeFromString.
|
||||
*/
|
||||
const extractGoElementTypeFromTypeNode = (typeNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
// slice_type: []User — element field is the element type
|
||||
if (typeNode.type === 'slice_type' || typeNode.type === 'array_type') {
|
||||
const elemNode = typeNode.childForFieldName('element');
|
||||
if (elemNode) return extractSimpleTypeName(elemNode);
|
||||
}
|
||||
// map_type: map[string]User — value field is the element type (for range, second var gets value)
|
||||
if (typeNode.type === 'map_type') {
|
||||
const valueNode = typeNode.childForFieldName('value');
|
||||
if (valueNode) return extractSimpleTypeName(valueNode);
|
||||
}
|
||||
// channel_type: chan User — the type argument is the element type
|
||||
if (typeNode.type === 'channel_type') {
|
||||
const valueNode = typeNode.childForFieldName('value') ?? typeNode.lastNamedChild;
|
||||
if (valueNode) return extractSimpleTypeName(valueNode);
|
||||
}
|
||||
// generic_type: Go 1.18+ generics (e.g., MySlice[User], Cache[string, User])
|
||||
// Use position-aware arg selection: 'first' for keys, 'last' for values.
|
||||
if (typeNode.type === 'generic_type') {
|
||||
const args = extractGenericTypeArgs(typeNode);
|
||||
if (args.length >= 1) return pos === 'first' ? args[0] : args[args.length - 1];
|
||||
}
|
||||
// Fallback: text-based extraction ([]User → User, User[] → User)
|
||||
return extractElementTypeFromString(typeNode.text, pos);
|
||||
};
|
||||
|
||||
/** Check if a Go type node represents a channel type. Used to determine
|
||||
* whether single-var range yields the element (channels) vs index (slices/maps). */
|
||||
const isChannelType = (
|
||||
iterableName: string,
|
||||
scopeEnv: ReadonlyMap<string, string>,
|
||||
declarationTypeNodes?: ReadonlyMap<string, SyntaxNode>,
|
||||
scope?: string,
|
||||
): boolean => {
|
||||
if (declarationTypeNodes && scope) {
|
||||
const typeNode = declarationTypeNodes.get(`${scope}\0${iterableName}`);
|
||||
if (typeNode) return typeNode.type === 'channel_type';
|
||||
}
|
||||
const t = scopeEnv.get(iterableName);
|
||||
return !!t && t.startsWith('chan ');
|
||||
};
|
||||
|
||||
/**
|
||||
* Walk up the AST from a for-statement to find the enclosing function declaration,
|
||||
* then search its parameters for one named `iterableName`.
|
||||
* Returns the element type extracted from its type annotation, or undefined.
|
||||
*
|
||||
* Go parameter_declaration has:
|
||||
* name field: identifier (the parameter name)
|
||||
* type field: the type node (slice_type for []User)
|
||||
*/
|
||||
const findGoParamElementType = (iterableName: string, startNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
while (current) {
|
||||
if (GO_FUNCTION_NODE_TYPES.has(current.type)) {
|
||||
const paramsNode = current.childForFieldName('parameters');
|
||||
if (paramsNode) {
|
||||
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
||||
const paramDecl = paramsNode.namedChild(i);
|
||||
if (!paramDecl || paramDecl.type !== 'parameter_declaration') continue;
|
||||
// parameter_declaration: name type — name field is the identifier
|
||||
const nameNode = paramDecl.childForFieldName('name');
|
||||
if (nameNode?.text === iterableName) {
|
||||
const typeNode = paramDecl.childForFieldName('type');
|
||||
if (typeNode) return extractGoElementTypeFromTypeNode(typeNode, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Go: for _, user := range users where users has a known slice type.
|
||||
*
|
||||
* Go uses a single `for_statement` node for all for-loop forms. We detect
|
||||
* range-based loops by looking for a `range_clause` child node. C-style for
|
||||
* loops (with `for_clause`) and infinite loops (no clause) are ignored.
|
||||
*
|
||||
* Tier 1c: resolves the element type via three strategies in priority order:
|
||||
* 1. declarationTypeNodes — raw type annotation AST node
|
||||
* 2. scopeEnv string — extractElementTypeFromString on the stored type
|
||||
* 3. AST walk — walks up to the enclosing function's parameters to read []User directly
|
||||
* For `_, user := range users`, the loop variable is the second identifier in
|
||||
* the `left` expression_list (index is discarded, value is the element).
|
||||
*/
|
||||
const extractForLoopBinding: ForLoopExtractor = (
|
||||
node: SyntaxNode,
|
||||
scopeEnv: Map<string, string>,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
scope: string,
|
||||
): void => {
|
||||
if (node.type !== 'for_statement') return;
|
||||
|
||||
// Find the range_clause child — this distinguishes range loops from other for forms.
|
||||
let rangeClause: SyntaxNode | null = null;
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child?.type === 'range_clause') {
|
||||
rangeClause = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!rangeClause) return;
|
||||
|
||||
// The iterable is the `right` field of the range_clause.
|
||||
const rightNode = rangeClause.childForFieldName('right');
|
||||
let iterableName: string | undefined;
|
||||
if (rightNode?.type === 'identifier') {
|
||||
iterableName = rightNode.text;
|
||||
} else if (rightNode?.type === 'selector_expression') {
|
||||
const field = rightNode.childForFieldName('field');
|
||||
if (field) iterableName = field.text;
|
||||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
const containerTypeName = scopeEnv.get(iterableName);
|
||||
const typeArgPos = methodToTypeArgPosition(undefined, containerTypeName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractGoElementTypeFromTypeNode, findGoParamElementType,
|
||||
typeArgPos,
|
||||
);
|
||||
if (!elementType) return;
|
||||
|
||||
// The loop variable(s) are in the `left` field.
|
||||
// Go range semantics:
|
||||
// Slice/Array/String: single-var → INDEX (int); two-var → (index, element)
|
||||
// Map: single-var → KEY; two-var → (key, value)
|
||||
// Channel: single-var → ELEMENT (channels have no index)
|
||||
const leftNode = rangeClause.childForFieldName('left');
|
||||
if (!leftNode) return;
|
||||
|
||||
let loopVarNode: SyntaxNode | null = null;
|
||||
if (leftNode.type === 'expression_list') {
|
||||
if (leftNode.namedChildCount >= 2) {
|
||||
// Two-var form: `_, user` or `i, user` — second variable gets element/value type
|
||||
loopVarNode = leftNode.namedChild(1);
|
||||
} else {
|
||||
// Single-var in expression_list — yields INDEX for slices/maps, ELEMENT for channels
|
||||
if (isChannelType(iterableName, scopeEnv, declarationTypeNodes, scope)) {
|
||||
loopVarNode = leftNode.namedChild(0);
|
||||
} else {
|
||||
return; // index-only range on slice/map — skip
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Plain identifier (single-var form without expression_list)
|
||||
if (isChannelType(iterableName, scopeEnv, declarationTypeNodes, scope)) {
|
||||
loopVarNode = leftNode;
|
||||
} else {
|
||||
return; // index-only range on slice/map — skip
|
||||
}
|
||||
}
|
||||
if (!loopVarNode) return;
|
||||
|
||||
// Skip the blank identifier `_`
|
||||
if (loopVarNode.text === '_') return;
|
||||
|
||||
const loopVarName = extractVarName(loopVarNode);
|
||||
if (loopVarName) scopeEnv.set(loopVarName, elementType);
|
||||
};
|
||||
|
||||
/** Go: alias := u (short_var_declaration) or var b = u (var_spec) */
|
||||
const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
|
||||
if (node.type === 'short_var_declaration') {
|
||||
@@ -226,8 +408,10 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
scanConstructorBinding,
|
||||
extractForLoopBinding,
|
||||
extractPendingAssignment,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ForLoopExtractor, PendingAssignmentExtractor, PatternBindingExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, findChildByType } from './shared.js';
|
||||
import { extractSimpleTypeName, extractVarName, findChildByType, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, type TypeArgPosition } from './shared.js';
|
||||
|
||||
// ── Java ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -89,14 +89,98 @@ 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 => {
|
||||
/** Extract element type from a Java type annotation AST node.
|
||||
* Handles generic_type (List<User>), array_type (User[]). */
|
||||
const extractJavaElementTypeFromTypeNode = (typeNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
if (typeNode.type === 'generic_type') {
|
||||
const args = extractGenericTypeArgs(typeNode);
|
||||
if (args.length >= 1) return pos === 'first' ? args[0] : args[args.length - 1];
|
||||
}
|
||||
if (typeNode.type === 'array_type') {
|
||||
const elemNode = typeNode.firstNamedChild;
|
||||
if (elemNode) return extractSimpleTypeName(elemNode);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Walk up from a for-each to the enclosing method_declaration and search parameters. */
|
||||
const findJavaParamElementType = (iterableName: string, startNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
while (current) {
|
||||
if (current.type === 'method_declaration' || current.type === 'constructor_declaration') {
|
||||
const paramsNode = current.childForFieldName('parameters');
|
||||
if (paramsNode) {
|
||||
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
||||
const param = paramsNode.namedChild(i);
|
||||
if (!param || param.type !== 'formal_parameter') continue;
|
||||
const nameNode = param.childForFieldName('name');
|
||||
if (nameNode?.text !== iterableName) continue;
|
||||
const typeNode = param.childForFieldName('type');
|
||||
if (typeNode) return extractJavaElementTypeFromTypeNode(typeNode, pos);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Java: for (User user : users) — extract loop variable binding.
|
||||
* Tier 1c: for `for (var user : users)`, resolves element type from iterable. */
|
||||
const extractJavaForLoopBinding: ForLoopExtractor = (
|
||||
node: SyntaxNode,
|
||||
scopeEnv: Map<string, string>,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
scope: 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);
|
||||
if (!varName) return;
|
||||
|
||||
// Explicit type (existing behavior): for (User user : users)
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
if (typeName && typeName !== 'var') {
|
||||
scopeEnv.set(varName, typeName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Tier 1c: var — resolve from iterable's container type
|
||||
const iterableNode = node.childForFieldName('value');
|
||||
if (!iterableNode) return;
|
||||
|
||||
let iterableName: string | undefined;
|
||||
let methodName: string | undefined;
|
||||
if (iterableNode.type === 'identifier') {
|
||||
iterableName = iterableNode.text;
|
||||
} else if (iterableNode.type === 'field_access') {
|
||||
const field = iterableNode.childForFieldName('field');
|
||||
if (field) iterableName = field.text;
|
||||
} else if (iterableNode.type === 'method_invocation') {
|
||||
// data.keySet() → method_invocation > object: identifier + name: identifier
|
||||
// Also handles this.data.values() → object is field_access, extract inner field name
|
||||
const obj = iterableNode.childForFieldName('object');
|
||||
const name = iterableNode.childForFieldName('name');
|
||||
if (obj?.type === 'identifier') {
|
||||
iterableName = obj.text;
|
||||
} else if (obj?.type === 'field_access') {
|
||||
const innerField = obj.childForFieldName('field');
|
||||
if (innerField) iterableName = innerField.text;
|
||||
}
|
||||
if (name) methodName = name.text;
|
||||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
const containerTypeName = scopeEnv.get(iterableName);
|
||||
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractJavaElementTypeFromTypeNode, findJavaParamElementType,
|
||||
typeArgPos,
|
||||
);
|
||||
if (elementType) scopeEnv.set(varName, elementType);
|
||||
};
|
||||
|
||||
/** Java: var alias = u → local_variable_declaration > variable_declarator with name/value */
|
||||
@@ -130,6 +214,19 @@ const extractJavaPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv
|
||||
* declares the new type, so no scopeEnv lookup is needed.
|
||||
*/
|
||||
const extractJavaPatternBinding: PatternBindingExtractor = (node) => {
|
||||
if (node.type === 'type_pattern') {
|
||||
// Java 17+ switch pattern: case User u -> ...
|
||||
// type_pattern has positional children (NO named fields):
|
||||
// namedChild(0) = type (type_identifier, e.g., User)
|
||||
// namedChild(1) = identifier (e.g., u)
|
||||
const typeNode = node.namedChild(0);
|
||||
const nameNode = node.namedChild(1);
|
||||
if (!typeNode || !nameNode) return undefined;
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
const varName = extractVarName(nameNode);
|
||||
if (!typeName || !varName) return undefined;
|
||||
return { varName, typeName };
|
||||
}
|
||||
if (node.type !== 'instanceof_expression') return undefined;
|
||||
const nameNode = node.childForFieldName('name');
|
||||
if (!nameNode) return undefined;
|
||||
@@ -143,11 +240,12 @@ const extractJavaPatternBinding: PatternBindingExtractor = (node) => {
|
||||
|
||||
export const javaTypeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: JAVA_DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: JAVA_FOR_LOOP_NODE_TYPES,
|
||||
patternBindingNodeTypes: new Set(['instanceof_expression', 'type_pattern']),
|
||||
extractDeclaration: extractJavaDeclaration,
|
||||
extractParameter: extractJavaParameter,
|
||||
extractInitializer: extractJavaInitializer,
|
||||
scanConstructorBinding: scanJavaConstructorBinding,
|
||||
forLoopNodeTypes: JAVA_FOR_LOOP_NODE_TYPES,
|
||||
extractForLoopBinding: extractJavaForLoopBinding,
|
||||
extractPendingAssignment: extractJavaPendingAssignment,
|
||||
extractPatternBinding: extractJavaPatternBinding,
|
||||
@@ -195,7 +293,10 @@ const extractKotlinDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: M
|
||||
}
|
||||
};
|
||||
|
||||
/** Kotlin: formal_parameter → type name */
|
||||
/** Kotlin: parameter / formal_parameter → type name.
|
||||
* Kotlin's tree-sitter grammar uses positional children (simple_identifier, user_type)
|
||||
* rather than named fields (name, type) on `parameter` nodes, so we fall back to
|
||||
* findChildByType when childForFieldName returns null. */
|
||||
const extractKotlinParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
|
||||
let nameNode: SyntaxNode | null = null;
|
||||
let typeNode: SyntaxNode | null = null;
|
||||
@@ -208,6 +309,10 @@ const extractKotlinParameter: ParameterExtractor = (node: SyntaxNode, env: Map<s
|
||||
typeNode = node.childForFieldName('type');
|
||||
}
|
||||
|
||||
// Fallback: Kotlin `parameter` nodes use positional children, not named fields
|
||||
if (!nameNode) nameNode = findChildByType(node, 'simple_identifier');
|
||||
if (!typeNode) typeNode = findChildByType(node, 'user_type');
|
||||
|
||||
if (!nameNode || !typeNode) return;
|
||||
const varName = extractVarName(nameNode);
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
@@ -279,19 +384,137 @@ 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
|
||||
/** Extract element type from a Kotlin type annotation AST node (user_type wrapping generic).
|
||||
* Kotlin: user_type → [type_identifier, type_arguments → [type_projection → user_type]]
|
||||
* Handles the type_projection wrapper that Kotlin uses for generic type arguments. */
|
||||
const extractKotlinElementTypeFromTypeNode = (typeNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
if (typeNode.type === 'user_type') {
|
||||
const argsNode = findChildByType(typeNode, 'type_arguments');
|
||||
if (argsNode && argsNode.namedChildCount >= 1) {
|
||||
const targetArg = pos === 'first'
|
||||
? argsNode.namedChild(0)
|
||||
: argsNode.namedChild(argsNode.namedChildCount - 1);
|
||||
if (!targetArg) return undefined;
|
||||
// Kotlin wraps type args in type_projection — unwrap to get the inner type
|
||||
const inner = targetArg.type === 'type_projection'
|
||||
? targetArg.firstNamedChild
|
||||
: targetArg;
|
||||
if (inner) return extractSimpleTypeName(inner);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Walk up from a for-loop to the enclosing function_declaration and search parameters.
|
||||
* Kotlin parameters use positional children (simple_identifier, user_type), not named fields. */
|
||||
const findKotlinParamElementType = (iterableName: string, startNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
while (current) {
|
||||
if (current.type === 'function_declaration') {
|
||||
const paramsNode = findChildByType(current, 'function_value_parameters');
|
||||
if (paramsNode) {
|
||||
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
||||
const param = paramsNode.namedChild(i);
|
||||
if (!param || param.type !== 'parameter') continue;
|
||||
const nameNode = findChildByType(param, 'simple_identifier');
|
||||
if (nameNode?.text !== iterableName) continue;
|
||||
const typeNode = findChildByType(param, 'user_type');
|
||||
if (typeNode) return extractKotlinElementTypeFromTypeNode(typeNode, pos);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Kotlin: for (user: User in users) — extract loop variable binding.
|
||||
* Tier 1c: for `for (user in users)` without annotation, resolves from iterable. */
|
||||
const extractKotlinForLoopBinding: ForLoopExtractor = (
|
||||
node: SyntaxNode,
|
||||
scopeEnv: Map<string, string>,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
scope: string,
|
||||
): void => {
|
||||
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);
|
||||
if (!varName) return;
|
||||
|
||||
// Explicit type annotation (existing behavior): for (user: User in users)
|
||||
const typeNode = findChildByType(varDecl, 'user_type');
|
||||
if (typeNode) {
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
if (typeName) scopeEnv.set(varName, typeName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Tier 1c: no annotation — resolve from iterable's container type
|
||||
// Kotlin for-loop children: [variable_declaration, iterable_expr, control_structure_body]
|
||||
// The iterable is the second named child of the for_statement (after variable_declaration)
|
||||
let iterableName: string | undefined;
|
||||
let methodName: string | undefined;
|
||||
let fallbackIterableName: string | undefined;
|
||||
let foundVarDecl = false;
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child === varDecl) { foundVarDecl = true; continue; }
|
||||
if (!foundVarDecl || !child) continue;
|
||||
if (child.type === 'simple_identifier') {
|
||||
iterableName = child.text;
|
||||
break;
|
||||
}
|
||||
if (child.type === 'navigation_expression') {
|
||||
// data.keys → navigation_expression > simple_identifier(data) + navigation_suffix > simple_identifier(keys)
|
||||
const obj = child.firstNamedChild;
|
||||
const suffix = findChildByType(child, 'navigation_suffix');
|
||||
const prop = suffix ? findChildByType(suffix, 'simple_identifier') : null;
|
||||
const hasCallSuffix = suffix ? findChildByType(suffix, 'call_suffix') !== null : false;
|
||||
// Always try object as iterable + property as method first (handles data.values, data.keys).
|
||||
// For bare property access without call_suffix, also save property as fallback
|
||||
// (handles this.users, repo.items where the property IS the iterable).
|
||||
if (obj?.type === 'simple_identifier') iterableName = obj.text;
|
||||
if (prop) methodName = prop.text;
|
||||
if (!hasCallSuffix && prop) {
|
||||
fallbackIterableName = prop.text;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (child.type === 'call_expression') {
|
||||
// data.values() → call_expression > navigation_expression > simple_identifier + navigation_suffix
|
||||
const callee = child.firstNamedChild;
|
||||
if (callee?.type === 'navigation_expression') {
|
||||
const obj = callee.firstNamedChild;
|
||||
if (obj?.type === 'simple_identifier') iterableName = obj.text;
|
||||
const suffix = findChildByType(callee, 'navigation_suffix');
|
||||
if (suffix) {
|
||||
const prop = findChildByType(suffix, 'simple_identifier');
|
||||
if (prop) methodName = prop.text;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
let containerTypeName = scopeEnv.get(iterableName);
|
||||
// Fallback: if object has no type in scope, try the property as the iterable name.
|
||||
// Handles patterns like this.users where the property itself is the iterable variable.
|
||||
if (!containerTypeName && fallbackIterableName) {
|
||||
iterableName = fallbackIterableName;
|
||||
methodName = undefined;
|
||||
containerTypeName = scopeEnv.get(iterableName);
|
||||
}
|
||||
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractKotlinElementTypeFromTypeNode, findKotlinParamElementType,
|
||||
typeArgPos,
|
||||
);
|
||||
if (elementType) scopeEnv.set(varName, elementType);
|
||||
};
|
||||
|
||||
/** Kotlin: val alias = u → property_declaration or variable_declaration.
|
||||
@@ -345,13 +568,42 @@ const extractKotlinPendingAssignment: PendingAssignmentExtractor = (node, scopeE
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Walk up from a node to find an ancestor of a given type. */
|
||||
const findAncestorByType = (node: SyntaxNode, type: string): SyntaxNode | undefined => {
|
||||
let current = node.parent;
|
||||
while (current) {
|
||||
if (current.type === type) return current;
|
||||
current = current.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const extractKotlinPatternBinding: PatternBindingExtractor = (node) => {
|
||||
if (node.type !== 'type_test') return undefined;
|
||||
const typeNode = node.lastNamedChild;
|
||||
if (!typeNode) return undefined;
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
if (!typeName) return undefined;
|
||||
const whenExpr = findAncestorByType(node, 'when_expression');
|
||||
if (!whenExpr) return undefined;
|
||||
const whenSubject = whenExpr.namedChild(0);
|
||||
const subject = whenSubject?.firstNamedChild ?? whenSubject;
|
||||
if (!subject) return undefined;
|
||||
const varName = extractVarName(subject);
|
||||
if (!varName) return undefined;
|
||||
return { varName, typeName };
|
||||
};
|
||||
|
||||
export const kotlinTypeConfig: LanguageTypeConfig = {
|
||||
allowPatternBindingOverwrite: true,
|
||||
declarationNodeTypes: KOTLIN_DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: KOTLIN_FOR_LOOP_NODE_TYPES,
|
||||
patternBindingNodeTypes: new Set(['type_test']),
|
||||
extractDeclaration: extractKotlinDeclaration,
|
||||
extractParameter: extractKotlinParameter,
|
||||
extractInitializer: extractKotlinInitializer,
|
||||
scanConstructorBinding: scanKotlinConstructorBinding,
|
||||
extractForLoopBinding: extractKotlinForLoopBinding,
|
||||
extractPendingAssignment: extractKotlinPendingAssignment,
|
||||
extractPatternBinding: extractKotlinPatternBinding,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, extractCalleeName } from './shared.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor, ForLoopExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, extractCalleeName, resolveIterableElementType, extractElementTypeFromString } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'assignment_expression', // For constructor inference: $x = new User()
|
||||
@@ -61,6 +61,15 @@ const normalizePhpType = (raw: string): string | undefined => {
|
||||
type = segments[segments.length - 1];
|
||||
// Skip uninformative types
|
||||
if (type === 'mixed' || type === 'void' || type === 'self' || type === 'static' || type === 'object') return undefined;
|
||||
// Extract element type from generic: Collection<User> → User
|
||||
// PHPDoc generics encode the element type in angle brackets. Since PHP's Strategy B
|
||||
// uses the scopeEnv value directly as the element type, we must store the inner type,
|
||||
// not the container name. This mirrors how User[] → User is handled by the [] strip above.
|
||||
const genericMatch = type.match(/^(\w+)\s*</);
|
||||
if (genericMatch) {
|
||||
const elementType = extractElementTypeFromString(type);
|
||||
return elementType ?? undefined;
|
||||
}
|
||||
if (/^\w+$/.test(type)) return type;
|
||||
return undefined;
|
||||
};
|
||||
@@ -190,8 +199,12 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
|
||||
|
||||
if (!nameNode || !typeNode) return;
|
||||
const varName = extractVarName(nameNode);
|
||||
if (!varName) return;
|
||||
// Don't overwrite PHPDoc-derived types (e.g. @param User[] $users → User)
|
||||
// with the less-specific AST type annotation (e.g. array).
|
||||
if (env.has(varName)) return;
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
if (varName && typeName) env.set(varName, typeName);
|
||||
if (typeName) env.set(varName, typeName);
|
||||
};
|
||||
|
||||
/** PHP: $x = SomeFactory() or $x = $this->getUser() — bind variable to call return type */
|
||||
@@ -259,12 +272,133 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
||||
return { lhs, rhs };
|
||||
};
|
||||
|
||||
const FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'foreach_statement',
|
||||
]);
|
||||
|
||||
/** Extract element type from a PHP type annotation AST node.
|
||||
* PHP has limited AST-level container types — `array` is a primitive_type with no generic args.
|
||||
* Named types (e.g., `Collection`) are returned as-is (container descriptor lookup handles them). */
|
||||
const extractPhpElementTypeFromTypeNode = (_typeNode: SyntaxNode): string | undefined => {
|
||||
// PHP AST type nodes don't carry generic parameters (array<User> is PHPDoc-only).
|
||||
// primitive_type 'array' and named_type 'Collection' don't encode element types.
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Walk up from a foreach to the enclosing function and search parameter type annotations.
|
||||
* PHP parameter type hints are limited (array, ClassName) — this extracts element type when possible. */
|
||||
const findPhpParamElementType = (iterableName: string, startNode: SyntaxNode): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
while (current) {
|
||||
if (current.type === 'method_declaration' || current.type === 'function_definition') {
|
||||
const paramsNode = current.childForFieldName('parameters');
|
||||
if (paramsNode) {
|
||||
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
||||
const param = paramsNode.namedChild(i);
|
||||
if (!param || param.type !== 'simple_parameter') continue;
|
||||
const nameNode = param.childForFieldName('name');
|
||||
if (nameNode?.text !== iterableName) continue;
|
||||
const typeNode = param.childForFieldName('type');
|
||||
if (typeNode) return extractPhpElementTypeFromTypeNode(typeNode);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* PHP: foreach ($users as $user) — extract loop variable binding.
|
||||
*
|
||||
* AST structure (from tree-sitter-php grammar):
|
||||
* foreach_statement — no named fields for iterable/value (only 'body')
|
||||
* children[0]: expression (iterable, e.g. $users)
|
||||
* children[1]: expression (simple value) OR pair ($key => $value)
|
||||
* pair children: expression (key), expression (value)
|
||||
*
|
||||
* PHP's PHPDoc @param normalizes `User[]` → `User` in the env, so the iterable's
|
||||
* stored type IS the element type. We first try resolveIterableElementType (for
|
||||
* constructor-binding cases that retain container types), then fall back to direct
|
||||
* scopeEnv lookup (for PHPDoc-normalized types).
|
||||
*/
|
||||
const extractForLoopBinding: ForLoopExtractor = (
|
||||
node: SyntaxNode,
|
||||
scopeEnv: Map<string, string>,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
scope: string,
|
||||
): void => {
|
||||
if (node.type !== 'foreach_statement') return;
|
||||
|
||||
// Collect non-body named children: first is the iterable, second is value or pair
|
||||
const children: SyntaxNode[] = [];
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child && child !== node.childForFieldName('body')) {
|
||||
children.push(child);
|
||||
}
|
||||
}
|
||||
if (children.length < 2) return;
|
||||
|
||||
const iterableNode = children[0];
|
||||
const valueOrPair = children[1];
|
||||
|
||||
// Determine the loop variable node
|
||||
let loopVarNode: SyntaxNode;
|
||||
if (valueOrPair.type === 'pair') {
|
||||
// $key => $value — the value is the last named child of the pair
|
||||
const lastChild = valueOrPair.namedChild(valueOrPair.namedChildCount - 1);
|
||||
if (!lastChild) return;
|
||||
// Handle by_ref: foreach ($arr as $k => &$v)
|
||||
loopVarNode = lastChild.type === 'by_ref' ? (lastChild.firstNamedChild ?? lastChild) : lastChild;
|
||||
} else {
|
||||
// Simple: foreach ($users as $user) or foreach ($users as &$user)
|
||||
loopVarNode = valueOrPair.type === 'by_ref' ? (valueOrPair.firstNamedChild ?? valueOrPair) : valueOrPair;
|
||||
}
|
||||
|
||||
const varName = extractVarName(loopVarNode);
|
||||
if (!varName) return;
|
||||
|
||||
// Get iterable variable name (PHP vars include $ prefix)
|
||||
let iterableName: string | undefined;
|
||||
if (iterableNode.type === 'variable_name') {
|
||||
iterableName = iterableNode.text;
|
||||
} else if (iterableNode?.type === 'member_access_expression') {
|
||||
const name = iterableNode.childForFieldName('name');
|
||||
// PHP properties are stored in scopeEnv with $ prefix ($users), but
|
||||
// member_access_expression.name returns without $ (users). Add $ to match.
|
||||
if (name) iterableName = '$' + name.text;
|
||||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
// Strategy A: try resolveIterableElementType (handles constructor-binding container types)
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractPhpElementTypeFromTypeNode, findPhpParamElementType,
|
||||
undefined,
|
||||
);
|
||||
if (elementType) {
|
||||
scopeEnv.set(varName, elementType);
|
||||
return;
|
||||
}
|
||||
|
||||
// Strategy B: direct scopeEnv lookup — PHP normalizePhpType strips User[] → User,
|
||||
// so the iterable's stored type is already the element type from PHPDoc annotations.
|
||||
const iterableType = scopeEnv.get(iterableName);
|
||||
if (iterableType) {
|
||||
scopeEnv.set(varName, iterableType);
|
||||
}
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
extractInitializer,
|
||||
scanConstructorBinding,
|
||||
extractReturnType,
|
||||
extractForLoopBinding,
|
||||
extractPendingAssignment,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName } from './shared.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor, PatternBindingExtractor, ForLoopExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, extractElementTypeFromString, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, type TypeArgPosition } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'assignment',
|
||||
@@ -134,6 +134,157 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||
return { varName: left.text, calleeName };
|
||||
};
|
||||
|
||||
const FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'for_statement',
|
||||
]);
|
||||
|
||||
/** Python function/method node types that carry a parameters list. */
|
||||
const PY_FUNCTION_NODE_TYPES = new Set([
|
||||
'function_definition', 'decorated_definition',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Extract element type from a Python type annotation AST node.
|
||||
* Handles:
|
||||
* subscript "List[User]" → extractElementTypeFromString("List[User]") → "User"
|
||||
* generic_type → extractGenericTypeArgs → first arg
|
||||
* Falls back to text-based extraction.
|
||||
*/
|
||||
const extractPyElementTypeFromAnnotation = (typeNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
// Unwrap 'type' wrapper node to get to the actual type (e.g., type > generic_type)
|
||||
const inner = typeNode.type === 'type' ? (typeNode.firstNamedChild ?? typeNode) : typeNode;
|
||||
|
||||
// Python subscript: List[User], Sequence[User] — use raw text
|
||||
if (inner.type === 'subscript') {
|
||||
return extractElementTypeFromString(inner.text, pos);
|
||||
}
|
||||
// generic_type: dict[str, User] — tree-sitter-python uses type_parameter child
|
||||
if (inner.type === 'generic_type') {
|
||||
// Try standard extractGenericTypeArgs first (handles type_arguments)
|
||||
const args = extractGenericTypeArgs(inner);
|
||||
if (args.length >= 1) return pos === 'first' ? args[0] : args[args.length - 1];
|
||||
// Fallback: look for type_parameter child (tree-sitter-python specific)
|
||||
for (let i = 0; i < inner.namedChildCount; i++) {
|
||||
const child = inner.namedChild(i);
|
||||
if (child?.type === 'type_parameter') {
|
||||
if (pos === 'first') {
|
||||
const firstArg = child.firstNamedChild;
|
||||
if (firstArg) return extractSimpleTypeName(firstArg);
|
||||
} else {
|
||||
const lastArg = child.lastNamedChild;
|
||||
if (lastArg) return extractSimpleTypeName(lastArg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: raw text extraction (handles User[], [User], etc.)
|
||||
return extractElementTypeFromString(inner.text, pos);
|
||||
};
|
||||
|
||||
/**
|
||||
* Walk up the AST from a for-statement to find the enclosing function definition,
|
||||
* then search its parameters for one named `iterableName`.
|
||||
* Returns the element type extracted from its type annotation, or undefined.
|
||||
*
|
||||
* Handles both `parameter` and `typed_parameter` node types in tree-sitter-python.
|
||||
* `typed_parameter` may not expose the name as a `name` field — falls back to
|
||||
* checking the first identifier-type named child.
|
||||
*/
|
||||
const findPyParamElementType = (iterableName: string, startNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
while (current) {
|
||||
if (current.type === 'function_definition') {
|
||||
const paramsNode = current.childForFieldName('parameters');
|
||||
if (paramsNode) {
|
||||
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
||||
const param = paramsNode.namedChild(i);
|
||||
if (!param) continue;
|
||||
// Try named `name` field first (parameter node), then first identifier child
|
||||
// (typed_parameter node may store name as first positional child)
|
||||
const nameNode = param.childForFieldName('name')
|
||||
?? (param.firstNamedChild?.type === 'identifier' ? param.firstNamedChild : null);
|
||||
if (nameNode?.text !== iterableName) continue;
|
||||
// Try `type` field, then last named child (typed_parameter stores type last)
|
||||
const typeAnnotation = param.childForFieldName('type')
|
||||
?? (param.namedChildCount >= 2 ? param.namedChild(param.namedChildCount - 1) : null);
|
||||
if (typeAnnotation && typeAnnotation !== nameNode) {
|
||||
return extractPyElementTypeFromAnnotation(typeAnnotation, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Python: for user in users: where users has a known container type annotation.
|
||||
*
|
||||
* AST node: `for_statement` with `left` (loop variable) and `right` (iterable).
|
||||
*
|
||||
* Tier 1c: resolves the element type via three strategies in priority order:
|
||||
* 1. declarationTypeNodes — raw type annotation AST node (covers stored container types)
|
||||
* 2. scopeEnv string — extractElementTypeFromString on the stored type
|
||||
* 3. AST walk — walks up to the enclosing function's parameters to read List[User] directly
|
||||
*/
|
||||
const extractForLoopBinding: ForLoopExtractor = (
|
||||
node: SyntaxNode,
|
||||
scopeEnv: Map<string, string>,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
scope: string,
|
||||
): void => {
|
||||
if (node.type !== 'for_statement') return;
|
||||
|
||||
// The iterable is the `right` field — may be identifier or call (data.items()/keys()/values()).
|
||||
const rightNode = node.childForFieldName('right');
|
||||
let iterableName: string | undefined;
|
||||
let methodName: string | undefined;
|
||||
if (rightNode?.type === 'identifier') {
|
||||
iterableName = rightNode.text;
|
||||
} else if (rightNode?.type === 'attribute') {
|
||||
const prop = rightNode.lastNamedChild;
|
||||
if (prop) iterableName = prop.text;
|
||||
} else if (rightNode?.type === 'call') {
|
||||
// data.items() → call > function: attribute > identifier('data') + identifier('items')
|
||||
const fn = rightNode.childForFieldName('function');
|
||||
if (fn?.type === 'attribute') {
|
||||
const obj = fn.firstNamedChild;
|
||||
if (obj?.type === 'identifier') iterableName = obj.text;
|
||||
// Extract method name: items, keys, values
|
||||
const method = fn.lastNamedChild;
|
||||
if (method?.type === 'identifier' && method !== obj) methodName = method.text;
|
||||
}
|
||||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
const containerTypeName = scopeEnv.get(iterableName);
|
||||
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractPyElementTypeFromAnnotation, findPyParamElementType,
|
||||
typeArgPos,
|
||||
);
|
||||
if (!elementType) return;
|
||||
|
||||
// The loop variable is the `left` field — identifier or pattern_list.
|
||||
const leftNode = node.childForFieldName('left');
|
||||
if (!leftNode) return;
|
||||
|
||||
// Handle tuple unpacking: for key, value in data.items()
|
||||
if (leftNode.type === 'pattern_list') {
|
||||
const lastChild = leftNode.lastNamedChild;
|
||||
if (lastChild?.type === 'identifier') {
|
||||
scopeEnv.set(lastChild.text, elementType);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const loopVarName = extractVarName(leftNode);
|
||||
if (loopVarName) scopeEnv.set(loopVarName, elementType);
|
||||
};
|
||||
|
||||
/** 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) => {
|
||||
@@ -157,11 +308,77 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Python match/case `as` pattern binding: `case User() as u:`
|
||||
*
|
||||
* AST structure (tree-sitter-python):
|
||||
* as_pattern
|
||||
* alias: as_pattern_target ← the bound variable name (e.g. "u")
|
||||
* children[0]: case_pattern ← wraps class_pattern (or is class_pattern directly)
|
||||
* class_pattern
|
||||
* dotted_name ← the class name (e.g. "User")
|
||||
*
|
||||
* The `alias` field is an `as_pattern_target` node whose `.text` is the identifier.
|
||||
* The class name lives in the first non-alias named child: either a `case_pattern`
|
||||
* wrapping a `class_pattern`, or a direct `class_pattern`.
|
||||
*
|
||||
* Conservative: returns undefined when:
|
||||
* - The node is not an `as_pattern`
|
||||
* - The pattern side is not a class_pattern (e.g. guard or literal match)
|
||||
* - The variable was already bound in scopeEnv
|
||||
*/
|
||||
const extractPatternBinding: PatternBindingExtractor = (node, scopeEnv) => {
|
||||
if (node.type !== 'as_pattern') return undefined;
|
||||
|
||||
// as_pattern: `case User() as u:` — binds matched value to a name.
|
||||
// Try named field first (future grammar versions may expose it), fall back to positional.
|
||||
if (node.namedChildCount < 2) return undefined;
|
||||
|
||||
const patternChild = node.namedChild(0);
|
||||
const varNameNode = node.childForFieldName('alias')
|
||||
?? node.namedChild(node.namedChildCount - 1);
|
||||
if (!patternChild || !varNameNode) return undefined;
|
||||
if (varNameNode.type !== 'identifier') return undefined;
|
||||
|
||||
const varName = varNameNode.text;
|
||||
if (!varName || scopeEnv.has(varName)) return undefined;
|
||||
|
||||
// Find the class_pattern — may be direct or wrapped in case_pattern.
|
||||
let classPattern: SyntaxNode | null = null;
|
||||
if (patternChild.type === 'class_pattern') {
|
||||
classPattern = patternChild;
|
||||
} else if (patternChild.type === 'case_pattern') {
|
||||
// Unwrap one level: case_pattern wraps class_pattern
|
||||
for (let j = 0; j < patternChild.namedChildCount; j++) {
|
||||
const inner = patternChild.namedChild(j);
|
||||
if (inner?.type === 'class_pattern') {
|
||||
classPattern = inner;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!classPattern) return undefined;
|
||||
|
||||
// class_pattern children: dotted_name (the class name) + optional keyword_pattern args.
|
||||
const classNameNode = classPattern.firstNamedChild;
|
||||
if (!classNameNode || (classNameNode.type !== 'dotted_name' && classNameNode.type !== 'identifier')) return undefined;
|
||||
const typeName = classNameNode.text;
|
||||
if (!typeName) return undefined;
|
||||
|
||||
return { varName, typeName };
|
||||
};
|
||||
|
||||
const PATTERN_BINDING_NODE_TYPES: ReadonlySet<string> = new Set(['as_pattern']);
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
|
||||
patternBindingNodeTypes: PATTERN_BINDING_NODE_TYPES,
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
extractInitializer,
|
||||
scanConstructorBinding,
|
||||
extractForLoopBinding,
|
||||
extractPendingAssignment,
|
||||
extractPatternBinding,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor } from './types.js';
|
||||
import { extractRubyConstructorAssignment, extractSimpleTypeName } from './shared.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor, ForLoopExtractor } from './types.js';
|
||||
import { extractRubyConstructorAssignment, extractSimpleTypeName, extractElementTypeFromString, extractVarName, resolveIterableElementType } from './shared.js';
|
||||
import type { SyntaxNode } from '../utils.js';
|
||||
|
||||
/**
|
||||
@@ -261,11 +261,151 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||
return { varName: left.text, calleeName };
|
||||
};
|
||||
|
||||
/** Ruby method node types that carry a parameter list. */
|
||||
const RUBY_METHOD_NODE_TYPES = new Set(['method', 'singleton_method']);
|
||||
|
||||
const FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set(['for']);
|
||||
|
||||
/**
|
||||
* Collect raw YARD @param type strings from comment nodes preceding a method.
|
||||
* Unlike collectYardParams which returns simplified type names, this returns the
|
||||
* raw bracket content (e.g., "Array<User>" not "Array") for element type extraction.
|
||||
*/
|
||||
const collectYardRawParams = (methodNode: SyntaxNode): Map<string, string> => {
|
||||
const params = new Map<string, string>();
|
||||
const commentTexts: string[] = [];
|
||||
|
||||
const collectComments = (startNode: SyntaxNode): void => {
|
||||
let sibling = startNode.previousSibling;
|
||||
while (sibling) {
|
||||
if (sibling.type === 'comment') {
|
||||
commentTexts.unshift(sibling.text);
|
||||
} else if (sibling.isNamed) {
|
||||
break;
|
||||
}
|
||||
sibling = sibling.previousSibling;
|
||||
}
|
||||
};
|
||||
|
||||
collectComments(methodNode);
|
||||
if (commentTexts.length === 0 && methodNode.parent?.type === 'body_statement') {
|
||||
collectComments(methodNode.parent);
|
||||
}
|
||||
|
||||
const commentBlock = commentTexts.join('\n');
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
YARD_PARAM_RE.lastIndex = 0;
|
||||
while ((match = YARD_PARAM_RE.exec(commentBlock)) !== null) {
|
||||
params.set(match[1], match[2]);
|
||||
}
|
||||
YARD_PARAM_ALT_RE.lastIndex = 0;
|
||||
while ((match = YARD_PARAM_ALT_RE.exec(commentBlock)) !== null) {
|
||||
if (!params.has(match[2])) params.set(match[2], match[1]);
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
/**
|
||||
* Walk up the AST from a for-statement to find the enclosing method,
|
||||
* then search its YARD @param annotations for one named `iterableName`.
|
||||
* Returns the element type extracted from the raw YARD type string.
|
||||
*
|
||||
* Example: `@param users [Array<User>]` → extracts "User" from "Array<User>".
|
||||
*/
|
||||
const findRubyParamElementType = (iterableName: string, startNode: SyntaxNode): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
while (current) {
|
||||
if (RUBY_METHOD_NODE_TYPES.has(current.type)) {
|
||||
const rawParams = collectYardRawParams(current);
|
||||
const rawType = rawParams.get(iterableName);
|
||||
if (rawType) return extractElementTypeFromString(rawType);
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Ruby: for user in users ... end
|
||||
*
|
||||
* tree-sitter-ruby `for` node structure:
|
||||
* pattern field: the loop variable (identifier)
|
||||
* value field: `in` node whose child is the iterable expression
|
||||
*
|
||||
* Tier 1c: resolves the element type via:
|
||||
* 1. scopeEnv string — extractElementTypeFromString on the stored type
|
||||
* 2. AST walk — walks up to the enclosing method's YARD @param to read Array<User> directly
|
||||
*
|
||||
* Ruby has no static types on loop variables, so this mainly works when the
|
||||
* iterable has a YARD-annotated container type (e.g., `@param users [Array<User>]`).
|
||||
*/
|
||||
const extractForLoopBinding: ForLoopExtractor = (
|
||||
node: SyntaxNode,
|
||||
scopeEnv: Map<string, string>,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
scope: string,
|
||||
): void => {
|
||||
if (node.type !== 'for') return;
|
||||
|
||||
// The loop variable is the `pattern` field (identifier).
|
||||
const patternNode = node.childForFieldName('pattern');
|
||||
if (!patternNode) return;
|
||||
const loopVarName = extractVarName(patternNode);
|
||||
if (!loopVarName) return;
|
||||
|
||||
// The iterable is inside the `value` field which is an `in` node wrapping the expression.
|
||||
const inNode = node.childForFieldName('value');
|
||||
if (!inNode) return;
|
||||
const iterableNode = inNode.firstNamedChild;
|
||||
let iterableName: string | undefined;
|
||||
if (iterableNode?.type === 'identifier') {
|
||||
iterableName = iterableNode.text;
|
||||
} else if (iterableNode?.type === 'call') {
|
||||
const method = iterableNode.childForFieldName('method');
|
||||
if (method) iterableName = method.text;
|
||||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
// Ruby has no extractFromTypeNode (no AST type annotations), pass a no-op.
|
||||
const noopExtractFromTypeNode = (): string | undefined => undefined;
|
||||
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
noopExtractFromTypeNode, findRubyParamElementType,
|
||||
undefined,
|
||||
);
|
||||
if (!elementType) return;
|
||||
|
||||
scopeEnv.set(loopVarName, elementType);
|
||||
};
|
||||
|
||||
/**
|
||||
* Ruby: alias_user = user → assignment with left/right identifier fields.
|
||||
* Only handles plain identifier RHS (not calls, not literals).
|
||||
* Skips if LHS already has a resolved type in scopeEnv.
|
||||
*/
|
||||
const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
|
||||
if (node.type !== 'assignment') return undefined;
|
||||
const lhsNode = node.childForFieldName('left');
|
||||
if (!lhsNode || lhsNode.type !== 'identifier') return undefined;
|
||||
const varName = lhsNode.text;
|
||||
if (scopeEnv.has(varName)) return undefined;
|
||||
const rhsNode = node.childForFieldName('right');
|
||||
if (!rhsNode || rhsNode.type !== 'identifier') return undefined;
|
||||
return { lhs: varName, rhs: rhsNode.text };
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
extractInitializer,
|
||||
scanConstructorBinding,
|
||||
extractReturnType,
|
||||
extractForLoopBinding,
|
||||
extractPendingAssignment,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor, PatternBindingExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractGenericTypeArgs } from './shared.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor, PatternBindingExtractor, ForLoopExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, type TypeArgPosition } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'let_declaration',
|
||||
@@ -35,7 +35,8 @@ const extractStructPatternType = (structPattern: SyntaxNode): string | undefined
|
||||
* Recursively scan a pattern tree for captured_pattern nodes (x @ StructType { .. })
|
||||
* and extract variable → type bindings from them.
|
||||
*/
|
||||
const extractCapturedPatternBindings = (pattern: SyntaxNode, env: Map<string, string>): void => {
|
||||
const extractCapturedPatternBindings = (pattern: SyntaxNode, env: Map<string, string>, depth = 0): void => {
|
||||
if (depth > 50) return;
|
||||
if (pattern.type === 'captured_pattern') {
|
||||
// captured_pattern: identifier @ inner_pattern
|
||||
// The first named child is the identifier, followed by the inner pattern.
|
||||
@@ -57,7 +58,7 @@ const extractCapturedPatternBindings = (pattern: SyntaxNode, env: Map<string, st
|
||||
if (pattern.type === 'tuple_struct_pattern') {
|
||||
for (let i = 0; i < pattern.namedChildCount; i++) {
|
||||
const child = pattern.namedChild(i);
|
||||
if (child) extractCapturedPatternBindings(child, env);
|
||||
if (child) extractCapturedPatternBindings(child, env, depth + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -215,10 +216,25 @@ const extractPatternBinding: PatternBindingExtractor = (
|
||||
declarationTypeNodes,
|
||||
scope,
|
||||
) => {
|
||||
if (node.type !== 'let_condition') return undefined;
|
||||
let patternNode: SyntaxNode | null = null;
|
||||
let valueNode: SyntaxNode | null = null;
|
||||
|
||||
const patternNode = node.childForFieldName('pattern');
|
||||
const valueNode = node.childForFieldName('value');
|
||||
if (node.type === 'let_condition') {
|
||||
patternNode = node.childForFieldName('pattern');
|
||||
valueNode = node.childForFieldName('value');
|
||||
} else if (node.type === 'match_arm') {
|
||||
// match_arm → pattern field is match_pattern wrapping the actual pattern
|
||||
const matchPatternNode = node.childForFieldName('pattern');
|
||||
// Unwrap match_pattern to get the tuple_struct_pattern inside
|
||||
patternNode = matchPatternNode?.type === 'match_pattern'
|
||||
? matchPatternNode.firstNamedChild
|
||||
: matchPatternNode;
|
||||
// source variable is in the parent match_expression's 'value' field
|
||||
const matchExpr = node.parent?.parent; // match_arm → match_block → match_expression
|
||||
if (matchExpr?.type === 'match_expression') {
|
||||
valueNode = matchExpr.childForFieldName('value');
|
||||
}
|
||||
}
|
||||
if (!patternNode || !valueNode) return undefined;
|
||||
|
||||
// Only handle tuple_struct_pattern: Some(x) or Ok(x)
|
||||
@@ -269,12 +285,131 @@ const extractPatternBinding: PatternBindingExtractor = (
|
||||
return { varName: innerVar, typeName: typeArgs[argIndex] };
|
||||
};
|
||||
|
||||
// --- For-loop Tier 1c ---
|
||||
|
||||
const FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set(['for_expression']);
|
||||
|
||||
/** Extract element type from a Rust type annotation AST node.
|
||||
* Handles: generic_type (Vec<User>), reference_type (&[User]), array_type ([User; N]),
|
||||
* slice_type ([User]). For call-graph purposes, strips references (&User → User). */
|
||||
const extractRustElementTypeFromTypeNode = (typeNode: SyntaxNode, pos: TypeArgPosition = 'last', depth = 0): string | undefined => {
|
||||
if (depth > 50) return undefined;
|
||||
// generic_type: Vec<User>, HashMap<K, V> — extract type arg based on position
|
||||
if (typeNode.type === 'generic_type') {
|
||||
const args = extractGenericTypeArgs(typeNode);
|
||||
if (args.length >= 1) return pos === 'first' ? args[0] : args[args.length - 1];
|
||||
}
|
||||
// reference_type: &[User] or &Vec<User> — unwrap the reference and recurse
|
||||
if (typeNode.type === 'reference_type') {
|
||||
const inner = typeNode.lastNamedChild;
|
||||
if (inner) return extractRustElementTypeFromTypeNode(inner, pos, depth + 1);
|
||||
}
|
||||
// array_type: [User; N] — element is the first child
|
||||
if (typeNode.type === 'array_type') {
|
||||
const elemNode = typeNode.firstNamedChild;
|
||||
if (elemNode) return extractSimpleTypeName(elemNode);
|
||||
}
|
||||
// slice_type: [User] — element is the first child
|
||||
if (typeNode.type === 'slice_type') {
|
||||
const elemNode = typeNode.firstNamedChild;
|
||||
if (elemNode) return extractSimpleTypeName(elemNode);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Walk up from a for-loop to the enclosing function_item and search parameters
|
||||
* for one named `iterableName`. Returns the element type from its annotation. */
|
||||
const findRustParamElementType = (iterableName: string, startNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
while (current) {
|
||||
if (current.type === 'function_item') {
|
||||
const paramsNode = current.childForFieldName('parameters');
|
||||
if (paramsNode) {
|
||||
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
||||
const param = paramsNode.namedChild(i);
|
||||
if (!param || param.type !== 'parameter') continue;
|
||||
const nameNode = param.childForFieldName('pattern');
|
||||
if (!nameNode) continue;
|
||||
// Unwrap reference patterns: &users, &mut users
|
||||
let identNode = nameNode;
|
||||
if (identNode.type === 'reference_pattern') {
|
||||
identNode = identNode.lastNamedChild ?? identNode;
|
||||
}
|
||||
if (identNode.type === 'mut_pattern') {
|
||||
identNode = identNode.firstNamedChild ?? identNode;
|
||||
}
|
||||
if (identNode.text !== iterableName) continue;
|
||||
const typeNode = param.childForFieldName('type');
|
||||
if (typeNode) return extractRustElementTypeFromTypeNode(typeNode, pos);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Rust: for user in &users where users has a known container type.
|
||||
* Unwraps reference_expression (&users, &mut users) to get the iterable name. */
|
||||
const extractForLoopBinding: ForLoopExtractor = (
|
||||
node: SyntaxNode,
|
||||
scopeEnv: Map<string, string>,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
scope: string,
|
||||
): void => {
|
||||
if (node.type !== 'for_expression') return;
|
||||
|
||||
const patternNode = node.childForFieldName('pattern');
|
||||
const valueNode = node.childForFieldName('value');
|
||||
if (!patternNode || !valueNode) return;
|
||||
|
||||
// Extract iterable name + method — may be &users, users, or users.iter()/keys()/values()
|
||||
let iterableName: string | undefined;
|
||||
let methodName: string | undefined;
|
||||
if (valueNode.type === 'reference_expression') {
|
||||
const inner = valueNode.lastNamedChild;
|
||||
if (inner?.type === 'identifier') iterableName = inner.text;
|
||||
} else if (valueNode.type === 'identifier') {
|
||||
iterableName = valueNode.text;
|
||||
} else if (valueNode.type === 'field_expression') {
|
||||
const prop = valueNode.lastNamedChild;
|
||||
if (prop) iterableName = prop.text;
|
||||
} else if (valueNode.type === 'call_expression') {
|
||||
// users.iter() → call_expression > function: field_expression > identifier + field_identifier
|
||||
const fieldExpr = valueNode.childForFieldName('function');
|
||||
if (fieldExpr?.type === 'field_expression') {
|
||||
const obj = fieldExpr.firstNamedChild;
|
||||
if (obj?.type === 'identifier') iterableName = obj.text;
|
||||
// Extract method name: iter, keys, values, into_iter, etc.
|
||||
const field = fieldExpr.lastNamedChild;
|
||||
if (field?.type === 'field_identifier') methodName = field.text;
|
||||
}
|
||||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
const containerTypeName = scopeEnv.get(iterableName);
|
||||
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractRustElementTypeFromTypeNode, findRustParamElementType,
|
||||
typeArgPos,
|
||||
);
|
||||
if (!elementType) return;
|
||||
|
||||
const loopVarName = extractVarName(patternNode);
|
||||
if (loopVarName) scopeEnv.set(loopVarName, elementType);
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
|
||||
patternBindingNodeTypes: new Set(['let_condition', 'match_arm']),
|
||||
extractDeclaration,
|
||||
extractInitializer,
|
||||
extractParameter,
|
||||
scanConstructorBinding,
|
||||
extractForLoopBinding,
|
||||
extractPendingAssignment,
|
||||
extractPatternBinding,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,171 @@
|
||||
import type { SyntaxNode } from '../utils.js';
|
||||
|
||||
/** Which type argument to extract from a multi-arg generic container.
|
||||
* - 'first': key type (e.g., K from Map<K,V>) — used for .keys(), .keySet()
|
||||
* - 'last': value type (e.g., V from Map<K,V>) — used for .values(), .items(), .iter() */
|
||||
export type TypeArgPosition = 'first' | 'last';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Container type descriptors — maps container base names to type parameter
|
||||
// semantics per access method. Replaces the simple KEY_METHODS heuristic.
|
||||
//
|
||||
// For user-defined generics (MyCache<K,V> extends Map<K,V>), heritage-aware
|
||||
// fallback can walk the EXTENDS chain to find a matching descriptor.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Describes which type parameter position each access method yields. */
|
||||
interface ContainerDescriptor {
|
||||
/** Number of type parameters (1 = single-element, 2 = key-value) */
|
||||
arity: number;
|
||||
/** Methods that yield the first type parameter (key type for maps) */
|
||||
keyMethods: ReadonlySet<string>;
|
||||
/** Methods that yield the last type parameter (value type) */
|
||||
valueMethods: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
/** Empty set for containers that have no key-yielding methods */
|
||||
const NO_KEYS: ReadonlySet<string> = new Set();
|
||||
|
||||
/** Standard key-yielding methods across languages */
|
||||
const STD_KEY_METHODS: ReadonlySet<string> = new Set(['keys']);
|
||||
const JAVA_KEY_METHODS: ReadonlySet<string> = new Set(['keySet']);
|
||||
const CSHARP_KEY_METHODS: ReadonlySet<string> = new Set(['Keys']);
|
||||
|
||||
/** Standard value-yielding methods across languages */
|
||||
const STD_VALUE_METHODS: ReadonlySet<string> = new Set(['values', 'get', 'pop', 'remove']);
|
||||
const CSHARP_VALUE_METHODS: ReadonlySet<string> = new Set(['Values', 'TryGetValue']);
|
||||
const SINGLE_ELEMENT_METHODS: ReadonlySet<string> = new Set([
|
||||
'iter', 'into_iter', 'iterator', 'get', 'first', 'last', 'pop',
|
||||
'peek', 'poll', 'find', 'filter', 'map',
|
||||
]);
|
||||
|
||||
const CONTAINER_DESCRIPTORS: ReadonlyMap<string, ContainerDescriptor> = new Map([
|
||||
// --- Map / Dict types (arity 2: key + value) ---
|
||||
['Map', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['WeakMap', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['HashMap', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['BTreeMap', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['LinkedHashMap', { arity: 2, keyMethods: JAVA_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['TreeMap', { arity: 2, keyMethods: JAVA_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['dict', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['Dict', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['Dictionary', { arity: 2, keyMethods: CSHARP_KEY_METHODS, valueMethods: CSHARP_VALUE_METHODS }],
|
||||
['SortedDictionary', { arity: 2, keyMethods: CSHARP_KEY_METHODS, valueMethods: CSHARP_VALUE_METHODS }],
|
||||
['Record', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['OrderedDict', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['ConcurrentHashMap', { arity: 2, keyMethods: JAVA_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['ConcurrentDictionary', { arity: 2, keyMethods: CSHARP_KEY_METHODS, valueMethods: CSHARP_VALUE_METHODS }],
|
||||
|
||||
// --- Single-element containers (arity 1) ---
|
||||
['Array', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['List', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['ArrayList', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['LinkedList',{ arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['Vec', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['VecDeque', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['Set', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['HashSet', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['BTreeSet', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['TreeSet', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['Queue', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['Deque', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['Stack', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['Sequence', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['Iterable', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['Iterator', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['IEnumerable', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['IList', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['ICollection', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['Collection', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['ObservableCollection', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['IEnumerator', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['SortedSet', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['Stream', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['MutableList', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['MutableSet', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['LinkedHashSet', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['ArrayDeque', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['PriorityQueue', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['MutableMap', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['list', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['set', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['tuple', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['frozenset', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
]);
|
||||
|
||||
/** Determine which type arg to extract based on container type name and access method.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. If container is known and method is in keyMethods → 'first'
|
||||
* 2. If container is known with arity 1 → 'last' (same as 'first' for single-arg)
|
||||
* 3. If container is unknown → fall back to method name heuristic
|
||||
* 4. Default: 'last' (value type)
|
||||
*/
|
||||
export function methodToTypeArgPosition(methodName: string | undefined, containerTypeName?: string): TypeArgPosition {
|
||||
if (containerTypeName) {
|
||||
const desc = CONTAINER_DESCRIPTORS.get(containerTypeName);
|
||||
if (desc) {
|
||||
// Single-element container: always 'last' (= only arg)
|
||||
if (desc.arity === 1) return 'last';
|
||||
// Multi-element: check if method yields key type
|
||||
if (methodName && desc.keyMethods.has(methodName)) return 'first';
|
||||
// Default for multi-element: value type
|
||||
return 'last';
|
||||
}
|
||||
}
|
||||
// Fallback for unknown containers: simple method name heuristic
|
||||
if (methodName && (methodName === 'keys' || methodName === 'keySet' || methodName === 'Keys')) {
|
||||
return 'first';
|
||||
}
|
||||
return 'last';
|
||||
}
|
||||
|
||||
/** Look up the container descriptor for a type name. Exported for heritage-chain lookups. */
|
||||
export function getContainerDescriptor(typeName: string): ContainerDescriptor | undefined {
|
||||
return CONTAINER_DESCRIPTORS.get(typeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared 3-strategy fallback for resolving the element type of a container variable.
|
||||
* Used by all for-loop extractors to resolve the loop variable's type from the iterable.
|
||||
*
|
||||
* Strategy 1: declarationTypeNodes — raw AST type annotation node (handles container types
|
||||
* where extractSimpleTypeName returned undefined, e.g., User[], List[User])
|
||||
* Strategy 2: scopeEnv string — extractElementTypeFromString on the stored type string
|
||||
* Strategy 3: AST walk — language-specific upward walk to enclosing function parameters
|
||||
*
|
||||
* @param extractFromTypeNode Language-specific function to extract element type from AST node
|
||||
* @param findParamElementType Optional language-specific AST walk to find parameter type
|
||||
* @param typeArgPos Which generic type arg to extract: 'first' for keys, 'last' for values (default)
|
||||
*/
|
||||
export function resolveIterableElementType(
|
||||
iterableName: string,
|
||||
node: SyntaxNode,
|
||||
scopeEnv: ReadonlyMap<string, string>,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
scope: string,
|
||||
extractFromTypeNode: (typeNode: SyntaxNode, pos?: TypeArgPosition) => string | undefined,
|
||||
findParamElementType?: (name: string, startNode: SyntaxNode, pos?: TypeArgPosition) => string | undefined,
|
||||
typeArgPos: TypeArgPosition = 'last',
|
||||
): string | undefined {
|
||||
// Strategy 1: declarationTypeNodes AST node (check current scope, then file scope)
|
||||
const typeNode = declarationTypeNodes.get(`${scope}\0${iterableName}`)
|
||||
?? (scope !== '' ? declarationTypeNodes.get(`\0${iterableName}`) : undefined);
|
||||
if (typeNode) {
|
||||
const t = extractFromTypeNode(typeNode, typeArgPos);
|
||||
if (t) return t;
|
||||
}
|
||||
// Strategy 2: scopeEnv string → extractElementTypeFromString
|
||||
const iterableType = scopeEnv.get(iterableName);
|
||||
if (iterableType) {
|
||||
const el = extractElementTypeFromString(iterableType, typeArgPos);
|
||||
if (el) return el;
|
||||
}
|
||||
// Strategy 3: AST walk to function parameters
|
||||
if (findParamElementType) return findParamElementType(iterableName, node, typeArgPos);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** 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).
|
||||
@@ -16,7 +182,8 @@ const NULLABLE_WRAPPER_TYPES = new Set([
|
||||
* (e.g., models.User → User), and nullable types (e.g., User? → User).
|
||||
* Returns undefined for complex types (unions, intersections, function types).
|
||||
*/
|
||||
export const extractSimpleTypeName = (typeNode: SyntaxNode): string | undefined => {
|
||||
export const extractSimpleTypeName = (typeNode: SyntaxNode, depth = 0): string | undefined => {
|
||||
if (depth > 50 || typeNode.text.length > 2048) return undefined;
|
||||
// Direct type identifier (includes Ruby 'constant' for class names)
|
||||
if (typeNode.type === 'type_identifier' || typeNode.type === 'identifier'
|
||||
|| typeNode.type === 'simple_identifier' || typeNode.type === 'constant') {
|
||||
@@ -40,14 +207,21 @@ export const extractSimpleTypeName = (typeNode: SyntaxNode): string | undefined
|
||||
}
|
||||
}
|
||||
|
||||
// C++ template_type (e.g., vector<User>, map<string, User>): extract base name
|
||||
if (typeNode.type === 'template_type') {
|
||||
const base = typeNode.childForFieldName('name') ?? typeNode.firstNamedChild;
|
||||
if (base) return extractSimpleTypeName(base, depth + 1);
|
||||
}
|
||||
|
||||
// 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') {
|
||||
if (typeNode.type === 'generic_type' || typeNode.type === 'parameterized_type'
|
||||
|| typeNode.type === 'generic_name') {
|
||||
const base = typeNode.childForFieldName('name')
|
||||
?? typeNode.childForFieldName('type')
|
||||
?? typeNode.firstNamedChild;
|
||||
if (!base) return undefined;
|
||||
const baseName = extractSimpleTypeName(base);
|
||||
const baseName = extractSimpleTypeName(base, depth + 1);
|
||||
// Unwrap known nullable wrappers: Optional<User> → User, Option<User> → User
|
||||
if (baseName && NULLABLE_WRAPPER_TYPES.has(baseName)) {
|
||||
const args = extractGenericTypeArgs(typeNode);
|
||||
@@ -59,7 +233,7 @@ export const extractSimpleTypeName = (typeNode: SyntaxNode): string | undefined
|
||||
// Nullable types (Kotlin User?, C# User?)
|
||||
if (typeNode.type === 'nullable_type') {
|
||||
const inner = typeNode.firstNamedChild;
|
||||
if (inner) return extractSimpleTypeName(inner);
|
||||
if (inner) return extractSimpleTypeName(inner, depth + 1);
|
||||
}
|
||||
|
||||
// Nullable union types (TS/JS: User | null, User | undefined, User | null | undefined)
|
||||
@@ -76,7 +250,7 @@ export const extractSimpleTypeName = (typeNode: SyntaxNode): string | undefined
|
||||
}
|
||||
// Only unwrap if exactly one meaningful type remains
|
||||
if (nonNullTypes.length === 1) {
|
||||
return extractSimpleTypeName(nonNullTypes[0]);
|
||||
return extractSimpleTypeName(nonNullTypes[0], depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,24 +258,25 @@ export const extractSimpleTypeName = (typeNode: SyntaxNode): string | undefined
|
||||
if (typeNode.type === 'type_annotation' || typeNode.type === 'type'
|
||||
|| typeNode.type === 'user_type') {
|
||||
const inner = typeNode.firstNamedChild;
|
||||
if (inner) return extractSimpleTypeName(inner);
|
||||
if (inner) return extractSimpleTypeName(inner, depth + 1);
|
||||
}
|
||||
|
||||
// Pointer/reference types (C++, Rust): User*, &User, &mut User
|
||||
if (typeNode.type === 'pointer_type' || typeNode.type === 'reference_type') {
|
||||
const inner = typeNode.firstNamedChild;
|
||||
if (inner) return extractSimpleTypeName(inner);
|
||||
if (inner) return extractSimpleTypeName(inner, depth + 1);
|
||||
}
|
||||
|
||||
// PHP primitive_type (string, int, float, bool)
|
||||
if (typeNode.type === 'primitive_type') {
|
||||
// Primitive/predefined types: string, int, float, bool, number, unknown, any
|
||||
// PHP: primitive_type; TS/JS: predefined_type
|
||||
if (typeNode.type === 'primitive_type' || typeNode.type === 'predefined_type') {
|
||||
return typeNode.text;
|
||||
}
|
||||
|
||||
// PHP named_type / optional_type
|
||||
if (typeNode.type === 'named_type' || typeNode.type === 'optional_type') {
|
||||
const inner = typeNode.childForFieldName('name') ?? typeNode.firstNamedChild;
|
||||
if (inner) return extractSimpleTypeName(inner);
|
||||
if (inner) return extractSimpleTypeName(inner, depth + 1);
|
||||
}
|
||||
|
||||
// Name node (PHP)
|
||||
@@ -119,7 +294,7 @@ export const extractSimpleTypeName = (typeNode: SyntaxNode): string | undefined
|
||||
export const extractVarName = (node: SyntaxNode): string | undefined => {
|
||||
if (node.type === 'identifier' || node.type === 'simple_identifier'
|
||||
|| node.type === 'variable_name' || node.type === 'name'
|
||||
|| node.type === 'constant') {
|
||||
|| node.type === 'constant' || node.type === 'property_identifier') {
|
||||
return node.text;
|
||||
}
|
||||
// variable_declarator (Java/C#): has a 'name' field
|
||||
@@ -141,9 +316,11 @@ export const TYPED_PARAMETER_TYPES = new Set([
|
||||
'optional_parameter', // TS: (x?: Foo)
|
||||
'formal_parameter', // Java/Kotlin
|
||||
'parameter', // C#/Rust/Go/Python/Swift
|
||||
'typed_parameter', // Python: def f(x: Foo) — distinct from 'parameter' in tree-sitter-python
|
||||
'parameter_declaration', // C/C++ void f(Type name)
|
||||
'simple_parameter', // PHP function(Foo $x)
|
||||
'property_promotion_parameter', // PHP 8.0+ constructor promotion: __construct(private Foo $x)
|
||||
'closure_parameter', // Rust: |user: User| — typed closure parameters
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -164,18 +341,20 @@ export const TYPED_PARAMETER_TYPES = new Set([
|
||||
* returns [] for non-generic types).
|
||||
* @returns Array of resolved type argument names. Unresolvable arguments are omitted.
|
||||
*/
|
||||
export const extractGenericTypeArgs = (typeNode: SyntaxNode): string[] => {
|
||||
export const extractGenericTypeArgs = (typeNode: SyntaxNode, depth = 0): string[] => {
|
||||
if (depth > 50) return [];
|
||||
// Unwrap wrapper nodes that may sit above the generic_type
|
||||
if (typeNode.type === 'type_annotation' || typeNode.type === 'type'
|
||||
|| typeNode.type === 'user_type' || typeNode.type === 'nullable_type'
|
||||
|| typeNode.type === 'optional_type') {
|
||||
const inner = typeNode.firstNamedChild;
|
||||
if (inner) return extractGenericTypeArgs(inner);
|
||||
if (inner) return extractGenericTypeArgs(inner, depth + 1);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Only process generic/parameterized type nodes
|
||||
if (typeNode.type !== 'generic_type' && typeNode.type !== 'parameterized_type') {
|
||||
// Only process generic/parameterized type nodes (includes C#'s generic_name)
|
||||
if (typeNode.type !== 'generic_type' && typeNode.type !== 'parameterized_type'
|
||||
&& typeNode.type !== 'generic_name') {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -316,3 +495,116 @@ export const findChildByType = (node: SyntaxNode, type: string): SyntaxNode | nu
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Internal helper: extract the first comma-separated argument from a string,
|
||||
// respecting nested angle-bracket and square-bracket depth.
|
||||
function extractFirstArg(args: string): string {
|
||||
let depth = 0;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const ch = args[i];
|
||||
if (ch === '<' || ch === '[') depth++;
|
||||
else if (ch === '>' || ch === ']') depth--;
|
||||
else if (ch === ',' && depth === 0) return args.slice(0, i).trim();
|
||||
}
|
||||
return args.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract element type from a container type string.
|
||||
* Uses bracket-balanced parsing (no regex) for generic argument extraction.
|
||||
* Returns undefined for ambiguous or unparseable strings.
|
||||
*
|
||||
* Handles:
|
||||
* - Array<User> → User (generic angle brackets)
|
||||
* - User[] → User (array suffix)
|
||||
* - []User → User (Go slice prefix)
|
||||
* - List[User] → User (Python subscript)
|
||||
* - [User] → User (Swift array sugar)
|
||||
* - vector<User> → User (C++ container)
|
||||
* - Vec<User> → User (Rust container)
|
||||
*
|
||||
* For multi-argument generics (Map<K, V>), returns the first or last type arg
|
||||
* based on `pos` ('first' for keys, 'last' for values — default 'last').
|
||||
* Returns undefined when the extracted type is not a simple word.
|
||||
*/
|
||||
export function extractElementTypeFromString(typeStr: string, pos: TypeArgPosition = 'last'): string | undefined {
|
||||
if (!typeStr || typeStr.length === 0 || typeStr.length > 2048) return undefined;
|
||||
|
||||
// 1. Array suffix: User[] → User
|
||||
if (typeStr.endsWith('[]')) {
|
||||
const base = typeStr.slice(0, -2).trim();
|
||||
return base && /^\w+$/.test(base) ? base : undefined;
|
||||
}
|
||||
|
||||
// 2. Go slice prefix: []User → User
|
||||
if (typeStr.startsWith('[]')) {
|
||||
const element = typeStr.slice(2).trim();
|
||||
return element && /^\w+$/.test(element) ? element : undefined;
|
||||
}
|
||||
|
||||
// 3. Swift array sugar: [User] → User
|
||||
// Must start with '[', end with ']', and contain no angle brackets
|
||||
// (to avoid confusing with List[User] handled below).
|
||||
if (typeStr.startsWith('[') && typeStr.endsWith(']') && !typeStr.includes('<')) {
|
||||
const element = typeStr.slice(1, -1).trim();
|
||||
return element && /^\w+$/.test(element) ? element : undefined;
|
||||
}
|
||||
|
||||
// 4. Generic bracket-balanced extraction: Array<User> / List[User] / Vec<User>
|
||||
// Find the first opening bracket (< or [) and pick the one that appears first.
|
||||
const openAngle = typeStr.indexOf('<');
|
||||
const openSquare = typeStr.indexOf('[');
|
||||
|
||||
let openIdx = -1;
|
||||
let openChar = '';
|
||||
let closeChar = '';
|
||||
|
||||
if (openAngle >= 0 && (openSquare < 0 || openAngle < openSquare)) {
|
||||
openIdx = openAngle;
|
||||
openChar = '<';
|
||||
closeChar = '>';
|
||||
} else if (openSquare >= 0) {
|
||||
openIdx = openSquare;
|
||||
openChar = '[';
|
||||
closeChar = ']';
|
||||
}
|
||||
|
||||
if (openIdx < 0) return undefined;
|
||||
|
||||
// Walk bracket-balanced from the character after the opening bracket to find
|
||||
// the matching close bracket, tracking depth for nested brackets.
|
||||
// All bracket types (<, >, [, ]) contribute to depth uniformly, but only the
|
||||
// selected closeChar can match at depth 0 (prevents cross-bracket miscounting).
|
||||
let depth = 0;
|
||||
const start = openIdx + 1;
|
||||
let lastCommaIdx = -1; // Track last top-level comma for 'last' position
|
||||
for (let i = start; i < typeStr.length; i++) {
|
||||
const ch = typeStr[i];
|
||||
if (ch === '<' || ch === '[') {
|
||||
depth++;
|
||||
} else if (ch === '>' || ch === ']') {
|
||||
if (depth === 0) {
|
||||
// At depth 0 — only match if it is our selected close bracket.
|
||||
if (ch !== closeChar) return undefined; // mismatched bracket = malformed
|
||||
if (pos === 'last' && lastCommaIdx >= 0) {
|
||||
// Return last arg (text after last comma)
|
||||
const lastArg = typeStr.slice(lastCommaIdx + 1, i).trim();
|
||||
return lastArg && /^\w+$/.test(lastArg) ? lastArg : undefined;
|
||||
}
|
||||
const inner = typeStr.slice(start, i).trim();
|
||||
const firstArg = extractFirstArg(inner);
|
||||
return firstArg && /^\w+$/.test(firstArg) ? firstArg : undefined;
|
||||
}
|
||||
depth--;
|
||||
} else if (ch === ',' && depth === 0) {
|
||||
if (pos === 'first') {
|
||||
// Return first arg (text before first comma)
|
||||
const arg = typeStr.slice(start, i).trim();
|
||||
return arg && /^\w+$/.test(arg) ? arg : undefined;
|
||||
}
|
||||
lastCommaIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -24,10 +24,14 @@ 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. */
|
||||
/** Extracts loop variable type binding from a for-each statement.
|
||||
* All parameters are required (aligned with PatternBindingExtractor convention)
|
||||
* to prevent new extractors from silently ignoring declarationTypeNodes/scope. */
|
||||
export type ForLoopExtractor = (
|
||||
node: SyntaxNode,
|
||||
scopeEnv: Map<string, string>,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
scope: string,
|
||||
) => void;
|
||||
|
||||
/** Extracts a plain-identifier assignment for Tier 2 propagation.
|
||||
@@ -57,10 +61,20 @@ export type PatternBindingExtractor = (
|
||||
|
||||
/** Per-language type extraction configuration */
|
||||
export interface LanguageTypeConfig {
|
||||
/** Allow pattern binding to overwrite existing scopeEnv entries.
|
||||
* WARNING: Enables function-scope type pollution. Only for languages with
|
||||
* smart-cast semantics (e.g., Kotlin `when/is`) where the subject variable
|
||||
* already exists in scopeEnv from its declaration. */
|
||||
readonly allowPatternBindingOverwrite?: boolean;
|
||||
/** 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>;
|
||||
/** Optional allowlist of AST node types on which extractPatternBinding should run.
|
||||
* When present, extractPatternBinding is only invoked for nodes whose type is in this set,
|
||||
* short-circuiting the call for all other node types. When absent, every node is passed to
|
||||
* extractPatternBinding (legacy behaviour). */
|
||||
patternBindingNodeTypes?: ReadonlySet<string>;
|
||||
/** Extract a (varName → typeName) binding from a declaration node */
|
||||
extractDeclaration: TypeBindingExtractor;
|
||||
/** Extract a (varName → typeName) binding from a parameter node */
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractCalleeName } from './shared.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor, ForLoopExtractor, PatternBindingExtractor } 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([
|
||||
'lexical_declaration',
|
||||
'variable_declaration',
|
||||
'function_declaration', // JSDoc @param on function declarations
|
||||
'method_definition', // JSDoc @param on class methods
|
||||
'public_field_definition', // class field: private users: User[]
|
||||
]);
|
||||
|
||||
const normalizeJsDocType = (raw: string): string | undefined => {
|
||||
@@ -80,6 +81,18 @@ const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<str
|
||||
return;
|
||||
}
|
||||
|
||||
// Class field: `private users: User[]` — public_field_definition has name + type fields directly.
|
||||
if (node.type === 'public_field_definition') {
|
||||
const nameNode = node.childForFieldName('name');
|
||||
const typeAnnotation = node.childForFieldName('type');
|
||||
if (!nameNode || !typeAnnotation) return;
|
||||
const varName = nameNode.text;
|
||||
if (!varName) return;
|
||||
const typeName = extractSimpleTypeName(typeAnnotation);
|
||||
if (typeName) env.set(varName, typeName);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const declarator = node.namedChild(i);
|
||||
if (declarator?.type !== 'variable_declarator') continue;
|
||||
@@ -191,6 +204,225 @@ const extractReturnType: ReturnTypeExtractor = (node) => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'for_in_statement',
|
||||
]);
|
||||
|
||||
/** TS function/method node types that carry a parameters list. */
|
||||
const TS_FUNCTION_NODE_TYPES = new Set([
|
||||
'function_declaration', 'function_expression', 'arrow_function',
|
||||
'method_definition', 'generator_function', 'generator_function_declaration',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Extract element type from a TypeScript type annotation AST node.
|
||||
* Handles:
|
||||
* type_annotation ": User[]" → array_type → type_identifier "User"
|
||||
* type_annotation ": Array<User>" → generic_type → extractGenericTypeArgs → "User"
|
||||
* Falls back to text-based extraction via extractElementTypeFromString.
|
||||
*/
|
||||
const extractTsElementTypeFromAnnotation = (typeAnnotation: SyntaxNode, pos: TypeArgPosition = 'last', depth = 0): string | undefined => {
|
||||
if (depth > 50) return undefined;
|
||||
// Unwrap type_annotation (the node text includes ': ' prefix)
|
||||
const inner = typeAnnotation.type === 'type_annotation'
|
||||
? (typeAnnotation.firstNamedChild ?? typeAnnotation)
|
||||
: typeAnnotation;
|
||||
|
||||
// readonly User[] — readonly_type wraps array_type: unwrap and recurse
|
||||
if (inner.type === 'readonly_type') {
|
||||
const wrapped = inner.firstNamedChild;
|
||||
if (wrapped) return extractTsElementTypeFromAnnotation(wrapped, pos, depth + 1);
|
||||
}
|
||||
|
||||
// User[] — array_type: first named child is the element type
|
||||
if (inner.type === 'array_type') {
|
||||
const elem = inner.firstNamedChild;
|
||||
if (elem) return extractSimpleTypeName(elem);
|
||||
}
|
||||
|
||||
// Array<User>, Map<string, User> — generic_type
|
||||
// pos determines which type arg: 'first' for keys, 'last' for values
|
||||
if (inner.type === 'generic_type') {
|
||||
const args = extractGenericTypeArgs(inner);
|
||||
if (args.length >= 1) return pos === 'first' ? args[0] : args[args.length - 1];
|
||||
}
|
||||
|
||||
// Fallback: strip ': ' prefix from type_annotation text and use string extraction
|
||||
const rawText = inner.text;
|
||||
return extractElementTypeFromString(rawText, pos);
|
||||
};
|
||||
|
||||
/**
|
||||
* Search a statement_block (function body) for a variable_declarator named `iterableName`
|
||||
* that has a type annotation, preceding the given `beforeNode`.
|
||||
* Returns the element type from the type annotation, or undefined.
|
||||
*/
|
||||
const findTsLocalDeclElementType = (
|
||||
iterableName: string,
|
||||
blockNode: SyntaxNode,
|
||||
beforeNode: SyntaxNode,
|
||||
pos: TypeArgPosition = 'last',
|
||||
): string | undefined => {
|
||||
for (let i = 0; i < blockNode.namedChildCount; i++) {
|
||||
const stmt = blockNode.namedChild(i);
|
||||
if (!stmt) continue;
|
||||
// Stop when we reach the for-loop itself
|
||||
if (stmt === beforeNode || stmt.startIndex >= beforeNode.startIndex) break;
|
||||
// Look for lexical_declaration or variable_declaration
|
||||
if (stmt.type !== 'lexical_declaration' && stmt.type !== 'variable_declaration') continue;
|
||||
for (let j = 0; j < stmt.namedChildCount; j++) {
|
||||
const decl = stmt.namedChild(j);
|
||||
if (decl?.type !== 'variable_declarator') continue;
|
||||
const nameNode = decl.childForFieldName('name');
|
||||
if (nameNode?.text !== iterableName) continue;
|
||||
const typeAnnotation = decl.childForFieldName('type');
|
||||
if (typeAnnotation) return extractTsElementTypeFromAnnotation(typeAnnotation, pos);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Walk up the AST from a for-loop node to find the enclosing function scope,
|
||||
* then search (1) its parameter list and (2) local declarations in the body
|
||||
* for a variable named `iterableName` with a container type annotation.
|
||||
* Returns the element type extracted from the annotation, or undefined.
|
||||
*/
|
||||
const findTsIterableElementType = (iterableName: string, startNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
// Capture the immediate statement_block parent to search local declarations
|
||||
const blockNode = current?.type === 'statement_block' ? current : null;
|
||||
|
||||
while (current) {
|
||||
if (TS_FUNCTION_NODE_TYPES.has(current.type)) {
|
||||
// Search function parameters
|
||||
const paramsNode = current.childForFieldName('parameters')
|
||||
?? current.childForFieldName('formal_parameters');
|
||||
if (paramsNode) {
|
||||
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
||||
const param = paramsNode.namedChild(i);
|
||||
if (!param) continue;
|
||||
const patternNode = param.childForFieldName('pattern') ?? param.childForFieldName('name');
|
||||
if (patternNode?.text === iterableName) {
|
||||
const typeAnnotation = param.childForFieldName('type');
|
||||
if (typeAnnotation) return extractTsElementTypeFromAnnotation(typeAnnotation, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Search local declarations in the function body (statement_block)
|
||||
if (blockNode) {
|
||||
const result = findTsLocalDeclElementType(iterableName, blockNode, startNode, pos);
|
||||
if (result) return result;
|
||||
}
|
||||
break; // stop at the nearest function boundary
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* TypeScript/JavaScript: for (const user of users) where users has a known array type.
|
||||
*
|
||||
* Both `for...of` and `for...in` use the same `for_in_statement` AST node in tree-sitter.
|
||||
* We differentiate by checking for the `of` keyword among the unnamed children.
|
||||
*
|
||||
* Tier 1c: resolves the element type via three strategies in priority order:
|
||||
* 1. declarationTypeNodes — raw type annotation AST node (covers Array<User> from declarations)
|
||||
* 2. scopeEnv string — extractElementTypeFromString on the stored type (covers locally annotated vars)
|
||||
* 3. AST walk — walks up to the enclosing function's parameters to read User[] annotations directly
|
||||
* Only handles `for...of`; `for...in` produces string keys, not element types.
|
||||
*/
|
||||
const extractForLoopBinding: ForLoopExtractor = (
|
||||
node: SyntaxNode,
|
||||
scopeEnv: Map<string, string>,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
scope: string,
|
||||
): void => {
|
||||
if (node.type !== 'for_in_statement') return;
|
||||
|
||||
// Confirm this is `for...of`, not `for...in`, by scanning unnamed children for the keyword text.
|
||||
let isForOf = false;
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child && !child.isNamed && child.text === 'of') {
|
||||
isForOf = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isForOf) return;
|
||||
|
||||
// The iterable is the `right` field — may be identifier or call_expression.
|
||||
const rightNode = node.childForFieldName('right');
|
||||
let iterableName: string | undefined;
|
||||
let methodName: string | undefined;
|
||||
if (rightNode?.type === 'identifier') {
|
||||
iterableName = rightNode.text;
|
||||
} else if (rightNode?.type === 'member_expression') {
|
||||
const prop = rightNode.childForFieldName('property');
|
||||
if (prop) iterableName = prop.text;
|
||||
} else if (rightNode?.type === 'call_expression') {
|
||||
// entries.values() → call_expression > function: member_expression > object + property
|
||||
// this.repos.values() → nested member_expression: extract property from inner member
|
||||
const fn = rightNode.childForFieldName('function');
|
||||
if (fn?.type === 'member_expression') {
|
||||
const obj = fn.childForFieldName('object');
|
||||
const prop = fn.childForFieldName('property');
|
||||
if (obj?.type === 'identifier') {
|
||||
iterableName = obj.text;
|
||||
} else if (obj?.type === 'member_expression') {
|
||||
// this.repos.values() → obj = this.repos → extract 'repos'
|
||||
const innerProp = obj.childForFieldName('property');
|
||||
if (innerProp) iterableName = innerProp.text;
|
||||
}
|
||||
if (prop?.type === 'property_identifier') methodName = prop.text;
|
||||
}
|
||||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
// Look up the container's base type name for descriptor-aware resolution
|
||||
const containerTypeName = scopeEnv.get(iterableName);
|
||||
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractTsElementTypeFromAnnotation, findTsIterableElementType,
|
||||
typeArgPos,
|
||||
);
|
||||
if (!elementType) return;
|
||||
|
||||
// The loop variable is the `left` field.
|
||||
const leftNode = node.childForFieldName('left');
|
||||
if (!leftNode) return;
|
||||
|
||||
// Handle destructured for-of: for (const [k, v] of entries)
|
||||
// AST: left = array_pattern directly (no variable_declarator wrapper)
|
||||
// Bind the LAST identifier to the element type (value in [key, value] patterns)
|
||||
if (leftNode.type === 'array_pattern') {
|
||||
const lastChild = leftNode.lastNamedChild;
|
||||
if (lastChild?.type === 'identifier') {
|
||||
scopeEnv.set(lastChild.text, elementType);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (leftNode.type === 'object_pattern') {
|
||||
// Object destructuring (e.g., `for (const { id } of users)`) destructures
|
||||
// into fields of the element type. Without field-level resolution, we cannot
|
||||
// bind individual properties to their correct types. Skip to avoid false bindings.
|
||||
return;
|
||||
}
|
||||
|
||||
let loopVarNode: SyntaxNode | null = leftNode;
|
||||
// `const user` parses as: left → variable_declarator containing an identifier named `user`
|
||||
if (loopVarNode.type === 'variable_declarator') {
|
||||
loopVarNode = loopVarNode.childForFieldName('name') ?? loopVarNode.firstNamedChild;
|
||||
}
|
||||
if (!loopVarNode) return;
|
||||
|
||||
const loopVarName = extractVarName(loopVarNode);
|
||||
if (loopVarName) scopeEnv.set(loopVarName, elementType);
|
||||
};
|
||||
|
||||
/** TS/JS: const alias = u → variable_declarator with name/value fields */
|
||||
const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
@@ -206,12 +438,31 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** TS instanceof narrowing: `x instanceof User` → bind x to User.
|
||||
* Only works when x has no prior type binding (e.g. x: unknown, untyped params).
|
||||
* Typed params (x: Animal) are blocked by the !scopeEnv.has() guard in buildTypeEnv.
|
||||
* Uses first-writer-wins, same as Rust match arm bindings. */
|
||||
const extractPatternBinding: PatternBindingExtractor = (node) => {
|
||||
if (node.type !== 'binary_expression') return undefined;
|
||||
const op = node.children.find(c => !c.isNamed && c.text === 'instanceof');
|
||||
if (!op) return undefined;
|
||||
// binary_expression children are positional — no left/right fields
|
||||
const left = node.namedChild(0);
|
||||
const right = node.namedChild(1);
|
||||
if (left?.type !== 'identifier' || right?.type !== 'identifier') return undefined;
|
||||
return { varName: left.text, typeName: right.text };
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
|
||||
patternBindingNodeTypes: new Set(['binary_expression']),
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
extractInitializer,
|
||||
scanConstructorBinding,
|
||||
extractReturnType,
|
||||
extractForLoopBinding,
|
||||
extractPendingAssignment,
|
||||
extractPatternBinding,
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ export const DEFINITION_CAPTURE_KEYS = [
|
||||
] as const;
|
||||
|
||||
/** Extract the definition node from a tree-sitter query capture map. */
|
||||
export const getDefinitionNodeFromCaptures = (captureMap: Record<string, any>): any | null => {
|
||||
export const getDefinitionNodeFromCaptures = (captureMap: Record<string, any>): SyntaxNode | null => {
|
||||
for (const key of DEFINITION_CAPTURE_KEYS) {
|
||||
if (captureMap[key]) return captureMap[key];
|
||||
}
|
||||
@@ -351,7 +351,7 @@ export const findEnclosingClassId = (node: any, filePath: string): string | null
|
||||
* Extract function name and label from a function_definition or similar AST node.
|
||||
* Handles C/C++ qualified_identifier (ClassName::MethodName) and other language patterns.
|
||||
*/
|
||||
export const extractFunctionName = (node: any): { funcName: string | null; label: string } => {
|
||||
export const extractFunctionName = (node: SyntaxNode): { funcName: string | null; label: string } => {
|
||||
let funcName: string | null = null;
|
||||
let label = 'Function';
|
||||
|
||||
@@ -366,21 +366,40 @@ export const extractFunctionName = (node: any): { funcName: string | null; label
|
||||
if (FUNCTION_DECLARATION_TYPES.has(node.type)) {
|
||||
// C/C++: function_definition -> [pointer_declarator ->] function_declarator -> qualified_identifier/identifier
|
||||
// Unwrap pointer_declarator / reference_declarator wrappers to reach function_declarator
|
||||
let declarator = node.childForFieldName?.('declarator') ||
|
||||
node.children?.find((c: any) => c.type === 'function_declarator');
|
||||
let declarator = node.childForFieldName?.('declarator');
|
||||
if (!declarator) {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c?.type === 'function_declarator') { declarator = c; break; }
|
||||
}
|
||||
}
|
||||
while (declarator && (declarator.type === 'pointer_declarator' || declarator.type === 'reference_declarator')) {
|
||||
declarator = declarator.childForFieldName?.('declarator') ||
|
||||
declarator.children?.find((c: any) =>
|
||||
c.type === 'function_declarator' || c.type === 'pointer_declarator' || c.type === 'reference_declarator');
|
||||
let nextDeclarator = declarator.childForFieldName?.('declarator');
|
||||
if (!nextDeclarator) {
|
||||
for (let i = 0; i < declarator.childCount; i++) {
|
||||
const c = declarator.child(i);
|
||||
if (c?.type === 'function_declarator' || c?.type === 'pointer_declarator' || c?.type === 'reference_declarator') { nextDeclarator = c; break; }
|
||||
}
|
||||
}
|
||||
declarator = nextDeclarator;
|
||||
}
|
||||
if (declarator) {
|
||||
const innerDeclarator = declarator.childForFieldName?.('declarator') ||
|
||||
declarator.children?.find((c: any) =>
|
||||
c.type === 'qualified_identifier' || c.type === 'identifier' || c.type === 'parenthesized_declarator');
|
||||
let innerDeclarator = declarator.childForFieldName?.('declarator');
|
||||
if (!innerDeclarator) {
|
||||
for (let i = 0; i < declarator.childCount; i++) {
|
||||
const c = declarator.child(i);
|
||||
if (c?.type === 'qualified_identifier' || c?.type === 'identifier' || c?.type === 'parenthesized_declarator') { innerDeclarator = c; break; }
|
||||
}
|
||||
}
|
||||
|
||||
if (innerDeclarator?.type === 'qualified_identifier') {
|
||||
const nameNode = innerDeclarator.childForFieldName?.('name') ||
|
||||
innerDeclarator.children?.find((c: any) => c.type === 'identifier');
|
||||
let nameNode = innerDeclarator.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < innerDeclarator.childCount; i++) {
|
||||
const c = innerDeclarator.child(i);
|
||||
if (c?.type === 'identifier') { nameNode = c; break; }
|
||||
}
|
||||
}
|
||||
if (nameNode?.text) {
|
||||
funcName = nameNode.text;
|
||||
label = 'Method';
|
||||
@@ -388,11 +407,19 @@ export const extractFunctionName = (node: any): { funcName: string | null; label
|
||||
} else if (innerDeclarator?.type === 'identifier') {
|
||||
funcName = innerDeclarator.text;
|
||||
} else if (innerDeclarator?.type === 'parenthesized_declarator') {
|
||||
const nestedId = innerDeclarator.children?.find((c: any) =>
|
||||
c.type === 'qualified_identifier' || c.type === 'identifier');
|
||||
let nestedId: SyntaxNode | null = null;
|
||||
for (let i = 0; i < innerDeclarator.childCount; i++) {
|
||||
const c = innerDeclarator.child(i);
|
||||
if (c?.type === 'qualified_identifier' || c?.type === 'identifier') { nestedId = c; break; }
|
||||
}
|
||||
if (nestedId?.type === 'qualified_identifier') {
|
||||
const nameNode = nestedId.childForFieldName?.('name') ||
|
||||
nestedId.children?.find((c: any) => c.type === 'identifier');
|
||||
let nameNode = nestedId.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < nestedId.childCount; i++) {
|
||||
const c = nestedId.child(i);
|
||||
if (c?.type === 'identifier') { nameNode = c; break; }
|
||||
}
|
||||
}
|
||||
if (nameNode?.text) {
|
||||
funcName = nameNode.text;
|
||||
label = 'Method';
|
||||
@@ -405,38 +432,72 @@ export const extractFunctionName = (node: any): { funcName: string | null; label
|
||||
|
||||
// Fallback for other languages (Kotlin uses simple_identifier, Swift uses simple_identifier)
|
||||
if (!funcName) {
|
||||
const nameNode = node.childForFieldName?.('name') ||
|
||||
node.children?.find((c: any) => c.type === 'identifier' || c.type === 'property_identifier' || c.type === 'simple_identifier');
|
||||
let nameNode = node.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c?.type === 'identifier' || c?.type === 'property_identifier' || c?.type === 'simple_identifier') { nameNode = c; break; }
|
||||
}
|
||||
}
|
||||
funcName = nameNode?.text;
|
||||
}
|
||||
} else if (node.type === 'impl_item') {
|
||||
const funcItem = node.children?.find((c: any) => c.type === 'function_item');
|
||||
let funcItem: SyntaxNode | null = null;
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c?.type === 'function_item') { funcItem = c; break; }
|
||||
}
|
||||
if (funcItem) {
|
||||
const nameNode = funcItem.childForFieldName?.('name') ||
|
||||
funcItem.children?.find((c: any) => c.type === 'identifier');
|
||||
let nameNode = funcItem.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < funcItem.childCount; i++) {
|
||||
const c = funcItem.child(i);
|
||||
if (c?.type === 'identifier') { nameNode = c; break; }
|
||||
}
|
||||
}
|
||||
funcName = nameNode?.text;
|
||||
label = 'Method';
|
||||
}
|
||||
} else if (node.type === 'method_definition') {
|
||||
const nameNode = node.childForFieldName?.('name') ||
|
||||
node.children?.find((c: any) => c.type === 'property_identifier');
|
||||
let nameNode = node.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c?.type === 'property_identifier') { nameNode = c; break; }
|
||||
}
|
||||
}
|
||||
funcName = nameNode?.text;
|
||||
label = 'Method';
|
||||
} else if (node.type === 'method_declaration' || node.type === 'constructor_declaration') {
|
||||
const nameNode = node.childForFieldName?.('name') ||
|
||||
node.children?.find((c: any) => c.type === 'identifier');
|
||||
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;
|
||||
label = 'Method';
|
||||
} else if (node.type === 'arrow_function' || node.type === 'function_expression') {
|
||||
const parent = node.parent;
|
||||
if (parent?.type === 'variable_declarator') {
|
||||
const nameNode = parent.childForFieldName?.('name') ||
|
||||
parent.children?.find((c: any) => c.type === 'identifier');
|
||||
let nameNode = parent.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < parent.childCount; i++) {
|
||||
const c = parent.child(i);
|
||||
if (c?.type === 'identifier') { nameNode = c; break; }
|
||||
}
|
||||
}
|
||||
funcName = nameNode?.text;
|
||||
}
|
||||
} else if (node.type === 'method' || node.type === 'singleton_method') {
|
||||
const nameNode = node.childForFieldName?.('name') ||
|
||||
node.children?.find((c: any) => c.type === 'identifier');
|
||||
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;
|
||||
label = 'Method';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#include "service.h"
|
||||
#include "repo.h"
|
||||
|
||||
void processUser() {
|
||||
UserService svc;
|
||||
svc.getUser().save();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
class Repo {
|
||||
public:
|
||||
bool save() { return true; }
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
#include "user.h"
|
||||
|
||||
class UserService {
|
||||
public:
|
||||
User getUser() { return User(); }
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
#include "User.h"
|
||||
#include "Repo.h"
|
||||
#include <vector>
|
||||
|
||||
void processUsers(std::vector<User>* usersPtr) {
|
||||
for (auto& user : *usersPtr) {
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
|
||||
void processRepos(std::vector<Repo>* reposPtr) {
|
||||
for (const auto& repo : *reposPtr) {
|
||||
repo.save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
class Repo {
|
||||
public:
|
||||
Repo(const std::string& name) : name_(name) {}
|
||||
void save() {}
|
||||
private:
|
||||
std::string name_;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
class User {
|
||||
public:
|
||||
User(const std::string& name) : name_(name) {}
|
||||
void save() {}
|
||||
private:
|
||||
std::string name_;
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
#include "User.h"
|
||||
#include "Repo.h"
|
||||
#include <vector>
|
||||
|
||||
void processUsers(const std::vector<User>& users) {
|
||||
for (auto& user : users) {
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
|
||||
void processRepos(const std::vector<Repo>& repos) {
|
||||
for (const auto& repo : repos) {
|
||||
repo.save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
class Repo {
|
||||
public:
|
||||
Repo(const std::string& name) : name_(name) {}
|
||||
void save() {}
|
||||
private:
|
||||
std::string name_;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
class User {
|
||||
public:
|
||||
User(const std::string& name) : name_(name) {}
|
||||
void save() {}
|
||||
private:
|
||||
std::string name_;
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
#include "user.h"
|
||||
#include <vector>
|
||||
|
||||
void processUsers(std::vector<User> users) {
|
||||
for (User& user : users) {
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#include "User.h"
|
||||
#include "Repo.h"
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
void processUserMap(std::map<std::string, User> userMap) {
|
||||
for (auto& [key, user] : userMap) {
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
|
||||
void processRepoMap(std::map<std::string, Repo> repoMap) {
|
||||
for (const auto& [key, repo] : repoMap) {
|
||||
repo.save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
class Repo {
|
||||
public:
|
||||
Repo(const std::string& name) : name_(name) {}
|
||||
void save() {}
|
||||
private:
|
||||
std::string name_;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
class User {
|
||||
public:
|
||||
User(const std::string& name) : name_(name) {}
|
||||
void save() {}
|
||||
private:
|
||||
std::string name_;
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ChainCall.Models;
|
||||
|
||||
public class Repo
|
||||
{
|
||||
public bool Save()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ChainCall.Models;
|
||||
|
||||
public class User
|
||||
{
|
||||
public bool Save()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using ChainCall.Services;
|
||||
|
||||
public class App
|
||||
{
|
||||
public void ProcessUser()
|
||||
{
|
||||
var svc = new UserService();
|
||||
svc.GetUser().Save();
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
using ChainCall.Models;
|
||||
|
||||
namespace ChainCall.Services;
|
||||
|
||||
public class UserService
|
||||
{
|
||||
public User GetUser()
|
||||
{
|
||||
return new User();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class App {
|
||||
public void ProcessValues(Dictionary<string, User> data) {
|
||||
foreach (var user in data.Values) {
|
||||
user.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessList(List<User> users) {
|
||||
foreach (var user in users) {
|
||||
user.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
public class Repo {
|
||||
public string Name { get; set; }
|
||||
public void Save() {}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
public class User {
|
||||
public string Name { get; set; }
|
||||
public void Save() {}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class App {
|
||||
private Dictionary<string, User> data;
|
||||
|
||||
public void ProcessValues() {
|
||||
foreach (var user in this.data.Values) {
|
||||
user.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessKeys() {
|
||||
foreach (var key in this.data.Keys) {
|
||||
key.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
public class Repo {
|
||||
public string Name { get; set; }
|
||||
public void Save() {}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
public class User {
|
||||
public string Name { get; set; }
|
||||
public void Save() {}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
namespace Models;
|
||||
|
||||
public class Repo
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public bool Save() { return true; }
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
namespace Models;
|
||||
|
||||
public class User
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public bool Save() { return true; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Models;
|
||||
|
||||
namespace App;
|
||||
|
||||
public class AppService
|
||||
{
|
||||
public void ProcessWithRecursivePattern(object obj)
|
||||
{
|
||||
if (obj is User { Name: "Alice" } u)
|
||||
{
|
||||
u.Save();
|
||||
}
|
||||
|
||||
var result = obj switch
|
||||
{
|
||||
Repo { Name: "main" } r => r.Save(),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Models;
|
||||
|
||||
public class Repo
|
||||
{
|
||||
public bool Save() { return false; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Models;
|
||||
|
||||
public class User
|
||||
{
|
||||
public bool Save() { return true; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Models;
|
||||
|
||||
namespace App;
|
||||
|
||||
public class AppService
|
||||
{
|
||||
public void Process(object obj)
|
||||
{
|
||||
if (obj is User user)
|
||||
{
|
||||
user.Save();
|
||||
}
|
||||
|
||||
switch (obj)
|
||||
{
|
||||
case Repo repo:
|
||||
repo.Save();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Models;
|
||||
|
||||
public class Repo
|
||||
{
|
||||
public bool Save() { return false; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Models;
|
||||
|
||||
public class User
|
||||
{
|
||||
public bool Save() { return true; }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Models;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace App;
|
||||
|
||||
public class AppService
|
||||
{
|
||||
public void ProcessUsers(List<User> users)
|
||||
{
|
||||
foreach (var user in users)
|
||||
{
|
||||
user.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessRepos(List<Repo> repos)
|
||||
{
|
||||
foreach (var repo in repos)
|
||||
{
|
||||
repo.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public void Direct(User u, Repo r)
|
||||
{
|
||||
u.Save();
|
||||
r.Save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,14 @@
|
||||
package main
|
||||
|
||||
import "example.com/chaincall/models"
|
||||
|
||||
type UserService struct{}
|
||||
|
||||
func (s *UserService) GetUser() *models.User {
|
||||
return &models.User{Name: "alice"}
|
||||
}
|
||||
|
||||
func processUser() {
|
||||
svc := &UserService{}
|
||||
svc.GetUser().Save()
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module example.com/chaincall
|
||||
|
||||
go 1.21
|
||||
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
type Repo struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (r *Repo) Save() bool {
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (u *User) Save() bool {
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
import "models"
|
||||
|
||||
func processMap(userMap map[string]models.User) {
|
||||
for _, user := range userMap {
|
||||
user.Save()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package models
|
||||
|
||||
type Repo struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func (r Repo) Save() {}
|
||||
@@ -0,0 +1,7 @@
|
||||
package models
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (u User) Save() {}
|
||||
@@ -0,0 +1,18 @@
|
||||
package src;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
public class App {
|
||||
public void processValues(Map<String, User> data) {
|
||||
for (var user : data.values()) {
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
|
||||
public void processList(List<User> users) {
|
||||
for (var user : users) {
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package src;
|
||||
|
||||
public class Repo {
|
||||
private String name;
|
||||
public Repo(String name) { this.name = name; }
|
||||
public void save() {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package src;
|
||||
|
||||
public class User {
|
||||
private String name;
|
||||
public User(String name) { this.name = name; }
|
||||
public void save() {}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import models.User;
|
||||
import models.Repo;
|
||||
|
||||
public class App {
|
||||
public static void processAny(Object obj) {
|
||||
switch (obj) {
|
||||
case User user -> user.save();
|
||||
case Repo repo -> repo.save();
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
|
||||
public static void handleUser(Object obj) {
|
||||
switch (obj) {
|
||||
case User user -> user.save();
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package models;
|
||||
|
||||
public class Repo {
|
||||
public void save() {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package models;
|
||||
|
||||
public class User {
|
||||
public void save() {}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
const { UserService } = require('./service');
|
||||
|
||||
function processUser() {
|
||||
const svc = new UserService();
|
||||
svc.getUser().save();
|
||||
}
|
||||
|
||||
module.exports = { processUser };
|
||||
@@ -0,0 +1,7 @@
|
||||
class Repo {
|
||||
save() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Repo };
|
||||
@@ -0,0 +1,12 @@
|
||||
const { User } = require('./user');
|
||||
|
||||
class UserService {
|
||||
/**
|
||||
* @returns {User}
|
||||
*/
|
||||
getUser() {
|
||||
return new User();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UserService };
|
||||
@@ -0,0 +1,7 @@
|
||||
class User {
|
||||
save() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { User };
|
||||
@@ -0,0 +1,29 @@
|
||||
fun processValues(data: HashMap<String, User>) {
|
||||
for (user in data.values) {
|
||||
user.save()
|
||||
}
|
||||
}
|
||||
|
||||
fun processKeys(data: HashMap<User, Repo>) {
|
||||
for (user in data.keys) {
|
||||
user.save()
|
||||
}
|
||||
}
|
||||
|
||||
fun processMutableMapValues(data: MutableMap<String, Repo>) {
|
||||
for (repo in data.values) {
|
||||
repo.save()
|
||||
}
|
||||
}
|
||||
|
||||
fun processList(users: List<User>) {
|
||||
for (user in users) {
|
||||
user.save()
|
||||
}
|
||||
}
|
||||
|
||||
fun processSet(repos: Set<Repo>) {
|
||||
for (repo in repos) {
|
||||
repo.save()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
class Repo(val name: String) {
|
||||
fun save() {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
class User(val name: String) {
|
||||
fun save() {}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package app
|
||||
|
||||
import models.User
|
||||
import models.Repo
|
||||
|
||||
fun processUsers(users: List<User>) {
|
||||
for (user in users) {
|
||||
user.save()
|
||||
}
|
||||
}
|
||||
|
||||
fun processRepos(repos: List<Repo>) {
|
||||
for (repo in repos) {
|
||||
repo.save()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package models
|
||||
|
||||
class Repo {
|
||||
fun save() {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package models
|
||||
|
||||
class User {
|
||||
fun save() {}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import models.User
|
||||
import models.Repo
|
||||
import models.Admin
|
||||
|
||||
// Three-arm when: each arm should resolve obj to its narrowed type
|
||||
fun processThreeArms(obj: Any) {
|
||||
when (obj) {
|
||||
is User -> obj.save()
|
||||
is Repo -> obj.save()
|
||||
is Admin -> obj.save()
|
||||
}
|
||||
}
|
||||
|
||||
// Multiple method calls within a single when arm
|
||||
fun processMultiCall(obj: Any) {
|
||||
when (obj) {
|
||||
is User -> {
|
||||
obj.validate()
|
||||
obj.save()
|
||||
}
|
||||
is Repo -> {
|
||||
obj.validate()
|
||||
obj.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// when with else branch — else should NOT narrow the type
|
||||
fun processWithElse(obj: Any) {
|
||||
when (obj) {
|
||||
is User -> obj.save()
|
||||
else -> println(obj)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package models
|
||||
|
||||
class Admin {
|
||||
fun save() {}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package models
|
||||
|
||||
class Repo {
|
||||
fun save() {}
|
||||
fun validate() {}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package models
|
||||
|
||||
class User {
|
||||
fun save() {}
|
||||
fun validate() {}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import models.User
|
||||
import models.Repo
|
||||
|
||||
fun processAny(obj: Any) {
|
||||
when (obj) {
|
||||
is User -> obj.save()
|
||||
is Repo -> obj.save()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleUser(obj: Any) {
|
||||
when (obj) {
|
||||
is User -> obj.save()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package models
|
||||
|
||||
class Repo {
|
||||
fun save() {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package models
|
||||
|
||||
class User {
|
||||
fun save() {}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
require_once 'User.php';
|
||||
require_once 'Repo.php';
|
||||
|
||||
class App {
|
||||
/**
|
||||
* PHPDoc generic Collection<User> — element type should resolve to User, not Collection.
|
||||
* @param Collection<User> $users
|
||||
*/
|
||||
public function processCollection($users): void {
|
||||
foreach ($users as $user) {
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PHPDoc array-style User[] — existing behavior, should still work.
|
||||
* @param User[] $repos
|
||||
*/
|
||||
public function processArray(array $repos): void {
|
||||
foreach ($repos as $repo) {
|
||||
$repo->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
class Repo {
|
||||
public string $name;
|
||||
|
||||
public function __construct(string $name) {
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function save(): void {}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
class User {
|
||||
public string $name;
|
||||
|
||||
public function __construct(string $name) {
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function save(): void {}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
require_once 'User.php';
|
||||
|
||||
class App {
|
||||
/** @param User[] $users */
|
||||
public function processUsers(array $users): void {
|
||||
foreach ($users as $user) {
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
class Repo {
|
||||
public string $name;
|
||||
|
||||
public function __construct(string $name) {
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function save(): void {}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
class User {
|
||||
public string $name;
|
||||
|
||||
public function __construct(string $name) {
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function save(): void {}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
require_once 'User.php';
|
||||
require_once 'Repo.php';
|
||||
|
||||
class App {
|
||||
/** @var User[] */
|
||||
private array $users;
|
||||
|
||||
public function __construct() {
|
||||
$this->users = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* $this->users member access in foreach — iterableName must use $ prefix
|
||||
* to match how property_declaration stores the variable in scopeEnv ($users).
|
||||
*
|
||||
* Uses a typed parameter to ensure the type is in the method's scopeEnv,
|
||||
* since class property @var types are stored at file scope (not method scope).
|
||||
*
|
||||
* @param User[] $users
|
||||
*/
|
||||
public function processMembers(array $users): void {
|
||||
foreach ($this->users as $user) {
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
class Repo {
|
||||
public string $name;
|
||||
|
||||
public function __construct(string $name) {
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function save(): void {}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
class User {
|
||||
public string $name;
|
||||
|
||||
public function __construct(string $name) {
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function save(): void {}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
from service import UserService
|
||||
|
||||
|
||||
def process_user():
|
||||
svc = UserService()
|
||||
svc.get_user().save()
|
||||
@@ -0,0 +1,3 @@
|
||||
class Repo:
|
||||
def save(self):
|
||||
pass
|
||||
@@ -0,0 +1,3 @@
|
||||
class User:
|
||||
def save(self):
|
||||
pass
|
||||
@@ -0,0 +1,6 @@
|
||||
from models.user import User
|
||||
|
||||
|
||||
class UserService:
|
||||
def get_user(self) -> User:
|
||||
return User()
|
||||
@@ -0,0 +1,5 @@
|
||||
from user import User
|
||||
|
||||
def process(data: dict[str, User]):
|
||||
for key, user in data.items():
|
||||
user.save()
|
||||
@@ -0,0 +1,6 @@
|
||||
class Repo:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
def save(self):
|
||||
pass
|
||||
@@ -0,0 +1,6 @@
|
||||
class User:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
def save(self):
|
||||
pass
|
||||
@@ -0,0 +1,8 @@
|
||||
from models.user import User
|
||||
from models.repo import Repo
|
||||
|
||||
|
||||
def process(x):
|
||||
match x:
|
||||
case User() as u:
|
||||
u.save() # should resolve to User#save, not Repo#save
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user