Skip to main content

relay_knowledge/code/index/deleted_symbols/
mod.rs

1//! Extracts deleted symbol names from versioned source diffs.
2
3use std::{
4    collections::BTreeSet,
5    path::{Path, PathBuf},
6};
7
8use crate::domain::{CodeRepositoryRegistration, CodeRepositorySelector};
9
10use super::{MAX_INCREMENTAL_GITLINK_EXPANDED_PATHS, tracked_entry_scope_for_selector};
11use crate::code::{
12    CodeIndexError,
13    changes::{self, GitChange, diff_changes, tracked_entries_with_scope},
14    git::resolve_ref,
15    parser::parse_indexed_file,
16    scope::{self, discover_source_layout, path_is_selected_with_layout, path_scope_overlaps},
17    snapshot::SnapshotBuild,
18    source::{
19        gitlink as source_gitlink, gitlink::paths as source_gitlink_paths,
20        source_bytes_after_content_verification, source_commit_is_filesystem, source_kind,
21    },
22};
23
24/// Extracts symbol names removed by a diff so impact can include deleted APIs.
25pub fn deleted_symbol_names_for_diff(
26    registration: &CodeRepositoryRegistration,
27    selector: &CodeRepositorySelector,
28    base_ref: &str,
29    head_ref: &str,
30) -> Result<Vec<String>, CodeIndexError> {
31    let root = PathBuf::from(&registration.root_path);
32    if source_commit_is_filesystem(base_ref) || source_commit_is_filesystem(head_ref) {
33        return Ok(Vec::new());
34    }
35    if source_kind(&root)?.is_filesystem() {
36        return Ok(Vec::new());
37    }
38    let base_commit = resolve_ref(&root, base_ref)?;
39    let head_commit = resolve_ref(&root, head_ref)?;
40    let changes = diff_changes(&root, base_ref, head_ref)?;
41    let entry_scope = tracked_entry_scope_for_selector(registration, selector);
42    let base_entries = tracked_entries_with_scope(&root, &base_commit, &entry_scope)?;
43    let source_layout = discover_source_layout(&base_entries);
44    let context = DeletedSymbolContext {
45        registration,
46        selector,
47        root: &root,
48        base_commit: &base_commit,
49        source_layout: &source_layout,
50    };
51    let mut names = Vec::new();
52
53    for change in changes {
54        match change {
55            GitChange::Deleted { path } | GitChange::Renamed { old_path: path, .. } => {
56                append_deleted_symbol_names_for_removed_path(
57                    &mut names,
58                    &context,
59                    &base_entries,
60                    &path,
61                )?;
62            }
63            GitChange::AddedOrModified { path } | GitChange::TypeChanged { path } => {
64                append_deleted_symbol_names_for_gitlink_update(
65                    &mut names,
66                    &context,
67                    &head_commit,
68                    &path,
69                )?;
70            }
71            GitChange::Copied { .. } => {}
72        }
73    }
74    names.sort();
75    names.dedup();
76
77    Ok(names)
78}
79
80struct DeletedSymbolContext<'a> {
81    registration: &'a CodeRepositoryRegistration,
82    selector: &'a CodeRepositorySelector,
83    root: &'a Path,
84    base_commit: &'a str,
85    source_layout: &'a scope::SourceLayoutDiscovery,
86}
87
88fn append_deleted_symbol_names_for_removed_path(
89    names: &mut Vec<String>,
90    context: &DeletedSymbolContext<'_>,
91    base_entries: &[changes::GitTreeEntry],
92    path: &str,
93) -> Result<(), CodeIndexError> {
94    if source_gitlink::gitlink_commit_at_tree(context.root, context.base_commit, path)?.is_some() {
95        let include_expanded_path = |path: &str| {
96            path_is_selected_with_layout(
97                path,
98                context.registration,
99                context.selector,
100                context.source_layout,
101            )
102        };
103        let paths = source_gitlink_paths::bounded_expanded_paths_under_with_selector(
104            base_entries,
105            path,
106            MAX_INCREMENTAL_GITLINK_EXPANDED_PATHS,
107            &source_gitlink::GitlinkPathSelector::new(
108                &include_expanded_path,
109                &include_expanded_path,
110            ),
111        )?;
112        for path in paths {
113            append_deleted_symbol_names_for_path(names, context, context.base_commit, &path)?;
114        }
115        return Ok(());
116    }
117
118    append_deleted_symbol_names_for_path(names, context, context.base_commit, path)
119}
120
121fn append_deleted_symbol_names_for_gitlink_update(
122    names: &mut Vec<String>,
123    context: &DeletedSymbolContext<'_>,
124    head_commit: &str,
125    path: &str,
126) -> Result<(), CodeIndexError> {
127    if !path_scope_overlaps(path, context.registration, context.selector) {
128        return Ok(());
129    }
130    let include_expanded_path = |path: &str| {
131        path_is_selected_with_layout(
132            path,
133            context.registration,
134            context.selector,
135            context.source_layout,
136        )
137    };
138    let expanded_scope_overlaps =
139        |path: &str| path_scope_overlaps(path, context.registration, context.selector);
140    let child_filters = |path: &str| {
141        scope::submodule_child_scope_filters(path, context.registration, context.selector)
142    };
143    let Some(expansion) = source_gitlink::changed_gitlink_path_expansion(
144        context.root,
145        path,
146        context.base_commit,
147        head_commit,
148        MAX_INCREMENTAL_GITLINK_EXPANDED_PATHS,
149        &source_gitlink::GitlinkPathSelector::new_with_child_filters(
150            &include_expanded_path,
151            &expanded_scope_overlaps,
152            &child_filters,
153        ),
154    )?
155    else {
156        return Ok(());
157    };
158    if !expansion.base_is_gitlink {
159        append_deleted_symbol_names_for_path(names, context, context.base_commit, path)?;
160        return Ok(());
161    }
162    if expansion.base_paths.is_empty() {
163        return Ok(());
164    }
165    for path in expansion.base_paths {
166        if !path_is_selected_with_layout(
167            &path,
168            context.registration,
169            context.selector,
170            context.source_layout,
171        ) {
172            continue;
173        }
174        let mut removed = symbol_names_for_path(context, context.base_commit, &path)?;
175        if expansion.head_paths.contains(&path) {
176            let retained = symbol_names_for_path(context, head_commit, &path)?;
177            removed.retain(|name| !retained.contains(name));
178        }
179        names.extend(removed);
180    }
181
182    Ok(())
183}
184
185fn append_deleted_symbol_names_for_path(
186    names: &mut Vec<String>,
187    context: &DeletedSymbolContext<'_>,
188    commit: &str,
189    path: &str,
190) -> Result<(), CodeIndexError> {
191    if !path_is_selected_with_layout(
192        path,
193        context.registration,
194        context.selector,
195        context.source_layout,
196    ) {
197        return Ok(());
198    }
199    names.extend(symbol_names_for_path(context, commit, path)?);
200
201    Ok(())
202}
203
204fn symbol_names_for_path(
205    context: &DeletedSymbolContext<'_>,
206    commit: &str,
207    path: &str,
208) -> Result<BTreeSet<String>, CodeIndexError> {
209    let bytes = source_bytes_after_content_verification(context.root, commit, path, None)?;
210    let mut build = SnapshotBuild::new_with_selector(
211        context.registration,
212        context.selector,
213        commit.to_owned(),
214        "deleted-symbol-seed".to_owned(),
215        true,
216        1,
217        0,
218    );
219    parse_indexed_file(&mut build, path, &bytes)?;
220
221    Ok(build
222        .symbols
223        .into_iter()
224        .map(|symbol| symbol.name)
225        .collect())
226}