struct graph
This commit is contained in:
@@ -21,7 +21,7 @@ pub struct GraphEdge {
|
||||
pub to: String,
|
||||
pub label: String,
|
||||
}
|
||||
fn contains(range: &Range, p: Position) -> bool {
|
||||
pub(super) fn contains(range: &Range, p: Position) -> bool {
|
||||
(range.start.line, range.start.character) <= (p.line, p.character)
|
||||
&& (p.line, p.character) < (range.end.line, range.end.character)
|
||||
}
|
||||
@@ -72,7 +72,7 @@ fn following(symbols: &[Value], p: Position) -> Option<&Value> {
|
||||
)
|
||||
})
|
||||
}
|
||||
fn excerpt(text: &str, range: &Range) -> String {
|
||||
pub(super) fn excerpt(text: &str, range: &Range) -> String {
|
||||
text.lines()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| *i >= range.start.line as usize && *i <= range.end.line as usize)
|
||||
@@ -111,7 +111,7 @@ fn concepts<'a>(node: &'a Value, out: &mut Vec<&'a Value>) {
|
||||
concepts(child, out);
|
||||
}
|
||||
}
|
||||
fn expression(
|
||||
pub(super) fn expression(
|
||||
node: &Value,
|
||||
path: &str,
|
||||
text: &str,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
mod constraints;
|
||||
mod graph;
|
||||
mod relationships;
|
||||
mod types;
|
||||
pub use graph::ConceptGraph;
|
||||
pub use relationships::GraphOptions;
|
||||
mod transport;
|
||||
|
||||
@@ -79,7 +79,12 @@ impl Clangd {
|
||||
..location.clone()
|
||||
})
|
||||
}
|
||||
fn graph_neighbors(&self, target: &Location, up: bool) -> Result<ConceptGraph> {
|
||||
fn graph_neighbors(
|
||||
&self,
|
||||
target: &Location,
|
||||
up: bool,
|
||||
ast_cache: &mut HashMap<String, Value>,
|
||||
) -> Result<ConceptGraph> {
|
||||
let uri = self.document(&target.path)?;
|
||||
let ast = self.connection.request("textDocument/ast", json!({"textDocument":{"uri":uri},"range":{"start":target.range.start,"end":target.range.start}}))?;
|
||||
if ast["kind"] == "Concept" && ast["role"] == "declaration" {
|
||||
@@ -93,6 +98,10 @@ impl Clangd {
|
||||
});
|
||||
return Ok(graph);
|
||||
}
|
||||
let ast = self.declaration_ast(target, ast, ast_cache)?;
|
||||
if super::types::is_type(&ast) {
|
||||
return self.type_structure(target, &ast, up);
|
||||
}
|
||||
let items = self.connection.request(
|
||||
"textDocument/prepareCallHierarchy",
|
||||
json!({"textDocument":{"uri":uri},"position":target.range.start}),
|
||||
@@ -100,7 +109,7 @@ impl Clangd {
|
||||
let item = items
|
||||
.as_array()
|
||||
.and_then(|a| a.first())
|
||||
.ok_or("Select a concept or function name to view its graph.")?;
|
||||
.ok_or("Select a concept, function, class, or struct name to view its graph.")?;
|
||||
let root = self.location(
|
||||
item["uri"].as_str().ok_or("Missing function URI")?,
|
||||
&item["selectionRange"],
|
||||
@@ -152,6 +161,9 @@ impl Clangd {
|
||||
label: "called by".into(),
|
||||
});
|
||||
}
|
||||
if up {
|
||||
self.function_constraints(target, &ast, &mut graph)?;
|
||||
}
|
||||
Ok(graph)
|
||||
}
|
||||
pub(super) fn relationship_graph(
|
||||
@@ -185,7 +197,9 @@ impl Clangd {
|
||||
let mut visited = HashSet::new();
|
||||
let mut symbols = HashMap::<(String, u32, u32), Location>::new();
|
||||
let mut edge_keys = HashSet::new();
|
||||
let mut structures = HashMap::new();
|
||||
let mut requests = 0;
|
||||
let mut ast_cache = HashMap::new();
|
||||
while let Some((location, parent, depth, up)) = queue.pop_front() {
|
||||
if depth >= options.depth || !visited.insert((key(&location), up)) {
|
||||
continue;
|
||||
@@ -195,7 +209,7 @@ impl Clangd {
|
||||
break;
|
||||
}
|
||||
requests += 1;
|
||||
let local = match self.graph_neighbors(&location, up) {
|
||||
let local = match self.graph_neighbors(&location, up, &mut ast_cache) {
|
||||
Ok(local) => local,
|
||||
Err(error) if depth == 0 => return Err(error),
|
||||
Err(error) => {
|
||||
@@ -205,6 +219,10 @@ impl Clangd {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let local_is_type = local
|
||||
.warnings
|
||||
.iter()
|
||||
.any(|w| w.starts_with("Type graphs show"));
|
||||
if graph.nodes.is_empty() {
|
||||
let mut root = local.nodes[0].clone();
|
||||
root.location = target.clone();
|
||||
@@ -222,16 +240,18 @@ impl Clangd {
|
||||
if start.elapsed().as_secs() >= 30 || graph.nodes.len() >= 150 {
|
||||
graph
|
||||
.warnings
|
||||
.push("Graph reached its 150-node limit.".into());
|
||||
.push("Graph reached its 150-node or 30-second budget.".into());
|
||||
break;
|
||||
}
|
||||
let symbol_node = matches!(node.kind.as_str(), "concept" | "function" | "usage")
|
||||
&& node.label
|
||||
!= format!(
|
||||
"{}:{}",
|
||||
node.location.path,
|
||||
node.location.range.start.line + 1
|
||||
);
|
||||
let symbol_node = matches!(
|
||||
node.kind.as_str(),
|
||||
"concept" | "function" | "type" | "field" | "usage"
|
||||
) && node.label
|
||||
!= format!(
|
||||
"{}:{}",
|
||||
node.location.path,
|
||||
node.location.range.start.line + 1
|
||||
);
|
||||
let old_id = node.id.clone();
|
||||
if symbol_node {
|
||||
let symbol_key = key(&node.location);
|
||||
@@ -255,6 +275,12 @@ impl Clangd {
|
||||
let ast = self.connection.request("textDocument/ast", json!({"textDocument":{"uri":uri},"range":{"start":node.location.range.start,"end":node.location.range.start}}))?;
|
||||
node.kind = match ast["kind"].as_str() {
|
||||
Some("Concept") => "concept",
|
||||
Some(
|
||||
"CXXRecord"
|
||||
| "ClassTemplate"
|
||||
| "ClassTemplateSpecialization"
|
||||
| "Record",
|
||||
) => "type",
|
||||
Some(
|
||||
"Function" | "FunctionTemplate" | "CXXMethod" | "CXXConstructor"
|
||||
| "CXXDestructor",
|
||||
@@ -265,10 +291,16 @@ impl Clangd {
|
||||
}
|
||||
}
|
||||
let node_key = key(&node.location);
|
||||
let structure_key = (
|
||||
node_key.clone(),
|
||||
node.location.range.end.line,
|
||||
node.location.range.end.character,
|
||||
node.kind.clone(),
|
||||
);
|
||||
let id = if symbol_node {
|
||||
known.get(&node_key).cloned()
|
||||
} else {
|
||||
None
|
||||
structures.get(&structure_key).cloned()
|
||||
};
|
||||
let id = id.unwrap_or_else(|| format!("g{}", graph.nodes.len()));
|
||||
mapping.insert(old_id, id.clone());
|
||||
@@ -277,10 +309,19 @@ impl Clangd {
|
||||
if symbol_node {
|
||||
known.insert(node_key, id.clone());
|
||||
}
|
||||
if !symbol_node {
|
||||
structures.insert(structure_key, id.clone());
|
||||
}
|
||||
graph.nodes.push(node.clone());
|
||||
}
|
||||
if symbol_node && matches!(node.kind.as_str(), "concept" | "function") {
|
||||
queue.push_back((node.location, id, depth + 1, up));
|
||||
if symbol_node && matches!(node.kind.as_str(), "concept" | "function" | "type") {
|
||||
// A type's members expand towards their dependencies/callees,
|
||||
// even though membership is drawn below the containing type.
|
||||
let direction = if local_is_type { true } else { up };
|
||||
queue.push_back((node.location.clone(), id.clone(), depth + 1, direction));
|
||||
if node.kind == "type" {
|
||||
queue.push_back((node.location, id, depth + 1, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
for edge in local.edges {
|
||||
|
||||
@@ -903,3 +903,221 @@ fn function_graph_depth_namespaces_and_cycles() {
|
||||
);
|
||||
server.stop();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires clangd"]
|
||||
fn type_graph_constraints_and_members() {
|
||||
let temp = tempdir().unwrap();
|
||||
let root = temp.path();
|
||||
let build = root.join("build");
|
||||
fs::create_dir_all(build.join("meson-info")).unwrap();
|
||||
fs::write(build.join("meson-info/intro-projectinfo.json"), "{}").unwrap();
|
||||
fs::write(
|
||||
root.join("types.hpp"),
|
||||
include_str!("../../../../examples/types/types.hpp"),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
root.join("main.cpp"),
|
||||
include_str!("../../../../examples/types/main.cpp"),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(build.join("compile_commands.json"),serde_json::to_vec(&json!([{"directory":root,"file":root.join("main.cpp"),"arguments":["clang++","-std=c++23","-c",root.join("main.cpp")]}])).unwrap()).unwrap();
|
||||
let server = Clangd::start(crate::open(root, Some(&build), false).unwrap()).unwrap();
|
||||
let source = include_str!("../../../../examples/types/types.hpp");
|
||||
let position = |needle: &str| {
|
||||
let (line, text) = source
|
||||
.lines()
|
||||
.enumerate()
|
||||
.find(|(_, l)| l.contains(needle))
|
||||
.unwrap();
|
||||
Position {
|
||||
line: line as u32,
|
||||
character: (text.find(needle).unwrap() + 1) as u32,
|
||||
}
|
||||
};
|
||||
let pos = position("Box :");
|
||||
let graph = server
|
||||
.graph(
|
||||
"types.hpp",
|
||||
pos,
|
||||
&GraphOptions {
|
||||
depth: 1,
|
||||
excluded_namespaces: vec![],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
for label in [
|
||||
"demo::Base",
|
||||
"demo::Helper",
|
||||
"demo::Box::value",
|
||||
"demo::Box::helper",
|
||||
"demo::Box::set",
|
||||
"demo::Box::run",
|
||||
"demo::Addable",
|
||||
"demo::Sized",
|
||||
"std::copyable",
|
||||
"std::constructible_from",
|
||||
"std::integral",
|
||||
"std::convertible_to",
|
||||
] {
|
||||
assert!(
|
||||
graph.nodes.iter().any(|n| n.label == label),
|
||||
"Missing {label}: {graph:?}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
graph
|
||||
.nodes
|
||||
.iter()
|
||||
.any(|n| n.kind == "requires" && n.detail.contains("u.size()"))
|
||||
);
|
||||
assert!(
|
||||
graph
|
||||
.nodes
|
||||
.iter()
|
||||
.any(|n| n.kind == "parameter" && n.label == "argument x")
|
||||
);
|
||||
assert!(
|
||||
graph
|
||||
.nodes
|
||||
.iter()
|
||||
.any(|n| n.kind == "template-parameter" && n.label == "template T")
|
||||
);
|
||||
let default_graph = server
|
||||
.graph("types.hpp", pos, &GraphOptions::default())
|
||||
.unwrap();
|
||||
assert!(
|
||||
!default_graph
|
||||
.nodes
|
||||
.iter()
|
||||
.any(|n| n.label.starts_with("std::"))
|
||||
);
|
||||
let node = |label: &str| graph.nodes.iter().find(|n| n.label == label).unwrap();
|
||||
let edge = |from: &str, to: &str, label: &str| {
|
||||
graph
|
||||
.edges
|
||||
.iter()
|
||||
.any(|e| e.from == from && e.to == to && e.label == label)
|
||||
};
|
||||
assert!(edge(&node("demo::Base").id, "selected", "base of"));
|
||||
assert!(edge(
|
||||
&node("demo::Helper").id,
|
||||
&node("demo::Box::helper").id,
|
||||
"type of"
|
||||
));
|
||||
assert!(edge(
|
||||
&node("template T").id,
|
||||
&node("demo::Box::value").id,
|
||||
"type of"
|
||||
));
|
||||
assert!(edge(
|
||||
&node("std::integral").id,
|
||||
&node("argument n").id,
|
||||
"constrains"
|
||||
));
|
||||
let u = graph
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| {
|
||||
n.label == "template U" && edge(&n.id, &node("demo::Box::set").id, "template parameter")
|
||||
})
|
||||
.unwrap();
|
||||
let argument = graph
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| n.label == "argument u" && edge(&n.id, &node("demo::Box::set").id, "argument"))
|
||||
.unwrap();
|
||||
assert!(edge(&node("demo::Sized").id, &u.id, "constrains"));
|
||||
assert!(edge(&u.id, &argument.id, "type of"));
|
||||
let calls = server
|
||||
.graph(
|
||||
"types.hpp",
|
||||
pos,
|
||||
&GraphOptions {
|
||||
depth: 2,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let work = calls
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| n.label == "demo::Helper::work")
|
||||
.unwrap();
|
||||
let set = calls
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| n.label == "demo::Box::set")
|
||||
.unwrap();
|
||||
assert!(
|
||||
calls
|
||||
.edges
|
||||
.iter()
|
||||
.any(|e| e.from == work.id && e.to == set.id && e.label == "called by")
|
||||
);
|
||||
assert!(
|
||||
graph
|
||||
.nodes
|
||||
.iter()
|
||||
.any(|n| n.kind == "operator" && n.label == "||")
|
||||
);
|
||||
assert!(
|
||||
graph
|
||||
.nodes
|
||||
.iter()
|
||||
.any(|n| n.kind == "type" && n.label.starts_with("std::vector")),
|
||||
"{graph:?}"
|
||||
);
|
||||
let method = server
|
||||
.graph(
|
||||
"types.hpp",
|
||||
position("set("),
|
||||
&GraphOptions {
|
||||
depth: 1,
|
||||
excluded_namespaces: vec![],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(method.title.ends_with("::set"), "{method:?}");
|
||||
assert!(
|
||||
method.nodes.iter().any(|n| n.label == "demo::Sized"),
|
||||
"{method:?}"
|
||||
);
|
||||
let constructor = server
|
||||
.graph(
|
||||
"types.hpp",
|
||||
position("Box(Addable"),
|
||||
&GraphOptions {
|
||||
depth: 1,
|
||||
excluded_namespaces: vec![],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
constructor
|
||||
.nodes
|
||||
.iter()
|
||||
.any(|n| n.label == "std::constructible_from"),
|
||||
"{constructor:?}"
|
||||
);
|
||||
let alias = server
|
||||
.graph("types.hpp", position("IntBox ="), &GraphOptions::default())
|
||||
.unwrap();
|
||||
assert!(
|
||||
alias.title == "demo::Box"
|
||||
|| alias
|
||||
.nodes
|
||||
.iter()
|
||||
.any(|n| n.kind == "type" && n.label == "demo::Box"),
|
||||
"{alias:?}"
|
||||
);
|
||||
let enumeration = server
|
||||
.graph("types.hpp", position("State {"), &GraphOptions::default())
|
||||
.unwrap();
|
||||
assert!(
|
||||
enumeration.nodes.iter().any(|n| n.label == "idle"),
|
||||
"{enumeration:?}"
|
||||
);
|
||||
server.stop();
|
||||
}
|
||||
|
||||
460
crates/cex-core/src/analysis/types.rs
Normal file
460
crates/cex-core/src/analysis/types.rs
Normal file
@@ -0,0 +1,460 @@
|
||||
//! Type structure and signature constraints come from clangd's AST and symbols.
|
||||
use super::graph::{ConceptGraph, GraphEdge, GraphNode, contains, excerpt, expression};
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn children(node: &Value) -> impl Iterator<Item = &Value> {
|
||||
node["children"].as_array().into_iter().flatten()
|
||||
}
|
||||
pub(super) fn is_record(node: &Value) -> bool {
|
||||
matches!(
|
||||
node["kind"].as_str(),
|
||||
Some(
|
||||
"CXXRecord"
|
||||
| "Record"
|
||||
| "ClassTemplate"
|
||||
| "ClassTemplateSpecialization"
|
||||
| "ClassTemplatePartialSpecialization"
|
||||
)
|
||||
) && node["role"] == "declaration"
|
||||
}
|
||||
pub(super) fn is_type(node: &Value) -> bool {
|
||||
is_record(node)
|
||||
|| node["role"] == "declaration"
|
||||
&& matches!(
|
||||
node["kind"].as_str(),
|
||||
Some("Enum" | "TypeAlias" | "Typedef" | "TypeAliasTemplate")
|
||||
)
|
||||
}
|
||||
fn is_function(node: &Value) -> bool {
|
||||
matches!(
|
||||
node["kind"].as_str(),
|
||||
Some("Function" | "CXXMethod" | "CXXConstructor" | "CXXDestructor" | "CXXConversion")
|
||||
) && node["role"] == "declaration"
|
||||
}
|
||||
fn range(node: &Value) -> Option<Range> {
|
||||
serde_json::from_value(node["range"].clone()).ok()
|
||||
}
|
||||
fn declaration<'a>(symbols: &'a [Value], node: &Value) -> Option<&'a Value> {
|
||||
let r = range(node)?;
|
||||
let body_start = children(node)
|
||||
.find(|c| c["kind"] == "Compound")
|
||||
.and_then(range)
|
||||
.map(|r| (r.start.line, r.start.character));
|
||||
for s in symbols {
|
||||
if let Some(found) = s["children"].as_array().and_then(|c| declaration(c, node)) {
|
||||
return Some(found);
|
||||
}
|
||||
if let Ok(selection) = serde_json::from_value::<Range>(s["selectionRange"].clone())
|
||||
&& contains(&r, selection.start)
|
||||
&& body_start.is_none_or(|p| (selection.start.line, selection.start.character) < p)
|
||||
&& (s["name"] == node["detail"]
|
||||
|| (is_function(node) || node["kind"] == "FunctionTemplate")
|
||||
&& matches!(s["kind"].as_u64(), Some(6 | 9 | 12)))
|
||||
{
|
||||
return Some(s);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
struct Builder<'a> {
|
||||
path: &'a str,
|
||||
text: &'a str,
|
||||
symbols: &'a [Value],
|
||||
graph: ConceptGraph,
|
||||
templates: HashMap<String, String>,
|
||||
}
|
||||
impl Builder<'_> {
|
||||
fn add(
|
||||
&mut self,
|
||||
node: &Value,
|
||||
kind: &str,
|
||||
label: String,
|
||||
owner: &str,
|
||||
relation: &str,
|
||||
above: bool,
|
||||
) -> Option<String> {
|
||||
if self.graph.nodes.len() >= 120 {
|
||||
return None;
|
||||
}
|
||||
let r = range(node)?;
|
||||
let mut location = declaration(self.symbols, node)
|
||||
.and_then(|s| serde_json::from_value(s["selectionRange"].clone()).ok())
|
||||
.unwrap_or_else(|| r.clone());
|
||||
if kind == "type" {
|
||||
for qualifier in children(node).filter(|c| c["role"] == "specifier") {
|
||||
if let Some(r) = range(qualifier) {
|
||||
location.start = r.end;
|
||||
}
|
||||
}
|
||||
}
|
||||
let id = format!("type{}", self.graph.nodes.len());
|
||||
self.graph.nodes.push(GraphNode {
|
||||
id: id.clone(),
|
||||
label,
|
||||
kind: kind.into(),
|
||||
detail: format!(
|
||||
"{}:{}\n{}",
|
||||
self.path,
|
||||
r.start.line + 1,
|
||||
excerpt(self.text, &r)
|
||||
),
|
||||
location: Location {
|
||||
path: self.path.into(),
|
||||
range: location,
|
||||
label: String::new(),
|
||||
},
|
||||
});
|
||||
self.graph.edges.push(GraphEdge {
|
||||
from: if above { id.clone() } else { owner.into() },
|
||||
to: if above { owner.into() } else { id.clone() },
|
||||
label: relation.into(),
|
||||
});
|
||||
Some(id)
|
||||
}
|
||||
fn constraint(&mut self, node: &Value, owner: &str) {
|
||||
let old = self.graph.edges.len();
|
||||
expression(node, self.path, self.text, &mut self.graph, owner, 0);
|
||||
for edge in &mut self.graph.edges[old..] {
|
||||
if edge.to == owner {
|
||||
edge.label = "constrains".into();
|
||||
}
|
||||
}
|
||||
}
|
||||
fn signature(&mut self, node: &Value, owner: &str) {
|
||||
// Only signature children: bodies, default arguments, and constructor
|
||||
// initializers must not be mistaken for associated constraints.
|
||||
let function = if is_function(node) {
|
||||
Some(node)
|
||||
} else {
|
||||
children(node).find(|c| is_function(c))
|
||||
};
|
||||
let mut templates = self.templates.clone();
|
||||
let mut params = Vec::new();
|
||||
if let Some(function) = function {
|
||||
for proto in children(function).filter(|c| c["kind"] == "FunctionProto") {
|
||||
for param in children(proto).filter(|c| c["kind"] == "ParmVar") {
|
||||
let label = format!(
|
||||
"argument {}",
|
||||
param["detail"].as_str().unwrap_or("(unnamed)")
|
||||
);
|
||||
if let Some(id) = self.add(param, "parameter", label, owner, "argument", true) {
|
||||
params.push((param, id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for child in children(node) {
|
||||
if matches!(
|
||||
child["kind"].as_str(),
|
||||
Some("TemplateTypeParm" | "NonTypeTemplateParm" | "TemplateTemplateParm")
|
||||
) && child["role"] == "declaration"
|
||||
{
|
||||
// Abbreviated function templates synthesize an `x:auto` template
|
||||
// parameter; attach its concept to the actual argument x.
|
||||
let name = child["detail"].as_str().unwrap_or("(unnamed)");
|
||||
let argument = name
|
||||
.strip_suffix(":auto")
|
||||
.and_then(|name| params.iter().find(|(p, _)| p["detail"] == name))
|
||||
.or_else(|| {
|
||||
range(child).and_then(|r| {
|
||||
params
|
||||
.iter()
|
||||
.find(|(p, _)| range(p).is_some_and(|pr| contains(&pr, r.start)))
|
||||
})
|
||||
});
|
||||
let id = if let Some((_, id)) = argument {
|
||||
Some(id.clone())
|
||||
} else {
|
||||
self.add(
|
||||
child,
|
||||
"template-parameter",
|
||||
format!("template {name}"),
|
||||
owner,
|
||||
"template parameter",
|
||||
true,
|
||||
)
|
||||
};
|
||||
if let Some(id) = id {
|
||||
templates.insert(name.to_owned(), id.clone());
|
||||
for concept in children(child)
|
||||
.filter(|c| c["kind"] == "Concept" && c["role"] == "reference")
|
||||
{
|
||||
self.constraint(concept, &id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if child["role"] == "expression" {
|
||||
self.constraint(child, owner);
|
||||
}
|
||||
}
|
||||
if let Some(function) = function
|
||||
&& !std::ptr::eq(function, node)
|
||||
{
|
||||
for child in children(function).filter(|c| c["role"] == "expression") {
|
||||
self.constraint(child, owner);
|
||||
}
|
||||
}
|
||||
if matches!(
|
||||
node["kind"].as_str(),
|
||||
Some("ClassTemplate" | "TypeAliasTemplate" | "ClassTemplatePartialSpecialization")
|
||||
) {
|
||||
self.templates = templates.clone();
|
||||
}
|
||||
// Some clangd versions expose constrained auto under the parameter type.
|
||||
for (param, id) in params {
|
||||
self.parameter_types(param, &id, &templates);
|
||||
let mut stack: Vec<_> = children(param).filter(|c| c["role"] == "type").collect();
|
||||
while let Some(n) = stack.pop() {
|
||||
if n["kind"] == "Concept" && n["role"] == "reference" {
|
||||
self.constraint(n, &id);
|
||||
} else {
|
||||
stack.extend(
|
||||
children(n).filter(|c| c["role"] == "type" || c["role"] == "reference"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn parameter_types(&mut self, node: &Value, owner: &str, templates: &HashMap<String, String>) {
|
||||
let mut stack: Vec<_> = children(node).filter(|n| n["role"] == "type").collect();
|
||||
let mut seen = HashSet::new();
|
||||
let mut visited = 0;
|
||||
while let Some(n) = stack.pop() {
|
||||
visited += 1;
|
||||
if visited > 1000 {
|
||||
break;
|
||||
}
|
||||
if n["kind"] == "TemplateTypeParm"
|
||||
&& let Some(id) = n["detail"].as_str().and_then(|name| templates.get(name))
|
||||
&& id != owner
|
||||
&& seen.insert(id.clone())
|
||||
{
|
||||
self.graph.edges.push(GraphEdge {
|
||||
from: id.clone(),
|
||||
to: owner.into(),
|
||||
label: "type of".into(),
|
||||
});
|
||||
}
|
||||
stack.extend(
|
||||
children(n).filter(|n| n["role"] == "type" || n["role"] == "template argument"),
|
||||
);
|
||||
}
|
||||
}
|
||||
fn composed_types(&mut self, node: &Value, owner: &str, relation: &str) {
|
||||
self.parameter_types(node, owner, &self.templates.clone());
|
||||
let mut stack: Vec<_> = children(node)
|
||||
.filter(|n| n["role"] == "type" || n["role"] == "base")
|
||||
.collect();
|
||||
let mut visited = 0;
|
||||
while let Some(n) = stack.pop() {
|
||||
visited += 1;
|
||||
if visited > 300 || self.graph.nodes.len() >= 120 {
|
||||
break;
|
||||
}
|
||||
if matches!(
|
||||
n["kind"].as_str(),
|
||||
Some("Record" | "TemplateSpecialization" | "Typedef" | "Enum")
|
||||
) && n["role"] == "type"
|
||||
{
|
||||
if let Some(r) = range(n) {
|
||||
self.add(n, "type", excerpt(self.text, &r), owner, relation, true);
|
||||
}
|
||||
} else if n["kind"] == "Builtin" {
|
||||
if let Some(r) = range(n) {
|
||||
self.add(
|
||||
n,
|
||||
"primitive",
|
||||
excerpt(self.text, &r),
|
||||
owner,
|
||||
relation,
|
||||
true,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
stack.extend(children(n).filter(|n| n["role"] == "type" || n["role"] == "base"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Clangd {
|
||||
pub(super) fn declaration_ast(
|
||||
&self,
|
||||
target: &Location,
|
||||
point_ast: Value,
|
||||
cache: &mut HashMap<String, Value>,
|
||||
) -> Result<Value> {
|
||||
// Fetch the file AST only for declarations whose template envelope may
|
||||
// have been omitted by the point query. Included headers are not expanded.
|
||||
if !is_type(&point_ast) && !is_function(&point_ast) {
|
||||
return Ok(point_ast);
|
||||
}
|
||||
let uri = self.document(&target.path)?;
|
||||
if !cache.contains_key(&target.path) {
|
||||
let full = self
|
||||
.connection
|
||||
.request("textDocument/ast", json!({"textDocument":{"uri":uri}}))?;
|
||||
cache.insert(target.path.clone(), full);
|
||||
}
|
||||
let full = &cache[&target.path];
|
||||
let mut stack = vec![full];
|
||||
let mut count = 0;
|
||||
while let Some(n) = stack.pop() {
|
||||
count += 1;
|
||||
if count > 100000 {
|
||||
return Err("Declaration AST exceeds the 100,000-node inspection limit.".into());
|
||||
}
|
||||
if let Some(r) = range(n)
|
||||
&& !contains(&r, target.range.start)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if matches!(
|
||||
n["kind"].as_str(),
|
||||
Some("ClassTemplate" | "FunctionTemplate" | "TypeAliasTemplate")
|
||||
) && children(n)
|
||||
.any(|c| (is_function(c) || is_type(c)) && c["range"] == point_ast["range"])
|
||||
{
|
||||
return Ok(n.clone());
|
||||
}
|
||||
stack.extend(children(n));
|
||||
}
|
||||
Ok(point_ast)
|
||||
}
|
||||
pub(super) fn type_structure(
|
||||
&self,
|
||||
target: &Location,
|
||||
ast: &Value,
|
||||
up: bool,
|
||||
) -> Result<ConceptGraph> {
|
||||
let text = self.read_target(&target.path)?.content;
|
||||
let symbols = self
|
||||
.query(&target.path, target.range.start, Query::Symbols)?
|
||||
.symbols;
|
||||
let mut b = Builder {
|
||||
path: &target.path,
|
||||
templates: HashMap::new(),
|
||||
text: &text,
|
||||
symbols: &symbols,
|
||||
graph: ConceptGraph {
|
||||
title: target.label.clone(),
|
||||
nodes: vec![GraphNode {
|
||||
id: "selected".into(),
|
||||
label: target.label.clone(),
|
||||
kind: "selected".into(),
|
||||
detail: range(ast).map(|r| excerpt(&text, &r)).unwrap_or_default(),
|
||||
location: target.clone(),
|
||||
}],
|
||||
edges: vec![],
|
||||
warnings: vec![],
|
||||
},
|
||||
};
|
||||
let record = if ast["kind"] == "ClassTemplate" {
|
||||
children(ast).find(|c| is_record(c))
|
||||
} else {
|
||||
Some(ast)
|
||||
};
|
||||
b.signature(ast, "selected");
|
||||
if up {
|
||||
if matches!(ast["kind"].as_str(), Some("TypeAlias" | "Typedef" | "Enum")) {
|
||||
b.composed_types(ast, "selected", "underlying type");
|
||||
}
|
||||
if ast["kind"] == "TypeAliasTemplate" {
|
||||
for alias in children(ast).filter(|c| c["kind"] == "TypeAlias") {
|
||||
b.composed_types(alias, "selected", "underlying type");
|
||||
}
|
||||
}
|
||||
if let Some(record) = record {
|
||||
for base in children(record).filter(|c| c["role"] == "base") {
|
||||
b.composed_types(base, "selected", "base of");
|
||||
}
|
||||
}
|
||||
} else if let Some(record) = record {
|
||||
if children(record)
|
||||
.filter(|c| c["role"] == "declaration")
|
||||
.count()
|
||||
> 80
|
||||
{
|
||||
b.graph
|
||||
.warnings
|
||||
.push("Only the first 80 member declarations were considered.".into());
|
||||
}
|
||||
for member in children(record)
|
||||
.filter(|c| c["role"] == "declaration")
|
||||
.take(80)
|
||||
{
|
||||
let function = if is_function(member) {
|
||||
Some(member)
|
||||
} else if member["kind"] == "FunctionTemplate" {
|
||||
children(member).find(|c| is_function(c))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (kind, relation) = if function.is_some_and(|f| f["kind"] == "CXXConstructor") {
|
||||
("function", "constructor")
|
||||
} else if function.is_some() {
|
||||
("function", "member")
|
||||
} else if is_record(member) || member["kind"] == "Enum" {
|
||||
("type", "nested type")
|
||||
} else if matches!(member["kind"].as_str(), Some("Field" | "Var")) {
|
||||
("field", "member")
|
||||
} else if matches!(
|
||||
member["kind"].as_str(),
|
||||
Some("TypeAlias" | "Typedef" | "TypeAliasTemplate")
|
||||
) {
|
||||
("type", "member alias")
|
||||
} else if member["kind"] == "EnumConstant" {
|
||||
("enumerator", "enumerator")
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let symbol = declaration(&symbols, function.unwrap_or(member));
|
||||
let label = symbol
|
||||
.and_then(|s| s["name"].as_str())
|
||||
.or(member["detail"].as_str())
|
||||
.unwrap_or("constructor")
|
||||
.to_owned();
|
||||
if let Some(id) = b.add(
|
||||
function.unwrap_or(member),
|
||||
kind,
|
||||
label,
|
||||
"selected",
|
||||
relation,
|
||||
false,
|
||||
) {
|
||||
if function.is_some() {
|
||||
b.signature(member, &id);
|
||||
} else {
|
||||
b.composed_types(member, &id, "type of");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if b.graph.nodes.len() >= 100 {
|
||||
b.graph.warnings.push("Type structure reached its node budget; some members or constraints may be omitted.".into());
|
||||
}
|
||||
b.graph.warnings.push("Type graphs show declared bases, members and signature constraints, not every template instantiation or inherited member. Set depth 2 to expand member calls; dependent calls may remain unresolved.".into());
|
||||
Ok(b.graph)
|
||||
}
|
||||
pub(super) fn function_constraints(
|
||||
&self,
|
||||
target: &Location,
|
||||
ast: &Value,
|
||||
graph: &mut ConceptGraph,
|
||||
) -> Result<()> {
|
||||
let text = self.read_target(&target.path)?.content;
|
||||
let symbols = self
|
||||
.query(&target.path, target.range.start, Query::Symbols)?
|
||||
.symbols;
|
||||
let mut b = Builder {
|
||||
path: &target.path,
|
||||
templates: HashMap::new(),
|
||||
text: &text,
|
||||
symbols: &symbols,
|
||||
graph: graph.clone(),
|
||||
};
|
||||
b.signature(ast, "selected");
|
||||
*graph = b.graph;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user