@@ -47,9 +47,11 @@ use usage::spec::unknown_flags::UnknownFlags;
4747#[ usage( effect = "read" , verbatim_doc_comment) ]
4848pub struct Diff {
4949 /// The spec as it was, typically the released one, use "-" to read from stdin
50+ #[ usage( value_hint = usage_rs:: ValueHint :: FilePath ) ]
5051 old : PathBuf ,
5152
5253 /// The spec as it is now, use "-" to read from stdin
54+ #[ usage( value_hint = usage_rs:: ValueHint :: FilePath ) ]
5355 new : PathBuf ,
5456
5557 /// Output format
@@ -542,54 +544,95 @@ fn diff_command_props(old: &SpecCommand, new: &SpecCommand, path: &str, c: &mut
542544 {
543545 c. metadata ( "help-changed" , path, "help text changed" . to_string ( ) ) ;
544546 }
547+
548+ diff_display_order ( old. display_order , new. display_order , path, "command" , c) ;
545549}
546550
551+ /// Pair the two flag lists up, then report.
552+ ///
553+ /// Two passes, because the two rules are not equals. A matching internal name is the
554+ /// primary pairing and claims its flag first; only then may a flag whose spellings
555+ /// moved onto a differently-named declaration claim what is left. Interleaving them let
556+ /// a spelling take a flag whose name still matched some later old flag, which then
557+ /// paired with it a second time and reported one comparison twice while losing another
558+ /// flag's removal entirely.
547559fn diff_flags ( old : & SpecCommand , new : & SpecCommand , path : & str , c : & mut Changes ) {
548- // Which new flags an old one has already been paired with, so a rename is not also
549- // reported as an addition.
550- let mut paired: Vec < bool > = vec ! [ false ; new. flags. len( ) ] ;
560+ /// What became of one old flag.
561+ enum Pairing {
562+ /// Paired by internal name, which is what the derive and the KDL both key on.
563+ Named ( usize ) ,
564+ /// Paired by a shared spelling. The name is not something a caller can type, so
565+ /// this is a rename rather than a removal — reported, and then compared in full,
566+ /// so a spelling that really did disappear is still reported.
567+ Renamed ( usize ) ,
568+ Gone ,
569+ }
570+
571+ let mut claimed: Vec < bool > = vec ! [ false ; new. flags. len( ) ] ;
572+ let mut pairings: Vec < Pairing > = Vec :: with_capacity ( old. flags . len ( ) ) ;
551573
552574 for was in & old. flags {
553- // By internal name first, which is what the derive and the KDL both key on.
554- if let Some ( position) = new. flags . iter ( ) . position ( |f| f. name == was. name ) {
555- paired[ position] = true ;
556- diff_flag ( was, & new. flags [ position] , path, c) ;
575+ match new
576+ . flags
577+ . iter ( )
578+ . enumerate ( )
579+ . find ( |( position, f) | !claimed[ * position] && f. name == was. name )
580+ . map ( |( position, _) | position)
581+ {
582+ Some ( position) => {
583+ claimed[ position] = true ;
584+ pairings. push ( Pairing :: Named ( position) ) ;
585+ }
586+ None => pairings. push ( Pairing :: Gone ) ,
587+ }
588+ }
589+
590+ for ( index, was) in old. flags . iter ( ) . enumerate ( ) {
591+ if !matches ! ( pairings[ index] , Pairing :: Gone ) {
557592 continue ;
558593 }
559- // Then by spelling: the name is not something a caller can type, so a flag whose
560- // spellings moved onto a differently-named declaration was renamed rather than
561- // removed. Compared in full afterwards, so a spelling that really did disappear
562- // is still reported.
563594 let spellings = flag_spellings ( was) ;
564- let renamed = new
595+ if let Some ( position ) = new
565596 . flags
566597 . iter ( )
567598 . enumerate ( )
568599 . find ( |( position, f) | {
569- !paired [ * position] && flag_spellings ( f) . iter ( ) . any ( |s| spellings. contains ( s) )
600+ !claimed [ * position] && flag_spellings ( f) . iter ( ) . any ( |s| spellings. contains ( s) )
570601 } )
571- . map ( |( position, _) | position) ;
572- match renamed {
573- Some ( position) => {
574- paired[ position] = true ;
575- let now = & new. flags [ position] ;
602+ . map ( |( position, _) | position)
603+ {
604+ claimed[ position] = true ;
605+ pairings[ index] = Pairing :: Renamed ( position) ;
606+ }
607+ }
608+
609+ // Reported in the old spec's declaration order, whichever pass did the pairing.
610+ for ( was, pairing) in old. flags . iter ( ) . zip ( & pairings) {
611+ match pairing {
612+ Pairing :: Named ( position) => diff_flag ( was, & new. flags [ * position] , path, c) ,
613+ Pairing :: Renamed ( position) => {
614+ let now = & new. flags [ * position] ;
576615 c. metadata (
577616 "flag-renamed" ,
578617 path,
579618 format ! ( "flag '{}' was renamed to '{}'" , was. name, now. name) ,
580619 ) ;
581620 diff_flag ( was, now, path, c) ;
582621 }
583- _ => c. breaking (
622+ Pairing :: Gone => c. breaking (
584623 "flag-removed" ,
585624 path,
586- format ! ( "flag '{}' ({}) was removed" , was. name, spellings. join( ", " ) ) ,
625+ format ! (
626+ "flag '{}' ({}) was removed" ,
627+ was. name,
628+ flag_spellings( was) . join( ", " )
629+ ) ,
587630 ) ,
588631 }
589632 }
590633
591634 for ( position, now) in new. flags . iter ( ) . enumerate ( ) {
592- if paired [ position] {
635+ if claimed [ position] {
593636 continue ;
594637 }
595638 let spellings = flag_spellings ( now) ;
@@ -968,6 +1011,8 @@ fn diff_flag(old: &SpecFlag, new: &SpecFlag, path: &str, c: &mut Changes) {
9681011 {
9691012 c. metadata ( "help-changed" , path, format ! ( "{subject} help text changed" ) ) ;
9701013 }
1014+
1015+ diff_display_order ( old. display_order , new. display_order , path, & subject, c) ;
9711016}
9721017
9731018fn diff_args ( old : & [ SpecArg ] , new : & [ SpecArg ] , path : & str , c : & mut Changes ) {
@@ -1218,6 +1263,8 @@ fn diff_arg(old: &SpecArg, new: &SpecArg, path: &str, subject: &str, c: &mut Cha
12181263 {
12191264 c. metadata ( "help-changed" , path, format ! ( "{subject} help text changed" ) ) ;
12201265 }
1266+
1267+ diff_display_order ( old. display_order , new. display_order , path, subject, c) ;
12211268}
12221269
12231270fn diff_choices (
@@ -1879,6 +1926,34 @@ fn diff_unknown_flags(
18791926 }
18801927}
18811928
1929+ /// Where a flag, argument or command sits in help output.
1930+ ///
1931+ /// Presentation only — nothing about parsing moves — but it is a declaration somebody
1932+ /// made deliberately, and the documentation lists it among the things a metadata finding
1933+ /// covers, so it is compared rather than promised.
1934+ fn diff_display_order (
1935+ old : Option < usize > ,
1936+ new : Option < usize > ,
1937+ path : & str ,
1938+ subject : & str ,
1939+ c : & mut Changes ,
1940+ ) {
1941+ if old == new {
1942+ return ;
1943+ }
1944+ let describe =
1945+ |order : Option < usize > | order. map_or ( "declaration order" . to_string ( ) , |o| o. to_string ( ) ) ;
1946+ c. metadata (
1947+ "display-order-changed" ,
1948+ path,
1949+ format ! (
1950+ "{subject} moves from {} to {} in help" ,
1951+ describe( old) ,
1952+ describe( new)
1953+ ) ,
1954+ ) ;
1955+ }
1956+
18821957/// How many values are required. Flags carry this as well as arguments do, and a flag
18831958/// that needs two values where it needed one rejects a command line that used to work.
18841959fn diff_var_min (
@@ -2719,4 +2794,63 @@ cmd "add" help="install" {
27192794 assert_eq ! ( removed. location, "ex install" ) ;
27202795 assert_eq ! ( removed. category, Category :: Breaking ) ;
27212796 }
2797+
2798+ #[ test]
2799+ fn a_name_match_claims_its_flag_before_a_moved_spelling_can ( ) {
2800+ // `--bar` moves from the flag named `foo` to the flag named `bar`, whose own
2801+ // `--baz` goes away. Pairing in one pass let `foo` claim the new `bar` by
2802+ // spelling, after which the old `bar` matched the same flag by name and was
2803+ // compared to it a second time — so `--baz` was never reported as removed.
2804+ let old = r#"
2805+ name "ex"
2806+ bin "ex"
2807+ flag "foo: --bar" help="one"
2808+ flag "bar: --baz" help="two"
2809+ "# ;
2810+ let new = r#"
2811+ name "ex"
2812+ bin "ex"
2813+ flag "bar: --bar" help="two"
2814+ "# ;
2815+ let found = changes ( old, new) ;
2816+ // One comparison per new flag: `bar` pairs with `bar` by name, and `foo` is gone.
2817+ let removed: Vec < & str > = found
2818+ . iter ( )
2819+ . filter ( |c| c. code == "flag-removed" )
2820+ . map ( |c| c. message . as_str ( ) )
2821+ . collect ( ) ;
2822+ assert_eq ! ( removed, [ "flag 'foo' (--bar) was removed" ] ) ;
2823+ assert_eq ! (
2824+ find( & found, "flag-spelling-removed" ) . message,
2825+ "flag '--bar' no longer answers to '--baz'"
2826+ ) ;
2827+ assert ! ( !found. iter( ) . any( |c| c. code == "flag-renamed" ) , "{found:?}" ) ;
2828+ }
2829+
2830+ #[ test]
2831+ fn display_order_is_compared_because_the_docs_say_it_is ( ) {
2832+ let old = r#"
2833+ name "ex"
2834+ bin "ex"
2835+ flag "--verbose" help="verbose" display_order=10
2836+ "# ;
2837+ let new = r#"
2838+ name "ex"
2839+ bin "ex"
2840+ flag "--verbose" help="verbose" display_order=20
2841+ "# ;
2842+ let found = changes ( old, new) ;
2843+ let moved = find ( & found, "display-order-changed" ) ;
2844+ assert_eq ! ( moved. category, Category :: Metadata ) ;
2845+ assert_eq ! (
2846+ moved. message,
2847+ "flag '--verbose' moves from 10 to 20 in help"
2848+ ) ;
2849+ // And dropping it entirely says what it falls back to.
2850+ let none = "name \" ex\" \n bin \" ex\" \n flag \" --verbose\" help=\" verbose\" \n " ;
2851+ assert_eq ! (
2852+ find( & changes( old, none) , "display-order-changed" ) . message,
2853+ "flag '--verbose' moves from 10 to declaration order in help"
2854+ ) ;
2855+ }
27222856}
0 commit comments