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}