devscout-rs 0.8.0

Fast code index for C# and TypeScript codebases -- map, find, refs, impact from the CLI.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
use super::*;

#[test]
fn stage2_end_to_end_static_property_access_emits_and_an_undeclared_member_does_not() {
    let files = fragments_for(&[
        (
            "Other/MessageUrn.cs",
            "namespace App.Consumers { public static class MessageUrn { public static string Prefix { get; } } }",
        ),
        (
            "Consumers/UsesProperty.cs",
            "\nnamespace App.Consumers;\n\npublic class UsesProperty\n{\n  public object Get() => MessageUrn.Prefix;\n  public object Miss() => MessageUrn.NotDeclared;\n}\n",
        ),
    ]);
    let g = resolve_graph(&no_git_root(), &files);
    assert_eq!(
        member_edges_from(&g, "Consumers/UsesProperty.cs"),
        vec![("App.Consumers.MessageUrn", 6)],
        "the declared property emits at its own line; the undeclared member does not"
    );
}

#[test]
fn stage2_end_to_end_a_declared_local_receiver_earns_an_edge_at_the_access_line() {
    let files = fragments_for(&[
        ("Other/Widget.cs", "namespace App.Other { public class Widget { public void Render() { } } }"),
        (
            "Consumers/LocalReceiver.cs",
            "\nusing App.Other;\n\nnamespace App.Consumers;\n\npublic class LocalReceiver\n{\n  public void Run()\n  {\n    Widget w = new Widget();\n    w.Render();\n  }\n}\n",
        ),
    ]);
    let g = resolve_graph(&no_git_root(), &files);
    assert_eq!(
        member_edges_from(&g, "Consumers/LocalReceiver.cs"),
        vec![("App.Other.Widget", 11)]
    );
}

#[test]
fn stage2_end_to_end_a_class_field_receiver_earns_an_edge_the_ctor_injection_shape() {
    let files = fragments_for(&[
        ("Other/IRepo.cs", "namespace App.Other { public interface IRepo { void Save(); } }"),
        (
            "Consumers/FieldReceiver.cs",
            "\nusing App.Other;\n\nnamespace App.Consumers;\n\npublic class FieldReceiver\n{\n  private readonly IRepo _repo;\n\n  public FieldReceiver(IRepo repo) { _repo = repo; }\n\n  public void Run() { _repo.Save(); }\n}\n",
        ),
    ]);
    let g = resolve_graph(&no_git_root(), &files);
    assert_eq!(
        member_edges_from(&g, "Consumers/FieldReceiver.cs"),
        vec![("App.Other.IRepo", 12)],
        "the field access in Run, not the constructor assignment"
    );
}

#[test]
fn stage2_end_to_end_a_chain_tail_earns_no_edge_from_a_fact_it_did_not_inherit() {
    // Widget declares BOTH Inner and Tail, so if the flattened tail
    // ("w.Inner") had inherited the head's receiverType it would have
    // produced a second, wrong edge. Stage-1 chain-tail regression class.
    let files = fragments_for(&[
        (
            "Other/Widget.cs",
            "namespace App.Other { public class Widget { public object Inner { get; } public void Tail() { } } }",
        ),
        (
            "Consumers/Chain.cs",
            "using App.Other;\n\nnamespace App.Consumers;\n\npublic class Chain\n{\n  public void Run()\n  {\n    Widget w = new Widget();\n    w.Inner.Tail();\n  }\n}\n",
        ),
    ]);
    let g = resolve_graph(&no_git_root(), &files);
    assert_eq!(
        member_edges_from(&g, "Consumers/Chain.cs"),
        vec![("App.Other.Widget", 10)],
        "only the head access (\"w.Inner\") earns a PRECISE edge, and it is the head's line, not the tail's"
    );
    // End-to-end half of the same split: the tail is guessed at by
    // member-name uniqueness, tagged, on the same line.
    assert_eq!(
        heuristic_member_edges_from(&g, "Consumers/Chain.cs"),
        vec![("App.Other.Widget", 10)],
        "exactly one guess -- the tail; the head already has its fact and is never second-guessed"
    );
}

// --- nested-qualifier chain: the qualifier ladder walking through
// nested types ---
//
// A qualified expression that crosses one or more nesting boundaries
// (`Outer.Inner.Value`, `Outer.Middle.Leaf.Value`) must bind to the LEAF
// def the compiler actually binds -- using the "+"-joined id `type_id`
// (extract.rs) gives every nested type -- never to an outer container
// with the next dotted segment misread as one of ITS members. Real
// fixtures run through this crate's own extractor, matching the tier
// (e) end-to-end tests above.

#[test]
fn end_to_end_two_level_nested_qualifier_binds_the_inner_type_and_its_const() {
    let files = fragments_for(&[
        (
            "Other/Outer.cs",
            "namespace App.Other { public static class Outer { public static class Inner { public const string Value = \"v\"; } } }",
        ),
        (
            "Consumers/UsesNested.cs",
            "\nusing App.Other;\n\nnamespace App.Consumers;\n\npublic class UsesNested\n{\n  public string Get() => Outer.Inner.Value;\n}\n",
        ),
    ]);
    let g = resolve_graph(&no_git_root(), &files);
    assert_eq!(
        member_edges_from(&g, "Consumers/UsesNested.cs"),
        vec![("App.Other.Outer+Inner", 8)],
        "exactly one precise edge, targeting the nested type -- not Outer"
    );
    let edge =
        find_edge(&g, |e| matches!(e, Edge::UsesMember { .. })).expect("uses-member edge present");
    match edge {
        Edge::UsesMember { member, .. } => {
            assert_eq!(member.as_deref(), Some("Value"))
        }
        _ => unreachable!(),
    }
}

#[test]
fn end_to_end_three_level_nested_qualifier_binds_the_leaf_type_and_its_const() {
    let files = fragments_for(&[
        (
            "Other/Outer.cs",
            "namespace App.Other { public struct Outer { public struct Middle { public struct Leaf { public const string Value = \"v\"; } } } }",
        ),
        (
            "Consumers/UsesNested.cs",
            "\nusing App.Other;\n\nnamespace App.Consumers;\n\npublic class UsesNested\n{\n  public string Get() => Outer.Middle.Leaf.Value;\n}\n",
        ),
    ]);
    let g = resolve_graph(&no_git_root(), &files);
    assert_eq!(
        member_edges_from(&g, "Consumers/UsesNested.cs"),
        vec![("App.Other.Outer+Middle+Leaf", 8)],
        "the whole chain binds to the LEAF struct, not the outermost container"
    );
    let edge =
        find_edge(&g, |e| matches!(e, Edge::UsesMember { .. })).expect("uses-member edge present");
    match edge {
        Edge::UsesMember { member, .. } => {
            assert_eq!(member.as_deref(), Some("Value"))
        }
        _ => unreachable!(),
    }
}

#[test]
fn end_to_end_nested_enum_member_binds_the_enum_and_not_the_enclosing_type() {
    let files = fragments_for(&[
        (
            "Other/Outer.cs",
            "namespace App.Other { public class Outer { public enum Kind { First, Second } } }",
        ),
        (
            "Consumers/UsesNested.cs",
            "\nusing App.Other;\n\nnamespace App.Consumers;\n\npublic class UsesNested\n{\n  public object Get() => Outer.Kind.First;\n}\n",
        ),
    ]);
    let g = resolve_graph(&no_git_root(), &files);
    let edge =
        find_edge(&g, |e| matches!(e, Edge::UsesMember { .. })).expect("uses-member edge present");
    match edge {
        Edge::UsesMember {
            to,
            to_file,
            member,
            heuristic,
            ..
        } => {
            assert_eq!(to, "App.Other.Outer+Kind.First");
            assert_eq!(to_file, "Other/Outer.cs");
            assert_eq!(member.as_deref(), Some("First"));
            assert!(!heuristic, "the enum member itself is the precise target");
        }
        _ => unreachable!(),
    }
}

#[test]
fn end_to_end_collision_a_namespace_type_must_not_shadow_a_same_named_nested_type() {
    // "Config" names two unrelated defs: a top-level class in namespace
    // Shared.Config, and a class nested in Outer. `Outer.Config.Value`
    // can only ever mean the NESTED one -- "Outer" already names a
    // specific type, so C# never even considers the unrelated
    // namespace's same-named class. The smallest fixture that puts both
    // candidates in the same simple-name pool: this only breaks once
    // the global-uniqueness fallback sees a same-named type ANYWHERE in
    // the corpus and cannot tell the two apart.
    let files = fragments_for(&[
        (
            "Shared/Config.cs",
            "namespace Shared.Config { public class Config { public const string Value = \"ns\"; } }",
        ),
        (
            "Other/Outer.cs",
            "namespace App.Other { public class Outer { public class Config { public const string Value = \"nested\"; } } }",
        ),
        (
            "Consumers/UsesNested.cs",
            "\nusing App.Other;\n\nnamespace App.Consumers;\n\npublic class UsesNested\n{\n  public string Get() => Outer.Config.Value;\n}\n",
        ),
    ]);
    let g = resolve_graph(&no_git_root(), &files);
    assert_eq!(
        member_edges_from(&g, "Consumers/UsesNested.cs"),
        vec![("App.Other.Outer+Config", 8)],
        "Outer.Config.Value must bind precisely to the NESTED Config -- never guess at the unrelated namespace-level Config"
    );
    assert!(
        !any_member_edge_targets(&g, "Shared.Config.Config"),
        "no edge of any tier may reach the namespace-level Config"
    );
}

#[test]
fn end_to_end_plain_const_on_the_outer_type_still_resolves_precisely() {
    // Control: no nesting at all. Guards the ladder change against
    // regressing the ordinary case.
    let files = fragments_for(&[
        (
            "Other/Outer.cs",
            "namespace App.Other { public static class Outer { public const string Value = \"v\"; } }",
        ),
        (
            "Consumers/UsesNested.cs",
            "\nusing App.Other;\n\nnamespace App.Consumers;\n\npublic class UsesNested\n{\n  public string Get() => Outer.Value;\n}\n",
        ),
    ]);
    let g = resolve_graph(&no_git_root(), &files);
    assert_eq!(
        member_edges_from(&g, "Consumers/UsesNested.cs"),
        vec![("App.Other.Outer", 8)]
    );
    let edge =
        find_edge(&g, |e| matches!(e, Edge::UsesMember { .. })).expect("uses-member edge present");
    match edge {
        Edge::UsesMember { member, .. } => {
            assert_eq!(member.as_deref(), Some("Value"))
        }
        _ => unreachable!(),
    }
}

#[test]
fn end_to_end_namespace_qualified_head_walks_nested_types_to_the_leaf() {
    let files = fragments_for(&[
        (
            "Other/Outer.cs",
            "namespace App.Other { public static class Outer { public static class Middle { public static class Leaf { public const string Value = \"v\"; } } } }",
        ),
        (
            "Consumers/UsesNested.cs",
            "\nnamespace App.Consumers;\n\npublic class UsesNested\n{\n  public string Get() => App.Other.Outer.Middle.Leaf.Value;\n}\n",
        ),
    ]);
    let g = resolve_graph(&no_git_root(), &files);
    assert_eq!(
        member_edges_from(&g, "Consumers/UsesNested.cs"),
        vec![("App.Other.Outer+Middle+Leaf", 6)],
        "a namespace-qualified head walks every nested level to the leaf, with no using at all"
    );
    let edge =
        find_edge(&g, |e| matches!(e, Edge::UsesMember { .. })).expect("uses-member edge present");
    match edge {
        Edge::UsesMember { member, .. } => {
            assert_eq!(member.as_deref(), Some("Value"))
        }
        _ => unreachable!(),
    }
    assert!(
        !any_member_edge_targets(&g, "App.Other.Outer"),
        "no uses-member edge, precise or heuristic, may target the outer container"
    );
}

#[test]
fn end_to_end_repeated_nested_leaf_name_binds_the_named_container() {
    let files = fragments_for(&[
        (
            "Other/Constants.cs",
            "namespace App.Other { public static class Constants { public static class SalesInvoice { public static class FormField { public const string RecId = \"a\"; } } public static class PurchaseOrder { public static class FormField { public const string RecId = \"b\"; } } } }",
        ),
        (
            "Consumers/UsesNested.cs",
            "\nnamespace App.Consumers;\n\npublic class UsesNested\n{\n  public string Get() => App.Other.Constants.SalesInvoice.FormField.RecId;\n}\n",
        ),
    ]);
    let g = resolve_graph(&no_git_root(), &files);
    assert_eq!(
        member_edges_from(&g, "Consumers/UsesNested.cs"),
        vec![("App.Other.Constants+SalesInvoice+FormField", 6)],
        "two same-named FormField leaves under different containers -- the qualifier's own container segment picks the right one"
    );
    let edge =
        find_edge(&g, |e| matches!(e, Edge::UsesMember { .. })).expect("uses-member edge present");
    match edge {
        Edge::UsesMember { member, .. } => {
            assert_eq!(member.as_deref(), Some("RecId"))
        }
        _ => unreachable!(),
    }
    assert!(
        !any_member_edge_targets(&g, "App.Other.Constants"),
        "no uses-member edge, precise or heuristic, may target the outer container"
    );
}

#[test]
fn end_to_end_repeated_nested_leaf_name_binds_through_a_using_head() {
    let files = fragments_for(&[
        (
            "Other/Constants.cs",
            "namespace App.Other { public static class Constants { public static class SalesInvoice { public static class FormField { public const string RecId = \"a\"; } } public static class PurchaseOrder { public static class FormField { public const string RecId = \"b\"; } } } }",
        ),
        (
            "Consumers/UsesNested.cs",
            "\nusing App.Other;\n\nnamespace App.Consumers;\n\npublic class UsesNested\n{\n  public string Get() => Constants.SalesInvoice.FormField.RecId;\n}\n",
        ),
    ]);
    let g = resolve_graph(&no_git_root(), &files);
    assert_eq!(
        member_edges_from(&g, "Consumers/UsesNested.cs"),
        vec![("App.Other.Constants+SalesInvoice+FormField", 8)],
        "the same walk through a using-resolved head instead of a namespace-qualified one"
    );
    let edge =
        find_edge(&g, |e| matches!(e, Edge::UsesMember { .. })).expect("uses-member edge present");
    match edge {
        Edge::UsesMember { member, .. } => {
            assert_eq!(member.as_deref(), Some("RecId"))
        }
        _ => unreachable!(),
    }
    assert!(
        !any_member_edge_targets(&g, "App.Other.Constants"),
        "no uses-member edge, precise or heuristic, may target the outer container"
    );
}

#[test]
fn end_to_end_nested_enum_two_levels_deep_behind_a_namespace_qualified_head() {
    let files = fragments_for(&[
        (
            "Other/Box.cs",
            "namespace App.Other { public static class Box { public static class Inner { public enum Kind { First, Second } } } }",
        ),
        (
            "Consumers/UsesNested.cs",
            "\nnamespace App.Consumers;\n\npublic class UsesNested\n{\n  public object Get() => App.Other.Box.Inner.Kind.First;\n}\n",
        ),
    ]);
    let g = resolve_graph(&no_git_root(), &files);
    assert_eq!(
        member_edges_from(&g, "Consumers/UsesNested.cs"),
        vec![("App.Other.Box+Inner+Kind.First", 6)],
        "the enum sits two nesting levels deep behind a namespace-qualified head"
    );
    let edge =
        find_edge(&g, |e| matches!(e, Edge::UsesMember { .. })).expect("uses-member edge present");
    match edge {
        Edge::UsesMember { member, .. } => {
            assert_eq!(member.as_deref(), Some("First"))
        }
        _ => unreachable!(),
    }
    assert!(
        !any_member_edge_targets(&g, "App.Other.Box"),
        "no uses-member edge, precise or heuristic, may target the outer container"
    );
}

#[test]
fn end_to_end_instance_receiver_named_like_a_type_keeps_its_receiver_typed_edge() {
    // `Settings` is BOTH a field of type JobSettings in the reference
    // site's class and a globally unique type name with a nested `Retry`.
    // The extractor types the field, so the chain is an instance access:
    // the nested walk and the nested-segment suppression must both stand
    // aside and leave the receiver tiers to bind JobSettings.Retry.
    let files = fragments_for(&[
        (
            "Domain/Settings.cs",
            "namespace App.Domain { public class Settings { public class Retry { public const int Max = 3; } } }",
        ),
        (
            "Web/Job.cs",
            "\nnamespace App.Web;\n\npublic class RetryPolicy { public int Max { get; set; } }\npublic class JobSettings { public RetryPolicy Retry { get; set; } }\npublic class Job\n{\n  private readonly JobSettings Settings;\n  public int M() => Settings.Retry.Max;\n}\n",
        ),
    ]);
    // The second window (`Settings.Retry` with member `Max`) is a
    // separate matter: the tail-name fallback in step 4 still answers it
    // by the globally unique `Retry`, which is the qualified-name
    // fallback's own problem and not the nested walk's -- this test pins
    // only the first window and the walk's abstention.
    let g = resolve_graph(&no_git_root(), &files);
    assert!(
        !any_member_edge_targets(&g, "App.Domain.Settings"),
        "the field access `Settings.Retry` never binds the same-named type: {:?}",
        member_edges_from(&g, "Web/Job.cs")
    );
    assert!(
        member_edges_from(&g, "Web/Job.cs").contains(&("App.Web.JobSettings", 9)),
        "the receiver-typed edge to JobSettings.Retry survives: {:?}",
        member_edges_from(&g, "Web/Job.cs")
    );
}

// --- tier (f): extension methods ---
//
// Real fixtures run through this crate's own extractor, so an extension fact
// that never gets recorded fails here rather than passing vacuously.