xsd-lookup
Version:
Multi-schema XSD lookup utility
1,276 lines (1,104 loc) • 104 kB
text/typescript
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