@@ -365,6 +365,27 @@ class Changed:
365365 elif tag == 'replace' : near [j1 + k_ + 1 ] = i1 + min (k_ + 1 , i2 - i1 ) # a rewritten line: the line in its position
366366 else : near [j1 + k_ + 1 ] = max (1 , i1 ) # a line this text lacks: the one before it
367367 taken = {exact [a ] for a , b , i , k , d , n in spans if a in exact }
368+ # A DECLARATION CAN BE LONGER IN THIS TEXT THAN IN THE GRAPH'S. Its end is carried to the line the graph's end
369+ # line became, so the lines this text has after it and the graph's text does not (statements a later text
370+ # deleted) fell outside it: removed from a method, they were charged to the declaration below or to nothing.
371+ # The span runs on over lines only this text has while they are still inside it: its braces not yet closed, or
372+ # (Python) indented deeper than its header
373+ mine = set (exact .values ())
374+ def grow (a2 , b2 , k ):
375+ if k not in ('method' , 'function' , 'constructor' , 'class' , 'interface' , 'enum' , 'type' ) or b2 >= len (OL ): return b2
376+ if is_py :
377+ ind = len (OL [a2 - 1 ]) - len (OL [a2 - 1 ].lstrip ()); x = b2
378+ while x < len (OL ) and x + 1 not in mine and (not OL [x ].strip () or len (OL [x ]) - len (OL [x ].lstrip ()) > ind ):
379+ x += 1
380+ if OL [x - 1 ].strip (): b2 = x
381+ return b2
382+ depth = sum (OS [x - 1 ].count ('{' ) - OS [x - 1 ].count ('}' ) for x in range (a2 , b2 + 1 ))
383+ if depth <= 0 or '{' not in '' .join (OS [a2 - 1 :b2 ]): return b2
384+ x = b2
385+ while x < len (OL ) and x + 1 not in mine :
386+ x += 1 ; depth += OS [x - 1 ].count ('{' ) - OS [x - 1 ].count ('}' )
387+ if depth <= 0 : return x
388+ return b2
368389 def renamed_line (a , d , n , k ):
369390 """A HEADER THE GRAPH HOLDS UNDER A NEW NAME is the same declaration when this text's line in its place declares
370391 another name with the same parameters (a rename the refresh already indexed). Dropped as "not declared here",
@@ -388,7 +409,7 @@ class Changed:
388409 b2 = a2 + (b - a ) if exact .get (b ) is None else exact [b ]
389410 else :
390411 b2 = exact .get (b ) or near .get (b ) or a2 + (b - a )
391- out .append ((a2 , max (a2 , b2 ), i , k , d , n ))
412+ out .append ((a2 , grow ( a2 , max (a2 , b2 ), k ), i , k , d , n ))
392413 out .sort (); return out
393414 def rel (self , f ):
394415 f = os .path .realpath (f ) if os .path .exists (f ) else f
@@ -797,7 +818,7 @@ class Changed:
797818 for a , b , i , k , d , n in decls :
798819 if ln < a <= ln + 3 and all (not L [x - 1 ].strip () or L [x - 1 ].strip ().startswith ('@' ) for x in range (ln + 1 , a )): return (a , b , i , k , d , n )
799820 return None
800- decorated = {}; stmt_pair = {}
821+ decorated = {}; stmt_pair = {}; gone_fields = []
801822 OK = strip_code (old , strings = False , hash_comments = is_py ).split ('\n ' ); NK = strip_code (new , strings = False , hash_comments = is_py ).split ('\n ' )
802823 # A LINE INSERTED INTO A HEADER CHANGES THE SIGNATURE. A parameter list written one parameter a line gains a
803824 # parameter as an inserted line, which no old line was changed for; charged to the callable as an insertion it
@@ -866,12 +887,20 @@ class Changed:
866887 elif m and m [3 ] != 'module' and m [0 ] <= ln <= self .header_end (OL , m [0 ]): key = ('signature' , m )
867888 elif m : key = ('body' , m )
868889 elif t and t [0 ] <= ln <= self .header_end (OL , t [0 ]): key = ('type' , t )
890+ # A FIELD THE GRAPH NEVER HELD is still named by its line. Read against a graph from a later text (a range, a
891+ # refresh), a field that text no longer declares has no span, and its deleted line came back as an anonymous
892+ # "inside <type>": the mirror of a new field, named from its inserted line (added)
893+ elif t and not is_py and (fp := self .field_parts (OS [ln - 1 ]))[0 ] and OS [ln - 1 ].rstrip ().endswith (';' ) \
894+ and '(' not in OS [ln - 1 ].split ('=' )[0 ] and self .declares (OS [ln - 1 ], fp [1 ], 'field' , False ) \
895+ and not still_declared (fp [1 ], 'field' , new_of .get (ln ) or ln ):
896+ gone_fields .append ((ln , t , fp [1 ]))
869897 elif t : key = ('inside' , t ) # between members: a field the graph did not record, a comment
870898 if key : hits .setdefault (key , set ()).add (ln )
871899 nd = [0 ] * (len (NL ) + 2 )
872900 for ii , text in enumerate (strip_code (new ).split ('\n ' ), 1 ): nd [ii ] = nd [ii - 1 ] + text .count ('{' ) - text .count ('}' )
873901 ndepth_ok = lambda j : nd [j - 1 ] <= 2
874- out = []
902+ out = [dict (kind = 'field' , symbol = f"{ t [4 ]} .{ nm } " , id = None , file = rel , line = ln , end = ln , old_lines = [ln ], target_kind = 'field' ,
903+ detail = 'removed or renamed' , target = None ) for ln , t , nm in gone_fields ]
875904 KIND_OF = lambda k : 'field' if k in ('field' , 'const' , 'enum_member' , 'variable' ) else 'type' if k in ('class' , 'interface' , 'enum' , 'type' , 'namespace' ) else 'signature'
876905 KIND = lambda k : 'field' if k in ('field' , 'const' , 'enum_member' , 'variable' ) else 'type' if k in ('class' , 'interface' , 'enum' , 'type' , 'namespace' ) else 'method'
877906 for (a , b , i , k , d , n ), decs in decorated .items ():
@@ -1568,6 +1597,9 @@ def main(argv):
15681597 for e in r :
15691598 if e .get ('new_file' ): e ['kind' ] = 'removed' ; e ['detail' ] = 'the file is gone at the newer side' ; continue
15701599 if e ['kind' ] == 'removed' : e ['kind' ] = 'added (declaration)' ; e ['target' ] = None
1600+ # and one the newer text lacks, read as new in the swapped pair, is one the range removed
1601+ elif e ['kind' ] == 'added' :
1602+ e .update (kind = 'removed' , detail = re .sub (r'^new ' , '' , e .get ('detail' ) or '' ) + ' gone at the newer side' , target = None ); continue
15711603 if e .get ('detail' ):
15721604 e ['detail' ] = ' → ' .join (reversed (e ['detail' ].split (' → ' ))) if ' → ' in e ['detail' ] and ':' not in e ['detail' ] else re .sub (r'(\w+): (.+?) → (.+?)(,|$)' , r'\1: \3 → \2\4' , e ['detail' ])
15731605 e ['detail' ] = re .sub (r'(^|, )([+-])(\w+)' , lambda m : m .group (1 ) + ('-' if m .group (2 ) == '+' else '+' ) + m .group (3 ), e ['detail' ])
@@ -1626,7 +1658,7 @@ def main(argv):
16261658 + ', ' .join (e ['symbol' ] for e in es [:10 ]) + (f" … +{ len (es ) - 10 } " if len (es ) > 10 else '' ))
16271659 # a note with a file is an addition in that file; one without is a caveat about the whole answer, and printing it
16281660 # under `added` made "the graph is built at the newer commit …" read as a new declaration
1629- for f , n , _ in notes : print (f" { 'added' :<10} { n } " if f else f"note: { n } " )
1661+ for f , n , _ in notes : print (f" { 'removed' if ' line(s) gone at ' in n else ' added' :<10} { n } " if f else f"note: { n } " )
16301662 # CASE DATA IS NOT A TEST: a changed file in a case directory or a fixture tree is an input its runner reads
16311663 cd = case_data (C .repo , {e ['file' ] for e in results if e .get ('file' ) and not e .get ('new_file' )} | set (outside ))
16321664 if cd :
0 commit comments