Skip to content

Commit 6122e9d

Browse files
committed
fix(bridge): self-referential contextual property leaks into definitions (issue #5)
definitionAndBoundSpan on a property assignment inside a contextually typed object literal (props options, defineProps type literals) returned the local assignment AND the contextual member; stock returns only the member. 19 of the 38 self-site extra-definition keys in the sim-nav baseline converged; the rest need a tsgo-side getContextualType fix (next comment on the issue). Three-link chain, all in the def path: - The checker shim's getContextualType dropped contextFlags entirely. tsgo's RPC takes no flags and its union can include the queried node's own inferred type, so getDefinitionFromObjectLiteralElement's IgnoreNodeInferences re-query returned the same self-containing union. Emulate the flag by dropping union constituents whose declarations all live inside the queried node (handles host nodes and tsgo NodeHandle declarations), with getNonNullableType overridden on the filtered delegate so stock's getPropertySymbolsFromContextualType can't reach back to the unfiltered original. - Stock's self-referential guard (p.valueDeclaration.name === node) never fires for tsgo-side property symbols: their declarations are NodeHandles, so .name identity across ASTs is impossible. Compare name spans after resolving the handle instead. Verified: options-api/component.ts:32:4 and component-name-description/component.vue:12:4 now return exactly stock's definition set; A-family batch 19/19 of the .ts + component-meta .vue keys match. Gates: sim-nav 37,409 units diff 1175 -> 1156 (fixed=19, new=0), sim-xfile 71/71, sim-edit THROWS 0, f2hl/f2r6/emptyparity/refs-exportspec/display-tokens/ computed-literal PASS, check:lib/enums/sourcefile-guard ok, volar suite 205/205.
1 parent 31026f3 commit 6122e9d

2 files changed

Lines changed: 160 additions & 2 deletions

File tree

‎patches/typescript/0001-tsgo-hooks.patch‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2043,6 +2043,65 @@ index dfd00ce2e9..a6b14ba3d9 100644
20432043
}
20442044
catch (err) {
20452045
// Ensure cache is reset if operation is cancelled
2046+
diff --git a/src/services/goToDefinition.ts b/src/services/goToDefinition.ts
2047+
index 0a45cc3e3e..d296a01666 100644
2048+
--- a/src/services/goToDefinition.ts
2049+
+++ b/src/services/goToDefinition.ts
2050+
@@ -316,7 +316,7 @@ function getDefinitionFromObjectLiteralElement(typeChecker: TypeChecker, node: N
2051+
const contextualType = element && typeChecker.getContextualType(element.parent);
2052+
if (contextualType) {
2053+
let properties = getPropertySymbolsFromContextualType(element, typeChecker, contextualType, /*unionSymbolOk*/ false);
2054+
- if (some(properties, p => !!(p.valueDeclaration && isObjectLiteralExpression(p.valueDeclaration.parent) && isObjectLiteralElementLike(p.valueDeclaration) && p.valueDeclaration.name === node))) {
2055+
+ if (some(properties, p => isSelfReferentialContextualProperty(p, node))) {
2056+
const withoutNodeInferencesType = typeChecker.getContextualType(element.parent, ContextFlags.IgnoreNodeInferences);
2057+
if (withoutNodeInferencesType) {
2058+
const withoutNodeInferencesProperties = getPropertySymbolsFromContextualType(element, typeChecker, withoutNodeInferencesType, /*unionSymbolOk*/ false);
2059+
@@ -331,6 +331,45 @@ function getDefinitionFromObjectLiteralElement(typeChecker: TypeChecker, node: N
2060+
return emptyArray;
2061+
}
2062+
2063+
+/**
2064+
+ * TNB (issue #5): the stock form of this test is
2065+
+ * `p.valueDeclaration && isObjectLiteralExpression(p.valueDeclaration.parent)
2066+
+ * && isObjectLiteralElementLike(p.valueDeclaration) && p.valueDeclaration.name === node`.
2067+
+ * tsgo-side property symbols carry declarations as NodeHandles — no `.parent`
2068+
+ * or `.name` links — so the identity test never detects the object literal's
2069+
+ * own properties as self-referential and the IgnoreNodeInferences re-query
2070+
+ * never runs (the self declaration then leaks into the definition list).
2071+
+ * Keep the stock test as the fast path; for handle-shaped declarations,
2072+
+ * resolve and compare name positions across ASTs.
2073+
+ */
2074+
+function isSelfReferentialContextualProperty(p: Symbol, node: Node): boolean {
2075+
+ const valueDeclaration = (p.valueDeclaration ?? p.declarations?.[0]) as Declaration | undefined;
2076+
+ if (!valueDeclaration) return false;
2077+
+ if (
2078+
+ isObjectLiteralExpression(valueDeclaration.parent)
2079+
+ && isObjectLiteralElementLike(valueDeclaration)
2080+
+ && (valueDeclaration as any).name === node
2081+
+ ) {
2082+
+ return true;
2083+
+ }
2084+
+ const anyDecl = valueDeclaration as any;
2085+
+ // Cross-AST path: tsgo-side declarations (NodeHandles) can never be === the
2086+
+ // host node, so the identity test above failed even though the declaration
2087+
+ // IS the element's own name. Resolve the handle and compare name positions.
2088+
+ if (typeof anyDecl.resolve !== "function") return false;
2089+
+ let rn: any;
2090+
+ try { rn = anyDecl._resolvedNode ?? anyDecl.resolve(); } catch { rn = undefined; }
2091+
+ if (!rn || !isObjectLiteralExpression(rn.parent) || !isObjectLiteralElementLike(rn)) return false;
2092+
+ const rnName = rn.name;
2093+
+ if (!rnName) return false;
2094+
+ const rnSf = typeof rn.getSourceFile === "function" ? rn.getSourceFile() : (rn as any).sourceFile;
2095+
+ const nodeSf = node.getSourceFile();
2096+
+ if (!rnSf || rnSf.fileName !== nodeSf.fileName) return false;
2097+
+ const rnStart = typeof rnName.getStart === "function" ? rnName.getStart() : rnName.pos;
2098+
+ const rnEnd = typeof rnName.getEnd === "function" ? rnName.getEnd() : rnName.end;
2099+
+ return rnStart === node.getStart() && rnEnd === node.getEnd();
2100+
+}
2101+
+
2102+
function getDefinitionFromOverriddenMember(typeChecker: TypeChecker, node: Node) {
2103+
const classElement = findAncestor(node, isClassElement);
2104+
if (!(classElement && classElement.name)) return;
20462105
diff --git a/src/services/services.ts b/src/services/services.ts
20472106
index 9885093ed6..0cbb4021a3 100644
20482107
--- a/src/services/services.ts

‎patches/typescript/overlay/src/compiler/tsgoChecker.ts‎

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import * as ts from "./_namespaces/ts.js";
1818
import { bindSourceFile } from "./binder.js";
1919
import { createSourceFile } from "./parser.js";
20-
import { SyntaxKind, SymbolFlags, SymbolFormatFlags, NodeFlags, ModifierFlags, JSDocParsingMode, ModuleKind, StructureIsReused, EmitHint, EmitFlags, type Path, type Program, type TypeChecker } from "./types.js";
20+
import { SyntaxKind, SymbolFlags, SymbolFormatFlags, NodeFlags, ModifierFlags, JSDocParsingMode, ModuleKind, StructureIsReused, EmitHint, EmitFlags, ContextFlags, type Path, type Program, type TypeChecker } from "./types.js";
2121
import { getBuildInfoText, getTsBuildInfoEmitOutputFilePath, createPrinterWithRemoveComments } from "./emitter.js";
2222
import { usingSingleLineStringWriter } from "./utilities.js";
2323
import { getParseTreeNode, isFunctionLike } from "./utilitiesPublic.js";
@@ -7649,6 +7649,94 @@ export function createTsgoChecker(program: any): any {
76497649
}
76507650
}
76517651

7652+
/**
7653+
* ContextFlags.IgnoreNodeInferences emulation: drop union constituents that
7654+
* are the queried node's own inferred type (their declarations all live
7655+
* inside the node's span). See getContextualType in the checker shim.
7656+
*/
7657+
function filterNodeInferredConstituents(t: any, node: any): any {
7658+
if (!t?.isUnion?.()) return t;
7659+
const members: any[] = t.types ?? [];
7660+
const kept = members.filter(m => !typeDeclaredWithin(m, node));
7661+
if (_traceSymEnabled) {
7662+
traceSym(`filterNodeInferred file=${node.getSourceFile?.()?.fileName} nodePos=${node.pos} members=${members.length} kept=${kept.length} within=[${members.map(m => typeDeclaredWithin(m, node) ? 1 : 0).join("")}]`);
7663+
}
7664+
if (kept.length === members.length) return t;
7665+
if (kept.length === 0) return undefined;
7666+
return unionDelegate(t, kept);
7667+
}
7668+
7669+
/** Union view over a filtered member list; delegates everything else to the original. */
7670+
function unionDelegate(original: any, members: any[]): any {
7671+
if (members.length === 1) return members[0];
7672+
const nonNullable = members.filter(m => !isNullableTypeShim(m));
7673+
// getNonNullableType must be overridden too: stock consumers
7674+
// (getPropertySymbolsFromContextualType) call it first, and plain
7675+
// delegation would hand back the UNfiltered original union.
7676+
const delegate = Object.create(original, {
7677+
isUnion: { value: () => true },
7678+
types: { value: members },
7679+
getNonNullableType: {
7680+
value: () => nonNullable.length === members.length ? delegate
7681+
: nonNullable.length === 0 ? undefined
7682+
: unionDelegate(original, nonNullable),
7683+
},
7684+
});
7685+
return delegate;
7686+
}
7687+
7688+
function isNullableTypeShim(m: any): boolean {
7689+
if (typeof m?.isNullableType === "function") {
7690+
try { if (m.isNullableType()) return true; } catch { /* fall through */ }
7691+
}
7692+
return m?.intrinsicName === "null" || m?.intrinsicName === "undefined";
7693+
}
7694+
7695+
function typeDeclaredWithin(t: any, node: any): boolean {
7696+
const sym = t.symbol ?? (typeof t.getSymbol === "function" ? (() => { try { return t.getSymbol(); } catch { return undefined; } })() : undefined);
7697+
const decls = sym?.declarations ?? [];
7698+
if (!decls.length) return false;
7699+
return decls.every((d: any) => nodeContainsPosition(node, d));
7700+
}
7701+
7702+
function declFileName(d: any): string | undefined {
7703+
// Host nodes expose getSourceFile(); tsgo checker symbol declarations
7704+
// arrive as NodeHandles with a `sourceFile` getter instead.
7705+
if (typeof d.getSourceFile === "function") {
7706+
try { return d.getSourceFile()?.fileName; } catch { /* fall through */ }
7707+
}
7708+
return d.sourceFile?.fileName;
7709+
}
7710+
7711+
function declSpan(d: any): { start: number; end: number; } | undefined {
7712+
if (typeof d.getStart === "function" && typeof d.getEnd === "function") {
7713+
return { start: d.getStart(), end: d.getEnd() };
7714+
}
7715+
// NodeHandle: resolve to the RemoteNode for positions.
7716+
if (typeof d.resolve === "function") {
7717+
let rn: any;
7718+
try { rn = d.resolve(); } catch { rn = undefined; }
7719+
if (rn) {
7720+
if (typeof rn.getStart === "function" && typeof rn.getEnd === "function") {
7721+
return { start: rn.getStart(), end: rn.getEnd() };
7722+
}
7723+
if (typeof rn.pos === "number" && typeof rn.end === "number") {
7724+
return { start: rn.pos, end: rn.end };
7725+
}
7726+
}
7727+
}
7728+
return undefined;
7729+
}
7730+
7731+
function nodeContainsPosition(node: any, d: any): boolean {
7732+
const sf = node.getSourceFile?.();
7733+
const dFile = declFileName(d);
7734+
if (!sf || !dFile || sf.fileName !== dFile) return false;
7735+
const span = declSpan(d);
7736+
if (!span || span.start < 0) return false;
7737+
return span.start >= node.pos && span.end <= node.end;
7738+
}
7739+
76527740
function fixupType(t: any): any {
76537741
if (Array.isArray(t)) { for (const i of t) fixupType(i); return t; }
76547742
if (t && typeof t === "object") {
@@ -7669,6 +7757,7 @@ export function createTsgoChecker(program: any): any {
76697757
}
76707758
resolveRawTypeProps(obj);
76717759
// tsgo may expose `aliasTypeArguments: []` on reference/array
7760+
76727761
// types. Rule helpers (no-unnecessary-type-assertion's
76737762
// containsAny) use `type.aliasTypeArguments ??
76747763
// checker.getTypeArguments(type)` — an empty array is
@@ -8524,7 +8613,7 @@ export function createTsgoChecker(program: any): any {
85248613
}
85258614
return errorOrAny();
85268615
},
8527-
getContextualType(node: any): any {
8616+
getContextualType(node: any, contextFlags?: number): any {
85288617
ensureProject();
85298618
const sf = node.getSourceFile?.();
85308619
if (!sf) return undefined;
@@ -8553,6 +8642,16 @@ export function createTsgoChecker(program: any): any {
85538642
_objCompletionPending = { hostNode: node, tsgoNode, results: [t] };
85548643
}
85558644
}
8645+
// ContextFlags.IgnoreNodeInferences: stock re-queries the contextual
8646+
// type excluding inference derived from the queried node itself
8647+
// (goToDefinition's object-literal path relies on this to drop the
8648+
// literal's own properties from the result). tsgo's RPC takes no
8649+
// flags and its union can include the node's own inferred type —
8650+
// emulate by dropping union constituents whose declarations all
8651+
// live inside the queried node (issue #5).
8652+
if (t && contextFlags !== undefined && (contextFlags & ContextFlags.IgnoreNodeInferences)) {
8653+
return filterNodeInferredConstituents(t, node);
8654+
}
85568655
return t;
85578656
},
85588657
getResolvedSignature(node: any): any {

0 commit comments

Comments
 (0)