|
|
@@ -16,7 +16,7 @@ import {
|
|
|
FrameworkResolver,
|
|
|
ImportMapping,
|
|
|
} from './types';
|
|
|
-import { matchReference, sameLanguageFamily, crossesKnownFamily } from './name-matcher';
|
|
|
+import { matchReference, matchDottedCallChain, sameLanguageFamily, crossesKnownFamily } from './name-matcher';
|
|
|
import { resolveViaImport, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef } from './import-resolver';
|
|
|
import { detectFrameworks } from './frameworks';
|
|
|
import { synthesizeCallbackEdges } from './callback-synthesizer';
|
|
|
@@ -27,6 +27,17 @@ import { logDebug } from '../errors';
|
|
|
import type { ReExport } from './types';
|
|
|
import { LRUCache } from './lru-cache';
|
|
|
|
|
|
+/** Node kinds that can declare supertypes (extends/implements). */
|
|
|
+const SUPERTYPE_BEARING_KINDS = new Set<Node['kind']>([
|
|
|
+ 'class', 'struct', 'interface', 'trait', 'protocol', 'enum',
|
|
|
+]);
|
|
|
+
|
|
|
+/** Languages whose chained calls use the dotted `inner().method` encoding. */
|
|
|
+const DOT_CHAIN_LANGUAGES = new Set(['java', 'kotlin', 'csharp']);
|
|
|
+
|
|
|
+/** The extractor's chained-receiver encoding: `<inner>().<method>`. */
|
|
|
+const CHAIN_SHAPE = /^(.+)\(\)\.(\w+)$/;
|
|
|
+
|
|
|
/**
|
|
|
* Cache size limits. Each per-resolver cache is bounded so memory
|
|
|
* stays flat on large codebases (20k+ files). Sizes were chosen to
|
|
|
@@ -185,6 +196,12 @@ export class ReferenceResolver {
|
|
|
private queries: QueryBuilder;
|
|
|
private context: ResolutionContext;
|
|
|
private frameworks: FrameworkResolver[] = [];
|
|
|
+ // Chained static-factory/fluent call refs the first pass couldn't resolve,
|
|
|
+ // collected in-memory (the batched resolver deletes unresolved refs from the
|
|
|
+ // DB, so they can't be re-read). Drained by resolveChainedCallsViaConformance
|
|
|
+ // once implements/extends edges exist, to resolve methods on a supertype the
|
|
|
+ // receiver conforms to (#750).
|
|
|
+ private deferredChainRefs: UnresolvedRef[] = [];
|
|
|
// Per-`.razor`/`.cshtml`-file `@using` namespace set (own directives + folder
|
|
|
// `_Imports.razor`, cascading to the project root). Used to disambiguate a
|
|
|
// markup type ref to the right C# namespace.
|
|
|
@@ -400,6 +417,25 @@ export class ReferenceResolver {
|
|
|
return result;
|
|
|
},
|
|
|
|
|
|
+ getSupertypes: (typeName: string, language) => {
|
|
|
+ // Union the `implements`/`extends` targets of every same-named type node.
|
|
|
+ // Matching by simple name (not id) reconciles a type declared in one node
|
|
|
+ // (`KF::Builder`) with conformance declared in a separate extension node
|
|
|
+ // (`KF.Builder: KFOptionSetter`) — both have name `Builder`.
|
|
|
+ const typeNodes = this.context
|
|
|
+ .getNodesByName(typeName)
|
|
|
+ .filter((n) => SUPERTYPE_BEARING_KINDS.has(n.kind) && n.language === language);
|
|
|
+ if (typeNodes.length === 0) return [];
|
|
|
+ const supertypes = new Set<string>();
|
|
|
+ for (const tn of typeNodes) {
|
|
|
+ for (const edge of this.queries.getOutgoingEdges(tn.id, ['implements', 'extends'])) {
|
|
|
+ const target = this.queries.getNodeById(edge.target);
|
|
|
+ if (target?.name && target.name !== typeName) supertypes.add(target.name);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return [...supertypes];
|
|
|
+ },
|
|
|
+
|
|
|
getImportMappings: (filePath: string, language) => {
|
|
|
const cacheKey = filePath;
|
|
|
const cached = this.importMappingCache.get(cacheKey);
|
|
|
@@ -684,7 +720,19 @@ export class ReferenceResolver {
|
|
|
candidates.push(nameResult);
|
|
|
}
|
|
|
|
|
|
- if (candidates.length === 0) return null;
|
|
|
+ if (candidates.length === 0) {
|
|
|
+ // Defer a chained static-factory/fluent call the first pass couldn't
|
|
|
+ // resolve — its method may live on a supertype the receiver conforms to,
|
|
|
+ // resolvable once implements/extends edges exist (the conformance pass).
|
|
|
+ if (
|
|
|
+ ref.referenceKind === 'calls' &&
|
|
|
+ DOT_CHAIN_LANGUAGES.has(ref.language) &&
|
|
|
+ CHAIN_SHAPE.test(ref.referenceName)
|
|
|
+ ) {
|
|
|
+ this.deferredChainRefs.push(ref);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
|
|
|
// Return highest confidence candidate
|
|
|
return candidates.reduce((best, curr) =>
|
|
|
@@ -767,6 +815,43 @@ export class ReferenceResolver {
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Second resolution pass for chained static-factory / fluent calls whose
|
|
|
+ * chained method is defined on a SUPERTYPE the receiver's type conforms to —
|
|
|
+ * a protocol-extension / inherited / default-interface method (#750). The
|
|
|
+ * first pass can't resolve these because `implements`/`extends` edges aren't
|
|
|
+ * built yet; this runs AFTER edges are persisted, so `context.getSupertypes`
|
|
|
+ * (and the conformance fallback in resolveMethodOnType) can walk them.
|
|
|
+ *
|
|
|
+ * Operates only on the leftover unresolved refs that have the `inner().method`
|
|
|
+ * chain shape, for the dotted-chain languages — a small set — and is idempotent
|
|
|
+ * (re-resolving an already-resolved ref is a no-op since it's been deleted).
|
|
|
+ * Returns the number of newly-created edges.
|
|
|
+ */
|
|
|
+ resolveChainedCallsViaConformance(): number {
|
|
|
+ const deferred = this.deferredChainRefs;
|
|
|
+ this.deferredChainRefs = [];
|
|
|
+ if (deferred.length === 0) return 0;
|
|
|
+
|
|
|
+ // Read fresh edges (the main pass built the implements/extends edges after
|
|
|
+ // these refs were deferred). matchDottedCallChain now resolves a method on a
|
|
|
+ // supertype via context.getSupertypes -> resolveMethodOnType's conformance walk.
|
|
|
+ this.clearCaches();
|
|
|
+ const resolved: ResolvedRef[] = [];
|
|
|
+ for (const ref of deferred) {
|
|
|
+ const match = this.gateLanguage(matchDottedCallChain(ref, this.context), ref);
|
|
|
+ if (match) resolved.push(match);
|
|
|
+ }
|
|
|
+ if (resolved.length === 0) return 0;
|
|
|
+
|
|
|
+ const edges = this.createEdges(resolved);
|
|
|
+ if (edges.length > 0) {
|
|
|
+ this.queries.insertEdges(edges);
|
|
|
+ this.clearCaches();
|
|
|
+ }
|
|
|
+ return edges.length;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Resolve and persist in batches to keep memory bounded.
|
|
|
* Processes unresolved references in chunks, persisting edges and cleaning
|