UNPKG

xsd-lookup

Version:
1,276 lines (1,104 loc) 104 kB
import * as fs from 'fs'; import * as path from 'path'; 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 { hierarchyLookups: Map<string, Element | null>; definitionReachability: Map<string, boolean>; elementSearchResults: Map<string, Element[]>; attributeCache: Map<string, AttributeInfo[]>; hierarchyValidation: Map<string, boolean>; elementDefinitionCache: Map<string, Element | undefined>; // New cache for getElementDefinition } export interface AttributeInfo { name: string; node: Element; } export interface EnhancedAttributeInfo { name: string; type?: string; 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 schemaIndex: SchemaIndex; private elementMap: Record<string, ElementMapEntry[]>; private elementContexts: Record<string, ElementContext[]>; private cache!: HierarchyCache; private maxCacheSize: number = 10000; constructor(xsdFilePath: string, includeFiles: string[] = []) { // Initialize caches and metrics first this.initializeCaches(); 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(); this.elementContexts = this.schemaIndex.elementContexts; } /** * Initialize all cache structures with empty maps */ private initializeCaches(): void { this.cache = { hierarchyLookups: new Map(), definitionReachability: new Map(), elementSearchResults: new Map(), attributeCache: new Map(), hierarchyValidation: new Map(), elementDefinitionCache: new Map() }; } /** * 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 { if (this.cache.hierarchyLookups.size > this.maxCacheSize) { // Simple LRU: clear oldest half const entries = Array.from(this.cache.hierarchyLookups.entries()); const toKeep = entries.slice(-Math.floor(this.maxCacheSize / 2)); this.cache.hierarchyLookups.clear(); toKeep.forEach(([key, value]) => this.cache.hierarchyLookups.set(key, value)); } // Apply same logic to other caches if (this.cache.definitionReachability.size > this.maxCacheSize) { const entries = Array.from(this.cache.definitionReachability.entries()); const toKeep = entries.slice(-Math.floor(this.maxCacheSize / 2)); this.cache.definitionReachability.clear(); toKeep.forEach(([key, value]) => this.cache.definitionReachability.set(key, value)); } if (this.cache.elementSearchResults.size > this.maxCacheSize) { const entries = Array.from(this.cache.elementSearchResults.entries()); const toKeep = entries.slice(-Math.floor(this.maxCacheSize / 2)); this.cache.elementSearchResults.clear(); toKeep.forEach(([key, value]) => this.cache.elementSearchResults.set(key, value)); } if (this.cache.attributeCache.size > this.maxCacheSize) { const entries = Array.from(this.cache.attributeCache.entries()); const toKeep = entries.slice(-Math.floor(this.maxCacheSize / 2)); this.cache.attributeCache.clear(); toKeep.forEach(([key, value]) => this.cache.attributeCache.set(key, value)); } 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)); } } /** * 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 xml = fs.readFileSync(filePath, 'utf8'); return new DOMParser().parseFromString(xml, 'application/xml') as any; } /** * 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 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)); } } } /** * 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 * @param ns The XML Schema namespace prefix * @returns Array of collected elements with their parent information */ private collectElements(node: Node, parentName: string | null, elements: ElementWithParent[] = [], ns: string = 'xs:'): ElementWithParent[] { if (!node) return elements; if (node.nodeType === 1) { const element = node as Element; if (element.nodeName === ns + 'element' && element.getAttribute('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, ns); } } return elements; } /** * 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 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; } /** * 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, ns: string = 'xs:'): SchemaIndex { 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; if (element.nodeName === ns + 'element' && element.getAttribute('name')) { const name = element.getAttribute('name')!; if (!elements[name]) elements[name] = []; elements[name].push(element); } else if (element.nodeName === ns + 'group' && element.getAttribute('name')) { groups[element.getAttribute('name')!] = element; } else if (element.nodeName === ns + 'attributeGroup' && element.getAttribute('name')) { attributeGroups[element.getAttribute('name')!] = element; } else if (element.nodeName === ns + 'complexType' && element.getAttribute('name')) { types[element.getAttribute('name')!] = element; } else if (element.nodeName === ns + 'simpleType' && element.getAttribute('name')) { types[element.getAttribute('name')!] = 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; // Only collect types and groups, not nested elements if (element.nodeName === ns + 'group' && element.getAttribute('name')) { groups[element.getAttribute('name')!] = element; } if (element.nodeName === ns + 'attributeGroup' && element.getAttribute('name')) { attributeGroups[element.getAttribute('name')!] = element; } if (element.nodeName === ns + 'complexType' && element.getAttribute('name')) { types[element.getAttribute('name')!] = element; } if (element.nodeName === ns + 'simpleType' && element.getAttribute('name')) { types[element.getAttribute('name')!] = 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 }; } /** * 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 elementContexts: Record<string, ElementContext[]> = {}; const ns = 'xs:'; // Build type-to-element mapping const typeToElements = this.buildTypeToElementMapping(globalElements, types); // 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, ns); } // 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, elementName, elementContexts, groups, types, ns, [elementName]); } } return elementContexts; } /** * 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>, ns: string, visitedGroups: Set<string> = new Set() ): void { // 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.nodeName === ns + '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.nodeName === ns + '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, ns, new Set(visitedGroups)); } } // Handle type extensions - extract elements from the base type if (node.nodeName === ns + '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.nodeName === ns + '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, ns, 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]); } /** * 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>, ns: string, parentContext: string[], visitedGroups: Set<string> = new Set() ): void { // 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.nodeName === ns + '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.nodeName === ns + '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, ns, parentContext, new Set(visitedGroups)); } } // Handle type extensions - extract elements from the base type if (node.nodeName === ns + '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.nodeName === ns + '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, ns, 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); } /** * 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>, ns: string, visitedTypes: Set<string> = new Set(), parentElementNames: string[] = [] ): void { // 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.nodeName === ns + '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, ns, new Set(), [elementName, ...currentParents]); } } } // If this is a group reference, extract elements from the group and mark with group membership if (node.nodeName === ns + '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, ns, immediateParent); } } // Handle type extensions - extract elements from the base type if (node.nodeName === ns + '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, ns, 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); } /** * Extract inline elements from a global element definition * This captures elements like param under params that are defined inline */ private extractInlineElementsFromElement( parentElement: Element, parentElementName: string, elementContexts: Record<string, ElementContext[]>, groups: Record<string, Element>, types: Record<string, Element>, ns: string, initialParents: string[] = [] ): void { 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.nodeName === ns + '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, ns, 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.nodeName === ns + 'group' && node.getAttribute('ref')) { const refGroupName = node.getAttribute('ref')!; const refGroup = groups[refGroupName]; if (refGroup) { this.extractElementsFromGroupWithParentContext(refGroup, refGroupName, elementContexts, groups, types, ns, currentParents); } } // Handle type extensions - extract elements from the base type if (node.nodeName === ns + '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, ns, 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); } public getElementDefinition(elementName: string, hierarchy: string[] = []): Element | undefined { // 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)) { return this.cache.elementDefinitionCache.get(fullCacheKey); } // 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) { for (const [cachedKey, cachedElement] of this.cache.elementDefinitionCache) { if (cachedKey.startsWith(`${elementName}::`)) { // Extract the cached hierarchy const cachedHierarchyStr = cachedKey.substring(`${elementName}::`.length); if (cachedHierarchyStr === '') continue; // Skip global element cache entries const cachedHierarchy = cachedHierarchyStr.split('|'); // Check if the current hierarchy starts with the cached hierarchy // If cached: [parent, grandparent] and current: [parent, grandparent, great-grandparent] // then we can use the cached result since it's a more specific match if (cachedHierarchy.length <= hierarchy.length) { let isMatch = true; for (let i = 0; i < cachedHierarchy.length; i++) { if (cachedHierarchy[i] !== hierarchy[i]) { isMatch = false; break; } } if (isMatch && cachedElement) { // Found a matching cached result for a shorter hierarchy // Cache this result for the current full hierarchy too this.cache.elementDefinitionCache.set(fullCacheKey, cachedElement); 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); break; // Exit the loop since we found a match } } } // Cache the final result (even if undefined) this.cache.elementDefinitionCache.set(fullCacheKey, result); this.ensureCacheSize(); return result; } /** * 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[] { // 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 []; } /** * 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 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; } /** * 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[] { if (!parentDef) return []; const ns = 'xs:'; 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.nodeName === ns + '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).nodeName === ns + '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.nodeName === ns + '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.nodeName === ns + '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.nodeName === ns + '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.nodeName === ns + 'sequence' || node.nodeName === ns + 'choice' || node.nodeName === ns + 'all' || node.nodeName === ns + 'complexType' || node.nodeName === ns + 'complexContent' || node.nodeName === ns + 'simpleContent' || node.nodeName === ns + '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); return results; } /** * 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[] { if (!parentDef) return []; const ns = 'xs:'; const results: Element[] = []; // Get the actual type definition to search in let typeNode = parentDef; // If parentDef is an element, get its type if (parentDef.nodeName === ns + '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).nodeName === ns + 'complexType') { typeNode = child as Element; break; } } } } // Search for IMMEDIATE child elements (limited depth for performance) const visited = new Set<Element>(); const searchInNode = (node: Element, depth: number = 0): void => { if (!node || node.nodeType !== 1) return; // Depth limit for performance - prevent excessive recursion if (depth > 20) { return; } // Use the actual DOM node reference for cycle detection if (visited.has(node)) return; visited.add(node); // If this is an element definition, add it to results if (node.nodeName === ns + 'element' && node.getAttribute('name')) { results.push(node); return; // Don't recurse into found elements - we only want immediate children } // Handle type references and extensions if (node.nodeName === ns + '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.nodeName === ns + '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.nodeName === ns + 'sequence' || node.nodeName === ns + 'choice' || node.nodeName === ns + 'all' || node.nodeName === ns + 'complexType' || node.nodeName === ns + 'complexContent' || node.nodeName === ns + 'simpleContent' || node.nodeName === ns + '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); } } } }; searchInNode(typeNode, 0); return results; } /** * Get enhanced attribute information including type and validation details */ public getElementAttributes(elementName: string, hierarchy: string[] = []): AttributeInfo[] { // STRICT HIERARCHY RULE: If hierarchy provided, only search in hierarchy context // Never fall back to global elements when hierarchy is specified // If no hierarchy provided, only search global elements if (hierarchy.length === 0) { return this.getElementAttributesWithHierarchy(elementName, []); } // Step 1: Use progressive hierarchy search to find attributes // Stop as soon as we find any attributes at any depth // for (let contextDepth = 1; contextDepth <= hierarchy.length; contextDepth++) { // // Take the first contextDepth elements (immediate context) // const currentContext = hierarchy.slice(0, contextDepth); // const contextAttrs = this.getElementAttributesWithHierarchy(elementName, currentContext); const contextAttrs = this.getElementAttributesWithHierarchy(elementName, hierarchy); if (contextAttrs.length > 0) { // Found attributes at this depth - return them immediately return contextAttrs; } // } // No attributes found at any depth - do NOT fall back to global search return []; } /** * Get element attributes with hierarchy context for internal use * @param elementName The element name to get attributes for * @param hierarchy The element hierarchy in bottom-up order * @returns Array of attribute information */ private getElementAttributesWithHierarchy(elementName: string, hierarchy: string[]): AttributeInfo[] { // Create cache key const cacheKey = `attrs:${elementName}:${hierarchy.join('>')}`; // Check cache first if (this.cache.attributeCache.has(cacheKey)) { return this.cache.attributeCache.get(cacheKey)!; } const attributes: Record<string, Element> = {}; // Get the correct element definition based on hierarchical context const def = this.getElementDefinition(elementName, hierarchy); if (!def) { // Cache empty result const emptyResult: AttributeInfo[] = []; this.cache.attributeCache.set(cacheKey, emptyResult); this.ensureCacheSize(); return emptyResult; } // Use the definition const bestDef = def; // Collect attributes from the element definition this.collectAttrs(bestDef, attributes); const result = Object.entries(attributes).map(([name, node]) => ({ name, node })); // Cache the result this.cache.attributeCache.set(cacheKey, result); this.ensureCacheSize(); return result; } /** * Recursively collect attributes from element and type definitions * @param node The current node to collect attributes from * @param attributes Record to accumulate found attributes * @param visited Set to track visited nodes and prevent infinite recursion */ private collectAttrs(node: Element, attributes: Record<string, Element>, visited: Set<string> = new Set()): void { if (!node || node.nodeType !== 1) return; const ns = 'xs:'; // Use a unique key for types/groups to avoid infinite recursion let key: string | null = null; if (node.nodeName === ns + 'complexType' && node.getAttribute('name')) { key = 'type:' + node.getAttribute('name'); } else if (node.nodeName === ns + 'group' && node.getAttribute('name')) { key = 'group:' + node.getAttribute('name'); } else if (node.nodeName === ns + 'attributeGroup' && node.getAttribute('name')) { key = 'attrgroup:' + node.getAttribute('name'); } else if (node.nodeName === ns + 'attributeGroup' && node.getAttribute('ref')) { key = 'attrgroupref:' + node.getAttribute('ref'); } if (key && visited.has(key)) return; if (key) visited.add(key); // Handle different node types if (node.nodeName === ns + 'attribute') { const name = node.getAttribute('name'); if (name) { attributes[name] = node; } } else if (node.nodeName === ns + 'attributeGroup' && node.getAttribute('ref')) { // Attribute group reference - resolve the reference const refName = node.getAttribute('ref')!; const group = this.schemaIndex.attributeGroups[refName]; if (group) { this.collectAttrs(group, attributes, visited); } } else if (node.nodeName === ns + 'attributeGroup' && node.getAttribute('name')) { // Named attribute group definition - process its children for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeType === 1) { this.collectAttrs(child as Element, attributes, visited); } } } else if (node.nodeName === ns + 'extension' && node.getAttribute('base')) { // Type extension - inherit from base and add own attributes const baseName = node.getAttribute('base')!; const base = this.schemaIndex.types[baseName]; if (base) { this.collectAttrs(base, attributes, visited); } // Also process the extension's own attributes and attribute groups for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeType === 1) { this.collectAttrs(child as Element, attributes, visited); } } } else if (node.nodeName === ns + 'complexContent' || node.nodeName === ns + 'simpleContent') { // Content wrapper - process children for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeType === 1) { this.collectAttrs(child as Element, attributes, visited); } } } else if (node.nodeName === ns + 'complexType' || node.nodeName === ns + 'sequence' || node.nodeName === ns + 'choice' || node.nodeName === ns + 'all') { // Structural nodes - traverse children but skip nested element definitions for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeType === 1 && (child as Element).nodeName !== ns + 'element') { this.collectAttrs(child as Element, attributes, visited); } } } // Handle type reference const typeName = node.getAttribute('type'); if (typeName && this.schemaIndex.types[typeName]) { this.collectAttrs(this.schemaIndex.types[typeName], attributes, visited); } // Handle inline complexType for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeType === 1 && (child as Element).nodeName === ns + 'complexType') { this.collectAttrs(child as Element, attributes, visited); break; } } } /** * Get enhanced attribute information including type and validation details */ public getElementAttributesWithTypes(elementName: string, hierarchy: string[] = []): EnhancedAttributeInfo[] { const attributes = this.getElementAttributes(elementName, hierarchy); // Enhance each attribute with type information return attributes.map(attr => { const enhancedAttr: EnhancedAttributeInfo = { name: attr.name, type: attr.node.getAttribute('type') || undefined, required: attr.node.getAttribute('use') === 'required' }; // Extract attribute's own annotation const annotation = Schema.extractAnnotationText(attr.node); if (annotation) { enhancedAttr.annotation = annotation; } // If the attribute has a type reference, get comprehensive validation information if (enhancedAttr.type) { const typeValidation = this.getTypeValidationInfo(enhancedAttr.type); Object.assign(enhancedAttr, typeValidation); } else { // Check for inline type definition (xs:simpleType) const inlineTypeValidation = this.getInlineTypeValidationInfo(attr.node); Object.assign(enhancedAttr, inlineTypeValidation); // If we found inline enumeration values, set the type to indicate it's an enumeration if (inlineTypeValidation.enumValues && inlineTypeValidation.enumValues.length > 0) { enhancedAttr.type = 'enumeration'; } } return enhancedAttr; }); } /** * Get comprehensive validation information for a type */ private getTypeValidationInfo(typeName: string): Partial<EnhancedAttributeInfo> { const typeNode = this.schemaIndex.types[typeName]; if (!typeNode) return {}; const validationInfo: Partial<EnhancedAttributeInfo> = {}; const ns = 'xs:'; const extractValidationRules = (node: Element): void => { if (!node || node.nodeType !== 1) return; // Use the reusable validation rule extraction this.extractValidationRulesFromNode(node, validationInfo); // Handle inheritance: if this is a restriction with a base type, inherit from base if (node.nodeName === ns + 'restriction') { const baseType = node.getAttribute('base'); if (baseType && baseType !== 'xs:string' && baseType.indexOf(':') === -1) { // This is a user-defined base type, not a built-in XSD type const baseInfo = this.getTypeValidationInfo(baseType); // Merge base info into current info (current restrictions take precedence) Object.assign(validationInfo, baseInfo, validationInfo); } } // Handle union types: collect validation info from all member types if (node.nodeName === ns + 'union') { const memberTypes = node.getAttribute('memberTypes'); if (memberTypes) { // Split memberTypes by whitespace to get individual type names const typeNames = memberTypes.trim().split(/\s+/); for (const memberTypeName of typeNames) { if (memberTypeName) { // Recursively get validation info for each member type const memberInfo = this.getTypeValidationInfo(memberTypeName); // Merge patterns (union means ANY pattern can match) if (memberInfo.patterns) { if (!validationInfo.patterns) validationInfo.patterns = []; validationInfo.patterns.push(...memberInfo.patterns); } // Merge enumerations (union means ANY enum value is valid) if (memberInfo.enumValues) { if (!validationInfo.enumValues) validationInfo.enumValues = []; validationInfo.enumValues.push(...memberInfo.enumValues); } // For numeric restrictions, use the most permissive ranges if (memberInfo.minInclusive !== undefined) { validationInfo.minInclusive = validation