Skip to main content

relay_knowledge/code/index/
deleted_symbols.rs

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