@@ -361,6 +361,7 @@ struct OpenClawTypographyTests {
361361 " Image(systemName: \" circle \" ).accessibilityLabel(Text(title)) " ,
362362 " .accessibilityLabel(Text(title)).accessibilityIdentifier( \" status \" ) " ,
363363 " Image(systemName: \" circle \" ).accessibilityLabel(Text(title)).accessibilityHint(Text(hint)) " ,
364+ " .accessibilityLabel(Text(self.statusLabel ?? LocalizedStringResource( \" Allowed \" ))) " ,
364365 " .accessibilityLabel(Text(title)) \n ) " ,
365366 " .accessibilityLabel( \n Text(title) \n ) " ,
366367 " Image(systemName: \" circle \" ) \n .accessibilityLabel( \n Text(title) \n ) " ,
@@ -492,6 +493,7 @@ struct OpenClawTypographyTests {
492493 return try self . swiftSourcesForTypographyAudit ( ) . flatMap { url -> [ String ] in
493494 let source = try String ( contentsOf: url, encoding: . utf8)
494495 let lines = source. components ( separatedBy: . newlines)
496+ let accessibilityMetadataTextLines = self . accessibilityMetadataTextLines ( in: lines)
495497 return lines. indices. compactMap { idx -> String ? in
496498 let rawLine = lines [ idx]
497499 let line = rawLine. trimmingCharacters ( in: . whitespaces)
@@ -503,7 +505,7 @@ struct OpenClawTypographyTests {
503505 || self . hasAllowedBrandedFontParameter ( window, line: rawLine, in: url)
504506
505507 if self . isTextOrLabelCall ( rawLine) ,
506- !self . isAccessibilityMetadataTextCall ( at : idx, in : lines ) ,
508+ !accessibilityMetadataTextLines . contains ( idx) ,
507509 !hasLocalFont
508510 {
509511 return " \( self . relativePath ( url) ) : \( idx + 1 ) : \( line) "
@@ -523,40 +525,167 @@ struct OpenClawTypographyTests {
523525 }
524526
525527 private static func isAccessibilityMetadataTextCall( at idx: Int , in lines: [ String ] ) -> Bool {
526- let rawLine = lines [ idx]
527- let textArgument = #"(?:[^()"\\]|\\.|"(?:\\.|[^"\\])*")*"#
528- let inlineAccessibilityText =
529- #"\.accessibility(?:Label|Value|Hint)\s*\(\s*Text\s*\( \#( textArgument) \)\s*\)"#
530-
531- var inlineRemainder = rawLine
532- var removedInlineMetadata = false
533- while let range = inlineRemainder. range ( of: inlineAccessibilityText, options: . regularExpression) {
534- inlineRemainder. removeSubrange ( range)
535- removedInlineMetadata = true
528+ self . accessibilityMetadataTextLines ( in: lines) . contains ( idx)
529+ }
530+
531+ private static func accessibilityMetadataTextLines( in lines: [ String ] ) -> Set < Int > {
532+ let code = self . maskedSwiftCode ( lines. joined ( separator: " \n " ) )
533+ let textToken = Array ( " Text " . utf8)
534+ let labelToken = Array ( " Label " . utf8)
535+ let modifiers = [
536+ Array ( " .accessibilityLabel " . utf8) ,
537+ Array ( " .accessibilityValue " . utf8) ,
538+ Array ( " .accessibilityHint " . utf8) ,
539+ ]
540+ var callCounts : [ Int : Int ] = [ : ]
541+ var metadataCallCounts : [ Int : Int ] = [ : ]
542+ var line = 0
543+
544+ for offset in code. indices {
545+ if code [ offset] == 10 {
546+ line += 1
547+ continue
548+ }
549+ if self . callOpeningParenthesis ( after: textToken, at: offset, in: code) != nil ||
550+ self . callOpeningParenthesis ( after: labelToken, at: offset, in: code) != nil
551+ {
552+ callCounts [ line, default: 0 ] += 1
553+ }
554+
555+ for modifier in modifiers {
556+ guard let modifierOpen = self . callOpeningParenthesis ( after: modifier, at: offset, in: code)
557+ else { continue }
558+ let textOffset = self . skippingWhitespace ( in: code, after: modifierOpen)
559+ guard let textOpen = self . callOpeningParenthesis ( after: textToken, at: textOffset, in: code) ,
560+ let textClose = self . matchingParenthesis ( in: code, openingAt: textOpen)
561+ else { continue }
562+ let metadataClose = self . skippingWhitespace ( in: code, after: textClose)
563+ guard metadataClose < code. count, code [ metadataClose] == 41 else { continue }
564+
565+ // Exempt only the direct Text argument; visible calls sharing its source line stay audited.
566+ let textLine = line + code[ offset..< textOffset] . count ( where: { $0 == 10 } )
567+ metadataCallCounts [ textLine, default: 0 ] += 1
568+ }
536569 }
537- if removedInlineMetadata {
538- // Do not let metadata Text hide a visual Text or Label sharing the audited line.
539- return !self . isTextOrLabelCall ( inlineRemainder)
570+
571+ return Set ( metadataCallCounts. compactMap { line, count in
572+ callCounts [ line] == count ? line : nil
573+ } )
574+ }
575+
576+ private static func callOpeningParenthesis(
577+ after token: [ UInt8 ] ,
578+ at offset: Int ,
579+ in code: [ UInt8 ] ) -> Int ?
580+ {
581+ guard offset + token. count <= code. count,
582+ code [ offset..< ( offset + token. count) ] . elementsEqual ( token) ,
583+ token. first == 46 || offset == 0 || !self . isSwiftIdentifierByte ( code [ offset - 1 ] )
584+ else { return nil }
585+ let afterToken = offset + token. count
586+ guard afterToken == code. count || !self . isSwiftIdentifierByte ( code [ afterToken] ) else { return nil }
587+ let opening = self . skippingWhitespace ( in: code, after: afterToken - 1 )
588+ return opening < code. count && code [ opening] == 40 ? opening : nil
589+ }
590+
591+ private static func skippingWhitespace( in code: [ UInt8 ] , after offset: Int ) -> Int {
592+ var cursor = offset + 1
593+ while cursor < code. count, code [ cursor] == 9 || code [ cursor] == 10 || code [ cursor] == 13 || code [ cursor] == 32 {
594+ cursor += 1
540595 }
596+ return cursor
597+ }
541598
542- let directText = #"^\s*Text\s*\( \#( textArgument) \)"#
543- guard let range = rawLine. range ( of: directText, options: . regularExpression) else { return false }
544- let remainder = rawLine. replacingCharacters ( in: range, with: " " )
545- guard !self . isTextOrLabelCall ( remainder) else { return false }
546-
547- let contextStart = max ( lines. startIndex, idx - 4 )
548- // Comment text cannot open a metadata modifier for a following visual Text.
549- let leadingContext = lines [ contextStart..< idx]
550- . map { line in
551- guard let comment = line. range ( of: " // " ) else { return line }
552- return String ( line [ ..< comment. lowerBound] )
599+ private static func matchingParenthesis( in code: [ UInt8 ] , openingAt offset: Int ) -> Int ? {
600+ var depth = 0
601+ for cursor in offset..< code. count {
602+ if code [ cursor] == 40 {
603+ depth += 1
604+ } else if code [ cursor] == 41 {
605+ depth -= 1
606+ if depth == 0 { return cursor }
553607 }
554- . joined ( separator: " \n " )
555- let accessibilityOpener =
556- #"(?s)(?:^|\n)[^\n]*\.accessibility(?:Label|Value|Hint)\s*\(\s*$"#
608+ }
609+ return nil
610+ }
611+
612+ private static func isSwiftIdentifierByte( _ byte: UInt8 ) -> Bool {
613+ byte == 95 || ( 48 ... 57 ) . contains ( byte) || ( 65 ... 90 ) . contains ( byte) || ( 97 ... 122 ) . contains ( byte) || byte >= 128
614+ }
557615
558- // Direct accessibility metadata is announced, not rendered. Complex expressions fail closed.
559- return leadingContext. range ( of: accessibilityOpener, options: . regularExpression) != nil
616+ private static func maskedSwiftCode( _ source: String ) -> [ UInt8 ] {
617+ let sourceBytes = Array ( source. utf8)
618+ var code = sourceBytes
619+ var cursor = 0
620+
621+ func mask( _ range: Range < Int > ) {
622+ for offset in range where code [ offset] != 10 && code [ offset] != 13 {
623+ code [ offset] = 32
624+ }
625+ }
626+
627+ while cursor < sourceBytes. count {
628+ if cursor + 1 < sourceBytes. count, sourceBytes [ cursor] == 47 , sourceBytes [ cursor + 1 ] == 47 {
629+ let start = cursor
630+ while cursor < sourceBytes. count, sourceBytes [ cursor] != 10 {
631+ cursor += 1
632+ }
633+ mask ( start..< cursor)
634+ continue
635+ }
636+ if cursor + 1 < sourceBytes. count, sourceBytes [ cursor] == 47 , sourceBytes [ cursor + 1 ] == 42 {
637+ let start = cursor
638+ var depth = 1
639+ cursor += 2
640+ while cursor < sourceBytes. count, depth > 0 {
641+ if cursor + 1 < sourceBytes. count, sourceBytes [ cursor] == 47 , sourceBytes [ cursor + 1 ] == 42 {
642+ depth += 1
643+ cursor += 2
644+ } else if cursor + 1 < sourceBytes. count,
645+ sourceBytes [ cursor] == 42 ,
646+ sourceBytes [ cursor + 1 ] == 47
647+ {
648+ depth -= 1
649+ cursor += 2
650+ } else {
651+ cursor += 1
652+ }
653+ }
654+ mask ( start..< cursor)
655+ continue
656+ }
657+
658+ let start = cursor
659+ var hashCount = 0
660+ while cursor < sourceBytes. count, sourceBytes [ cursor] == 35 {
661+ hashCount += 1
662+ cursor += 1
663+ }
664+ guard cursor < sourceBytes. count, sourceBytes [ cursor] == 34 else {
665+ cursor = start + 1
666+ continue
667+ }
668+ let quoteCount = cursor + 2 < sourceBytes. count &&
669+ sourceBytes [ cursor + 1 ] == 34 && sourceBytes [ cursor + 2 ] == 34 ? 3 : 1
670+ cursor += quoteCount
671+ while cursor < sourceBytes. count {
672+ if hashCount == 0 , quoteCount == 1 , sourceBytes [ cursor] == 92 {
673+ cursor = min ( sourceBytes. count, cursor + 2 )
674+ continue
675+ }
676+ let closingEnd = cursor + quoteCount + hashCount
677+ if closingEnd <= sourceBytes. count,
678+ sourceBytes [ cursor..< ( cursor + quoteCount) ] . allSatisfy ( { $0 == 34 } ) ,
679+ sourceBytes [ ( cursor + quoteCount) ..< closingEnd] . allSatisfy ( { $0 == 35 } )
680+ {
681+ cursor = closingEnd
682+ break
683+ }
684+ cursor += 1
685+ }
686+ mask ( start..< cursor)
687+ }
688+ return code
560689 }
561690
562691 private static func isShorthandControlCall( _ line: String ) -> Bool {
0 commit comments