@@ -5,9 +5,11 @@ import (
55 "fmt"
66 "math"
77 "regexp"
8+ "sort"
89 "strings"
910
1011 "github.com/HexmosTech/blastradius/client"
12+ "github.com/HexmosTech/blastradius/score"
1113 "github.com/HexmosTech/blastradius/symbols"
1214)
1315
@@ -115,33 +117,172 @@ func matchSingleIdentSwap(oldLine, newLine, symName string) *DeclRename {
115117
116118// renameOldNameSignal runs a best-effort text search for rn.OldName and
117119// returns a Signal counting how many places in the repo still reference the
118- // pre-rename name - i.e. call sites/references that are about to break until
119- // they're migrated to rn.NewName. Weighted 1.5x relative to the plain
120- // sqrt(refs) used for an ordinary text-reference count elsewhere in this
121- // package, since this is live, unmigrated breakage risk right now, not just
122- // historical usage. Unlike the ordinary text-reference count, no "-1 for the
123- // symbol's own definition" adjustment is applied: the definition itself was
124- // renamed away, so every remaining match under the old name is an external
125- // reference.
120+ // pre-rename name after the declaration moved to rn.NewName. Weighted 1.5x
121+ // relative to the plain sqrt(refs) used for an ordinary text-reference count
122+ // elsewhere in this package, since an unmigrated reference is a live risk
123+ // right now, not just historical usage. Unlike the ordinary text-reference
124+ // count, no "-1 for the symbol's own definition" adjustment is applied: the
125+ // definition itself was renamed away, so every remaining match under the old
126+ // name is an external reference.
127+ //
128+ // The wording states only what was measured. search_code is grep-based, so a
129+ // match can equally be a real call, a comment, or a string literal - calling
130+ // these references "about to break" asserted a compile failure that nothing
131+ // here verifies. Points are unchanged; this is phrasing only.
126132func renameOldNameSignal (ctx context.Context , c * client.Client , rn DeclRename ) Signal {
127133 usage , err := c .SearchCodeUsage (ctx , rn .OldName )
128134 if err != nil {
129135 // Distinguish "checked, found nothing" from "couldn't check" - the
130136 // latter must not silently read as 0 points/0 references, which
131- // would falsely assert the rename is safe when it simply wasn't
132- // verified.
137+ // would falsely assert the rename is fully migrated when it simply
138+ // wasn't verified.
133139 return Signal {
134- Name : "Callers of old name (about to break) " ,
135- Detail : fmt .Sprintf ("could not check references to the pre-rename name %q: %v" , rn .OldName , err ),
140+ Name : "Still uses the old name" ,
141+ Detail : fmt .Sprintf ("could not check references to the old name %q: %v" , rn .OldName , err ),
136142 Points : 0 ,
137143 Category : "graph" ,
138144 }
139145 }
140146 refs := usage .TotalMatches
141147 return Signal {
142- Name : "Callers of old name (about to break) " ,
143- Detail : fmt .Sprintf ("%d reference(s) to the pre-rename name %q still exist and will break until migrated to %q" , refs , rn .OldName , rn .NewName ),
148+ Name : "Still uses the old name" ,
149+ Detail : fmt .Sprintf ("%d reference(s) to %q remain after the rename to %q" , refs , rn .OldName , rn .NewName ),
144150 Points : 1.5 * math .Sqrt (float64 (refs )),
145151 Category : "graph" ,
146152 }
147153}
154+
155+ // oldNameCallerLimit caps how many enclosing symbols search_code is asked to
156+ // resolve for a pre-rename name. Generous enough that a normal rename's call
157+ // sites all fit, bounded so a rename of a very common word (e.g. "get") can't
158+ // turn one hunk into a thousand-node graph.
159+ const oldNameCallerLimit = 60
160+
161+ // preRenameCallerLabels are the node labels that can actually contain a
162+ // reference that breaks. search_code resolves a match to whatever node encloses
163+ // it, which includes documentation and structural nodes (Section, File, Folder,
164+ // Package, ...) - a README mentioning the old name is not a caller, and listing
165+ // it as one would misrepresent the breakage.
166+ var preRenameCallerLabels = map [string ]bool {
167+ "Function" : true ,
168+ "Method" : true ,
169+ "Class" : true ,
170+ "Struct" : true ,
171+ "Variable" : true ,
172+ "Route" : true ,
173+ }
174+
175+ // oldNameCallers finds the symbols that still reference rn.OldName - the call
176+ // sites that break until they're migrated to rn.NewName - and returns them as
177+ // depth-1 CallerRefs flagged PreRename.
178+ //
179+ // These cannot come from CALLS fan-in: the graph is indexed against the
180+ // post-rename tree, so the old name has no node and every edge that pointed at
181+ // it was dropped as unresolvable. search_code's compact mode is the way in - it
182+ // resolves each textual hit to the graph node enclosing it, which is exactly
183+ // the caller we want.
184+ //
185+ // Best-effort by the same rule as renameOldNameSignal's error path: a failed
186+ // lookup returns no callers rather than failing the report. It contributes no
187+ // points either way - scoring stays entirely with renameOldNameSignal.
188+ func oldNameCallers (ctx context.Context , c * client.Client , rn DeclRename , selfQN string ) []CallerRef {
189+ matches , err := c .SearchCodeSymbols (ctx , rn .OldName , oldNameCallerLimit )
190+ if err != nil {
191+ return nil
192+ }
193+ return preRenameCallersFrom (matches .Matches , selfQN )
194+ }
195+
196+ // preRenameCallersFrom is oldNameCallers' pure half: turn search_code's
197+ // enclosing-symbol matches into a deduplicated, name-sorted caller list.
198+ func preRenameCallersFrom (matches []client.CodeSymbolMatch , selfQN string ) []CallerRef {
199+ var callers []CallerRef
200+ seen := make (map [string ]bool )
201+ for _ , m := range matches {
202+ // The renamed symbol's own declaration is not one of its callers.
203+ // It can still match: a method body referencing its own old name,
204+ // or a stale doc comment above the new declaration.
205+ if m .QualifiedName == "" || m .QualifiedName == selfQN || seen [m .QualifiedName ] {
206+ continue
207+ }
208+ if ! preRenameCallerLabels [m .Label ] {
209+ continue
210+ }
211+ seen [m .QualifiedName ] = true
212+ callers = append (callers , CallerRef {
213+ QualifiedName : m .QualifiedName ,
214+ Depth : 1 ,
215+ Weight : 1 ,
216+ PreRename : true ,
217+ })
218+ }
219+ sort .Slice (callers , func (i , j int ) bool { return callers [i ].QualifiedName < callers [j ].QualifiedName })
220+ return callers
221+ }
222+
223+ // expandPreRenameCallers adds the transitive fan-in of direct pre-rename
224+ // callers. Unlike the old name itself, those callers are ordinary present-day
225+ // graph nodes, so score.FanIn can walk them - which is what gives a rename's
226+ // sunburst/flamegraph the same depth structure as any other symbol's instead of
227+ // a single flat ring.
228+ //
229+ // Each discovered caller comes back one hop further from the renamed symbol
230+ // than it is from the direct caller, reached *through* that direct caller, so
231+ // its Depth is shifted by 1 and its Path prefixed accordingly. Anything already
232+ // present as a direct pre-rename caller is left alone - the shallower depth is
233+ // the truthful one.
234+ func expandPreRenameCallers (ctx context.Context , q score.GraphQuerier , direct []CallerRef , cfg score.Config , selfQN string ) []CallerRef {
235+ if len (direct ) == 0 {
236+ return nil
237+ }
238+ directQN := make ([]string , 0 , len (direct ))
239+ seen := make (map [string ]bool , len (direct ))
240+ for _ , c := range direct {
241+ directQN = append (directQN , c .QualifiedName )
242+ seen [c .QualifiedName ] = true
243+ }
244+ // One fewer hop than a normal walk: the direct pre-rename callers are
245+ // already one hop out from the renamed symbol, so the budget left for
246+ // their own callers is MaxDepth-1.
247+ expandCfg := cfg
248+ expandCfg .MaxDepth = cfg .MaxDepth - 1
249+ if expandCfg .MaxDepth < 1 {
250+ return nil
251+ }
252+ scores , err := score .FanIn (ctx , q , directQN , expandCfg )
253+ if err != nil {
254+ return nil // best-effort: the direct callers alone are still worth showing
255+ }
256+
257+ best := make (map [string ]CallerRef )
258+ for viaQN , ss := range scores {
259+ for _ , cc := range ss .Callers {
260+ qn := cc .QualifiedName
261+ if qn == "" || qn == selfQN || seen [qn ] {
262+ continue
263+ }
264+ depth := cc .Depth + 1
265+ if existing , ok := best [qn ]; ok && existing .Depth <= depth {
266+ continue
267+ }
268+ best [qn ] = CallerRef {
269+ QualifiedName : qn ,
270+ Depth : depth ,
271+ Weight : cc .Weight * cfg .Decay ,
272+ Path : append ([]string {viaQN }, cc .Path ... ),
273+ PreRename : true ,
274+ }
275+ }
276+ }
277+ expanded := make ([]CallerRef , 0 , len (best ))
278+ for _ , c := range best {
279+ expanded = append (expanded , c )
280+ }
281+ sort .Slice (expanded , func (i , j int ) bool {
282+ if expanded [i ].Depth != expanded [j ].Depth {
283+ return expanded [i ].Depth < expanded [j ].Depth
284+ }
285+ return expanded [i ].QualifiedName < expanded [j ].QualifiedName
286+ })
287+ return expanded
288+ }
0 commit comments