@@ -369,12 +369,73 @@ function isExcludedForMomentScan(modulePath: string, filePath: string): boolean
369369 || segments . includes ( "coverage" ) ;
370370}
371371
372- function searchRegexInFile ( content : string , pattern : RegExp ) : boolean {
373- try {
374- return pattern . test ( content ) ;
375- } catch {
376- return false ;
372+ function findReadmeConfigObjects ( content : string ) : Array < { hasTrailingComma : boolean } > {
373+ const lines = content . split ( / \r ? \n / u) ;
374+ const objects : Array < { hasTrailingComma : boolean } > = [ ] ;
375+ const headingRegex = / ^ ( # { 2 , 6 } ) \s + ( .+ ?) \s * $ / u;
376+ const fenceOpenRegex = / ^ ` ` ` ( [ A - Z a - z 0 - 9 _ - ] + ) ? \s * $ / u;
377+ const fenceCloseRegex = / ^ ` ` ` \s * $ / u;
378+
379+ let inConfigSection = false ;
380+ let configSectionLevel = 0 ;
381+ let inFence = false ;
382+ let fenceLanguage = "" ;
383+ let fenceLines : string [ ] = [ ] ;
384+ let fenceWasInConfigSection = false ;
385+
386+ for ( const line of lines ) {
387+ if ( ! inFence ) {
388+ const headingMatch = line . match ( headingRegex ) ;
389+ if ( headingMatch ) {
390+ const headingLevel = headingMatch [ 1 ] . length ;
391+ const headingText = headingMatch [ 2 ] . trim ( ) . toLowerCase ( ) ;
392+
393+ if ( headingText === "config" || headingText === "configuration" ) {
394+ inConfigSection = true ;
395+ configSectionLevel = headingLevel ;
396+ }
397+ else if ( inConfigSection && headingLevel <= configSectionLevel ) {
398+ inConfigSection = false ;
399+ }
400+ }
401+
402+ const fenceOpenMatch = line . match ( fenceOpenRegex ) ;
403+ if ( fenceOpenMatch ) {
404+ inFence = true ;
405+ fenceLanguage = ( fenceOpenMatch [ 1 ] ?? "" ) . toLowerCase ( ) ;
406+ fenceLines = [ ] ;
407+ fenceWasInConfigSection = inConfigSection ;
408+ }
409+
410+ continue ;
411+ }
412+
413+ if ( fenceCloseRegex . test ( line ) ) {
414+ const isJavaScriptFence = fenceLanguage === "" || fenceLanguage === "js" || fenceLanguage === "javascript" ;
415+ if ( fenceWasInConfigSection && isJavaScriptFence ) {
416+ const blockText = fenceLines . join ( "\n" ) ;
417+ const hasModuleKey = / \b m o d u l e \s * : / u. test ( blockText ) ;
418+ const hasConfigKey = / \b c o n f i g \s * : / u. test ( blockText ) ;
419+
420+ if ( hasModuleKey && hasConfigKey ) {
421+ const lastNonEmptyLine = [ ...fenceLines ] . reverse ( ) . find ( ( fenceLine ) => fenceLine . trim ( ) . length > 0 ) ?? "" ;
422+ objects . push ( {
423+ hasTrailingComma : / ^ \s * \} , \s * (?: \/ \/ .* ) ? $ / u. test ( lastNonEmptyLine )
424+ } ) ;
425+ }
426+ }
427+
428+ inFence = false ;
429+ fenceLanguage = "" ;
430+ fenceLines = [ ] ;
431+ fenceWasInConfigSection = false ;
432+ continue ;
433+ }
434+
435+ fenceLines . push ( line ) ;
377436 }
437+
438+ return objects ;
378439}
379440
380441/**
@@ -513,8 +574,8 @@ export async function analyzeModule(
513574 }
514575
515576 // Check for config example
516- const configRegex = / \{ \s * [ ^ } ] * ? \s * c o n f i g : \s * \{ \s * [ ^ } ] * \} (?: [ , \s ] \s * [ ^ } ] * ? ) } / ;
517- const hasConfigExample = searchRegexInFile ( content , configRegex ) ;
577+ const configObjects = findReadmeConfigObjects ( content ) ;
578+ const hasConfigExample = configObjects . length > 0 ;
518579
519580 if ( ! hasConfigExample ) {
520581 const falsePositivesConfig = [ "MMM-CalendarExt2" ] ;
@@ -525,8 +586,7 @@ export async function analyzeModule(
525586 }
526587 } else {
527588 // Check for trailing comma
528- const trailingCommaRegex = / \{ \s * [ ^ } ] * ?\s * c o n f i g : \s * \{ \s * [ ^ } ] * \} (?: [ , \s ] \s * [ ^ } ] * ?) } , / ;
529- const hasTrailingComma = searchRegexInFile ( content , trailingCommaRegex ) ;
589+ const hasTrailingComma = configObjects . some ( ( object ) => object . hasTrailingComma ) ;
530590 const falsePositivesTrailing = [ "MMM-MealieMenu" , "MMM-Remote-Control" ] ;
531591 if ( ! hasTrailingComma && ! falsePositivesTrailing . includes ( moduleName ) ) {
532592 issues . push (
0 commit comments