Skip to main content

cpd_semantic/
pass.rs

1//! `--semantic` as a clone pass of the finder, and the reader of functions
2//! it shares with `--compare`.
3
4use crate::search::{
5    Embedder, SemanticParams, SemanticScope, SemanticUnit, Thresholds, UnitSource,
6    find_semantic_clones,
7};
8use crate::units::{extract_units, supports_units};
9use cpd_core::models::CpdClone;
10use cpd_finder::pass::{ClonePass, PassContext, PassSource};
11use std::sync::{Arc, Mutex, MutexGuard};
12
13/// Collects the functions of the files the finder shows it, one
14/// [`UnitSource`] per detection source. [`SemanticPass`] reads files through
15/// it; `--compare` uses it alone, with no clone detection.
16#[derive(Default)]
17pub struct UnitReader {
18    sources: Mutex<Vec<UnitSource>>,
19}
20
21impl UnitReader {
22    /// The functions read since the last call, in source-id order, so that
23    /// the result does not depend on which worker read which file.
24    pub fn take_sources(&self) -> Vec<UnitSource> {
25        let mut sources = std::mem::take(&mut *self.lock());
26        sources.sort_by(|a, b| a.id.cmp(&b.id));
27        sources
28    }
29
30    fn lock(&self) -> MutexGuard<'_, Vec<UnitSource>> {
31        // A worker that panicked while pushing leaves whole entries only.
32        self.sources
33            .lock()
34            .unwrap_or_else(|poisoned| poisoned.into_inner())
35    }
36}
37
38impl ClonePass for UnitReader {
39    /// Named after `--compare`, the one mode that runs it on its own;
40    /// inside [`SemanticPass`] the pass's own name is used.
41    fn name(&self) -> &'static str {
42        "--compare"
43    }
44
45    fn reads(&self, format: &str) -> bool {
46        supports_units(format)
47    }
48
49    fn read(&self, format: &str, content: &str, sources: &[PassSource<'_>]) {
50        let maps = extract_units(content, format);
51        let found: Vec<UnitSource> = sources
52            .iter()
53            .filter_map(|source| {
54                // A component's script blocks of one format form one map;
55                // each detection source keeps the functions inside it.
56                let map = maps.iter().find(|m| m.format == source.format)?;
57                let units: Vec<SemanticUnit> = map
58                    .units
59                    .iter()
60                    .filter_map(|u| {
61                        let unit = SemanticUnit::build(
62                            u.grammar,
63                            u.name.clone(),
64                            u.start.clone(),
65                            u.end.clone(),
66                            u.text.clone(),
67                            source.spans,
68                        )?;
69                        Some(SemanticUnit {
70                            test: u.test,
71                            ..unit
72                        })
73                    })
74                    .collect();
75                (!units.is_empty()).then(|| UnitSource {
76                    id: source.id.to_string(),
77                    format: source.format.to_string(),
78                    units,
79                    path_label: Default::default(),
80                })
81            })
82            .collect();
83        if !found.is_empty() {
84            self.lock().extend(found);
85        }
86    }
87
88    /// Finds nothing: the reader only collects.
89    fn find(&self, _: &PassContext<'_>) -> Result<Vec<CpdClone>, String> {
90        Ok(Vec::new())
91    }
92}
93
94/// Finds semantic clones among the functions of the files the finder shows
95/// it: reads each file's functions while the finder holds it, then embeds
96/// and pairs them once the token passes are done.
97pub struct SemanticPass {
98    embedder: Arc<dyn Embedder>,
99    thresholds: Thresholds,
100    scope: SemanticScope,
101    reader: UnitReader,
102}
103
104impl SemanticPass {
105    /// `thresholds`: the similarities the rules need, on the scale of the
106    /// embedder's model (see `SemanticOptions::thresholds`); `scope`: pairs
107    /// within one language, across languages, or both.
108    pub fn new(embedder: Arc<dyn Embedder>, thresholds: Thresholds, scope: SemanticScope) -> Self {
109        Self {
110            embedder,
111            thresholds,
112            scope,
113            reader: UnitReader::default(),
114        }
115    }
116}
117
118impl ClonePass for SemanticPass {
119    fn name(&self) -> &'static str {
120        "--semantic"
121    }
122
123    fn reads(&self, format: &str) -> bool {
124        self.reader.reads(format)
125    }
126
127    fn read(&self, format: &str, content: &str, sources: &[PassSource<'_>]) {
128        self.reader.read(format, content, sources);
129    }
130
131    fn find(&self, context: &PassContext<'_>) -> Result<Vec<CpdClone>, String> {
132        let mut sources = self.reader.take_sources();
133        for source in &mut sources {
134            source.path_label = (context.label)(&source.id);
135        }
136        let params = SemanticParams {
137            thresholds: self.thresholds,
138            min_tokens: context.min_tokens,
139            min_lines: context.min_lines,
140            scope: self.scope,
141        };
142        find_semantic_clones(&sources, self.embedder.as_ref(), &params, context.existing)
143    }
144}