refactor(ingestion): extract shared qualified-name normalizer (#1982)

Move normalizeQualifiedName/splitQualifiedName out of class-extractors/
generic.ts into utils/qualified-name.ts so the structure-phase
buildQualifiedName, the scope-resolution inheritance resolver, and the
per-language capture emitters can all key against ONE normalizer. A raw
'::' qualifier must normalize to the exact '.'-joined key the
QualifiedNameIndex already holds, or the qualified lookup silently misses
(the #1982 resolution-side foundation). Pure relocation — byte-identical
function bodies; tsc clean; existing C++ nested-collision tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-06-03 06:49:43 +00:00
parent e2641628c7
commit 35a37d9240
2 changed files with 44 additions and 14 deletions

View file

@ -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<ClassLikeNodeLabel>([
'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<string>,

View file

@ -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) : [];
};