xsd-lookup
Version:
Multi-schema XSD lookup utility
1,268 lines (1,133 loc) • 159 kB
text/typescript
import * as fs from 'fs';
import * as path from 'path';
import { pathToFileURL } from 'url';
import { DOMParser } from '@xmldom/xmldom';
interface ElementMapEntry {
parent: string | null;
node: Element;
}
interface ElementWithParent {
name: string;
parent: string | null;
node: Element;
}
interface SchemaIndex {
elements: Record<string, Element[]>;
types: Record<string, Element>;
groups: Record<string, Element>;
attributeGroups: Record<string, Element>;
elementContexts: Record<string, ElementContext[]>; // Track group membership
}
interface HierarchyCache {
attributeCache: WeakMap<Element, Record<string, Element>>;
enhancedAttributesCache: WeakMap<Element, Record<string, EnhancedAttributeInfo>>;
elementDefinitionCache: Map<string, Element | undefined>; // New cache for getElementDefinition
// Performance caches (not size-limited via ensureCacheSize):
elementsInDefinitionCache: WeakMap<Element, Element[]>; // cache of findAllElementsInDefinition
elementsInDefinitionByNameCache: WeakMap<Element, Map<string, Element[]>>; // cache of findElementsInDefinition
contentModelCache?: WeakMap<Element, Element | null>; // cache of findContentModel
annotationCache?: WeakMap<Element, string>; // cache of extractAnnotationText/type fallback
possibleChildrenResultCache: WeakMap<Element, Map<string, Map<string, string>>>; // cache final results per key (Record for low overhead)
// New caches for performance
validChildCache?: WeakMap<Element, Map<string, Map<string, boolean>>>; // parentDef -> prevKey -> childName -> boolean
modelNextNamesCache?: WeakMap<Element, Map<string, Set<string>>>; // contentModel -> prevKey -> allowed names
modelStartNamesCache?: WeakMap<Element, Set<string>>; // contentModel -> start names
containsCache: WeakMap<Element, WeakMap<Element, boolean>>; // item Element -> elementName -> contains?
validationsCache: WeakMap<Element, Partial<EnhancedAttributeInfo>>; // element -> validationName -> isValid
}
type CacheCounter = { hits: number; misses: number; sets: number };
type CacheStats = {
attributeCache: CacheCounter;
enhancedAttributesCache: CacheCounter;
elementDefinitionCache: CacheCounter;
elementsInDefinitionByNameCache: CacheCounter;
elementsInDefinitionCache: CacheCounter;
contentModelCache: CacheCounter;
annotationCache: CacheCounter;
possibleChildrenResultCache: CacheCounter;
validChildCache: CacheCounter;
modelStartNamesCache: CacheCounter;
modelNextNamesCache: CacheCounter;
containsCache: CacheCounter;
validationsCache: CacheCounter;
};
export interface ElementLocation {
uri: string;
line: number;
column: number;
lengthOfStartTag: number;
}
export interface EnhancedAttributeInfo {
name: string;
type?: string;
location?: ElementLocation;
required?: boolean;
enumValues?: string[];
enumValuesAnnotations?: Map<string, string>; // Map of enum value to its annotation text
annotation?: string; // Attribute's own annotation text
patterns?: string[]; // Changed to support multiple patterns
minLength?: number;
maxLength?: number;
minInclusive?: number;
maxInclusive?: number;
minExclusive?: number;
maxExclusive?: number;
}
export interface AttributeValidationResult {
isValid: boolean;
expectedType?: string;
allowedValues?: string[];
errorMessage?: string;
restrictions?: string[];
}
interface ElementContext {
element: Element;
groups: string[]; // Groups this element belongs to
parents: string[]; // Parent element types this can appear in
}
export class Schema {
private doc: Document;
private name: string = '';
private schemaIndex: SchemaIndex;
private elementMap: Record<string, ElementMapEntry[]>;
private cache!: HierarchyCache;
private cacheStats!: CacheStats;
private methodTimings: Record<string, number> = {};
private methodCalls: Record<string, number> = {};
private maxCacheSize: number = 100000;
private shouldProfileCaches: boolean = false;
private shouldProfileMethods: boolean = false;
private static readonly numericTypes = new Set<string>([
'xs:int', 'xs:integer', 'xs:long', 'xs:short', 'xs:byte',
'xs:float', 'xs:double', 'xs:decimal',
'xs:positiveInteger', 'xs:negativeInteger', 'xs:nonPositiveInteger', 'xs:nonNegativeInteger',
'xs:unsignedInt', 'xs:unsignedLong', 'xs:unsignedShort', 'xs:unsignedByte'
]);
private static readonly builtInTypes = new Set<string>([
'string', 'int', 'integer', 'decimal', 'boolean', 'date', 'time', 'dateTime',
'duration', 'float', 'double', 'anyURI', 'QName', 'NOTATION'
]);
constructor(xsdFilePath: string, includeFiles: string[] = []) {
// Initialize caches and metrics first
this.initializeCaches();
this.initializeCacheStats();
this.shouldProfileCaches = ((process.env.XSDL_PROFILE_CACHES || '').trim() === '1');
this.shouldProfileMethods = ((process.env.XSDL_PROFILE_METHODS || '').trim() === '1');
this.name = path.basename(xsdFilePath);
this.doc = this.loadXml(xsdFilePath);
// Merge include files if any
for (const includeFile of includeFiles) {
const includeDoc = this.loadXml(includeFile);
this.mergeXsds(this.doc, includeDoc);
}
// Build indexes
this.schemaIndex = this.indexSchema(this.doc.documentElement as any);
this.elementMap = this.buildElementMap();
}
/**
* Initialize all cache structures with empty maps
*/
private initializeCaches(): void {
this.cache = {
attributeCache: new WeakMap<Element, Record<string, Element>>(),
enhancedAttributesCache: new WeakMap<Element, Record<string, EnhancedAttributeInfo>>(),
elementDefinitionCache: new Map(),
elementsInDefinitionCache: new WeakMap<Element, Element[]>(),
elementsInDefinitionByNameCache: new WeakMap<Element, Map<string, Element[]>>(),
contentModelCache: new WeakMap<Element, Element | null>(),
annotationCache: new WeakMap<Element, string>(),
possibleChildrenResultCache: new WeakMap<Element, Map<string, Map<string, string>>>(),
validChildCache: new WeakMap<Element, Map<string, Map<string, boolean>>>(),
modelNextNamesCache: new WeakMap<Element, Map<string, Set<string>>>(),
modelStartNamesCache: new WeakMap<Element, Set<string>>(),
containsCache: new WeakMap<Element, WeakMap<Element, boolean>>(),
validationsCache: new WeakMap<Element, Partial<EnhancedAttributeInfo>>()
};
}
private initializeCacheStats(): void {
const zero = (): CacheCounter => ({ hits: 0, misses: 0, sets: 0 });
this.cacheStats = {
attributeCache: zero(),
enhancedAttributesCache: zero(),
elementDefinitionCache: zero(),
elementsInDefinitionByNameCache: zero(),
elementsInDefinitionCache: zero(),
contentModelCache: zero(),
annotationCache: zero(),
possibleChildrenResultCache: zero(),
validChildCache: zero(),
modelStartNamesCache: zero(),
modelNextNamesCache: zero(),
containsCache: zero(),
validationsCache: zero(),
};
}
private printCacheStats(): void {
if (!this.shouldProfileCaches) return;
const entries = Object.keys(this.cacheStats) as (keyof CacheStats)[];
const rows = entries
.map((name) => {
const c = this.cacheStats[name];
return { name: String(name), hits: c.hits, misses: c.misses, sets: c.sets };
})
.sort((a, b) => a.name.localeCompare(b.name));
const headers = ['Name', 'Hits', 'Misses', 'Sets'];
const col1W = Math.max(headers[0].length, ...rows.map(r => r.name.length));
const col2W = Math.max(headers[1].length, ...rows.map(r => String(r.hits).length));
const col3W = Math.max(headers[2].length, ...rows.map(r => String(r.misses).length));
const col4W = Math.max(headers[3].length, ...rows.map(r => String(r.sets).length));
const pad = (s: string, w: number) => s + ' '.repeat(Math.max(0, w - s.length));
const lines: string[] = [];
lines.push(`=== XSD-Lookup Cache Stats for "${this.name}" ===`);
lines.push(
pad(headers[0], col1W) + ' ' +
pad(headers[1], col2W) + ' ' +
pad(headers[2], col3W) + ' ' +
pad(headers[3], col4W)
);
lines.push(
'-'.repeat(col1W) + ' ' +
'-'.repeat(col2W) + ' ' +
'-'.repeat(col3W) + ' ' +
'-'.repeat(col4W)
);
for (const r of rows) {
lines.push(
pad(r.name, col1W) + ' ' +
pad(String(r.hits), col2W) + ' ' +
pad(String(r.misses), col3W) + ' ' +
pad(String(r.sets), col4W)
);
}
// eslint-disable-next-line no-console
console.log(lines.join('\n'));
}
// Method timing profiling
private profStart(): number { return (globalThis.performance?.now?.() ?? Date.now()); }
private profEnd(method: string, t0: number): void {
if (!this.shouldProfileMethods) return;
const t1 = (globalThis.performance?.now?.() ?? Date.now());
const dt = t1 - t0;
this.methodTimings[method] = (this.methodTimings[method] || 0) + dt;
this.methodCalls[method] = (this.methodCalls[method] || 0) + 1;
}
private printMethodStats(): void {
if (!this.shouldProfileMethods) return;
const rows = Object.entries(this.methodTimings)
.map(([name, total]) => {
const calls = this.methodCalls[name] || 0;
const avg = calls > 0 ? total / calls : 0;
return { name, total, calls, avg };
})
.sort((a, b) => b.total - a.total);
if (rows.length === 0) return;
const headers = ['Name', 'Total Time (ms)', 'Count Calls', 'Average Time (ms)'];
const col1W = Math.max(headers[0].length, ...rows.map(r => r.name.length));
const col2W = Math.max(headers[1].length, ...rows.map(r => r.total.toFixed(3).length));
const col3W = Math.max(headers[2].length, ...rows.map(r => String(r.calls).length));
const col4W = Math.max(headers[3].length, ...rows.map(r => r.avg.toFixed(3).length));
const pad = (s: string, w: number) => s + ' '.repeat(Math.max(0, w - s.length));
const lines: string[] = [];
lines.push(`=== XSD-Lookup Method Profile for "${this.name}" ===`);
lines.push(
pad(headers[0], col1W) + ' ' +
pad(headers[1], col2W) + ' ' +
pad(headers[2], col3W) + ' ' +
pad(headers[3], col4W)
);
lines.push(
'-'.repeat(col1W) + ' ' +
'-'.repeat(col2W) + ' ' +
'-'.repeat(col3W) + ' ' +
'-'.repeat(col4W)
);
for (const r of rows) {
lines.push(
pad(r.name, col1W) + ' ' +
pad(r.total.toFixed(3), col2W) + ' ' +
pad(String(r.calls), col3W) + ' ' +
pad(r.avg.toFixed(3), col4W)
);
}
// eslint-disable-next-line no-console
console.log(lines.join('\n'));
}
/**
* Clear all caches by reinitializing them
*/
public clearCache(): void {
this.initializeCaches();
}
/**
* Ensure cache sizes don't exceed the maximum limit using simple LRU eviction
*/
private ensureCacheSize(): void {
const __profiling = this.shouldProfileMethods;
const __t0 = __profiling ? this.profStart() : 0;
try {
if (this.cache.elementDefinitionCache.size > this.maxCacheSize) {
const entries = Array.from(this.cache.elementDefinitionCache.entries());
const toKeep = entries.slice(-Math.floor(this.maxCacheSize / 2));
this.cache.elementDefinitionCache.clear();
toKeep.forEach(([key, value]) => this.cache.elementDefinitionCache.set(key, value));
}
} finally {
if (__profiling) {
this.profEnd('ensureCacheSize', __t0);
}
}
}
/**
* Load and parse an XML file into a DOM Document
* @param filePath The path to the XML file to load
* @returns Parsed DOM Document
*/
private loadXml(filePath: string): Document {
const __profiling = this.shouldProfileMethods;
const __t0 = __profiling ? this.profStart() : 0;
try {
const xml = fs.readFileSync(filePath, 'utf8');
const doc = new DOMParser().parseFromString(xml, 'application/xml') as any;
// Pre-split file into lines for faster per-line operations
const lines = xml.split(/\r\n|\r|\n/);
const linesCount = lines.length;
// Annotate all elements in this document with their source file path so callers can resolve origin
try {
const annotate = (node: Node) => {
if (!node) return;
if (node.nodeType === 1) {
const el = node as Element;
// Store as a non-XSD attribute to avoid interfering with schema semantics
if (!el.getAttribute('data-source-file')) {
el.setAttribute('data-source-file', filePath);
}
const anyEl = el as any;
const line: number | undefined = anyEl.lineNumber ?? anyEl.line;
const column: number | undefined = anyEl.columnNumber ?? anyEl.column;
if (typeof line === 'number' && typeof column === 'number' && line >= 1 && line <= linesCount && column >= 1 && !el.getAttribute('start-tag-length')) {
const lineStr = lines[line - 1];
if (column <= lineStr.length && lineStr[column - 1] === '<') {
const closingTagIdx = lineStr.indexOf('>', column - 1);
const length = closingTagIdx >= 0 ? closingTagIdx - (column - 1) + 1 : lineStr.length - (column - 1);
if (typeof closingTagIdx === 'number') {
el.setAttribute('start-tag-length', String(length));
}
}
}
}
// Recurse children
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < (node.childNodes ? node.childNodes.length : 0); i++) {
annotate(node.childNodes[i]);
}
};
if (doc && doc.documentElement) annotate(doc.documentElement);
} catch {
// Best-effort; if annotation fails, we still return the parsed document
}
return doc;
} finally {
if (__profiling) {
this.profEnd('loadXml', __t0);
}
}
}
/**
* Merge included XSD documents into the main schema document
* @param mainDoc The main schema document to merge into
* @param includeDoc The included schema document to merge from
*/
private mergeXsds(mainDoc: Document, includeDoc: Document): void {
const __profiling = this.shouldProfileMethods;
const __t0 = __profiling ? this.profStart() : 0;
try {
const mainSchema = mainDoc.documentElement;
const includeSchema = includeDoc.documentElement;
for (let i = 0; i < includeSchema.childNodes.length; i++) {
const node = includeSchema.childNodes[i];
if (node.nodeType === 1) {
mainSchema.appendChild(node.cloneNode(true));
}
}
} finally {
if (__profiling) {
this.profEnd('mergeXsds', __t0);
}
}
}
/**
* Recursively collect all element definitions from the schema DOM
* @param node The current node to examine
* @param parentName The name of the parent element
* @param elements Array to collect found elements into
* @returns Array of collected elements with their parent information
*/
private collectElements(node: Node, parentName: string | null, elements: ElementWithParent[] = []): ElementWithParent[] {
const __profiling = this.shouldProfileMethods;
const __t0 = __profiling ? this.profStart() : 0;
try {
if (!node) return elements;
if (node.nodeType === 1) {
const element = node as Element;
if (element.localName === 'element' && element.hasAttribute('name')) {
elements.push({
name: element.getAttribute('name')!,
parent: parentName,
node: element
});
}
// Recurse into children
for (let i = 0; i < element.childNodes.length; i++) {
this.collectElements(element.childNodes[i], element.getAttribute('name') || parentName, elements);
}
}
return elements;
} finally {
if (__profiling) {
this.profEnd('collectElements', __t0);
}
}
}
/**
* Build a map of element names to their definitions and parent relationships
* @returns Record mapping element names to arrays of their definitions
*/
private buildElementMap(): Record<string, ElementMapEntry[]> {
const __profiling = this.shouldProfileMethods;
const __t0 = __profiling ? this.profStart() : 0;
try {
const elements = this.collectElements(this.doc.documentElement, null);
const elementMap: Record<string, ElementMapEntry[]> = {};
elements.forEach(e => {
if (!elementMap[e.name]) elementMap[e.name] = [];
elementMap[e.name].push({ parent: e.parent, node: e.node });
});
return elementMap;
} finally {
if (__profiling) {
this.profEnd('buildElementMap', __t0);
}
}
}
/**
* Index the schema by collecting all global elements, groups, attribute groups, and types
* @param root The root schema element
* @param ns The XML Schema namespace prefix
* @returns Complete schema index with all definitions and contexts
*/
private indexSchema(root: Element): SchemaIndex {
const __profiling = this.shouldProfileMethods;
const __t0 = __profiling ? this.profStart() : 0;
try {
const elements: Record<string, Element[]> = {}; // Changed to arrays
const groups: Record<string, Element> = {};
const attributeGroups: Record<string, Element> = {};
const types: Record<string, Element> = {};
// First, collect only direct children of the schema root (truly global elements)
for (let i = 0; i < root.childNodes.length; i++) {
const child = root.childNodes[i];
if (child.nodeType === 1) {
const element = child as Element;
const elementName = element.getAttribute('name');
if (elementName) {
if (element.localName === 'element') {
if (!elements[elementName]) elements[elementName] = [];
elements[elementName].push(element);
}
else if (element.localName === 'group') {
groups[elementName] = element;
}
else if (element.localName === 'attributeGroup') {
attributeGroups[elementName] = element;
}
else if (element.localName === 'complexType') {
types[elementName] = element;
}
else if (element.localName === 'simpleType') {
types[elementName] = element;
}
}
}
}
// Then walk recursively to collect all types, groups, and attribute groups (which can be nested)
const walkForTypesAndGroups = (node: Node): void => {
if (!node || node.nodeType !== 1) return;
const element = node as Element;
const elementName = element.getAttribute('name');
// Only collect types and groups, not nested elements
if (elementName) {
if (element.localName === 'group') {
groups[elementName] = element;
}
if (element.localName === 'attributeGroup') {
attributeGroups[elementName] = element;
}
if (element.localName === 'complexType' && elementName) {
types[elementName] = element;
}
if (element.localName === 'simpleType' && elementName) {
types[elementName] = element;
}
}
// Recurse into children
for (let i = 0; i < element.childNodes.length; i++) {
walkForTypesAndGroups(element.childNodes[i]);
}
};
walkForTypesAndGroups(root);
// Build comprehensive element contexts including group membership
const elementContexts = this.buildElementContexts(elements, groups, types);
return { elements, groups, attributeGroups, types, elementContexts };
} finally {
if (__profiling) this.profEnd('indexSchema', __t0);
}
}
/**
* Build comprehensive element contexts, including elements reachable through groups
*/
private buildElementContexts(
globalElements: Record<string, Element[]>,
groups: Record<string, Element>,
types: Record<string, Element>
): Record<string, ElementContext[]> {
const __profiling = this.shouldProfileMethods;
const __t0 = __profiling ? this.profStart() : 0;
try {
const elementContexts: Record<string, ElementContext[]> = {};
// First, add all global elements as their own contexts
for (const [elementName, elements] of Object.entries(globalElements)) {
if (!elementContexts[elementName]) elementContexts[elementName] = [];
for (const element of elements) {
elementContexts[elementName].push({
element,
groups: [], // Global elements don't belong to groups directly
parents: [] // Will be filled in later when we analyze where they can appear
});
}
}
// Then, traverse all groups to find elements defined within them
for (const [groupName, groupElement] of Object.entries(groups)) {
this.extractElementsFromGroup(groupElement, groupName, elementContexts, groups, types);
}
// IMPORTANT: Also traverse all global elements to find inline element definitions
// This captures cases like param under params, where param is defined inline
// AND handles type references in context (e.g., library element using interrupt_library type)
for (const [elementName, elements] of Object.entries(globalElements)) {
for (const element of elements) {
this.extractInlineElementsFromElement(element, elementContexts, groups, types, [elementName]);
}
}
return elementContexts;
} finally {
if (__profiling) {
this.profEnd('buildElementContexts', __t0);
}
}
}
/**
* Extract all elements from a group and add them to element contexts
*/
private extractElementsFromGroup(
groupElement: Element,
groupName: string,
elementContexts: Record<string, ElementContext[]>,
groups: Record<string, Element>,
types: Record<string, Element>,
visitedGroups: Set<string> = new Set()
): void {
const __profiling = this.shouldProfileMethods;
const __t0 = __profiling ? this.profStart() : 0;
try {
// Prevent infinite recursion in group references
if (visitedGroups.has(groupName)) return;
visitedGroups.add(groupName);
const extractElements = (node: Element, currentGroups: string[]): void => {
if (!node || node.nodeType !== 1) return;
// If this is an element definition, add it to contexts
if (node.localName === 'element' && node.getAttribute('name')) {
const elementName = node.getAttribute('name')!;
if (!elementContexts[elementName]) elementContexts[elementName] = [];
// Check if we already have this exact element in this group context
const existingContext = elementContexts[elementName].find(ctx =>
ctx.element === node &&
JSON.stringify(ctx.groups.sort()) === JSON.stringify(currentGroups.sort())
);
if (!existingContext) {
elementContexts[elementName].push({
element: node,
groups: [...currentGroups],
parents: [] // Will be filled in later
});
}
}
// If this is a group reference, recursively extract from the referenced group
if (node.localName === 'group' && node.getAttribute('ref')) {
const refGroupName = node.getAttribute('ref')!;
const refGroup = groups[refGroupName];
if (refGroup && !visitedGroups.has(refGroupName)) {
this.extractElementsFromGroup(refGroup, refGroupName, elementContexts, groups, types, new Set(visitedGroups));
}
}
// Handle type extensions - extract elements from the base type
if (node.localName === 'extension' && node.getAttribute('base')) {
const baseName = node.getAttribute('base')!;
const baseType = types[baseName];
if (baseType) {
// Extract elements from the base type within the current element's context
// For group elements, we need to find the parent element
let parentElement = node.parentNode;
while (parentElement && parentElement.nodeType === 1) {
const parentElem = parentElement as Element;
if (parentElem.localName === 'element') {
const parentElementName = parentElem.getAttribute('name');
if (parentElementName) {
// Extract elements from the base type with the parent element as context
this.extractElementsFromType(baseType, baseName, elementContexts, groups, types, new Set(), [parentElementName]);
}
break;
}
parentElement = parentElement.parentNode;
}
}
}
// Recursively process all children
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i];
if (child.nodeType === 1) {
extractElements(child as Element, currentGroups);
}
}
};
// Start extraction with the current group in the context
extractElements(groupElement, [groupName]);
} finally {
if (__profiling) {
this.profEnd('extractElementsFromGroup', __t0);
}
}
}
/**
* Extract elements from a group while maintaining parent context information
*/
private extractElementsFromGroupWithParentContext(
groupElement: Element,
groupName: string,
elementContexts: Record<string, ElementContext[]>,
groups: Record<string, Element>,
types: Record<string, Element>,
parentContext: string[],
visitedGroups: Set<string> = new Set()
): void {
const __profiling = this.shouldProfileMethods;
const __t0 = __profiling ? this.profStart() : 0;
try {
// Prevent infinite recursion
if (visitedGroups.has(groupName)) return;
visitedGroups.add(groupName);
const extractElements = (node: Element): void => {
if (!node || node.nodeType !== 1) return;
// If this is an element definition, add it to contexts with group and parent info
if (node.localName === 'element' && node.getAttribute('name')) {
const elementName = node.getAttribute('name')!;
if (!elementContexts[elementName]) elementContexts[elementName] = [];
// Add context with both group membership and parent information
elementContexts[elementName].push({
element: node,
groups: [groupName],
parents: [...parentContext] // Use the passed parent context
});
}
// If this is a group reference, recursively extract
if (node.localName === 'group' && node.getAttribute('ref')) {
const refGroupName = node.getAttribute('ref')!;
const refGroup = groups[refGroupName];
if (refGroup && !visitedGroups.has(refGroupName)) {
this.extractElementsFromGroupWithParentContext(refGroup, refGroupName, elementContexts, groups, types, parentContext, new Set(visitedGroups));
}
}
// Handle type extensions - extract elements from the base type
if (node.localName === 'extension' && node.getAttribute('base')) {
const baseName = node.getAttribute('base')!;
const baseType = types[baseName];
if (baseType) {
// Extract elements from the base type within the current element's context
// For group elements with parent context, we need to find the parent element
let parentElement = node.parentNode;
while (parentElement && parentElement.nodeType === 1) {
const parentElem = parentElement as Element;
if (parentElem.localName === 'element') {
const parentElementName = parentElem.getAttribute('name');
if (parentElementName) {
// Extract elements from the base type with the parent element as context
this.extractElementsFromType(baseType, baseName, elementContexts, groups, types, new Set(), [parentElementName, ...parentContext]);
}
break;
}
parentElement = parentElement.parentNode;
}
}
}
// Recursively process all children
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i];
if (child.nodeType === 1) {
extractElements(child as Element);
}
}
};
extractElements(groupElement);
} finally {
if (__profiling) {
this.profEnd('extractElementsFromGroupWithParentContext', __t0);
}
}
}
/**
* Extract all elements from a complex type and add them to element contexts
*/
private extractElementsFromType(
typeElement: Element,
typeName: string,
elementContexts: Record<string, ElementContext[]>,
groups: Record<string, Element>,
types: Record<string, Element>,
visitedTypes: Set<string> = new Set(),
parentElementNames: string[] = []
): void {
const __profiling = this.shouldProfileMethods;
const __t0 = __profiling ? this.profStart() : 0;
try {
// Prevent infinite recursion in type references
if (visitedTypes.has(typeName)) return;
visitedTypes.add(typeName);
// Use the provided parent element names instead of the type name
const currentParents = parentElementNames.length > 0 ? parentElementNames : [typeName];
const extractElements = (node: Element, currentParents: string[]): void => {
if (!node || node.nodeType !== 1) return;
// If this is an element definition, add it to contexts
if (node.localName === 'element' && node.getAttribute('name')) {
const elementName = node.getAttribute('name')!;
if (!elementContexts[elementName]) elementContexts[elementName] = [];
// Check if we already have this exact element in this parent context
const existingContext = elementContexts[elementName].find(ctx =>
ctx.element === node &&
JSON.stringify(ctx.parents.sort()) === JSON.stringify(currentParents.sort())
);
if (!existingContext) {
elementContexts[elementName].push({
element: node,
groups: [], // Will be filled if this element is found through group references
parents: [...currentParents]
});
// Check if this element also has a type reference - if so, extract elements from that type
const typeAttr = node.getAttribute('type');
if (typeAttr && types[typeAttr] && !visitedTypes.has(typeAttr)) {
// Extract elements from the referenced type with this element as parent
this.extractElementsFromType(types[typeAttr], typeAttr, elementContexts, groups, types, new Set(), [elementName, ...currentParents]);
}
}
}
// If this is a group reference, extract elements from the group and mark with group membership
if (node.localName === 'group' && node.getAttribute('ref')) {
const refGroupName = node.getAttribute('ref')!;
const refGroup = groups[refGroupName];
if (refGroup) {
// Extract elements from the group and mark them with group membership
// Only pass the immediate parent, not the full chain
const immediateParent = currentParents.length > 0 ? [currentParents[0]] : [];
this.extractElementsFromGroupWithParentContext(refGroup, refGroupName, elementContexts, groups, types, immediateParent);
}
}
// Handle type extensions - extract elements from the base type
if (node.localName === 'extension' && node.getAttribute('base')) {
const baseName = node.getAttribute('base')!;
const baseType = types[baseName];
if (baseType && !visitedTypes.has(baseName)) {
// Extract elements from the base type with the same parent context
this.extractElementsFromType(baseType, baseName, elementContexts, groups, types, new Set([...visitedTypes, baseName]), currentParents);
}
}
// Recursively process all children
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i];
if (child.nodeType === 1) {
extractElements(child as Element, currentParents);
}
}
};
// Start extraction with the current parent element names
extractElements(typeElement, currentParents);
} finally {
if (__profiling) {
this.profEnd('extractElementsFromType', __t0);
}
}
}
/**
* Extract inline elements from a global element definition
* This captures elements like param under params that are defined inline
*/
private extractInlineElementsFromElement(
parentElement: Element,
elementContexts: Record<string, ElementContext[]>,
groups: Record<string, Element>,
types: Record<string, Element>,
initialParents: string[] = []
): void {
const __profiling = this.shouldProfileMethods;
const __t0 = __profiling ? this.profStart() : 0;
try {
const extractInlineElements = (node: Element, currentParents: string[], isRootElement: boolean = false): void => {
if (!node || node.nodeType !== 1) return;
// If this is an inline element definition, add it to contexts
if (node.localName === 'element' && node.getAttribute('name')) {
const elementName = node.getAttribute('name')!;
if (!elementContexts[elementName]) elementContexts[elementName] = [];
// Only add to contexts if this is not the root element we started with
if (!isRootElement) {
// Add this inline element with its parent context
elementContexts[elementName].push({
element: node,
groups: [], // Inline elements don't belong to groups directly
parents: [...currentParents]
});
}
// IMPORTANT: When we find an element, it becomes a potential parent for nested elements
// Continue recursion with this element added to the parent chain
// BUT: Don't add the root element to its own parent chain
const newParents = isRootElement ? currentParents : [elementName, ...currentParents];
// Check if this element has a type reference - if so, extract elements from that type
const typeAttr = node.getAttribute('type');
if (typeAttr && types[typeAttr]) {
// Extract elements from the referenced type with this element as parent
this.extractElementsFromType(types[typeAttr], typeAttr, elementContexts, groups, types, new Set(), [elementName]);
}
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i];
if (child.nodeType === 1) {
extractInlineElements(child as Element, newParents, false);
}
}
return; // Don't process children again with the old parent chain
}
// If this is a group reference, extract elements from the group
if (node.localName === 'group' && node.getAttribute('ref')) {
const refGroupName = node.getAttribute('ref')!;
const refGroup = groups[refGroupName];
if (refGroup) {
this.extractElementsFromGroupWithParentContext(refGroup, refGroupName, elementContexts, groups, types, currentParents);
}
}
// Handle type extensions - extract elements from the base type
if (node.localName === 'extension' && node.getAttribute('base')) {
const baseName = node.getAttribute('base')!;
const baseType = types[baseName];
if (baseType) {
// Extract elements from the base type with the same parent context
this.extractElementsFromType(baseType, baseName, elementContexts, groups, types, new Set(), currentParents);
}
}
// Recursively process all children with the same parent context
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i];
if (child.nodeType === 1) {
extractInlineElements(child as Element, currentParents, false);
}
}
};
// Start extraction with the initial parents as context, marking the root element
extractInlineElements(parentElement, initialParents, true);
} finally {
if (__profiling) {
this.profEnd('extractInlineElements', __t0);
}
}
}
public getElementDefinition(elementName: string, hierarchy: string[] = []): Element | undefined {
const __profiling = this.shouldProfileMethods;
const __t0 = __profiling ? this.profStart() : 0;
try {
// Create cache key from element name and hierarchy
const hierarchyKey = hierarchy.length > 0 ? hierarchy.join('|') : '';
const fullCacheKey = `${elementName}::${hierarchyKey}`;
// Check if we have an exact match in cache
if (this.cache.elementDefinitionCache.has(fullCacheKey)) {
if (this.shouldProfileCaches) this.cacheStats.elementDefinitionCache.hits++;
const cached = this.cache.elementDefinitionCache.get(fullCacheKey);
return cached;
}
if (this.shouldProfileCaches) this.cacheStats.elementDefinitionCache.misses++;
const elementsFromMap = this.elementMap[elementName];
if (elementsFromMap && elementsFromMap.length === 1) {
const elementFromMap = elementsFromMap[0].node;
this.cache.elementDefinitionCache.set(fullCacheKey, elementFromMap);
if (this.shouldProfileCaches) this.cacheStats.elementDefinitionCache.sets++;
this.ensureCacheSize();
return elementFromMap;
}
// Check for partial matches in cache - look for any cached key that starts with our element name
// and has a hierarchy that our current hierarchy extends
if (hierarchy.length > 0) {
let keyPrefix = '';
for (const segment of hierarchy) {
keyPrefix += `|${segment}`;
const fullKey = `${elementName}::${keyPrefix}`;
const cachedElement = this.cache.elementDefinitionCache.get(fullKey);
if (cachedElement) {
if (this.shouldProfileCaches) this.cacheStats.elementDefinitionCache.hits++;
this.cache.elementDefinitionCache.set(fullCacheKey, cachedElement);
if (this.shouldProfileCaches) this.cacheStats.elementDefinitionCache.sets++;
this.ensureCacheSize();
return cachedElement;
}
}
}
// No cache hit, perform the actual search
let result: Element | undefined;
// Step 1: If no hierarchy provided, only return global elements
if (hierarchy.length === 0) {
// Look for global elements (direct children of schema root)
const globalElements = this.getGlobalElementDefinitions(elementName);
result = globalElements.length > 0 ? globalElements[0] : undefined;
} else {
// Step 2: INCREMENTAL HIERARCHY APPROACH
// hierarchy = [immediate_parent, grandparent, great_grandparent, ...]
// Try each level incrementally: bottom-up expansion, then top-down search
for (let level = 1; level <= hierarchy.length; level++) {
// Take the first 'level' elements from hierarchy (bottom-up expansion)
const currentHierarchy = hierarchy.slice(0, level);
// Reverse for top-down search: [great_grandparent, ..., grandparent, immediate_parent]
const topDownHierarchy = [...currentHierarchy].reverse();
// Try to find element with this hierarchy level using top-down search
const foundElement = this.findElementTopDown(elementName, topDownHierarchy);
if (foundElement) {
// Found a definition at this level
result = foundElement;
// Cache this intermediate result too (it might be useful for future lookups)
const intermediateCacheKey = `${elementName}::${currentHierarchy.join('|')}`;
this.cache.elementDefinitionCache.set(intermediateCacheKey, result);
if (this.shouldProfileCaches) this.cacheStats.elementDefinitionCache.sets++;
break; // Exit the loop since we found a match
}
}
}
// Cache the final result (even if undefined)
this.cache.elementDefinitionCache.set(fullCacheKey, result);
if (this.shouldProfileCaches) this.cacheStats.elementDefinitionCache.sets++;
this.ensureCacheSize();
return result;
} finally {
if (__profiling) this.profEnd('getElementDefinition', __t0);
}
}
/**
* Get global element definitions by name (direct children of schema root only)
* @param elementName The name of the element to find
* @returns Array of global element definitions
*/
private getGlobalElementDefinitions(elementName: string): Element[] {
const __profiling = this.shouldProfileMethods;
const __t0 = __profiling ? this.profStart() : 0;
try {
// Only return truly global elements (direct children of schema root)
if (this.schemaIndex.elements[elementName]) {
return this.schemaIndex.elements[elementName];
}
// Do NOT fall back to elementMap as it contains nested elements too
return [];
} finally {
if (__profiling) {
this.profEnd('getGlobalElementDefinitions', __t0);
}
}
}
/**
* Get global element or type definitions by name for hierarchical search
* @param name The name to search for
* @returns Array of element or type definitions matching the name
*/
private getGlobalElementOrTypeDefs(name: string): Element[] {
const __profiling = this.shouldProfileMethods;
const __t0 = __profiling ? this.profStart() : 0;
try {
const defs: Element[] = [];
const seenNodes = new Set<Element>();
// Check global elements
if (this.schemaIndex.elements[name]) {
for (const element of this.schemaIndex.elements[name]) {
if (!seenNodes.has(element)) {
defs.push(element);
seenNodes.add(element);
}
}
}
// Check global types
if (this.schemaIndex.types[name]) {
const typeElement = this.schemaIndex.types[name];
if (!seenNodes.has(typeElement)) {
defs.push(typeElement);
seenNodes.add(typeElement);
}
}
// For hierarchical search, we also need to include elements from elementMap
// but ONLY when this method is called from hierarchical search context
// The elementMap contains elements that may be reachable through hierarchy
const mapDefs = this.elementMap[name] || [];
for (const mapDef of mapDefs) {
if (!seenNodes.has(mapDef.node)) {
defs.push(mapDef.node);
seenNodes.add(mapDef.node);
}
}
return defs;
} finally {
if (__profiling) {
this.profEnd('getGlobalElementOrTypeDefs', __t0);
}
}
}
/**
* Find element definitions within a parent definition by element name
* @param parentDef The parent element or type definition to search in
* @param elementName The name of the element to find
* @returns Array of matching element definitions
*/
private findElementsInDefinition(parentDef: Element, elementName: string): Element[] {
const __profiling = this.shouldProfileMethods;
const __t0 = __profiling ? this.profStart() : 0;
try {
if (!parentDef) return [];
const cache = this.cache.elementsInDefinitionByNameCache;
let cached: Map<string, Element[]>;
if (cache.has(parentDef)) {
cached = cache.get(parentDef)!;
if (cached && cached.has(elementName)) {
if (this.shouldProfileCaches) this.cacheStats.elementsInDefinitionByNameCache.hits++;
return cached.get(elementName)!;
}
} else {
cached = new Map();
cache.set(parentDef, cached);
}
if (this.shouldProfileCaches) this.cacheStats.elementsInDefinitionByNameCache.misses++;
const results: Element[] = [];
let maxSearchDepth = 0;
// Get the actual type definition to search in
let typeNode = parentDef;
// If parentDef is an element, get its type
if (parentDef.localName === 'element') {
const typeName = parentDef.getAttribute('type');
if (typeName && this.schemaIndex.types[typeName]) {
typeNode = this.schemaIndex.types[typeName];
} else {
// Look for inline complexType
for (let i = 0; i < parentDef.childNodes.length; i++) {
const child = parentDef.childNodes[i];
if (child.nodeType === 1 && (child as Element).localName === 'complexType') {
typeNode = child as Element;
break;
}
}
}
}
// Search recursively through the type definition with optimizations
const visited = new Set<Element>();
const searchInNode = (node: Element, depth: number = 0): void => {
if (!node || node.nodeType !== 1) return;
// Track maximum search depth for metrics
maxSearchDepth = Math.max(maxSearchDepth, depth);
// Depth limit for performance - prevent excessive recursion
if (depth > 20) return;
// Use the actual DOM node reference for cycle detection to avoid false positives
// This ensures we only skip when we encounter the exact same node again
if (visited.has(node)) return;
visited.add(node);
// Check if this is the element we're looking for
if (node.localName === 'element' && node.getAttribute('name') === elementName) {
results.push(node);
return; // Don't recurse into found elements - early exit optimization
}
// Early exit optimization: if we've found enough results, stop searching
if (results.length >= 3) return;
// Handle type references and extensions
if (node.localName === 'extension' && node.getAttribute('base')) {
const baseName = node.getAttribute('base')!;
const baseType = this.schemaIndex.types[baseName];
if (baseType) {
searchInNode(baseType, depth + 1);
}
}
// Handle group references
if (node.localName === 'group' && node.getAttribute('ref')) {
const refName = node.getAttribute('ref')!;
const groupDef = this.schemaIndex.groups[refName];
if (groupDef) {
searchInNode(groupDef, depth + 1);
}
}
// Handle structural elements - recurse into ALL children
if (node.localName === 'sequence' ||
node.localName === 'choice' ||
node.localName === 'all' ||
node.localName === 'complexType' ||
node.localName === 'complexContent' ||
node.localName === 'simpleContent' ||
node.localName === 'group') {
// For structural nodes, recursively search all children
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i];
if (child.nodeType === 1) {
searchInNode(child as Element, depth + 1);
// Early exit if we found enough results
if (results.length >= 3) break;
}
}
}
};
searchInNode(typeNode);
if (this.shouldProfileCaches) this.cacheStats.elementsInDefinitionByNameCache.sets++;
cached.set(elementName, results);
return results;
} finally {
if (__profiling) {
this.profEnd('findElementsInDefinition', __t0);
}
}
}
/**
* Find ALL immediate child elements within a parent definition (without filtering by name)
* This is similar to findElementsInDefinition but returns all direct child elements
* @param parentDef The parent element or type definition to search in
* @returns Array of all immediate child element definitions
*/
private findAllElementsInDefinition(parentDef: Element): Element[] {
const __profiling = this.shouldProfileMethods;
const __t0 = __profiling ? this.profStart() : 0;
try {
if (!parentDef) return [];
const cache = this.cache.elementsInDefinitionCache;
const cached = cache.get(parentDef);
if (cached) {
if (this.shouldProfileCaches) this.cacheStats.elementsInDefinitionCache.hits++;
return cached;
}
if (this.shouldProfileCaches) this.cacheStats.elementsInDefinitionCache.misses++;
const results: Element[] = [];
// Get the actual type definition to search in
let typeNode = parentDef;
// If parentDef is an element, get its type
if (parentDef.localName === 'element') {
const typeName = parentDef.getAttribute('type');
if (typeName && this.schemaIndex.types[typeName]) {
typeNode = this.schemaIndex.types[typeName];
} else {
// Look for inline complexType
for (let i = 0; i < parentDef.childNodes.length; i++) {
const child = parentDef.childNodes[i];
if (child.nodeType === 1 && (child as Element).localName === 'complexType') {
typeNode = child as Element;
break;
}
}
}
}