1use std::io::{BufRead, Write};
4use std::path::{Path, PathBuf};
5
6use petgraph::visit::EdgeRef;
7use serde::{Deserialize, Serialize};
8
9use crate::error::Diagnostic;
10use crate::graph::{CodeGraph, NodeData};
11use crate::language::LangId;
12use crate::model::{
13 FileExtraction, IdGenerator, SourceRange, Symbol, SymbolId, SymbolKind, UnresolvedImport,
14 UnresolvedReference, Visibility,
15};
16use crate::output::shard::edge::{ShardEdge, validate_edge};
17use crate::output::shard::error::ShardError;
18use crate::output::shard::name::{node_belongs_to_file, normalized_path, stable_node_name};
19
20pub const SHARD_SCHEMA_VERSION: u32 = 2;
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ShardFile {
25 pub schema_version: u32,
26 pub path: PathBuf,
27 pub language: LangId,
28 pub symbols: Vec<ShardSymbol>,
29 pub imports: Vec<UnresolvedImport>,
30 pub references: Vec<UnresolvedReference>,
31 pub diagnostics: Vec<Diagnostic>,
32 pub ast_node_count: usize,
33 pub edges: Vec<ShardEdge>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ShardSymbol {
39 pub name: String,
40 pub kind: SymbolKind,
41 pub source_range: SourceRange,
42 pub visibility: Option<Visibility>,
43 pub signature: Option<String>,
44 pub docstring: Option<String>,
45 pub is_async: bool,
46}
47
48#[derive(Debug)]
50pub struct LoadedShard {
51 pub file: FileExtraction,
52 pub edges: Vec<ShardEdge>,
53}
54
55impl ShardFile {
56 pub fn from_extraction(
58 extraction: &FileExtraction,
59 graph: &CodeGraph,
60 ) -> Result<Self, ShardError> {
61 normalized_path(&extraction.path)?;
62 for diagnostic in &extraction.diagnostics {
63 normalized_path(&diagnostic.path)?;
64 }
65 let symbols = extraction.symbols.iter().map(ShardSymbol::from).collect();
66 let mut edges = graph
67 .graph()
68 .edge_references()
69 .filter(|edge| {
70 !matches!(graph.graph()[edge.source()], NodeData::Data(_))
71 && !matches!(graph.graph()[edge.target()], NodeData::Data(_))
72 && (node_belongs_to_file(graph, edge.source(), &extraction.path)
73 || node_belongs_to_file(graph, edge.target(), &extraction.path))
74 })
75 .map(|edge| {
76 let source_name = stable_node_name(graph, edge.source())?;
77 let target_name = stable_node_name(graph, edge.target())?;
78 Ok(ShardEdge {
79 source_name,
80 target_name,
81 kind: edge.weight().kind.into(),
82 confidence: edge.weight().confidence,
83 flow_kind: edge.weight().flow_kind.map(Into::into),
84 })
85 })
86 .collect::<Result<Vec<_>, ShardError>>()?;
87 edges.sort_by(|left, right| {
88 (
89 &left.source_name,
90 &left.target_name,
91 left.kind,
92 left.flow_kind,
93 )
94 .cmp(&(
95 &right.source_name,
96 &right.target_name,
97 right.kind,
98 right.flow_kind,
99 ))
100 });
101 for (edge_index, edge) in edges.iter().enumerate() {
102 validate_edge(edge, 1, edge_index)?;
103 }
104
105 Ok(Self {
106 schema_version: SHARD_SCHEMA_VERSION,
107 path: extraction.path.clone(),
108 language: extraction.lang,
109 symbols,
110 imports: extraction.imports.clone(),
111 references: extraction.references.clone(),
112 diagnostics: extraction.diagnostics.clone(),
113 ast_node_count: extraction.ast_node_count,
114 edges,
115 })
116 }
117
118 pub fn load(self, id_gen: &IdGenerator<SymbolId>) -> Result<LoadedShard, ShardError> {
120 if self.schema_version != SHARD_SCHEMA_VERSION {
121 return Err(ShardError::SchemaVersion {
122 line: 1,
123 found: self.schema_version,
124 expected: SHARD_SCHEMA_VERSION,
125 });
126 }
127
128 let symbols = self
129 .symbols
130 .into_iter()
131 .map(|symbol| symbol.into_symbol(&self.path, self.language, id_gen.next()))
132 .collect();
133 let mut file = FileExtraction::empty(self.path, self.language);
134 file.symbols = symbols;
135 file.imports = self.imports;
136 file.references = self.references;
137 file.diagnostics = self.diagnostics;
138 file.ast_node_count = self.ast_node_count;
139
140 Ok(LoadedShard {
141 file,
142 edges: self.edges,
143 })
144 }
145}
146
147impl From<&Symbol> for ShardSymbol {
148 fn from(symbol: &Symbol) -> Self {
149 Self {
150 name: symbol.name.clone(),
151 kind: symbol.kind,
152 source_range: symbol.source_range.clone(),
153 visibility: symbol.visibility,
154 signature: symbol.signature.clone(),
155 docstring: symbol.docstring.clone(),
156 is_async: symbol.is_async,
157 }
158 }
159}
160
161impl ShardSymbol {
162 fn into_symbol(self, file_path: &Path, language: LangId, id: SymbolId) -> Symbol {
163 Symbol {
164 id,
165 name: self.name,
166 kind: self.kind,
167 language,
168 file_path: file_path.to_path_buf(),
169 source_range: self.source_range,
170 visibility: self.visibility,
171 signature: self.signature,
172 docstring: self.docstring,
173 is_async: self.is_async,
174 }
175 }
176}
177
178pub fn write_shard<W: Write>(mut writer: W, files: &[ShardFile]) -> Result<(), ShardError> {
182 for (line_index, file) in files.iter().enumerate() {
183 validate_file(file, line_index + 1)?;
184 serde_json::to_writer(&mut writer, file).map_err(ShardError::Encode)?;
185 writer.write_all(b"\n")?;
186 }
187 writer.flush()?;
188 Ok(())
189}
190
191pub fn read_shard<R: BufRead>(reader: R) -> Result<Vec<ShardFile>, ShardError> {
192 let mut files = Vec::new();
193 for (line_index, line) in reader.lines().enumerate() {
194 let line_number = line_index + 1;
195 let line = line?;
196 if line.trim().is_empty() {
197 continue;
198 }
199 let file: ShardFile = serde_json::from_str(&line).map_err(|source| ShardError::Decode {
200 line: line_number,
201 source,
202 })?;
203 validate_file(&file, line_number)?;
204 files.push(file);
205 }
206 Ok(files)
207}
208
209pub(crate) fn validate_file(file: &ShardFile, line: usize) -> Result<(), ShardError> {
210 if file.schema_version != SHARD_SCHEMA_VERSION {
211 return Err(ShardError::SchemaVersion {
212 line,
213 found: file.schema_version,
214 expected: SHARD_SCHEMA_VERSION,
215 });
216 }
217 normalized_path(&file.path)?;
218 for diagnostic in &file.diagnostics {
219 normalized_path(&diagnostic.path)?;
220 }
221 for (edge_index, edge) in file.edges.iter().enumerate() {
222 validate_edge(edge, line, edge_index)?;
223 }
224 Ok(())
225}