use super::{
BTreeSet, DirectPropagation, FallibleKind, Language, OperationAttributes, OperationEdge,
OperationEdgeKind, OperationKind, OperationNode, SemanticGraphError, SemanticOperationGraph,
SemanticRuleMatcher, SemanticRuleScope, TypeTag, cross_language_api_correspondence,
match_same_variant_rule, registered_rules,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OperationObservation {
pub source_offset: u64,
pub api_name: String,
pub type_tag: Option<TypeTag>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConstructObservation {
pub source_offset: u64,
pub kind: OperationKind,
pub fallible_kind: Option<FallibleKind>,
pub direct_propagation: Option<DirectPropagation>,
pub resource_kind: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SemanticSourceRange {
pub start: u64,
pub end: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SemanticGraphWindow {
pub graph: SemanticOperationGraph,
pub source_range: SemanticSourceRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApiNormalization {
pub graph: Option<SemanticOperationGraph>,
pub node_source_ranges: Vec<SemanticSourceRange>,
pub excluded_observations: usize,
}
pub fn normalize_registered_apis(
language: Language,
build_variant_fingerprint: [u8; 32],
observations: Vec<OperationObservation>,
) -> Result<ApiNormalization, SemanticGraphError> {
normalize_registered_observations(
language,
build_variant_fingerprint,
observations,
Vec::new(),
)
}
pub fn normalize_registered_observations(
language: Language,
build_variant_fingerprint: [u8; 32],
observations: Vec<OperationObservation>,
constructs: Vec<ConstructObservation>,
) -> Result<ApiNormalization, SemanticGraphError> {
let api_with_ranges = observations.into_iter().map(|observation| {
let range = SemanticSourceRange {
start: observation.source_offset,
end: observation.source_offset,
};
(observation, range)
});
let constructs_with_ranges = constructs.into_iter().map(|construct| {
let range = SemanticSourceRange {
start: construct.source_offset,
end: construct.source_offset,
};
(construct, range)
});
normalize_registered_observations_with_ranges(
language,
build_variant_fingerprint,
api_with_ranges.collect(),
constructs_with_ranges.collect(),
)
}
pub fn normalize_registered_observations_with_ranges(
language: Language,
build_variant_fingerprint: [u8; 32],
observations: Vec<(OperationObservation, SemanticSourceRange)>,
constructs: Vec<(ConstructObservation, SemanticSourceRange)>,
) -> Result<ApiNormalization, SemanticGraphError> {
if observations
.iter()
.map(|(_, range)| range)
.chain(constructs.iter().map(|(_, range)| range))
.any(|range| range.end < range.start)
{
return Err(SemanticGraphError::InvalidSourceRange);
}
let observation_count = observations.len();
let mut nodes: Vec<_> = observations
.into_iter()
.enumerate()
.filter_map(|(source_index, (observation, source_range))| {
let kind = registered_api_kind(language, &observation.api_name)?;
let order = observation.api_name.clone();
Some((
observation.source_offset,
source_index,
order,
source_range,
OperationNode {
kind,
attributes: OperationAttributes {
type_tag: observation.type_tag,
api_names: BTreeSet::from([observation.api_name]),
resource_kind: None,
fallible_kind: None,
direct_propagation: None,
structure_fingerprint: None,
},
},
ObservationSource::Api,
))
})
.collect();
let recognized_api_count = nodes.len();
nodes.extend(constructs.into_iter().enumerate().map(
|(source_index, (construct, source_range))| {
(
construct.source_offset,
source_index,
construct.kind.name().to_owned(),
source_range,
OperationNode {
kind: construct.kind,
attributes: OperationAttributes {
fallible_kind: construct.fallible_kind,
direct_propagation: construct.direct_propagation,
resource_kind: construct.resource_kind,
..OperationAttributes::default()
},
},
ObservationSource::Construct,
)
},
));
nodes.sort_by(|left, right| {
left.0
.cmp(&right.0)
.then_with(|| left.1.cmp(&right.1))
.then_with(|| left.2.cmp(&right.2))
});
nodes.dedup_by(coincident_operation);
let node_source_ranges = nodes.iter().map(|(_, _, _, range, _, _)| *range).collect();
let nodes: Vec<_> = nodes
.into_iter()
.map(|(_, _, _, _, node, _)| node)
.collect();
let excluded_observations = observation_count.saturating_sub(recognized_api_count);
if nodes.is_empty() {
return Ok(ApiNormalization {
graph: None,
node_source_ranges,
excluded_observations,
});
}
let edges = operation_edges(&nodes)?;
Ok(ApiNormalization {
graph: Some(SemanticOperationGraph::new(
language,
build_variant_fingerprint,
nodes,
edges,
)?),
node_source_ranges,
excluded_observations,
})
}
fn operation_edges(nodes: &[OperationNode]) -> Result<Vec<OperationEdge>, SemanticGraphError> {
let mut edges = (1..nodes.len())
.map(|index| {
Ok(OperationEdge {
from: u32::try_from(index - 1).map_err(|_| SemanticGraphError::GraphTooLarge)?,
to: u32::try_from(index).map_err(|_| SemanticGraphError::GraphTooLarge)?,
kind: OperationEdgeKind::Data,
})
})
.collect::<Result<Vec<_>, SemanticGraphError>>()?;
for (index, pair) in nodes.windows(2).enumerate() {
let [acquire, release] = pair else {
continue;
};
if acquire.kind == OperationKind::AcquireResource
&& release.kind == OperationKind::ReleaseResource
&& acquire.attributes.resource_kind == release.attributes.resource_kind
{
edges.push(OperationEdge {
from: u32::try_from(index).map_err(|_| SemanticGraphError::GraphTooLarge)?,
to: u32::try_from(index + 1).map_err(|_| SemanticGraphError::GraphTooLarge)?,
kind: OperationEdgeKind::ResourceLifetime,
});
}
}
Ok(edges)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ObservationSource {
Api,
Construct,
}
fn coincident_operation(
left: &mut (
u64,
usize,
String,
SemanticSourceRange,
OperationNode,
ObservationSource,
),
right: &mut (
u64,
usize,
String,
SemanticSourceRange,
OperationNode,
ObservationSource,
),
) -> bool {
left.0 == right.0
&& left.3 == right.3
&& left.4.kind == right.4.kind
&& (left.5 != right.5 || left.1 == right.1)
}
pub fn registered_semantic_windows(
normalization: &ApiNormalization,
) -> Result<Vec<SemanticGraphWindow>, SemanticGraphError> {
let Some(graph) = &normalization.graph else {
return Ok(Vec::new());
};
if graph.nodes.len() != normalization.node_source_ranges.len() {
return Err(SemanticGraphError::SourceRangeCountMismatch);
}
let mut windows = Vec::new();
for rule in registered_rules()
.iter()
.copied()
.filter(|rule| rule.scope == SemanticRuleScope::SameBuildVariant)
{
match rule.matcher {
SemanticRuleMatcher::EquivalentSequence => {
let mut start = 0;
while start < graph.nodes.len() {
while start < graph.nodes.len()
&& !rule
.pattern
.permitted_kinds
.contains(&graph.nodes[start].kind)
{
start += 1;
}
let end = graph.nodes[start..]
.iter()
.position(|node| !rule.pattern.permitted_kinds.contains(&node.kind))
.map_or(graph.nodes.len(), |length| start + length);
if start < end {
let window = semantic_graph_window(
graph,
&normalization.node_source_ranges,
start,
end,
)?;
if match_same_variant_rule(rule, &window.graph, &window.graph).is_some() {
windows.push(window);
}
}
start = end.saturating_add(1);
}
}
SemanticRuleMatcher::ExactApiSequence { api_names } => {
if !api_names.is_empty() && api_names.len() <= graph.nodes.len() {
for start in 0..=graph.nodes.len() - api_names.len() {
let window = semantic_graph_window(
graph,
&normalization.node_source_ranges,
start,
start + api_names.len(),
)?;
if match_same_variant_rule(rule, &window.graph, &window.graph).is_some() {
windows.push(window);
}
}
}
}
SemanticRuleMatcher::DirectConstruct { .. } => {
for index in 0..graph.nodes.len() {
let window = semantic_graph_window(
graph,
&normalization.node_source_ranges,
index,
index + 1,
)?;
if match_same_variant_rule(rule, &window.graph, &window.graph).is_some() {
windows.push(window);
}
}
}
SemanticRuleMatcher::ResourceLifecycle => {
for index in 0..graph.nodes.len().saturating_sub(1) {
let window = semantic_graph_window(
graph,
&normalization.node_source_ranges,
index,
index + 2,
)?;
if match_same_variant_rule(rule, &window.graph, &window.graph).is_some() {
windows.push(window);
}
}
}
}
}
windows.sort_by_key(|window| window.source_range);
windows.dedup_by(|left, right| {
left.source_range == right.source_range && left.graph == right.graph
});
Ok(windows)
}
fn semantic_graph_window(
graph: &SemanticOperationGraph,
ranges: &[SemanticSourceRange],
start: usize,
end: usize,
) -> Result<SemanticGraphWindow, SemanticGraphError> {
let source_range = SemanticSourceRange {
start: ranges[start].start,
end: ranges[end - 1].end,
};
let offset = u32::try_from(start).map_err(|_| SemanticGraphError::GraphTooLarge)?;
let limit = u32::try_from(end).map_err(|_| SemanticGraphError::GraphTooLarge)?;
let edges = graph
.edges
.iter()
.filter(|edge| {
edge.from >= offset && edge.from < limit && edge.to >= offset && edge.to < limit
})
.map(|edge| OperationEdge {
from: edge.from - offset,
to: edge.to - offset,
kind: edge.kind,
})
.collect();
Ok(SemanticGraphWindow {
graph: SemanticOperationGraph::new(
graph.language,
graph.build_variant_fingerprint,
graph.nodes[start..end].to_vec(),
edges,
)?,
source_range,
})
}
fn registered_api_kind(language: Language, api_name: &str) -> Option<OperationKind> {
cross_language_api_correspondence(language, api_name)
.map(|entry| entry.operation)
.or_else(|| {
matches!(
(language, api_name),
(
Language::Rust,
"rust::ToString::to_string" | "rust::str::parse"
) | (Language::Cpp, "std::to_string" | "std::stoull")
)
.then_some(OperationKind::Map)
})
}