mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-21 05:13:43 +00:00
refactor(di): make resolver metadata framework-neutral
This commit is contained in:
parent
710eb729d4
commit
61bfd0eb18
5 changed files with 61 additions and 38 deletions
|
|
@ -20,13 +20,17 @@ import { springDiResolver } from './spring.js';
|
|||
|
||||
/** A successful injection-site match, produced by a per-language resolver. */
|
||||
export interface DiInjectionMatch {
|
||||
/** The requested bean type name. */
|
||||
/** The requested dependency type name. */
|
||||
targetTypeName: string;
|
||||
/** A collection receives every matching provider; a single site must be
|
||||
* disambiguated by qualifier, primary status, or uncertainty. */
|
||||
/** A collection receives every matching provider; a single site may need
|
||||
* framework-specific named/preferred-provider disambiguation. */
|
||||
cardinality: 'single' | 'collection';
|
||||
/** Statically known provider/bean name requested at the injection site. */
|
||||
qualifier?: string;
|
||||
/** Statically known provider name requested at the injection site. The
|
||||
* resolver owns the human-readable explanation of that selection. */
|
||||
namedSelection?: {
|
||||
name: string;
|
||||
reason: string;
|
||||
};
|
||||
/** Human-readable edge reason. Framework specifics (names, idioms,
|
||||
* collection wrapper, gating annotation) live in this payload so the
|
||||
* shared `di` phase stays framework-neutral. */
|
||||
|
|
@ -35,12 +39,11 @@ export interface DiInjectionMatch {
|
|||
|
||||
/** Provider metadata used by the shared resolver without naming a framework. */
|
||||
export interface DiProviderMatch {
|
||||
/** Whether the class is statically known to be container-managed. */
|
||||
isBean: boolean;
|
||||
/** Bean names and qualifier aliases that can satisfy a named injection. */
|
||||
/** Provider names and aliases that can satisfy a named injection. */
|
||||
names: readonly string[];
|
||||
/** Whether this provider has the framework's preferred-candidate marker. */
|
||||
primary: boolean;
|
||||
/** Present when the framework marks this as its preferred candidate. The
|
||||
* value is appended to the emitted edge reason when it disambiguates. */
|
||||
preferenceReason?: string;
|
||||
}
|
||||
|
||||
/** Per-language DI behavior. Matchers receive whole nodes so the shared phase
|
||||
|
|
|
|||
|
|
@ -257,11 +257,16 @@ export const springDiFieldMatcher = (
|
|||
function isInjectionMatch(value: unknown): value is DiInjectionMatch {
|
||||
if (value === null || typeof value !== 'object') return false;
|
||||
const match = value as Partial<DiInjectionMatch>;
|
||||
const namedSelection = match.namedSelection;
|
||||
return (
|
||||
typeof match.targetTypeName === 'string' &&
|
||||
(match.cardinality === 'single' || match.cardinality === 'collection') &&
|
||||
typeof match.reason === 'string' &&
|
||||
(match.qualifier === undefined || typeof match.qualifier === 'string')
|
||||
(namedSelection === undefined ||
|
||||
(typeof namedSelection === 'object' &&
|
||||
namedSelection !== null &&
|
||||
typeof namedSelection.name === 'string' &&
|
||||
typeof namedSelection.reason === 'string'))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -269,10 +274,9 @@ function isProviderMatch(value: unknown): value is DiProviderMatch {
|
|||
if (value === null || typeof value !== 'object') return false;
|
||||
const provider = value as Partial<DiProviderMatch>;
|
||||
return (
|
||||
typeof provider.isBean === 'boolean' &&
|
||||
typeof provider.primary === 'boolean' &&
|
||||
Array.isArray(provider.names) &&
|
||||
provider.names.every((name) => typeof name === 'string')
|
||||
provider.names.every((name) => typeof name === 'string') &&
|
||||
(provider.preferenceReason === undefined || typeof provider.preferenceReason === 'string')
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -304,9 +304,8 @@ export function attachJavaSpringDiMetadata(
|
|||
if (explicitBeanName !== undefined) names.add(explicitBeanName);
|
||||
else if (!hasDynamicBeanName) names.add(defaultBeanName(classNode.properties.name));
|
||||
const provider: DiProviderMatch = {
|
||||
isBean: true,
|
||||
names: [...names],
|
||||
primary,
|
||||
...(primary ? { preferenceReason: 'selected @Primary' } : {}),
|
||||
};
|
||||
classNode.properties[SPRING_DI_PROVIDER_PROPERTY] = provider;
|
||||
}
|
||||
|
|
@ -363,7 +362,14 @@ export function attachJavaSpringDiMetadata(
|
|||
matches.push({
|
||||
targetTypeName: parsedType.targetTypeName,
|
||||
cardinality: parsedType.cardinality,
|
||||
...(qualifier === undefined ? {} : { qualifier }),
|
||||
...(qualifier === undefined
|
||||
? {}
|
||||
: {
|
||||
namedSelection: {
|
||||
name: qualifier,
|
||||
reason: `qualifier "${qualifier}"`,
|
||||
},
|
||||
}),
|
||||
reason: `Spring DI: ${trigger} ${location}: ${parsedType.displayType}`,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -446,9 +446,8 @@ export function attachKotlinSpringDiMetadata(
|
|||
if (explicitBeanName !== undefined) names.add(explicitBeanName);
|
||||
else if (!hasDynamicBeanName) names.add(defaultBeanName(classNode.properties.name));
|
||||
const provider: DiProviderMatch = {
|
||||
isBean: true,
|
||||
names: [...names],
|
||||
primary,
|
||||
...(primary ? { preferenceReason: 'selected @Primary' } : {}),
|
||||
};
|
||||
classNode.properties[SPRING_DI_PROVIDER_PROPERTY] = provider;
|
||||
}
|
||||
|
|
@ -499,7 +498,14 @@ export function attachKotlinSpringDiMetadata(
|
|||
matches.push({
|
||||
targetTypeName: parsedType.targetTypeName,
|
||||
cardinality: parsedType.cardinality,
|
||||
...(qualifier === undefined ? {} : { qualifier }),
|
||||
...(qualifier === undefined
|
||||
? {}
|
||||
: {
|
||||
namedSelection: {
|
||||
name: qualifier,
|
||||
reason: `qualifier "${qualifier}"`,
|
||||
},
|
||||
}),
|
||||
reason: `Spring DI: ${trigger} ${location}: ${parsedType.displayType}`,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,12 +76,12 @@ function providerCandidates(
|
|||
providers: ReadonlyMap<string, DiProviderMatch>,
|
||||
): string[] {
|
||||
const all = [...ids];
|
||||
const managed = all.filter((id) => providers.get(id)?.isBean === true);
|
||||
// Recall-first fallback: bean metadata is deliberately incomplete (custom
|
||||
// stereotypes, factory-created beans, and legacy indexes can all omit it).
|
||||
// Prefer known managed providers when present, but keep structurally valid
|
||||
const recognized = all.filter((id) => providers.has(id));
|
||||
// Recall-first fallback: provider metadata can be incomplete (custom
|
||||
// registration mechanisms and legacy indexes can omit it). Prefer
|
||||
// framework-recognized providers when present, but keep structurally valid
|
||||
// candidates when none are known instead of dropping the injection entirely.
|
||||
return managed.length > 0 ? managed : all;
|
||||
return recognized.length > 0 ? recognized : all;
|
||||
}
|
||||
|
||||
export const diPhase: PipelinePhase<DIOutput> = {
|
||||
|
|
@ -200,16 +200,17 @@ export const diPhase: PipelinePhase<DIOutput> = {
|
|||
if (structural.size === 0) continue;
|
||||
|
||||
let viable = providerCandidates(structural, providers);
|
||||
const qualifier = candidate.qualifier;
|
||||
if (qualifier !== undefined) {
|
||||
viable = viable.filter((id) => providers.get(id)?.names.includes(qualifier) === true);
|
||||
const namedSelection = candidate.namedSelection;
|
||||
if (namedSelection !== undefined) {
|
||||
viable = viable.filter(
|
||||
(id) => providers.get(id)?.names.includes(namedSelection.name) === true,
|
||||
);
|
||||
if (viable.length === 0) continue;
|
||||
}
|
||||
|
||||
if (candidate.cardinality === 'collection') {
|
||||
const confidence = candidate.qualifier === undefined ? 0.8 : 0.9;
|
||||
const suffix =
|
||||
candidate.qualifier === undefined ? '' : `; qualifier "${candidate.qualifier}"`;
|
||||
const confidence = namedSelection === undefined ? 0.8 : 0.9;
|
||||
const suffix = namedSelection === undefined ? '' : `; ${namedSelection.reason}`;
|
||||
for (const targetId of viable) {
|
||||
queueEdge({
|
||||
sourceId: consumerClassId,
|
||||
|
|
@ -222,24 +223,27 @@ export const diPhase: PipelinePhase<DIOutput> = {
|
|||
}
|
||||
|
||||
if (viable.length === 1) {
|
||||
const suffix =
|
||||
candidate.qualifier === undefined ? '' : `; qualifier "${candidate.qualifier}"`;
|
||||
const suffix = namedSelection === undefined ? '' : `; ${namedSelection.reason}`;
|
||||
queueEdge({
|
||||
sourceId: consumerClassId,
|
||||
targetId: viable[0],
|
||||
confidence: candidate.qualifier === undefined ? 0.9 : 0.95,
|
||||
confidence: namedSelection === undefined ? 0.9 : 0.95,
|
||||
reason: candidate.reason + suffix,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const primary = viable.filter((id) => providers.get(id)?.primary === true);
|
||||
if (candidate.qualifier === undefined && primary.length === 1) {
|
||||
const preferred = viable.flatMap((id) => {
|
||||
const reason = providers.get(id)?.preferenceReason;
|
||||
return reason === undefined ? [] : [{ id, reason }];
|
||||
});
|
||||
if (namedSelection === undefined && preferred.length === 1) {
|
||||
const selected = preferred[0];
|
||||
queueEdge({
|
||||
sourceId: consumerClassId,
|
||||
targetId: primary[0],
|
||||
targetId: selected.id,
|
||||
confidence: 0.95,
|
||||
reason: `${candidate.reason}; selected @Primary`,
|
||||
reason: `${candidate.reason}; ${selected.reason}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue