diff --git a/gitnexus/src/core/ingestion/class-extractors/generic.ts b/gitnexus/src/core/ingestion/class-extractors/generic.ts index 1f11adc44..7ed86a934 100644 --- a/gitnexus/src/core/ingestion/class-extractors/generic.ts +++ b/gitnexus/src/core/ingestion/class-extractors/generic.ts @@ -6,6 +6,7 @@ import type { ClassLikeNodeLabel, ExtractedClassSymbol, } from '../class-types.js'; +import { normalizeQualifiedName, splitQualifiedName } from '../utils/qualified-name.js'; const DEFAULT_SCOPE_NAME_NODE_TYPES = new Set([ 'nested_namespace_specifier', @@ -58,20 +59,6 @@ const CLASS_LIKE_LABELS = new Set([ 'Record', ]); -const normalizeQualifiedName = (value: string): string => - value - .replace(/\s+/g, '') - .replace(/^::/, '') - .replace(/::/g, '.') - .replace(/\\/g, '.') - .replace(/\.+/g, '.') - .replace(/^\.+|\.+$/g, ''); - -const splitQualifiedName = (value: string): string[] => { - const normalized = normalizeQualifiedName(value); - return normalized ? normalized.split('.').filter(Boolean) : []; -}; - const extractScopeSegmentsFromNode = ( scopeNode: SyntaxNode, scopeNameNodeTypes: ReadonlySet, diff --git a/gitnexus/src/core/ingestion/utils/qualified-name.ts b/gitnexus/src/core/ingestion/utils/qualified-name.ts new file mode 100644 index 000000000..dcd3d5ffb --- /dev/null +++ b/gitnexus/src/core/ingestion/utils/qualified-name.ts @@ -0,0 +1,43 @@ +/** + * Shared qualified-name normalization. + * + * One canonical transform from a raw, language-specific qualified name + * (`Other::Inner`, `pkg\Sub\Type`, ` A . B `) to the `.`-joined form the + * graph and the `QualifiedNameIndex` are keyed by (`Other.Inner`, `pkg.Sub.Type`, + * `A.B`). Extracted from `class-extractors/generic.ts` so the structure-phase + * `buildQualifiedName`, the scope-resolution inheritance resolver, and the + * per-language capture emitters all key against ONE normalizer — a raw `::` + * qualifier must normalize to the exact key the index already holds, or the + * qualified lookup silently misses (issue #1982). + * + * Do NOT confuse with `heritage-extractors/supertype-alternation.ts`'s + * `simplifyRawName`, which collapses a qualified name to its LAST segment + * (`Other::Inner` → `Inner`) — that is a tail extractor, not a normalizer, and + * using it as a lookup key guarantees a miss. + * + * Pure string functions; no AST or tree-sitter dependency. + */ + +/** + * Normalize a raw qualified name to the `.`-joined canonical form: + * strips whitespace, converts `::` and `\` separators to `.`, collapses + * repeated dots, and trims leading/trailing dots. + */ +export const normalizeQualifiedName = (value: string): string => + value + .replace(/\s+/g, '') + .replace(/^::/, '') + .replace(/::/g, '.') + .replace(/\\/g, '.') + .replace(/\.+/g, '.') + .replace(/^\.+|\.+$/g, ''); + +/** + * Split a raw qualified name into its normalized, non-empty segments + * (`Other::Inner` → `['Other', 'Inner']`). Returns `[]` for an empty or + * separator-only input. + */ +export const splitQualifiedName = (value: string): string[] => { + const normalized = normalizeQualifiedName(value); + return normalized ? normalized.split('.').filter(Boolean) : []; +};