fix(resolution): a type never inherits from itself (#2229)

An extends/implements ref that settled on the declaring type is re-chosen
among the other types of its name, the one the written qualifier leads to
(owner path plus file package), or none.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-01 01:46:07 +00:00
committed by GitHub
parent 92f141f04f
commit b281819854
3 changed files with 104 additions and 0 deletions
+30
View File
@@ -8566,6 +8566,10 @@ export function matchReference(
// selected = useStore(s => s.reset); selected()` is the store's `reset`.)
if (target && target.name === ref.referenceName && isOutsideJsLocal(target, ref, context)) return null;
}
// A type never inherits from itself: cats' `trait BigDecimalInstances extends
// cats.kernel.instances.BigDecimalInstances` and `trait AllOps … with
// Bifoldable.AllOps` name another type of their own name.
if (result && result.targetNodeId === ref.fromNodeId && isInheritanceRef(ref)) return otherSupertypeNamed(ref, context);
// Nor does a value's initializer call the value: sttp's `val response =
// basicRequest.get(…).response(asStringAlways)` is a request's `response`.
if (result && result.targetNodeId === ref.fromNodeId && ref.referenceKind === 'calls' &&
@@ -8573,6 +8577,32 @@ export function matchReference(
return result ? retargetSelfOverload(result, ref, context) : result;
}
/**
* The supertype an inheritance ref names when the name is the declaring
* type's own: another type of that name — the one the written qualifier
* (`cats.kernel.instances.`, `Bifoldable.`) leads to, by its owner and its
* file's package. Null unless exactly one fits.
*/
function otherSupertypeNamed(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
const name = ref.referenceName.split(/::|\./).pop()!;
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split(/\r?\n/)[ref.line - 1] ?? '';
const written = new RegExp(`^\\s*((?:[\\w$]+\\.)+)${name.replace(/\$/g, '\\$')}\\b`).exec(line.slice(Math.max(0, ref.column)));
const qualifier = written ? written[1]!.slice(0, -1) : '';
const candidates = context.getNodesByName(name).filter((n) =>
n.id !== ref.fromNodeId && isSupertypeTarget(n) && sameLanguageFamily(n.language, ref.language) &&
(qualifier === '' || ownerPathOf(n, context) === qualifier || ownerPathOf(n, context).endsWith(`.${qualifier}`)));
return candidates.length === 1 ? { original: ref, targetNodeId: candidates[0]!.id, confidence: 0.8, resolvedBy: 'qualified-name' } : null;
}
/** A declaration's dotted owner path — its file's package clauses, then its enclosing types (`cats.kernel.instances`, `cats.Bifoldable`). */
function ownerPathOf(n: Node, context: ResolutionContext): string {
const text = context.readFile(n.filePath) ?? '';
const pkg = [...text.matchAll(/^\s*package\s+([\w.]+)\s*;?\s*$/gm)].map((m) => m[1]!).join('.');
const cut = n.qualifiedName.lastIndexOf('::');
const owners = cut > 0 ? n.qualifiedName.slice(0, cut).replace(/::/g, '.') : '';
return [pkg, owners].filter((p) => p !== '' && !(pkg !== '' && p === owners && owners.startsWith(pkg))).join('.');
}
/** Reference kinds a bare JS/TS local can be: a call, a value, a construction. */
const JS_LOCAL_REF_KINDS: ReadonlySet<string> = new Set(['calls', 'references', 'function_ref', 'instantiates']);