Merge branch 'config-catalog' into catalog-flatten
This commit is contained in:
3 files changed
+58
-8
No files matched your search
@@ -196,6 +196,26 @@ export function apply(ctx: Context, config: Config): void {}
|
||||
`,
|
||||
}))).toThrow(/config type 'Config' is imported from '@fix\/dep'/)
|
||||
})
|
||||
|
||||
it('hard-errors when one name resolves to two different declarations across the closure', () => {
|
||||
expect(() => collectConfigCatalog(make({
|
||||
'src/index.ts': `import type { Context } from 'cordis'
|
||||
import type { A } from './a.ts'
|
||||
import type { B } from './b.ts'
|
||||
/** Fixture config. */
|
||||
export interface Config {
|
||||
/** A. */
|
||||
a?: A
|
||||
/** B. */
|
||||
b?: B
|
||||
}
|
||||
/** Load. */
|
||||
export function apply(ctx: Context, config: Config): void {}
|
||||
`,
|
||||
'src/a.ts': '/** First Option. */\nexport interface Option {\n /** X. */\n x?: string\n}\n/** A. */\nexport interface A {\n /** O. */\n o?: Option\n}\n',
|
||||
'src/b.ts': '/** Second Option. */\nexport interface Option {\n /** Y. */\n y?: string\n}\n/** B. */\nexport interface B {\n /** O. */\n o?: Option\n}\n',
|
||||
}))).toThrow(/type name 'Option' resolves to two different declarations/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('gen-config-catalog schema cross-check', () => {
|
||||
|
||||
@@ -41,7 +41,10 @@
|
||||
* - Every type NAME a pasted declaration references resolves: pasted
|
||||
* transitively when package-local, linked when it is another plugin's
|
||||
* config type / a core-data-structures entry / a workspace or external
|
||||
* import. An unresolvable name is an error, never silently unexplained.
|
||||
* import. An unresolvable name is an error, and so is a NAME COLLISION —
|
||||
* two distinct declarations, or a declaration and an import, sharing one
|
||||
* name across the closure (a verbatim fence has a single flat namespace) —
|
||||
* never a silent skip.
|
||||
* - The runtime schemastery schema (`Config` export or `static Config`),
|
||||
* when present, is walked statically — `z.object` keys, nested object/array
|
||||
* compositions as key PATHS (`agents[].id`), and `z.intersect` composition
|
||||
@@ -700,28 +703,54 @@ export function collectConfigCatalog(scanRoot: string = root): CatalogEntry[] {
|
||||
entry.configTypeName = typeName
|
||||
const pastes: Paste[] = []
|
||||
const refs = new Map<string, TypeRef>()
|
||||
const pastedNames = new Set<string>()
|
||||
// A bare name is the fence's whole namespace: two DIFFERENT declarations
|
||||
// (or a declaration in one file and an import in another) sharing a name
|
||||
// cannot both render unambiguously, so every resolution is identity-checked
|
||||
// by source pointer and a collision is a violation, never a silent skip.
|
||||
const pastedDeclByName = new Map<string, string>()
|
||||
const queue: { name: string; from: FileCtx }[] = [{ name: typeName, from: ctx }]
|
||||
for (let item = queue.shift(); item !== undefined; item = queue.shift()) {
|
||||
const { name, from } = item
|
||||
if (pastedNames.has(name)) continue
|
||||
const resolved = resolveTypeName(from, name, cache, violations)
|
||||
if (resolved === null) {
|
||||
violations.push(`${pkg}: config declaration references '${name}' (via ${from.rel}), which is neither declared in the package, imported, nor a known global type.`)
|
||||
continue
|
||||
}
|
||||
if ('ref' in resolved) {
|
||||
if (name === typeName) violations.push(`${pkg}: config type '${name}' is imported from '${resolved.ref.specifier}'; a plugin's config type must live in its own package.`)
|
||||
else refs.set(name, resolved.ref)
|
||||
if (name === typeName) {
|
||||
violations.push(`${pkg}: config type '${name}' is imported from '${resolved.ref.specifier}'; a plugin's config type must live in its own package.`)
|
||||
continue
|
||||
}
|
||||
if (pastedDeclByName.has(name)) {
|
||||
violations.push(`${pkg}: '${name}' resolves to a package-local declaration (${pastedDeclByName.get(name) ?? ''}) in one file and an import from '${resolved.ref.specifier}' in another; rename one so the fence is unambiguous.`)
|
||||
continue
|
||||
}
|
||||
const existing = refs.get(name)
|
||||
if (existing && (existing.specifier !== resolved.ref.specifier || existing.imported !== resolved.ref.imported)) {
|
||||
violations.push(`${pkg}: '${name}' is imported from both '${existing.specifier}' (${existing.imported}) and '${resolved.ref.specifier}' (${resolved.ref.imported}) across the pasted closure; disambiguate the aliases.`)
|
||||
continue
|
||||
}
|
||||
refs.set(name, resolved.ref)
|
||||
continue
|
||||
}
|
||||
pastedNames.add(name)
|
||||
pastes.push({ text: pasteText(resolved.ctx, resolved.decl), source: pointer(resolved.ctx.rel, resolved.ctx.sf, resolved.decl) })
|
||||
const declKey = pointer(resolved.ctx.rel, resolved.ctx.sf, resolved.decl)
|
||||
const prior = pastedDeclByName.get(name)
|
||||
if (prior === declKey) continue // same declaration reached again — benign
|
||||
if (prior !== undefined) {
|
||||
violations.push(`${pkg}: type name '${name}' resolves to two different declarations (${prior} and ${declKey}) across the pasted closure; rename one — a verbatim fence cannot carry two same-named declarations.`)
|
||||
continue
|
||||
}
|
||||
if (refs.has(name)) {
|
||||
violations.push(`${pkg}: '${name}' resolves to an import from '${refs.get(name)?.specifier ?? ''}' in one file and a package-local declaration (${declKey}) in another; rename one so the fence is unambiguous.`)
|
||||
continue
|
||||
}
|
||||
pastedDeclByName.set(name, declKey)
|
||||
pastes.push({ text: pasteText(resolved.ctx, resolved.decl), source: declKey })
|
||||
checkMemberDocs(resolved.ctx, resolved.decl, violations)
|
||||
const names = new Set<string>()
|
||||
collectTypeNames(resolved.decl, names)
|
||||
for (const n of names) {
|
||||
if (GLOBAL_TYPES.has(n) || pastedNames.has(n)) continue
|
||||
if (GLOBAL_TYPES.has(n)) continue
|
||||
queue.push({ name: n, from: resolved.ctx })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"excluded": [
|
||||
"docs/AGENTS.md",
|
||||
"docs/module-graph.md",
|
||||
"docs/config-catalog.md",
|
||||
"docs/cordis-catalog/",
|
||||
"docs/tool-catalog/",
|
||||
"docs/persistence-catalog/",
|
||||
|
||||
Reference in New Issue
Block a user