Skip to main content

devscout_rs/graph/
artifact.rs

1use std::fs;
2use std::io;
3use std::path::Path;
4
5use serde::{Deserialize, Serialize};
6
7use super::def::Def;
8use super::edge::{Edge, EdgesByKind, HeuristicByTier};
9use super::ordered::Percent1;
10use super::paths::{atomic_write_json, graph_json_path};
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13/// Represents `Stats`.
14pub struct Stats {
15    /// The def count value.
16    pub def_count: usize,
17    /// The file count value.
18    pub file_count: usize,
19    /// The edges by kind value.
20    pub edges_by_kind: EdgesByKind,
21    /// The ambiguous count value.
22    pub ambiguous_count: usize,
23    /// The ambiguous pct value.
24    pub ambiguous_pct: Percent1,
25    /// The unresolved external count value.
26    pub unresolved_external_count: usize,
27    /// Appended LAST, and always serialized (unlike the edge flag above): the
28    /// key is written unconditionally, so a graph with no guesses still
29    /// carries `"heuristic_edge_count":0`. `default` is for the READ side only
30    /// -- a graph.json written before this counter existed has no such key,
31    /// and it must read back as 0 rather than fail to parse.
32    #[serde(default)]
33    pub heuristic_edge_count: usize,
34    /// Test-coverage stage -- appended LAST and always serialized, like the
35    /// stage-4 counter above it. Counts merged DEF ROWS, not fragment entries,
36    /// so a partial test class split across two files is one test def.
37    #[serde(default)]
38    pub test_def_count: usize,
39    /// The TS resolver's own four counters, omitted entirely when the repo
40    /// carries no TS fragment (the same omit-when-empty rule every other
41    /// appended fact follows). Appended after `test_def_count`, and NOT last
42    /// any more: `heuristic_by_tier` below is the newer fact and takes the
43    /// tail, so a TS repo's stats block appends in the order the facts were
44    /// added rather than interleaving the newest one before an older key.
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub ts: Option<crate::tsgraph::TsStats>,
47    /// The tier split of `heuristic_edge_count`, appended LAST and always
48    /// serialized, like the two counters above it. `default` is for the READ
49    /// side only: a graph.json written before the tiers existed has no such
50    /// key and must read back as two zeros rather than fail to parse.
51    ///
52    /// A C#-only repo writes no `ts` key at all, so for such a tree this key
53    /// still follows `test_def_count` directly and the bytes are unchanged.
54    #[serde(default)]
55    pub heuristic_by_tier: HeuristicByTier,
56}
57
58/// One row of the full name index. Field order (`name`, `kind`,
59/// `file`, `line`, `owner`) is significant; `owner` carries the
60/// declaring type's def id for a member and is omitted for a type, an enum
61/// member, and every markup or resource key.
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
63pub struct GraphName {
64    /// The name value.
65    pub name: String,
66    /// The kind value.
67    pub kind: String,
68    /// The file value.
69    pub file: String,
70    /// The line value.
71    pub line: usize,
72    #[serde(default, skip_serializing_if = "String::is_empty")]
73    /// The owner value.
74    pub owner: String,
75}
76
77/// One `.csproj` project as graph.json persists it.
78///
79/// Field order (`id`, `name`, `refs`, `test`) is significant, and the last two are
80/// omit-when-empty/omit-when-false: a leaf project that references nothing
81/// and is not a test project serializes as just its `id` and `name`.
82///
83/// Deliberately NOT `project::Unit`: that type also carries `dir`, which is
84/// always `id`'s parent directory and so is recomputed on read rather than
85/// stored (`project::units_from_graph`). Nothing about which FILE belongs to
86/// which unit is persisted either -- `ProjectModel` derives that from the
87/// unit list.
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89pub struct GraphUnit {
90    /// Repo-relative path to the `.csproj` file, which is also this unit's
91    /// identity -- what `refs` entries name.
92    pub id: String,
93    /// The project name (the csproj file name without its extension).
94    pub name: String,
95    /// The `id`s of this project's DIRECT `ProjectReference` targets, not
96    /// transitively closed. Omitted when empty.
97    #[serde(default, skip_serializing_if = "Vec::is_empty")]
98    pub refs: Vec<String>,
99    /// Whether this is a test project. Omitted when false.
100    #[serde(default, skip_serializing_if = "is_not_test")]
101    pub test: bool,
102}
103
104fn is_not_test(b: &bool) -> bool {
105    !*b
106}
107
108/// The version stamped into every graph.json this build writes, and the one
109/// `rebuild_graph` demands before it reuses an artifact it did not just
110/// produce.
111///
112/// Bumped to 2 when `uses-member` edges gained `tier` and `member`, and to 3
113/// when the `implements`/`overrides` edge kinds joined the graph: a
114/// schema-2 graph is READABLE (the two new `edges_by_kind` counters default to
115/// 0 and no edge of either new kind can be present) but it is missing facts
116/// the query layer now reports, so it gets rebuilt rather than trusted.
117pub const GRAPH_SCHEMA_VERSION: u32 = 3;
118
119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
120/// Represents `Graph`.
121pub struct Graph {
122    /// The schema version value.
123    pub schema_version: u32,
124    /// The built at head value.
125    pub built_at_head: Option<String>,
126    /// The defs value.
127    pub defs: Vec<Def>,
128    /// The edges value.
129    pub edges: Vec<Edge>,
130    /// The stats value.
131    pub stats: Stats,
132    /// Appended LAST, after `stats`, and omitted when empty: the
133    /// house rule for every added field, and what keeps a graph built over a
134    /// set that declares no name byte-identical to what it was.
135    #[serde(default, skip_serializing_if = "Vec::is_empty")]
136    pub names: Vec<GraphName>,
137    /// The repo's `.csproj` projects, sorted by `id` -- appended LAST, after
138    /// `names`, and omitted entirely when the repo declares none. A tree with
139    /// no `.csproj` therefore serializes exactly as it did before the project
140    /// model existed.
141    #[serde(default, skip_serializing_if = "Vec::is_empty")]
142    pub units: Vec<GraphUnit>,
143}
144
145/// Reads and deserializes the repository graph, returning `None` on failure.
146pub fn read_graph(root: &Path) -> Option<Graph> {
147    let text = fs::read_to_string(graph_json_path(root)).ok()?;
148    serde_json::from_str(&text).ok()
149}
150
151pub(crate) fn write_graph(root: &Path, graph: &Graph) -> io::Result<()> {
152    atomic_write_json(&graph_json_path(root), graph)
153}