@@ -311,6 +311,81 @@ export function getManifestDependencies(manifest: Manifest): string[] {
311311 return result ;
312312}
313313
314+ /**
315+ * Recursively searches for an element with the specified id attribute in a parsed XML object.
316+ *
317+ * @param obj - parsed XML object to search in
318+ * @param id - id attribute value to search for
319+ * @param attrPrefix - attribute prefix used by the parser (default: '@_')
320+ * @returns true if an element with the specified id is found
321+ */
322+ function hasElementWithId ( obj : unknown , id : string , attrPrefix : string = '@_' ) : boolean {
323+ if ( ! obj || typeof obj !== 'object' ) {
324+ return false ;
325+ }
326+
327+ const objRecord = obj as Record < string , unknown > ;
328+
329+ // Check if current object has the id attribute
330+ if ( objRecord [ `${ attrPrefix } id` ] === id ) {
331+ return true ;
332+ }
333+
334+ // Recursively search in all properties
335+ for ( const key in objRecord ) {
336+ if ( key . startsWith ( attrPrefix ) ) {
337+ continue ; // Skip attributes
338+ }
339+
340+ if ( checkIdInValue ( objRecord [ key ] , id , attrPrefix ) ) {
341+ return true ;
342+ }
343+ }
344+
345+ return false ;
346+ }
347+
348+ /**
349+ * Checks if a value (object or array) contains an element with the specified id.
350+ *
351+ * @param value - value to check (can be array or object)
352+ * @param id - id to search for
353+ * @param attrPrefix - attribute prefix used by the parser
354+ * @returns true if id is found in the value
355+ */
356+ function checkIdInValue ( value : unknown , id : string , attrPrefix : string ) : boolean {
357+ if ( Array . isArray ( value ) ) {
358+ return value . some ( ( item ) => hasElementWithId ( item , id , attrPrefix ) ) ;
359+ }
360+ if ( typeof value === 'object' && value !== null ) {
361+ return hasElementWithId ( value , id , attrPrefix ) ;
362+ }
363+ return false ;
364+ }
365+
366+ /**
367+ * Checks if an element with the specified id is available (does not exist) in the XML content.
368+ *
369+ * @param id - id to check for availability
370+ * @param xmlContent - XML content as string
371+ * @returns true if the id is available (not found), false if it exists
372+ */
373+ function checkElementIdAvailable ( id : string , xmlContent : string ) : boolean {
374+ const parser = new XMLParser ( {
375+ ignoreAttributes : false ,
376+ attributeNamePrefix : '@_' ,
377+ parseAttributeValue : false
378+ } ) ;
379+
380+ try {
381+ const xmlDocument : unknown = parser . parse ( xmlContent ) ;
382+ return xmlDocument ? ! hasElementWithId ( xmlDocument , id ) : true ;
383+ } catch {
384+ // Parse error = no valid document = no element with id
385+ return true ;
386+ }
387+ }
388+
314389/**
315390 * Validates if an id is unique across XML files (fragments and views) in the project.
316391 * Synchronous overload - when files are provided directly.
@@ -347,99 +422,13 @@ export function validateId(
347422 options : { files ?: never ; appPath : string ; memFs ?: Editor }
348423) : Promise < boolean > ;
349424
350- /**
351- * Validates if an id is unique across XML files (fragments and views) in the project.
352- * Asynchronous overload - when no options are provided.
353- *
354- * @param baseId - id to validate
355- * @param validatedIds - array of ids that are already validated/used
356- * @param options - undefined (no validation options)
357- * @returns Promise that resolves to true (always valid when no files to check)
358- */
359- export function validateId ( baseId : string , validatedIds ?: string [ ] , options ?: undefined ) : Promise < boolean > ;
360-
361425// Implementation
362426export function validateId (
363427 baseId : string ,
364- validatedIds ? : string [ ] ,
365- options ? : { files ?: string [ ] ; appPath ?: string ; memFs ?: Editor }
428+ validatedIds : string [ ] | undefined ,
429+ options : { files ?: string [ ] ; appPath ?: string ; memFs ?: Editor }
366430) : boolean | Promise < boolean > {
367- const { memFs, appPath, files : fileContents } = options ?? { } ;
368-
369- /**
370- * Checks if an element with the specified id is available (does not exist) in the XML content.
371- *
372- * @param id - id to check for availability
373- * @param xmlContent - XML content as string
374- * @returns true if the id is available (not found), false if it exists
375- */
376- function checkElementIdAvailable ( id : string , xmlContent : string ) : boolean {
377- const parser = new XMLParser ( {
378- ignoreAttributes : false ,
379- attributeNamePrefix : '@_' ,
380- parseAttributeValue : false
381- } ) ;
382-
383- try {
384- const xmlDocument : unknown = parser . parse ( xmlContent ) ;
385- return xmlDocument ? ! hasElementWithId ( xmlDocument , id ) : true ;
386- } catch {
387- // Parse error = no valid document = no element with id
388- return true ;
389- }
390- }
391-
392- /**
393- * Checks if a value (object or array) contains an element with the specified id.
394- *
395- * @param value - value to check (can be array or object)
396- * @param id - id to search for
397- * @param attrPrefix - attribute prefix used by the parser
398- * @returns true if id is found in the value
399- */
400- function checkIdInValue ( value : unknown , id : string , attrPrefix : string ) : boolean {
401- if ( Array . isArray ( value ) ) {
402- return value . some ( ( item ) => hasElementWithId ( item , id , attrPrefix ) ) ;
403- }
404- if ( typeof value === 'object' && value !== null ) {
405- return hasElementWithId ( value , id , attrPrefix ) ;
406- }
407- return false ;
408- }
409-
410- /**
411- * Recursively searches for an element with the specified id attribute in a parsed XML object.
412- *
413- * @param obj - parsed XML object to search in
414- * @param id - id attribute value to search for
415- * @param attrPrefix - attribute prefix used by the parser (default: '@_')
416- * @returns true if an element with the specified id is found
417- */
418- function hasElementWithId ( obj : unknown , id : string , attrPrefix : string = '@_' ) : boolean {
419- if ( ! obj || typeof obj !== 'object' ) {
420- return false ;
421- }
422-
423- const objRecord = obj as Record < string , unknown > ;
424-
425- // Check if current object has the id attribute
426- if ( objRecord [ `${ attrPrefix } id` ] === id ) {
427- return true ;
428- }
429-
430- // Recursively search in all properties
431- for ( const key in objRecord ) {
432- if ( key . startsWith ( attrPrefix ) ) {
433- continue ; // Skip attributes
434- }
435-
436- if ( checkIdInValue ( objRecord [ key ] , id , attrPrefix ) ) {
437- return true ;
438- }
439- }
440-
441- return false ;
442- }
431+ const { memFs, appPath, files : fileContents } = options ;
443432
444433 /**
445434 * Validates the ID against the provided files.
@@ -448,42 +437,42 @@ export function validateId(
448437 * @returns true if the id is unique (available), false if it already exists
449438 */
450439 function validateAgainstFiles ( files : string [ ] ) : boolean {
451- return (
452- files . every ( ( content ) => content === '' || checkElementIdAvailable ( baseId , content ) ) &&
453- ! validatedIds ?. includes ( baseId )
454- ) ;
440+ // Check validatedIds first - fast O(n) check avoids expensive XML parsing
441+ if ( validatedIds ?. includes ( baseId ) ) {
442+ return false ;
443+ }
444+
445+ // Only parse XML files if validatedIds check passed
446+ return files . every ( ( content ) => content === '' || checkElementIdAvailable ( baseId , content ) ) ;
455447 }
456448
457449 // Synchronous path: when files are provided directly
458450 if ( fileContents !== undefined ) {
459451 return validateAgainstFiles ( fileContents ) ;
460452 }
461453
462- // Asynchronous path: when appPath is provided or no options
454+ // Asynchronous path: when appPath is provided
455+ if ( ! appPath ) {
456+ throw new Error ( 'validateId requires either files or appPath to be provided in options' ) ;
457+ }
458+
463459 return ( async ( ) : Promise < boolean > => {
464- let files : string [ ] | undefined ;
465- if ( appPath ) {
466- // Ensure we have a memFs instance
467- const fsEditor = memFs ?? create ( createStorage ( ) ) ;
468-
469- const xmlFilePaths = await findFilesByExtension (
470- '.xml' ,
471- appPath ,
472- [ '.git' , 'node_modules' , 'dist' , 'annotations' , 'localService' ] ,
473- fsEditor
474- ) ;
475- const lookupFiles = [ '.fragment.xml' , '.view.xml' ] ;
476- const filteredPaths = xmlFilePaths . filter ( ( fileName : string ) =>
477- lookupFiles . some ( ( lookupFile ) => fileName . endsWith ( lookupFile ) )
478- ) ;
479-
480- // Read file contents from paths using memFs
481- files = filteredPaths . map ( ( path : string ) => fsEditor . read ( path ) ) ;
482- }
460+ // Ensure we have a memFs instance
461+ const fsEditor = memFs ?? create ( createStorage ( ) ) ;
462+
463+ const xmlFilePaths = await findFilesByExtension (
464+ '.xml' ,
465+ appPath ,
466+ [ '.git' , 'node_modules' , 'dist' , 'annotations' , 'localService' ] ,
467+ fsEditor
468+ ) ;
469+ const lookupFiles = [ '.fragment.xml' , '.view.xml' ] ;
470+ const filteredPaths = xmlFilePaths . filter ( ( fileName : string ) =>
471+ lookupFiles . some ( ( lookupFile ) => fileName . endsWith ( lookupFile ) )
472+ ) ;
483473
484- if ( files ) {
485- return validateAgainstFiles ( files ) ;
486- }
487- return true ;
474+ // Read file contents from paths using memFs
475+ const files = filteredPaths . map ( ( path : string ) => fsEditor . read ( path ) ) ;
476+ return validateAgainstFiles ( files ) ;
488477 } ) ( ) ;
489478}
0 commit comments