Merge remote-tracking branch 'origin/main' into fix/kotlin-property-initializer-walk

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
danusha2345
2026-09-08 23:52:14 +03:00
6 changed files with 478 additions and 5 deletions
+4
View File
@@ -230,6 +230,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Kotlin `init { }` blocks and destructuring declarations no longer swallow their code: `init { val cfg = load() }` and `val (a, b) = makePair()` contributed no call edge at all, and now attribute to the enclosing class or file.
- A Kotlin property's accessor body now belongs to the property whichever line it is written on, instead of being dropped (same line) or handed to the enclosing class (own line).
- Kotlin properties that hold a lambda, a SAM callback or an anonymous object — `private val frameListener = CameraFrameListener { … }`, the way Android and MSDK callbacks are almost always declared — now contribute call edges. Previously everything inside such an initializer was dropped, so a function reached only through one of these callbacks looked like it had no callers at all and its blast radius came back far too small. Delegated properties (`by lazy { … }`) and plain initializers (`val x = compute()`) were affected the same way and are fixed too. Re-index with `codegraph index -f` after upgrading to pick up the new edges.
- Imports from Node built-ins or npm packages no longer connect to unrelated type members with matching names; re-index after upgrading to clear existing false dependencies. Thanks @ctype-lab. (#1537)
- Inheritance relationships no longer attach external Rust or npm supertypes to unrelated local symbols with the same name, including in Svelte, Vue and Astro components; re-index after upgrading to clear existing false relationships. Thanks @ctype-lab. (#1536)
- PHP static calls through imported class aliases now reach the correct class when services and repositories share method names, so callers and impact analysis show the right dependencies after re-indexing. (#1545)
- TypeScript/JavaScript: a call through a field of the enclosing class — `this.mailer.send()` — now resolves on the field's declared type, so a delegating wrapper that shares the method's name no longer records itself as its own callee and `callers`, `impact` and trace stop lying on that shape. A field whose type is external or a builtin stays unresolved rather than guessed. Re-index after upgrading. (#1496)
- TypeScript and JavaScript collection calls through local variables and their nested properties no longer link to unrelated project methods; re-index after upgrading. (#1566)
+215
View File
@@ -0,0 +1,215 @@
/**
* Reference target-kind gate — `extends`/`implements` and `imports`.
*
* The name-matcher treats node kind as a scoring BONUS, never a filter, and
* awards no bonus at all for inheritance refs. When exactly one same-named
* node exists, the single-candidate shortcut adopts it unconditionally at
* confidence 0.9. So a supertype that lives OUTSIDE the repo — imported by a
* bare name — bound to whatever local symbol happened to share that name,
* asserting an inheritance relationship absent from the source:
*
* use std::error::Error; // the supertype is out-of-repo
* impl Error for MapperError {} // ...but `MapperError::Error` is a variant
* → implements: enum MapperError -> enum_member Error
*
* The gate drops any inheritance resolution whose target cannot be a
* supertype. It only ever removes edges, so the tests below pin BOTH
* directions: the false edge is gone, and every legitimate supertype kind
* (in-repo trait, interface, class, and TS object-type alias) still resolves.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { CodeGraph } from '../src';
describe('reference target-kind gate', () => {
let dir: string;
beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'inh-kind-')); });
afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
const write = (rel: string, body: string) => {
const p = path.join(dir, rel);
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, body);
};
type InhEdge = { src: string; srcKind: string; tgt: string; tgtKind: string; kind: string };
const load = async (): Promise<{ edges: InhEdge[]; failed: { name: string; kind: string }[] }> => {
const cg = await CodeGraph.init(dir, { silent: true });
await cg.indexAll();
const db = (cg as any).db.db;
const edges: InhEdge[] = db
.prepare(
`SELECT s.name src, s.kind srcKind, t.name tgt, t.kind tgtKind, e.kind kind
FROM edges e
JOIN nodes s ON s.id = e.source
JOIN nodes t ON t.id = e.target
WHERE e.kind IN ('extends', 'implements')`
)
.all();
const failed: { name: string; kind: string }[] = db
.prepare(
`SELECT reference_name name, reference_kind kind
FROM unresolved_refs
WHERE reference_kind IN ('extends', 'implements')`
)
.all();
cg.close?.();
return { edges, failed };
};
const has = (edges: InhEdge[], src: string, tgt: string, tgtKind: string) =>
edges.some((e) => e.src === src && e.tgt === tgt && e.tgtKind === tgtKind);
it('drops an out-of-repo Rust supertype that name-matched a local enum member', async () => {
write(
'src/lib.rs',
`use std::error::Error;\n\n` +
`pub enum MapperError {\n Error,\n Missing,\n}\n\n` +
`impl Error for MapperError {}\n`
);
const { edges, failed } = await load();
expect(has(edges, 'MapperError', 'Error', 'enum_member')).toBe(false);
// The reference is not silently forgotten — it stays on record as failed,
// which is the honest outcome for a supertype the repo does not contain.
expect(failed.some((r) => r.name === 'Error')).toBe(true);
});
it('does not relocate the false edge onto a same-named local type alias', async () => {
// The kind filter alone would have moved this edge from the enum member to
// `type Error`, which IS a legal supertype kind — still false data, and
// harder for a consumer to reject. Locality is what removes it.
write('src/alias.rs', `pub type Error = String;\n`);
write(
'src/lib.rs',
`mod alias;\n\nuse std::error::Error;\n\n` +
`pub enum MapperError {\n Missing,\n}\n\n` +
`impl Error for MapperError {}\n`
);
const { edges, failed } = await load();
expect(edges.filter((e) => e.tgt === 'Error')).toEqual([]);
expect(failed.some((r) => r.name === 'Error')).toBe(true);
});
it('keeps a supertype imported by an in-repo `use` path', async () => {
write('src/ports.rs', `pub trait Sha256Port {\n fn hash(&self) -> String;\n}\n`);
write(
'src/lib.rs',
`mod ports;\n\nuse crate::ports::Sha256Port;\n\n` +
`pub struct Hasher {\n salt: String,\n}\n\n` +
`impl Sha256Port for Hasher {\n fn hash(&self) -> String { String::new() }\n}\n`
);
const { edges } = await load();
expect(has(edges, 'Hasher', 'Sha256Port', 'trait')).toBe(true);
});
it('keeps a trait reached through a re-exported sibling-crate module', async () => {
// `crate::ports` here is a re-export of ANOTHER crate's module, so no
// `src/ports.rs` exists to walk to. Treating "module path does not resolve
// to a file" as proof of out-of-repo deleted 13 real trait implementations
// on the reference fixture — hence the rule keys on stdlib roots only.
write('Cargo.toml', `[workspace]\nmembers = ["core", "app"]\n`);
write('core/Cargo.toml', `[package]\nname = "pupil_core"\nversion = "0.1.0"\n`);
write('core/src/lib.rs', `pub mod ports;\n`);
write('core/src/ports.rs', `pub trait CacheStore {\n fn get(&self);\n}\n`);
write('app/Cargo.toml', `[package]\nname = "app"\nversion = "0.1.0"\n`);
write('app/src/lib.rs', `pub use pupil_core::ports;\n\npub mod platform;\n`);
write(
'app/src/platform.rs',
`use crate::ports::CacheStore;\n\npub struct SafStorage {\n root: String,\n}\n\n` +
`impl CacheStore for SafStorage {\n fn get(&self) {}\n}\n`
);
const { edges } = await load();
expect(has(edges, 'SafStorage', 'CacheStore', 'trait')).toBe(true);
});
it('still resolves an in-repo Rust trait (the gate is not a blanket block)', async () => {
write(
'src/lib.rs',
`pub trait Mapper {\n fn map(&self) -> u32;\n}\n\n` +
`pub enum MapperError {\n Mapper,\n}\n\n` +
`pub struct Real {\n n: u32,\n}\n\n` +
`impl Mapper for Real {\n fn map(&self) -> u32 { 1 }\n}\n`
);
const { edges } = await load();
expect(has(edges, 'Real', 'Mapper', 'trait')).toBe(true);
expect(has(edges, 'Real', 'Mapper', 'enum_member')).toBe(false);
});
it('keeps a TypeScript class implementing an object-type alias', async () => {
write(
'src/api.ts',
`export type SearchApi = { query(q: string): string };\n\n` +
`export class LocalSearch implements SearchApi {\n` +
` query(q: string): string { return q; }\n}\n`
);
const { edges } = await load();
expect(has(edges, 'LocalSearch', 'SearchApi', 'type_alias')).toBe(true);
});
it.each([
['svelte', 'src/Box.svelte', '<script lang="ts">\n$IMPORT$\nexport class SfcBox implements Serializable {\n n = 1;\n}\n</script>\n<div>hi</div>\n'],
['vue', 'src/Box.vue', '<script lang="ts">\n$IMPORT$\nexport class SfcBox implements Serializable {\n n = 1;\n}\n</script>\n<template><div/></template>\n'],
['astro', 'src/Box.astro', '---\n$IMPORT$\nexport class SfcBox implements Serializable {\n n = 1;\n}\n---\n<div/>\n'],
])('drops an npm supertype in a %s single-file component', async (_lang, file, body) => {
// An SFC imports inside its <script> block (Astro: the `---` frontmatter)
// with ordinary ES module syntax, so a bare specifier there is external for
// exactly the same reason it is in a .ts file. Missing that, the npm
// supertype name-matched the local class below.
write('package.json', `{"name":"sfc","version":"1.0.0"}\n`);
write('src/models.ts', `export class Serializable {\n a = 1;\n}\n`);
write(file, body.replace('$IMPORT$', `import { Serializable } from 'some-npm-pkg';\n`));
const { edges, failed } = await load();
expect(edges.filter((e) => e.tgt === 'Serializable')).toEqual([]);
expect(failed.some((r) => r.name === 'Serializable')).toBe(true);
});
it('keeps an SFC supertype imported from a relative path', async () => {
write('package.json', `{"name":"sfc","version":"1.0.0"}\n`);
write('src/models.ts', `export class Serializable {\n a = 1;\n}\n`);
write(
'src/Box.svelte',
`<script lang="ts">\nimport { Serializable } from './models';\n\n` +
`export class SfcBox implements Serializable {\n n = 1;\n}\n</script>\n<div>hi</div>\n`
);
const { edges } = await load();
expect(has(edges, 'SfcBox', 'Serializable', 'class')).toBe(true);
});
it('does not resolve an import to a type member that shares its name', async () => {
// `import * as path from 'node:path'` is unresolvable — the module is
// external — so the name-matcher looked for any node called `path` and
// found a class property. No language lets you import a type's member.
write('src/types.ts', `export class Request {\n path = '';\n url = '';\n}\n`);
write(
'src/run.ts',
`import * as path from 'node:path';\n\nexport function run() {\n return path.join('a', 'b');\n}\n`
);
const cg = await CodeGraph.init(dir, { silent: true });
await cg.indexAll();
const db = (cg as any).db.db;
const rows: { tgt: string; tgtKind: string }[] = db
.prepare(
`SELECT t.name tgt, t.kind tgtKind
FROM edges e JOIN nodes t ON t.id = e.target
WHERE e.kind = 'imports'`
)
.all();
cg.close?.();
expect(rows.filter((r) => r.tgtKind === 'property' || r.tgtKind === 'field')).toEqual([]);
});
it('keeps class extends class and class implements interface', async () => {
write(
'src/base.ts',
`export interface Runner { run(): void }\n` +
`export class Base { run(): void {} }\n` +
`export class Child extends Base implements Runner { run(): void {} }\n`
);
const { edges } = await load();
expect(has(edges, 'Child', 'Base', 'class')).toBe(true);
expect(has(edges, 'Child', 'Runner', 'interface')).toBe(true);
});
});
+136 -1
View File
@@ -316,6 +316,21 @@ const C_CPP_STDLIB_HEADERS = new Set([
'version',
]);
/**
* Languages whose imports are ES-module specifiers, extracted by
* `extractJSImports` and therefore classified by the same bare-specifier /
* alias / workspace rules. Svelte, Vue and Astro belong here: an SFC imports
* inside its `<script>` block (Astro: the `---` frontmatter) with exactly the
* same syntax, and leaving them out made `isExternalImport` answer "not
* external" for every npm specifier in an SFC.
*/
const ESM_IMPORT_LANGUAGES = new Set<Language>([
'typescript', 'tsx', 'javascript', 'jsx', 'arkts', 'svelte', 'vue', 'astro',
]);
/** Rust path roots that always name a standard-library crate. */
const RUST_STDLIB_ROOTS = new Set(['std', 'core', 'alloc', 'proc_macro']);
/**
* Check if an import is external (npm package, etc.)
*
@@ -344,7 +359,7 @@ function isExternalImport(
}
// Common external patterns
if (language === 'typescript' || language === 'javascript' || language === 'tsx' || language === 'jsx' || language === 'arkts') {
if (ESM_IMPORT_LANGUAGES.has(language)) {
// Node built-ins
if (['fs', 'path', 'os', 'crypto', 'http', 'https', 'url', 'util', 'events', 'stream', 'child_process', 'buffer'].includes(importPath)) {
return true;
@@ -2487,3 +2502,123 @@ function resolveStaticMember(
}
return candidates[0];
}
/**
* Rust `use` declarations, flattened to `localName → full path`.
*
* Rust is the one supported language with NO `ImportMapping` extraction (see
* `extractImportMappings`), so this is the only channel that can tell whether
* a bare type name in a Rust file was brought in by a `use`. Handles nested
* groups (`use a::{b::C, d as E}`), globs (skipped — they bind no single
* name), and `as` aliases.
*/
function collectRustUseBindings(content: string): Map<string, string> {
const out = new Map<string, string>();
// Expand one level of `{...}` at a time so `a::{b::{C, D}, E}` flattens.
const expand = (spec: string): string[] => {
const open = spec.indexOf('{');
if (open === -1) return [spec.trim()];
const prefix = spec.slice(0, open);
let depth = 0;
let close = -1;
for (let i = open; i < spec.length; i++) {
if (spec[i] === '{') depth++;
else if (spec[i] === '}') {
depth--;
if (depth === 0) { close = i; break; }
}
}
if (close === -1) return [];
const suffix = spec.slice(close + 1);
const inner = spec.slice(open + 1, close);
const parts: string[] = [];
let depth2 = 0;
let start = 0;
for (let i = 0; i <= inner.length; i++) {
const ch = inner[i];
if (ch === '{') depth2++;
else if (ch === '}') depth2--;
if (i === inner.length || (ch === ',' && depth2 === 0)) {
const seg = inner.slice(start, i).trim();
if (seg) parts.push(seg);
start = i + 1;
}
}
return parts.flatMap((p) => expand(prefix + p + suffix));
};
// `use` items end at the first `;`. Attributes/visibility (`pub use`) are
// irrelevant to the binding itself.
const useRe = /(^|\n)\s*(?:pub(?:\([^)]*\))?\s+)?use\s+([^;]+);/g;
let m: RegExpExecArray | null;
while ((m = useRe.exec(content)) !== null) {
for (const spec of expand(m[2]!.replace(/\s+/g, ' '))) {
const aliasMatch = /^(.*?)\s+as\s+([A-Za-z_]\w*)$/.exec(spec);
const rawPath = (aliasMatch ? aliasMatch[1]! : spec).trim();
if (!rawPath || rawPath.endsWith('*')) continue;
const segments = rawPath.split('::').map((s) => s.trim()).filter(Boolean);
const leaf = segments[segments.length - 1];
if (!leaf) continue;
const local = aliasMatch ? aliasMatch[2]! : leaf;
out.set(local, segments.join('::'));
}
}
return out;
}
/**
* Is `name`, as used in `ref`'s file, bound by an import whose module lives
* OUTSIDE the repository?
*
* When it is, no in-repo node can be the referent: the symbol is defined in a
* third-party crate/package, and any same-named local symbol the name-matcher
* finds is a coincidence. Rust `use std::error::Error;` + `impl Error for
* MapperError {}` bound to a local `MapperError::Error` variant, and once
* non-type kinds were filtered out it simply moved to an unrelated local
* `type Error` alias — restricting kinds alone RELOCATES the false edge
* instead of removing it, so locality has to be checked too.
*
* Answers only when it can be CERTAIN, because a false "yes" deletes a real
* edge. Two languages qualify, each with an oracle that cannot be wrong:
*
* - **Rust** — the `use` path is rooted at a standard-library crate
* (`std`/`core`/`alloc`/`proc_macro`), which by definition ships outside
* any repository. Deliberately NOT generalized to "the module path doesn't
* resolve to a file": a crate can re-export another workspace crate's
* modules (`pub use pupil_core::{ports, domain};`), so `crate::ports::X`
* has no `src/ports/` directory to walk yet is entirely in-repo — that
* generalization measured 13 real trait implementations deleted.
* - **ES modules** — `isExternalImport`, which already accounts for tsconfig
* path aliases and monorepo workspace packages.
*
* Everything else returns false and resolves exactly as before. JVM and Python
* imports notably do NOT go through `resolveImportPath` (they have dedicated
* FQN/module matchers), so there is no trustworthy oracle to consult here.
*/
export function isBoundToOutOfRepoImport(
ref: UnresolvedRef,
context: ResolutionContext
): boolean {
const name = ref.referenceName;
if (name.includes('::') || name.includes('.')) return false; // qualified refs resolve by path
if (ref.language === 'rust') {
const content = context.readFile(ref.filePath);
if (!content) return false;
const usePath = collectRustUseBindings(content).get(name);
if (!usePath) return false;
const segments = usePath.split('::');
if (segments.length < 2 || !RUST_STDLIB_ROOTS.has(segments[0]!)) return false;
// 2015-edition crate-relative paths can shadow a stdlib root with a local
// module of the same name — if the path walks to a real file, it's local.
return resolveRustModuleFile(segments.slice(0, -1), ref.filePath, context) === null;
}
if (!ESM_IMPORT_LANGUAGES.has(ref.language)) return false;
for (const imp of context.getImportMappings(ref.filePath, ref.language)) {
if (imp.localName !== name) continue;
return isExternalImport(imp.source, ref.language, context);
}
return false;
}
+58 -2
View File
@@ -15,9 +15,12 @@ import {
ResolutionContext,
FrameworkResolver,
ImportMapping,
SUPERTYPE_TARGET_KINDS,
isInheritanceRef,
isImportableKind,
} from './types';
import { isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesKnownFamily, dumpNameMatcherProfile, clearNameMatcherMemos } from './name-matcher';
import { resolveViaImport, resolvePhpImportedStaticCall, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef, isNixPathImportRef, clearImportResolverMemos, resolveImportPath } from './import-resolver';
import { resolveViaImport, resolvePhpImportedStaticCall, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef, isNixPathImportRef, isBoundToOutOfRepoImport, clearImportResolverMemos, resolveImportPath } from './import-resolver';
import { ResolverPool, minRefsForPool } from './resolver-pool';
import { detectFrameworks } from './frameworks';
import { synthesizeCallbackEdges } from './callback-synthesizer';
@@ -36,6 +39,11 @@ const SUPERTYPE_BEARING_KINDS = new Set<Node['kind']>([
'class', 'struct', 'interface', 'trait', 'protocol', 'enum',
]);
// SUPERTYPE_TARGET_KINDS (the kinds an extends/implements edge may TARGET)
// lives in ./types — the name-matcher needs the same set to restrict its
// candidate pool before ranking. It is deliberately wider than
// SUPERTYPE_BEARING_KINDS above, which is about the DECLARING side.
/**
* Languages whose chained static-factory/fluent calls defer to the conformance
* second pass. Dotted-receiver languages resolve via matchDottedCallChain; the
@@ -857,9 +865,18 @@ export class ReferenceResolver {
}
/**
* Resolve a single reference
* Resolve a single reference.
*
* Thin decorator over `resolveOneInner` so every strategy — framework,
* import, name-match, chain, CFML component path — passes through the
* inheritance target-kind gate at ONE seam. Filtering inside the
* name-matcher would have covered `matchByExactName` only.
*/
resolveOne(ref: UnresolvedRef): ResolvedRef | null {
return this.gateTargetKind(this.resolveOneInner(ref), ref);
}
private resolveOneInner(ref: UnresolvedRef): ResolvedRef | null {
// Skip built-in/external references
if (this.isBuiltInOrExternal(ref)) {
return null;
@@ -2582,6 +2599,45 @@ export class ReferenceResolver {
return this.persistDeferredReferences(deferred, resolved);
}
/**
* Drop a resolution whose target cannot be what the reference names.
* Applied at the `resolveOne` seam so it covers every strategy uniformly —
* framework, import, name-match, chain, CFML component path.
*
* For `imports`: the target must be importable. A member that only exists
* inside a type never is.
*
* For `extends`/`implements`, it cannot be describing a real supertype when:
*
* 1. The target's kind can never be a supertype (an enum member, a method,
* a variable). `matchByExactName` additionally narrows its candidate
* pool by the same set, so a legitimate supertype outranks a same-named
* non-type rather than merely losing its edge.
* 2. The name is imported from outside the repo, so NO local node is the
* referent. Without this, filtering by kind alone just relocates the
* false edge onto the next same-named local type.
*
* Direction is one-way: this only ever REMOVES an edge, never adds one. A
* dropped ref stays in `unresolved_refs` as `failed`, which is the honest
* record for a supertype that lives outside the repo — silent beats wrong.
*/
private gateTargetKind(result: ResolvedRef | null, ref: UnresolvedRef): ResolvedRef | null {
if (!result) return result;
// An `imports` reference names something importable — never a member that
// only exists inside a type.
if (ref.referenceKind === 'imports') {
const target = this.queries.getNodeById(result.targetNodeId);
return target && !isImportableKind(target.kind) ? null : result;
}
if (!isInheritanceRef(ref)) return result;
const target = this.queries.getNodeById(result.targetNodeId);
if (target && !SUPERTYPE_TARGET_KINDS.has(target.kind)) return null;
if (isBoundToOutOfRepoImport(ref, this.context)) return null;
return result;
}
private gateLanguage(result: ResolvedRef | null, ref: UnresolvedRef): ResolvedRef | null {
if (!result) return result;
const tgt = this.getLanguageFromNodeId(result.targetNodeId);
+14 -2
View File
@@ -6,7 +6,7 @@
import * as path from 'path';
import { Language, Node } from '../types';
import { UnresolvedRef, ResolvedRef, ResolutionContext } from './types';
import { UnresolvedRef, ResolvedRef, ResolutionContext, SUPERTYPE_TARGET_KINDS, isInheritanceRef, isImportableKind } from './types';
import { blankStringContents, stripCommentsForRegex } from './strip-comments';
import { JS_BUILT_INS } from './js-builtins';
@@ -762,7 +762,19 @@ export function matchByExactName(
.filter((n) => !(bareJs && n.kind === 'method'))
// A name the file binds itself (a parameter, a const) shadows every other
// file's symbol of that name, so a bare call has no cross-file candidate.
.filter((n) => !(bareJs && n.filePath !== ref.filePath && isLocallyBoundJsName(ref.referenceName, ref.filePath, context)));
.filter((n) => !(bareJs && n.filePath !== ref.filePath && isLocallyBoundJsName(ref.referenceName, ref.filePath, context)))
// An `extends`/`implements` ref names a supertype, so anything that can't
// BE one is not a candidate at all. This is eligibility, not
// ranking: kind is only a scoring bonus below (and none is awarded for
// inheritance refs), so without this a same-named `enum_member` outranked
// the real `trait`, and as the sole candidate was adopted outright by the
// single-match shortcut. Restricting the pool BEFORE ranking lets the
// legitimate supertype win instead of merely dropping the false edge.
.filter((n) => !isInheritanceRef(ref) || SUPERTYPE_TARGET_KINDS.has(n.kind))
// Likewise for `imports`: a member that only exists inside a type is not
// importable, so it is not a candidate. Without this a `path`/`id`/`url`
// import resolved to some interface's same-named property.
.filter((n) => ref.referenceKind !== 'imports' || isImportableKind(n.kind));
if (candidates.length === 0) {
return null;
+51
View File
@@ -295,3 +295,54 @@ export type ReExport =
/** Module specifier of the upstream module. */
source: string;
};
/**
* Node kinds an `extends`/`implements` edge may legally TARGET — the things a
* type can actually inherit from or conform to.
*
* Kept deliberately wide: `type_alias` because TS `class X implements
* SomeAliasedObjectType` is valid, `component` because a framework component
* node stands in for a class, and `module`/`namespace` because whole
* languages inherit from one — Ruby `include Trackable` targets a `module`,
* Erlang `-behaviour(gen_server)` targets the behaviour module, which Erlang
* extraction indexes as a `namespace` (the conformance pass in
* `resolution/index.ts` makes the same `module` allowance).
*
* Everything omitted (`enum_member`, `method`, `field`, `property`,
* `variable`, `constant`, `function`, `parameter`, `import`, `export`,
* `file`, `route`) can never be a supertype in any supported language, so an
* inheritance edge pointing at one is false data.
*
* Why this is needed: the name-matcher scores node kind as a BONUS,
* never a filter, and awards no bonus at all for inheritance refs — so a
* same-named non-type outranked (or, as the sole candidate, was adopted
* outright as) the real supertype. Rust `use std::error::Error;` + `impl Error
* for MapperError {}` bound to the local `MapperError::Error` VARIANT. The
* supertype is out-of-repo and simply unresolvable; a failed ref is correct.
*/
export const SUPERTYPE_TARGET_KINDS = new Set<Node['kind']>([
'class', 'struct', 'interface', 'trait', 'protocol', 'enum', 'union',
'type_alias', 'component', 'module', 'namespace',
]);
/** True for the reference kinds that assert an inheritance/conformance relation. */
export function isInheritanceRef(ref: UnresolvedRef): boolean {
return ref.referenceKind === 'extends' || ref.referenceKind === 'implements';
}
/**
* Node kinds an `imports` edge may never TARGET: members that only exist
* INSIDE a type. No language lets you import a class's property, an
* interface's method or an enum's variant — you import the type that
* contains it. The name-matcher has no kind filter, so a bare
* `import path from 'node:path'` (unresolvable, since the module is external)
* name-matched an interface property called `path` in an unrelated file.
*/
const NON_IMPORTABLE_KINDS = new Set<Node['kind']>([
'property', 'field', 'method', 'enum_member', 'parameter',
]);
/** Can an `imports` reference legally resolve to this node kind? */
export function isImportableKind(kind: Node['kind']): boolean {
return !NON_IMPORTABLE_KINDS.has(kind);
}